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 { useState, useEffect, useCallback } from 'react';
4import { apiClient } from '@/lib/client/apiClient';
5import { useStore } from '@/contexts/StoreContext';
6
7interface Location {
8 f100: number;
9 f101: string;
10 f180?: number;
11 f182?: string;
12}
13
14interface Department {
15 f100: number;
16 f101: string;
17 f1100?: number;
18}
19
20interface SalesCubeItem {
21 f100: number; // Location ID
22 f101: number; // Department ID
23 f102: string; // Date info (format: "something|something|day")
24 f201: number; // Sales amount
25 f202?: number; // Cost/GP amount
26}
27
28interface DailyTotals {
29 [key: number]: {
30 c1: number; // Recharge Toner
31 c2: number; // Recharge Ink
32 c3: number; // Compatible CW Toner
33 c4: number; // Compatible Other Toner
34 c5: number; // Compatible CW Inks
35 c6: number; // Compatible Other Inks
36 c7: number; // New Toner
37 c8: number; // New Ink
38 c9: number; // Other
39 c10: number; // R&M
40 c11: number; // Hardware & Software
41 c12: number; // H&S Gross Profit
42 c13: number; // Excluded Sales
43 c12_errors: number;
44 };
45}
46
47export default function FranchiseReportPage() {
48 const { session } = useStore();
49 const [selectedMonth, setSelectedMonth] = useState<number>(new Date().getMonth() + 1);
50 const [selectedYear, setSelectedYear] = useState<number>(new Date().getFullYear());
51 const [locations, setLocations] = useState<Location[]>([]);
52 const [departments, setDepartments] = useState<Department[]>([]);
53 const [currentTab, setCurrentTab] = useState<number>(0);
54 const [currentLocId, setCurrentLocId] = useState<number>(0);
55 const [currentLocName, setCurrentLocName] = useState<string>('All Stores');
56 const [dailyTotals, setDailyTotals] = useState<DailyTotals>({});
57 const [columnTotals, setColumnTotals] = useState<number[]>(Array(14).fill(0));
58 const [loading, setLoading] = useState(false);
59 const [salesCube, setSalesCube] = useState<any>(null);
60
61 const monthNames = [
62 'January', 'February', 'March', 'April', 'May', 'June',
63 'July', 'August', 'September', 'October', 'November', 'December'
64 ];
65
66 // Initialize location for POS users to current store
67 useEffect(() => {
68 if (session?.store) {
69 const isPosUser = session.store.type !== 'management';
70 if (isPosUser) {
71 setCurrentLocId(Number(session.store.id) || 0);
72 setCurrentLocName(session.store.name || 'Current Store');
73 }
74 }
75 }, [session?.store]);
76
77 const loadLocations = useCallback(async () => {
78 try {
79 console.log('[Franchise Report] Loading locations...');
80 const response = await apiClient.get('/api/v1/locations');
81 console.log('[Franchise Report] Locations response:', response);
82 if (response && Array.isArray(response)) {
83 setLocations(response);
84 console.log('[Franchise Report] Loaded locations:', response.length);
85 }
86 } catch (error) {
87 console.error('[Franchise Report] Error loading locations:', error);
88 }
89 }, []);
90
91 const loadDepartments = useCallback(async () => {
92 try {
93 console.log('[Franchise Report] Loading departments...');
94 const response = await apiClient.get('/api/v1/departments');
95 console.log('[Franchise Report] Departments response:', response);
96 if (response && Array.isArray(response)) {
97 setDepartments(response);
98 console.log('[Franchise Report] Loaded departments:', response.length);
99 }
100 } catch (error) {
101 console.error('[Franchise Report] Error loading departments:', error);
102 }
103 }, []);
104
105 const runReport = useCallback(async () => {
106 console.log('[Franchise Report] Running report...', { selectedMonth, selectedYear });
107 setLoading(true);
108
109 // Build date range in the format expected by BUCK API
110 const fromDate = `1-${selectedMonth}-${selectedYear}`;
111 let nextMonth = selectedMonth + 1;
112 let nextYear = selectedYear;
113 if (nextMonth === 13) {
114 nextMonth = 1;
115 nextYear++;
116 }
117 const toDate = `1-${nextMonth}-${nextYear}`;
118
119 try {
120 console.log('[Franchise Report] Fetching sales cube...', { fromDate, toDate });
121
122 // The sales-cube API route will handle the BUCK formatting
123 // Pass mode=5 and date range
124 const response = await apiClient.get(
125 `/v1/reports/sales-cube?mode=5&fromDate=${fromDate}&toDate=${toDate}`
126 );
127 console.log('[Franchise Report] Sales cube response:', response);
128
129 if (response?.APPD && response.APPD.length > 0) {
130 setSalesCube(response);
131 processSalesCube(response.APPD);
132 console.log('[Franchise Report] Processed sales cube data');
133 } else {
134 console.warn('[Franchise Report] No sales data found for this period');
135 // Initialize empty data so the table shows zeros
136 setSalesCube(response);
137 processSalesCube([]);
138 }
139 } catch (error) {
140 console.error('[Franchise Report] Error loading sales data:', error);
141 } finally {
142 setLoading(false);
143 }
144 }, [selectedMonth, selectedYear]);
145
146 // Load locations and departments on mount
147 useEffect(() => {
148 loadLocations();
149 loadDepartments();
150 }, [loadLocations, loadDepartments]);
151
152 // Auto-run report when locations and departments are loaded
153 useEffect(() => {
154 if (locations.length > 0 && departments.length > 0) {
155 console.log('[Franchise Report] Auto-running report with', locations.length, 'locations and', departments.length, 'departments');
156 runReport();
157 }
158 }, [locations.length, departments.length, selectedMonth, selectedYear, runReport]);
159
160 const processSalesCube = (cubeData: SalesCubeItem[]) => {
161 console.log('[processSalesCube] Processing cube data', {
162 itemCount: cubeData.length,
163 currentLocId,
164 currentLocName,
165 sessionStoreId: session?.store?.id,
166 isPosUser: session?.store?.type !== 'management'
167 });
168
169 // Initialize daily totals
170 const dailyData: DailyTotals = {};
171 for (let day = 1; day <= 31; day++) {
172 dailyData[day] = {
173 c1: 0, c2: 0, c3: 0, c4: 0, c5: 0, c6: 0, c7: 0, c8: 0,
174 c9: 0, c10: 0, c11: 0, c12: 0, c13: 0, c12_errors: 0
175 };
176 }
177
178 // Determine which locations to include
179 let matchLocs: number[] = [];
180 if (currentLocId < 0) {
181 const loc = locations.find(l => l.f100 === currentLocId);
182 if (loc?.f182) {
183 matchLocs = loc.f182.split('|').map(Number);
184 }
185 }
186
187 // Process each cube item
188 cubeData.forEach((item: SalesCubeItem) => {
189 const depId = item.f101;
190 const dayOfMonth = parseInt(item.f102.split('|')[2]);
191 const amount = item.f201;
192 const cost = item.f202 || 0;
193
194 console.log('[processSalesCube] Processing item', {
195 locationId: item.f100,
196 departmentId: depId,
197 date: item.f102,
198 day: dayOfMonth,
199 amount,
200 currentLocId
201 });
202
203 // Check if this location should be included
204 // For POS users on "Current Store" tab (currentTab === 0), currentLocId will be their store ID
205 // For management on "All Stores" tab, currentLocId will be 0
206 const includeLocation =
207 currentLocId === 0 ||
208 currentLocId === item.f100 ||
209 (matchLocs.length > 0 && matchLocs.includes(item.f100));
210
211 if (!includeLocation) return;
212 if (dayOfMonth < 1 || dayOfMonth > 31) return;
213
214 // Map department to column based on department ID
215 // Fieldpine uses LocAll arrays which map to display columns differently
216 let columnKey: keyof Omit<DailyTotals[number], 'c12_errors'> | null = null;
217
218 switch (depId) {
219 case 1: columnKey = 'c3'; break; // Dept 1 → LocAll3 → Compatible CW Toner
220 case 2: columnKey = 'c5'; break; // Dept 2 → LocAll5 → Compatible CW Inks
221 case 3: columnKey = 'c1'; break; // Dept 3 → LocAll1 → Recharge Toner
222 case 4: columnKey = 'c2'; break; // Dept 4 → LocAll2 → Recharge Ink
223 case 5: columnKey = 'c9'; break; // Dept 5 → LocAll9 → Other/Paper
224 case 6:
225 case 7:
226 case 9:
227 case 11:
228 case 12:
229 case 15:
230 case 16:
231 columnKey = 'c11'; break; // → LocAll11 → Hardware & Software
232 case 8: columnKey = 'c7'; break; // Dept 8 → LocAll7 → New Toner
233 case 10: columnKey = 'c8'; break; // Dept 10 → LocAll8 → New Ink
234 case 13: columnKey = 'c9'; break; // Dept 13 → LocAll9 → Other
235 case 14: columnKey = 'c10'; break; // Dept 14 → LocAll10 → R&M
236 default:
237 columnKey = 'c13'; break; // → LocAll13 → Excluded
238 }
239
240 console.log('[processSalesCube] Mapped dept', depId, 'to column', columnKey, 'amount', amount);
241
242 if (columnKey) {
243 dailyData[dayOfMonth][columnKey] += amount;
244
245 // Handle Hardware/Software gross profit
246 if (columnKey === 'c11') {
247 dailyData[dayOfMonth].c12 += cost;
248 if (cost <= 0 || cost > amount) {
249 dailyData[dayOfMonth].c12_errors++;
250 }
251 }
252 }
253 });
254
255 // Calculate hardware gross profit (c11 - c12)
256 Object.keys(dailyData).forEach(day => {
257 const d = parseInt(day);
258 dailyData[d].c12 = dailyData[d].c11 - dailyData[d].c12;
259 });
260
261 // Calculate column totals
262 const totals = Array(14).fill(0);
263 Object.values(dailyData).forEach(day => {
264 totals[1] += day.c1;
265 totals[2] += day.c2;
266 totals[3] += day.c3;
267 totals[4] += day.c4;
268 totals[5] += day.c5;
269 totals[6] += day.c6;
270 totals[7] += day.c7;
271 totals[8] += day.c8;
272 totals[9] += day.c9;
273 totals[10] += day.c10;
274 totals[11] += day.c11;
275 totals[12] += day.c12;
276 totals[13] += day.c13;
277 });
278
279 // Total sales
280 totals[0] = totals[1] + totals[2] + totals[3] + totals[4] + totals[5] +
281 totals[6] + totals[7] + totals[8] + totals[9] + totals[10] + totals[11];
282
283 setDailyTotals(dailyData);
284 setColumnTotals(totals);
285 };
286
287 const switchLocation = (tabIndex: number) => {
288 setCurrentTab(tabIndex);
289
290 // For POS users, "All Stores" should be the current store
291 const isPosUser = session?.store?.type !== 'management';
292
293 if (tabIndex === 0) {
294 if (isPosUser) {
295 // Set to current store for POS users
296 setCurrentLocId(Number(session?.store?.id) || 0);
297 setCurrentLocName(session?.store?.name || 'Current Store');
298 } else {
299 // Set to all stores for management users
300 setCurrentLocId(0);
301 setCurrentLocName('All Stores');
302 }
303 } else if (tabIndex - 1 < locations.length) {
304 const loc = locations[tabIndex - 1];
305 setCurrentLocId(loc.f100);
306 setCurrentLocName(loc.f101);
307 }
308
309 // Re-process with new location filter
310 if (salesCube?.APPD) {
311 processSalesCube(salesCube.APPD);
312 }
313 };
314
315 const getDaysInMonth = (month: number, year: number) => {
316 return new Date(year, month, 0).getDate();
317 };
318
319 const getDayName = (day: number, month: number, year: number) => {
320 const date = new Date(year, month - 1, day);
321 const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
322 return days[date.getDay()];
323 };
324
325 const isWeekend = (day: number, month: number, year: number) => {
326 const date = new Date(year, month - 1, day);
327 const dayOfWeek = date.getDay();
328 return dayOfWeek === 0 || dayOfWeek === 6;
329 };
330
331 const formatCurrency = (value: number) => {
332 return value.toFixed(2);
333 };
334
335 // Calculate totals for bottom section
336 const totalInk = columnTotals[2] + columnTotals[5] + columnTotals[6] + columnTotals[8];
337 const totalToner = columnTotals[1] + columnTotals[3] + columnTotals[4] + columnTotals[7];
338 const totalHardSoft = columnTotals[11];
339 const totalOther = columnTotals[9] + columnTotals[10];
340 const totalHSGP = columnTotals[12];
341 const grandTotal = columnTotals[0];
342
343 const franchiseFee = grandTotal * 0.06;
344 const franchiseFeeGST = franchiseFee * 0.10;
345 const franchiseFeeTotal = franchiseFee + franchiseFeeGST;
346
347 const adLevy = grandTotal * 0.03;
348 const adLevyGST = adLevy * 0.10;
349 const adLevyTotal = adLevy + adLevyGST;
350
351 const daysInMonth = getDaysInMonth(selectedMonth, selectedYear);
352
353 return (
354 <div className="p-6">
355 <style jsx>{`
356 @media print {
357 @page {
358 size: A4 landscape;
359 margin: 10mm;
360 }
361
362 body {
363 print-color-adjust: exact;
364 -webkit-print-color-adjust: exact;
365 }
366
367 * {
368 overflow: visible !important;
369 max-height: none !important;
370 height: auto !important;
371 }
372
373 .print-hide {
374 display: none !important;
375 }
376
377 .print-full-width {
378 width: 100% !important;
379 max-width: 100% !important;
380 box-shadow: none !important;
381 border-radius: 0 !important;
382 overflow: visible !important;
383 max-height: none !important;
384 height: auto !important;
385 }
386
387 .print-show {
388 display: block !important;
389 box-shadow: none !important;
390 border-radius: 0 !important;
391 margin-top: 10pt !important;
392 page-break-inside: avoid;
393 }
394
395 table {
396 font-size: 7pt !important;
397 width: 100% !important;
398 }
399
400 th, td {
401 padding: 1px 3px !important;
402 }
403
404 tbody tr {
405 page-break-inside: avoid;
406 }
407 }
408 `}</style>
409
410 {/* Header */}
411 <div className="mb-6">
412 <div className="mb-4">
413 <h1 className="text-2xl font-bold text-text">Franchise Report</h1>
414 <p className="text-sm text-muted">Report Version 1 Sep 2023</p>
415 </div>
416 <p className="text-sm text-danger font-semibold mb-4">
417 Please validate this report is correct before using results.
418 </p>
419 </div>
420
421 {/* Controls */}
422 <div className="bg-surface p-4 rounded-lg shadow border border-border mb-6 print-hide">
423 <div className="flex items-center gap-4">
424 <select
425 value={selectedMonth}
426 onChange={(e) => setSelectedMonth(Number(e.target.value))}
427 className="border border-border rounded px-3 py-2 bg-surface text-text focus:ring-2 focus:ring-brand"
428 >
429 {monthNames.map((name, idx) => (
430 <option key={idx} value={idx + 1}>{name}</option>
431 ))}
432 </select>
433
434 <input
435 type="number"
436 value={selectedYear}
437 onChange={(e) => setSelectedYear(Number(e.target.value))}
438 className="border border-border rounded px-3 py-2 w-24 bg-surface text-text focus:ring-2 focus:ring-brand"
439 />
440
441 <button
442 onClick={runReport}
443 disabled={loading}
444 className="bg-brand text-white px-4 py-2 rounded hover:opacity-90 disabled:opacity-50"
445 >
446 {loading ? 'Loading...' : 'Run'}
447 </button>
448
449 <button
450 onClick={() => window.print()}
451 className="bg-brand text-white px-4 py-2 rounded hover:opacity-90"
452 >
453 Save as PDF
454 </button>
455 </div>
456 </div>
457
458 {/* Location Tabs */}
459 <div className="mb-6 print-hide">
460 <div className="flex gap-2 flex-wrap border-b border-border">
461 <button
462 onClick={() => switchLocation(0)}
463 className={`px-4 py-2 ${
464 currentTab === 0
465 ? 'border-b-2 border-brand font-semibold text-brand'
466 : 'text-muted hover:text-text'
467 }`}
468 >
469 {session?.store?.type === 'management' ? 'All Stores' : 'Current Store'}
470 </button>
471 {locations.map((loc, idx) => (
472 <button
473 key={loc.f100}
474 onClick={() => switchLocation(idx + 1)}
475 className={`px-4 py-2 ${
476 currentTab === idx + 1
477 ? 'border-b-2 border-brand font-semibold text-brand'
478 : 'text-muted hover:text-text'
479 } ${loc.f180 === 1 ? 'text-sm' : ''}`}
480 >
481 {loc.f101}
482 </button>
483 ))}
484 </div>
485 </div>
486
487 {/* Main Table */}
488 <div className="bg-surface rounded-lg shadow overflow-auto border border-border mb-6 max-h-[600px] print-full-width">
489 <table className="w-full text-sm border-collapse">
490 <thead>
491 <tr className="bg-surface-2 border-b-2 border-border">
492 <th rowSpan={2} className="border border-border px-2 py-2 text-center align-bottom">
493 DAY
494 </th>
495 <th colSpan={2} className="border border-border px-2 py-1 text-center">
496 Recharge
497 </th>
498 <th colSpan={4} className="border border-border px-2 py-1 text-center">
499 Compatible
500 </th>
501 <th colSpan={2} className="border border-border px-2 py-1 text-center">
502 New
503 </th>
504 <th colSpan={2} className="border border-border px-2 py-1 text-center">
505 &nbsp;
506 </th>
507 <th rowSpan={2} className="border border-border px-2 py-2 text-center align-bottom">
508 Hardware &amp;<br />Software
509 </th>
510 <th rowSpan={2} className="border border-border px-2 py-2 text-center align-bottom">
511 H&amp;S<br />Gross Profit
512 </th>
513 <th rowSpan={2} className="border border-border px-2 py-2 text-center align-bottom">
514 Excluded<br />Sales
515 </th>
516 <th rowSpan={2} className="border border-border px-2 py-2 text-center align-bottom">
517 Total Sales
518 </th>
519 </tr>
520 <tr className="bg-surface-2 border-b-2 border-border">
521 <th className="border border-border px-2 py-1 text-center">Toner</th>
522 <th className="border border-border px-2 py-1 text-center">Ink</th>
523 <th className="border border-border px-2 py-1 text-center">CW Toner</th>
524 <th className="border border-border px-2 py-1 text-center">Other Toner</th>
525 <th className="border border-border px-2 py-1 text-center">CW Inks</th>
526 <th className="border border-border px-2 py-1 text-center">Other Inks</th>
527 <th className="border border-border px-2 py-1 text-center">Toner</th>
528 <th className="border border-border px-2 py-1 text-center">Ink</th>
529 <th className="border border-border px-2 py-1 text-center">Other</th>
530 <th className="border border-border px-2 py-1 text-center">R&amp;M</th>
531 </tr>
532 </thead>
533 <tbody>
534 {Array.from({ length: daysInMonth }, (_, i) => i + 1).map(day => {
535 const dayData = dailyTotals[day] || {
536 c1: 0, c2: 0, c3: 0, c4: 0, c5: 0, c6: 0, c7: 0, c8: 0,
537 c9: 0, c10: 0, c11: 0, c12: 0, c13: 0, c12_errors: 0
538 };
539 const weekend = isWeekend(day, selectedMonth, selectedYear);
540 const dayName = getDayName(day, selectedMonth, selectedYear);
541 const totalForDay = dayData.c1 + dayData.c2 + dayData.c3 + dayData.c4 +
542 dayData.c5 + dayData.c6 + dayData.c7 + dayData.c8 +
543 dayData.c9 + dayData.c10 + dayData.c11;
544
545 return (
546 <tr key={day} className={weekend ? 'bg-surface-2/50' : 'bg-surface'}>
547 <td className="border border-border px-2 py-1 text-right text-text">
548 {dayName} {day}
549 </td>
550 <td className="border border-border px-2 py-1 text-right text-text">{formatCurrency(dayData.c1)}</td>
551 <td className="border border-border px-2 py-1 text-right text-text">{formatCurrency(dayData.c2)}</td>
552 <td className="border border-border px-2 py-1 text-right text-text">{formatCurrency(dayData.c3)}</td>
553 <td className="border border-border px-2 py-1 text-right text-text">{formatCurrency(dayData.c4)}</td>
554 <td className="border border-border px-2 py-1 text-right text-text">{formatCurrency(dayData.c5)}</td>
555 <td className="border border-border px-2 py-1 text-right text-text">{formatCurrency(dayData.c6)}</td>
556 <td className="border border-border px-2 py-1 text-right text-text">{formatCurrency(dayData.c7)}</td>
557 <td className="border border-border px-2 py-1 text-right text-text">{formatCurrency(dayData.c8)}</td>
558 <td className="border border-border px-2 py-1 text-right text-text">{formatCurrency(dayData.c9)}</td>
559 <td className="border border-border px-2 py-1 text-right text-text">{formatCurrency(dayData.c10)}</td>
560 <td className="border border-border px-2 py-1 text-right text-text">{formatCurrency(dayData.c11)}</td>
561 <td
562 className={`border border-border px-2 py-1 text-right text-text ${
563 dayData.c12_errors > 0 ? 'bg-danger/20' : ''
564 }`}
565 >
566 {formatCurrency(dayData.c12)}
567 </td>
568 <td className="border border-border px-2 py-1 text-right text-text">{formatCurrency(dayData.c13)}</td>
569 <td className="border border-border px-2 py-1 text-right font-semibold text-text">
570 {formatCurrency(totalForDay)}
571 </td>
572 </tr>
573 );
574 })}
575
576 {/* Totals Row */}
577 <tr className="bg-surface-2 font-bold border-t-2 border-border">
578 <td className="border border-border px-2 py-2 text-right text-text">Total</td>
579 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(columnTotals[1])}</td>
580 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(columnTotals[2])}</td>
581 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(columnTotals[3])}</td>
582 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(columnTotals[4])}</td>
583 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(columnTotals[5])}</td>
584 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(columnTotals[6])}</td>
585 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(columnTotals[7])}</td>
586 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(columnTotals[8])}</td>
587 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(columnTotals[9])}</td>
588 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(columnTotals[10])}</td>
589 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(columnTotals[11])}</td>
590 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(columnTotals[12])}</td>
591 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(columnTotals[13])}</td>
592 <td className="border border-border px-2 py-2 text-right text-text">{formatCurrency(grandTotal)}</td>
593 </tr>
594 </tbody>
595 </table>
596 </div>
597
598 {/* Summary Calculations */}
599 <div className="bg-surface p-6 rounded-lg shadow border border-border print-show">
600 <table className="w-full max-w-2xl">
601 <tbody>
602 <tr>
603 <td className="py-2">Total Ink Sales</td>
604 <td></td>
605 <td className="text-right font-semibold">{formatCurrency(totalInk)}</td>
606 </tr>
607 <tr>
608 <td className="py-2">Total Toner Sales</td>
609 <td></td>
610 <td className="text-right font-semibold">{formatCurrency(totalToner)}</td>
611 </tr>
612 <tr>
613 <td className="py-2">Total Hard/Software Sales</td>
614 <td></td>
615 <td className="text-right font-semibold italic">{formatCurrency(totalHardSoft)}</td>
616 </tr>
617 <tr>
618 <td className="py-2">Total Other and R&amp;M Sales</td>
619 <td></td>
620 <td className="text-right font-semibold">{formatCurrency(totalOther)}</td>
621 </tr>
622 <tr>
623 <td className="py-2">Total H&amp;S Gross Profit</td>
624 <td></td>
625 <td className="text-right font-semibold">{formatCurrency(totalHSGP)}</td>
626 </tr>
627 <tr>
628 <td className="py-2"></td>
629 <td className="bg-surface-2 px-3 text-text">Total Excluding GST</td>
630 <td className="bg-surface-2 text-right font-bold px-3 text-text">{formatCurrency(grandTotal)}</td>
631 </tr>
632
633 <tr>
634 <td colSpan={3} className="py-4"><hr /></td>
635 </tr>
636
637 <tr>
638 <td className="py-2">Franchise Fee</td>
639 <td className="bg-surface-2 px-3 text-text">{formatCurrency(grandTotal)} x 6%</td>
640 <td className="text-right font-semibold">{formatCurrency(franchiseFee)}</td>
641 </tr>
642 <tr>
643 <td></td>
644 <td className="px-3">Plus GST 10%</td>
645 <td className="text-right font-semibold">{formatCurrency(franchiseFeeGST)}</td>
646 </tr>
647 <tr>
648 <td></td>
649 <td className="px-3">Total Payable</td>
650 <td className="text-right font-bold text-lg">$ {formatCurrency(franchiseFeeTotal)}</td>
651 </tr>
652
653 <tr>
654 <td colSpan={3} className="py-4"><hr /></td>
655 </tr>
656
657 <tr>
658 <td className="py-2">Advertising Levy</td>
659 <td className="bg-surface-2 px-3 text-text">{formatCurrency(grandTotal)} x 3%</td>
660 <td className="text-right font-semibold">{formatCurrency(adLevy)}</td>
661 </tr>
662 <tr>
663 <td></td>
664 <td className="px-3">Plus GST 10%</td>
665 <td className="text-right font-semibold">{formatCurrency(adLevyGST)}</td>
666 </tr>
667 <tr>
668 <td></td>
669 <td className="px-3">Total Payable</td>
670 <td className="text-right font-bold text-lg">$ {formatCurrency(adLevyTotal)}</td>
671 </tr>
672 </tbody>
673 </table>
674 </div>
675
676 {/* Print styles */}
677 <style jsx global>{`
678 @media print {
679 body {
680 print-color-adjust: exact;
681 -webkit-print-color-adjust: exact;
682 }
683 .noprint {
684 display: none !important;
685 }
686 }
687 `}</style>
688 </div>
689 );
690}