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,37 @@
1
+ import { create } from 'zustand';
2
+ import { persist } from 'zustand/middleware';
3
+
4
+ interface AppState {
5
+ sidebarOpen: boolean;
6
+ sidebarCollapsed: boolean;
7
+ currentBranch: string;
8
+ theme: 'light' | 'dark';
9
+ locale: 'en' | 'ar';
10
+ density: 'compact' | 'comfortable' | 'touch';
11
+ currency: string;
12
+ toggleSidebar: () => void;
13
+ setSidebarCollapsed: (collapsed: boolean) => void;
14
+ setTheme: (theme: 'light' | 'dark') => void;
15
+ setLocale: (locale: 'en' | 'ar') => void;
16
+ setCurrency: (currency: string) => void;
17
+ }
18
+
19
+ export const useAppStore = create<AppState>()(
20
+ persist(
21
+ (set) => ({
22
+ sidebarOpen: true,
23
+ sidebarCollapsed: false,
24
+ currentBranch: 'Main Branch (Riyadh)',
25
+ theme: 'light',
26
+ locale: 'en',
27
+ density: 'comfortable',
28
+ currency: 'SDG',
29
+ toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
30
+ setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }),
31
+ setTheme: (theme) => set({ theme }),
32
+ setLocale: (locale) => set({ locale }),
33
+ setCurrency: (currency) => set({ currency }),
34
+ }),
35
+ { name: 'app-storage' }
36
+ )
37
+ );
@@ -0,0 +1,52 @@
1
+ import { create } from 'zustand';
2
+ import { api } from '@/lib/api';
3
+
4
+ interface User {
5
+ id: string;
6
+ email: string;
7
+ full_name: string;
8
+ full_name_ar?: string;
9
+ role: string;
10
+ is_active: boolean;
11
+ }
12
+
13
+ interface AuthState {
14
+ user: User | null;
15
+ isAuthenticated: boolean;
16
+ isLoading: boolean;
17
+ error: string | null;
18
+ login: (credentials: { email: string; password: string; remember_me?: boolean }) => Promise<void>;
19
+ logout: () => Promise<void>;
20
+ checkAuth: () => Promise<void>;
21
+ }
22
+
23
+ export const useAuthStore = create<AuthState>((set) => ({
24
+ user: null,
25
+ isAuthenticated: false,
26
+ isLoading: false,
27
+ error: null,
28
+ login: async (credentials) => {
29
+ set({ isLoading: true, error: null });
30
+ try {
31
+ const user = await api.login(credentials) as User;
32
+ set({ user, isAuthenticated: true, isLoading: false });
33
+ } catch (e: any) {
34
+ set({ isLoading: false, error: e.message || 'Login failed' });
35
+ throw e;
36
+ }
37
+ },
38
+ logout: async () => {
39
+ try {
40
+ await api.logout();
41
+ } catch {}
42
+ set({ user: null, isAuthenticated: false });
43
+ },
44
+ checkAuth: async () => {
45
+ try {
46
+ const user = await api.getMe() as User;
47
+ set({ user, isAuthenticated: true });
48
+ } catch {
49
+ set({ user: null, isAuthenticated: false });
50
+ }
51
+ },
52
+ }));
@@ -0,0 +1,144 @@
1
+ import { create } from 'zustand';
2
+ import { persist } from 'zustand/middleware';
3
+
4
+ export interface PaymentMethod {
5
+ id: string;
6
+ name: string;
7
+ type: 'cash' | 'mobile';
8
+ logo: string; // emoji or URL
9
+ enabled: boolean;
10
+ color: string; // hex color for the card accent
11
+ }
12
+
13
+ export interface CompletedSale {
14
+ id: string;
15
+ items: { name: string; quantity: number; unit_price: number; line_total: number }[];
16
+ subtotal: number;
17
+ discount: number;
18
+ total: number;
19
+ paymentMethodId: string;
20
+ paymentMethodName: string;
21
+ paymentType: 'cash' | 'mobile';
22
+ customerName: string | null;
23
+ timestamp: string; // ISO string
24
+ }
25
+
26
+ interface PaymentMethodsState {
27
+ methods: PaymentMethod[];
28
+ completedSales: CompletedSale[];
29
+ requireCashSession: boolean;
30
+
31
+ // Settings
32
+ setRequireCashSession: (val: boolean) => void;
33
+
34
+ // Methods CRUD
35
+ addMethod: (method: Omit<PaymentMethod, 'id'>) => void;
36
+ updateMethod: (id: string, updates: Partial<Omit<PaymentMethod, 'id'>>) => void;
37
+ deleteMethod: (id: string) => void;
38
+ toggleMethod: (id: string) => void;
39
+
40
+ // Sales
41
+ recordSale: (sale: Omit<CompletedSale, 'id' | 'timestamp'>) => void;
42
+ clearSalesHistory: () => void;
43
+
44
+ // Computed helpers
45
+ getSalesTotals: () => {
46
+ cash: number;
47
+ mobile: number;
48
+ byMethod: Record<string, { name: string; total: number; count: number }>;
49
+ grandTotal: number;
50
+ totalCount: number;
51
+ };
52
+ }
53
+
54
+ const defaultMethods: PaymentMethod[] = [
55
+ { id: 'cash', name: 'Cash', type: 'cash', logo: '💵', enabled: true, color: '#22C55E' },
56
+ { id: 'bankak', name: 'Bankak', type: 'mobile', logo: '🏦', enabled: true, color: '#3B82F6' },
57
+ { id: 'fawry', name: 'Fawry', type: 'mobile', logo: '📱', enabled: true, color: '#F59E0B' },
58
+ { id: 'amin', name: 'Amin', type: 'mobile', logo: '💳', enabled: true, color: '#8B5CF6' },
59
+ ];
60
+
61
+ export const usePaymentMethodsStore = create<PaymentMethodsState>()(
62
+ persist(
63
+ (set, get) => ({
64
+ methods: defaultMethods,
65
+ completedSales: [],
66
+ requireCashSession: false, // Default to false (optional)
67
+
68
+ setRequireCashSession: (val) => set({ requireCashSession: val }),
69
+
70
+ addMethod: (method) =>
71
+ set((state) => ({
72
+ methods: [
73
+ ...state.methods,
74
+ { ...method, id: `pm-${Date.now()}-${Math.random().toString(36).substr(2, 5)}` },
75
+ ],
76
+ })),
77
+
78
+ updateMethod: (id, updates) =>
79
+ set((state) => ({
80
+ methods: state.methods.map((m) =>
81
+ m.id === id ? { ...m, ...updates } : m
82
+ ),
83
+ })),
84
+
85
+ deleteMethod: (id) =>
86
+ set((state) => ({
87
+ methods: state.methods.filter((m) => m.id !== id),
88
+ })),
89
+
90
+ toggleMethod: (id) =>
91
+ set((state) => ({
92
+ methods: state.methods.map((m) =>
93
+ m.id === id ? { ...m, enabled: !m.enabled } : m
94
+ ),
95
+ })),
96
+
97
+ recordSale: (sale) =>
98
+ set((state) => ({
99
+ completedSales: [
100
+ ...state.completedSales,
101
+ {
102
+ ...sale,
103
+ id: `sale-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
104
+ timestamp: new Date().toISOString(),
105
+ },
106
+ ],
107
+ })),
108
+
109
+ clearSalesHistory: () => set({ completedSales: [] }),
110
+
111
+ getSalesTotals: () => {
112
+ const { completedSales } = get();
113
+ let cash = 0;
114
+ let mobile = 0;
115
+ const byMethod: Record<string, { name: string; total: number; count: number }> = {};
116
+
117
+ for (const sale of completedSales) {
118
+ if (sale.paymentType === 'cash') {
119
+ cash += sale.total;
120
+ } else {
121
+ mobile += sale.total;
122
+ }
123
+
124
+ if (!byMethod[sale.paymentMethodId]) {
125
+ byMethod[sale.paymentMethodId] = { name: sale.paymentMethodName, total: 0, count: 0 };
126
+ }
127
+ byMethod[sale.paymentMethodId].total += sale.total;
128
+ byMethod[sale.paymentMethodId].count += 1;
129
+ }
130
+
131
+ return {
132
+ cash,
133
+ mobile,
134
+ byMethod,
135
+ grandTotal: cash + mobile,
136
+ totalCount: completedSales.length,
137
+ };
138
+ },
139
+ }),
140
+ {
141
+ name: 'payment-methods-storage',
142
+ }
143
+ )
144
+ );
@@ -0,0 +1,344 @@
1
+ import { create } from 'zustand';
2
+
3
+ export interface CartItem {
4
+ id: string;
5
+ medicine_id: string;
6
+ name: string;
7
+ batch: string;
8
+ lot_number: string;
9
+ quantity: number;
10
+ unit_price: number;
11
+ line_total: number;
12
+ }
13
+
14
+ export interface CustomerInfo {
15
+ id: string;
16
+ name: string;
17
+ phone?: string;
18
+ }
19
+
20
+ export interface DiscountInfo {
21
+ type: 'none' | 'percentage' | 'fixed' | 'final_total';
22
+ value: number;
23
+ amount: number;
24
+ }
25
+
26
+ export interface HeldSale {
27
+ id: string;
28
+ cart: CartItem[];
29
+ customer: CustomerInfo | null;
30
+ discount: DiscountInfo;
31
+ subtotal: number;
32
+ total: number;
33
+ timestamp: Date;
34
+ note: string;
35
+ }
36
+
37
+ interface PosState {
38
+ // Cart
39
+ cart: CartItem[];
40
+ customer: CustomerInfo | null;
41
+ subtotal: number;
42
+ total: number;
43
+ tax: number;
44
+
45
+ // Discount
46
+ discount: DiscountInfo;
47
+
48
+ // Held sales
49
+ heldSales: HeldSale[];
50
+
51
+ // UI State
52
+ keypadVisible: boolean;
53
+ keypadMode: 'qty' | 'price' | 'discount';
54
+ keypadValue: string;
55
+ selectedItemId: string | null;
56
+ activePanel: 'keypad' | 'customer' | 'discount' | 'hold' | null;
57
+
58
+ // Cart actions
59
+ addItem: (item: Omit<CartItem, 'id' | 'line_total'>) => void;
60
+ removeItem: (id: string) => void;
61
+ updateQuantity: (id: string, quantity: number) => void;
62
+ clearCart: () => void;
63
+ selectItem: (id: string | null) => void;
64
+
65
+ // Customer
66
+ setCustomer: (customer: CustomerInfo | null) => void;
67
+
68
+ // Discount
69
+ applyDiscount: (type: 'percentage' | 'fixed' | 'final_total', value: number) => void;
70
+ clearDiscount: () => void;
71
+
72
+ // Hold/Resume
73
+ holdCurrentSale: (note?: string) => void;
74
+ resumeSale: (id: string) => void;
75
+ deleteHeldSale: (id: string) => void;
76
+
77
+ // Sale completion
78
+ completeSale: () => void;
79
+
80
+ // Keypad
81
+ toggleKeypad: () => void;
82
+ setKeypadMode: (mode: 'qty' | 'price' | 'discount') => void;
83
+ setKeypadValue: (value: string) => void;
84
+ applyKeypadValue: () => void;
85
+
86
+ // Panel
87
+ setActivePanel: (panel: 'keypad' | 'customer' | 'discount' | 'hold' | null) => void;
88
+ }
89
+
90
+ const recalculate = (cart: CartItem[], discount: DiscountInfo) => {
91
+ const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0);
92
+ let discountAmount = 0;
93
+
94
+ switch (discount.type) {
95
+ case 'percentage':
96
+ discountAmount = subtotal * (discount.value / 100);
97
+ break;
98
+ case 'fixed':
99
+ discountAmount = discount.value;
100
+ break;
101
+ case 'final_total':
102
+ discountAmount = Math.max(0, subtotal - discount.value);
103
+ break;
104
+ default:
105
+ discountAmount = 0;
106
+ }
107
+
108
+ const total = Math.max(0, subtotal - discountAmount);
109
+ return { subtotal, total, discountAmount };
110
+ };
111
+
112
+ export const usePosStore = create<PosState>((set, get) => ({
113
+ cart: [],
114
+ customer: null,
115
+ subtotal: 0,
116
+ total: 0,
117
+ tax: 0,
118
+ discount: { type: 'none', value: 0, amount: 0 },
119
+ heldSales: [],
120
+ keypadVisible: false,
121
+ keypadMode: 'qty',
122
+ keypadValue: '',
123
+ selectedItemId: null,
124
+ activePanel: null,
125
+
126
+ addItem: (item) => set((state) => {
127
+ const existingItemIndex = state.cart.findIndex(
128
+ (i) => i.medicine_id === item.medicine_id && i.batch === item.batch
129
+ );
130
+
131
+ let newCart = [...state.cart];
132
+
133
+ if (existingItemIndex >= 0) {
134
+ const existingItem = newCart[existingItemIndex];
135
+ const newQuantity = existingItem.quantity + item.quantity;
136
+ newCart[existingItemIndex] = {
137
+ ...existingItem,
138
+ quantity: newQuantity,
139
+ line_total: newQuantity * existingItem.unit_price,
140
+ };
141
+ } else {
142
+ const newItem: CartItem = {
143
+ ...item,
144
+ id: `item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
145
+ line_total: item.quantity * item.unit_price,
146
+ };
147
+ newCart.push(newItem);
148
+ }
149
+
150
+ const { subtotal, total, discountAmount } = recalculate(newCart, state.discount);
151
+
152
+ return {
153
+ cart: newCart,
154
+ subtotal,
155
+ total,
156
+ discount: { ...state.discount, amount: discountAmount },
157
+ selectedItemId: existingItemIndex >= 0 ? newCart[existingItemIndex].id : newCart[newCart.length - 1].id,
158
+ };
159
+ }),
160
+
161
+ removeItem: (id) => set((state) => {
162
+ const newCart = state.cart.filter((item) => item.id !== id);
163
+ const { subtotal, total, discountAmount } = recalculate(newCart, state.discount);
164
+
165
+ return {
166
+ cart: newCart,
167
+ subtotal,
168
+ total,
169
+ discount: { ...state.discount, amount: discountAmount },
170
+ selectedItemId: state.selectedItemId === id ? null : state.selectedItemId,
171
+ };
172
+ }),
173
+
174
+ updateQuantity: (id, quantity) => set((state) => {
175
+ if (quantity <= 0) {
176
+ const newCart = state.cart.filter((item) => item.id !== id);
177
+ const { subtotal, total, discountAmount } = recalculate(newCart, state.discount);
178
+ return {
179
+ cart: newCart,
180
+ subtotal,
181
+ total,
182
+ discount: { ...state.discount, amount: discountAmount },
183
+ selectedItemId: state.selectedItemId === id ? null : state.selectedItemId,
184
+ };
185
+ }
186
+
187
+ const newCart = state.cart.map((item) =>
188
+ item.id === id
189
+ ? { ...item, quantity, line_total: quantity * item.unit_price }
190
+ : item
191
+ );
192
+
193
+ const { subtotal, total, discountAmount } = recalculate(newCart, state.discount);
194
+
195
+ return {
196
+ cart: newCart,
197
+ subtotal,
198
+ total,
199
+ discount: { ...state.discount, amount: discountAmount },
200
+ };
201
+ }),
202
+
203
+ clearCart: () => set({
204
+ cart: [],
205
+ subtotal: 0,
206
+ total: 0,
207
+ discount: { type: 'none', value: 0, amount: 0 },
208
+ customer: null,
209
+ selectedItemId: null,
210
+ }),
211
+
212
+ selectItem: (id) => set({ selectedItemId: id }),
213
+
214
+ setCustomer: (customer) => set({ customer }),
215
+
216
+ applyDiscount: (type, value) => set((state) => {
217
+ const newDiscount: DiscountInfo = { type, value, amount: 0 };
218
+ const { subtotal, total, discountAmount } = recalculate(state.cart, newDiscount);
219
+ newDiscount.amount = discountAmount;
220
+
221
+ return {
222
+ discount: newDiscount,
223
+ subtotal,
224
+ total,
225
+ };
226
+ }),
227
+
228
+ clearDiscount: () => set((state) => {
229
+ const newDiscount: DiscountInfo = { type: 'none', value: 0, amount: 0 };
230
+ const { subtotal, total } = recalculate(state.cart, newDiscount);
231
+
232
+ return {
233
+ discount: newDiscount,
234
+ subtotal,
235
+ total,
236
+ };
237
+ }),
238
+
239
+ holdCurrentSale: (note = '') => set((state) => {
240
+ if (state.cart.length === 0) return state;
241
+
242
+ const newHeldSale: HeldSale = {
243
+ id: `HOLD-${Date.now()}`,
244
+ cart: [...state.cart],
245
+ customer: state.customer,
246
+ discount: { ...state.discount },
247
+ subtotal: state.subtotal,
248
+ total: state.total,
249
+ timestamp: new Date(),
250
+ note,
251
+ };
252
+
253
+ return {
254
+ heldSales: [...state.heldSales, newHeldSale],
255
+ cart: [],
256
+ customer: null,
257
+ subtotal: 0,
258
+ total: 0,
259
+ discount: { type: 'none', value: 0, amount: 0 },
260
+ selectedItemId: null,
261
+ };
262
+ }),
263
+
264
+ resumeSale: (id) => set((state) => {
265
+ const saleToResume = state.heldSales.find((sale) => sale.id === id);
266
+ if (!saleToResume) return state;
267
+
268
+ return {
269
+ cart: saleToResume.cart,
270
+ customer: saleToResume.customer,
271
+ discount: saleToResume.discount,
272
+ subtotal: saleToResume.subtotal,
273
+ total: saleToResume.total,
274
+ heldSales: state.heldSales.filter((sale) => sale.id !== id),
275
+ selectedItemId: null,
276
+ };
277
+ }),
278
+
279
+ deleteHeldSale: (id) => set((state) => ({
280
+ heldSales: state.heldSales.filter((sale) => sale.id !== id),
281
+ })),
282
+
283
+ completeSale: () => set({
284
+ cart: [],
285
+ customer: null,
286
+ subtotal: 0,
287
+ total: 0,
288
+ discount: { type: 'none', value: 0, amount: 0 },
289
+ selectedItemId: null,
290
+ }),
291
+
292
+ toggleKeypad: () => set((state) => ({ keypadVisible: !state.keypadVisible })),
293
+
294
+ setKeypadMode: (mode) => set({ keypadMode: mode, keypadValue: '' }),
295
+
296
+ setKeypadValue: (value) => set({ keypadValue: value }),
297
+
298
+ applyKeypadValue: () => set((state) => {
299
+ if (!state.selectedItemId && state.keypadMode !== 'discount') return state;
300
+
301
+ const numericValue = parseFloat(state.keypadValue);
302
+ if (isNaN(numericValue) && state.keypadValue !== '') return state;
303
+
304
+ if (state.keypadMode === 'qty' && state.selectedItemId) {
305
+ if (state.keypadValue === '') return state;
306
+ const newCart = state.cart.map((item) => {
307
+ if (item.id === state.selectedItemId) {
308
+ const newQty = numericValue;
309
+ return { ...item, quantity: newQty, line_total: newQty * item.unit_price };
310
+ }
311
+ return item;
312
+ }).filter((item) => item.quantity > 0);
313
+
314
+ const { subtotal, total, discountAmount } = recalculate(newCart, state.discount);
315
+ return { cart: newCart, subtotal, total, discount: { ...state.discount, amount: discountAmount }, keypadValue: '' };
316
+ }
317
+
318
+ if (state.keypadMode === 'price' && state.selectedItemId) {
319
+ if (state.keypadValue === '') return state;
320
+ const newCart = state.cart.map((item) => {
321
+ if (item.id === state.selectedItemId) {
322
+ return { ...item, unit_price: numericValue, line_total: item.quantity * numericValue };
323
+ }
324
+ return item;
325
+ });
326
+
327
+ const { subtotal, total, discountAmount } = recalculate(newCart, state.discount);
328
+ return { cart: newCart, subtotal, total, discount: { ...state.discount, amount: discountAmount }, keypadValue: '' };
329
+ }
330
+
331
+ if (state.keypadMode === 'discount') {
332
+ if (state.keypadValue === '') return state;
333
+ // Simple logic: apply percentage for keypad, could be expanded. Let's just apply percentage.
334
+ const newDiscount: DiscountInfo = { type: 'percentage', value: numericValue, amount: 0 };
335
+ const { subtotal, total, discountAmount } = recalculate(state.cart, newDiscount);
336
+ newDiscount.amount = discountAmount;
337
+ return { discount: newDiscount, subtotal, total, keypadValue: '' };
338
+ }
339
+
340
+ return state;
341
+ }),
342
+
343
+ setActivePanel: (panel) => set({ activePanel: panel }),
344
+ }));
@@ -0,0 +1,85 @@
1
+ import type { Config } from 'tailwindcss';
2
+
3
+ const config: Config = {
4
+ content: [
5
+ './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6
+ './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7
+ './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8
+ ],
9
+ darkMode: 'class',
10
+ theme: {
11
+ extend: {
12
+ colors: {
13
+ pos: {
14
+ 50: '#FAF5F9',
15
+ 100: '#F0E6EE',
16
+ 200: '#DCC8D9',
17
+ 300: '#C4A3BD',
18
+ 400: '#9B6E91',
19
+ 500: '#714B67',
20
+ 600: '#5D3D55',
21
+ 700: '#4A3044',
22
+ 800: '#362333',
23
+ 900: '#231622',
24
+ },
25
+ success: '#12B76A',
26
+ warning: '#F79009',
27
+ danger: '#F04438',
28
+ brand: {
29
+ 50: '#EAEFFE',
30
+ 100: '#D5DFFE',
31
+ 200: '#ABBDFE',
32
+ 300: '#829DFD',
33
+ 400: '#587CFC',
34
+ 500: '#2A50D8',
35
+ 600: '#213EAF',
36
+ 700: '#172C85',
37
+ 800: '#0E1A5C',
38
+ 900: '#050833',
39
+ },
40
+ teal: {
41
+ 50: '#E7F6F5',
42
+ 100: '#D0ECE9',
43
+ 200: '#A1D9D4',
44
+ 300: '#72C5BE',
45
+ 400: '#43B2A9',
46
+ 500: '#12A594',
47
+ 600: '#0E8476',
48
+ 700: '#0B6359',
49
+ 800: '#07423B',
50
+ 900: '#04211E',
51
+ },
52
+ status: {
53
+ success: '#12B76A',
54
+ warning: '#F79009',
55
+ danger: '#F04438',
56
+ info: '#3B82F6'
57
+ },
58
+ gray: {
59
+ 0: '#FFFFFF',
60
+ 50: '#F9FAFB',
61
+ 100: '#F3F4F6',
62
+ 200: '#E5E7EB',
63
+ 300: '#D1D5DB',
64
+ 400: '#9CA3AF',
65
+ 500: '#6B7280',
66
+ 600: '#4B5563',
67
+ 700: '#374151',
68
+ 800: '#1F2937',
69
+ 900: '#111827',
70
+ 950: '#030712'
71
+ },
72
+ background: 'var(--background)',
73
+ foreground: 'var(--foreground)',
74
+ surface: 'var(--surface)',
75
+ border: 'var(--border)',
76
+ },
77
+ fontFamily: {
78
+ sans: ['var(--font-inter)'],
79
+ },
80
+ },
81
+ },
82
+ plugins: [],
83
+ };
84
+
85
+ export default config;
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26
+ "exclude": ["node_modules"]
27
+ }
Binary file
Binary file
package/img/logo.png ADDED
Binary file