3import { useState, useEffect } from 'react';
4import { apiClient } from '@/lib/client/apiClient';
5import { Icon } from '@/contexts/IconContext';
10 StorePerformanceChart,
11 TellerPerformanceChart,
14} from '@/components/Charts';
19 f150?: number; // Number of sales
20 f211?: number; // Receipt total
30interface ReportOption {
37export default function SalesReportsPage() {
38 const [todayReceipts, setTodayReceipts] = useState(0);
39 const [storeStats, setStoreStats] = useState<TodayStats[]>([]);
40 const [topProducts, setTopProducts] = useState<TopProduct[]>([]);
41 const [loading, setLoading] = useState(true);
42 const [hourlyData, setHourlyData] = useState<any[]>([]);
43 const [paymentData, setPaymentData] = useState<any[]>([]);
44 const [dailyData, setDailyData] = useState<any[]>([]);
45 const [tellerData, setTellerData] = useState<any[]>([]);
47 const reportOptions: ReportOption[] = [
50 description: 'Sales currently on layby',
51 icon: 'shopping_cart',
52 link: '/pages/reports/sales-reports/layby'
56 description: 'Orders that did not ship',
57 icon: 'trending_down',
58 link: '/pages/reports/sales-reports/lost-sales'
61 title: 'Edited Sales',
62 description: 'Sales edited after completion',
64 link: '/pages/reports/sales-reports/edited-sales'
67 title: 'Sales by Store',
68 description: 'Sales totals per location',
70 link: '/pages/reports/sales-reports/sales-by-store'
73 title: 'Sales by Month',
74 description: 'Sales per location by month',
75 icon: 'calendar_month',
76 link: '/pages/reports/sales-reports/sales-by-month'
79 title: 'Open Quotations',
80 description: 'Open quotes requiring action',
82 link: '/pages/reports/sales-reports/open-quotations'
85 title: 'Sales by Teller',
86 description: 'Sales completed by staff members',
88 link: '/pages/reports/sales-reports/sales-by-teller'
91 title: 'Sales by Customer',
92 description: 'Sales grouped by customer',
94 link: '/pages/reports/sales-reports/sales-by-customer'
97 title: 'Sales by Account',
98 description: 'Sales grouped by account',
99 icon: 'account_balance',
100 link: '/pages/reports/sales-reports/sales-by-account'
104 description: 'Sales by customer city, postcode, keywords, referral tags, and more',
106 link: '/pages/reports/sales-reports/sales-by-x'
109 title: 'Basket Sizes',
110 description: 'Breakdown of items per sale',
111 icon: 'shopping_bag',
112 link: '/pages/reports/sales-reports/basket-sizes'
115 title: 'Sales with Discounts',
116 description: 'List of sales with a discount applied',
118 link: '/pages/reports/sales-reports/sales-with-discounts'
121 title: 'Sale Items Review',
122 description: 'Individual saleline details',
124 link: '/pages/reports/sales-reports/sale-items-review'
127 title: 'Sale Header Review',
128 description: 'Single line per sale',
130 link: '/pages/reports/sales-reports/sale-header-review'
133 title: 'Past Sales Finder',
134 description: 'Useful keyword search if specifics such as customer are unknown',
136 link: '/pages/reports/sales-reports/past-sales-finder'
139 title: 'Payment List',
140 description: 'List of individual payment transactions',
142 link: '/pages/reports/sales-reports/payment-list'
146 description: 'Key performance indicators by store',
148 link: '/pages/reports/sales-reports/sales-kpi'
156 const loadAllData = async () => {
160 // Load all data in parallel
161 await Promise.allSettled([
170 console.error('Error loading sales report data:', error);
176 const loadTodayStats = async () => {
178 // Get today's date range
179 const today = new Date();
180 const startOfDay = new Date(today.setHours(0, 0, 0, 0));
181 const endOfDay = new Date(today.setHours(23, 59, 59, 999));
183 // Fetch store performance from dedicated endpoint
184 const params = new URLSearchParams({
185 startDate: startOfDay.toISOString().split('T')[0],
186 endDate: endOfDay.toISOString().split('T')[0]
189 const response = await fetch(`/api/v1/stats/store-performance?${params}`);
191 console.warn('[Store Performance] API call failed');
195 const data = await response.json();
196 const stores = data.APPD || [];
198 if (stores.length === 0) {
199 console.warn('[Store Performance] No store data available');
203 // Calculate total revenue and map store data to the format expected by the page
204 const totalRevenue = stores.reduce((sum: number, item: any) =>
205 sum + (parseFloat(item.f120) || 0), 0);
206 setTodayReceipts(totalRevenue);
208 const storeArray: TodayStats[] = stores
209 .map((item: any) => ({
210 f101: item.f2000 || 'Unknown Store', // Location name
211 f150: parseInt(item.f107) || 0, // Number of sales
212 f211: parseFloat(item.f120) || 0 // Sales amount
214 .filter((s: any) => s.f211 > 0)
215 .sort((a: any, b: any) => b.f211 - a.f211);
217 if (storeArray.length > 0) {
218 console.log('[Store Performance] Loaded', storeArray.length, 'stores, total revenue:', totalRevenue);
219 setStoreStats(storeArray);
222 console.error('Error loading today stats:', error);
226 const loadTopProducts = async () => {
228 // Get today's date range
229 const today = new Date();
230 const startOfDay = new Date(today.setHours(0, 0, 0, 0));
231 const endOfDay = new Date(today.setHours(23, 59, 59, 999));
233 // Fetch top products from dedicated endpoint
234 const params = new URLSearchParams({
235 startDate: startOfDay.toISOString().split('T')[0],
236 endDate: endOfDay.toISOString().split('T')[0]
239 const response = await fetch(`/api/v1/stats/top-products?${params}`);
241 console.warn('[Top Products] API call failed');
246 const data = await response.json();
247 const products = data.APPD || [];
249 if (products.length === 0) {
250 console.warn('[Top Products] No product data available');
255 // Map product data to the format expected by the page
256 const totalRevenue = products.reduce((sum: number, item: any) =>
257 sum + (parseFloat(item.f120) || 0), 0);
259 const productArray: TopProduct[] = products
260 .map((item: any) => {
261 const revenue = parseFloat(item.f120) || 0;
262 const percentage = totalRevenue > 0 ? (revenue / totalRevenue) * 100 : 0;
265 pid: String(item.f1000) || '0', // Product ID
266 name: (item.f2000 || 'Unknown Product').substring(0, 30), // Product name
267 value: Math.round(revenue), // Revenue
268 percentage: Math.max(percentage, 2)
271 .filter((p: any) => p.value > 0)
272 .sort((a: any, b: any) => b.value - a.value)
273 .slice(0, 10); // Top 10 products
275 if (productArray.length > 0) {
276 console.log('[Top Products] Loaded', productArray.length, 'products');
277 setTopProducts(productArray);
282 console.error('Error loading top products:', error);
287 const loadHourlyStats = async () => {
289 // Get today's date range
290 const today = new Date();
291 const startOfDay = new Date(today.setHours(0, 0, 0, 0));
292 const endOfDay = new Date(today.setHours(23, 59, 59, 999));
294 // Fetch individual sales from dedicated endpoint
295 const params = new URLSearchParams({
296 startDate: startOfDay.toISOString().split('T')[0],
297 endDate: endOfDay.toISOString().split('T')[0]
300 const response = await fetch(`/api/v1/stats/hourly-sales?${params}`);
302 console.warn('[Hourly Stats] API call failed');
307 const data = await response.json();
308 const sales = data.APPD || [];
310 if (sales.length === 0) {
311 console.warn('[Hourly Stats] No sales data available');
316 // Group sales by hour
317 const hourCounts: { [key: number]: { count: number; total: number } } = {};
319 sales.forEach((item: any) => {
320 // Extract timestamp from sale record
321 const timestamp = item.f131 || item.f132 || item.CompletedDt;
324 const date = new Date(timestamp);
325 const hour = date.getHours();
327 if (!hourCounts[hour]) {
328 hourCounts[hour] = { count: 0, total: 0 };
330 hourCounts[hour].count += 1;
331 hourCounts[hour].total += parseFloat(item.f120 || item.f110 || 0);
333 // Skip invalid dates
338 // Convert to array with proper hour labels, sorted by hour
339 const hourlyArray = Object.entries(hourCounts)
340 .map(([hourStr, data]) => {
341 const hour = parseInt(hourStr);
342 let hourLabel: string;
343 if (hour === 0) hourLabel = '12AM';
344 else if (hour < 12) hourLabel = `${hour}AM`;
345 else if (hour === 12) hourLabel = '12PM';
346 else hourLabel = `${hour - 12}PM`;
351 amount: Math.round(data.total)
355 // Sort by actual hour number
356 const getHour = (label: string) => {
357 const isPM = label.includes('PM');
358 const num = parseInt(label);
359 if (num === 12) return isPM ? 12 : 0;
360 return isPM ? num + 12 : num;
362 return getHour(a.hour) - getHour(b.hour);
365 if (hourlyArray.length > 0) {
366 console.log('[Hourly Stats] Loaded', hourlyArray.length, 'hours');
367 setHourlyData(hourlyArray);
372 console.error('Error loading hourly stats:', error);
377 const loadPaymentStats = async () => {
379 // Fetch payment statistics from dedicated endpoint
380 const response = await fetch('/api/v1/stats/today-payment');
382 console.warn('[Payment Stats] API call failed');
387 const result = await response.json();
388 if (!result.success || !result.data || result.data.length === 0) {
389 console.warn('[Payment Stats] No payment data available');
394 // Aggregate payment methods across all stores
395 const paymentTotals: { [key: string]: number } = {};
398 // Process each store's payment data
399 result.data.forEach((store: any) => {
400 // f300-f330 contain amounts, f500-f530 contain payment method names
401 for (let i = 0; i <= 30; i++) {
402 const amountField = `f${300 + i}`;
403 const nameField = `f${500 + i}`;
404 const amount = parseFloat(store[amountField]) || 0;
405 const name = store[nameField];
407 if (amount > 0 && name && name !== 'Unknown' && name !== 'Other') {
408 if (!paymentTotals[name]) {
409 paymentTotals[name] = 0;
411 paymentTotals[name] += amount;
412 grandTotal += amount;
417 // Convert to array with percentages and sort by amount
418 const paymentArray = Object.entries(paymentTotals)
419 .map(([method, amount]) => ({
421 amount: Math.round(amount * 100) / 100,
422 percentage: grandTotal > 0 ? Math.round((amount / grandTotal) * 100) : 0
424 .sort((a, b) => b.amount - a.amount)
425 .slice(0, 10); // Top 10 payment methods
427 if (paymentArray.length > 0) {
428 setPaymentData(paymentArray);
433 console.error('Error loading payment stats:', error);
438 const loadDailyStats = async () => {
440 // Get date range for last 7 days to get full week data
441 const today = new Date();
442 const weekAgo = new Date(today);
443 weekAgo.setDate(weekAgo.getDate() - 7);
445 // Fetch day-of-week sales from dedicated endpoint
446 const params = new URLSearchParams({
447 startDate: weekAgo.toISOString().split('T')[0],
448 endDate: today.toISOString().split('T')[0]
451 const response = await fetch(`/api/v1/stats/daily-sales?${params}`);
453 console.warn('[Daily Stats] API call failed');
458 const data = await response.json();
459 const days = data.APPD || [];
461 if (days.length === 0) {
462 console.warn('[Daily Stats] No daily data available');
467 // Map day-of-week data to chart format
468 // sale.cube with dispmode=dow returns data with day indices (0=Sun, 1=Mon, etc.)
469 const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
470 const dayMap: { [key: string]: { count: number; total: number } } = {
471 'Mon': { count: 0, total: 0 },
472 'Tue': { count: 0, total: 0 },
473 'Wed': { count: 0, total: 0 },
474 'Thu': { count: 0, total: 0 },
475 'Fri': { count: 0, total: 0 },
476 'Sat': { count: 0, total: 0 },
477 'Sun': { count: 0, total: 0 }
480 days.forEach((item: any) => {
481 // f2000 or similar field might contain day index or name
482 const dayIndex = item.f100 || item.DayOfWeek;
483 if (dayIndex !== undefined && dayIndex >= 0 && dayIndex < 7) {
484 const dayName = dayNames[dayIndex];
485 dayMap[dayName].count += parseInt(item.f107) || 0;
486 dayMap[dayName].total += parseFloat(item.f120) || 0;
490 // Convert to array in week order (Mon-Sun)
491 const weekDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
492 const dailyArray = weekDays.map(day => ({
494 sales: dayMap[day].count,
495 amount: Math.round(dayMap[day].total)
498 const totalSales = dailyArray.reduce((sum, day) => sum + day.sales, 0);
499 if (totalSales > 0) {
500 console.log('[Daily Stats] Loaded week data, total sales:', totalSales);
501 setDailyData(dailyArray);
506 console.error('Error loading daily stats:', error);
511 const loadTellerStats = async () => {
513 // Get today's date range
514 const today = new Date();
515 const startOfDay = new Date(today.setHours(0, 0, 0, 0));
516 const endOfDay = new Date(today.setHours(23, 59, 59, 999));
518 // Fetch teller performance from dedicated endpoint
519 const params = new URLSearchParams({
520 startDate: startOfDay.toISOString().split('T')[0],
521 endDate: endOfDay.toISOString().split('T')[0]
524 const response = await fetch(`/api/v1/stats/teller-performance?${params}`);
526 console.warn('[Teller Stats] API call failed');
531 const result = await response.json();
532 if (!result.success || !result.data || result.data.length === 0) {
533 console.warn('[Teller Stats] No teller data available');
538 // Map teller data to chart format
539 const tellerArray = result.data
540 .map((item: any) => ({
541 teller: item.f106 || 'Unknown', // Teller name
542 sales: parseInt(item.f107) || 0, // Number of sales
543 amount: Math.round(parseFloat(item.f120) || 0) // Sales amount
545 .filter((t: any) => t.teller !== 'Unknown' && t.amount > 0)
546 .sort((a: any, b: any) => b.amount - a.amount)
547 .slice(0, 10); // Top 10 tellers
549 setTellerData(tellerArray);
551 console.error('Error loading teller stats:', error);
556 const formatCurrency = (value: number) => {
557 return new Intl.NumberFormat('en-NZ', {
560 minimumFractionDigits: 0,
561 maximumFractionDigits: 0
566 <div className="p-6">
568 <div className="mb-6">
569 <h1 className="text-3xl font-bold mb-2 flex items-center gap-2">
570 Sales Reports <Icon name="assessment" size={32} className="text-brand" />
572 <p className="text-muted">Sales analysis and reporting</p>
575 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
576 {/* Sales Today Summary */}
577 <div className="relative overflow-hidden bg-gradient-to-br from-success/10 via-surface to-surface rounded-xl shadow-lg border border-success/20 p-6 hover:shadow-xl transition-shadow duration-300">
578 <div className="absolute top-0 right-0 w-32 h-32 bg-success/5 rounded-full -mr-16 -mt-16"></div>
579 <div className="absolute bottom-0 left-0 w-24 h-24 bg-success/5 rounded-full -ml-12 -mb-12"></div>
580 <div className="relative z-10">
581 <div className="flex items-center gap-2 mb-4">
582 <Icon name="trending_up" size={28} className="text-success" />
583 <h2 className="text-xl font-semibold text-success">Sales Today</h2>
586 <div className="text-center py-4">
587 <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-success"></div>
591 <div className="mb-4">
592 <div className="text-sm text-muted mb-1">Total Receipts</div>
593 <div className="text-3xl font-bold text-success">
594 {formatCurrency(todayReceipts)}
598 {storeStats.length > 0 && (
599 <div className="space-y-2 max-h-40 overflow-y-auto">
600 {storeStats.slice(0, 5).map((store, idx) => (
601 <div key={idx} className="flex justify-between items-center text-sm bg-surface/50 rounded-lg p-2 border border-border/30">
602 <span className="text-text truncate mr-2 font-medium">{store.f101}</span>
603 <span className="font-semibold text-success">{formatCurrency(store.f211 || 0)}</span>
613 {/* Top 10 Products Today */}
614 <div className="relative overflow-hidden bg-gradient-to-br from-brand/10 via-surface to-surface rounded-xl shadow-lg border border-brand/20 p-6 lg:col-span-2 hover:shadow-xl transition-shadow duration-300">
615 <div className="absolute top-0 right-0 w-40 h-40 bg-brand/5 rounded-full -mr-20 -mt-20"></div>
616 <div className="absolute bottom-0 left-0 w-32 h-32 bg-brand/5 rounded-full -ml-16 -mb-16"></div>
617 <div className="relative z-10">
618 <div className="flex items-center gap-2 mb-4">
619 <Icon name="shopping_bag" size={28} className="text-brand" />
620 <h2 className="text-xl font-semibold text-brand">Top 10 Selling Today</h2>
622 {topProducts.length > 0 ? (
623 <div className="space-y-3">
624 {topProducts.map((product, idx) => (
625 <div key={idx} className="bg-surface/50 rounded-lg p-3 border border-border/30 hover:border-brand/50 transition-colors">
626 <div className="flex justify-between items-center text-sm mb-2">
627 <div className="flex items-center gap-2 flex-1 mr-4">
628 <span className="flex items-center justify-center w-6 h-6 rounded-full bg-brand/20 text-brand text-xs font-bold">
631 <span className="text-text truncate font-medium" title={product.name}>
635 <span className="text-brand font-semibold">
636 {product.percentage.toFixed(1)}%
639 <div className="w-full bg-surface-2 rounded-full h-2.5 overflow-hidden">
641 className="bg-gradient-to-r from-brand to-brand2 h-2.5 rounded-full transition-all duration-500 shadow-sm"
642 style={{ width: `${Math.min(product.percentage, 100)}%` }}
649 <div className="text-center text-muted py-8">
650 <Icon name="info" size={48} className="mx-auto mb-2 opacity-50" />
651 <p>No product data available</p>
658 {/* Report Options Grid */}
659 <div className="bg-surface rounded-xl shadow-sm border border-border p-6">
660 <h2 className="text-xl font-semibold mb-6 flex items-center gap-2">
661 <Icon name="folder_open" size={24} className="text-brand" />
664 <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
665 {reportOptions.map((option, idx) => (
669 className="group relative block bg-gradient-to-br from-surface to-surface-2 border border-border rounded-xl p-4 text-center 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"
670 title={option.description}
672 <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>
673 <div className="relative z-10">
674 <div className="mb-3 flex justify-center">
675 <div className="w-14 h-14 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">
676 <Icon name={option.icon} size={28} className="text-brand group-hover:scale-110 transition-transform duration-300" />
679 <div className="text-sm font-semibold text-text group-hover:text-brand transition-colors duration-200">
682 {option.description && (
683 <div className="text-xs text-muted mt-1 line-clamp-2">
693 {/* Widgets Section */}
694 <div className="mt-8">
695 <h2 className="text-xl font-semibold mb-4">Live Dashboards</h2>
696 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
698 {/* Sales Today by Hour */}
699 <div className="bg-surface rounded-lg shadow overflow-hidden">
700 <div className="p-3 bg-success text-surface font-semibold text-sm">
703 <div className="p-4 h-64">
704 <SalesHourlyChart data={hourlyData.length > 0 ? hourlyData : undefined} />
708 {/* Payment Methods Today */}
709 <div className="bg-surface rounded-lg shadow overflow-hidden">
710 <div className="p-3 bg-success text-surface font-semibold text-sm">
711 Payment Methods Today
713 <div className="p-4 h-64">
714 <PaymentMethodsChart data={paymentData.length > 0 ? paymentData : undefined} />
718 {/* Store Performance Today */}
719 <div className="bg-surface rounded-lg shadow overflow-hidden">
720 <div className="p-3 bg-success text-surface font-semibold text-sm">
721 Store Performance Today
723 <div className="p-4 h-64">
724 <StorePerformanceChart
725 data={storeStats.map(store => ({
726 store: store.f101 || 'Unknown',
727 sales: store.f150 || 0,
728 amount: store.f211 || 0
734 {/* Top Products Today */}
735 <div className="bg-surface rounded-lg shadow overflow-hidden">
736 <div className="p-3 bg-brand text-surface font-semibold text-sm">
739 <div className="p-4 h-64">
741 data={topProducts.slice(0, 5).map((product, idx) => ({
743 quantity: Math.floor(product.percentage / 3), // Convert percentage to quantity
744 value: product.value * Math.floor(product.percentage / 3)
750 {/* Top Tellers Today */}
751 <div className="bg-surface rounded-lg shadow overflow-hidden">
752 <div className="p-3 bg-brand text-surface font-semibold text-sm">
755 <div className="p-4 h-64">
756 <TellerPerformanceChart data={tellerData} />
760 {/* Top Customers Today */}
761 <div className="bg-surface rounded-lg shadow overflow-hidden">
762 <div className="p-3 bg-brand text-surface font-semibold text-sm">
765 <div className="p-4 h-64">
766 <TopCustomersChart />
772 {/* Sales Trend Analysis */}
773 <div className="mt-8">
774 <h2 className="text-xl font-semibold mb-4">Sales Trend Analysis</h2>
775 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
777 {/* Weekly Sales Trend */}
778 <div className="bg-surface rounded-lg shadow overflow-hidden">
779 <div className="p-3 bg-info text-surface font-semibold">
782 <div className="p-4 h-64">
785 data={dailyData.length > 0 ? dailyData : undefined}
790 {/* Recent Activity Widget */}
791 <div className="bg-surface rounded-lg shadow overflow-hidden">
792 <div className="p-3 bg-brand text-surface font-semibold">
793 Store Activity Summary
795 <div className="p-4 h-64">
796 <div className="space-y-3 max-h-56 overflow-y-auto">
797 {storeStats.length > 0 ? (
798 storeStats.map((store, idx) => (
799 <div key={idx} className="border-b pb-2 last:border-b-0">
800 <div className="flex justify-between items-start">
801 <div className="flex-1">
802 <div className="text-sm font-medium text-text">{store.f101}</div>
803 <div className="text-xs text-muted">
804 {store.f150} transactions
807 <div className="text-sm font-semibold text-success">
808 ${typeof store.f211 === 'number' ? store.f211.toFixed(2) : '0.00'}
814 <div className="text-center text-muted py-8">
815 No store data available
824 {/* Note about data availability */}
825 {(hourlyData.length === 0 && paymentData.length === 0 && dailyData.length === 0) && !loading && (
826 <div className="mt-6 bg-info/10 border-l-4 border-brand p-4">
827 <div className="flex">
828 <div className="flex-shrink-0">
829 <svg className="h-5 w-5 text-info" viewBox="0 0 20 20" fill="currentColor">
830 <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
833 <div className="ml-3">
834 <p className="text-sm text-info">
835 <strong>Note:</strong> Some charts may show empty data because there are no sales transactions for today yet.
836 Sales data will appear as transactions are processed throughout the day.
843 {/* Legacy demo data removed */}
844 <div className="hidden">
846 { id: '12345', amount: 89.99, time: '2:35 PM', item: 'Brother TN-2430 Toner', payment: 'EFTPOS' },
847 { id: '12344', amount: 45.99, time: '2:28 PM', item: 'HP 65 Black Ink', payment: 'Cash' },
848 { id: '12343', amount: 129.50, time: '2:15 PM', item: 'Canon Multipack', payment: 'EFTPOS' },
849 { id: '12342', amount: 38.99, time: '2:05 PM', item: 'Canon PG-545 Black', payment: 'Mobile Pay' },
850 { id: '12341', amount: 65.99, time: '1:58 PM', item: 'Samsung MLT-D111S', payment: 'EFTPOS' },
851 ].map((sale, idx) => (
852 <div key={idx} className="border-b pb-2 last:border-b-0">
853 <div className="flex justify-between items-start">
854 <div className="flex-1">
855 <div className="text-sm font-medium text-text">Sale #{sale.id}</div>
856 <div className="text-xs text-muted truncate">{sale.item}</div>
857 <div className="text-xs text-muted">
858 {sale.time} • {sale.payment}
861 <div className="text-sm font-semibold text-green-600">