Skip to content

Commit 9a0179e

Browse files
committed
Add DesktopFiscalTransaction log migration and entity model
- Created a new migration to add the DesktopFiscalTransactions table with various fields including ClientTransactionId, TimestampUtc, DocumentType, and others. - Implemented the DesktopFiscalTransactionEntity class with data annotations for validation and indexing. - Ensured unique indexing on ClientTransactionId and composite indexing on Status, DocumentType, and TimestampUtc for optimized queries.
1 parent 3ae9d24 commit 9a0179e

32 files changed

Lines changed: 8126 additions & 99 deletions

SECRETS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ dotnet user-secrets set "SAP:Password" "YOUR_SAP_PASSWORD"
3333
# dotnet user-secrets set "Webhooks:WebhookSecret" "YOUR_SAP_WEBHOOK_SECRET"
3434

3535
# SAP attachment share access (only if the UNC share requires Windows auth)
36-
# dotnet user-secrets set "SAP:AttachmentsPath" "\\10.10.10.6\B1_SHF\Paths\Attachments"
36+
# dotnet user-secrets set "SAP:AttachmentsPath" "\\kfdb\b1_shf\Paths\Attachments\"
37+
# dotnet user-secrets set "SAP:AttachmentsServiceLayerSourcePath" "/path/visible/to/linux/service-layer/Paths/Attachments/"
3738
# dotnet user-secrets set "SAP:AttachmentsUsername" "share-user"
3839
# dotnet user-secrets set "SAP:AttachmentsPassword" "YOUR_SHARE_PASSWORD"
3940
# dotnet user-secrets set "SAP:AttachmentsDomain" "KEFALOS"

ShopInventory.Web/Common/Errors/Errors.Report.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ public static class Report
88
{
99
public static Error LoadMerchandiserPurchaseOrdersFailed(string message) =>
1010
Error.Failure("Report.LoadMerchandiserPurchaseOrdersFailed", message);
11+
12+
public static Error LoadFiscalTransactionsFailed(string message) =>
13+
Error.Failure("Report.LoadFiscalTransactionsFailed", message);
1114
}
1215
}

ShopInventory.Web/Components/Layout/NavMenu.razor

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,16 @@
556556
</NavLink>
557557
</Authorized>
558558
</AuthorizeView>
559+
<AuthorizeView Roles="Admin,Manager,Cashier" Context="fiscalTransactionCtx">
560+
<Authorized>
561+
<NavLink class="snav-link sub-link" href="/reports/fiscal-transactions" Match="NavLinkMatch.Prefix">
562+
<span class="snav-icon">
563+
<MudIcon Icon="@Icons.Material.Filled.FactCheck" />
564+
</span>
565+
<span class="snav-text">Fiscal Txn Log</span>
566+
</NavLink>
567+
</Authorized>
568+
</AuthorizeView>
559569

560570
<AuthorizeView Roles="Admin,Manager,Merchandiser" Context="timesheetsAdminCtx">
561571
<Authorized>

ShopInventory.Web/Components/Pages/FiscalTransactionLog.razor

Lines changed: 694 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
using MediatR;
2+
using Microsoft.AspNetCore.Components;
3+
using Microsoft.AspNetCore.Components.Authorization;
4+
using ShopInventory.Web.Data;
5+
using ShopInventory.Web.Features.Reports.Queries.GetFiscalTransactionLog;
6+
using ShopInventory.Web.Services;
7+
8+
namespace ShopInventory.Web.Components.Pages;
9+
10+
public partial class FiscalTransactionLog : IDisposable
11+
{
12+
private const int TransactionsPerPage = 25;
13+
14+
[Inject] private IMediator Mediator { get; set; } = default!;
15+
[Inject] private IAuditService AuditService { get; set; } = default!;
16+
[Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = default!;
17+
[Inject] private ILogger<FiscalTransactionLog> Logger { get; set; } = default!;
18+
19+
private GetFiscalTransactionLogResult reportResult = new();
20+
private FiscalTransactionLogItemModel? selectedTransaction;
21+
private bool isDetailsModalOpen;
22+
private bool isLoading = true;
23+
private bool hasInitialized;
24+
private bool hasLoggedView;
25+
private bool awaitingAuthentication;
26+
private string? errorMessage;
27+
private string searchTerm = string.Empty;
28+
private string selectedStatus = "All";
29+
private string selectedDocumentType = "All";
30+
private DateTime? fromDate = DateTime.Today.AddDays(-30);
31+
private DateTime? toDate = DateTime.Today;
32+
private int currentPage = 1;
33+
34+
private string PageSummary => reportResult.TotalCount == 0
35+
? "No records returned"
36+
: $"{reportResult.TotalCount} total records across {Math.Max(1, currentPage)} page(s)";
37+
38+
private string LatestActivityText => reportResult.Summary.LatestTransactionAtUtc.HasValue
39+
? $"Latest activity {FormatTimestamp(reportResult.Summary.LatestTransactionAtUtc.Value)}"
40+
: "No activity yet";
41+
42+
protected override void OnInitialized()
43+
{
44+
AuthStateProvider.AuthenticationStateChanged += HandleAuthenticationStateChanged;
45+
}
46+
47+
protected override async Task OnAfterRenderAsync(bool firstRender)
48+
{
49+
if (!firstRender || hasInitialized)
50+
{
51+
return;
52+
}
53+
54+
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
55+
if (!(authState.User.Identity?.IsAuthenticated ?? false))
56+
{
57+
awaitingAuthentication = true;
58+
isLoading = false;
59+
return;
60+
}
61+
62+
hasInitialized = true;
63+
await LoadReportAsync();
64+
StateHasChanged();
65+
}
66+
67+
private async Task LoadReportAsync()
68+
{
69+
isLoading = true;
70+
errorMessage = null;
71+
72+
try
73+
{
74+
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
75+
if (!(authState.User.Identity?.IsAuthenticated ?? false))
76+
{
77+
awaitingAuthentication = true;
78+
reportResult = new GetFiscalTransactionLogResult();
79+
selectedTransaction = null;
80+
isDetailsModalOpen = false;
81+
return;
82+
}
83+
84+
awaitingAuthentication = false;
85+
var selectedTransactionId = selectedTransaction?.Id;
86+
var result = await Mediator.Send(new GetFiscalTransactionLogQuery(
87+
fromDate,
88+
toDate,
89+
string.IsNullOrWhiteSpace(searchTerm) ? null : searchTerm.Trim(),
90+
NormalizeFilter(selectedStatus),
91+
NormalizeFilter(selectedDocumentType),
92+
currentPage,
93+
TransactionsPerPage));
94+
95+
result.SwitchFirst(
96+
value =>
97+
{
98+
reportResult = value;
99+
selectedTransaction = value.Transactions.FirstOrDefault(transaction => transaction.Id == selectedTransactionId);
100+
isDetailsModalOpen = isDetailsModalOpen && selectedTransaction is not null;
101+
},
102+
error =>
103+
{
104+
reportResult = new GetFiscalTransactionLogResult();
105+
selectedTransaction = null;
106+
isDetailsModalOpen = false;
107+
errorMessage = error.Description;
108+
});
109+
110+
if (!result.IsError && !hasLoggedView)
111+
{
112+
hasLoggedView = true;
113+
await AuditService.LogAsync(AuditActions.ViewReports, "Report", "FiscalTransactionLog");
114+
}
115+
}
116+
catch (Exception ex)
117+
{
118+
Logger.LogError(ex, "Failed to load fiscal transaction log page");
119+
reportResult = new GetFiscalTransactionLogResult();
120+
selectedTransaction = null;
121+
isDetailsModalOpen = false;
122+
errorMessage = "Failed to load fiscal transaction log.";
123+
}
124+
finally
125+
{
126+
isLoading = false;
127+
}
128+
}
129+
130+
private async void HandleAuthenticationStateChanged(Task<AuthenticationState> authenticationStateTask)
131+
{
132+
try
133+
{
134+
var authState = await authenticationStateTask;
135+
if (!(authState.User.Identity?.IsAuthenticated ?? false))
136+
{
137+
return;
138+
}
139+
140+
await InvokeAsync(async () =>
141+
{
142+
if (!hasInitialized)
143+
{
144+
hasInitialized = true;
145+
}
146+
147+
if (awaitingAuthentication || (!hasLoggedView && string.IsNullOrWhiteSpace(errorMessage)))
148+
{
149+
await LoadReportAsync();
150+
StateHasChanged();
151+
}
152+
});
153+
}
154+
catch (Exception ex)
155+
{
156+
Logger.LogDebug(ex, "Authentication state change handler failed for fiscal transaction log page");
157+
}
158+
}
159+
160+
private async Task ApplyFiltersAsync()
161+
{
162+
currentPage = 1;
163+
await LoadReportAsync();
164+
}
165+
166+
private async Task ClearFiltersAsync()
167+
{
168+
searchTerm = string.Empty;
169+
selectedStatus = "All";
170+
selectedDocumentType = "All";
171+
fromDate = DateTime.Today.AddDays(-30);
172+
toDate = DateTime.Today;
173+
currentPage = 1;
174+
await LoadReportAsync();
175+
}
176+
177+
private async Task ReloadAsync() => await LoadReportAsync();
178+
179+
private async Task PreviousPageAsync()
180+
{
181+
if (currentPage <= 1)
182+
{
183+
return;
184+
}
185+
186+
currentPage--;
187+
await LoadReportAsync();
188+
}
189+
190+
private async Task NextPageAsync()
191+
{
192+
if (!reportResult.HasMore)
193+
{
194+
return;
195+
}
196+
197+
currentPage++;
198+
await LoadReportAsync();
199+
}
200+
201+
private void OpenTransactionDetails(FiscalTransactionLogItemModel transaction)
202+
{
203+
selectedTransaction = transaction;
204+
isDetailsModalOpen = true;
205+
}
206+
207+
private void CloseTransactionDetails()
208+
=> isDetailsModalOpen = false;
209+
210+
private string GetRowClass(FiscalTransactionLogItemModel transaction)
211+
=> selectedTransaction?.Id == transaction.Id ? "ftr-row--selected" : string.Empty;
212+
213+
private string GetStatusBadgeClass(string? status)
214+
=> (status ?? string.Empty).Trim().ToLowerInvariant() switch
215+
{
216+
"success" => "ftr-status-badge ftr-status-badge--success",
217+
"fiscalised" => "ftr-status-badge ftr-status-badge--info",
218+
"not fiscalised" => "ftr-status-badge ftr-status-badge--warning",
219+
"failed" => "ftr-status-badge ftr-status-badge--danger",
220+
_ => "ftr-status-badge ftr-status-badge--muted"
221+
};
222+
223+
private static string? NormalizeFilter(string? value)
224+
=> string.IsNullOrWhiteSpace(value) || string.Equals(value, "All", StringComparison.OrdinalIgnoreCase)
225+
? null
226+
: value.Trim();
227+
228+
private static string DisplayCustomer(FiscalTransactionLogItemModel transaction)
229+
=> string.IsNullOrWhiteSpace(transaction.CardName) ? "Walk-in / not captured" : transaction.CardName;
230+
231+
private static string DisplayCardCode(FiscalTransactionLogItemModel transaction)
232+
=> string.IsNullOrWhiteSpace(transaction.CardCode) ? "No card code" : transaction.CardCode;
233+
234+
private static string DisplayVerification(FiscalTransactionLogItemModel transaction)
235+
=> string.IsNullOrWhiteSpace(transaction.VerificationCode) ? "Not captured" : transaction.VerificationCode;
236+
237+
private static string DisplayReceiptGlobalNo(FiscalTransactionLogItemModel transaction)
238+
=> transaction.ReceiptGlobalNo.HasValue && transaction.ReceiptGlobalNo.Value > 0
239+
? transaction.ReceiptGlobalNo.Value.ToString()
240+
: "Not captured";
241+
242+
private static string DisplayOperator(FiscalTransactionLogItemModel transaction)
243+
=> string.IsNullOrWhiteSpace(transaction.CreatedByUsername)
244+
? (string.IsNullOrWhiteSpace(transaction.CreatedByUserId) ? "Unknown operator" : transaction.CreatedByUserId)
245+
: transaction.CreatedByUsername;
246+
247+
private static string DisplayOriginalInvoice(FiscalTransactionLogItemModel transaction)
248+
=> string.IsNullOrWhiteSpace(transaction.OriginalInvoiceNumber) ? "Not applicable" : transaction.OriginalInvoiceNumber;
249+
250+
private static string DisplayDeviceSerial(FiscalTransactionLogItemModel transaction)
251+
=> string.IsNullOrWhiteSpace(transaction.DeviceSerialNumber) ? "Not captured" : transaction.DeviceSerialNumber;
252+
253+
private static string DisplayDeviceId(FiscalTransactionLogItemModel transaction)
254+
=> string.IsNullOrWhiteSpace(transaction.DeviceId) ? "Not captured" : transaction.DeviceId;
255+
256+
private static string DisplayFiscalDay(FiscalTransactionLogItemModel transaction)
257+
=> string.IsNullOrWhiteSpace(transaction.FiscalDay) ? "Not captured" : transaction.FiscalDay;
258+
259+
private static string DisplayMessage(FiscalTransactionLogItemModel transaction)
260+
=> string.IsNullOrWhiteSpace(transaction.Message) ? "No operator message captured." : transaction.Message;
261+
262+
private static string FormatAmount(decimal amount, string? currency)
263+
=> string.IsNullOrWhiteSpace(currency) ? amount.ToString("N2") : $"{currency} {amount:N2}";
264+
265+
private static string FormatTimestamp(DateTime timestampUtc)
266+
=> IAuditService.ToCAT(timestampUtc).ToString("yyyy-MM-dd HH:mm");
267+
268+
public void Dispose()
269+
{
270+
AuthStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged;
271+
}
272+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace ShopInventory.Web.Features.Reports.Queries.GetFiscalTransactionLog;
2+
3+
public sealed class FiscalTransactionLogItemModel
4+
{
5+
public int Id { get; init; }
6+
public string ClientTransactionId { get; init; } = string.Empty;
7+
public DateTime TimestampUtc { get; init; }
8+
public string DocumentType { get; init; } = string.Empty;
9+
public int DocNum { get; init; }
10+
public string Status { get; init; } = string.Empty;
11+
public string? Message { get; init; }
12+
public string? VerificationCode { get; init; }
13+
public string? QRCode { get; init; }
14+
public string? DeviceSerialNumber { get; init; }
15+
public string? DeviceId { get; init; }
16+
public string? FiscalDay { get; init; }
17+
public int? ReceiptGlobalNo { get; init; }
18+
public string? CardCode { get; init; }
19+
public string? CardName { get; init; }
20+
public decimal DocTotal { get; init; }
21+
public decimal VatSum { get; init; }
22+
public string? Currency { get; init; }
23+
public string? OriginalInvoiceNumber { get; init; }
24+
public string SourceSystem { get; init; } = string.Empty;
25+
public string? CreatedByUserId { get; init; }
26+
public string? CreatedByUsername { get; init; }
27+
public DateTime CreatedAtUtc { get; init; }
28+
public DateTime LastSyncedAtUtc { get; init; }
29+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace ShopInventory.Web.Features.Reports.Queries.GetFiscalTransactionLog;
2+
3+
public sealed class FiscalTransactionLogSummaryModel
4+
{
5+
public int TotalTransactions { get; init; }
6+
public int SuccessCount { get; init; }
7+
public int FiscalisedCount { get; init; }
8+
public int NotFiscalisedCount { get; init; }
9+
public int FailedCount { get; init; }
10+
public int UniqueOperators { get; init; }
11+
public DateTime? LatestTransactionAtUtc { get; init; }
12+
}

0 commit comments

Comments
 (0)