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,97 @@
1
+ 'use client';
2
+ import { useAppStore } from '@/store/app-store';
3
+ import { useAuthStore } from '@/store/auth-store';
4
+ import { Menu, Search, Bell, Sun, Moon, Globe } from 'lucide-react';
5
+ import { Input } from './input';
6
+ import { useRouter, usePathname } from '@/i18n/routing';
7
+ import { useLocale } from 'next-intl';
8
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
9
+ import { NotificationCenter } from './notification-center';
10
+
11
+ export function TopBar() {
12
+ const toggleSidebar = useAppStore(state => state.toggleSidebar);
13
+ const theme = useAppStore(state => state.theme);
14
+ const setTheme = useAppStore(state => state.setTheme);
15
+ const setLocaleStore = useAppStore(state => state.setLocale);
16
+ const user = useAuthStore(state => state.user);
17
+ const logoutAction = useAuthStore(state => state.logout);
18
+
19
+ const handleLogout = () => {
20
+ logoutAction();
21
+ router.push('/login');
22
+ };
23
+
24
+ const router = useRouter();
25
+ const pathname = usePathname();
26
+ const locale = useLocale();
27
+
28
+ const toggleTheme = () => {
29
+ setTheme(theme === 'light' ? 'dark' : 'light');
30
+ };
31
+
32
+ const toggleLanguage = () => {
33
+ const nextLocale = locale === 'en' ? 'ar' : 'en';
34
+ setLocaleStore(nextLocale);
35
+ router.replace(pathname, { locale: nextLocale });
36
+ };
37
+
38
+ return (
39
+ <header className="h-20 bg-background border-b flex items-center justify-between px-4 sticky top-0 z-10">
40
+ <div className="flex items-center gap-4 flex-1">
41
+ <button
42
+ onClick={toggleSidebar}
43
+ className="p-3 -ml-2 md:hidden rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500"
44
+ >
45
+ <Menu size={20} />
46
+ </button>
47
+ <div className="hidden md:block w-96">
48
+ <Input
49
+ placeholder="Search anything (Cmd+K)..."
50
+ leadingIcon={<Search size={16} />}
51
+ className="bg-surface border-transparent focus:border-brand-500 focus:bg-background"
52
+ />
53
+ </div>
54
+ </div>
55
+
56
+ <div className="flex items-center gap-2 md:gap-4">
57
+ <NotificationCenter />
58
+
59
+ <button onClick={toggleLanguage} className="p-3 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 font-medium text-sm flex items-center gap-1">
60
+ <Globe size={18} />
61
+ <span className="hidden sm:inline">{locale === 'en' ? 'عر' : 'EN'}</span>
62
+ </button>
63
+
64
+ <button onClick={toggleTheme} className="p-3 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500">
65
+ {theme === 'dark' ? <Sun size={20} /> : <Moon size={20} />}
66
+ </button>
67
+
68
+ <DropdownMenu.Root>
69
+ <DropdownMenu.Trigger asChild>
70
+ <div className="h-8 w-8 rounded-full bg-brand-500 text-white flex items-center justify-center font-semibold text-sm cursor-pointer ml-2 select-none hover:ring-2 hover:ring-brand-500/50 transition-all">
71
+ {user?.full_name?.charAt(0) || 'A'}
72
+ </div>
73
+ </DropdownMenu.Trigger>
74
+ <DropdownMenu.Portal>
75
+ <DropdownMenu.Content align="end" sideOffset={5} className="w-48 bg-surface border border-border rounded-lg shadow-lg p-1 z-50 animate-in fade-in zoom-in duration-200">
76
+ <div className="px-2 py-2 border-b border-border mb-1">
77
+ <p className="text-sm font-medium text-foreground">{user?.full_name || 'Admin'}</p>
78
+ <p className="text-xs text-muted-foreground">{user?.email || 'admin'}</p>
79
+ </div>
80
+ <DropdownMenu.Item
81
+ className="text-sm px-2 py-1.5 outline-none rounded-md cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 text-foreground transition-colors flex items-center"
82
+ >
83
+ Profile Settings
84
+ </DropdownMenu.Item>
85
+ <DropdownMenu.Item
86
+ onClick={handleLogout}
87
+ className="text-sm px-2 py-1.5 outline-none rounded-md cursor-pointer hover:bg-red-500/10 text-danger transition-colors flex items-center mt-1"
88
+ >
89
+ Log Out
90
+ </DropdownMenu.Item>
91
+ </DropdownMenu.Content>
92
+ </DropdownMenu.Portal>
93
+ </DropdownMenu.Root>
94
+ </div>
95
+ </header>
96
+ );
97
+ }
@@ -0,0 +1,13 @@
1
+ import { getRequestConfig } from 'next-intl/server';
2
+ import { routing } from './routing';
3
+
4
+ export default getRequestConfig(async ({ requestLocale }) => {
5
+ let locale = await requestLocale;
6
+ if (!locale || !routing.locales.includes(locale as any)) {
7
+ locale = routing.defaultLocale;
8
+ }
9
+ return {
10
+ locale,
11
+ messages: (await import(`../messages/${locale}.json`)).default
12
+ };
13
+ });
@@ -0,0 +1,9 @@
1
+ import { defineRouting } from 'next-intl/routing';
2
+ import { createNavigation } from 'next-intl/navigation';
3
+
4
+ export const routing = defineRouting({
5
+ locales: ['en', 'ar'],
6
+ defaultLocale: 'en'
7
+ });
8
+
9
+ export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing);
@@ -0,0 +1,240 @@
1
+ const API_BASE = '/api/v1'; // Use Next.js rewrite proxy
2
+
3
+ async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> {
4
+ const res = await fetch(`${API_BASE}${endpoint}`, {
5
+ ...options,
6
+ headers: {
7
+ 'Content-Type': 'application/json',
8
+ ...options?.headers,
9
+ },
10
+ credentials: 'include',
11
+ });
12
+
13
+ if (!res.ok) {
14
+ if (res.status === 401 && typeof window !== 'undefined' && !window.location.pathname.includes('/login')) {
15
+ window.location.href = '/en/login';
16
+ }
17
+ const error = await res.json().catch(() => ({ error: { message: res.statusText } }));
18
+
19
+ let errorMsg = `API Error: ${res.status}`;
20
+ if (error?.detail) {
21
+ if (Array.isArray(error.detail)) {
22
+ errorMsg = error.detail.map((e: any) => `${e.loc?.join('.')} ${e.msg}`).join(', ');
23
+ } else if (typeof error.detail === 'string') {
24
+ errorMsg = error.detail;
25
+ }
26
+ } else if (error?.error?.message) {
27
+ errorMsg = error.error.message;
28
+ }
29
+
30
+ const err: any = new Error(errorMsg);
31
+ err.status = res.status;
32
+ err.data = error;
33
+ throw err;
34
+ }
35
+ return res.json();
36
+ }
37
+
38
+ export const api = {
39
+ // Auth
40
+ login: (data: { email: string; password: string; remember_me?: boolean }) => fetchApi('/auth/login', { method: 'POST', body: JSON.stringify(data) }),
41
+ logout: () => fetchApi('/auth/logout', { method: 'POST' }),
42
+ getMe: () => fetchApi('/auth/me'),
43
+
44
+ // Users
45
+ getUsers: () => fetchApi('/users'),
46
+ getUser: (id: string) => fetchApi(`/users/${id}`),
47
+ updateUser: (id: string, data: any) => fetchApi(`/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
48
+ deleteUser: (id: string) => fetchApi(`/users/${id}`, { method: 'DELETE' }),
49
+ registerUser: (data: any) => fetchApi('/auth/register', { method: 'POST', body: JSON.stringify(data) }),
50
+
51
+ // Medicines
52
+ getMedicines: () => fetchApi('/medicines'),
53
+ getMedicine: (id: string) => fetchApi(`/medicines/${id}`),
54
+ searchMedicines: (q: string) => fetchApi(`/medicines/search?q=${encodeURIComponent(q)}`),
55
+ createMedicine: (data: any) => fetchApi('/medicines', { method: 'POST', body: JSON.stringify(data) }),
56
+ createMedicineWithBatch: (data: any) => fetchApi('/medicines/with-batch', { method: 'POST', body: JSON.stringify(data) }),
57
+ bulkImportMedicines: async (file: File) => {
58
+ const formData = new FormData();
59
+ formData.append('file', file);
60
+ const res = await fetch(`${API_BASE}/medicines/bulk-import`, {
61
+ method: 'POST',
62
+ body: formData,
63
+ credentials: 'include',
64
+ });
65
+ if (!res.ok) {
66
+ const error = await res.json().catch(() => ({ error: { message: res.statusText } }));
67
+ throw new Error(error?.error?.message || `API Error: ${res.status}`);
68
+ }
69
+ return res.json();
70
+ },
71
+ downloadImportTemplate: async (format: 'csv' | 'xlsx' = 'csv') => {
72
+ const res = await fetch(`${API_BASE}/medicines/import-template?format=${format}`, {
73
+ credentials: 'include',
74
+ });
75
+ if (!res.ok) throw new Error('Failed to download template');
76
+ const blob = await res.blob();
77
+ const url = window.URL.createObjectURL(blob);
78
+ const a = document.createElement('a');
79
+ a.href = url;
80
+ a.download = `medicine_import_template.${format}`;
81
+ a.click();
82
+ window.URL.revokeObjectURL(url);
83
+ },
84
+ updateMedicine: (id: string, data: any) => fetchApi(`/medicines/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
85
+ deleteMedicine: (id: string) => fetchApi(`/medicines/${id}`, { method: 'DELETE' }),
86
+ getCategories: () => fetchApi('/medicines/categories'),
87
+ getCategoryTree: () => fetchApi('/medicines/categories/tree'),
88
+ createCategory: (data: any) => fetchApi('/medicines/categories', { method: 'POST', body: JSON.stringify(data) }),
89
+ updateCategory: (id: string, data: any) => fetchApi(`/medicines/categories/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
90
+ deleteCategory: (id: string) => fetchApi(`/medicines/categories/${id}`, { method: 'DELETE' }),
91
+ getBatches: (medicineId: string) => fetchApi(`/medicines/${medicineId}/batches`),
92
+ createBatch: (medicineId: string, data: any) => fetchApi(`/medicines/${medicineId}/batches`, { method: 'POST', body: JSON.stringify(data) }),
93
+ updateBatch: (medicineId: string, batchId: string, data: any) => fetchApi(`/medicines/${medicineId}/batches/${batchId}`, { method: 'PUT', body: JSON.stringify(data) }),
94
+ deactivateBatch: (medicineId: string, batchId: string) => fetchApi(`/medicines/${medicineId}/batches/${batchId}`, { method: 'DELETE' }),
95
+
96
+ // Inventory
97
+ getStock: () => fetchApi('/inventory/stock'),
98
+ getMedicineStock: (id: string) => fetchApi(`/inventory/stock/${id}`),
99
+ createAdjustment: (data: any) => fetchApi('/inventory/adjustments', { method: 'POST', body: JSON.stringify(data) }),
100
+ getMovements: () => fetchApi('/inventory/movements'),
101
+ getMedicineMovements: (medicineId: string) => fetchApi(`/inventory/movements?medicine_id=${medicineId}`),
102
+ // Inventory Alerts
103
+ getLowStock: () => fetchApi('/inventory/alerts/low-stock'),
104
+ getOutOfStock: () => fetchApi('/inventory/alerts/out-of-stock'),
105
+ getExpiring: () => fetchApi('/inventory/alerts/expiring'),
106
+ getExpired: () => fetchApi('/inventory/alerts/expired'),
107
+
108
+ // Sales
109
+ processSale: (data: any) => fetchApi('/sales/', {
110
+ method: 'POST',
111
+ body: JSON.stringify(data),
112
+ }),
113
+ getSales: (params?: { start_date?: string, end_date?: string, status?: string, search?: string }) => {
114
+ let url = '/sales/';
115
+ if (params) {
116
+ const q = new URLSearchParams();
117
+ if (params.start_date) q.append('start_date', params.start_date);
118
+ if (params.end_date) q.append('end_date', params.end_date);
119
+ if (params.status && params.status !== 'ALL') q.append('status', params.status);
120
+ if (params.search) q.append('search', params.search);
121
+ if (q.toString()) url += `?${q.toString()}`;
122
+ }
123
+ return fetchApi(url);
124
+ },
125
+ getSaleById: (id: string) => fetchApi(`/sales/${id}`),
126
+ returnSale: (id: string) => fetchApi(`/sales/${id}/return`, { method: 'POST' }),
127
+
128
+ // Suppliers
129
+ getSuppliers: () => fetchApi('/suppliers'),
130
+ createSupplier: (data: any) => fetchApi('/suppliers', { method: 'POST', body: JSON.stringify(data) }),
131
+
132
+ // Customers
133
+ getCustomers: () => fetchApi('/customers'),
134
+ getCustomer: (id: string) => fetchApi(`/customers/${id}`),
135
+ searchCustomers: (q: string) => fetchApi(`/customers/search?q=${encodeURIComponent(q)}`),
136
+ createCustomer: (data: any) => fetchApi('/customers', { method: 'POST', body: JSON.stringify(data) }),
137
+ updateCustomer: (id: string, data: any) => fetchApi(`/customers/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
138
+ deleteCustomer: (id: string) => fetchApi(`/customers/${id}`, { method: 'DELETE' }),
139
+ getCustomerPurchases: (id: string) => fetchApi(`/customers/${id}/purchases`),
140
+ getCustomerPayments: (id: string) => fetchApi(`/customers/${id}/payments`),
141
+ addCustomerPayment: (id: string, data: any) => fetchApi(`/customers/${id}/payments`, { method: 'POST', body: JSON.stringify(data) }),
142
+ getCustomerCreditLedger: (id: string) => fetchApi(`/customers/${id}/credit-ledger`),
143
+ updateCustomerNotes: (id: string, notes: string) => fetchApi(`/customers/${id}/notes`, { method: 'PATCH', body: JSON.stringify({ notes }) }),
144
+
145
+ // Cash Sessions
146
+ openSession: (data: any) => fetchApi('/cash-sessions/open', { method: 'POST', body: JSON.stringify(data) }),
147
+ closeSession: (id: string, data: any) => fetchApi(`/cash-sessions/${id}/close`, { method: 'POST', body: JSON.stringify(data) }),
148
+ getActiveSession: () => fetchApi('/cash-sessions/active'),
149
+ getSessions: () => fetchApi('/cash-sessions'),
150
+
151
+ // Reports
152
+ getDailySales: (date: string) => fetchApi(`/reports/sales/daily?date=${date}`),
153
+ getMonthlySales: (year: number, month: number) => fetchApi(`/reports/sales/monthly?year=${year}&month=${month}`),
154
+ getSalesRange: (start: string, end: string) => fetchApi(`/reports/sales/range?start_date=${start}&end_date=${end}`),
155
+ getStockValue: () => fetchApi('/reports/inventory/stock-value'),
156
+ getExpirySummary: () => fetchApi('/reports/inventory/expiry-summary'),
157
+ getTopProducts: (limit = 10) => fetchApi(`/reports/sales/top-products?limit=${limit}`),
158
+ getProfit: (start?: string, end?: string) => {
159
+ let url = '/reports/financial/profit';
160
+ if (start && end) url += `?start_date=${start}&end_date=${end}`;
161
+ return fetchApi(url);
162
+ },
163
+ getCustomerBalances: () => fetchApi('/reports/customers/balances'),
164
+ getPaymentBreakdown: (start?: string, end?: string) => {
165
+ let url = '/reports/financial/payment-breakdown';
166
+ if (start && end) url += `?start_date=${start}&end_date=${end}`;
167
+ return fetchApi(url);
168
+ },
169
+ getMedicineMovement: (start?: string, end?: string) => {
170
+ let url = '/reports/medicines/movement';
171
+ if (start && end) url += `?start_date=${start}&end_date=${end}`;
172
+ return fetchApi(url);
173
+ },
174
+
175
+ // Admin
176
+ wipeData: () => fetchApi('/admin/wipe-data', { method: 'POST' }),
177
+ factoryReset: () => fetchApi('/admin/factory-reset', { method: 'POST' }),
178
+
179
+ // Admin - Audit Logs
180
+ getAuditLogs: (params?: { page?: number; per_page?: number; module?: string; action?: string; search?: string }) => {
181
+ const q = new URLSearchParams();
182
+ if (params?.page) q.append('page', String(params.page));
183
+ if (params?.per_page) q.append('per_page', String(params.per_page));
184
+ if (params?.module && params.module !== 'all') q.append('module', params.module);
185
+ if (params?.action && params.action !== 'all') q.append('action', params.action);
186
+ if (params?.search) q.append('search', params.search);
187
+ return fetchApi(`/admin/audit-logs?${q.toString()}`);
188
+ },
189
+
190
+ // Admin - Backups
191
+ createBackup: () => fetchApi('/admin/backups', { method: 'POST' }),
192
+ getBackups: () => fetchApi('/admin/backups'),
193
+ deleteBackup: (filename: string) => fetchApi(`/admin/backups/${filename}`, { method: 'DELETE' }),
194
+ downloadBackup: async (filename: string) => {
195
+ const res = await fetch(`/api/v1/admin/backups/${filename}/download`, { credentials: 'include' });
196
+ if (!res.ok) throw new Error('Failed to download backup');
197
+ const blob = await res.blob();
198
+ const url = window.URL.createObjectURL(blob);
199
+ const a = document.createElement('a');
200
+ a.href = url;
201
+ a.download = filename;
202
+ a.click();
203
+ window.URL.revokeObjectURL(url);
204
+ },
205
+ restoreBackup: async (file: File) => {
206
+ const formData = new FormData();
207
+ formData.append('file', file);
208
+ const res = await fetch(`/api/v1/admin/backups/restore`, {
209
+ method: 'POST',
210
+ body: formData,
211
+ credentials: 'include',
212
+ });
213
+ if (!res.ok) {
214
+ const error = await res.json().catch(() => ({ detail: res.statusText }));
215
+ throw new Error(error?.detail || `API Error: ${res.status}`);
216
+ }
217
+ return res.json();
218
+ },
219
+ // Branches
220
+ getBranches: () => fetchApi('/branches'),
221
+ getBranch: (id: string) => fetchApi(`/branches/${id}`),
222
+ createBranch: (data: any) => fetchApi('/branches', { method: 'POST', body: JSON.stringify(data) }),
223
+ updateBranch: (id: string, data: any) => fetchApi(`/branches/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
224
+
225
+ // Roles & Permissions
226
+ getRoles: () => fetchApi('/roles'),
227
+ getRole: (id: string) => fetchApi(`/roles/${id}`),
228
+ createRole: (data: any) => fetchApi('/roles', { method: 'POST', body: JSON.stringify(data) }),
229
+ updateRole: (id: string, data: any) => fetchApi(`/roles/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
230
+ deleteRole: (id: string) => fetchApi(`/roles/${id}`, { method: 'DELETE' }),
231
+ getPermissions: () => fetchApi('/roles/permissions/all'),
232
+ getPermissionsByModule: () => fetchApi('/roles/permissions/by-module'),
233
+
234
+ // Notifications
235
+ getNotifications: () => fetchApi('/notifications'),
236
+ getUnreadNotificationCount: () => fetchApi('/notifications/unread-count'),
237
+ markNotificationAsRead: (id: string) => fetchApi(`/notifications/${id}/read`, { method: 'PATCH' }),
238
+ markAllNotificationsAsRead: () => fetchApi('/notifications/read-all', { method: 'POST' }),
239
+ deleteNotification: (id: string) => fetchApi(`/notifications/${id}`, { method: 'DELETE' }),
240
+ };
@@ -0,0 +1,30 @@
1
+ export const DOSAGE_FORMS = [
2
+ { value: 'tablet', label_en: 'Tablet', label_ar: 'قرص' },
3
+ { value: 'capsule', label_en: 'Capsule', label_ar: 'كبسولة' },
4
+ { value: 'syrup', label_en: 'Syrup', label_ar: 'شراب' },
5
+ { value: 'suspension', label_en: 'Suspension', label_ar: 'معلق' },
6
+ { value: 'injection', label_en: 'Injection', label_ar: 'حقنة' },
7
+ { value: 'infusion', label_en: 'Infusion', label_ar: 'تسريب' },
8
+ { value: 'cream', label_en: 'Cream', label_ar: 'كريم' },
9
+ { value: 'ointment', label_en: 'Ointment', label_ar: 'مرهم' },
10
+ { value: 'gel', label_en: 'Gel', label_ar: 'جل' },
11
+ { value: 'drops', label_en: 'Drops', label_ar: 'قطرات' },
12
+ { value: 'eye_drops', label_en: 'Eye Drops', label_ar: 'قطرات عين' },
13
+ { value: 'ear_drops', label_en: 'Ear Drops', label_ar: 'قطرات أذن' },
14
+ { value: 'nasal_spray', label_en: 'Nasal Spray', label_ar: 'بخاخ أنف' },
15
+ { value: 'inhaler', label_en: 'Inhaler', label_ar: 'بخاخ' },
16
+ { value: 'suppository', label_en: 'Suppository', label_ar: 'تحاميل' },
17
+ { value: 'powder', label_en: 'Powder', label_ar: 'بودرة' },
18
+ ] as const;
19
+
20
+ export const BASE_UNITS = [
21
+ { value: 'Pack', label_en: 'Pack', label_ar: 'عبوة' },
22
+ { value: 'Tablet', label_en: 'Tablet', label_ar: 'قرص' },
23
+ { value: 'Bottle', label_en: 'Bottle', label_ar: 'زجاجة' },
24
+ { value: 'Box', label_en: 'Box', label_ar: 'صندوق' },
25
+ { value: 'Tube', label_en: 'Tube', label_ar: 'أنبوب' },
26
+ { value: 'Strip', label_en: 'Strip', label_ar: 'شريط' },
27
+ { value: 'Vial', label_en: 'Vial', label_ar: 'قارورة' },
28
+ { value: 'Ampoule', label_en: 'Ampoule', label_ar: 'أمبولة' },
29
+ { value: 'Piece', label_en: 'Piece', label_ar: 'قطعة' },
30
+ ] as const;
@@ -0,0 +1,32 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
8
+ export function formatMoney(amount: number, currency: string = 'SDG'): string {
9
+ return new Intl.NumberFormat('en-SA', {
10
+ style: 'currency',
11
+ currency,
12
+ }).format(amount);
13
+ }
14
+
15
+ export function formatDate(date: string | Date, locale: string = 'en'): string {
16
+ return new Intl.DateTimeFormat(locale, {
17
+ year: 'numeric',
18
+ month: 'short',
19
+ day: 'numeric',
20
+ }).format(new Date(date));
21
+ }
22
+
23
+ export function formatNumber(num: number, locale: string = 'en'): string {
24
+ return new Intl.NumberFormat(locale).format(num);
25
+ }
26
+
27
+ export function formatCompactNumber(num: number, locale: string = 'en'): string {
28
+ return new Intl.NumberFormat(locale, {
29
+ notation: 'compact',
30
+ maximumFractionDigits: 2,
31
+ }).format(num);
32
+ }
@@ -0,0 +1,106 @@
1
+ {
2
+ "nav": {
3
+ "dashboard": "لوحة القيادة",
4
+ "pos": "نقطة البيع",
5
+ "sales": "سجل المبيعات",
6
+ "inventory": "المخزون",
7
+ "catalog": "دليل الأدوية",
8
+ "purchasing": "المشتريات",
9
+ "customers": "العملاء",
10
+ "reports": "التقارير",
11
+ "admin": "الإعدادات"
12
+ },
13
+ "common": {
14
+ "search": "بحث...",
15
+ "add": "إضافة جديد",
16
+ "edit": "تعديل",
17
+ "delete": "حذف",
18
+ "save": "حفظ التغييرات",
19
+ "cancel": "إلغاء",
20
+ "loading": "جاري التحميل...",
21
+ "noResults": "لم يتم العثور على نتائج."
22
+ },
23
+ "auth": {
24
+ "login": "تسجيل الدخول",
25
+ "logout": "تسجيل الخروج",
26
+ "email": "البريد الإلكتروني",
27
+ "password": "كلمة المرور",
28
+ "welcome": "مرحباً بك مجدداً"
29
+ },
30
+ "pos": {
31
+ "scanBarcode": "امسح الباركود أو ابحث عن دواء (F2)",
32
+ "searchMedicine": "بحث عن دواء",
33
+ "subtotal": "المجموع الفرعي",
34
+ "discount": "خصم (F7)",
35
+ "total": "الإجمالي",
36
+ "takePayment": "دفع",
37
+ "completeSale": "إتمام البيع",
38
+ "holdSale": "تعليق البيع",
39
+ "heldSales": "المبيعات المعلقة",
40
+ "newSale": "بيع جديد",
41
+ "change": "المتبقي",
42
+ "cash": "نقدي",
43
+ "card": "بطاقة",
44
+ "walkIn": "عميل نقدي",
45
+ "clearCart": "إلغاء (F10)",
46
+ "amountDue": "المبلغ المستحق"
47
+ },
48
+ "dashboard": {
49
+ "todaySales": "مبيعات اليوم",
50
+ "revenue": "الإيرادات",
51
+ "lowStock": "نقص المخزون",
52
+ "expiringMedicines": "تنتهي قريباً",
53
+ "topProducts": "المنتجات الأكثر مبيعاً",
54
+ "recentTransactions": "المعاملات الأخيرة"
55
+ },
56
+ "medicines": {
57
+ "name": "اسم الدواء",
58
+ "genericName": "الاسم العلمي",
59
+ "brandName": "الاسم التجاري",
60
+ "sku": "رمز الصنف",
61
+ "barcode": "الباركود",
62
+ "category": "الفئة",
63
+ "price": "السعر",
64
+ "stock": "المخزون",
65
+ "status": "الحالة",
66
+ "actions": "إجراءات"
67
+ },
68
+ "inventory": {
69
+ "stockOnHand": "المخزون المتوفر",
70
+ "batches": "الكميات",
71
+ "adjustments": "التسويات",
72
+ "expiring": "المنتهية",
73
+ "lowStock": "نقص المخزون"
74
+ },
75
+ "settings": {
76
+ "title": "الإعدادات",
77
+ "subtitle": "إدارة تكوين نظام الصيدلية",
78
+ "users": "المستخدمون",
79
+ "usersDesc": "إدارة حسابات المستخدمين والوصول",
80
+ "roles": "الأدوار والصلاحيات",
81
+ "rolesDesc": "تكوين التحكم في الوصول القائم على الأدوار",
82
+ "branches": "الفروع",
83
+ "branchesDesc": "إدارة فروع الصيدلية",
84
+ "payment": "طرق الدفع",
85
+ "paymentDesc": "تكوين طرق الدفع المقبولة",
86
+ "backup": "النسخ الاحتياطي والاستعادة",
87
+ "backupDesc": "النسخ الاحتياطي واستعادة قاعدة البيانات",
88
+ "audit": "سجل التدقيق",
89
+ "auditDesc": "سجل النشاطات والتغييرات",
90
+ "system": "إعدادات النظام",
91
+ "systemDesc": "التكوين العام للنظام",
92
+ "about": "حول",
93
+ "aboutDesc": "معلومات النظام والإصدار"
94
+ },
95
+ "Notifications": {
96
+ "title": "الإشعارات",
97
+ "markAllAsRead": "تحديد الكل كمقروء",
98
+ "noNotifications": "لا توجد إشعارات",
99
+ "noNotificationsDesc": "أنت على اطلاع بكل جديد!",
100
+ "delete": "حذف",
101
+ "justNow": "الآن",
102
+ "minutesAgo": "منذ {count} دقيقة",
103
+ "hoursAgo": "منذ {count} ساعة",
104
+ "daysAgo": "منذ {count} يوم"
105
+ }
106
+ }
@@ -0,0 +1,106 @@
1
+ {
2
+ "nav": {
3
+ "dashboard": "Dashboard",
4
+ "pos": "Point of Sale",
5
+ "sales": "Sales History",
6
+ "inventory": "Inventory",
7
+ "catalog": "Medicine Catalog",
8
+ "purchasing": "Purchasing",
9
+ "customers": "Customers",
10
+ "reports": "Reports",
11
+ "admin": "Settings"
12
+ },
13
+ "common": {
14
+ "search": "Search...",
15
+ "add": "Add New",
16
+ "edit": "Edit",
17
+ "delete": "Delete",
18
+ "save": "Save Changes",
19
+ "cancel": "Cancel",
20
+ "loading": "Loading...",
21
+ "noResults": "No results found."
22
+ },
23
+ "auth": {
24
+ "login": "Sign In",
25
+ "logout": "Log Out",
26
+ "email": "Email Address",
27
+ "password": "Password",
28
+ "welcome": "Welcome back"
29
+ },
30
+ "pos": {
31
+ "scanBarcode": "Scan barcode or search medicine (F2)",
32
+ "searchMedicine": "Search medicine",
33
+ "subtotal": "Subtotal",
34
+ "discount": "Discount (F7)",
35
+ "total": "Total",
36
+ "takePayment": "TAKE PAYMENT",
37
+ "completeSale": "COMPLETE SALE",
38
+ "holdSale": "Hold Sale",
39
+ "heldSales": "Held Sales",
40
+ "newSale": "New Sale",
41
+ "change": "Change",
42
+ "cash": "Cash",
43
+ "card": "Card",
44
+ "walkIn": "Walk-in Customer",
45
+ "clearCart": "Void (F10)",
46
+ "amountDue": "Amount Due"
47
+ },
48
+ "dashboard": {
49
+ "todaySales": "Today's Sales",
50
+ "revenue": "Revenue",
51
+ "lowStock": "Low Stock",
52
+ "expiringMedicines": "Expiring Soon",
53
+ "topProducts": "Top Products",
54
+ "recentTransactions": "Recent Transactions"
55
+ },
56
+ "medicines": {
57
+ "name": "Medicine Name",
58
+ "genericName": "Generic Name",
59
+ "brandName": "Brand Name",
60
+ "sku": "SKU",
61
+ "barcode": "Barcode",
62
+ "category": "Category",
63
+ "price": "Price",
64
+ "stock": "Stock",
65
+ "status": "Status",
66
+ "actions": "Actions"
67
+ },
68
+ "inventory": {
69
+ "stockOnHand": "Stock on Hand",
70
+ "batches": "Batches",
71
+ "adjustments": "Adjustments",
72
+ "expiring": "Expiring",
73
+ "lowStock": "Low Stock"
74
+ },
75
+ "settings": {
76
+ "title": "Settings",
77
+ "subtitle": "Manage your pharmacy system configuration",
78
+ "users": "Users",
79
+ "usersDesc": "Manage user accounts and access",
80
+ "roles": "Roles & Permissions",
81
+ "rolesDesc": "Configure role-based access control",
82
+ "branches": "Branches",
83
+ "branchesDesc": "Manage pharmacy branches",
84
+ "payment": "Payment Methods",
85
+ "paymentDesc": "Configure accepted payment methods",
86
+ "backup": "Backup & Restore",
87
+ "backupDesc": "Database backup and recovery",
88
+ "audit": "Audit Logs",
89
+ "auditDesc": "Activity and change history",
90
+ "system": "System Settings",
91
+ "systemDesc": "General system configuration",
92
+ "about": "About",
93
+ "aboutDesc": "System information and version"
94
+ },
95
+ "Notifications": {
96
+ "title": "Notifications",
97
+ "markAllAsRead": "Mark all as read",
98
+ "noNotifications": "No notifications",
99
+ "noNotificationsDesc": "You're all caught up!",
100
+ "delete": "Delete",
101
+ "justNow": "Just now",
102
+ "minutesAgo": "{count}m ago",
103
+ "hoursAgo": "{count}h ago",
104
+ "daysAgo": "{count}d ago"
105
+ }
106
+ }
@@ -0,0 +1,8 @@
1
+ import createMiddleware from 'next-intl/middleware';
2
+ import { routing } from './i18n/routing';
3
+
4
+ export default createMiddleware(routing);
5
+
6
+ export const config = {
7
+ matcher: ['/', '/(ar|en)/:path*', '/((?!api|_next|_vercel|.*\\..*).*)']
8
+ };