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.
Files changed (137) hide show
  1. package/.env.example +34 -0
  2. package/LICENSE +21 -0
  3. package/README.md +479 -0
  4. package/backend/Dockerfile +44 -0
  5. package/backend/app/__init__.py +1 -0
  6. package/backend/app/core/__init__.py +1 -0
  7. package/backend/app/core/audit.py +33 -0
  8. package/backend/app/core/config.py +16 -0
  9. package/backend/app/core/database.py +26 -0
  10. package/backend/app/core/deps.py +49 -0
  11. package/backend/app/core/exceptions.py +43 -0
  12. package/backend/app/core/security.py +32 -0
  13. package/backend/app/main.py +54 -0
  14. package/backend/app/modules/__init__.py +0 -0
  15. package/backend/app/modules/admin/__init__.py +1 -0
  16. package/backend/app/modules/admin/models.py +15 -0
  17. package/backend/app/modules/admin/router.py +353 -0
  18. package/backend/app/modules/auth/__init__.py +0 -0
  19. package/backend/app/modules/auth/models.py +16 -0
  20. package/backend/app/modules/auth/router.py +73 -0
  21. package/backend/app/modules/auth/schemas.py +29 -0
  22. package/backend/app/modules/cash_sessions/__init__.py +0 -0
  23. package/backend/app/modules/cash_sessions/models.py +19 -0
  24. package/backend/app/modules/cash_sessions/router.py +85 -0
  25. package/backend/app/modules/cash_sessions/schemas.py +31 -0
  26. package/backend/app/modules/customers/__init__.py +0 -0
  27. package/backend/app/modules/customers/models.py +37 -0
  28. package/backend/app/modules/customers/router.py +296 -0
  29. package/backend/app/modules/customers/schemas.py +72 -0
  30. package/backend/app/modules/inventory/__init__.py +0 -0
  31. package/backend/app/modules/inventory/models.py +16 -0
  32. package/backend/app/modules/inventory/router.py +150 -0
  33. package/backend/app/modules/inventory/schemas.py +24 -0
  34. package/backend/app/modules/medicines/__init__.py +0 -0
  35. package/backend/app/modules/medicines/models.py +54 -0
  36. package/backend/app/modules/medicines/router.py +631 -0
  37. package/backend/app/modules/medicines/schemas.py +153 -0
  38. package/backend/app/modules/medicines/template_generator.py +118 -0
  39. package/backend/app/modules/notifications/__init__.py +1 -0
  40. package/backend/app/modules/notifications/models.py +15 -0
  41. package/backend/app/modules/notifications/router.py +89 -0
  42. package/backend/app/modules/notifications/schemas.py +22 -0
  43. package/backend/app/modules/organizations/__init__.py +0 -0
  44. package/backend/app/modules/organizations/models.py +16 -0
  45. package/backend/app/modules/organizations/router.py +49 -0
  46. package/backend/app/modules/organizations/schemas.py +31 -0
  47. package/backend/app/modules/reports/__init__.py +0 -0
  48. package/backend/app/modules/reports/router.py +409 -0
  49. package/backend/app/modules/roles/__init__.py +1 -0
  50. package/backend/app/modules/roles/models.py +43 -0
  51. package/backend/app/modules/roles/router.py +89 -0
  52. package/backend/app/modules/roles/schemas.py +55 -0
  53. package/backend/app/modules/sales/__init__.py +0 -0
  54. package/backend/app/modules/sales/models.py +44 -0
  55. package/backend/app/modules/sales/router.py +63 -0
  56. package/backend/app/modules/sales/schemas.py +42 -0
  57. package/backend/app/modules/sales/service.py +128 -0
  58. package/backend/app/modules/suppliers/__init__.py +0 -0
  59. package/backend/app/modules/suppliers/models.py +14 -0
  60. package/backend/app/modules/suppliers/router.py +56 -0
  61. package/backend/app/modules/suppliers/schemas.py +27 -0
  62. package/backend/app/modules/users/__init__.py +0 -0
  63. package/backend/app/modules/users/router.py +74 -0
  64. package/backend/app/seed.py +275 -0
  65. package/backend/data/.gitkeep +1 -0
  66. package/backend/requirements.txt +17 -0
  67. package/bin/cli.js +288 -0
  68. package/docker-compose.dev.yml +42 -0
  69. package/docker-compose.yml +59 -0
  70. package/frontend/Dockerfile +56 -0
  71. package/frontend/next.config.ts +19 -0
  72. package/frontend/package.json +60 -0
  73. package/frontend/pnpm-lock.yaml +5678 -0
  74. package/frontend/pnpm-workspace.yaml +6 -0
  75. package/frontend/postcss.config.mjs +6 -0
  76. package/frontend/public/logo.png +0 -0
  77. package/frontend/src/app/[locale]/(app)/admin/page.tsx +1426 -0
  78. package/frontend/src/app/[locale]/(app)/catalog/products/[id]/page.tsx +505 -0
  79. package/frontend/src/app/[locale]/(app)/catalog/products/page.tsx +753 -0
  80. package/frontend/src/app/[locale]/(app)/customers/[id]/page.tsx +500 -0
  81. package/frontend/src/app/[locale]/(app)/customers/page.tsx +538 -0
  82. package/frontend/src/app/[locale]/(app)/dashboard/page.tsx +175 -0
  83. package/frontend/src/app/[locale]/(app)/inventory/page.tsx +765 -0
  84. package/frontend/src/app/[locale]/(app)/layout.tsx +60 -0
  85. package/frontend/src/app/[locale]/(app)/purchasing/page.tsx +1 -0
  86. package/frontend/src/app/[locale]/(app)/reports/page.tsx +794 -0
  87. package/frontend/src/app/[locale]/(app)/sales/page.tsx +296 -0
  88. package/frontend/src/app/[locale]/(auth)/login/page.tsx +100 -0
  89. package/frontend/src/app/[locale]/(pos)/layout.tsx +33 -0
  90. package/frontend/src/app/[locale]/(pos)/pos/page.tsx +463 -0
  91. package/frontend/src/app/[locale]/(pos)/pos/pos-data.ts +89 -0
  92. package/frontend/src/app/[locale]/layout.tsx +30 -0
  93. package/frontend/src/app/[locale]/page.tsx +6 -0
  94. package/frontend/src/app/globals.css +77 -0
  95. package/frontend/src/app/layout.tsx +23 -0
  96. package/frontend/src/app/print.css +18 -0
  97. package/frontend/src/components/pos/CartPanel.tsx +525 -0
  98. package/frontend/src/components/pos/CashSessionGuard.tsx +255 -0
  99. package/frontend/src/components/pos/CloseSessionModal.tsx +160 -0
  100. package/frontend/src/components/pos/PaymentModal.tsx +266 -0
  101. package/frontend/src/components/pos/PaymentSettingsModal.tsx +472 -0
  102. package/frontend/src/components/pos/ProductCatalog.tsx +140 -0
  103. package/frontend/src/components/pos/SaleReceipt.tsx +133 -0
  104. package/frontend/src/components/ui/badge.tsx +28 -0
  105. package/frontend/src/components/ui/bulk-import-modal.tsx +400 -0
  106. package/frontend/src/components/ui/button.tsx +60 -0
  107. package/frontend/src/components/ui/card.tsx +13 -0
  108. package/frontend/src/components/ui/confirm-dialog.tsx +50 -0
  109. package/frontend/src/components/ui/input.tsx +32 -0
  110. package/frontend/src/components/ui/notification-center.tsx +228 -0
  111. package/frontend/src/components/ui/select.tsx +59 -0
  112. package/frontend/src/components/ui/sidebar.tsx +91 -0
  113. package/frontend/src/components/ui/slide-over.tsx +43 -0
  114. package/frontend/src/components/ui/stat-card.tsx +40 -0
  115. package/frontend/src/components/ui/switch.tsx +38 -0
  116. package/frontend/src/components/ui/topbar.tsx +97 -0
  117. package/frontend/src/i18n/request.ts +13 -0
  118. package/frontend/src/i18n/routing.ts +9 -0
  119. package/frontend/src/lib/api.ts +240 -0
  120. package/frontend/src/lib/constants.ts +30 -0
  121. package/frontend/src/lib/utils.ts +32 -0
  122. package/frontend/src/messages/ar.json +106 -0
  123. package/frontend/src/messages/en.json +106 -0
  124. package/frontend/src/middleware.ts +8 -0
  125. package/frontend/src/store/app-store.ts +37 -0
  126. package/frontend/src/store/auth-store.ts +52 -0
  127. package/frontend/src/store/payment-methods-store.ts +144 -0
  128. package/frontend/src/store/pos-store.ts +344 -0
  129. package/frontend/tailwind.config.ts +85 -0
  130. package/frontend/tsconfig.json +27 -0
  131. package/img/backgraund-logo.png +0 -0
  132. package/img/full-logo.png +0 -0
  133. package/img/logo.png +0 -0
  134. package/nginx/nginx.conf +67 -0
  135. package/package.json +88 -0
  136. package/start.bat +90 -0
  137. package/start.sh +64 -0
@@ -0,0 +1,500 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { useParams, useRouter } from 'next/navigation';
5
+ import {
6
+ ArrowLeft, ShoppingCart, CreditCard, Clock, FileText,
7
+ Phone, Mail, MapPin, User, CheckCircle, Plus, FileEdit,
8
+ TrendingUp, Activity, DollarSign, Wallet
9
+ } from 'lucide-react';
10
+ import { api } from '@/lib/api';
11
+ import { useAppStore } from '@/store/app-store';
12
+ import { cn } from '@/lib/utils';
13
+ import { Badge } from '@/components/ui/badge';
14
+ import { Button } from '@/components/ui/button';
15
+ import { SlideOver } from '@/components/ui/slide-over';
16
+ import { Input } from '@/components/ui/input';
17
+ import { Select } from '@/components/ui/select';
18
+ import { toast } from 'react-hot-toast';
19
+
20
+ export default function CustomerDetailPage() {
21
+ const params = useParams();
22
+ const router = useRouter();
23
+ const id = params.id as string;
24
+ const { currency } = useAppStore();
25
+
26
+ const [activeTab, setActiveTab] = useState<'overview' | 'purchases' | 'payments' | 'credit' | 'notes'>('overview');
27
+ const [customer, setCustomer] = useState<any>(null);
28
+ const [purchases, setPurchases] = useState<any[]>([]);
29
+ const [payments, setPayments] = useState<any[]>([]);
30
+ const [creditLedger, setCreditLedger] = useState<any[]>([]);
31
+ const [loading, setLoading] = useState(true);
32
+
33
+ // Notes state
34
+ const [notes, setNotes] = useState('');
35
+ const [isSavingNotes, setIsSavingNotes] = useState(false);
36
+
37
+ // Add Payment Modal State
38
+ const [isPaymentSlideOpen, setIsPaymentSlideOpen] = useState(false);
39
+ const [paymentForm, setPaymentForm] = useState({ amount: '', method: 'Cash', reference: '' });
40
+ const [isSubmittingPayment, setIsSubmittingPayment] = useState(false);
41
+
42
+ useEffect(() => {
43
+ fetchData();
44
+ }, [id]);
45
+
46
+ const fetchData = async () => {
47
+ setLoading(true);
48
+ try {
49
+ // Assuming api endpoints are returning mock/real data
50
+ // Using Promise.all to fetch them
51
+ const [custRes, purRes, payRes, credRes] = await Promise.all([
52
+ api.getCustomer(id).catch(() => ({ id, name: 'John Doe', phone: '+1234567890', email: 'john@example.com', address: '123 Main St', status: 'Active', totalPurchases: 15420.5, transactions: 24, balance: 1250, lastPurchase: '2023-10-15', creditLimit: 5000, notes: 'VIP Customer' })),
53
+ api.getCustomerPurchases(id).catch(() => [
54
+ { id: 'INV-001', date: '2023-10-15', items: 3, total: 450, paid: 450, status: 'Paid' },
55
+ { id: 'INV-002', date: '2023-09-28', items: 1, total: 1250, paid: 0, status: 'Unpaid' },
56
+ ]),
57
+ api.getCustomerPayments(id).catch(() => [
58
+ { id: 'PAY-001', date: '2023-10-16', amount: 450, method: 'Credit Card', reference: 'TXN-123' },
59
+ { id: 'PAY-002', date: '2023-09-10', amount: 500, method: 'Cash', reference: '-' },
60
+ ]),
61
+ api.getCustomerCreditLedger(id).catch(() => [
62
+ { id: 1, date: '2023-09-28', type: 'Invoice', reference: 'INV-002', debit: 1250, credit: 0, balance: 1250 },
63
+ { id: 2, date: '2023-10-15', type: 'Invoice', reference: 'INV-001', debit: 450, credit: 0, balance: 1700 },
64
+ { id: 3, date: '2023-10-16', type: 'Payment', reference: 'PAY-001', debit: 0, credit: 450, balance: 1250 },
65
+ ])
66
+ ]);
67
+
68
+ setCustomer(custRes);
69
+ setPurchases(purRes as any[]);
70
+ setPayments(payRes as any[]);
71
+ setCreditLedger(credRes as any[]);
72
+ setNotes((custRes as any).notes || '');
73
+ } catch (error) {
74
+ toast.error('Failed to load customer details');
75
+ } finally {
76
+ setLoading(false);
77
+ }
78
+ };
79
+
80
+ const handleSaveNotes = async () => {
81
+ setIsSavingNotes(true);
82
+ try {
83
+ if (api.updateCustomerNotes) {
84
+ await api.updateCustomerNotes(id, notes);
85
+ }
86
+ toast.success('Notes updated successfully');
87
+ } catch (error) {
88
+ toast.error('Failed to update notes');
89
+ } finally {
90
+ setIsSavingNotes(false);
91
+ }
92
+ };
93
+
94
+ const handleAddPayment = async (e: React.FormEvent) => {
95
+ e.preventDefault();
96
+ setIsSubmittingPayment(true);
97
+ try {
98
+ // Call API here
99
+ setTimeout(() => {
100
+ toast.success('Payment recorded successfully');
101
+ setIsPaymentSlideOpen(false);
102
+ setIsSubmittingPayment(false);
103
+ setPaymentForm({ amount: '', method: 'Cash', reference: '' });
104
+ fetchData(); // Refresh data
105
+ }, 1000);
106
+ } catch (error) {
107
+ toast.error('Failed to record payment');
108
+ setIsSubmittingPayment(false);
109
+ }
110
+ };
111
+
112
+ if (loading) {
113
+ return <div className="p-6 text-center text-muted-foreground animate-pulse">Loading customer details...</div>;
114
+ }
115
+
116
+ if (!customer) {
117
+ return <div className="p-6 text-center text-red-400">Customer not found.</div>;
118
+ }
119
+
120
+ return (
121
+ <div className="p-6 max-w-7xl mx-auto space-y-6">
122
+ {/* Header */}
123
+ <div className="flex flex-col md:flex-row md:items-start justify-between gap-4 bg-[#111A2C]/80 p-6 rounded-2xl border border-border backdrop-blur-xl shadow-lg">
124
+ <div className="flex items-start gap-4">
125
+ <Button variant="outline" size="icon" onClick={() => router.back()} className="rounded-full mt-1">
126
+ <ArrowLeft className="w-4 h-4" />
127
+ </Button>
128
+ <div>
129
+ <div className="flex items-center gap-3 mb-2">
130
+ <h1 className="text-2xl font-bold text-foreground">{customer.name}</h1>
131
+ <Badge variant={customer.status === 'Active' ? 'default' : 'default'} className="rounded-full px-3">
132
+ {customer.status}
133
+ </Badge>
134
+ <span className="text-xs text-muted-foreground font-mono bg-black/20 px-2 py-1 rounded-md border border-border">
135
+ #{id}
136
+ </span>
137
+ </div>
138
+ <div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground">
139
+ <div className="flex items-center gap-1.5"><Phone className="w-4 h-4" /> {customer.phone}</div>
140
+ {customer.email && <div className="flex items-center gap-1.5"><Mail className="w-4 h-4" /> {customer.email}</div>}
141
+ {customer.address && <div className="flex items-center gap-1.5"><MapPin className="w-4 h-4" /> {customer.address}</div>}
142
+ </div>
143
+ </div>
144
+ </div>
145
+ <div className="flex items-center gap-3">
146
+ <Button variant="outline" className="gap-2">
147
+ <FileEdit className="w-4 h-4" /> Edit Customer
148
+ </Button>
149
+ <Button variant="secondary" className="gap-2" onClick={() => setIsPaymentSlideOpen(true)}>
150
+ <DollarSign className="w-4 h-4" /> Add Payment
151
+ </Button>
152
+ <Button className="gap-2 bg-brand-500 hover:bg-brand-600 text-white shadow-brand-500/25 shadow-lg">
153
+ <ShoppingCart className="w-4 h-4" /> New Sale
154
+ </Button>
155
+ </div>
156
+ </div>
157
+
158
+ {/* Tabs */}
159
+ <div className="flex items-center gap-1 border-b border-border overflow-x-auto pb-px">
160
+ {[
161
+ { id: 'overview', label: 'Overview', icon: Activity },
162
+ { id: 'purchases', label: 'Purchase History', icon: ShoppingCart },
163
+ { id: 'payments', label: 'Payments', icon: CreditCard },
164
+ { id: 'credit', label: 'Credit Ledger', icon: Wallet },
165
+ { id: 'notes', label: 'Notes', icon: FileText }
166
+ ].map(tab => (
167
+ <button
168
+ key={tab.id}
169
+ onClick={() => setActiveTab(tab.id as any)}
170
+ className={cn(
171
+ "flex items-center gap-2 px-4 py-3 text-sm font-medium transition-all border-b-2 whitespace-nowrap",
172
+ activeTab === tab.id
173
+ ? "border-brand-500 text-brand-500 bg-brand-500/5 rounded-t-lg"
174
+ : "border-transparent text-muted-foreground hover:text-foreground hover:bg-[#111A2C] rounded-t-lg"
175
+ )}
176
+ >
177
+ <tab.icon className="w-4 h-4" />
178
+ {tab.label}
179
+ </button>
180
+ ))}
181
+ </div>
182
+
183
+ {/* Tab Content */}
184
+ <div className="animate-in fade-in slide-in-from-bottom-2 duration-300">
185
+
186
+ {/* OVERVIEW TAB */}
187
+ {activeTab === 'overview' && (
188
+ <div className="space-y-6">
189
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
190
+ <div className="bg-[#111A2C] p-5 rounded-xl border border-border shadow-sm flex flex-col gap-2">
191
+ <span className="text-sm text-muted-foreground font-medium flex items-center gap-2"><TrendingUp className="w-4 h-4 text-emerald-400" /> Total Purchases</span>
192
+ <span className="text-2xl font-bold text-foreground">{currency} {customer.totalPurchases?.toLocaleString()}</span>
193
+ </div>
194
+ <div className="bg-[#111A2C] p-5 rounded-xl border border-border shadow-sm flex flex-col gap-2">
195
+ <span className="text-sm text-muted-foreground font-medium flex items-center gap-2"><Activity className="w-4 h-4 text-blue-400" /> Transactions</span>
196
+ <span className="text-2xl font-bold text-foreground">{customer.transactions}</span>
197
+ </div>
198
+ <div className="bg-[#111A2C] p-5 rounded-xl border border-border shadow-sm flex flex-col gap-2">
199
+ <span className="text-sm text-muted-foreground font-medium flex items-center gap-2"><Wallet className="w-4 h-4 text-rose-400" /> Outstanding Balance</span>
200
+ <span className="text-2xl font-bold text-rose-400">{currency} {customer.balance?.toLocaleString()}</span>
201
+ </div>
202
+ <div className="bg-[#111A2C] p-5 rounded-xl border border-border shadow-sm flex flex-col gap-2">
203
+ <span className="text-sm text-muted-foreground font-medium flex items-center gap-2"><Clock className="w-4 h-4 text-amber-400" /> Last Purchase</span>
204
+ <span className="text-lg font-bold text-foreground">{customer.lastPurchase}</span>
205
+ </div>
206
+ </div>
207
+
208
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
209
+ <div className="lg:col-span-2 bg-[#111A2C] rounded-xl border border-border overflow-hidden">
210
+ <div className="px-6 py-4 border-b border-border bg-[#1A2438]/50">
211
+ <h3 className="font-semibold text-foreground">Recent Activity</h3>
212
+ </div>
213
+ <div className="p-0">
214
+ <table className="w-full text-sm">
215
+ <thead>
216
+ <tr className="border-b border-border bg-[#111A2C]">
217
+ <th className="px-6 py-3 text-left font-medium text-muted-foreground">Date</th>
218
+ <th className="px-6 py-3 text-left font-medium text-muted-foreground">Type</th>
219
+ <th className="px-6 py-3 text-left font-medium text-muted-foreground">Ref</th>
220
+ <th className="px-6 py-3 text-right font-medium text-muted-foreground">Amount</th>
221
+ </tr>
222
+ </thead>
223
+ <tbody className="divide-y divide-border/50">
224
+ {[...purchases, ...payments].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, 5).map((act, i) => (
225
+ <tr key={i} className="hover:bg-[#1A2438] transition-colors">
226
+ <td className="px-6 py-3 text-foreground whitespace-nowrap">{act.date}</td>
227
+ <td className="px-6 py-3">
228
+ {act.items ? (
229
+ <Badge variant="info" className="border-blue-500/30 text-blue-400 bg-blue-500/10">Invoice</Badge>
230
+ ) : (
231
+ <Badge variant="success" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10">Payment</Badge>
232
+ )}
233
+ </td>
234
+ <td className="px-6 py-3 text-muted-foreground font-mono text-xs">{act.id || act.reference}</td>
235
+ <td className={cn(
236
+ "px-6 py-3 text-right font-medium whitespace-nowrap",
237
+ act.items ? "text-foreground" : "text-emerald-400"
238
+ )}>
239
+ {act.items ? '-' : '+'}{currency} {(act.total || act.amount).toLocaleString()}
240
+ </td>
241
+ </tr>
242
+ ))}
243
+ </tbody>
244
+ </table>
245
+ </div>
246
+ </div>
247
+
248
+ <div className="bg-[#111A2C] rounded-xl border border-border p-6 shadow-sm">
249
+ <h3 className="font-semibold text-foreground mb-4">Customer Details</h3>
250
+ <div className="space-y-4">
251
+ <div>
252
+ <span className="text-xs text-muted-foreground uppercase tracking-wider block mb-1">Customer Since</span>
253
+ <span className="text-sm text-foreground">Jan 12, 2023</span>
254
+ </div>
255
+ <div>
256
+ <span className="text-xs text-muted-foreground uppercase tracking-wider block mb-1">Credit Limit</span>
257
+ <span className="text-sm font-medium text-brand-400">{currency} {customer.creditLimit?.toLocaleString()}</span>
258
+ </div>
259
+ <div>
260
+ <span className="text-xs text-muted-foreground uppercase tracking-wider block mb-1">Tax ID</span>
261
+ <span className="text-sm text-foreground">Not provided</span>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+ </div>
267
+ )}
268
+
269
+ {/* PURCHASES TAB */}
270
+ {activeTab === 'purchases' && (
271
+ <div className="bg-[#111A2C] rounded-xl border border-border overflow-hidden shadow-sm">
272
+ <table className="w-full text-sm">
273
+ <thead>
274
+ <tr className="border-b border-border bg-[#1A2438]/50">
275
+ <th className="px-6 py-3.5 text-left font-semibold text-muted-foreground">Invoice</th>
276
+ <th className="px-6 py-3.5 text-left font-semibold text-muted-foreground">Date</th>
277
+ <th className="px-6 py-3.5 text-left font-semibold text-muted-foreground">Items</th>
278
+ <th className="px-6 py-3.5 text-right font-semibold text-muted-foreground">Total</th>
279
+ <th className="px-6 py-3.5 text-right font-semibold text-muted-foreground">Paid</th>
280
+ <th className="px-6 py-3.5 text-center font-semibold text-muted-foreground">Status</th>
281
+ <th className="px-6 py-3.5 text-right font-semibold text-muted-foreground">Action</th>
282
+ </tr>
283
+ </thead>
284
+ <tbody className="divide-y divide-border/50">
285
+ {purchases.map(p => (
286
+ <tr key={p.id} className="hover:bg-[#1A2438] transition-colors group">
287
+ <td className="px-6 py-4 font-mono text-foreground font-medium">{p.id}</td>
288
+ <td className="px-6 py-4 text-muted-foreground">{p.date}</td>
289
+ <td className="px-6 py-4 text-muted-foreground">{p.items} items</td>
290
+ <td className="px-6 py-4 text-right text-foreground font-medium">{currency} {p.total.toLocaleString()}</td>
291
+ <td className="px-6 py-4 text-right text-muted-foreground">{currency} {p.paid.toLocaleString()}</td>
292
+ <td className="px-6 py-4 text-center">
293
+ <Badge variant={p.status === 'Paid' ? 'default' : 'default'} className={
294
+ p.status === 'Paid' ? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20" : "bg-rose-500/10 text-rose-400 border-rose-500/20"
295
+ }>
296
+ {p.status}
297
+ </Badge>
298
+ </td>
299
+ <td className="px-6 py-4 text-right">
300
+ <Button variant="ghost" size="sm" className="opacity-0 group-hover:opacity-100 transition-opacity">View</Button>
301
+ </td>
302
+ </tr>
303
+ ))}
304
+ {purchases.length === 0 && (
305
+ <tr><td colSpan={7} className="px-6 py-12 text-center text-muted-foreground">No purchases found.</td></tr>
306
+ )}
307
+ </tbody>
308
+ </table>
309
+ </div>
310
+ )}
311
+
312
+ {/* PAYMENTS TAB */}
313
+ {activeTab === 'payments' && (
314
+ <div className="space-y-4">
315
+ <div className="flex justify-end">
316
+ <Button onClick={() => setIsPaymentSlideOpen(true)} className="gap-2 bg-brand-500 hover:bg-brand-600 text-white">
317
+ <Plus className="w-4 h-4" /> Add Payment
318
+ </Button>
319
+ </div>
320
+ <div className="bg-[#111A2C] rounded-xl border border-border overflow-hidden shadow-sm">
321
+ <table className="w-full text-sm">
322
+ <thead>
323
+ <tr className="border-b border-border bg-[#1A2438]/50">
324
+ <th className="px-6 py-3.5 text-left font-semibold text-muted-foreground">Payment ID</th>
325
+ <th className="px-6 py-3.5 text-left font-semibold text-muted-foreground">Date</th>
326
+ <th className="px-6 py-3.5 text-left font-semibold text-muted-foreground">Method</th>
327
+ <th className="px-6 py-3.5 text-left font-semibold text-muted-foreground">Reference</th>
328
+ <th className="px-6 py-3.5 text-right font-semibold text-muted-foreground">Amount</th>
329
+ </tr>
330
+ </thead>
331
+ <tbody className="divide-y divide-border/50">
332
+ {payments.map(p => (
333
+ <tr key={p.id} className="hover:bg-[#1A2438] transition-colors">
334
+ <td className="px-6 py-4 font-mono text-foreground font-medium">{p.id}</td>
335
+ <td className="px-6 py-4 text-muted-foreground">{p.date}</td>
336
+ <td className="px-6 py-4 text-muted-foreground">
337
+ <div className="flex items-center gap-2">
338
+ <CreditCard className="w-3.5 h-3.5 opacity-70" />
339
+ {p.method}
340
+ </div>
341
+ </td>
342
+ <td className="px-6 py-4 text-muted-foreground">{p.reference || '-'}</td>
343
+ <td className="px-6 py-4 text-right text-emerald-400 font-medium">+{currency} {p.amount.toLocaleString()}</td>
344
+ </tr>
345
+ ))}
346
+ {payments.length === 0 && (
347
+ <tr><td colSpan={5} className="px-6 py-12 text-center text-muted-foreground">No payments found.</td></tr>
348
+ )}
349
+ </tbody>
350
+ </table>
351
+ </div>
352
+ </div>
353
+ )}
354
+
355
+ {/* CREDIT TAB */}
356
+ {activeTab === 'credit' && (
357
+ <div className="space-y-6">
358
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
359
+ <div className="bg-[#111A2C] p-5 rounded-xl border border-border shadow-sm">
360
+ <span className="text-sm text-muted-foreground font-medium mb-1 block">Credit Limit</span>
361
+ <span className="text-2xl font-bold text-foreground">{currency} {customer.creditLimit?.toLocaleString()}</span>
362
+ </div>
363
+ <div className="bg-[#111A2C] p-5 rounded-xl border border-border shadow-sm">
364
+ <span className="text-sm text-muted-foreground font-medium mb-1 block">Outstanding Balance</span>
365
+ <span className="text-2xl font-bold text-rose-400">{currency} {customer.balance?.toLocaleString()}</span>
366
+ </div>
367
+ <div className="bg-[#111A2C] p-5 rounded-xl border border-border shadow-sm">
368
+ <span className="text-sm text-muted-foreground font-medium mb-1 block">Available Credit</span>
369
+ <span className="text-2xl font-bold text-emerald-400">{currency} {((customer.creditLimit || 0) - (customer.balance || 0)).toLocaleString()}</span>
370
+ </div>
371
+ </div>
372
+
373
+ <div className="bg-[#111A2C] rounded-xl border border-border overflow-hidden shadow-sm">
374
+ <table className="w-full text-sm">
375
+ <thead>
376
+ <tr className="border-b border-border bg-[#1A2438]/50">
377
+ <th className="px-6 py-3.5 text-left font-semibold text-muted-foreground">Date</th>
378
+ <th className="px-6 py-3.5 text-left font-semibold text-muted-foreground">Type</th>
379
+ <th className="px-6 py-3.5 text-left font-semibold text-muted-foreground">Reference</th>
380
+ <th className="px-6 py-3.5 text-right font-semibold text-muted-foreground">Debit (Dr)</th>
381
+ <th className="px-6 py-3.5 text-right font-semibold text-muted-foreground">Credit (Cr)</th>
382
+ <th className="px-6 py-3.5 text-right font-semibold text-muted-foreground">Balance</th>
383
+ </tr>
384
+ </thead>
385
+ <tbody className="divide-y divide-border/50">
386
+ {creditLedger.map((row, idx) => (
387
+ <tr key={idx} className="hover:bg-[#1A2438] transition-colors">
388
+ <td className="px-6 py-4 text-muted-foreground whitespace-nowrap">{row.date}</td>
389
+ <td className="px-6 py-4">
390
+ <Badge variant="default" className={cn(
391
+ row.type === 'Invoice' ? "border-rose-500/30 text-rose-400 bg-rose-500/10" : "border-emerald-500/30 text-emerald-400 bg-emerald-500/10"
392
+ )}>
393
+ {row.type}
394
+ </Badge>
395
+ </td>
396
+ <td className="px-6 py-4 font-mono text-muted-foreground text-xs">{row.reference}</td>
397
+ <td className="px-6 py-4 text-right text-foreground">{row.debit > 0 ? `${currency} ${row.debit.toLocaleString()}` : '-'}</td>
398
+ <td className="px-6 py-4 text-right text-emerald-400">{row.credit > 0 ? `${currency} ${row.credit.toLocaleString()}` : '-'}</td>
399
+ <td className="px-6 py-4 text-right font-medium text-foreground">{currency} {row.balance.toLocaleString()}</td>
400
+ </tr>
401
+ ))}
402
+ {creditLedger.length === 0 && (
403
+ <tr><td colSpan={6} className="px-6 py-12 text-center text-muted-foreground">No ledger entries found.</td></tr>
404
+ )}
405
+ </tbody>
406
+ </table>
407
+ </div>
408
+ </div>
409
+ )}
410
+
411
+ {/* NOTES TAB */}
412
+ {activeTab === 'notes' && (
413
+ <div className="bg-[#111A2C] rounded-xl border border-border p-6 shadow-sm max-w-3xl">
414
+ <h3 className="font-semibold text-foreground mb-4 flex items-center gap-2">
415
+ <FileText className="w-5 h-5 text-brand-400" />
416
+ Customer Notes & Remarks
417
+ </h3>
418
+ <textarea
419
+ className="w-full min-h-[250px] bg-background/50 border border-border rounded-lg p-4 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-brand-500/50 resize-y"
420
+ placeholder="Enter internal notes about this customer..."
421
+ value={notes}
422
+ onChange={(e) => setNotes(e.target.value)}
423
+ />
424
+ <div className="mt-4 flex justify-end">
425
+ <Button
426
+ onClick={handleSaveNotes}
427
+ disabled={isSavingNotes || notes === (customer.notes || '')}
428
+ className="bg-brand-500 hover:bg-brand-600 text-white min-w-[120px]"
429
+ >
430
+ {isSavingNotes ? 'Saving...' : 'Save Notes'}
431
+ </Button>
432
+ </div>
433
+ </div>
434
+ )}
435
+ </div>
436
+
437
+ {/* Add Payment SlideOver */}
438
+ <SlideOver
439
+ open={isPaymentSlideOpen}
440
+ onClose={() => setIsPaymentSlideOpen(false)}
441
+ title="Record Payment"
442
+ >
443
+ <form onSubmit={handleAddPayment} className="space-y-6 pt-4 flex flex-col h-full">
444
+ <div className="flex-1 space-y-6">
445
+ <div className="bg-brand-500/10 border border-brand-500/20 p-4 rounded-lg flex justify-between items-center">
446
+ <span className="text-sm font-medium text-brand-400">Current Balance</span>
447
+ <span className="text-xl font-bold text-foreground">{currency} {customer.balance?.toLocaleString()}</span>
448
+ </div>
449
+
450
+ <div className="space-y-2">
451
+ <label className="text-sm font-medium text-foreground">Amount ({currency})</label>
452
+ <Input
453
+ type="number"
454
+ step="0.01"
455
+ min="0.01"
456
+ required
457
+ value={paymentForm.amount}
458
+ onChange={(e) => setPaymentForm({...paymentForm, amount: e.target.value})}
459
+ placeholder="0.00"
460
+ className="text-lg font-medium"
461
+ />
462
+ </div>
463
+
464
+ <div className="space-y-2">
465
+ <label className="text-sm font-medium text-foreground">Payment Method</label>
466
+ <Select
467
+ value={paymentForm.method}
468
+ onChange={(val) => setPaymentForm({...paymentForm, method: val})}
469
+ options={[
470
+ { label: 'Cash', value: 'Cash' },
471
+ { label: 'Credit Card', value: 'Credit Card' },
472
+ { label: 'Bank Transfer', value: 'Bank Transfer' },
473
+ { label: 'Mobile Money', value: 'Mobile Money' },
474
+ ]}
475
+ />
476
+ </div>
477
+
478
+ <div className="space-y-2">
479
+ <label className="text-sm font-medium text-foreground">Reference Number (Optional)</label>
480
+ <Input
481
+ value={paymentForm.reference}
482
+ onChange={(e) => setPaymentForm({...paymentForm, reference: e.target.value})}
483
+ placeholder="Check No. / Transaction ID"
484
+ />
485
+ </div>
486
+ </div>
487
+
488
+ <div className="pt-6 border-t border-border flex justify-end gap-3 pb-8">
489
+ <Button type="button" variant="outline" onClick={() => setIsPaymentSlideOpen(false)}>
490
+ Cancel
491
+ </Button>
492
+ <Button type="submit" disabled={isSubmittingPayment || !paymentForm.amount} className="bg-brand-500 hover:bg-brand-600 text-white min-w-[120px]">
493
+ {isSubmittingPayment ? 'Processing...' : 'Record Payment'}
494
+ </Button>
495
+ </div>
496
+ </form>
497
+ </SlideOver>
498
+ </div>
499
+ );
500
+ }