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,472 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { X, Plus, Trash2, Edit2, Check, ToggleLeft, ToggleRight, TrendingUp, DollarSign, Smartphone, Banknote } from 'lucide-react';
5
+ import { usePaymentMethodsStore } from '@/store/payment-methods-store';
6
+ import type { PaymentMethod } from '@/store/payment-methods-store';
7
+ import { useAppStore } from '@/store/app-store';
8
+ import { formatMoney } from '@/lib/utils';
9
+
10
+ interface PaymentSettingsModalProps {
11
+ open: boolean;
12
+ onClose: () => void;
13
+ }
14
+
15
+ const EMOJI_OPTIONS = ['🏦', '📱', '💳', '💰', '🏧', '📲', '🪙', '💸', '🔄', '🛒'];
16
+ const COLOR_OPTIONS = ['#3B82F6', '#F59E0B', '#8B5CF6', '#EF4444', '#10B981', '#EC4899', '#06B6D4', '#F97316'];
17
+
18
+ export default function PaymentSettingsModal({ open, onClose }: PaymentSettingsModalProps) {
19
+ const { currency } = useAppStore();
20
+ const { methods, addMethod, updateMethod, deleteMethod, toggleMethod, completedSales, getSalesTotals, clearSalesHistory, requireCashSession, setRequireCashSession } = usePaymentMethodsStore();
21
+
22
+ const [activeTab, setActiveTab] = useState<'methods' | 'summary'>('methods');
23
+ const [isAdding, setIsAdding] = useState(false);
24
+ const [editingId, setEditingId] = useState<string | null>(null);
25
+
26
+ // Add form state
27
+ const [newName, setNewName] = useState('');
28
+ const [newLogo, setNewLogo] = useState('🏦');
29
+ const [newColor, setNewColor] = useState('#3B82F6');
30
+
31
+ // Edit form state
32
+ const [editName, setEditName] = useState('');
33
+ const [editLogo, setEditLogo] = useState('');
34
+ const [editColor, setEditColor] = useState('');
35
+
36
+ const mobileMethods = methods.filter((m) => m.type === 'mobile');
37
+ const totals = getSalesTotals();
38
+
39
+ const handleAdd = () => {
40
+ if (!newName.trim()) return;
41
+ addMethod({
42
+ name: newName.trim(),
43
+ type: 'mobile',
44
+ logo: newLogo,
45
+ enabled: true,
46
+ color: newColor,
47
+ });
48
+ setNewName('');
49
+ setNewLogo('🏦');
50
+ setNewColor('#3B82F6');
51
+ setIsAdding(false);
52
+ };
53
+
54
+ const startEdit = (method: PaymentMethod) => {
55
+ setEditingId(method.id);
56
+ setEditName(method.name);
57
+ setEditLogo(method.logo);
58
+ setEditColor(method.color);
59
+ };
60
+
61
+ const handleSaveEdit = () => {
62
+ if (!editingId || !editName.trim()) return;
63
+ updateMethod(editingId, {
64
+ name: editName.trim(),
65
+ logo: editLogo,
66
+ color: editColor,
67
+ });
68
+ setEditingId(null);
69
+ };
70
+
71
+ const handleDelete = (id: string) => {
72
+ if (id === 'cash') return; // Can't delete cash
73
+ deleteMethod(id);
74
+ };
75
+
76
+ if (!open) return null;
77
+
78
+ return (
79
+ <div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[60] flex items-center justify-center p-4">
80
+ <div className="bg-background rounded-[16px] max-w-[640px] w-[95%] max-h-[85vh] shadow-[0_25px_50px_rgba(0,0,0,0.25)] flex flex-col overflow-hidden">
81
+ {/* Header */}
82
+ <div className="flex items-center justify-between px-6 py-4 border-b border-border">
83
+ <h2 className="text-xl font-bold text-foreground">Payment Settings</h2>
84
+ <button onClick={onClose} className="p-2 hover:bg-surface rounded-full text-foreground/50 transition-colors">
85
+ <X size={22} />
86
+ </button>
87
+ </div>
88
+
89
+ {/* Tabs */}
90
+ <div className="flex border-b border-border">
91
+ <button
92
+ onClick={() => setActiveTab('methods')}
93
+ className={`flex-1 py-3 text-sm font-medium transition-colors ${
94
+ activeTab === 'methods'
95
+ ? 'text-pos-500 border-b-2 border-pos-500'
96
+ : 'text-foreground/50 hover:text-foreground/70'
97
+ }`}
98
+ >
99
+ Payment Methods
100
+ </button>
101
+ <button
102
+ onClick={() => setActiveTab('summary')}
103
+ className={`flex-1 py-3 text-sm font-medium transition-colors relative ${
104
+ activeTab === 'summary'
105
+ ? 'text-pos-500 border-b-2 border-pos-500'
106
+ : 'text-foreground/50 hover:text-foreground/70'
107
+ }`}
108
+ >
109
+ Sales Summary
110
+ {completedSales.length > 0 && (
111
+ <span className="ml-1.5 bg-pos-500 text-white text-[10px] font-bold rounded-full px-1.5 py-0.5">
112
+ {completedSales.length}
113
+ </span>
114
+ )}
115
+ </button>
116
+ </div>
117
+
118
+ {/* Content */}
119
+ <div className="flex-1 overflow-y-auto p-6">
120
+ {/* ===== METHODS TAB ===== */}
121
+ {activeTab === 'methods' && (
122
+ <div>
123
+ {/* Cash - always present, not editable */}
124
+ <div className="text-[11px] font-semibold text-foreground/40 uppercase tracking-wide mb-2">Cash Payment</div>
125
+ <div className="flex items-center justify-between bg-surface rounded-xl p-4 mb-6 border border-border">
126
+ <div className="flex items-center gap-3">
127
+ <div className="w-10 h-10 rounded-full bg-green-100 dark:bg-green-500/20 flex items-center justify-center text-xl">
128
+ 💵
129
+ </div>
130
+ <div>
131
+ <div className="font-semibold text-foreground">Cash</div>
132
+ <div className="text-[11px] text-foreground/40">Default payment method</div>
133
+ </div>
134
+ </div>
135
+ <span className="text-xs text-green-500 font-medium bg-green-100 dark:bg-green-500/20 px-2 py-1 rounded-full">
136
+ Always On
137
+ </span>
138
+ </div>
139
+
140
+ {/* Mobile Bank Methods */}
141
+ <div className="flex items-center justify-between mb-3">
142
+ <div className="text-[11px] font-semibold text-foreground/40 uppercase tracking-wide">Mobile Bank Methods</div>
143
+ <button
144
+ onClick={() => setIsAdding(true)}
145
+ className="flex items-center gap-1 text-xs font-medium text-pos-500 hover:text-pos-600 transition-colors"
146
+ >
147
+ <Plus size={14} /> Add New
148
+ </button>
149
+ </div>
150
+
151
+ <div className="space-y-2">
152
+ {mobileMethods.map((method) => (
153
+ <div key={method.id} className="bg-surface rounded-xl p-3 border border-border">
154
+ {editingId === method.id ? (
155
+ /* Edit mode */
156
+ <div className="space-y-3">
157
+ <div className="flex gap-2">
158
+ <input
159
+ type="text"
160
+ value={editName}
161
+ onChange={(e) => setEditName(e.target.value)}
162
+ className="flex-1 bg-background border border-border rounded-lg px-3 py-2 text-sm text-foreground focus:outline-none focus:border-pos-400"
163
+ placeholder="Method name"
164
+ />
165
+ <button onClick={handleSaveEdit} className="bg-pos-500 text-white px-3 py-2 rounded-lg hover:bg-pos-600 transition-colors">
166
+ <Check size={16} />
167
+ </button>
168
+ <button onClick={() => setEditingId(null)} className="bg-surface border border-border text-foreground/50 px-3 py-2 rounded-lg hover:bg-border transition-colors">
169
+ <X size={16} />
170
+ </button>
171
+ </div>
172
+ {/* Emoji picker */}
173
+ <div>
174
+ <div className="text-[10px] text-foreground/40 mb-1">Icon</div>
175
+ <div className="flex gap-1.5 flex-wrap">
176
+ {EMOJI_OPTIONS.map((emoji) => (
177
+ <button
178
+ key={emoji}
179
+ onClick={() => setEditLogo(emoji)}
180
+ className={`w-9 h-9 rounded-lg flex items-center justify-center text-lg transition-all ${
181
+ editLogo === emoji ? 'bg-pos-100 dark:bg-pos-500/20 border-2 border-pos-400 scale-110' : 'bg-background border border-border hover:bg-surface'
182
+ }`}
183
+ >
184
+ {emoji}
185
+ </button>
186
+ ))}
187
+ </div>
188
+ </div>
189
+ {/* Color picker */}
190
+ <div>
191
+ <div className="text-[10px] text-foreground/40 mb-1">Color</div>
192
+ <div className="flex gap-1.5 flex-wrap">
193
+ {COLOR_OPTIONS.map((color) => (
194
+ <button
195
+ key={color}
196
+ onClick={() => setEditColor(color)}
197
+ className={`w-7 h-7 rounded-full transition-all ${
198
+ editColor === color ? 'ring-2 ring-pos-500 ring-offset-2 ring-offset-background scale-110' : 'hover:scale-105'
199
+ }`}
200
+ style={{ backgroundColor: color }}
201
+ />
202
+ ))}
203
+ </div>
204
+ </div>
205
+ </div>
206
+ ) : (
207
+ /* Display mode */
208
+ <div className="flex items-center justify-between">
209
+ <div className="flex items-center gap-3">
210
+ <div
211
+ className="w-10 h-10 rounded-full flex items-center justify-center text-xl"
212
+ style={{ backgroundColor: method.color + '20' }}
213
+ >
214
+ {method.logo}
215
+ </div>
216
+ <div>
217
+ <div className={`font-semibold ${method.enabled ? 'text-foreground' : 'text-foreground/40 line-through'}`}>{method.name}</div>
218
+ <div className="text-[11px] text-foreground/40">Mobile Bank</div>
219
+ </div>
220
+ </div>
221
+ <div className="flex items-center gap-2">
222
+ <button
223
+ onClick={() => toggleMethod(method.id)}
224
+ className="text-foreground/40 hover:text-foreground/70 transition-colors"
225
+ title={method.enabled ? 'Disable' : 'Enable'}
226
+ >
227
+ {method.enabled ? (
228
+ <ToggleRight size={24} className="text-pos-500" />
229
+ ) : (
230
+ <ToggleLeft size={24} />
231
+ )}
232
+ </button>
233
+ <button
234
+ onClick={() => startEdit(method)}
235
+ className="p-1.5 hover:bg-background rounded-lg text-foreground/40 hover:text-foreground/70 transition-colors"
236
+ >
237
+ <Edit2 size={14} />
238
+ </button>
239
+ <button
240
+ onClick={() => handleDelete(method.id)}
241
+ className="p-1.5 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg text-foreground/40 hover:text-red-500 transition-colors"
242
+ >
243
+ <Trash2 size={14} />
244
+ </button>
245
+ </div>
246
+ </div>
247
+ )}
248
+ </div>
249
+ ))}
250
+
251
+ {mobileMethods.length === 0 && !isAdding && (
252
+ <div className="text-center py-6 text-foreground/30">
253
+ <Smartphone size={24} className="mx-auto mb-2 opacity-50" />
254
+ <p className="text-sm">No mobile bank methods</p>
255
+ </div>
256
+ )}
257
+ </div>
258
+
259
+ {/* Add New Form */}
260
+ {isAdding && (
261
+ <div className="mt-3 bg-surface rounded-xl p-4 border-2 border-dashed border-pos-300">
262
+ <div className="text-sm font-semibold text-foreground mb-3">New Mobile Bank</div>
263
+ <div className="space-y-3">
264
+ <input
265
+ type="text"
266
+ value={newName}
267
+ onChange={(e) => setNewName(e.target.value)}
268
+ className="w-full bg-background border border-border rounded-lg px-3 py-2 text-sm text-foreground placeholder:text-foreground/30 focus:outline-none focus:border-pos-400"
269
+ placeholder="e.g. MTN Mobile Money"
270
+ autoFocus
271
+ />
272
+ {/* Emoji picker */}
273
+ <div>
274
+ <div className="text-[10px] text-foreground/40 mb-1">Icon</div>
275
+ <div className="flex gap-1.5 flex-wrap">
276
+ {EMOJI_OPTIONS.map((emoji) => (
277
+ <button
278
+ key={emoji}
279
+ onClick={() => setNewLogo(emoji)}
280
+ className={`w-9 h-9 rounded-lg flex items-center justify-center text-lg transition-all ${
281
+ newLogo === emoji ? 'bg-pos-100 dark:bg-pos-500/20 border-2 border-pos-400 scale-110' : 'bg-background border border-border hover:bg-surface'
282
+ }`}
283
+ >
284
+ {emoji}
285
+ </button>
286
+ ))}
287
+ </div>
288
+ </div>
289
+ {/* Color picker */}
290
+ <div>
291
+ <div className="text-[10px] text-foreground/40 mb-1">Color</div>
292
+ <div className="flex gap-1.5 flex-wrap">
293
+ {COLOR_OPTIONS.map((color) => (
294
+ <button
295
+ key={color}
296
+ onClick={() => setNewColor(color)}
297
+ className={`w-7 h-7 rounded-full transition-all ${
298
+ newColor === color ? 'ring-2 ring-pos-500 ring-offset-2 ring-offset-background scale-110' : 'hover:scale-105'
299
+ }`}
300
+ style={{ backgroundColor: color }}
301
+ />
302
+ ))}
303
+ </div>
304
+ </div>
305
+ <div className="flex gap-2 pt-1">
306
+ <button
307
+ onClick={handleAdd}
308
+ disabled={!newName.trim()}
309
+ className="flex-1 bg-pos-500 text-white py-2 rounded-lg text-sm font-semibold hover:bg-pos-600 transition-colors disabled:opacity-40"
310
+ >
311
+ Add Method
312
+ </button>
313
+ <button
314
+ onClick={() => setIsAdding(false)}
315
+ className="bg-surface border border-border text-foreground/60 py-2 px-4 rounded-lg text-sm hover:bg-border transition-colors"
316
+ >
317
+ Cancel
318
+ </button>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ )}
323
+
324
+ {/* Register Shift Setting */}
325
+ <div className="mt-6 pt-4 border-t border-border">
326
+ <div className="text-[11px] font-semibold text-foreground/40 uppercase tracking-wide mb-2">Shift & Register Controls</div>
327
+ <div className="flex items-center justify-between bg-surface rounded-xl p-4 border border-border">
328
+ <div>
329
+ <div className="font-semibold text-foreground text-sm">Require Cash Register Shifts</div>
330
+ <div className="text-xs text-foreground/50 mt-0.5">Prompt cashier to open with float and close with count</div>
331
+ </div>
332
+ <button
333
+ onClick={() => setRequireCashSession(!requireCashSession)}
334
+ className="text-foreground/40 hover:text-foreground/70 transition-colors ml-4"
335
+ title={requireCashSession ? 'Enabled' : 'Disabled'}
336
+ >
337
+ {requireCashSession ? (
338
+ <ToggleRight size={28} className="text-pos-500" />
339
+ ) : (
340
+ <ToggleLeft size={28} />
341
+ )}
342
+ </button>
343
+ </div>
344
+ </div>
345
+ </div>
346
+ )}
347
+
348
+ {/* ===== SALES SUMMARY TAB ===== */}
349
+ {activeTab === 'summary' && (
350
+ <div>
351
+ {/* Overview Cards */}
352
+ <div className="grid grid-cols-3 gap-3 mb-6">
353
+ <div className="bg-surface rounded-xl p-4 border border-border text-center">
354
+ <div className="text-[10px] font-semibold text-foreground/40 uppercase mb-1">Total Sales</div>
355
+ <div className="text-xl font-black text-foreground">{totals.totalCount}</div>
356
+ <div className="text-sm font-semibold text-pos-500 mt-1">{formatMoney(totals.grandTotal, currency)}</div>
357
+ </div>
358
+ <div className="bg-green-50 dark:bg-green-500/10 rounded-xl p-4 border border-green-200 dark:border-green-500/20 text-center">
359
+ <div className="text-[10px] font-semibold text-green-600 dark:text-green-400 uppercase mb-1">Cash</div>
360
+ <div className="text-xl font-black text-green-700 dark:text-green-300">{totals.byMethod['cash']?.count || 0}</div>
361
+ <div className="text-sm font-semibold text-green-600 dark:text-green-400 mt-1">{formatMoney(totals.cash, currency)}</div>
362
+ </div>
363
+ <div className="bg-blue-50 dark:bg-blue-500/10 rounded-xl p-4 border border-blue-200 dark:border-blue-500/20 text-center">
364
+ <div className="text-[10px] font-semibold text-blue-600 dark:text-blue-400 uppercase mb-1">Mobile</div>
365
+ <div className="text-xl font-black text-blue-700 dark:text-blue-300">{Object.values(totals.byMethod).filter((_, i) => i > 0).reduce((sum, m) => sum + m.count, 0) || (totals.totalCount - (totals.byMethod['cash']?.count || 0))}</div>
366
+ <div className="text-sm font-semibold text-blue-600 dark:text-blue-400 mt-1">{formatMoney(totals.mobile, currency)}</div>
367
+ </div>
368
+ </div>
369
+
370
+ {/* Per-method breakdown */}
371
+ <div className="text-[11px] font-semibold text-foreground/40 uppercase tracking-wide mb-3">Breakdown by Method</div>
372
+ <div className="space-y-2 mb-6">
373
+ {Object.entries(totals.byMethod).map(([methodId, data]) => {
374
+ const method = methods.find((m) => m.id === methodId);
375
+ const percentage = totals.grandTotal > 0 ? (data.total / totals.grandTotal) * 100 : 0;
376
+ return (
377
+ <div key={methodId} className="bg-surface rounded-xl p-3 border border-border">
378
+ <div className="flex items-center justify-between mb-2">
379
+ <div className="flex items-center gap-2">
380
+ <div
381
+ className="w-8 h-8 rounded-full flex items-center justify-center text-lg"
382
+ style={{ backgroundColor: (method?.color || '#6B7280') + '20' }}
383
+ >
384
+ {method?.logo || '💳'}
385
+ </div>
386
+ <div>
387
+ <div className="text-sm font-semibold text-foreground">{data.name}</div>
388
+ <div className="text-[10px] text-foreground/40">{data.count} sales</div>
389
+ </div>
390
+ </div>
391
+ <div className="text-right">
392
+ <div className="text-sm font-bold text-foreground">{formatMoney(data.total, currency)}</div>
393
+ <div className="text-[10px] text-foreground/40">{percentage.toFixed(1)}%</div>
394
+ </div>
395
+ </div>
396
+ {/* Progress bar */}
397
+ <div className="w-full h-1.5 bg-border rounded-full overflow-hidden">
398
+ <div
399
+ className="h-full rounded-full transition-all duration-500"
400
+ style={{
401
+ width: `${percentage}%`,
402
+ backgroundColor: method?.color || '#6B7280',
403
+ }}
404
+ />
405
+ </div>
406
+ </div>
407
+ );
408
+ })}
409
+
410
+ {Object.keys(totals.byMethod).length === 0 && (
411
+ <div className="text-center py-8 text-foreground/30">
412
+ <TrendingUp size={28} className="mx-auto mb-2 opacity-50" />
413
+ <p className="text-sm">No sales recorded yet</p>
414
+ <p className="text-[11px] mt-1">Complete a sale to see the breakdown</p>
415
+ </div>
416
+ )}
417
+ </div>
418
+
419
+ {/* Recent sales */}
420
+ {completedSales.length > 0 && (
421
+ <>
422
+ <div className="flex items-center justify-between mb-3">
423
+ <div className="text-[11px] font-semibold text-foreground/40 uppercase tracking-wide">
424
+ Recent Sales ({completedSales.length})
425
+ </div>
426
+ <button
427
+ onClick={() => {
428
+ if (confirm('Clear all sales history?')) clearSalesHistory();
429
+ }}
430
+ className="text-[11px] text-red-500 hover:text-red-600 font-medium"
431
+ >
432
+ Clear History
433
+ </button>
434
+ </div>
435
+ <div className="space-y-1.5 max-h-[200px] overflow-y-auto">
436
+ {[...completedSales].reverse().slice(0, 20).map((sale) => {
437
+ const method = methods.find((m) => m.id === sale.paymentMethodId);
438
+ return (
439
+ <div key={sale.id} className="flex items-center justify-between bg-surface rounded-lg p-2.5 border border-border">
440
+ <div className="flex items-center gap-2 flex-1 min-w-0">
441
+ <div
442
+ className="w-7 h-7 rounded-full flex items-center justify-center text-sm shrink-0"
443
+ style={{ backgroundColor: (method?.color || '#6B7280') + '20' }}
444
+ >
445
+ {method?.logo || '💳'}
446
+ </div>
447
+ <div className="min-w-0">
448
+ <div className="text-xs font-medium text-foreground truncate">
449
+ {sale.items.length} item{sale.items.length > 1 ? 's' : ''}
450
+ {sale.customerName && ` • ${sale.customerName}`}
451
+ </div>
452
+ <div className="text-[10px] text-foreground/40">
453
+ {sale.paymentMethodName} • {new Date(sale.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
454
+ </div>
455
+ </div>
456
+ </div>
457
+ <span className="text-sm font-bold text-foreground shrink-0 ml-2">
458
+ {formatMoney(sale.total, currency)}
459
+ </span>
460
+ </div>
461
+ );
462
+ })}
463
+ </div>
464
+ </>
465
+ )}
466
+ </div>
467
+ )}
468
+ </div>
469
+ </div>
470
+ </div>
471
+ );
472
+ }
@@ -0,0 +1,140 @@
1
+ 'use client';
2
+
3
+ import { useState, useMemo } from 'react';
4
+ import { Star, MoreHorizontal } from 'lucide-react';
5
+ import { useAppStore } from '@/store/app-store';
6
+ import { formatMoney } from '@/lib/utils';
7
+
8
+ export interface Product {
9
+ id: string;
10
+ medicine_id: string;
11
+ name: string;
12
+ category: string;
13
+ price: number;
14
+ stock: number;
15
+ batch: string;
16
+ lot_number: string;
17
+ expiry: string;
18
+ requires_prescription: boolean;
19
+ favorite: boolean;
20
+ }
21
+
22
+ interface ProductCatalogProps {
23
+ products: Product[];
24
+ categories: string[];
25
+ onProductClick: (product: Product) => void;
26
+ isLoading?: boolean;
27
+ }
28
+
29
+ export default function ProductCatalog({ products, categories, onProductClick, isLoading }: ProductCatalogProps) {
30
+ const [activeCategory, setActiveCategory] = useState('All');
31
+ const { currency } = useAppStore();
32
+
33
+ const filteredProducts = useMemo(() => {
34
+ if (activeCategory === 'All') return products;
35
+ return products.filter((p) => p.category === activeCategory);
36
+ }, [products, activeCategory]);
37
+
38
+ if (isLoading) {
39
+ return (
40
+ <div className="flex-1 flex flex-col bg-gray-100 dark:bg-[#0B1121] overflow-hidden">
41
+ {/* Skeleton category bar */}
42
+ <div className="flex overflow-x-auto bg-white dark:bg-[#111A2C] border-b border-gray-200 dark:border-gray-800 p-2 gap-2">
43
+ {Array.from({ length: 6 }).map((_, i) => (
44
+ <div key={i} className="h-8 w-20 bg-gray-200 dark:bg-gray-700 rounded-md animate-pulse" />
45
+ ))}
46
+ </div>
47
+ {/* Skeleton product grid */}
48
+ <div className="flex-1 overflow-y-auto p-2 md:p-4">
49
+ <div className="grid grid-cols-2 md:grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-2 md:gap-4">
50
+ {Array.from({ length: 12 }).map((_, i) => (
51
+ <div key={i} className="bg-white dark:bg-[#1A2438] rounded-xl p-4 min-h-[110px] flex flex-col justify-between animate-pulse border border-gray-200 dark:border-transparent">
52
+ <div className="h-4 bg-gray-200 dark:bg-gray-600 rounded w-3/4 mb-2" />
53
+ <div className="h-3 bg-gray-200 dark:bg-gray-600 rounded w-1/2 mb-4" />
54
+ <div className="flex justify-between">
55
+ <div className="h-4 bg-gray-200 dark:bg-gray-600 rounded w-1/3" />
56
+ <div className="h-3 w-3 bg-gray-200 dark:bg-gray-600 rounded-full" />
57
+ </div>
58
+ </div>
59
+ ))}
60
+ </div>
61
+ </div>
62
+ </div>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <div className="flex-1 flex flex-col bg-gray-100 dark:bg-[#0B1121] overflow-hidden">
68
+ {/* Category Bar */}
69
+ <div className="flex overflow-x-auto bg-white dark:bg-[#111A2C] border-b border-gray-200 dark:border-gray-800 p-0 m-0 scroll-smooth no-scrollbar">
70
+ {categories.map((cat) => (
71
+ <button
72
+ key={cat}
73
+ onClick={() => setActiveCategory(cat)}
74
+ className={`
75
+ px-6 py-3 text-sm whitespace-nowrap transition-colors border-none outline-none cursor-pointer border-b-2
76
+ ${
77
+ activeCategory === cat
78
+ ? 'bg-pos-500 text-white dark:bg-blue-500/10 dark:text-blue-400 font-semibold border-pos-500 dark:border-blue-500'
79
+ : 'bg-transparent text-gray-600 dark:text-gray-400 font-medium hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-white/5 border-transparent'
80
+ }
81
+ `}
82
+ >
83
+ {cat}
84
+ </button>
85
+ ))}
86
+ {categories.length > 5 && (
87
+ <button className="px-4 py-3 border-l border-gray-200 dark:border-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-white/5 hover:text-gray-700 dark:hover:text-gray-200">
88
+ <MoreHorizontal size={18} />
89
+ </button>
90
+ )}
91
+ </div>
92
+
93
+ {/* Product Grid */}
94
+ <div className="flex-1 overflow-y-auto p-2 md:p-4">
95
+ <div className="grid grid-cols-2 md:grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-2 md:gap-4">
96
+ {filteredProducts.map((product) => (
97
+ <div
98
+ key={product.id}
99
+ onClick={() => product.stock > 0 ? onProductClick(product) : null}
100
+ className={`bg-white dark:bg-[#1A2438] hover:bg-gray-50 dark:hover:bg-[#23304a] transition-all duration-200 rounded-xl p-4 min-h-[110px] flex flex-col justify-between border border-gray-200 dark:border-transparent hover:border-pos-500/50 dark:hover:border-blue-500/50 shadow-sm ${
101
+ product.stock === 0 ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
102
+ }`}
103
+ >
104
+ <div className="flex justify-between items-start">
105
+ <span className="text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-2 pr-2 leading-snug">
106
+ {product.name}
107
+ </span>
108
+ {product.requires_prescription && (
109
+ <span className="text-[10px] font-bold text-pos-600 bg-pos-100 dark:text-blue-400 dark:bg-blue-500/10 px-1.5 py-0.5 rounded flex-shrink-0">
110
+ Rx
111
+ </span>
112
+ )}
113
+ </div>
114
+
115
+ <div className="mt-4 flex items-center justify-between w-full">
116
+ <span className="text-sm font-semibold text-pos-600 dark:text-blue-400">
117
+ {formatMoney(product.price, currency)}
118
+ </span>
119
+ {product.stock === 0 ? (
120
+ <span className="text-[10px] font-bold text-red-500 bg-red-100 dark:bg-red-500/20 px-2 py-0.5 rounded">
121
+ OUT OF STOCK
122
+ </span>
123
+ ) : product.stock <= 10 ? (
124
+ <span className="text-[10px] font-medium text-orange-500">
125
+ Only {product.stock} left
126
+ </span>
127
+ ) : (
128
+ <span className="text-[10px] text-foreground/40 flex items-center gap-1">
129
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 inline-block"></span>
130
+ {product.stock}
131
+ </span>
132
+ )}
133
+ </div>
134
+ </div>
135
+ ))}
136
+ </div>
137
+ </div>
138
+ </div>
139
+ );
140
+ }