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);