pharmacy-erp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +34 -0
- package/LICENSE +21 -0
- package/README.md +479 -0
- package/backend/Dockerfile +44 -0
- package/backend/app/__init__.py +1 -0
- package/backend/app/core/__init__.py +1 -0
- package/backend/app/core/audit.py +33 -0
- package/backend/app/core/config.py +16 -0
- package/backend/app/core/database.py +26 -0
- package/backend/app/core/deps.py +49 -0
- package/backend/app/core/exceptions.py +43 -0
- package/backend/app/core/security.py +32 -0
- package/backend/app/main.py +54 -0
- package/backend/app/modules/__init__.py +0 -0
- package/backend/app/modules/admin/__init__.py +1 -0
- package/backend/app/modules/admin/models.py +15 -0
- package/backend/app/modules/admin/router.py +353 -0
- package/backend/app/modules/auth/__init__.py +0 -0
- package/backend/app/modules/auth/models.py +16 -0
- package/backend/app/modules/auth/router.py +73 -0
- package/backend/app/modules/auth/schemas.py +29 -0
- package/backend/app/modules/cash_sessions/__init__.py +0 -0
- package/backend/app/modules/cash_sessions/models.py +19 -0
- package/backend/app/modules/cash_sessions/router.py +85 -0
- package/backend/app/modules/cash_sessions/schemas.py +31 -0
- package/backend/app/modules/customers/__init__.py +0 -0
- package/backend/app/modules/customers/models.py +37 -0
- package/backend/app/modules/customers/router.py +296 -0
- package/backend/app/modules/customers/schemas.py +72 -0
- package/backend/app/modules/inventory/__init__.py +0 -0
- package/backend/app/modules/inventory/models.py +16 -0
- package/backend/app/modules/inventory/router.py +150 -0
- package/backend/app/modules/inventory/schemas.py +24 -0
- package/backend/app/modules/medicines/__init__.py +0 -0
- package/backend/app/modules/medicines/models.py +54 -0
- package/backend/app/modules/medicines/router.py +631 -0
- package/backend/app/modules/medicines/schemas.py +153 -0
- package/backend/app/modules/medicines/template_generator.py +118 -0
- package/backend/app/modules/notifications/__init__.py +1 -0
- package/backend/app/modules/notifications/models.py +15 -0
- package/backend/app/modules/notifications/router.py +89 -0
- package/backend/app/modules/notifications/schemas.py +22 -0
- package/backend/app/modules/organizations/__init__.py +0 -0
- package/backend/app/modules/organizations/models.py +16 -0
- package/backend/app/modules/organizations/router.py +49 -0
- package/backend/app/modules/organizations/schemas.py +31 -0
- package/backend/app/modules/reports/__init__.py +0 -0
- package/backend/app/modules/reports/router.py +409 -0
- package/backend/app/modules/roles/__init__.py +1 -0
- package/backend/app/modules/roles/models.py +43 -0
- package/backend/app/modules/roles/router.py +89 -0
- package/backend/app/modules/roles/schemas.py +55 -0
- package/backend/app/modules/sales/__init__.py +0 -0
- package/backend/app/modules/sales/models.py +44 -0
- package/backend/app/modules/sales/router.py +63 -0
- package/backend/app/modules/sales/schemas.py +42 -0
- package/backend/app/modules/sales/service.py +128 -0
- package/backend/app/modules/suppliers/__init__.py +0 -0
- package/backend/app/modules/suppliers/models.py +14 -0
- package/backend/app/modules/suppliers/router.py +56 -0
- package/backend/app/modules/suppliers/schemas.py +27 -0
- package/backend/app/modules/users/__init__.py +0 -0
- package/backend/app/modules/users/router.py +74 -0
- package/backend/app/seed.py +275 -0
- package/backend/data/.gitkeep +1 -0
- package/backend/requirements.txt +17 -0
- package/bin/cli.js +288 -0
- package/docker-compose.dev.yml +42 -0
- package/docker-compose.yml +59 -0
- package/frontend/Dockerfile +56 -0
- package/frontend/next.config.ts +19 -0
- package/frontend/package.json +60 -0
- package/frontend/pnpm-lock.yaml +5678 -0
- package/frontend/pnpm-workspace.yaml +6 -0
- package/frontend/postcss.config.mjs +6 -0
- package/frontend/public/logo.png +0 -0
- package/frontend/src/app/[locale]/(app)/admin/page.tsx +1426 -0
- package/frontend/src/app/[locale]/(app)/catalog/products/[id]/page.tsx +505 -0
- package/frontend/src/app/[locale]/(app)/catalog/products/page.tsx +753 -0
- package/frontend/src/app/[locale]/(app)/customers/[id]/page.tsx +500 -0
- package/frontend/src/app/[locale]/(app)/customers/page.tsx +538 -0
- package/frontend/src/app/[locale]/(app)/dashboard/page.tsx +175 -0
- package/frontend/src/app/[locale]/(app)/inventory/page.tsx +765 -0
- package/frontend/src/app/[locale]/(app)/layout.tsx +60 -0
- package/frontend/src/app/[locale]/(app)/purchasing/page.tsx +1 -0
- package/frontend/src/app/[locale]/(app)/reports/page.tsx +794 -0
- package/frontend/src/app/[locale]/(app)/sales/page.tsx +296 -0
- package/frontend/src/app/[locale]/(auth)/login/page.tsx +100 -0
- package/frontend/src/app/[locale]/(pos)/layout.tsx +33 -0
- package/frontend/src/app/[locale]/(pos)/pos/page.tsx +463 -0
- package/frontend/src/app/[locale]/(pos)/pos/pos-data.ts +89 -0
- package/frontend/src/app/[locale]/layout.tsx +30 -0
- package/frontend/src/app/[locale]/page.tsx +6 -0
- package/frontend/src/app/globals.css +77 -0
- package/frontend/src/app/layout.tsx +23 -0
- package/frontend/src/app/print.css +18 -0
- package/frontend/src/components/pos/CartPanel.tsx +525 -0
- package/frontend/src/components/pos/CashSessionGuard.tsx +255 -0
- package/frontend/src/components/pos/CloseSessionModal.tsx +160 -0
- package/frontend/src/components/pos/PaymentModal.tsx +266 -0
- package/frontend/src/components/pos/PaymentSettingsModal.tsx +472 -0
- package/frontend/src/components/pos/ProductCatalog.tsx +140 -0
- package/frontend/src/components/pos/SaleReceipt.tsx +133 -0
- package/frontend/src/components/ui/badge.tsx +28 -0
- package/frontend/src/components/ui/bulk-import-modal.tsx +400 -0
- package/frontend/src/components/ui/button.tsx +60 -0
- package/frontend/src/components/ui/card.tsx +13 -0
- package/frontend/src/components/ui/confirm-dialog.tsx +50 -0
- package/frontend/src/components/ui/input.tsx +32 -0
- package/frontend/src/components/ui/notification-center.tsx +228 -0
- package/frontend/src/components/ui/select.tsx +59 -0
- package/frontend/src/components/ui/sidebar.tsx +91 -0
- package/frontend/src/components/ui/slide-over.tsx +43 -0
- package/frontend/src/components/ui/stat-card.tsx +40 -0
- package/frontend/src/components/ui/switch.tsx +38 -0
- package/frontend/src/components/ui/topbar.tsx +97 -0
- package/frontend/src/i18n/request.ts +13 -0
- package/frontend/src/i18n/routing.ts +9 -0
- package/frontend/src/lib/api.ts +240 -0
- package/frontend/src/lib/constants.ts +30 -0
- package/frontend/src/lib/utils.ts +32 -0
- package/frontend/src/messages/ar.json +106 -0
- package/frontend/src/messages/en.json +106 -0
- package/frontend/src/middleware.ts +8 -0
- package/frontend/src/store/app-store.ts +37 -0
- package/frontend/src/store/auth-store.ts +52 -0
- package/frontend/src/store/payment-methods-store.ts +144 -0
- package/frontend/src/store/pos-store.ts +344 -0
- package/frontend/tailwind.config.ts +85 -0
- package/frontend/tsconfig.json +27 -0
- package/img/backgraund-logo.png +0 -0
- package/img/full-logo.png +0 -0
- package/img/logo.png +0 -0
- package/nginx/nginx.conf +67 -0
- package/package.json +88 -0
- package/start.bat +90 -0
- package/start.sh +64 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { api } from '@/lib/api';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import { Select as CustomSelect } from '@/components/ui/select';
|
|
7
|
+
import { toast } from 'react-hot-toast';
|
|
8
|
+
import {
|
|
9
|
+
BarChart3, TrendingUp, Package, Users, Truck, Pill, Download,
|
|
10
|
+
Calendar, DollarSign, ArrowUpRight, ArrowDownRight, FileText,
|
|
11
|
+
FileSpreadsheet, DownloadCloud, AlertTriangle, Activity
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
import { cn } from '@/lib/utils';
|
|
14
|
+
import {
|
|
15
|
+
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer,
|
|
16
|
+
BarChart, Bar, PieChart, Pie, Cell, Legend
|
|
17
|
+
} from 'recharts';
|
|
18
|
+
import jsPDF from 'jspdf';
|
|
19
|
+
import autoTable from 'jspdf-autotable';
|
|
20
|
+
import { useAppStore } from '@/store/app-store';
|
|
21
|
+
|
|
22
|
+
// ─── Tab Configuration ──────────────────────────────────────────────────
|
|
23
|
+
const TABS = [
|
|
24
|
+
{ id: 'sales', label: 'Sales Reports', icon: TrendingUp, description: 'Revenue and sales trends' },
|
|
25
|
+
{ id: 'inventory', label: 'Inventory Reports', icon: Package, description: 'Stock valuation and expiry' },
|
|
26
|
+
{ id: 'financial', label: 'Financial Reports', icon: DollarSign, description: 'Profit margins and cash flow' },
|
|
27
|
+
{ id: 'customer', label: 'Customer Reports', icon: Users, description: 'Balances and purchase history' },
|
|
28
|
+
{ id: 'supplier', label: 'Supplier Reports', icon: Truck, description: 'Purchases and orders' },
|
|
29
|
+
{ id: 'medicine', label: 'Medicine Reports', icon: Pill, description: 'Fast moving and dead stock' },
|
|
30
|
+
{ id: 'export', label: 'Export Center', icon: DownloadCloud, description: 'Download CSV and PDF reports' },
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
type TabId = typeof TABS[number]['id'];
|
|
34
|
+
|
|
35
|
+
// ─── Chart Colors ───────────────────────────────────────────────────────
|
|
36
|
+
const COLORS = ['#8b5cf6', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#6366f1', '#ec4899'];
|
|
37
|
+
|
|
38
|
+
export default function ReportsPage() {
|
|
39
|
+
const [activeTab, setActiveTab] = useState<TabId>('sales');
|
|
40
|
+
const { currency } = useAppStore();
|
|
41
|
+
|
|
42
|
+
// ── Data State ──
|
|
43
|
+
const [salesData, setSalesData] = useState<any[]>([]);
|
|
44
|
+
const [topProducts, setTopProducts] = useState<any[]>([]);
|
|
45
|
+
const [stockValue, setStockValue] = useState(0);
|
|
46
|
+
const [expirySummary, setExpirySummary] = useState<any>({});
|
|
47
|
+
const [profitData, setProfitData] = useState<any>({ revenue: 0, cogs: 0, profit: 0 });
|
|
48
|
+
const [customerBalances, setCustomerBalances] = useState<any[]>([]);
|
|
49
|
+
const [loading, setLoading] = useState(true);
|
|
50
|
+
const [salesKPI, setSalesKPI] = useState({ revenue: 0, orders: 0, avgOrder: 0 });
|
|
51
|
+
const [salesChartData, setSalesChartData] = useState<{name: string; revenue: number; orders: number}[]>([]);
|
|
52
|
+
const [paymentBreakdown, setPaymentBreakdown] = useState<any>({ total_revenue: 0, methods: [] });
|
|
53
|
+
const [methodFilter, setMethodFilter] = useState<string>('all');
|
|
54
|
+
const [medicineAnalysis, setMedicineAnalysis] = useState<any>({ fast_moving: [], slow_moving: [], dead_stock: [] });
|
|
55
|
+
|
|
56
|
+
// ── Filters ──
|
|
57
|
+
const [dateRange, setDateRange] = useState('7days'); // 7days, 30days, year
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
fetchData();
|
|
61
|
+
}, [activeTab, dateRange]);
|
|
62
|
+
|
|
63
|
+
const fetchData = async () => {
|
|
64
|
+
setLoading(true);
|
|
65
|
+
try {
|
|
66
|
+
const end = new Date();
|
|
67
|
+
const start = new Date();
|
|
68
|
+
start.setDate(end.getDate() - (dateRange === '7days' ? 7 : dateRange === '30days' ? 30 : 365));
|
|
69
|
+
const startStr = start.toISOString().split('T')[0];
|
|
70
|
+
const endStr = end.toISOString().split('T')[0];
|
|
71
|
+
|
|
72
|
+
if (activeTab === 'sales') {
|
|
73
|
+
const [rangeData, top] = await Promise.all([
|
|
74
|
+
api.getSalesRange(startStr, endStr).catch(() => null),
|
|
75
|
+
api.getTopProducts(5).catch(() => [])
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
setTopProducts(Array.isArray(top) ? top : []);
|
|
79
|
+
|
|
80
|
+
if (rangeData) {
|
|
81
|
+
const totalRevenue = (rangeData as any).total_revenue || 0;
|
|
82
|
+
const totalOrders = (rangeData as any).total_sales || 0;
|
|
83
|
+
setSalesKPI({
|
|
84
|
+
revenue: totalRevenue,
|
|
85
|
+
orders: totalOrders,
|
|
86
|
+
avgOrder: totalOrders > 0 ? totalRevenue / totalOrders : 0
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if ((rangeData as any).daily_breakdown) {
|
|
90
|
+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
91
|
+
const chartData = (rangeData as any).daily_breakdown.map((d: any) => {
|
|
92
|
+
const date = new Date(d.date);
|
|
93
|
+
return {
|
|
94
|
+
name: dayNames[date.getDay()] || d.date,
|
|
95
|
+
revenue: d.revenue || d.total_revenue || 0,
|
|
96
|
+
orders: d.count || d.total_sales || 0
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
setSalesChartData(chartData);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
setSalesKPI({ revenue: 0, orders: 0, avgOrder: 0 });
|
|
103
|
+
setSalesChartData([]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
else if (activeTab === 'inventory') {
|
|
108
|
+
const val = await api.getStockValue().catch(() => ({ total_stock_value: 0 }));
|
|
109
|
+
setStockValue((val as any).total_stock_value || 0);
|
|
110
|
+
|
|
111
|
+
const exp = await api.getExpirySummary().catch(() => ({ expired: 0, expiring_30_days: 0, expiring_60_days: 0, expiring_90_days: 0, safe: 0 }));
|
|
112
|
+
setExpirySummary(exp);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
else if (activeTab === 'financial') {
|
|
116
|
+
const [profit, breakdown] = await Promise.all([
|
|
117
|
+
api.getProfit(startStr, endStr).catch(() => ({ revenue: 0, cogs: 0, profit: 0 })),
|
|
118
|
+
api.getPaymentBreakdown(startStr, endStr).catch(() => ({ total_revenue: 0, methods: [] })),
|
|
119
|
+
]);
|
|
120
|
+
setProfitData(profit);
|
|
121
|
+
setPaymentBreakdown(breakdown);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
else if (activeTab === 'customer') {
|
|
125
|
+
const balances = await api.getCustomerBalances().catch(() => []);
|
|
126
|
+
setCustomerBalances(Array.isArray(balances) ? balances : []);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
else if (activeTab === 'medicine') {
|
|
130
|
+
const movement = await api.getMedicineMovement(startStr, endStr).catch(() => ({ fast_moving: [], slow_moving: [], dead_stock: [] }));
|
|
131
|
+
setMedicineAnalysis(movement);
|
|
132
|
+
}
|
|
133
|
+
} finally {
|
|
134
|
+
setLoading(false);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const expiryData = [
|
|
139
|
+
{ name: 'Expired', value: expirySummary.expired || 0 },
|
|
140
|
+
{ name: '< 30 Days', value: expirySummary.expiring_30_days || 0 },
|
|
141
|
+
{ name: '30-60 Days', value: expirySummary.expiring_60_days || 0 },
|
|
142
|
+
{ name: '60-90 Days', value: expirySummary.expiring_90_days || 0 },
|
|
143
|
+
{ name: 'Safe (>90)', value: expirySummary.safe || 0 },
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
// ── Export Functions ──
|
|
147
|
+
const exportToCSV = (reportType: string) => {
|
|
148
|
+
let data = '';
|
|
149
|
+
let filename = `${reportType}_report.csv`;
|
|
150
|
+
|
|
151
|
+
if (reportType === 'sales') {
|
|
152
|
+
data = 'Day,Revenue,Orders\n' + salesChartData.map(d => `${d.name},${d.revenue},${d.orders}`).join('\n');
|
|
153
|
+
} else if (reportType === 'inventory') {
|
|
154
|
+
data = 'Status,Count\n' + expiryData.map(d => `${d.name},${d.value}`).join('\n');
|
|
155
|
+
} else if (reportType === 'customers') {
|
|
156
|
+
data = 'Customer,Phone,Balance\n' + customerBalances.map(c => `${c.name},${c.phone || 'N/A'},${Number(c.credit_balance).toFixed(2)}`).join('\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const blob = new Blob([data], { type: 'text/csv' });
|
|
160
|
+
const url = window.URL.createObjectURL(blob);
|
|
161
|
+
const a = document.createElement('a');
|
|
162
|
+
a.href = url;
|
|
163
|
+
a.download = filename;
|
|
164
|
+
a.click();
|
|
165
|
+
toast.success(`${filename} exported successfully`);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const exportToPDF = (reportType: string) => {
|
|
169
|
+
const doc = new jsPDF();
|
|
170
|
+
|
|
171
|
+
doc.setFontSize(20);
|
|
172
|
+
doc.text(`Pharma ERP - ${reportType.charAt(0).toUpperCase() + reportType.slice(1)} Report`, 14, 22);
|
|
173
|
+
doc.setFontSize(10);
|
|
174
|
+
doc.text(`Generated on: ${new Date().toLocaleString()}`, 14, 30);
|
|
175
|
+
|
|
176
|
+
let head = [[]] as any[];
|
|
177
|
+
let body = [] as any[];
|
|
178
|
+
|
|
179
|
+
if (reportType === 'sales') {
|
|
180
|
+
head = [['Day', `Revenue (${currency})`, 'Orders']];
|
|
181
|
+
body = salesChartData.map(d => [d.name, d.revenue.toLocaleString(), d.orders.toString()]);
|
|
182
|
+
if (body.length === 0) body = [['No data', '-', '-']];
|
|
183
|
+
} else if (reportType === 'customers') {
|
|
184
|
+
head = [['Customer', 'Phone', 'Credit Balance']];
|
|
185
|
+
body = customerBalances.length > 0
|
|
186
|
+
? customerBalances.map(c => [c.name, c.phone || 'N/A', `${currency} ${Number(c.credit_balance).toFixed(2)}`])
|
|
187
|
+
: [['No data', '-', '-']];
|
|
188
|
+
} else {
|
|
189
|
+
head = [['Data', 'Value']];
|
|
190
|
+
body = [['Total Stock Value', `${currency} ${stockValue.toLocaleString(undefined, {minimumFractionDigits: 2})}`]];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
autoTable(doc, {
|
|
194
|
+
startY: 40,
|
|
195
|
+
head: head,
|
|
196
|
+
body: body,
|
|
197
|
+
theme: 'grid',
|
|
198
|
+
headStyles: { fillColor: [139, 92, 246] }
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
doc.save(`${reportType}_report.pdf`);
|
|
202
|
+
toast.success(`PDF report generated`);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
206
|
+
// RENDER
|
|
207
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
208
|
+
return (
|
|
209
|
+
<div className="flex flex-col gap-0 h-full min-h-[calc(100vh-120px)]">
|
|
210
|
+
{/* Header */}
|
|
211
|
+
<header className="flex-none rounded-t-xl border border-border bg-surface p-6 shadow-sm">
|
|
212
|
+
<div className="flex items-center justify-between">
|
|
213
|
+
<div className="flex items-center gap-3">
|
|
214
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-brand-500/10">
|
|
215
|
+
<BarChart3 className="h-5 w-5 text-brand-400" />
|
|
216
|
+
</div>
|
|
217
|
+
<div>
|
|
218
|
+
<h1 className="text-2xl font-bold tracking-tight text-foreground">Reports & Analytics</h1>
|
|
219
|
+
<p className="text-sm text-muted-foreground">Comprehensive insights into your pharmacy operations</p>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
<div className="w-48">
|
|
223
|
+
<CustomSelect
|
|
224
|
+
value={dateRange}
|
|
225
|
+
onChange={setDateRange}
|
|
226
|
+
options={[
|
|
227
|
+
{ value: '7days', label: 'Last 7 Days' },
|
|
228
|
+
{ value: '30days', label: 'Last 30 Days' },
|
|
229
|
+
{ value: 'year', label: 'This Year' },
|
|
230
|
+
]}
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</header>
|
|
235
|
+
|
|
236
|
+
{/* Two-panel layout */}
|
|
237
|
+
<div className="flex-1 flex flex-col md:flex-row rounded-b-xl border border-t-0 border-border bg-surface overflow-hidden shadow-sm">
|
|
238
|
+
{/* Left Sidebar Nav */}
|
|
239
|
+
<nav className="w-full md:w-64 flex-none border-b md:border-b-0 md:border-r border-border bg-background/50 p-3 overflow-x-auto md:overflow-y-auto scrollbar-hide">
|
|
240
|
+
<div className="flex md:flex-col gap-1 space-y-0 md:space-y-1">
|
|
241
|
+
{TABS.map((tab) => {
|
|
242
|
+
const Icon = tab.icon;
|
|
243
|
+
const isActive = activeTab === tab.id;
|
|
244
|
+
return (
|
|
245
|
+
<button
|
|
246
|
+
key={tab.id}
|
|
247
|
+
onClick={() => setActiveTab(tab.id)}
|
|
248
|
+
className={cn(
|
|
249
|
+
"flex whitespace-nowrap flex-none w-auto md:w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-medium transition-all duration-150",
|
|
250
|
+
isActive
|
|
251
|
+
? "bg-brand-500/10 text-brand-400 shadow-sm"
|
|
252
|
+
: "text-muted-foreground hover:bg-gray-100 hover:text-foreground dark:hover:bg-gray-800/60 dark:hover:text-gray-200"
|
|
253
|
+
)}
|
|
254
|
+
>
|
|
255
|
+
<Icon className={cn("h-4 w-4 flex-none", isActive ? "text-brand-400" : "text-muted-foreground")} />
|
|
256
|
+
<span className="truncate">{tab.label}</span>
|
|
257
|
+
</button>
|
|
258
|
+
);
|
|
259
|
+
})}
|
|
260
|
+
</div>
|
|
261
|
+
</nav>
|
|
262
|
+
|
|
263
|
+
{/* Right Content Panel */}
|
|
264
|
+
<main className="flex-1 overflow-y-auto p-6 bg-background/30">
|
|
265
|
+
{activeTab === 'sales' && renderSalesTab()}
|
|
266
|
+
{activeTab === 'inventory' && renderInventoryTab()}
|
|
267
|
+
{activeTab === 'financial' && renderFinancialTab()}
|
|
268
|
+
{activeTab === 'customer' && renderCustomerTab()}
|
|
269
|
+
{activeTab === 'supplier' && renderSupplierTab()}
|
|
270
|
+
{activeTab === 'medicine' && renderMedicineTab()}
|
|
271
|
+
{activeTab === 'export' && renderExportTab()}
|
|
272
|
+
</main>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
278
|
+
// TAB RENDERS
|
|
279
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
280
|
+
|
|
281
|
+
function renderSalesTab() {
|
|
282
|
+
return (
|
|
283
|
+
<div className="space-y-6 fade-in">
|
|
284
|
+
<h2 className="text-lg font-semibold text-foreground">Sales Performance</h2>
|
|
285
|
+
|
|
286
|
+
{/* KPI Cards */}
|
|
287
|
+
<div className="grid grid-cols-3 gap-4">
|
|
288
|
+
<div className="rounded-xl border border-border bg-surface p-5 shadow-sm">
|
|
289
|
+
<p className="text-sm font-medium text-muted-foreground">Total Revenue</p>
|
|
290
|
+
<p className="text-3xl font-bold text-foreground mt-2">{currency} {salesKPI.revenue.toLocaleString(undefined, {minimumFractionDigits: 2})}</p>
|
|
291
|
+
<p className="text-xs text-green-400 flex items-center mt-2 font-medium">
|
|
292
|
+
<ArrowUpRight className="h-3 w-3 mr-1" /> +12.5% from previous period
|
|
293
|
+
</p>
|
|
294
|
+
</div>
|
|
295
|
+
<div className="rounded-xl border border-border bg-surface p-5 shadow-sm">
|
|
296
|
+
<p className="text-sm font-medium text-muted-foreground">Total Orders</p>
|
|
297
|
+
<p className="text-3xl font-bold text-foreground mt-2">{salesKPI.orders.toLocaleString()}</p>
|
|
298
|
+
<p className="text-xs text-green-400 flex items-center mt-2 font-medium">
|
|
299
|
+
<ArrowUpRight className="h-3 w-3 mr-1" /> +5.2% from previous period
|
|
300
|
+
</p>
|
|
301
|
+
</div>
|
|
302
|
+
<div className="rounded-xl border border-border bg-surface p-5 shadow-sm">
|
|
303
|
+
<p className="text-sm font-medium text-muted-foreground">Average Order Value</p>
|
|
304
|
+
<p className="text-3xl font-bold text-foreground mt-2">{currency} {salesKPI.avgOrder.toFixed(2)}</p>
|
|
305
|
+
<p className="text-xs text-red-400 flex items-center mt-2 font-medium">
|
|
306
|
+
<ArrowDownRight className="h-3 w-3 mr-1" /> -1.4% from previous period
|
|
307
|
+
</p>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
{/* Chart */}
|
|
312
|
+
<div className="rounded-xl border border-border bg-surface p-5 shadow-sm h-[350px]">
|
|
313
|
+
<h3 className="text-sm font-medium text-foreground mb-4">Revenue Trend</h3>
|
|
314
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
315
|
+
<LineChart data={salesChartData}>
|
|
316
|
+
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#374151" />
|
|
317
|
+
<XAxis dataKey="name" stroke="#9ca3af" fontSize={12} tickLine={false} axisLine={false} />
|
|
318
|
+
<YAxis stroke="#9ca3af" fontSize={12} tickLine={false} axisLine={false} tickFormatter={v => `${currency} ${v}`} />
|
|
319
|
+
<RechartsTooltip
|
|
320
|
+
contentStyle={{ backgroundColor: '#1f2937', borderColor: '#374151', borderRadius: '8px' }}
|
|
321
|
+
itemStyle={{ color: '#e5e7eb' }}
|
|
322
|
+
/>
|
|
323
|
+
<Line type="monotone" dataKey="revenue" stroke="#8b5cf6" strokeWidth={3} dot={{ r: 4, fill: '#8b5cf6' }} activeDot={{ r: 6 }} />
|
|
324
|
+
</LineChart>
|
|
325
|
+
</ResponsiveContainer>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
{/* Top Products */}
|
|
329
|
+
<div className="rounded-xl border border-border bg-surface shadow-sm overflow-hidden">
|
|
330
|
+
<div className="p-5 border-b border-border">
|
|
331
|
+
<h3 className="text-sm font-medium text-foreground">Top Selling Products</h3>
|
|
332
|
+
</div>
|
|
333
|
+
<table className="w-full text-left text-sm">
|
|
334
|
+
<thead className="bg-background text-xs uppercase text-muted-foreground border-b border-border">
|
|
335
|
+
<tr>
|
|
336
|
+
<th className="px-5 py-3 font-semibold">Product</th>
|
|
337
|
+
<th className="px-5 py-3 font-semibold text-right">Quantity Sold</th>
|
|
338
|
+
<th className="px-5 py-3 font-semibold text-right">Revenue</th>
|
|
339
|
+
</tr>
|
|
340
|
+
</thead>
|
|
341
|
+
<tbody className="divide-y divide-border">
|
|
342
|
+
{topProducts.length > 0 ? topProducts.map((p, i) => (
|
|
343
|
+
<tr key={i} className="hover:bg-gray-800/20">
|
|
344
|
+
<td className="px-5 py-3 font-medium text-foreground">{p.name_en} <span className="text-xs text-muted-foreground ml-2">{p.name_ar}</span></td>
|
|
345
|
+
<td className="px-5 py-3 text-right text-muted-foreground">{p.total_quantity}</td>
|
|
346
|
+
<td className="px-5 py-3 text-right text-foreground font-medium">{currency} {Number(p.total_revenue).toFixed(2)}</td>
|
|
347
|
+
</tr>
|
|
348
|
+
)) : (
|
|
349
|
+
<tr><td colSpan={3} className="px-5 py-8 text-center text-muted-foreground">No sales data available for this period.</td></tr>
|
|
350
|
+
)}
|
|
351
|
+
</tbody>
|
|
352
|
+
</table>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function renderInventoryTab() {
|
|
359
|
+
return (
|
|
360
|
+
<div className="space-y-6 fade-in">
|
|
361
|
+
<h2 className="text-lg font-semibold text-foreground">Inventory & Expiry</h2>
|
|
362
|
+
|
|
363
|
+
<div className="grid grid-cols-2 gap-6">
|
|
364
|
+
{/* Stock Value Card */}
|
|
365
|
+
<div className="rounded-xl border border-border bg-surface p-6 shadow-sm flex flex-col justify-center items-center text-center">
|
|
366
|
+
<div className="h-16 w-16 rounded-full bg-brand-500/10 flex items-center justify-center mb-4">
|
|
367
|
+
<Package className="h-8 w-8 text-brand-400" />
|
|
368
|
+
</div>
|
|
369
|
+
<p className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Total Stock Value</p>
|
|
370
|
+
<p className="text-4xl font-bold text-foreground mt-2">{currency} {stockValue.toLocaleString(undefined, {minimumFractionDigits: 2})}</p>
|
|
371
|
+
<p className="text-sm text-muted-foreground mt-4 max-w-xs">Based on latest purchase prices of all batches currently in stock.</p>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
{/* Expiry Pie Chart */}
|
|
375
|
+
<div className="rounded-xl border border-border bg-surface p-5 shadow-sm h-[300px]">
|
|
376
|
+
<h3 className="text-sm font-medium text-foreground mb-2">Expiry Distribution (Batches)</h3>
|
|
377
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
378
|
+
<PieChart>
|
|
379
|
+
<Pie data={expiryData} cx="50%" cy="50%" innerRadius={60} outerRadius={90} paddingAngle={5} dataKey="value">
|
|
380
|
+
{expiryData.map((entry, index) => (
|
|
381
|
+
<Cell key={`cell-${index}`} fill={
|
|
382
|
+
entry.name === 'Expired' ? '#ef4444' :
|
|
383
|
+
entry.name === '< 30 Days' ? '#f97316' :
|
|
384
|
+
entry.name === '30-60 Days' ? '#f59e0b' :
|
|
385
|
+
entry.name === '60-90 Days' ? '#eab308' : '#10b981'
|
|
386
|
+
} />
|
|
387
|
+
))}
|
|
388
|
+
</Pie>
|
|
389
|
+
<RechartsTooltip contentStyle={{ backgroundColor: '#1f2937', borderColor: '#374151', borderRadius: '8px' }} itemStyle={{ color: '#e5e7eb' }} />
|
|
390
|
+
<Legend verticalAlign="bottom" height={36} iconType="circle" />
|
|
391
|
+
</PieChart>
|
|
392
|
+
</ResponsiveContainer>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function renderFinancialTab() {
|
|
400
|
+
const revenue = Number(profitData.revenue || 0);
|
|
401
|
+
const cogs = Number(profitData.cogs || 0);
|
|
402
|
+
const profit = Number(profitData.profit || 0);
|
|
403
|
+
const margin = revenue > 0 ? Math.round((profit / revenue) * 100) : 0;
|
|
404
|
+
|
|
405
|
+
const methods: any[] = paymentBreakdown?.methods || [];
|
|
406
|
+
const cashTotal = methods.filter((m: any) => m.type === 'cash').reduce((s: number, m: any) => s + m.amount, 0);
|
|
407
|
+
const mobileTotal = methods.filter((m: any) => m.type === 'mobile').reduce((s: number, m: any) => s + m.amount, 0);
|
|
408
|
+
const mobileMethods = methods.filter((m: any) => m.type === 'mobile');
|
|
409
|
+
const cashMethods = methods.filter((m: any) => m.type === 'cash');
|
|
410
|
+
|
|
411
|
+
const filteredMethods = methodFilter === 'all'
|
|
412
|
+
? methods
|
|
413
|
+
: methodFilter === 'cash' || methodFilter === 'mobile'
|
|
414
|
+
? methods.filter((m: any) => m.type === methodFilter)
|
|
415
|
+
: methods.filter((m: any) => m.method === methodFilter);
|
|
416
|
+
|
|
417
|
+
const METHOD_ICONS: Record<string, string> = { cash: '💵', bankak: '🏦', fawry: '📱', amin: '💳' };
|
|
418
|
+
const METHOD_COLORS: Record<string, string> = { cash: '#22C55E', bankak: '#3B82F6', fawry: '#F59E0B', amin: '#8B5CF6' };
|
|
419
|
+
|
|
420
|
+
// Build dynamic filter options
|
|
421
|
+
const filterOptions = ['all', 'cash'];
|
|
422
|
+
if (mobileMethods.length > 0) {
|
|
423
|
+
filterOptions.push('mobile');
|
|
424
|
+
mobileMethods.forEach((m: any) => filterOptions.push(m.method));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<div className="space-y-6 fade-in">
|
|
429
|
+
<h2 className="text-lg font-semibold text-foreground">Financial Summary</h2>
|
|
430
|
+
|
|
431
|
+
{/* Summary Cards */}
|
|
432
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
433
|
+
<div className="rounded-xl border border-border bg-surface p-5 shadow-sm">
|
|
434
|
+
<p className="text-sm font-medium text-muted-foreground">Gross Revenue</p>
|
|
435
|
+
<p className="text-3xl font-bold text-foreground mt-2">{currency} {revenue.toLocaleString()}</p>
|
|
436
|
+
</div>
|
|
437
|
+
<div className="rounded-xl border border-border bg-surface p-5 shadow-sm">
|
|
438
|
+
<p className="text-sm font-medium text-muted-foreground">Cost of Goods Sold (COGS)</p>
|
|
439
|
+
<p className="text-3xl font-bold text-foreground mt-2">{currency} {cogs.toLocaleString()}</p>
|
|
440
|
+
</div>
|
|
441
|
+
<div className="rounded-xl border border-border bg-brand-500/5 p-5 shadow-sm">
|
|
442
|
+
<p className="text-sm font-medium text-brand-400">Gross Profit</p>
|
|
443
|
+
<p className="text-3xl font-bold text-brand-500 mt-2">{currency} {profit.toLocaleString()}</p>
|
|
444
|
+
{margin > 0 && <Badge variant="success" className="mt-3">{margin}% Margin</Badge>}
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
{/* Payment Method Breakdown */}
|
|
449
|
+
<div className="rounded-xl border border-border bg-surface shadow-sm overflow-hidden">
|
|
450
|
+
<div className="p-5 border-b border-border flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 overflow-x-auto">
|
|
451
|
+
<div className="shrink-0">
|
|
452
|
+
<h3 className="text-base font-semibold text-foreground">Payment Method Breakdown</h3>
|
|
453
|
+
<p className="text-xs text-muted-foreground mt-0.5">Revenue split by payment channel</p>
|
|
454
|
+
</div>
|
|
455
|
+
<div className="flex items-center rounded-lg border border-border bg-background p-0.5 shrink-0 whitespace-nowrap">
|
|
456
|
+
{filterOptions.map(f => (
|
|
457
|
+
<button
|
|
458
|
+
key={f}
|
|
459
|
+
onClick={() => setMethodFilter(f)}
|
|
460
|
+
className={cn(
|
|
461
|
+
"px-3 py-1.5 text-xs font-medium rounded-md transition-all",
|
|
462
|
+
methodFilter === f
|
|
463
|
+
? "bg-brand-500 text-white shadow-sm"
|
|
464
|
+
: "text-muted-foreground hover:text-foreground"
|
|
465
|
+
)}
|
|
466
|
+
>
|
|
467
|
+
{f === 'all' ? 'All'
|
|
468
|
+
: f === 'cash' ? '💵 Cash'
|
|
469
|
+
: f === 'mobile' ? '📱 Mobile'
|
|
470
|
+
: `${METHOD_ICONS[f] || '💳'} ${methods.find((m: any) => m.method === f)?.label}`}
|
|
471
|
+
</button>
|
|
472
|
+
))}
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
|
|
476
|
+
{methods.length === 0 ? (
|
|
477
|
+
<div className="p-12 text-center">
|
|
478
|
+
<DollarSign className="h-10 w-10 mx-auto text-muted-foreground/30 mb-3" />
|
|
479
|
+
<p className="text-sm text-muted-foreground">No payment data for the selected period</p>
|
|
480
|
+
</div>
|
|
481
|
+
) : (
|
|
482
|
+
<div className="p-5 space-y-5">
|
|
483
|
+
{/* Cash vs Mobile Summary Bar */}
|
|
484
|
+
{methodFilter === 'all' && revenue > 0 && (
|
|
485
|
+
<div className="space-y-2">
|
|
486
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
487
|
+
<span>💵 Cash: {currency} {cashTotal.toLocaleString()}</span>
|
|
488
|
+
<span>📱 Mobile: {currency} {mobileTotal.toLocaleString()}</span>
|
|
489
|
+
</div>
|
|
490
|
+
<div className="h-3 bg-border rounded-full overflow-hidden flex">
|
|
491
|
+
{cashTotal > 0 && (
|
|
492
|
+
<div
|
|
493
|
+
className="h-full bg-green-500 transition-all duration-700 rounded-l-full"
|
|
494
|
+
style={{ width: `${(cashTotal / revenue) * 100}%` }}
|
|
495
|
+
/>
|
|
496
|
+
)}
|
|
497
|
+
{mobileTotal > 0 && (
|
|
498
|
+
<div
|
|
499
|
+
className="h-full bg-blue-500 transition-all duration-700 rounded-r-full"
|
|
500
|
+
style={{ width: `${(mobileTotal / revenue) * 100}%` }}
|
|
501
|
+
/>
|
|
502
|
+
)}
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
)}
|
|
506
|
+
|
|
507
|
+
{/* Individual Methods */}
|
|
508
|
+
<div className="space-y-3">
|
|
509
|
+
{filteredMethods.map((m: any) => {
|
|
510
|
+
const icon = METHOD_ICONS[m.method] || (m.type === 'mobile' ? '📱' : '💵');
|
|
511
|
+
const color = METHOD_COLORS[m.method] || (m.type === 'mobile' ? '#3B82F6' : '#22C55E');
|
|
512
|
+
return (
|
|
513
|
+
<div key={m.method} className="group rounded-lg border border-border bg-background/50 p-4 hover:border-brand-500/30 transition-all">
|
|
514
|
+
<div className="flex items-center justify-between mb-2">
|
|
515
|
+
<div className="flex items-center gap-3">
|
|
516
|
+
<span className="text-xl">{icon}</span>
|
|
517
|
+
<div>
|
|
518
|
+
<p className="text-sm font-semibold text-foreground">{m.label}</p>
|
|
519
|
+
<p className="text-xs text-muted-foreground">{m.count} transaction{m.count !== 1 ? 's' : ''}</p>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
<div className="text-right">
|
|
523
|
+
<p className="text-lg font-bold text-foreground tabular-nums">{currency} {m.amount.toLocaleString()}</p>
|
|
524
|
+
<p className="text-xs font-medium tabular-nums" style={{ color }}>{m.percentage}%</p>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
<div className="h-2 bg-border rounded-full overflow-hidden">
|
|
528
|
+
<div
|
|
529
|
+
className="h-full rounded-full transition-all duration-700"
|
|
530
|
+
style={{ width: `${m.percentage}%`, backgroundColor: color }}
|
|
531
|
+
/>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
);
|
|
535
|
+
})}
|
|
536
|
+
</div>
|
|
537
|
+
|
|
538
|
+
{/* Mobile Money Sub-breakdown */}
|
|
539
|
+
{methodFilter === 'all' && mobileMethods.length > 1 && (
|
|
540
|
+
<div className="mt-4 rounded-lg border border-dashed border-blue-500/30 bg-blue-500/5 p-4">
|
|
541
|
+
<p className="text-xs font-semibold text-blue-400 uppercase tracking-wider mb-3">📱 Mobile Money Breakdown</p>
|
|
542
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
|
543
|
+
{mobileMethods.map((m: any) => {
|
|
544
|
+
const icon = METHOD_ICONS[m.method] || '📱';
|
|
545
|
+
const color = METHOD_COLORS[m.method] || '#3B82F6';
|
|
546
|
+
const mobilePercent = mobileTotal > 0 ? Math.round((m.amount / mobileTotal) * 100) : 0;
|
|
547
|
+
return (
|
|
548
|
+
<div key={m.method} className="rounded-lg bg-background/80 border border-border p-3 text-center">
|
|
549
|
+
<span className="text-2xl">{icon}</span>
|
|
550
|
+
<p className="text-xs font-semibold text-foreground mt-1">{m.label}</p>
|
|
551
|
+
<p className="text-sm font-bold text-foreground mt-0.5">{currency} {m.amount.toLocaleString()}</p>
|
|
552
|
+
<p className="text-xs mt-0.5" style={{ color }}>{mobilePercent}% of mobile</p>
|
|
553
|
+
</div>
|
|
554
|
+
);
|
|
555
|
+
})}
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
)}
|
|
559
|
+
</div>
|
|
560
|
+
)}
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function renderCustomerTab() {
|
|
567
|
+
return (
|
|
568
|
+
<div className="space-y-6 fade-in">
|
|
569
|
+
<h2 className="text-lg font-semibold text-foreground">Customer Balances</h2>
|
|
570
|
+
|
|
571
|
+
<div className="rounded-xl border border-border bg-surface shadow-sm overflow-hidden">
|
|
572
|
+
<table className="w-full text-left text-sm">
|
|
573
|
+
<thead className="bg-background text-xs uppercase text-muted-foreground border-b border-border">
|
|
574
|
+
<tr>
|
|
575
|
+
<th className="px-5 py-3 font-semibold">Customer</th>
|
|
576
|
+
<th className="px-5 py-3 font-semibold">Phone</th>
|
|
577
|
+
<th className="px-5 py-3 font-semibold text-right">Credit Limit</th>
|
|
578
|
+
<th className="px-5 py-3 font-semibold text-right">Current Balance</th>
|
|
579
|
+
<th className="px-5 py-3 font-semibold text-center">Status</th>
|
|
580
|
+
</tr>
|
|
581
|
+
</thead>
|
|
582
|
+
<tbody className="divide-y divide-border">
|
|
583
|
+
{customerBalances.length > 0 ? customerBalances.map(c => (
|
|
584
|
+
<tr key={c.id} className="hover:bg-gray-800/20">
|
|
585
|
+
<td className="px-5 py-3 font-medium text-foreground">{c.name}</td>
|
|
586
|
+
<td className="px-5 py-3 text-muted-foreground">{c.phone || '-'}</td>
|
|
587
|
+
<td className="px-5 py-3 text-right text-muted-foreground">{currency} {Number(c.credit_limit).toFixed(2)}</td>
|
|
588
|
+
<td className="px-5 py-3 text-right text-foreground font-bold">{currency} {Number(c.credit_balance).toFixed(2)}</td>
|
|
589
|
+
<td className="px-5 py-3 text-center">
|
|
590
|
+
{c.credit_balance > (c.credit_limit * 0.9) ? (
|
|
591
|
+
<Badge variant="danger">Near Limit</Badge>
|
|
592
|
+
) : <Badge variant="success">Good</Badge>}
|
|
593
|
+
</td>
|
|
594
|
+
</tr>
|
|
595
|
+
)) : (
|
|
596
|
+
<tr><td colSpan={5} className="px-5 py-12 text-center text-muted-foreground">No customers with outstanding credit balances.</td></tr>
|
|
597
|
+
)}
|
|
598
|
+
</tbody>
|
|
599
|
+
</table>
|
|
600
|
+
</div>
|
|
601
|
+
</div>
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function renderSupplierTab() {
|
|
606
|
+
return (
|
|
607
|
+
<div className="flex flex-col items-center justify-center h-full text-center space-y-4 fade-in py-12">
|
|
608
|
+
<div className="h-20 w-20 bg-gray-800/50 rounded-full flex items-center justify-center">
|
|
609
|
+
<Truck className="h-10 w-10 text-muted-foreground" />
|
|
610
|
+
</div>
|
|
611
|
+
<div>
|
|
612
|
+
<h2 className="text-xl font-bold text-foreground">Supplier Reports</h2>
|
|
613
|
+
<p className="text-muted-foreground mt-2 max-w-md mx-auto">Detailed supplier performance, purchase history, and outstanding orders reporting is currently under development.</p>
|
|
614
|
+
</div>
|
|
615
|
+
</div>
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function renderMedicineTab() {
|
|
620
|
+
const { fast_moving = [], slow_moving = [], dead_stock = [] } = medicineAnalysis;
|
|
621
|
+
|
|
622
|
+
return (
|
|
623
|
+
<div className="space-y-6 fade-in">
|
|
624
|
+
<div>
|
|
625
|
+
<h2 className="text-lg font-semibold text-foreground">Medicine Analysis</h2>
|
|
626
|
+
<p className="text-sm text-muted-foreground">Sales movement and dead stock identification for the selected period</p>
|
|
627
|
+
</div>
|
|
628
|
+
|
|
629
|
+
{/* Summary Overview */}
|
|
630
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
631
|
+
<div className="rounded-xl border border-green-500/20 bg-green-500/5 p-5 shadow-sm">
|
|
632
|
+
<div className="flex items-center gap-3 text-green-500 mb-2">
|
|
633
|
+
<TrendingUp className="h-5 w-5" />
|
|
634
|
+
<p className="text-sm font-semibold">Fast Moving</p>
|
|
635
|
+
</div>
|
|
636
|
+
<p className="text-3xl font-bold text-foreground">{fast_moving.length}</p>
|
|
637
|
+
<p className="text-xs text-muted-foreground mt-1">Top selling items by volume</p>
|
|
638
|
+
</div>
|
|
639
|
+
|
|
640
|
+
<div className="rounded-xl border border-amber-500/20 bg-amber-500/5 p-5 shadow-sm">
|
|
641
|
+
<div className="flex items-center gap-3 text-amber-500 mb-2">
|
|
642
|
+
<ArrowDownRight className="h-5 w-5" />
|
|
643
|
+
<p className="text-sm font-semibold">Slow Moving</p>
|
|
644
|
+
</div>
|
|
645
|
+
<p className="text-3xl font-bold text-foreground">{slow_moving.length}</p>
|
|
646
|
+
<p className="text-xs text-muted-foreground mt-1">Items with very few sales</p>
|
|
647
|
+
</div>
|
|
648
|
+
|
|
649
|
+
<div className="rounded-xl border border-red-500/20 bg-red-500/5 p-5 shadow-sm">
|
|
650
|
+
<div className="flex items-center gap-3 text-red-500 mb-2">
|
|
651
|
+
<AlertTriangle className="h-5 w-5" />
|
|
652
|
+
<p className="text-sm font-semibold">Dead Stock</p>
|
|
653
|
+
</div>
|
|
654
|
+
<p className="text-3xl font-bold text-foreground">{dead_stock.length}</p>
|
|
655
|
+
<p className="text-xs text-muted-foreground mt-1">Items in stock with 0 sales</p>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
|
|
659
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
660
|
+
{/* Fast Moving */}
|
|
661
|
+
<div className="rounded-xl border border-border bg-surface shadow-sm overflow-hidden flex flex-col">
|
|
662
|
+
<div className="p-4 border-b border-border bg-background/50 flex items-center justify-between">
|
|
663
|
+
<h3 className="font-semibold text-foreground flex items-center gap-2">
|
|
664
|
+
<span className="text-green-500">🚀</span> Fast Moving
|
|
665
|
+
</h3>
|
|
666
|
+
</div>
|
|
667
|
+
<div className="divide-y divide-border overflow-auto max-h-[400px]">
|
|
668
|
+
{fast_moving.length > 0 ? fast_moving.map((m: any, i: number) => (
|
|
669
|
+
<div key={m.id} className="p-4 hover:bg-background/50 transition-colors flex items-center justify-between gap-3">
|
|
670
|
+
<div className="flex items-center gap-3">
|
|
671
|
+
<div className="h-6 w-6 rounded-full bg-green-500/10 text-green-500 flex items-center justify-center text-xs font-bold">{i + 1}</div>
|
|
672
|
+
<div>
|
|
673
|
+
<p className="text-sm font-medium text-foreground line-clamp-1">{m.name_en}</p>
|
|
674
|
+
<p className="text-xs text-muted-foreground line-clamp-1">{m.name_ar}</p>
|
|
675
|
+
</div>
|
|
676
|
+
</div>
|
|
677
|
+
<div className="text-right shrink-0">
|
|
678
|
+
<p className="text-sm font-bold text-foreground">{m.quantity} <span className="text-xs font-normal text-muted-foreground">sold</span></p>
|
|
679
|
+
<p className="text-xs text-green-400 font-medium">{currency} {m.revenue.toLocaleString()}</p>
|
|
680
|
+
</div>
|
|
681
|
+
</div>
|
|
682
|
+
)) : (
|
|
683
|
+
<div className="p-8 text-center text-muted-foreground text-sm">No sales data found.</div>
|
|
684
|
+
)}
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
|
|
688
|
+
{/* Slow Moving */}
|
|
689
|
+
<div className="rounded-xl border border-border bg-surface shadow-sm overflow-hidden flex flex-col">
|
|
690
|
+
<div className="p-4 border-b border-border bg-background/50 flex items-center justify-between">
|
|
691
|
+
<h3 className="font-semibold text-foreground flex items-center gap-2">
|
|
692
|
+
<span className="text-amber-500">🐢</span> Slow Moving
|
|
693
|
+
</h3>
|
|
694
|
+
</div>
|
|
695
|
+
<div className="divide-y divide-border overflow-auto max-h-[400px]">
|
|
696
|
+
{slow_moving.length > 0 ? slow_moving.map((m: any, i: number) => (
|
|
697
|
+
<div key={m.id} className="p-4 hover:bg-background/50 transition-colors flex items-center justify-between gap-3">
|
|
698
|
+
<div className="flex items-center gap-3">
|
|
699
|
+
<div className="h-6 w-6 rounded-full bg-amber-500/10 text-amber-500 flex items-center justify-center text-xs font-bold">{i + 1}</div>
|
|
700
|
+
<div>
|
|
701
|
+
<p className="text-sm font-medium text-foreground line-clamp-1">{m.name_en}</p>
|
|
702
|
+
<p className="text-xs text-muted-foreground line-clamp-1">{m.name_ar}</p>
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
<div className="text-right shrink-0">
|
|
706
|
+
<p className="text-sm font-bold text-foreground">{m.quantity} <span className="text-xs font-normal text-muted-foreground">sold</span></p>
|
|
707
|
+
<p className="text-xs text-amber-400 font-medium">{currency} {m.revenue.toLocaleString()}</p>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
)) : (
|
|
711
|
+
<div className="p-8 text-center text-muted-foreground text-sm">No sales data found.</div>
|
|
712
|
+
)}
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
|
|
716
|
+
{/* Dead Stock */}
|
|
717
|
+
<div className="rounded-xl border border-border bg-surface shadow-sm overflow-hidden flex flex-col">
|
|
718
|
+
<div className="p-4 border-b border-border bg-background/50 flex items-center justify-between">
|
|
719
|
+
<h3 className="font-semibold text-foreground flex items-center gap-2">
|
|
720
|
+
<span className="text-red-500">💀</span> Dead Stock
|
|
721
|
+
</h3>
|
|
722
|
+
<Badge variant="danger" className="text-[10px]">0 Sales</Badge>
|
|
723
|
+
</div>
|
|
724
|
+
<div className="divide-y divide-border overflow-auto max-h-[400px]">
|
|
725
|
+
{dead_stock.length > 0 ? dead_stock.map((m: any) => (
|
|
726
|
+
<div key={m.id} className="p-4 hover:bg-background/50 transition-colors flex items-center justify-between gap-3">
|
|
727
|
+
<div>
|
|
728
|
+
<p className="text-sm font-medium text-foreground line-clamp-1">{m.name_en}</p>
|
|
729
|
+
<p className="text-xs text-muted-foreground line-clamp-1">{m.name_ar}</p>
|
|
730
|
+
</div>
|
|
731
|
+
<div className="text-right shrink-0">
|
|
732
|
+
<Badge variant="outline" className="bg-red-500/5 text-red-400 border-red-500/20">
|
|
733
|
+
{m.stock} in stock
|
|
734
|
+
</Badge>
|
|
735
|
+
</div>
|
|
736
|
+
</div>
|
|
737
|
+
)) : (
|
|
738
|
+
<div className="p-8 text-center text-muted-foreground text-sm flex flex-col items-center">
|
|
739
|
+
<span className="text-2xl mb-2">🎉</span>
|
|
740
|
+
No dead stock found! Everything is selling.
|
|
741
|
+
</div>
|
|
742
|
+
)}
|
|
743
|
+
</div>
|
|
744
|
+
</div>
|
|
745
|
+
</div>
|
|
746
|
+
</div>
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
751
|
+
// TAB: EXPORT CENTER
|
|
752
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
753
|
+
function renderExportTab() {
|
|
754
|
+
const exports = [
|
|
755
|
+
{ id: 'sales', label: 'Sales Transactions', desc: 'Detailed log of all sales, items, and discounts' },
|
|
756
|
+
{ id: 'inventory', label: 'Inventory Snapshot', desc: 'Current stock levels and expiry dates' },
|
|
757
|
+
{ id: 'customers', label: 'Customer Balances', desc: 'List of customers and their credit status' },
|
|
758
|
+
];
|
|
759
|
+
|
|
760
|
+
return (
|
|
761
|
+
<div className="space-y-6 fade-in">
|
|
762
|
+
<div>
|
|
763
|
+
<h2 className="text-lg font-semibold text-foreground">Export Center</h2>
|
|
764
|
+
<p className="text-sm text-muted-foreground">Download comprehensive reports for external analysis</p>
|
|
765
|
+
</div>
|
|
766
|
+
|
|
767
|
+
<div className="grid gap-4 max-w-3xl">
|
|
768
|
+
{exports.map(exp => (
|
|
769
|
+
<div key={exp.id} className="flex items-center justify-between rounded-xl border border-border bg-surface p-5 shadow-sm hover:border-brand-500/30 transition-colors">
|
|
770
|
+
<div>
|
|
771
|
+
<h3 className="font-semibold text-foreground">{exp.label}</h3>
|
|
772
|
+
<p className="text-sm text-muted-foreground">{exp.desc}</p>
|
|
773
|
+
</div>
|
|
774
|
+
<div className="flex items-center gap-3">
|
|
775
|
+
<button
|
|
776
|
+
onClick={() => exportToCSV(exp.id)}
|
|
777
|
+
className="inline-flex items-center gap-2 rounded-lg bg-gray-800 px-3 py-2 text-sm font-medium text-gray-200 hover:bg-gray-700 transition-colors"
|
|
778
|
+
>
|
|
779
|
+
<FileSpreadsheet className="h-4 w-4" /> CSV
|
|
780
|
+
</button>
|
|
781
|
+
<button
|
|
782
|
+
onClick={() => exportToPDF(exp.id)}
|
|
783
|
+
className="inline-flex items-center gap-2 rounded-lg bg-brand-500/10 text-brand-400 px-3 py-2 text-sm font-medium hover:bg-brand-500/20 transition-colors"
|
|
784
|
+
>
|
|
785
|
+
<FileText className="h-4 w-4" /> PDF
|
|
786
|
+
</button>
|
|
787
|
+
</div>
|
|
788
|
+
</div>
|
|
789
|
+
))}
|
|
790
|
+
</div>
|
|
791
|
+
</div>
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
}
|