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,463 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import { usePosStore } from '@/store/pos-store';
|
|
5
|
+
import { useAppStore } from '@/store/app-store';
|
|
6
|
+
import { formatMoney } from '@/lib/utils';
|
|
7
|
+
import { api } from '@/lib/api';
|
|
8
|
+
import CartPanel from '@/components/pos/CartPanel';
|
|
9
|
+
import ProductCatalog from '@/components/pos/ProductCatalog';
|
|
10
|
+
import PaymentModal from '@/components/pos/PaymentModal';
|
|
11
|
+
import PaymentSettingsModal from '@/components/pos/PaymentSettingsModal';
|
|
12
|
+
import { CashSessionGuard } from '@/components/pos/CashSessionGuard';
|
|
13
|
+
import { CloseSessionModal } from '@/components/pos/CloseSessionModal';
|
|
14
|
+
import { SaleReceipt } from '@/components/pos/SaleReceipt';
|
|
15
|
+
import type { Product } from '@/components/pos/ProductCatalog';
|
|
16
|
+
import {
|
|
17
|
+
Menu,
|
|
18
|
+
Search,
|
|
19
|
+
ScanBarcode,
|
|
20
|
+
ChevronDown,
|
|
21
|
+
User,
|
|
22
|
+
X,
|
|
23
|
+
Store,
|
|
24
|
+
Sun,
|
|
25
|
+
Moon,
|
|
26
|
+
LogOut,
|
|
27
|
+
Calculator,
|
|
28
|
+
} from 'lucide-react';
|
|
29
|
+
import { useRouter } from '@/i18n/routing';
|
|
30
|
+
import toast from 'react-hot-toast';
|
|
31
|
+
|
|
32
|
+
export default function PosPage() {
|
|
33
|
+
return (
|
|
34
|
+
<CashSessionGuard>
|
|
35
|
+
{(session) => <PosContent session={session} />}
|
|
36
|
+
</CashSessionGuard>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function PosContent({ session }: { session: { id: string; opening_amount: number; opened_at: string } }) {
|
|
41
|
+
const [search, setSearch] = useState('');
|
|
42
|
+
const [paymentMode, setPaymentMode] = useState(false);
|
|
43
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
44
|
+
const [showDropdown, setShowDropdown] = useState(false);
|
|
45
|
+
const [closeSessionOpen, setCloseSessionOpen] = useState(false);
|
|
46
|
+
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
|
47
|
+
const [mobileTab, setMobileTab] = useState<'cart' | 'products'>('products');
|
|
48
|
+
|
|
49
|
+
// Receipt State
|
|
50
|
+
const [receiptOpen, setReceiptOpen] = useState(false);
|
|
51
|
+
const [saleReceiptData, setSaleReceiptData] = useState<any>(null);
|
|
52
|
+
|
|
53
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
54
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
55
|
+
const userMenuRef = useRef<HTMLDivElement>(null);
|
|
56
|
+
const router = useRouter();
|
|
57
|
+
const { currency, theme, setTheme } = useAppStore();
|
|
58
|
+
|
|
59
|
+
const [products, setProducts] = useState<Product[]>([]);
|
|
60
|
+
const [categories, setCategories] = useState<string[]>(['All']);
|
|
61
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
62
|
+
|
|
63
|
+
const fetchPosData = useCallback(async () => {
|
|
64
|
+
try {
|
|
65
|
+
setIsLoading(true);
|
|
66
|
+
const [medicines, stockItems, categoriesTree]: [any, any, any] = await Promise.all([
|
|
67
|
+
api.getMedicines().catch(() => []),
|
|
68
|
+
api.getStock().catch(() => []),
|
|
69
|
+
api.getCategoryTree().catch(() => [])
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const catMap = new Map<string, string>();
|
|
73
|
+
const addCats = (nodes: any[]) => {
|
|
74
|
+
nodes.forEach((node: any) => {
|
|
75
|
+
catMap.set(node.id, node.name_en);
|
|
76
|
+
if (node.children) addCats(node.children);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
if (Array.isArray(categoriesTree)) {
|
|
80
|
+
addCats(categoriesTree);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const stockMap = new Map<string, number>();
|
|
84
|
+
if (Array.isArray(stockItems)) {
|
|
85
|
+
stockItems.forEach((item: any) => {
|
|
86
|
+
if (item?.medicine?.id) {
|
|
87
|
+
stockMap.set(item.medicine.id, item.total_quantity);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const medList = Array.isArray(medicines) ? medicines : [];
|
|
93
|
+
const realProducts: Product[] = medList.map((med: any) => ({
|
|
94
|
+
id: med.id,
|
|
95
|
+
medicine_id: med.id,
|
|
96
|
+
name: med.name_en || med.name || 'Unknown',
|
|
97
|
+
category: med.category_id ? (catMap.get(med.category_id) || 'Other') : 'Other',
|
|
98
|
+
price: Number(med.selling_price) || 0,
|
|
99
|
+
stock: stockMap.get(med.id) || 0,
|
|
100
|
+
batch: '-',
|
|
101
|
+
lot_number: '-',
|
|
102
|
+
expiry: '-',
|
|
103
|
+
requires_prescription: med.requires_prescription || false,
|
|
104
|
+
favorite: false
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
const catSet = new Set<string>(['All']);
|
|
108
|
+
realProducts.forEach(p => catSet.add(p.category));
|
|
109
|
+
|
|
110
|
+
setProducts(realProducts);
|
|
111
|
+
setCategories(Array.from(catSet));
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('Failed to fetch POS data', error);
|
|
114
|
+
setProducts([]);
|
|
115
|
+
setCategories(['All']);
|
|
116
|
+
} finally {
|
|
117
|
+
setIsLoading(false);
|
|
118
|
+
}
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
fetchPosData();
|
|
123
|
+
}, [fetchPosData]);
|
|
124
|
+
|
|
125
|
+
// Store
|
|
126
|
+
const addItem = usePosStore((s) => s.addItem);
|
|
127
|
+
const cart = usePosStore((s) => s.cart);
|
|
128
|
+
const total = usePosStore((s) => s.total);
|
|
129
|
+
|
|
130
|
+
// Search filtering
|
|
131
|
+
const searchResults =
|
|
132
|
+
search.length > 1
|
|
133
|
+
? products.filter((p) => p.name.toLowerCase().includes(search.toLowerCase()))
|
|
134
|
+
: [];
|
|
135
|
+
|
|
136
|
+
// Product click handler
|
|
137
|
+
const handleProductClick = useCallback(
|
|
138
|
+
(product: Product) => {
|
|
139
|
+
if (product.stock <= 0) {
|
|
140
|
+
toast.error(`${product.name} is out of stock`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
addItem({
|
|
144
|
+
medicine_id: product.medicine_id,
|
|
145
|
+
name: product.name,
|
|
146
|
+
batch: product.batch,
|
|
147
|
+
lot_number: product.lot_number,
|
|
148
|
+
quantity: 1,
|
|
149
|
+
unit_price: product.price,
|
|
150
|
+
});
|
|
151
|
+
setSearch('');
|
|
152
|
+
setShowDropdown(false);
|
|
153
|
+
},
|
|
154
|
+
[addItem]
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Barcode Scanner Listener (Detect hardware scanners fast-typing + enter)
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
let barcodeBuffer = '';
|
|
160
|
+
let lastKeyTime = Date.now();
|
|
161
|
+
|
|
162
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
163
|
+
// Global keyboard shortcuts
|
|
164
|
+
if (e.key === 'F2') {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
searchInputRef.current?.focus();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (e.key === 'F9' && cart.length > 0) {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
setPaymentMode(true);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (e.key === 'Escape') {
|
|
175
|
+
if (paymentMode) setPaymentMode(false);
|
|
176
|
+
if (showDropdown) setShowDropdown(false);
|
|
177
|
+
if (userMenuOpen) setUserMenuOpen(false);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// If user is focused on an input/textarea (not scanning outside), skip global barcode hook
|
|
182
|
+
const activeTag = document.activeElement?.tagName.toLowerCase();
|
|
183
|
+
const isInputActive = activeTag === 'input' || activeTag === 'textarea';
|
|
184
|
+
|
|
185
|
+
const currentTime = Date.now();
|
|
186
|
+
const timeDiff = currentTime - lastKeyTime;
|
|
187
|
+
lastKeyTime = currentTime;
|
|
188
|
+
|
|
189
|
+
// Barcode scanners usually send characters within 30-50ms of each other
|
|
190
|
+
if (timeDiff > 80) {
|
|
191
|
+
barcodeBuffer = '';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (e.key === 'Enter') {
|
|
195
|
+
if (barcodeBuffer.length >= 3) {
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
const scannedCode = barcodeBuffer.trim();
|
|
198
|
+
barcodeBuffer = '';
|
|
199
|
+
|
|
200
|
+
// Look up scanned barcode in products list or via API search
|
|
201
|
+
const found = products.find(p => p.id === scannedCode || p.name.toLowerCase() === scannedCode.toLowerCase());
|
|
202
|
+
if (found) {
|
|
203
|
+
handleProductClick(found);
|
|
204
|
+
toast.success(`Scanned: ${found.name}`);
|
|
205
|
+
} else {
|
|
206
|
+
api.searchMedicines(scannedCode)
|
|
207
|
+
.then((res: any) => {
|
|
208
|
+
if (Array.isArray(res) && res.length > 0) {
|
|
209
|
+
const match = products.find(p => p.medicine_id === res[0].id);
|
|
210
|
+
if (match) {
|
|
211
|
+
handleProductClick(match);
|
|
212
|
+
toast.success(`Scanned: ${match.name}`);
|
|
213
|
+
} else {
|
|
214
|
+
toast.error(`Scanned item not in stock catalog`);
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
toast.error(`Barcode not found: ${scannedCode}`);
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
.catch(() => {
|
|
221
|
+
toast.error(`Product not found for code: ${scannedCode}`);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} else if (e.key.length === 1 && !isInputActive) {
|
|
226
|
+
barcodeBuffer += e.key;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
231
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
232
|
+
}, [cart.length, paymentMode, showDropdown, userMenuOpen, products, handleProductClick]);
|
|
233
|
+
|
|
234
|
+
// Click outside to close dropdowns
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
237
|
+
if (
|
|
238
|
+
dropdownRef.current &&
|
|
239
|
+
!dropdownRef.current.contains(e.target as Node) &&
|
|
240
|
+
searchInputRef.current &&
|
|
241
|
+
!searchInputRef.current.contains(e.target as Node)
|
|
242
|
+
) {
|
|
243
|
+
setShowDropdown(false);
|
|
244
|
+
}
|
|
245
|
+
if (
|
|
246
|
+
userMenuRef.current &&
|
|
247
|
+
!userMenuRef.current.contains(e.target as Node)
|
|
248
|
+
) {
|
|
249
|
+
setUserMenuOpen(false);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
253
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
254
|
+
}, []);
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<div className="flex flex-col h-full bg-background overflow-hidden">
|
|
258
|
+
{/* ===== HEADER ===== */}
|
|
259
|
+
<header className="h-[60px] bg-background border-b border-border flex items-center justify-between px-2 md:px-4 z-10 shrink-0 gap-2">
|
|
260
|
+
<div className="flex items-center gap-1 md:gap-3 shrink-0">
|
|
261
|
+
<button className="p-1.5 md:p-2 text-foreground hover:bg-surface rounded-lg transition-colors md:hidden">
|
|
262
|
+
<Menu size={20} />
|
|
263
|
+
</button>
|
|
264
|
+
<button
|
|
265
|
+
onClick={() => router.push('/dashboard')}
|
|
266
|
+
className="hidden sm:flex items-center gap-1.5 text-sm font-medium text-foreground/70 hover:text-foreground border border-border hover:bg-surface px-3 py-1.5 rounded-md transition-colors"
|
|
267
|
+
>
|
|
268
|
+
Exit POS <span className="text-foreground/40 font-serif">→</span>
|
|
269
|
+
</button>
|
|
270
|
+
{session?.id && (
|
|
271
|
+
<div className="hidden lg:flex items-center gap-2 px-2.5 py-1 bg-emerald-500/10 border border-emerald-500/20 text-emerald-600 dark:text-emerald-400 text-xs font-medium rounded-full">
|
|
272
|
+
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
|
273
|
+
<span>Register Open</span>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<div className="hidden md:flex items-center gap-2 text-pos-500 dark:text-pos-300 font-bold text-lg lg:text-xl shrink-0">
|
|
279
|
+
<Store size={24} className="text-pos-500 dark:text-pos-300" />
|
|
280
|
+
<span>Pharmacy POS</span>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div className="flex items-center gap-1.5 md:gap-3 flex-1 md:flex-none justify-end">
|
|
284
|
+
<div className="relative flex-1 max-w-[400px] md:w-[260px]">
|
|
285
|
+
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-foreground/40" />
|
|
286
|
+
<input
|
|
287
|
+
ref={searchInputRef}
|
|
288
|
+
type="text"
|
|
289
|
+
value={search}
|
|
290
|
+
onChange={(e) => {
|
|
291
|
+
setSearch(e.target.value);
|
|
292
|
+
setShowDropdown(true);
|
|
293
|
+
}}
|
|
294
|
+
onFocus={() => setShowDropdown(true)}
|
|
295
|
+
placeholder="Search / Scan barcode... (F2)"
|
|
296
|
+
className="w-full bg-surface border border-border text-foreground rounded-lg py-2 pl-9 pr-3 text-sm focus:outline-none focus:border-pos-400 focus:ring-1 focus:ring-pos-200 placeholder:text-foreground/40 transition-shadow"
|
|
297
|
+
/>
|
|
298
|
+
|
|
299
|
+
{showDropdown && searchResults.length > 0 && (
|
|
300
|
+
<div
|
|
301
|
+
ref={dropdownRef}
|
|
302
|
+
className="absolute top-full left-0 right-0 mt-1 bg-background border border-border rounded-lg shadow-[0_10px_25px_rgba(0,0,0,0.15)] dark:shadow-[0_10px_25px_rgba(0,0,0,0.4)] max-h-[320px] overflow-y-auto z-50"
|
|
303
|
+
>
|
|
304
|
+
{searchResults.map((p) => (
|
|
305
|
+
<div
|
|
306
|
+
key={p.id}
|
|
307
|
+
onClick={() => handleProductClick(p)}
|
|
308
|
+
className="p-3 border-b border-border hover:bg-surface cursor-pointer flex justify-between items-center last:border-b-0"
|
|
309
|
+
>
|
|
310
|
+
<div className="flex flex-col">
|
|
311
|
+
<span className="font-medium text-sm text-foreground">{p.name}</span>
|
|
312
|
+
<span className="text-xs text-foreground/50">
|
|
313
|
+
Stock: {p.stock} | {p.stock === 0 ? 'Out of stock' : 'In stock'}
|
|
314
|
+
</span>
|
|
315
|
+
</div>
|
|
316
|
+
<span className="font-bold text-sm text-pos-600">
|
|
317
|
+
{formatMoney(p.price, currency)}
|
|
318
|
+
</span>
|
|
319
|
+
</div>
|
|
320
|
+
))}
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<button
|
|
326
|
+
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
|
|
327
|
+
className="p-1.5 md:p-2 bg-transparent border border-border rounded-lg text-foreground hover:bg-surface transition-colors shrink-0"
|
|
328
|
+
title={theme === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
|
|
329
|
+
>
|
|
330
|
+
{theme === 'dark' ? <Sun size={18} className="md:w-5 md:h-5" /> : <Moon size={18} className="md:w-5 md:h-5" />}
|
|
331
|
+
</button>
|
|
332
|
+
|
|
333
|
+
<button
|
|
334
|
+
onClick={() => searchInputRef.current?.focus()}
|
|
335
|
+
className="p-1.5 md:p-2 bg-transparent border border-border rounded-lg text-foreground hover:bg-surface transition-colors shrink-0"
|
|
336
|
+
title="Scan Barcode (Focus Search)"
|
|
337
|
+
>
|
|
338
|
+
<ScanBarcode size={18} className="md:w-5 md:h-5" />
|
|
339
|
+
</button>
|
|
340
|
+
|
|
341
|
+
{/* User Menu & Close Register */}
|
|
342
|
+
<div className="relative shrink-0" ref={userMenuRef}>
|
|
343
|
+
<button
|
|
344
|
+
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
|
345
|
+
className="flex items-center gap-1 md:gap-2 hover:bg-surface rounded-lg p-1 md:pr-2 transition-colors border border-transparent hover:border-border"
|
|
346
|
+
>
|
|
347
|
+
<div className="w-7 h-7 md:w-8 md:h-8 rounded-full bg-pos-500 text-white flex items-center justify-center font-bold text-xs md:text-sm">
|
|
348
|
+
A
|
|
349
|
+
</div>
|
|
350
|
+
<span className="text-sm font-medium text-foreground/80 hidden lg:block">Administrator</span>
|
|
351
|
+
<ChevronDown size={14} className="text-foreground/40 md:w-4 md:h-4" />
|
|
352
|
+
</button>
|
|
353
|
+
|
|
354
|
+
{userMenuOpen && (
|
|
355
|
+
<div className="absolute right-0 mt-2 w-48 bg-surface border border-border rounded-xl shadow-xl py-1.5 z-50">
|
|
356
|
+
{session?.id ? (
|
|
357
|
+
<>
|
|
358
|
+
<div className="px-3 py-2 border-b border-border text-xs text-foreground/50">
|
|
359
|
+
Opening Float: <span className="font-semibold text-foreground">{formatMoney(session.opening_amount, currency)}</span>
|
|
360
|
+
</div>
|
|
361
|
+
<button
|
|
362
|
+
onClick={() => {
|
|
363
|
+
setUserMenuOpen(false);
|
|
364
|
+
setCloseSessionOpen(true);
|
|
365
|
+
}}
|
|
366
|
+
className="w-full px-3 py-2 text-left text-sm text-foreground hover:bg-background flex items-center gap-2 text-orange-500 font-medium transition-colors"
|
|
367
|
+
>
|
|
368
|
+
<Calculator size={16} />
|
|
369
|
+
Close Register
|
|
370
|
+
</button>
|
|
371
|
+
</>
|
|
372
|
+
) : null}
|
|
373
|
+
<button
|
|
374
|
+
onClick={() => {
|
|
375
|
+
setUserMenuOpen(false);
|
|
376
|
+
router.push('/dashboard');
|
|
377
|
+
}}
|
|
378
|
+
className="w-full px-3 py-2 text-left text-sm text-foreground hover:bg-background flex items-center gap-2 transition-colors"
|
|
379
|
+
>
|
|
380
|
+
<LogOut size={16} />
|
|
381
|
+
Exit to Dashboard
|
|
382
|
+
</button>
|
|
383
|
+
</div>
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
</header>
|
|
388
|
+
|
|
389
|
+
{/* ===== MAIN CONTENT ===== */}
|
|
390
|
+
<div className="flex flex-col md:flex-row flex-1 overflow-hidden h-full">
|
|
391
|
+
|
|
392
|
+
{/* Mobile Tabs */}
|
|
393
|
+
<div className="md:hidden flex border-b border-border bg-background shrink-0">
|
|
394
|
+
<button
|
|
395
|
+
onClick={() => setMobileTab('products')}
|
|
396
|
+
className={`flex-1 py-3 text-sm font-semibold transition-colors ${mobileTab === 'products' ? 'text-pos-600 border-b-2 border-pos-600' : 'text-foreground/60'}`}
|
|
397
|
+
>
|
|
398
|
+
Products
|
|
399
|
+
</button>
|
|
400
|
+
<button
|
|
401
|
+
onClick={() => setMobileTab('cart')}
|
|
402
|
+
className={`flex-1 py-3 text-sm font-semibold transition-colors ${mobileTab === 'cart' ? 'text-pos-600 border-b-2 border-pos-600' : 'text-foreground/60'}`}
|
|
403
|
+
>
|
|
404
|
+
Cart ({cart.length})
|
|
405
|
+
</button>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
{/* Left: Cart Panel */}
|
|
409
|
+
<div className={`w-full md:w-5/12 lg:w-[35%] h-full flex-col ${mobileTab === 'cart' ? 'flex' : 'hidden md:flex'}`}>
|
|
410
|
+
<CartPanel onPayment={() => setPaymentMode(true)} products={products} />
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
{/* Right: Product Catalog */}
|
|
414
|
+
<div className={`w-full md:w-7/12 lg:w-[65%] h-full flex-col ${mobileTab === 'products' ? 'flex' : 'hidden md:flex'}`}>
|
|
415
|
+
<ProductCatalog
|
|
416
|
+
products={products}
|
|
417
|
+
categories={categories}
|
|
418
|
+
onProductClick={handleProductClick}
|
|
419
|
+
isLoading={isLoading}
|
|
420
|
+
/>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
{/* ===== PAYMENT MODAL ===== */}
|
|
425
|
+
<PaymentModal
|
|
426
|
+
open={paymentMode}
|
|
427
|
+
onClose={() => setPaymentMode(false)}
|
|
428
|
+
onSettingsOpen={() => { setPaymentMode(false); setSettingsOpen(true); }}
|
|
429
|
+
onSaleComplete={fetchPosData}
|
|
430
|
+
cashSessionId={session.id}
|
|
431
|
+
onSaleSuccess={(receiptData) => {
|
|
432
|
+
setSaleReceiptData(receiptData);
|
|
433
|
+
setReceiptOpen(true);
|
|
434
|
+
}}
|
|
435
|
+
/>
|
|
436
|
+
|
|
437
|
+
{/* ===== PAYMENT SETTINGS MODAL ===== */}
|
|
438
|
+
<PaymentSettingsModal
|
|
439
|
+
open={settingsOpen}
|
|
440
|
+
onClose={() => setSettingsOpen(false)}
|
|
441
|
+
/>
|
|
442
|
+
|
|
443
|
+
{/* ===== SALE RECEIPT MODAL ===== */}
|
|
444
|
+
<SaleReceipt
|
|
445
|
+
open={receiptOpen}
|
|
446
|
+
onClose={() => setReceiptOpen(false)}
|
|
447
|
+
saleData={saleReceiptData}
|
|
448
|
+
/>
|
|
449
|
+
|
|
450
|
+
{/* ===== CLOSE REGISTER MODAL ===== */}
|
|
451
|
+
<CloseSessionModal
|
|
452
|
+
open={closeSessionOpen}
|
|
453
|
+
onClose={() => setCloseSessionOpen(false)}
|
|
454
|
+
sessionId={session.id}
|
|
455
|
+
openingAmount={session.opening_amount}
|
|
456
|
+
onSessionClosed={() => {
|
|
457
|
+
setCloseSessionOpen(false);
|
|
458
|
+
window.location.reload();
|
|
459
|
+
}}
|
|
460
|
+
/>
|
|
461
|
+
</div>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export interface Product {
|
|
2
|
+
id: string;
|
|
3
|
+
medicine_id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
category: string;
|
|
6
|
+
price: number;
|
|
7
|
+
stock: number;
|
|
8
|
+
batch: string;
|
|
9
|
+
lot_number: string;
|
|
10
|
+
expiry: string;
|
|
11
|
+
requires_prescription: boolean;
|
|
12
|
+
favorite: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const categories = [
|
|
16
|
+
'All', 'Medicines', 'Antibiotics', 'Pain Relief', 'Vitamins',
|
|
17
|
+
'Respiratory', 'Cardiovascular', 'Diabetes', 'Skin Care', 'Other'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export const mockProducts: Product[] = [
|
|
21
|
+
// Medicines
|
|
22
|
+
{ id: 'p1', medicine_id: 'm1', name: 'Metformin 500mg + Glimepiride 2mg', category: 'Medicines', price: 450, stock: 120, batch: 'B-001', lot_number: '000001', expiry: '12/2027', requires_prescription: true, favorite: true },
|
|
23
|
+
{ id: 'p2', medicine_id: 'm2', name: 'Perindopril 5mg + Indapamide 1.25mg', category: 'Medicines', price: 550, stock: 85, batch: 'B-002', lot_number: '000002', expiry: '06/2028', requires_prescription: true, favorite: false },
|
|
24
|
+
{ id: 'p3', medicine_id: 'm3', name: 'Atorvastatin 10mg', category: 'Medicines', price: 300, stock: 200, batch: 'B-003', lot_number: '000003', expiry: '01/2026', requires_prescription: true, favorite: true },
|
|
25
|
+
{ id: 'p4', medicine_id: 'm4', name: 'Atorvastatin 20mg', category: 'Medicines', price: 400, stock: 150, batch: 'B-003', lot_number: '000004', expiry: '01/2026', requires_prescription: true, favorite: false },
|
|
26
|
+
|
|
27
|
+
// Antibiotics
|
|
28
|
+
{ id: 'p5', medicine_id: 'm5', name: 'Amoxicillin 500mg', category: 'Antibiotics', price: 250, stock: 300, batch: 'B-004', lot_number: 'Lot-9854', expiry: '11/2025', requires_prescription: true, favorite: true },
|
|
29
|
+
{ id: 'p6', medicine_id: 'm6', name: 'Azithromycin 250mg', category: 'Antibiotics', price: 350, stock: 50, batch: 'B-005', lot_number: 'Lot-9855', expiry: '03/2026', requires_prescription: true, favorite: false },
|
|
30
|
+
{ id: 'p7', medicine_id: 'm7', name: 'Ciprofloxacin 500mg', category: 'Antibiotics', price: 400, stock: 90, batch: 'B-006', lot_number: 'Lot-9856', expiry: '08/2027', requires_prescription: true, favorite: false },
|
|
31
|
+
|
|
32
|
+
// Pain Relief
|
|
33
|
+
{ id: 'p8', medicine_id: 'm8', name: 'Acetylsalicylic Acid 75mg', category: 'Pain Relief', price: 150, stock: 400, batch: 'B-007', lot_number: '000005', expiry: '05/2028', requires_prescription: false, favorite: true },
|
|
34
|
+
{ id: 'p9', medicine_id: 'm9', name: 'Acetylsalicylic Acid 80mg Gastro-Resistant', category: 'Pain Relief', price: 180, stock: 250, batch: 'B-007', lot_number: '000006', expiry: '05/2028', requires_prescription: false, favorite: false },
|
|
35
|
+
{ id: 'p10', medicine_id: 'm10', name: 'Acetylsalicylic Acid 160mg Gastro-Resistant', category: 'Pain Relief', price: 220, stock: 180, batch: 'B-008', lot_number: '000007', expiry: '09/2026', requires_prescription: false, favorite: false },
|
|
36
|
+
{ id: 'p11', medicine_id: 'm11', name: 'Paracetamol 500mg', category: 'Pain Relief', price: 50, stock: 500, batch: 'B-009', lot_number: '000008', expiry: '12/2028', requires_prescription: false, favorite: true },
|
|
37
|
+
{ id: 'p12', medicine_id: 'm12', name: 'Ibuprofen 400mg', category: 'Pain Relief', price: 120, stock: 350, batch: 'B-010', lot_number: '000009', expiry: '07/2027', requires_prescription: false, favorite: true },
|
|
38
|
+
|
|
39
|
+
// Vitamins
|
|
40
|
+
{ id: 'p13', medicine_id: 'm13', name: 'Ascorbic Acid 500mg', category: 'Vitamins', price: 100, stock: 450, batch: 'B-011', lot_number: '000010', expiry: '10/2026', requires_prescription: false, favorite: true },
|
|
41
|
+
{ id: 'p14', medicine_id: 'm14', name: 'B Vitamins Formula', category: 'Vitamins', price: 250, stock: 120, batch: 'B-012', lot_number: '000011', expiry: '04/2027', requires_prescription: false, favorite: false },
|
|
42
|
+
{ id: 'p15', medicine_id: 'm15', name: 'Vitamin D3 1000 IU', category: 'Vitamins', price: 180, stock: 200, batch: 'B-013', lot_number: '000012', expiry: '02/2028', requires_prescription: false, favorite: true },
|
|
43
|
+
{ id: 'p16', medicine_id: 'm16', name: 'Multivitamin Supplements', category: 'Vitamins', price: 300, stock: 80, batch: 'B-014', lot_number: '000013', expiry: '11/2025', requires_prescription: false, favorite: false },
|
|
44
|
+
|
|
45
|
+
// Respiratory
|
|
46
|
+
{ id: 'p17', medicine_id: 'm17', name: 'Aminosidine 125mg/5ml', category: 'Respiratory', price: 280, stock: 60, batch: 'B-015', lot_number: '000014', expiry: '06/2026', requires_prescription: true, favorite: false },
|
|
47
|
+
{ id: 'p18', medicine_id: 'm18', name: 'Aminosidine 250mg', category: 'Respiratory', price: 420, stock: 45, batch: 'B-016', lot_number: '000015', expiry: '06/2026', requires_prescription: true, favorite: false },
|
|
48
|
+
{ id: 'p19', medicine_id: 'm19', name: 'Salbutamol Inhaler 100mcg', category: 'Respiratory', price: 150, stock: 150, batch: 'B-017', lot_number: '000016', expiry: '01/2027', requires_prescription: true, favorite: true },
|
|
49
|
+
|
|
50
|
+
// Cardiovascular
|
|
51
|
+
{ id: 'p20', medicine_id: 'm20', name: 'Atenolol 25mg', category: 'Cardiovascular', price: 200, stock: 110, batch: 'B-018', lot_number: '000017', expiry: '03/2028', requires_prescription: true, favorite: false },
|
|
52
|
+
{ id: 'p21', medicine_id: 'm21', name: 'Atenolol 50mg', category: 'Cardiovascular', price: 250, stock: 130, batch: 'B-018', lot_number: '000018', expiry: '03/2028', requires_prescription: true, favorite: true },
|
|
53
|
+
{ id: 'p22', medicine_id: 'm22', name: 'Atenolol 100mg', category: 'Cardiovascular', price: 300, stock: 90, batch: 'B-019', lot_number: '000019', expiry: '05/2027', requires_prescription: true, favorite: false },
|
|
54
|
+
{ id: 'p23', medicine_id: 'm23', name: 'Amlodipine 5mg', category: 'Cardiovascular', price: 180, stock: 220, batch: 'B-020', lot_number: '000020', expiry: '11/2026', requires_prescription: true, favorite: true },
|
|
55
|
+
{ id: 'p24', medicine_id: 'm24', name: 'Amlodipine 10mg', category: 'Cardiovascular', price: 220, stock: 190, batch: 'B-020', lot_number: '000021', expiry: '11/2026', requires_prescription: true, favorite: false },
|
|
56
|
+
{ id: 'p25', medicine_id: 'm25', name: 'Amlodipine 5mg+Losartan 50mg', category: 'Cardiovascular', price: 400, stock: 150, batch: 'B-021', lot_number: '000022', expiry: '08/2027', requires_prescription: true, favorite: true },
|
|
57
|
+
{ id: 'p26', medicine_id: 'm26', name: 'Amlodipine 2.5mg+Hydrochlorothiazide 12.5mg', category: 'Cardiovascular', price: 350, stock: 75, batch: 'B-022', lot_number: '000023', expiry: '10/2025', requires_prescription: true, favorite: false },
|
|
58
|
+
|
|
59
|
+
// Diabetes
|
|
60
|
+
{ id: 'p27', medicine_id: 'm27', name: 'Metformin 850mg', category: 'Diabetes', price: 200, stock: 300, batch: 'B-023', lot_number: '000024', expiry: '09/2028', requires_prescription: true, favorite: true },
|
|
61
|
+
{ id: 'p28', medicine_id: 'm28', name: 'Glimepiride 4mg', category: 'Diabetes', price: 180, stock: 140, batch: 'B-024', lot_number: '000025', expiry: '02/2027', requires_prescription: true, favorite: false },
|
|
62
|
+
|
|
63
|
+
// Skin Care
|
|
64
|
+
{ id: 'p29', medicine_id: 'm29', name: 'Benzoyl Peroxide 5%', category: 'Skin Care', price: 450, stock: 65, batch: 'B-025', lot_number: '000026', expiry: '07/2026', requires_prescription: false, favorite: true },
|
|
65
|
+
{ id: 'p30', medicine_id: 'm30', name: 'Benzoyl Peroxide 10%', category: 'Skin Care', price: 550, stock: 40, batch: 'B-026', lot_number: '000027', expiry: '07/2026', requires_prescription: false, favorite: false },
|
|
66
|
+
{ id: 'p31', medicine_id: 'm31', name: 'Azelaic Acid 20%', category: 'Skin Care', price: 600, stock: 80, batch: 'B-027', lot_number: '000028', expiry: '12/2025', requires_prescription: false, favorite: true },
|
|
67
|
+
{ id: 'p32', medicine_id: 'm32', name: 'Acitretin 25mg', category: 'Skin Care', price: 900, stock: 25, batch: 'B-028', lot_number: '000029', expiry: '04/2028', requires_prescription: true, favorite: false },
|
|
68
|
+
|
|
69
|
+
// Other
|
|
70
|
+
{ id: 'p33', medicine_id: 'm33', name: 'Activated Charcoal 125mg tablet', category: 'Other', price: 150, stock: 120, batch: 'B-029', lot_number: '000030', expiry: '01/2029', requires_prescription: false, favorite: false },
|
|
71
|
+
{ id: 'p34', medicine_id: 'm34', name: 'Antihemorrhoids Preparations', category: 'Other', price: 320, stock: 85, batch: 'B-030', lot_number: '000031', expiry: '05/2026', requires_prescription: false, favorite: true },
|
|
72
|
+
{ id: 'p35', medicine_id: 'm35', name: 'Adarone Chlorhydrate 200mg', category: 'Other', price: 480, stock: 30, batch: 'B-031', lot_number: '000032', expiry: '08/2027', requires_prescription: true, favorite: false },
|
|
73
|
+
{ id: 'p36', medicine_id: 'm36', name: 'Adapalene 0.1%', category: 'Other', price: 750, stock: 55, batch: 'B-032', lot_number: '000033', expiry: '11/2026', requires_prescription: true, favorite: true },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
export interface Customer {
|
|
77
|
+
id: string;
|
|
78
|
+
name: string;
|
|
79
|
+
phone: string;
|
|
80
|
+
balance: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const mockCustomers: Customer[] = [
|
|
84
|
+
{ id: 'c1', name: 'Ahmed Ali', phone: '0912 345 678', balance: 0 },
|
|
85
|
+
{ id: 'c2', name: 'Mohammed Hassan', phone: '0999 123 456', balance: 250 },
|
|
86
|
+
{ id: 'c3', name: 'Fatima Omar', phone: '0911 222 333', balance: 1500 },
|
|
87
|
+
{ id: 'c4', name: 'Sara Ibrahim', phone: '0922 444 555', balance: 0 },
|
|
88
|
+
{ id: 'c5', name: 'Khalid Youssef', phone: '0933 666 777', balance: 800 },
|
|
89
|
+
];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NextIntlClientProvider } from 'next-intl';
|
|
2
|
+
import { getMessages } from 'next-intl/server';
|
|
3
|
+
import { notFound } from 'next/navigation';
|
|
4
|
+
import { routing } from '@/i18n/routing';
|
|
5
|
+
import { Toaster } from 'react-hot-toast';
|
|
6
|
+
|
|
7
|
+
export default async function LocaleLayout({
|
|
8
|
+
children,
|
|
9
|
+
params
|
|
10
|
+
}: {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
params: Promise<{ locale: string }>;
|
|
13
|
+
}) {
|
|
14
|
+
const { locale } = await params;
|
|
15
|
+
if (!routing.locales.includes(locale as any)) {
|
|
16
|
+
notFound();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const messages = await getMessages();
|
|
20
|
+
const dir = locale === 'ar' ? 'rtl' : 'ltr';
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div dir={dir} className="min-h-screen bg-background">
|
|
24
|
+
<NextIntlClientProvider messages={messages} locale={locale}>
|
|
25
|
+
{children}
|
|
26
|
+
<Toaster position="top-center" />
|
|
27
|
+
</NextIntlClientProvider>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: #FFFFFF;
|
|
8
|
+
--foreground: #111827;
|
|
9
|
+
--surface: #F9FAFB;
|
|
10
|
+
--border: #E5E7EB;
|
|
11
|
+
|
|
12
|
+
--brand-primary: #2A50D8;
|
|
13
|
+
--brand-hover: #213EAF;
|
|
14
|
+
--brand-active: #172C85;
|
|
15
|
+
|
|
16
|
+
--success: #12B76A;
|
|
17
|
+
--warning: #F79009;
|
|
18
|
+
--danger: #F04438;
|
|
19
|
+
--info: #3B82F6;
|
|
20
|
+
|
|
21
|
+
--radius: 0.5rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.dark {
|
|
25
|
+
--background: #0B1220;
|
|
26
|
+
--foreground: #F9FAFB;
|
|
27
|
+
--surface: #121926;
|
|
28
|
+
--border: #202939;
|
|
29
|
+
|
|
30
|
+
--brand-primary: #587CFC;
|
|
31
|
+
--brand-hover: #829DFD;
|
|
32
|
+
--brand-active: #ABBDFE;
|
|
33
|
+
|
|
34
|
+
--success: #12B76A;
|
|
35
|
+
--warning: #F79009;
|
|
36
|
+
--danger: #F04438;
|
|
37
|
+
--info: #3B82F6;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@layer base {
|
|
42
|
+
* {
|
|
43
|
+
@apply border-[var(--border)];
|
|
44
|
+
}
|
|
45
|
+
body {
|
|
46
|
+
@apply bg-[var(--background)] text-[var(--foreground)] antialiased;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Scrollbar Styling */
|
|
51
|
+
::-webkit-scrollbar {
|
|
52
|
+
width: 8px;
|
|
53
|
+
height: 8px;
|
|
54
|
+
}
|
|
55
|
+
::-webkit-scrollbar-track {
|
|
56
|
+
@apply bg-transparent;
|
|
57
|
+
}
|
|
58
|
+
::-webkit-scrollbar-thumb {
|
|
59
|
+
@apply bg-gray-300 dark:bg-gray-700 rounded-full;
|
|
60
|
+
}
|
|
61
|
+
::-webkit-scrollbar-thumb:hover {
|
|
62
|
+
@apply bg-gray-400 dark:bg-gray-600;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* RTL Utilities */
|
|
66
|
+
[dir="rtl"] {
|
|
67
|
+
font-family: 'Inter', system-ui, sans-serif; /* Fallback for Arabic if not using specific font */
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Hide scrollbar utility for POS category bar */
|
|
71
|
+
.no-scrollbar::-webkit-scrollbar {
|
|
72
|
+
display: none;
|
|
73
|
+
}
|
|
74
|
+
.no-scrollbar {
|
|
75
|
+
-ms-overflow-style: none;
|
|
76
|
+
scrollbar-width: none;
|
|
77
|
+
}
|