diff --git a/.claude/hooks/db-dev-guard.ps1 b/.claude/hooks/db-dev-guard.ps1
new file mode 100644
index 00000000..4f212be8
--- /dev/null
+++ b/.claude/hooks/db-dev-guard.ps1
@@ -0,0 +1,25 @@
+# Claude Code PreToolUse hook: DB-vedelem — csak _DEV nevu adatbazis erheto el shell-parancsbol.
+# Bemenet: hook JSON a stdin-en; ha a parancs connection stringet tartalmaz (Initial Catalog= / Database=)
+# es a DB-nev NEM tartalmazza a "_DEV"-et, a tool-hivast DENY-jal blokkolja.
+
+$payloadText = [Console]::In.ReadToEnd()
+if ([string]::IsNullOrWhiteSpace($payloadText)) { exit 0 }
+
+try { $payload = $payloadText | ConvertFrom-Json } catch { exit 0 }
+
+$command = $payload.tool_input.command
+if ([string]::IsNullOrWhiteSpace($command)) { exit 0 }
+
+$pattern = [regex]'(?i)(?:Initial\s+Catalog|Database)\s*=\s*([^;"''\s]+)'
+
+foreach ($match in $pattern.Matches($command)) {
+ $dbName = $match.Groups[1].Value
+ if ($dbName -notmatch '(?i)_DEV') {
+ $reason = "Blokkolt: csak _DEV adatbazis modosithato! (talalt adatbazis: $dbName)"
+ $result = @{ hookSpecificOutput = @{ hookEventName = 'PreToolUse'; permissionDecision = 'deny'; permissionDecisionReason = $reason } }
+ Write-Output ($result | ConvertTo-Json -Depth 5 -Compress)
+ exit 0
+ }
+}
+
+exit 0
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 00000000..96d98deb
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,17 @@
+{
+ "hooks": {
+ "PreToolUse": [
+ {
+ "matcher": "Bash|PowerShell",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"H:/Applications/Mango/Source/FruitBankHybridApp/.claude/hooks/db-dev-guard.ps1\"",
+ "timeout": 30,
+ "statusMessage": "DB guard (_DEV) ellenőrzés..."
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index d11af6ab..983a40cc 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -57,7 +57,11 @@
"Bash(rm -f \"H:/Applications/Aycode/Source/AyCode.Core/AyCode.Entities/IPostalParty.cs\" *)",
"PowerShell(winget install Microsoft.DotNet.Runtime.9 --architecture x64 --accept-source-agreements --accept-package-agreements)",
"PowerShell($url = \"https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.17/dotnet-runtime-9.0.17-win-x64.exe\"; $dst = \"$env:TEMP\\\\dotnet-runtime-9.0.17-win-x64.exe\"; Invoke-WebRequest -Uri $url -OutFile $dst; Get-Item $dst | Select-Object Name, Length)",
- "PowerShell($p = Start-Process \"$env:TEMP\\\\dotnet-runtime-9.0.17-win-x64.exe\" -ArgumentList \"/install\",\"/quiet\",\"/norestart\" -Wait -PassThru; \"ExitCode: $\\($p.ExitCode\\)\"; dotnet --list-runtimes | Select-String \"9\\\\.0\")"
+ "PowerShell($p = Start-Process \"$env:TEMP\\\\dotnet-runtime-9.0.17-win-x64.exe\" -ArgumentList \"/install\",\"/quiet\",\"/norestart\" -Wait -PassThru; \"ExitCode: $\\($p.ExitCode\\)\"; dotnet --list-runtimes | Select-String \"9\\\\.0\")",
+ "Skill(update-config)",
+ "Skill(update-config:*)",
+ "PowerShell($script = \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBankHybridApp\\\\.claude\\\\hooks\\\\db-dev-guard.ps1\"; \"--- A\\) PROD \\(deny vart\\):\"; '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"sqlcmd -S 1.2.3.4 -Q \\\\\"x\\\\\" # Initial Catalog=FruitBank;User ID=sa\"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File $script; \"--- B\\) DEV \\(semmi vart\\):\"; '{\"tool_name\":\"PowerShell\",\"tool_input\":{\"command\":\"$cs = \\\\\"Data Source=x;Initial Catalog=FruitBank_DEV;\\\\\"\"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File $script; \"--- C\\) nincs conn \\(semmi vart\\):\"; '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"ls -la\"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File $script; \"--- D\\) Database= prod \\(deny vart\\):\"; '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"x Database=MangoManagement;y\"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File $script; \"--- KESZ\")",
+ "Bash(cd \"H:/Applications/Mango/Source/FruitBankHybridApp\" && grep -rhi \"DevExpress.Blazor\" --include=\"*.csproj\" . 2>/dev/null | grep -i version | head -5; echo \"---packages.lock/obj---\"; find . -path \"*DevExpress.Blazor*\" -name \"*.nuspec\" 2>/dev/null | head; echo \"---globalpackages---\"; ls ~/.nuget/packages/ 2>/dev/null | grep -i devexpress.blazor | head)"
]
}
}
diff --git a/FruitBank.Common/Entities/EkaerHistory.cs b/FruitBank.Common/Entities/EkaerHistory.cs
index 3b319d5f..218952d3 100644
--- a/FruitBank.Common/Entities/EkaerHistory.cs
+++ b/FruitBank.Common/Entities/EkaerHistory.cs
@@ -1,9 +1,11 @@
-using AyCode.Core.Serializers.Attributes;
+using System.Text.Json.Serialization;
+using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using AyCode.Interfaces.TimeStampInfo;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
+
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
@@ -19,8 +21,18 @@ public sealed class EkaerHistory: MgEntityBase, ITimeStampInfo
[ToonDescription(Purpose = "Direction of the goods movement: false = incoming shipment (Shipping), true = outgoing delivery (Order).")]
public bool IsOutgoing { get; set; }
- [ToonDescription(Purpose = "Lifecycle state of the declaration: Pending (auto-created, not yet generated), Generated (tradeCard XML produced and valid), ValidationError (generation produced errors, see ErrorText), Sent (accepted by NAV, EkaerNumber filled), SendError (NAV call failed, see ErrorText).")]
- public EkaerStatus Status { get; set; }
+ [ToonDescription(Purpose = "Lifecycle state of the declaration, stored as int (see EkaerStatus): 0 Pending (auto-created, not yet generated), 1 Generated (tradeCard XML produced and valid), 2 ValidationError (generation produced errors, see ErrorText), 3 Sent (accepted by NAV, EkaerNumber filled), 4 SendError (NAV call failed, see ErrorText).")]
+ public int StatusId { get; set; }
+
+ /// A enum-nézete. A tárolt érték az int oszlop (StatusId) — a linq2db réteg
+ /// az enum-property-t nem perzisztálta (az oszlop kimaradt az insertből, a DB default írt 0-t), ezért a nop-minta:
+ /// int oszlop + nem-mappelt enum wrapper.
+ [NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, Newtonsoft.Json.JsonIgnore, JsonIgnore]
+ public EkaerStatus Status
+ {
+ get => (EkaerStatus)StatusId;
+ set => StatusId = (int)value;
+ }
[ToonDescription(Purpose = "The generated NAV EKÁER tradeCard request XML exactly as it was (or will be) submitted — audit copy and source of the read-only detail view. Null until the first Generate.")]
public string? XmlDoc { get; set; }
diff --git a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs
index 9e31b37c..74463d40 100644
--- a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs
+++ b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs
@@ -36,6 +36,7 @@ public interface IFruitBankDataControllerCommon
public Task UpdateEkaerHistory(EkaerHistory ekaerHistory);
public Task GenerateEkaerXmlDocument(int shippingDocumentId);
public Task CreateEkaerHistory(int foreignKey, bool isOutgoing);
+ public Task CreateMissingEkaerHistories(DateTime fromDate);
#endregion EkaerHistory
#region CargoPartner
diff --git a/FruitBank.Common/SignalRs/SignalRTags.cs b/FruitBank.Common/SignalRs/SignalRTags.cs
index 77e72728..3232da03 100644
--- a/FruitBank.Common/SignalRs/SignalRTags.cs
+++ b/FruitBank.Common/SignalRs/SignalRTags.cs
@@ -125,6 +125,7 @@ public class SignalRTags : AcSignalRTags
public const int UpdateEkaerHistory = 189;
public const int GenerateEkaerXmlDocument = 190;
public const int CreateEkaerHistory = 191;
+ public const int CreateMissingEkaerHistories = 192;
public const int AuthenticateUser = 195;
public const int RefreshToken = 200;
diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetail.razor b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetail.razor
index c01fffe2..f6a2e487 100644
--- a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetail.razor
+++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetail.razor
@@ -3,6 +3,7 @@
@using AyCode.Core.Interfaces
@using AyCode.Core.Loggers
@using AyCode.Services.Nav.Ekaer.Models
+@using FruitBank.Common.Entities
@using FruitBankHybrid.Shared.Services.Loggers
@using FruitBankHybrid.Shared.Services.SignalRs
@@ -27,6 +28,12 @@
}
+@if (!string.IsNullOrWhiteSpace(ErrorText))
+{
+ @* A generálás/küldés hibái egyben olvashatóan — soronként (a validátor NewLine-nal fűzi). *@
+ @ErrorText
+}
+
ParentDataItem == null;
string GridCss => !IsMasterGrid ? "hide-toolbar" : string.Empty;
+ private string? ErrorText => (ParentDataItem as EkaerHistory)?.ErrorText;
+
private AcObservableCollection TradeCardItems = [];
private LoggerClient _logger;
diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor
index be85e3d2..f47d636b 100644
--- a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor
+++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor
@@ -12,6 +12,7 @@
@inject IEnumerable LogWriters
@inject FruitBankSignalRClient FruitBankSignalRClient
+@inject IJSRuntime JS
@@ -26,11 +27,44 @@
OnGridFocusedRowChanged="Grid_FocusedRowChanged">
-
-
+
+
+ @* A kézi NAV-beadás fázisában a Status / EKÁER szám / SentDate kézzel szerkeszthető. *@
+
+
+
+
+
+ @if (!string.IsNullOrWhiteSpace(context.DisplayText))
+ {
+
+ ⚠️@context.DisplayText
+
+ }
+
+
-
-
+
+
+
+
+ @{
+ var row = (EkaerHistory)context.DataItem;
+
+
+ }
+
+
@@ -56,8 +90,29 @@
@if (IsMasterGrid)
{
- @* EKÁER-rekordot a rendszer hoz létre (auto-rekord) — kézi new/edit/delete tiltva. *@
-
+ @* EKÁER-rekord a toolbar "sorok létrehozása" gombbal készül (new/delete tiltva — nincs automata
+ érzékelés, a létrehozás explicit és idempotens). Az Edit a kézi NAV-beadás fázisában
+ engedélyezett: Status / EKÁER szám / SentDate kézi rögzítéséhez. *@
+
+
+ @* A dátumválasztó editor csak Template-ben mehet toolbarba; a gomb natív DxToolbarItem,
+ hogy a toolbar stílusát/igazítását kapja (lásd: "Ai process..." minta a GridShippingDocument-ben). *@
+
+
+
+
+
+
+
+
+
}
@@ -110,4 +165,87 @@
FocusedRowVisibleIndex = args.VisibleIndex;
EditItemsEnabled = true;
}
+
+ private readonly HashSet _generatingIds = [];
+
+ // Elküldött bejelentést nem generálunk újra némán (az már a NAV-nál van — módosítás külön művelet lesz);
+ // kimenő (Order) generálás még nincs implementálva.
+ private bool CanGenerate(EkaerHistory ekaerHistory)
+ => ekaerHistory.Status != EkaerStatus.Sent && !ekaerHistory.IsOutgoing && !_generatingIds.Contains(ekaerHistory.Id);
+
+ // Hibás (validációs hibás) bejelentés XML-jét nem adjuk a user kezébe — ne kerülhessen kézzel a NAV-hoz.
+ private static bool CanCopy(EkaerHistory ekaerHistory)
+ => !string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc) && string.IsNullOrWhiteSpace(ekaerHistory.ErrorText);
+
+ private async Task OnGenerateClick(EkaerHistory ekaerHistory)
+ {
+ if (!_generatingIds.Add(ekaerHistory.Id)) return;
+
+ try
+ {
+ var updated = await FruitBankSignalRClient.GenerateEkaerXmlDocument(ekaerHistory.ForeignKey);
+
+ if (updated == null)
+ {
+ _logger.Error($"GenerateEkaerXmlDocument null választ adott; ForeignKey: {ekaerHistory.ForeignKey}");
+ return;
+ }
+
+ // A sor frissítése helyben — a grid ugyanazt a példányt mutatja.
+ ekaerHistory.Status = updated.Status;
+ ekaerHistory.XmlDoc = updated.XmlDoc;
+ ekaerHistory.ErrorText = updated.ErrorText;
+ ekaerHistory.EkaerNumber = updated.EkaerNumber;
+ ekaerHistory.SentDate = updated.SentDate;
+ ekaerHistory.Modified = updated.Modified;
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"GenerateEkaerXmlDocument hiba; ForeignKey: {ekaerHistory.ForeignKey}", ex);
+ }
+ finally
+ {
+ _generatingIds.Remove(ekaerHistory.Id);
+ Grid?.Reload();
+ }
+ }
+
+ DateTime CreateFromDate { get; set; } = DateTime.Today.AddDays(-3);
+ private bool _creatingMissing;
+
+ private async Task OnCreateMissingClick()
+ {
+ if (_creatingMissing) return;
+ _creatingMissing = true;
+
+ try
+ {
+ var createdCount = await FruitBankSignalRClient.CreateMissingEkaerHistories(CreateFromDate);
+ _logger.Info($"CreateMissingEkaerHistories; created: {createdCount}; fromDate: {CreateFromDate:yyyy.MM.dd}");
+
+ if (createdCount > 0) await ReloadDataFromDb(true);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"CreateMissingEkaerHistories hiba; fromDate: {CreateFromDate:yyyy.MM.dd}", ex);
+ }
+ finally
+ {
+ _creatingMissing = false;
+ }
+ }
+
+ private async Task OnCopyClick(EkaerHistory ekaerHistory)
+ {
+ if (!CanCopy(ekaerHistory)) return;
+
+ try
+ {
+ await JS.InvokeVoidAsync("navigator.clipboard.writeText", ekaerHistory.XmlDoc);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"XmlDoc vágólapra másolása sikertelen; EkaerHistory.Id: {ekaerHistory.Id}", ex);
+ }
+ }
}
diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs
index 8dbe9808..7bad3074 100644
--- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs
+++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs
@@ -81,6 +81,7 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
public Task UpdateEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.UpdateEkaerHistory, ekaerHistory);
public Task GenerateEkaerXmlDocument(int shippingDocumentId) => GetByIdAsync(SignalRTags.GenerateEkaerXmlDocument, shippingDocumentId);
public Task CreateEkaerHistory(int foreignKey, bool isOutgoing) => GetByIdAsync(SignalRTags.CreateEkaerHistory, [foreignKey, isOutgoing]);
+ public Task CreateMissingEkaerHistories(DateTime fromDate) => GetByIdAsync(SignalRTags.CreateMissingEkaerHistories, fromDate);
#endregion EkaerHistory
#region CargoPartner
diff --git a/FruitBankHybrid.Shared/wwwroot/app.css b/FruitBankHybrid.Shared/wwwroot/app.css
index 52abce8f..020e5a8d 100644
--- a/FruitBankHybrid.Shared/wwwroot/app.css
+++ b/FruitBankHybrid.Shared/wwwroot/app.css
@@ -67,6 +67,20 @@ h1:focus {
--icon-ekaer-mask-image: url("images/ekaer-fluent.svg");
}
+/* EKÁER toolbar dátumválasztó (GridEkaerHistory) — kompakt szélesség, hogy illeszkedjen a toolbar-elemek közé. */
+.ekaer-create-from-date {
+ width: 9rem;
+}
+
+/* A Template-es toolbar-elem konténere alapból teljes magasságra nyúlik, és az editor felülre csúszik —
+ középre igazítjuk, hogy egy vonalban legyen a többi (natív) toolbar-gombbal. */
+.ekaer-create-from-date-item .dxbl-toolbar-item-content,
+.ekaer-create-from-date-item .dxbl-toolbar-item-template {
+ display: flex;
+ align-items: center;
+ height: 100%;
+}
+
.icon {
width: var(--icon-width);
height: var(--icon-height);