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,50 @@
1
+ import * as React from 'react';
2
+ import * as DialogPrimitive from '@radix-ui/react-dialog';
3
+
4
+ export interface ConfirmDialogProps {
5
+ open: boolean;
6
+ onConfirm: () => void;
7
+ onCancel: () => void;
8
+ title: string;
9
+ message: string;
10
+ confirmText?: string;
11
+ isLoading?: boolean;
12
+ }
13
+
14
+ export function ConfirmDialog({ open, onConfirm, onCancel, title, message, confirmText = 'Delete', isLoading }: ConfirmDialogProps) {
15
+ return (
16
+ <DialogPrimitive.Root open={open} onOpenChange={(isOpen) => !isOpen && onCancel()}>
17
+ <DialogPrimitive.Portal>
18
+ <DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
19
+ <DialogPrimitive.Content className="fixed left-[50%] top-[50%] z-50 grid w-full max-w-md translate-x-[-50%] translate-y-[-50%] gap-4 border border-gray-800 bg-[#0B1220] p-6 shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-xl">
20
+ <div className="flex flex-col space-y-2 text-center sm:text-left">
21
+ <DialogPrimitive.Title className="text-lg font-semibold text-white">
22
+ {title}
23
+ </DialogPrimitive.Title>
24
+ <DialogPrimitive.Description className="text-sm text-gray-400">
25
+ {message}
26
+ </DialogPrimitive.Description>
27
+ </div>
28
+ <div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-4">
29
+ <button
30
+ type="button"
31
+ onClick={onCancel}
32
+ disabled={isLoading}
33
+ className="mt-2 inline-flex h-10 items-center justify-center rounded-md border border-gray-700 bg-transparent px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:opacity-50 sm:mt-0"
34
+ >
35
+ Cancel
36
+ </button>
37
+ <button
38
+ type="button"
39
+ onClick={onConfirm}
40
+ disabled={isLoading}
41
+ className="inline-flex h-10 items-center justify-center rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50"
42
+ >
43
+ {isLoading ? 'Processing...' : confirmText}
44
+ </button>
45
+ </div>
46
+ </DialogPrimitive.Content>
47
+ </DialogPrimitive.Portal>
48
+ </DialogPrimitive.Root>
49
+ );
50
+ }
@@ -0,0 +1,32 @@
1
+ import { forwardRef, InputHTMLAttributes } from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
5
+ error?: boolean;
6
+ leadingIcon?: React.ReactNode;
7
+ }
8
+
9
+ export const Input = forwardRef<HTMLInputElement, InputProps>(
10
+ ({ className, error, leadingIcon, ...props }, ref) => {
11
+ return (
12
+ <div className="relative flex items-center w-full">
13
+ {leadingIcon && (
14
+ <div className="absolute left-3 text-gray-400">
15
+ {leadingIcon}
16
+ </div>
17
+ )}
18
+ <input
19
+ className={cn(
20
+ "flex h-10 w-full rounded-lg border border-border bg-background px-3 py-2 text-sm placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-brand-500 disabled:cursor-not-allowed disabled:opacity-50 transition-shadow",
21
+ leadingIcon && "pl-10",
22
+ error && "border-danger focus:ring-danger",
23
+ className
24
+ )}
25
+ ref={ref}
26
+ {...props}
27
+ />
28
+ </div>
29
+ );
30
+ }
31
+ );
32
+ Input.displayName = 'Input';
@@ -0,0 +1,228 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import * as Popover from '@radix-ui/react-popover';
5
+ import { Bell, Info, AlertTriangle, XCircle, CheckCircle, Trash2, CheckCheck } from 'lucide-react';
6
+ import { api } from '@/lib/api';
7
+ import { cn, formatDate } from '@/lib/utils';
8
+ import { useLocale, useTranslations } from 'next-intl';
9
+ import { motion, AnimatePresence } from 'framer-motion';
10
+
11
+ interface Notification {
12
+ id: string;
13
+ type: 'info' | 'warning' | 'danger' | 'success';
14
+ title: string;
15
+ title_ar?: string;
16
+ message: string;
17
+ message_ar?: string;
18
+ is_read: boolean;
19
+ created_at: string;
20
+ }
21
+
22
+ export function NotificationCenter() {
23
+ const [notifications, setNotifications] = useState<Notification[]>([]);
24
+ const [unreadCount, setUnreadCount] = useState(0);
25
+ const [isOpen, setIsOpen] = useState(false);
26
+ const locale = useLocale();
27
+ const t = useTranslations('Notifications');
28
+
29
+ const fetchUnreadCount = async () => {
30
+ try {
31
+ const data = await api.getUnreadNotificationCount() as { count: number };
32
+ setUnreadCount(data.count);
33
+ } catch (error) {
34
+ console.error('Failed to fetch unread count:', error);
35
+ }
36
+ };
37
+
38
+ const fetchNotifications = async () => {
39
+ try {
40
+ const data = await api.getNotifications() as Notification[];
41
+ setNotifications(data);
42
+ } catch (error) {
43
+ console.error('Failed to fetch notifications:', error);
44
+ }
45
+ };
46
+
47
+ useEffect(() => {
48
+ fetchUnreadCount();
49
+ const interval = setInterval(fetchUnreadCount, 30000); // 30s interval
50
+ return () => clearInterval(interval);
51
+ }, []);
52
+
53
+ useEffect(() => {
54
+ if (isOpen) {
55
+ fetchNotifications();
56
+ fetchUnreadCount();
57
+ }
58
+ }, [isOpen]);
59
+
60
+ const handleMarkAsRead = async (id: string) => {
61
+ try {
62
+ await api.markNotificationAsRead(id);
63
+ setNotifications((prev) =>
64
+ prev.map((n) => (n.id === id ? { ...n, is_read: true } : n))
65
+ );
66
+ setUnreadCount((prev) => Math.max(0, prev - 1));
67
+ } catch (error) {
68
+ console.error('Failed to mark as read:', error);
69
+ }
70
+ };
71
+
72
+ const handleMarkAllAsRead = async () => {
73
+ try {
74
+ await api.markAllNotificationsAsRead();
75
+ setNotifications((prev) => prev.map((n) => ({ ...n, is_read: true })));
76
+ setUnreadCount(0);
77
+ } catch (error) {
78
+ console.error('Failed to mark all as read:', error);
79
+ }
80
+ };
81
+
82
+ const handleDelete = async (e: React.MouseEvent, id: string) => {
83
+ e.stopPropagation();
84
+ try {
85
+ await api.deleteNotification(id);
86
+ setNotifications((prev) => {
87
+ const notif = prev.find((n) => n.id === id);
88
+ if (notif && !notif.is_read) {
89
+ setUnreadCount((c) => Math.max(0, c - 1));
90
+ }
91
+ return prev.filter((n) => n.id !== id);
92
+ });
93
+ } catch (error) {
94
+ console.error('Failed to delete notification:', error);
95
+ }
96
+ };
97
+
98
+ const getIcon = (type: Notification['type']) => {
99
+ switch (type) {
100
+ case 'info':
101
+ return <Info className="text-blue-500" size={20} />;
102
+ case 'warning':
103
+ return <AlertTriangle className="text-amber-500" size={20} />;
104
+ case 'danger':
105
+ return <XCircle className="text-red-500" size={20} />;
106
+ case 'success':
107
+ return <CheckCircle className="text-green-500" size={20} />;
108
+ default:
109
+ return <Info className="text-blue-500" size={20} />;
110
+ }
111
+ };
112
+
113
+ const getRelativeTime = (dateString: string) => {
114
+ const date = new Date(dateString);
115
+ const now = new Date();
116
+ const diffMs = now.getTime() - date.getTime();
117
+ const diffMins = Math.floor(diffMs / 60000);
118
+ const diffHrs = Math.floor(diffMins / 60);
119
+ const diffDays = Math.floor(diffHrs / 24);
120
+
121
+ if (diffMins < 1) return t('justNow');
122
+ if (diffMins < 60) return t('minutesAgo', { count: diffMins });
123
+ if (diffHrs < 24) return t('hoursAgo', { count: diffHrs });
124
+ if (diffDays < 7) return t('daysAgo', { count: diffDays });
125
+
126
+ return formatDate(date, locale);
127
+ };
128
+
129
+ return (
130
+ <Popover.Root open={isOpen} onOpenChange={setIsOpen}>
131
+ <Popover.Trigger asChild>
132
+ <button className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 relative transition-colors focus:outline-none focus:ring-2 focus:ring-brand-500/50">
133
+ <Bell size={20} />
134
+ {unreadCount > 0 && (
135
+ <span className="absolute top-1.5 right-1.5 min-w-[8px] h-2 px-1 text-[10px] font-bold leading-none text-white bg-danger rounded-full border border-background flex items-center justify-center">
136
+ {unreadCount > 9 ? '9+' : ''}
137
+ </span>
138
+ )}
139
+ </button>
140
+ </Popover.Trigger>
141
+
142
+ <AnimatePresence>
143
+ {isOpen && (
144
+ <Popover.Portal forceMount>
145
+ <Popover.Content
146
+ align="end"
147
+ sideOffset={8}
148
+ asChild
149
+ className="z-50 w-80 sm:w-96 rounded-xl bg-surface/95 backdrop-blur-xl border border-border shadow-2xl overflow-hidden"
150
+ >
151
+ <motion.div
152
+ initial={{ opacity: 0, y: 10, scale: 0.95 }}
153
+ animate={{ opacity: 1, y: 0, scale: 1 }}
154
+ exit={{ opacity: 0, y: 10, scale: 0.95 }}
155
+ transition={{ duration: 0.2, ease: "easeOut" }}
156
+ >
157
+ {/* Header */}
158
+ <div className="flex items-center justify-between p-4 border-b border-border bg-surface/50">
159
+ <h3 className="font-semibold text-foreground text-lg">{t('title')}</h3>
160
+ {unreadCount > 0 && (
161
+ <button
162
+ onClick={handleMarkAllAsRead}
163
+ className="text-xs font-medium text-brand-500 hover:text-brand-600 transition-colors flex items-center gap-1"
164
+ >
165
+ <CheckCheck size={14} />
166
+ {t('markAllAsRead')}
167
+ </button>
168
+ )}
169
+ </div>
170
+
171
+ {/* List */}
172
+ <div className="max-h-[400px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700">
173
+ {notifications.length === 0 ? (
174
+ <div className="flex flex-col items-center justify-center py-12 px-4 text-center">
175
+ <div className="w-16 h-16 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center mb-4 text-gray-400">
176
+ <Bell size={32} />
177
+ </div>
178
+ <p className="text-foreground font-medium">{t('noNotifications')}</p>
179
+ <p className="text-sm text-muted-foreground mt-1">{t('noNotificationsDesc')}</p>
180
+ </div>
181
+ ) : (
182
+ <div className="divide-y divide-border">
183
+ {notifications.map((notification) => (
184
+ <div
185
+ key={notification.id}
186
+ onClick={() => !notification.is_read && handleMarkAsRead(notification.id)}
187
+ className={cn(
188
+ "relative group p-4 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors cursor-pointer flex gap-3",
189
+ !notification.is_read ? "bg-brand-500/5" : ""
190
+ )}
191
+ >
192
+ <div className="flex-shrink-0 mt-1">
193
+ {getIcon(notification.type)}
194
+ </div>
195
+ <div className="flex-1 min-w-0">
196
+ <div className="flex justify-between items-start mb-1 gap-2">
197
+ <h4 className={cn("text-sm font-semibold truncate", !notification.is_read ? "text-foreground" : "text-muted-foreground")}>
198
+ {locale === 'ar' && notification.title_ar ? notification.title_ar : notification.title}
199
+ </h4>
200
+ <span className="text-[11px] text-muted-foreground whitespace-nowrap flex-shrink-0">
201
+ {getRelativeTime(notification.created_at)}
202
+ </span>
203
+ </div>
204
+ <p className="text-sm text-muted-foreground line-clamp-2 leading-snug">
205
+ {locale === 'ar' && notification.message_ar ? notification.message_ar : notification.message}
206
+ </p>
207
+ </div>
208
+
209
+ <button
210
+ onClick={(e) => handleDelete(e, notification.id)}
211
+ className="absolute top-4 right-4 p-1.5 rounded-md text-gray-400 hover:text-danger hover:bg-danger/10 opacity-0 group-hover:opacity-100 transition-all focus:opacity-100 rtl:right-auto rtl:left-4"
212
+ aria-label={t('delete')}
213
+ >
214
+ <Trash2 size={16} />
215
+ </button>
216
+ </div>
217
+ ))}
218
+ </div>
219
+ )}
220
+ </div>
221
+ </motion.div>
222
+ </Popover.Content>
223
+ </Popover.Portal>
224
+ )}
225
+ </AnimatePresence>
226
+ </Popover.Root>
227
+ );
228
+ }
@@ -0,0 +1,59 @@
1
+ import * as React from 'react';
2
+ import * as SelectPrimitive from '@radix-ui/react-select';
3
+ import { Check, ChevronDown } from 'lucide-react';
4
+ import { cn } from '@/lib/utils'; // Assuming cn exists, otherwise will use simple classes
5
+
6
+ export interface SelectProps {
7
+ options: { value: string; label: string }[];
8
+ value: string;
9
+ onChange: (value: string) => void;
10
+ placeholder?: string;
11
+ disabled?: boolean;
12
+ error?: boolean;
13
+ className?: string;
14
+ }
15
+
16
+ export function Select({ options, value, onChange, placeholder, disabled, error, className }: SelectProps) {
17
+ return (
18
+ <SelectPrimitive.Root value={value} onValueChange={onChange} disabled={disabled}>
19
+ <SelectPrimitive.Trigger
20
+ className={cn(
21
+ "flex h-10 w-full items-center justify-between rounded-lg border bg-background px-3 py-2 text-sm text-foreground ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-brand-500 disabled:cursor-not-allowed disabled:opacity-50",
22
+ error ? "border-red-500 focus:ring-red-500" : "border-border",
23
+ className
24
+ )}
25
+ >
26
+ <SelectPrimitive.Value placeholder={placeholder}>
27
+ {value ? options.find(opt => String(opt.value) === String(value))?.label : undefined}
28
+ </SelectPrimitive.Value>
29
+ <SelectPrimitive.Icon asChild>
30
+ <ChevronDown className="h-4 w-4 opacity-50" />
31
+ </SelectPrimitive.Icon>
32
+ </SelectPrimitive.Trigger>
33
+ <SelectPrimitive.Portal>
34
+ <SelectPrimitive.Content
35
+ className="relative z-50 max-h-64 min-w-[8rem] w-[var(--radix-select-trigger-width)] overflow-hidden overflow-y-auto rounded-md border border-border bg-surface text-foreground shadow-md animate-in fade-in-80"
36
+ position="popper"
37
+ sideOffset={4}
38
+ >
39
+ <SelectPrimitive.Viewport className="p-1">
40
+ {options.map((option) => (
41
+ <SelectPrimitive.Item
42
+ key={option.value}
43
+ value={option.value}
44
+ className="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-brand-500/10 focus:text-brand-500 data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
45
+ >
46
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
47
+ <SelectPrimitive.ItemIndicator>
48
+ <Check className="h-4 w-4" />
49
+ </SelectPrimitive.ItemIndicator>
50
+ </span>
51
+ <SelectPrimitive.ItemText>{option.label}</SelectPrimitive.ItemText>
52
+ </SelectPrimitive.Item>
53
+ ))}
54
+ </SelectPrimitive.Viewport>
55
+ </SelectPrimitive.Content>
56
+ </SelectPrimitive.Portal>
57
+ </SelectPrimitive.Root>
58
+ );
59
+ }
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+ import { useTranslations } from 'next-intl';
3
+ import Image from 'next/image';
4
+ import { useAppStore } from '@/store/app-store';
5
+ import { Link, usePathname } from '@/i18n/routing';
6
+ import { cn } from '@/lib/utils';
7
+ import {
8
+ LayoutDashboard,
9
+ MonitorPlay,
10
+ Package,
11
+ ShoppingCart,
12
+ Users,
13
+ FileText,
14
+ Settings,
15
+ Pill,
16
+ ChevronLeft,
17
+ ChevronRight
18
+ } from 'lucide-react';
19
+
20
+ export function Sidebar() {
21
+ const t = useTranslations('nav');
22
+ const pathname = usePathname();
23
+ const sidebarOpen = useAppStore(state => state.sidebarOpen);
24
+ const sidebarCollapsed = useAppStore(state => state.sidebarCollapsed);
25
+ const setSidebarCollapsed = useAppStore(state => state.setSidebarCollapsed);
26
+
27
+ const navItems = [
28
+ { icon: LayoutDashboard, label: t('dashboard'), href: '/dashboard' },
29
+ { icon: MonitorPlay, label: t('pos'), href: '/pos' },
30
+ { icon: ShoppingCart, label: t('sales'), href: '/sales' },
31
+ { icon: Package, label: t('inventory'), href: '/inventory' },
32
+ { icon: Pill, label: t('catalog'), href: '/catalog/products' },
33
+ { icon: Users, label: t('customers'), href: '/customers' },
34
+ { icon: FileText, label: t('reports'), href: '/reports' },
35
+ { icon: Settings, label: t('admin'), href: '/admin' },
36
+ ];
37
+
38
+ return (
39
+ <aside className={cn(
40
+ "bg-background border-r flex flex-col transition-all duration-300 z-20 h-full fixed md:relative",
41
+ sidebarOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0",
42
+ sidebarCollapsed ? "w-20" : "w-64"
43
+ )}>
44
+ <div className="h-20 flex items-center justify-between px-4 border-b">
45
+ {!sidebarCollapsed && (
46
+ <div className="flex items-center gap-3">
47
+ <div className="flex items-center justify-center w-12 h-12 relative overflow-visible">
48
+ <Image src="/logo.png" alt="Pharma ERP Logo" width={100} height={100} className="object-contain scale-150" priority />
49
+ </div>
50
+ <span className="font-bold text-xl text-brand-600 dark:text-brand-400">Pharma ERP</span>
51
+ </div>
52
+ )}
53
+ {sidebarCollapsed && (
54
+ <div className="mx-auto flex justify-center w-full items-center h-full overflow-visible">
55
+ <Image src="/logo.png" alt="Pharma ERP Logo" width={100} height={100} className="object-contain scale-150" priority />
56
+ </div>
57
+ )}
58
+ <button
59
+ onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
60
+ className="hidden md:flex p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500"
61
+ >
62
+ {sidebarCollapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
63
+ </button>
64
+ </div>
65
+
66
+ <div className="flex-1 overflow-y-auto py-4">
67
+ <nav className="space-y-1 px-3">
68
+ {navItems.map((item) => {
69
+ const isActive = pathname.startsWith(item.href);
70
+ return (
71
+ <Link
72
+ key={item.href}
73
+ href={item.href}
74
+ className={cn(
75
+ "flex items-center rounded-lg px-3 py-2.5 transition-colors group",
76
+ isActive
77
+ ? "bg-brand-50 text-brand-600 dark:bg-brand-900/50 dark:text-brand-400"
78
+ : "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
79
+ )}
80
+ title={sidebarCollapsed ? item.label : undefined}
81
+ >
82
+ <item.icon className={cn("flex-shrink-0", sidebarCollapsed ? "mx-auto" : "mr-3", isActive ? "text-brand-600 dark:text-brand-400" : "text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300")} size={20} />
83
+ {!sidebarCollapsed && <span className="font-medium text-sm">{item.label}</span>}
84
+ </Link>
85
+ )
86
+ })}
87
+ </nav>
88
+ </div>
89
+ </aside>
90
+ );
91
+ }
@@ -0,0 +1,43 @@
1
+ import * as React from 'react';
2
+ import * as DialogPrimitive from '@radix-ui/react-dialog';
3
+ import { X } from 'lucide-react';
4
+ import { cn } from '@/lib/utils'; // Assuming cn exists
5
+
6
+ export interface SlideOverProps {
7
+ open: boolean;
8
+ onClose: () => void;
9
+ title: string;
10
+ children: React.ReactNode;
11
+ footer?: React.ReactNode;
12
+ }
13
+
14
+ export function SlideOver({ open, onClose, title, children, footer }: SlideOverProps) {
15
+ return (
16
+ <DialogPrimitive.Root open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
17
+ <DialogPrimitive.Portal>
18
+ <DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
19
+ <DialogPrimitive.Content className="fixed inset-y-0 right-0 z-50 flex w-full sm:max-w-md flex-col border-l border-gray-800 bg-[#0B1220] shadow-2xl transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right">
20
+ <div className="flex items-center justify-between border-b border-gray-800 px-6 py-4">
21
+ <DialogPrimitive.Title className="text-lg font-semibold text-white">
22
+ {title}
23
+ </DialogPrimitive.Title>
24
+ <DialogPrimitive.Close className="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
25
+ <X className="h-5 w-5 text-gray-400 hover:text-white" />
26
+ <span className="sr-only">Close</span>
27
+ </DialogPrimitive.Close>
28
+ </div>
29
+
30
+ <div className="relative flex-1 overflow-y-auto px-6 py-6 text-gray-300">
31
+ {children}
32
+ </div>
33
+
34
+ {footer && (
35
+ <div className="border-t border-gray-800 bg-[#050814] px-6 py-4">
36
+ {footer}
37
+ </div>
38
+ )}
39
+ </DialogPrimitive.Content>
40
+ </DialogPrimitive.Portal>
41
+ </DialogPrimitive.Root>
42
+ );
43
+ }
@@ -0,0 +1,40 @@
1
+ import { Card } from './card';
2
+ import { cn } from '@/lib/utils';
3
+ import { TrendingUp, TrendingDown } from 'lucide-react';
4
+
5
+ interface StatCardProps {
6
+ title: string;
7
+ value: string;
8
+ trend?: number;
9
+ icon?: React.ReactNode;
10
+ className?: string;
11
+ }
12
+
13
+ export function StatCard({ title, value, trend, icon, className }: StatCardProps) {
14
+ const isPositive = trend && trend > 0;
15
+ const isNegative = trend && trend < 0;
16
+
17
+ return (
18
+ <Card className={cn("p-5 flex flex-col", className)}>
19
+ <div className="flex items-center justify-between mb-4">
20
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">{title}</h3>
21
+ {icon && <div className="text-gray-400 bg-surface p-2 rounded-lg">{icon}</div>}
22
+ </div>
23
+ <div className="mt-auto min-w-0">
24
+ <div className="text-2xl font-bold text-foreground truncate">{value}</div>
25
+ {trend !== undefined && (
26
+ <div className="flex items-center mt-2 text-sm">
27
+ <span className={cn(
28
+ "flex items-center font-medium",
29
+ isPositive ? "text-success" : isNegative ? "text-danger" : "text-gray-500"
30
+ )}>
31
+ {isPositive ? <TrendingUp size={16} className="mr-1" /> : isNegative ? <TrendingDown size={16} className="mr-1" /> : null}
32
+ {Math.abs(trend)}%
33
+ </span>
34
+ <span className="text-gray-500 ml-2">vs last month</span>
35
+ </div>
36
+ )}
37
+ </div>
38
+ </Card>
39
+ );
40
+ }
@@ -0,0 +1,38 @@
1
+ import * as React from 'react';
2
+ import * as SwitchPrimitives from '@radix-ui/react-switch';
3
+ import { cn } from '@/lib/utils'; // Assuming cn exists
4
+
5
+ export interface SwitchProps {
6
+ checked: boolean;
7
+ onCheckedChange: (checked: boolean) => void;
8
+ label?: string;
9
+ description?: string;
10
+ }
11
+
12
+ export function Switch({ checked, onCheckedChange, label, description }: SwitchProps) {
13
+ return (
14
+ <div className="flex items-center space-x-3">
15
+ <SwitchPrimitives.Root
16
+ checked={checked}
17
+ onCheckedChange={onCheckedChange}
18
+ className={cn(
19
+ "peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0B1220] disabled:cursor-not-allowed disabled:opacity-50",
20
+ checked ? "bg-brand-500" : "bg-gray-600"
21
+ )}
22
+ >
23
+ <SwitchPrimitives.Thumb
24
+ className={cn(
25
+ "pointer-events-none block h-5 w-5 rounded-full bg-white shadow-lg ring-0 transition-transform",
26
+ checked ? "translate-x-5" : "translate-x-0"
27
+ )}
28
+ />
29
+ </SwitchPrimitives.Root>
30
+ {(label || description) && (
31
+ <div className="flex flex-col">
32
+ {label && <label className="text-sm font-medium text-gray-200 cursor-pointer" onClick={() => onCheckedChange(!checked)}>{label}</label>}
33
+ {description && <span className="text-xs text-gray-400">{description}</span>}
34
+ </div>
35
+ )}
36
+ </div>
37
+ );
38
+ }