EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
page.tsx
Go to the documentation of this file.
1"use client";
2
3import React, { useState, useEffect } from 'react';
4import { apiClient } from '@/lib/client/apiClient';
5import { fieldpineApi } from "@/lib/client/fieldpineApi";
6import { Icon } from '@/contexts/IconContext';
7
8interface PaymentType {
9 id: number;
10 name: string;
11 count: number;
12 amount: number;
13 countPos: number;
14 amountPos: number;
15 countNeg: number;
16 amountNeg: number;
17 eftCards?: CardType[];
18}
19
20interface CardType {
21 type: string;
22 count: number;
23 amount: number;
24}
25
26interface LaneData {
27 id: number;
28 name: string;
29 locationName?: string;
30 grandTotal: number;
31 bankCash: number;
32 bankCashCount: number;
33 payments: { [key: string]: { value: number; count: number; name: string } };
34}
35
36export default function EndOfDayPage() {
37 const [fromDate, setFromDate] = useState('');
38 const [toDate, setToDate] = useState('');
39 const [loading, setLoading] = useState(false);
40 const [verticalMode, setVerticalMode] = useState(false);
41
42 // Summary totals
43 const [totalSales, setTotalSales] = useState(0);
44 const [totalCount, setTotalCount] = useState(0);
45 const [payOffAccount, setPayOffAccount] = useState(0);
46 const [prepay, setPrepay] = useState(0);
47 const [salesNet, setSalesNet] = useState(0);
48 const [custRevenue, setCustRevenue] = useState(0);
49 const [custCount, setCustCount] = useState(0);
50 const [cashRevenue, setCashRevenue] = useState(0);
51 const [cashCount, setCashCount] = useState(0);
52
53 // Payment data
54 const [paymentTypes, setPaymentTypes] = useState<PaymentType[]>([]);
55 const [bankingSummary, setBankingSummary] = useState<{ name: string; count: number; amount: number }[]>([]);
56 const [bankingTotal, setBankingTotal] = useState(0);
57 const [rounding, setRounding] = useState(0);
58 const [reconciles, setReconciles] = useState<{ status: 'ok' | 'error' | 'warning'; message: string } | null>(null);
59
60 // Breakdown data
61 const [storeBreakdown, setStoreBreakdown] = useState<LaneData[]>([]);
62 const [laneBreakdown, setLaneBreakdown] = useState<LaneData[]>([]);
63
64 // Load vertical mode preference from localStorage
65 useEffect(() => {
66 if (typeof window !== 'undefined' && window.localStorage) {
67 const saved = localStorage.getItem('tsvertmode');
68 if (saved === '1') setVerticalMode(true);
69 }
70
71 // Set yesterday's date by default
72 const yesterday = new Date();
73 yesterday.setDate(yesterday.getDate() - 1);
74 const dateStr = yesterday.toISOString().split('T')[0];
75 setToDate(dateStr);
76 }, []);
77
78 const formatCurrency = (value: number) => {
79 return new Intl.NumberFormat('en-NZ', {
80 style: 'currency',
81 currency: 'NZD',
82 minimumFractionDigits: 2,
83 maximumFractionDigits: 2
84 }).format(value);
85 };
86
87 const formatNumber = (value: number) => {
88 return new Intl.NumberFormat('en-NZ', {
89 minimumFractionDigits: 0,
90 maximumFractionDigits: 0
91 }).format(value);
92 };
93
94 const formatDateForAPI = (date: string) => {
95 if (!date) return '';
96 const d = new Date(date + 'T00:00:00');
97 const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
98 return `${d.getDate()}${months[d.getMonth()]}${d.getFullYear()}`;
99 };
100
101 const buildPredicates = () => {
102 let predicates = '&10=311,320';
103
104 if (toDate) {
105 const stopDate = formatDateForAPI(toDate);
106
107 if (fromDate) {
108 // Date range
109 const startDate = formatDateForAPI(fromDate);
110 predicates += `&9=f101,4,${startDate}`;
111
112 // Add one day to stop date for exclusive end
113 const stopDateObj = new Date(toDate + 'T00:00:00');
114 stopDateObj.setDate(stopDateObj.getDate() + 1);
115 const stopDatePlus = formatDateForAPI(stopDateObj.toISOString().split('T')[0]);
116 predicates += `&9=f101,1,${stopDatePlus}`;
117 } else {
118 // Single day
119 predicates += `&9=f101,100,${stopDate}`;
120 }
121 }
122
123 return predicates;
124 };
125
126 const getDateRangeForLinks = () => {
127 if (!toDate) return { start: '', end: '' };
128
129 const stopDate = formatDateForAPI(toDate);
130 const stopDateObj = new Date(toDate + 'T00:00:00');
131 stopDateObj.setDate(stopDateObj.getDate() + 1);
132 const stopDatePlus = formatDateForAPI(stopDateObj.toISOString().split('T')[0]);
133
134 if (fromDate) {
135 return { start: formatDateForAPI(fromDate), end: stopDatePlus };
136 } else {
137 return { start: stopDate, end: stopDatePlus };
138 }
139 };
140
141 const loadTradingSummary = async () => {
142 const apiKey = sessionStorage.getItem("fieldpine_apikey");
143 if (!apiKey) {
144 alert("Please log in first");
145 return;
146 }
147
148 setLoading(true);
149
150 try {
151 const predicates = buildPredicates();
152
153 // Load main summary
154 const summaryResponse = await fieldpineApi({
155 endpoint: `/buck?3=retailmax.elink.summary.trading${predicates}`,
156 apiKey: apiKey
157 });
158
159 if (summaryResponse?.data) {
160 const data = summaryResponse.data;
161
162 // Extract totals
163 const totRev = Number(data.f200 || 0);
164 const payOff = Number(data.f130 || 0);
165 const prepayVal = Number(data.f131 || 0);
166
167 setTotalSales(totRev);
168 setTotalCount(Number(data.f120 || 0));
169 setPayOffAccount(payOff);
170 setPrepay(prepayVal);
171 setSalesNet(totRev - payOff - prepayVal);
172
173 setCustRevenue(Number(data.f201 || 0));
174 setCustCount(Number(data.f121 || 0));
175 setCashRevenue(Number(data.f202 || 0));
176 setCashCount(Number(data.f122 || 0));
177
178 // Process payment types
179 if (data.APPT && Array.isArray(data.APPT)) {
180 const payments: PaymentType[] = [];
181 const banking: { name: string; count: number; amount: number }[] = [];
182 let netCash = 0;
183 let netCashCount = 0;
184 let roundingAmt = 0;
185 let grandTotal = 0;
186
187 for (const pt of data.APPT) {
188 const payType: PaymentType = {
189 id: Number(pt.f100 || 0),
190 name: String(pt.f101 || ''),
191 count: Number(pt.f105 || 0),
192 amount: Number(pt.f102 || 0),
193 countPos: Number(pt.f106 || 0),
194 amountPos: Number(pt.f103 || 0),
195 countNeg: Number(pt.f107 || 0),
196 amountNeg: Number(pt.f104 || 0)
197 };
198
199 // Extract card types if available
200 if (pt.EFTC && Array.isArray(pt.EFTC)) {
201 payType.eftCards = pt.EFTC.map((card: any) => ({
202 type: String(card.CardType || card.f100 || '??'),
203 count: Number(card.Count || card.f101 || 0),
204 amount: Number(card.Amount || card.f102 || 0)
205 }));
206 }
207
208 payments.push(payType);
209
210 // Calculate banking totals
211 switch (payType.id) {
212 case 1: // Cash
213 netCash += payType.amount;
214 netCashCount += payType.count;
215 grandTotal += payType.amount;
216 break;
217 case 5: // EFTpos failure - skip
218 break;
219 case 6: // Change
220 netCash -= payType.amount;
221 grandTotal -= payType.amount;
222 break;
223 case 7: // Rounding
224 roundingAmt = payType.amount;
225 break;
226 default:
227 banking.push({
228 name: payType.name,
229 count: payType.count,
230 amount: payType.amount
231 });
232 grandTotal += payType.amount;
233 break;
234 }
235 }
236
237 // Add net cash to banking summary
238 if (netCash !== 0 || netCashCount !== 0) {
239 banking.unshift({
240 name: 'Net Cash',
241 count: netCashCount,
242 amount: netCash
243 });
244 grandTotal += 0; // Already included above
245 }
246
247 setPaymentTypes(payments);
248 setBankingSummary(banking);
249 setBankingTotal(grandTotal);
250 setRounding(roundingAmt);
251
252 // Check reconciliation
253 const diff = Math.abs((grandTotal + roundingAmt) - totRev);
254 if (diff < 0.01) {
255 if (roundingAmt !== 0) {
256 setReconciles({
257 status: 'ok',
258 message: `Reconciles to ${formatCurrency(totRev)} ✔`
259 });
260 } else {
261 setReconciles({
262 status: 'ok',
263 message: `Reconciles to ${formatCurrency(totRev)} ✔`
264 });
265 }
266 } else {
267 setReconciles({
268 status: 'error',
269 message: `Difference of ${formatCurrency(diff)} - totals do not match!`
270 });
271 }
272 }
273 }
274
275 // Load store breakdown
276 loadStoreBreakdown(predicates, apiKey);
277
278 // Load lane breakdown
279 loadLaneBreakdown(predicates, apiKey);
280
281 } catch (error) {
282 console.error('Error loading trading summary:', error);
283 alert('Error loading data. Please try again.');
284 } finally {
285 setLoading(false);
286 }
287 };
288
289 const loadStoreBreakdown = async (predicates: string, apiKey: string) => {
290 try {
291 const response = await fieldpineApi({
292 endpoint: `/buck?3=retailmax.elink.summary.trading&15=2,location${predicates}`,
293 apiKey: apiKey
294 });
295
296 if (response?.data?.APPT && Array.isArray(response.data.APPT)) {
297 const stores: { [key: number]: LaneData } = {};
298
299 for (const pt of response.data.APPT) {
300 const locId = Number(pt.f300 || 0);
301
302 if (!stores[locId]) {
303 stores[locId] = {
304 id: locId,
305 name: String(pt.f301 || locId),
306 grandTotal: 0,
307 bankCash: 0,
308 bankCashCount: 0,
309 payments: {}
310 };
311 }
312
313 const store = stores[locId];
314 const payId = Number(pt.f100 || 0);
315 const payName = String(pt.f101 || '');
316 const amount = Number(pt.f102 || 0);
317 const count = Number(pt.f105 || 0);
318
319 store.payments[payId] = { value: amount, count: count, name: payName };
320
321 // Calculate totals
322 switch (payId) {
323 case 1: // Cash
324 store.grandTotal += amount;
325 store.bankCash += amount;
326 store.bankCashCount += count;
327 break;
328 case 5: // EFTpos failure
329 break;
330 case 6: // Change
331 store.grandTotal -= amount;
332 store.bankCash -= amount;
333 break;
334 case 7: // Rounding
335 break;
336 default:
337 store.grandTotal += amount;
338 break;
339 }
340 }
341
342 setStoreBreakdown(Object.values(stores));
343 }
344 } catch (error) {
345 console.error('Error loading store breakdown:', error);
346 }
347 };
348
349 const loadLaneBreakdown = async (predicates: string, apiKey: string) => {
350 try {
351 const response = await fieldpineApi({
352 endpoint: `/buck?3=retailmax.elink.summary.trading&15=2,srcuid${predicates}`,
353 apiKey: apiKey
354 });
355
356 if (response?.data?.APPT && Array.isArray(response.data.APPT)) {
357 const lanes: { [key: number]: LaneData } = {};
358
359 for (const pt of response.data.APPT) {
360 const laneId = Number(pt.f110 || 0);
361
362 if (!lanes[laneId]) {
363 lanes[laneId] = {
364 id: laneId,
365 name: String(laneId),
366 locationName: String(pt.f301 || ''),
367 grandTotal: 0,
368 bankCash: 0,
369 bankCashCount: 0,
370 payments: {}
371 };
372 }
373
374 const lane = lanes[laneId];
375 const payId = Number(pt.f100 || 0);
376 const payName = String(pt.f101 || '');
377 const amount = Number(pt.f102 || 0);
378 const count = Number(pt.f105 || 0);
379
380 lane.payments[payId] = { value: amount, count: count, name: payName };
381
382 // Calculate totals
383 switch (payId) {
384 case 1: // Cash
385 lane.grandTotal += amount;
386 lane.bankCash += amount;
387 lane.bankCashCount += count;
388 break;
389 case 5: // EFTpos failure
390 break;
391 case 6: // Change
392 lane.grandTotal -= amount;
393 lane.bankCash -= amount;
394 break;
395 case 7: // Rounding
396 break;
397 default:
398 lane.grandTotal += amount;
399 break;
400 }
401 }
402
403 setLaneBreakdown(Object.values(lanes));
404 }
405 } catch (error) {
406 console.error('Error loading lane breakdown:', error);
407 }
408 };
409
410 const handleVerticalModeChange = (checked: boolean) => {
411 setVerticalMode(checked);
412 if (typeof window !== 'undefined' && window.localStorage) {
413 localStorage.setItem('tsvertmode', checked ? '1' : '0');
414 }
415 };
416
417 const dateRange = getDateRangeForLinks();
418
419 return (
420 <div className="p-6">
421 <h1 className="text-3xl font-bold mb-4">End of Day Trading Summary</h1>
422
423 {/* Quick Navigation */}
424 <div className="mb-6 bg-surface p-4 rounded-lg shadow border border-border">
425 <div className="flex items-center justify-between mb-3">
426 <div className="text-sm font-semibold text-text">Useful End of Day Reports</div>
427 <div className="text-xs text-muted">Common reports for end of day reconciliation</div>
428 </div>
429 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
430 <a
431 href="/pages/reports/sales-reports/sale-items-review"
432 className="group relative block bg-gradient-to-br from-surface to-surface-2 border border-border rounded-xl p-4 hover:shadow-xl hover:scale-[1.02] hover:border-brand/50 hover:from-brand/5 hover:to-brand/10 transition-all duration-300 overflow-hidden"
433 >
434 <div className="absolute inset-0 bg-gradient-to-br from-brand/0 to-brand/0 group-hover:from-brand/5 group-hover:to-brand/10 transition-all duration-300"></div>
435 <div className="relative z-10 flex flex-col gap-2">
436 <div className="flex items-center gap-3">
437 <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-brand/10 to-brand/20 group-hover:from-brand/20 group-hover:to-brand/30 flex items-center justify-center shadow-sm group-hover:shadow-md transition-all duration-300">
438 <Icon name="list_alt" size={24} className="text-brand group-hover:scale-110 transition-transform duration-300" />
439 </div>
440 <div className="font-semibold text-text group-hover:text-brand transition-colors duration-200">Sale Items Review</div>
441 </div>
442 <div className="text-xs text-muted">Individual saleline details</div>
443 </div>
444 </a>
445 <a
446 href="/pages/reports/sales-reports/sale-header-review"
447 className="group relative block bg-gradient-to-br from-surface to-surface-2 border border-border rounded-xl p-4 hover:shadow-xl hover:scale-[1.02] hover:border-brand/50 hover:from-brand/5 hover:to-brand/10 transition-all duration-300 overflow-hidden"
448 >
449 <div className="absolute inset-0 bg-gradient-to-br from-brand/0 to-brand/0 group-hover:from-brand/5 group-hover:to-brand/10 transition-all duration-300"></div>
450 <div className="relative z-10 flex flex-col gap-2">
451 <div className="flex items-center gap-3">
452 <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-brand/10 to-brand/20 group-hover:from-brand/20 group-hover:to-brand/30 flex items-center justify-center shadow-sm group-hover:shadow-md transition-all duration-300">
453 <Icon name="description" size={24} className="text-brand group-hover:scale-110 transition-transform duration-300" />
454 </div>
455 <div className="font-semibold text-text group-hover:text-brand transition-colors duration-200">Sale Header Review</div>
456 </div>
457 <div className="text-xs text-muted">Single line per sale</div>
458 </div>
459 </a>
460 <a
461 href="/pages/reports/sales-reports/payment-list"
462 className="group relative block bg-gradient-to-br from-surface to-surface-2 border border-border rounded-xl p-4 hover:shadow-xl hover:scale-[1.02] hover:border-brand/50 hover:from-brand/5 hover:to-brand/10 transition-all duration-300 overflow-hidden"
463 >
464 <div className="absolute inset-0 bg-gradient-to-br from-brand/0 to-brand/0 group-hover:from-brand/5 group-hover:to-brand/10 transition-all duration-300"></div>
465 <div className="relative z-10 flex flex-col gap-2">
466 <div className="flex items-center gap-3">
467 <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-brand/10 to-brand/20 group-hover:from-brand/20 group-hover:to-brand/30 flex items-center justify-center shadow-sm group-hover:shadow-md transition-all duration-300">
468 <Icon name="credit_card" size={24} className="text-brand group-hover:scale-110 transition-transform duration-300" />
469 </div>
470 <div className="font-semibold text-text group-hover:text-brand transition-colors duration-200">Payment List</div>
471 </div>
472 <div className="text-xs text-muted">Individual payment transactions</div>
473 </div>
474 </a>
475 <a
476 href="/pages/reports/sales-reports/sales-kpi"
477 className="group relative block bg-gradient-to-br from-surface to-surface-2 border border-border rounded-xl p-4 hover:shadow-xl hover:scale-[1.02] hover:border-brand/50 hover:from-brand/5 hover:to-brand/10 transition-all duration-300 overflow-hidden"
478 >
479 <div className="absolute inset-0 bg-gradient-to-br from-brand/0 to-brand/0 group-hover:from-brand/5 group-hover:to-brand/10 transition-all duration-300"></div>
480 <div className="relative z-10 flex flex-col gap-2">
481 <div className="flex items-center gap-3">
482 <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-brand/10 to-brand/20 group-hover:from-brand/20 group-hover:to-brand/30 flex items-center justify-center shadow-sm group-hover:shadow-md transition-all duration-300">
483 <Icon name="bar_chart" size={24} className="text-brand group-hover:scale-110 transition-transform duration-300" />
484 </div>
485 <div className="font-semibold text-text group-hover:text-brand transition-colors duration-200">Sales KPI</div>
486 </div>
487 <div className="text-xs text-muted">Key performance indicators by store</div>
488 </div>
489 </a>
490 </div>
491 </div>
492
493 {/* Date Selection */}
494 <div className="mb-6 bg-surface p-4 rounded-lg shadow">
495 <div className="flex flex-wrap items-center gap-4">
496 <div>
497 <label className="block text-sm font-medium mb-1">From (included)</label>
498 <input
499 type="date"
500 value={fromDate}
501 onChange={(e) => setFromDate(e.target.value)}
502 className="border rounded px-3 py-2"
503 />
504 </div>
505 <div>
506 <label className="block text-sm font-medium mb-1">To (included)</label>
507 <input
508 type="date"
509 value={toDate}
510 onChange={(e) => setToDate(e.target.value)}
511 className="border rounded px-3 py-2"
512 />
513 </div>
514 <div className="self-end">
515 <button
516 onClick={loadTradingSummary}
517 disabled={loading}
518 className="bg-brand text-white px-6 py-2 rounded hover:bg-brand/90 disabled:bg-muted/50"
519 >
520 {loading ? 'Loading...' : 'Load Date'}
521 </button>
522 </div>
523 </div>
524 <p className="text-sm text-muted mt-2">
525 Tip: You only need to enter a "to" date if you want a single day
526 </p>
527 </div>
528
529 {/* Summary Boxes */}
530 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
531 {/* Total Turnover */}
532 <div className="bg-surface p-4 rounded-lg shadow border border-border">
533 <h3 className="font-bold text-lg mb-3">Total Turnover</h3>
534 <div className="space-y-2">
535 <div className="flex justify-between">
536 <span>Turnover</span>
537 <span className="font-mono">{formatCurrency(totalSales)}</span>
538 </div>
539 <div className="flex justify-between">
540 <span>Count</span>
541 <span className="font-mono">{formatNumber(totalCount)}</span>
542 </div>
543 <div className="flex justify-between">
544 <span>Pay Off Account</span>
545 <span className="font-mono">{formatCurrency(payOffAccount)}</span>
546 </div>
547 <div className="flex justify-between">
548 <span>Prepay Topup</span>
549 <span className="font-mono">{formatCurrency(prepay)}</span>
550 </div>
551 <div className="flex justify-between font-bold pt-2 border-t">
552 <span>Estimated Sales</span>
553 <span className="font-mono">{formatCurrency(salesNet)}</span>
554 </div>
555 </div>
556 </div>
557
558 {/* Banking Summary */}
559 <div className="bg-surface p-4 rounded-lg shadow border border-border">
560 <h3 className="font-bold text-lg mb-3">Banking Summary</h3>
561 {bankingSummary.length > 0 ? (
562 <div className="space-y-2">
563 {bankingSummary.map((item, idx) => (
564 <div key={idx} className="flex justify-between">
565 <span>{item.name}</span>
566 <span className="font-mono bg-lime-200 px-2 rounded">{formatCurrency(item.amount)}</span>
567 </div>
568 ))}
569 <div className="flex justify-between font-bold pt-2 border-t">
570 <span>Totals</span>
571 <span className="font-mono bg-lime-200 px-2 rounded">{formatCurrency(bankingTotal)}</span>
572 </div>
573 {reconciles && (
574 <div className={`text-xs pt-2 ${reconciles.status === 'ok' ? 'text-green-700' : 'text-red-700'}`}>
575 {reconciles.message}
576 </div>
577 )}
578 </div>
579 ) : (
580 <p className="text-muted text-sm">No data</p>
581 )}
582 </div>
583
584 {/* Known Customers */}
585 <div className="bg-surface p-4 rounded-lg shadow border border-border">
586 <h3 className="font-bold text-lg mb-3">Known Customers</h3>
587 <div className="space-y-2">
588 <div className="flex justify-between">
589 <span>Turnover</span>
590 <span className="font-mono">{formatCurrency(custRevenue)}</span>
591 </div>
592 <div className="flex justify-between">
593 <span>Count</span>
594 <span className="font-mono">{formatNumber(custCount)}</span>
595 </div>
596 </div>
597 </div>
598
599 {/* Cash Customers */}
600 <div className="bg-surface p-4 rounded-lg shadow border border-border">
601 <h3 className="font-bold text-lg mb-3">Cash Customers</h3>
602 <div className="space-y-2">
603 <div className="flex justify-between">
604 <span>Turnover</span>
605 <span className="font-mono">{formatCurrency(cashRevenue)}</span>
606 </div>
607 <div className="flex justify-between">
608 <span>Count</span>
609 <span className="font-mono">{formatNumber(cashCount)}</span>
610 </div>
611 </div>
612 </div>
613 </div>
614
615 {/* EFTpos Card Types */}
616 {paymentTypes.some(pt => pt.eftCards && pt.eftCards.length > 0) && (
617 <div className="bg-surface p-4 rounded-lg shadow mb-6">
618 <h3 className="font-bold text-lg mb-3">EFTpos Cardtypes</h3>
619 <div className="overflow-x-auto">
620 <table className="min-w-full border-collapse border border-border">
621 <thead>
622 <tr className="bg-surface-2">
623 <th className="border border-border px-4 py-2 text-left">CardType</th>
624 <th className="border border-border px-4 py-2 text-center">Count</th>
625 <th className="border border-border px-4 py-2 text-right">Total</th>
626 </tr>
627 </thead>
628 <tbody>
629 {paymentTypes.map(pt =>
630 pt.eftCards?.map((card, idx) => (
631 <tr key={`${pt.id}-${idx}`}>
632 <td className="border border-border px-4 py-2">{card.type}</td>
633 <td className="border border-border px-4 py-2 text-center">{card.count}</td>
634 <td className="border border-border px-4 py-2 text-right font-mono">{formatCurrency(card.amount)}</td>
635 </tr>
636 ))
637 )}
638 </tbody>
639 </table>
640 <p className="text-xs text-muted mt-2">
641 These figures are best effort, review your EFTpos provider for more details
642 </p>
643 </div>
644 </div>
645 )}
646
647 {/* Actual Payment Methods */}
648 {paymentTypes.length > 0 && (
649 <div className="bg-surface p-4 rounded-lg shadow mb-6">
650 <h3 className="font-bold text-lg mb-3">Actual Payment Methods</h3>
651 <div className="overflow-x-auto">
652 <table className="min-w-full border-collapse border border-border">
653 <thead>
654 <tr className="bg-surface-2">
655 <th className="border border-border px-4 py-2 text-left">Tender</th>
656 <th className="border border-border px-4 py-2 text-right">Total Count</th>
657 <th className="border border-border px-4 py-2 text-right">Total (Net)</th>
658 <th className="border border-border px-4 py-2 text-right">Count &gt; 0</th>
659 <th className="border border-border px-4 py-2 text-right">Total &gt; 0</th>
660 <th className="border border-border px-4 py-2 text-right">Count &lt; 0</th>
661 <th className="border border-border px-4 py-2 text-right">Total &lt; 0</th>
662 </tr>
663 </thead>
664 <tbody>
665 {paymentTypes.map(pt => (
666 <tr key={pt.id} className={pt.id === 5 ? 'bg-red-50' : ''}>
667 <td className="border border-border px-4 py-2">
668 <a
669 href={`/report/pos/sales/fieldpine/payments_list.htm?sdt=${dateRange.start}&edt=${dateRange.end}&ptype=${pt.id}`}
670 className="text-brand hover:underline"
671 target="_blank"
672 >
673 {pt.name}
674 </a>
675 </td>
676 <td className="border border-border px-4 py-2 text-right font-mono">{formatNumber(pt.count)}</td>
677 <td className="border border-border px-4 py-2 text-right font-mono">{formatCurrency(pt.amount)}</td>
678 <td className="border border-border px-4 py-2 text-right font-mono">{formatNumber(pt.countPos)}</td>
679 <td className="border border-border px-4 py-2 text-right font-mono">{formatCurrency(pt.amountPos)}</td>
680 <td className="border border-border px-4 py-2 text-right font-mono">{formatNumber(pt.countNeg)}</td>
681 <td className="border border-border px-4 py-2 text-right font-mono">{formatCurrency(pt.amountNeg)}</td>
682 </tr>
683 ))}
684 </tbody>
685 </table>
686 <p className="text-sm text-muted mt-2">
687 Warning: Cash in this table is amount tendered, remember to subtract change
688 </p>
689 </div>
690 </div>
691 )}
692
693 {/* By Store Breakdown */}
694 {storeBreakdown.length > 0 && (
695 <div className="bg-surface p-4 rounded-lg shadow mb-6">
696 <div className="flex justify-between items-center mb-3">
697 <h2 className="font-bold text-xl">By Store Breakdown (full detail)</h2>
698 <label className="flex items-center gap-2">
699 <input
700 type="checkbox"
701 checked={verticalMode}
702 onChange={(e) => handleVerticalModeChange(e.target.checked)}
703 />
704 <span>Vertical Display</span>
705 </label>
706 </div>
707
708 {verticalMode ? (
709 <VerticalStoreBreakdown stores={storeBreakdown} formatCurrency={formatCurrency} formatNumber={formatNumber} />
710 ) : (
711 <HorizontalStoreBreakdown stores={storeBreakdown} formatCurrency={formatCurrency} formatNumber={formatNumber} />
712 )}
713 </div>
714 )}
715
716 {/* By Lane Breakdown */}
717 {laneBreakdown.length > 0 && (
718 <div className="bg-surface p-4 rounded-lg shadow">
719 <h2 className="font-bold text-xl mb-3">By Each Trading Lane Breakdown (full detail)</h2>
720 <HorizontalLaneBreakdown lanes={laneBreakdown} formatCurrency={formatCurrency} formatNumber={formatNumber} />
721 </div>
722 )}
723 </div>
724 );
725}
726
727// Vertical Store Breakdown Component
728function VerticalStoreBreakdown({
729 stores,
730 formatCurrency,
731 formatNumber
732}: {
733 stores: LaneData[];
734 formatCurrency: (n: number) => string;
735 formatNumber: (n: number) => string;
736}) {
737 // Collect all payment types
738 const paymentTypes = new Set<number>();
739 const specialPayments = new Set<number>();
740
741 stores.forEach(store => {
742 Object.keys(store.payments).forEach(key => {
743 const payId = Number(key);
744 if ([1, 5, 6, 7].includes(payId)) {
745 specialPayments.add(payId);
746 } else {
747 paymentTypes.add(payId);
748 }
749 });
750 });
751
752 return (
753 <div className="overflow-x-auto">
754 <table className="min-w-full border-collapse border border-border">
755 <thead>
756 <tr className="bg-surface-2">
757 <th className="border border-border px-4 py-2 text-left">Store</th>
758 <th className="border border-border px-4 py-2 text-center">Total</th>
759 <th className="border border-border px-4 py-2 text-center">Net Cash</th>
760 {Array.from(paymentTypes).map(payId => {
761 const sampleStore = stores.find(s => s.payments[payId]);
762 return (
763 <th key={payId} className="border border-border px-4 py-2 text-center">
764 {sampleStore?.payments[payId]?.name || payId}
765 </th>
766 );
767 })}
768 {Array.from(specialPayments).filter(id => ![1].includes(id)).map(payId => {
769 const sampleStore = stores.find(s => s.payments[payId]);
770 return (
771 <th key={payId} className="border border-border px-4 py-2 text-center bg-surface-2 text-xs">
772 {sampleStore?.payments[payId]?.name || payId}
773 </th>
774 );
775 })}
776 </tr>
777 </thead>
778 <tbody>
779 {stores.map(store => (
780 <tr key={store.id}>
781 <td className="border border-border px-4 py-2 font-bold">{store.name}</td>
782 <td className="border border-border px-4 py-2 text-right font-mono font-bold">
783 {formatCurrency(store.grandTotal)}
784 </td>
785 <td className="border border-border px-4 py-2 text-right font-mono">
786 {formatCurrency(store.bankCash)}
787 </td>
788 {Array.from(paymentTypes).map(payId => (
789 <td key={payId} className="border border-border px-4 py-2 text-right font-mono">
790 {store.payments[payId] ? formatCurrency(store.payments[payId].value) : ''}
791 </td>
792 ))}
793 {Array.from(specialPayments).filter(id => ![1].includes(id)).map(payId => (
794 <td key={payId} className="border border-border px-4 py-2 text-right font-mono text-xs bg-surface-2">
795 {store.payments[payId] ? formatCurrency(store.payments[payId].value) : ''}
796 </td>
797 ))}
798 </tr>
799 ))}
800 </tbody>
801 </table>
802 </div>
803 );
804}
805
806// Horizontal Store Breakdown Component
807function HorizontalStoreBreakdown({
808 stores,
809 formatCurrency,
810 formatNumber
811}: {
812 stores: LaneData[];
813 formatCurrency: (n: number) => string;
814 formatNumber: (n: number) => string;
815}) {
816 // Collect all payment types
817 const mainPaymentTypes = new Set<number>();
818 const detailPaymentTypes = new Set<number>();
819
820 stores.forEach(store => {
821 Object.keys(store.payments).forEach(key => {
822 const payId = Number(key);
823 if ([1, 5, 6, 7].includes(payId)) {
824 detailPaymentTypes.add(payId);
825 } else {
826 mainPaymentTypes.add(payId);
827 }
828 });
829 });
830
831 return (
832 <div className="overflow-x-auto">
833 <table className="min-w-full border-collapse border border-border">
834 <thead>
835 <tr className="bg-surface-2">
836 <th className="border border-border px-4 py-2">&nbsp;</th>
837 {stores.map(store => (
838 <th key={store.id} colSpan={2} className="border border-border px-4 py-2 text-center">
839 <b>{store.name}</b>
840 </th>
841 ))}
842 </tr>
843 <tr className="bg-surface-2">
844 <th className="border border-border px-4 py-2">Tender</th>
845 {stores.map(store => (
846 <React.Fragment key={store.id}>
847 <th className="border border-border px-2 py-2 text-right text-sm">Count</th>
848 <th className="border border-border px-2 py-2 text-right text-sm">Amount</th>
849 </React.Fragment>
850 ))}
851 </tr>
852 </thead>
853 <tbody>
854 {/* Net Cash Row */}
855 <tr>
856 <td className="border border-border px-4 py-2">Net Cash</td>
857 {stores.map(store => (
858 <React.Fragment key={store.id}>
859 <td className="border border-border px-2 py-2 text-right font-mono">{formatNumber(store.bankCashCount)}</td>
860 <td className="border border-border px-2 py-2 text-right font-mono">{formatCurrency(store.bankCash)}</td>
861 </React.Fragment>
862 ))}
863 </tr>
864
865 {/* Main Payment Types */}
866 {Array.from(mainPaymentTypes).map(payId => {
867 const sampleStore = stores.find(s => s.payments[payId]);
868 return (
869 <tr key={payId}>
870 <td className="border border-border px-4 py-2">{sampleStore?.payments[payId]?.name || payId}</td>
871 {stores.map(store => (
872 <React.Fragment key={store.id}>
873 <td className="border border-border px-2 py-2 text-right font-mono">
874 {store.payments[payId] ? formatNumber(store.payments[payId].count) : ''}
875 </td>
876 <td className="border border-border px-2 py-2 text-right font-mono">
877 {store.payments[payId] ? formatCurrency(store.payments[payId].value) : ''}
878 </td>
879 </React.Fragment>
880 ))}
881 </tr>
882 );
883 })}
884
885 {/* Totals Row */}
886 <tr className="font-bold border-t-2 border-black">
887 <td className="border border-border px-4 py-2">Totals</td>
888 {stores.map(store => (
889 <React.Fragment key={store.id}>
890 <td className="border border-border px-2 py-2"></td>
891 <td className="border border-border px-2 py-2 text-right font-mono">{formatCurrency(store.grandTotal)}</td>
892 </React.Fragment>
893 ))}
894 </tr>
895
896 {/* Spacer */}
897 <tr>
898 <td colSpan={stores.length * 2 + 1} className="px-4 py-2 text-sm text-muted">
899 <br />Actual figures summarised above
900 </td>
901 </tr>
902
903 {/* Detail Payment Types */}
904 {Array.from(detailPaymentTypes).map(payId => {
905 const sampleStore = stores.find(s => s.payments[payId]);
906 return (
907 <tr key={payId} className="text-xs">
908 <td className="border border-border px-4 py-2">{sampleStore?.payments[payId]?.name || payId}</td>
909 {stores.map(store => (
910 <React.Fragment key={store.id}>
911 <td className="border border-border px-2 py-2 text-right font-mono">
912 {store.payments[payId] ? formatNumber(store.payments[payId].count) : ''}
913 </td>
914 <td className="border border-border px-2 py-2 text-right font-mono">
915 {store.payments[payId] ? formatCurrency(store.payments[payId].value) : ''}
916 </td>
917 </React.Fragment>
918 ))}
919 </tr>
920 );
921 })}
922 </tbody>
923 </table>
924 </div>
925 );
926}
927
928// Horizontal Lane Breakdown Component
929function HorizontalLaneBreakdown({
930 lanes,
931 formatCurrency,
932 formatNumber
933}: {
934 lanes: LaneData[];
935 formatCurrency: (n: number) => string;
936 formatNumber: (n: number) => string;
937}) {
938 // Collect all payment types
939 const mainPaymentTypes = new Set<number>();
940 const detailPaymentTypes = new Set<number>();
941
942 lanes.forEach(lane => {
943 Object.keys(lane.payments).forEach(key => {
944 const payId = Number(key);
945 if ([1, 5, 6, 7].includes(payId)) {
946 detailPaymentTypes.add(payId);
947 } else {
948 mainPaymentTypes.add(payId);
949 }
950 });
951 });
952
953 return (
954 <div className="overflow-x-auto">
955 <table className="min-w-full border-collapse border border-border">
956 <thead>
957 <tr className="bg-surface-2">
958 <th className="border border-border px-4 py-2">&nbsp;</th>
959 {lanes.map(lane => (
960 <th key={lane.id} colSpan={2} className="border border-border px-4 py-2 text-center">
961 {lane.id}
962 </th>
963 ))}
964 </tr>
965 <tr className="bg-surface-2">
966 <th className="border border-border px-4 py-2">&nbsp;</th>
967 {lanes.map(lane => (
968 <th key={lane.id} colSpan={2} className="border border-border px-4 py-2 text-center">
969 <b>{lane.locationName}</b>
970 </th>
971 ))}
972 </tr>
973 <tr className="bg-surface-2">
974 <th className="border border-border px-4 py-2">Tender</th>
975 {lanes.map(lane => (
976 <React.Fragment key={lane.id}>
977 <th className="border border-border px-2 py-2 text-right text-sm">Count</th>
978 <th className="border border-border px-2 py-2 text-right text-sm">Amount</th>
979 </React.Fragment>
980 ))}
981 </tr>
982 </thead>
983 <tbody>
984 {/* Net Cash Row */}
985 <tr>
986 <td className="border border-border px-4 py-2">Net Cash</td>
987 {lanes.map(lane => (
988 <React.Fragment key={lane.id}>
989 <td className="border border-border px-2 py-2 text-right font-mono">{formatNumber(lane.bankCashCount)}</td>
990 <td className="border border-border px-2 py-2 text-right font-mono">{formatCurrency(lane.bankCash)}</td>
991 </React.Fragment>
992 ))}
993 </tr>
994
995 {/* Main Payment Types */}
996 {Array.from(mainPaymentTypes).map(payId => {
997 const sampleLane = lanes.find(l => l.payments[payId]);
998 return (
999 <tr key={payId}>
1000 <td className="border border-border px-4 py-2">{sampleLane?.payments[payId]?.name || payId}</td>
1001 {lanes.map(lane => (
1002 <React.Fragment key={lane.id}>
1003 <td className="border border-border px-2 py-2 text-right font-mono">
1004 {lane.payments[payId] ? formatNumber(lane.payments[payId].count) : ''}
1005 </td>
1006 <td className="border border-border px-2 py-2 text-right font-mono">
1007 {lane.payments[payId] ? formatCurrency(lane.payments[payId].value) : ''}
1008 </td>
1009 </React.Fragment>
1010 ))}
1011 </tr>
1012 );
1013 })}
1014
1015 {/* Totals Row */}
1016 <tr className="font-bold border-t-2 border-black">
1017 <td className="border border-border px-4 py-2">Totals</td>
1018 {lanes.map(lane => (
1019 <React.Fragment key={lane.id}>
1020 <td className="border border-border px-2 py-2"></td>
1021 <td className="border border-border px-2 py-2 text-right font-mono">{formatCurrency(lane.grandTotal)}</td>
1022 </React.Fragment>
1023 ))}
1024 </tr>
1025
1026 {/* Spacer */}
1027 <tr>
1028 <td colSpan={lanes.length * 2 + 1} className="px-4 py-2 text-sm text-muted">
1029 <br />Actual figures summarised above
1030 </td>
1031 </tr>
1032
1033 {/* Detail Payment Types */}
1034 {Array.from(detailPaymentTypes).map(payId => {
1035 const sampleLane = lanes.find(l => l.payments[payId]);
1036 return (
1037 <tr key={payId} className="text-xs">
1038 <td className="border border-border px-4 py-2">{sampleLane?.payments[payId]?.name || payId}</td>
1039 {lanes.map(lane => (
1040 <React.Fragment key={lane.id}>
1041 <td className="border border-border px-2 py-2 text-right font-mono">
1042 {lane.payments[payId] ? formatNumber(lane.payments[payId].count) : ''}
1043 </td>
1044 <td className="border border-border px-2 py-2 text-right font-mono">
1045 {lane.payments[payId] ? formatCurrency(lane.payments[payId].value) : ''}
1046 </td>
1047 </React.Fragment>
1048 ))}
1049 </tr>
1050 );
1051 })}
1052 </tbody>
1053 </table>
1054 </div>
1055 );
1056}