1+ using MediatR ;
2+ using Microsoft . AspNetCore . Components ;
3+ using Microsoft . JSInterop ;
4+ using ShopInventory . Web . Data ;
5+ using ShopInventory . Web . Features . Reports . Queries . GetAccountSalesPaymentReport ;
6+ using ShopInventory . Web . Services ;
7+
8+ namespace ShopInventory . Web . Components . Pages ;
9+
10+ public partial class AccountSalesPaymentReport : IDisposable
11+ {
12+ private const string DefaultAccountCodesText = "CIS006, COR006, COR007, MAC006, MAC009, COR008, COR011, VAN008-019" ;
13+
14+ [ Inject ] private IMediator Mediator { get ; set ; } = default ! ;
15+ [ Inject ] private IAuditService AuditService { get ; set ; } = default ! ;
16+ [ Inject ] private IJSRuntime JS { get ; set ; } = default ! ;
17+ [ Inject ] private IReportExportService ExportService { get ; set ; } = default ! ;
18+ [ Inject ] private ILogger < AccountSalesPaymentReport > Logger { get ; set ; } = default ! ;
19+
20+ private GetAccountSalesPaymentReportResult reportResult = new ( ) ;
21+ private CancellationTokenSource loadCts = new ( ) ;
22+ private bool isDisposed ;
23+ private bool hasLoggedView ;
24+ private bool hasRequestedReport ;
25+ private bool isExporting ;
26+ private bool isLoading ;
27+ private string ? errorMessage ;
28+ private DateTime ? fromDate = DateTime . UtcNow . Date . AddDays ( - 30 ) ;
29+ private DateTime ? toDate = DateTime . UtcNow . Date ;
30+ private AccountSalesPaymentGrouping selectedGrouping = AccountSalesPaymentGrouping . Daily ;
31+ private string accountCodesText = DefaultAccountCodesText ;
32+
33+ private List < AccountSalesPaymentPeriodResult > VisiblePeriods => reportResult . Periods
34+ . Where ( period => period . InvoiceCount > 0 || period . PaymentCount > 0 || period . TotalQuantitySold > 0 )
35+ . OrderByDescending ( period => period . PeriodStartUtc )
36+ . ToList ( ) ;
37+
38+ private IEnumerable < string > DisplayedRequestedAccounts => reportResult . RequestedAccountCodes . Any ( )
39+ ? reportResult . RequestedAccountCodes
40+ : SplitAccountCodes ( accountCodesText ) ;
41+
42+ private string SelectedGroupingLabel => selectedGrouping switch
43+ {
44+ AccountSalesPaymentGrouping . Daily => "Daily" ,
45+ AccountSalesPaymentGrouping . Weekly => "Weekly" ,
46+ AccountSalesPaymentGrouping . Monthly => "Monthly" ,
47+ _ => "Daily"
48+ } ;
49+
50+ private string GroupingPeriodNoun => selectedGrouping switch
51+ {
52+ AccountSalesPaymentGrouping . Daily => "days" ,
53+ AccountSalesPaymentGrouping . Weekly => "weeks" ,
54+ AccountSalesPaymentGrouping . Monthly => "months" ,
55+ _ => "days"
56+ } ;
57+
58+ private string GroupingPeriodSingular => selectedGrouping switch
59+ {
60+ AccountSalesPaymentGrouping . Daily => "day" ,
61+ AccountSalesPaymentGrouping . Weekly => "week" ,
62+ AccountSalesPaymentGrouping . Monthly => "month" ,
63+ _ => "day"
64+ } ;
65+
66+ private string PrimaryActionLabel => hasRequestedReport ? "Refresh report" : "Load report" ;
67+
68+ private bool CanExportReport => hasRequestedReport && reportResult . GeneratedAtUtc != default ;
69+
70+ private async Task LoadReportAsync ( )
71+ {
72+ var cancellationToken = BeginLoad ( ) ;
73+ hasRequestedReport = true ;
74+ isLoading = true ;
75+ errorMessage = null ;
76+
77+ try
78+ {
79+ var result = await Mediator . Send (
80+ new GetAccountSalesPaymentReportQuery (
81+ fromDate ,
82+ toDate ,
83+ selectedGrouping ,
84+ accountCodesText ) ,
85+ cancellationToken ) ;
86+
87+ if ( cancellationToken . IsCancellationRequested || isDisposed )
88+ {
89+ return ;
90+ }
91+
92+ result . SwitchFirst (
93+ value => reportResult = value ,
94+ error =>
95+ {
96+ reportResult = new GetAccountSalesPaymentReportResult ( ) ;
97+ errorMessage = error . Description ;
98+ } ) ;
99+
100+ if ( ! result . IsError && ! hasLoggedView )
101+ {
102+ hasLoggedView = true ;
103+ await AuditService . LogAsync ( AuditActions . ViewReports , "Report" , nameof ( AccountSalesPaymentReport ) ) ;
104+ }
105+ }
106+ catch ( OperationCanceledException ) when ( cancellationToken . IsCancellationRequested )
107+ {
108+ }
109+ catch ( Exception ex )
110+ {
111+ Logger . LogError ( ex , "Failed to load account sales and incoming payment report page" ) ;
112+ reportResult = new GetAccountSalesPaymentReportResult ( ) ;
113+ errorMessage = "Failed to load the account sales and incoming payment report." ;
114+ }
115+ finally
116+ {
117+ if ( ! isDisposed )
118+ {
119+ isLoading = false ;
120+ await InvokeAsync ( StateHasChanged ) ;
121+ }
122+ }
123+ }
124+
125+ private async Task ApplyFiltersAsync ( ) => await LoadReportAsync ( ) ;
126+
127+ private async Task ResetFiltersAsync ( )
128+ {
129+ fromDate = DateTime . UtcNow . Date . AddDays ( - 30 ) ;
130+ toDate = DateTime . UtcNow . Date ;
131+ selectedGrouping = AccountSalesPaymentGrouping . Daily ;
132+ accountCodesText = DefaultAccountCodesText ;
133+ await LoadReportAsync ( ) ;
134+ }
135+
136+ private async Task SetGroupingAsync ( AccountSalesPaymentGrouping grouping )
137+ {
138+ if ( selectedGrouping == grouping )
139+ {
140+ return ;
141+ }
142+
143+ selectedGrouping = grouping ;
144+ await LoadReportAsync ( ) ;
145+ }
146+
147+ private async Task ExportToExcelAsync ( )
148+ {
149+ if ( ! CanExportReport )
150+ {
151+ return ;
152+ }
153+
154+ isExporting = true ;
155+ errorMessage = null ;
156+
157+ try
158+ {
159+ var bytes = ExportService . ExportAccountSalesPaymentReportToExcel ( reportResult ) ;
160+ var base64 = Convert . ToBase64String ( bytes ) ;
161+ await JS . InvokeVoidAsync (
162+ "downloadFile" ,
163+ $ "Account_Sales_And_Payments_{ IAuditService . ToCAT ( DateTime . UtcNow ) : yyyyMMdd_HHmm} .xlsx",
164+ base64 ) ;
165+ }
166+ catch ( Exception ex )
167+ {
168+ Logger . LogError ( ex , "Failed to export account sales and incoming payment report to Excel" ) ;
169+ errorMessage = "Failed to export the account sales and incoming payment report to Excel." ;
170+ }
171+ finally
172+ {
173+ isExporting = false ;
174+ }
175+ }
176+
177+ private string GetGroupingButtonClass ( AccountSalesPaymentGrouping grouping ) =>
178+ selectedGrouping == grouping
179+ ? "aspr-grouping-button aspr-grouping-button--active"
180+ : "aspr-grouping-button" ;
181+
182+ private string FormatGeneratedAt ( ) =>
183+ reportResult . GeneratedAtUtc == default
184+ ? string . Empty
185+ : IAuditService . ToCAT ( reportResult . GeneratedAtUtc ) . ToString ( "dd MMM yyyy HH:mm 'CAT'" ) ;
186+
187+ private static string FormatAmounts ( decimal usd , decimal zig )
188+ {
189+ var parts = new List < string > ( ) ;
190+ if ( usd != 0 || zig == 0 )
191+ {
192+ parts . Add ( $ "USD { usd : N2} ") ;
193+ }
194+
195+ if ( zig != 0 || usd == 0 )
196+ {
197+ parts . Add ( $ "ZiG { zig : N2} ") ;
198+ }
199+
200+ return string . Join ( " • " , parts ) ;
201+ }
202+
203+ private static string FormatRates ( decimal usdRate , decimal zigRate )
204+ {
205+ var parts = new List < string > ( ) ;
206+ if ( usdRate != 0 || zigRate == 0 )
207+ {
208+ parts . Add ( $ "USD { usdRate : N2} %") ;
209+ }
210+
211+ if ( zigRate != 0 || usdRate == 0 )
212+ {
213+ parts . Add ( $ "ZiG { zigRate : N2} %") ;
214+ }
215+
216+ return string . Join ( " • " , parts ) ;
217+ }
218+
219+ private static IEnumerable < AccountSalesPaymentAccountResult > GetOrderedAccounts (
220+ IEnumerable < AccountSalesPaymentAccountResult > accounts ,
221+ bool includeZeroActivity = false )
222+ {
223+ var filteredAccounts = includeZeroActivity
224+ ? accounts
225+ : accounts . Where ( account => account . InvoiceCount > 0 || account . PaymentCount > 0 || account . TotalQuantitySold > 0 ) ;
226+
227+ return filteredAccounts
228+ . OrderByDescending ( account => account . TotalSalesUsd + account . TotalSalesZig + account . IncomingPaymentsUsd + account . IncomingPaymentsZig )
229+ . ThenBy ( account => account . CardCode , StringComparer . OrdinalIgnoreCase ) ;
230+ }
231+
232+ private static List < string > SplitAccountCodes ( string value ) =>
233+ value
234+ . Split ( new [ ] { ',' , ';' , '\r ' , '\n ' } , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries )
235+ . Where ( accountCode => ! string . IsNullOrWhiteSpace ( accountCode ) )
236+ . Distinct ( StringComparer . OrdinalIgnoreCase )
237+ . ToList ( ) ;
238+
239+ private CancellationToken BeginLoad ( )
240+ {
241+ loadCts . Cancel ( ) ;
242+ loadCts . Dispose ( ) ;
243+ loadCts = new CancellationTokenSource ( ) ;
244+ return loadCts . Token ;
245+ }
246+
247+ public void Dispose ( )
248+ {
249+ isDisposed = true ;
250+ loadCts . Cancel ( ) ;
251+ loadCts . Dispose ( ) ;
252+ }
253+ }
0 commit comments