pharmacy-erp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +34 -0
- package/LICENSE +21 -0
- package/README.md +479 -0
- package/backend/Dockerfile +44 -0
- package/backend/app/__init__.py +1 -0
- package/backend/app/core/__init__.py +1 -0
- package/backend/app/core/audit.py +33 -0
- package/backend/app/core/config.py +16 -0
- package/backend/app/core/database.py +26 -0
- package/backend/app/core/deps.py +49 -0
- package/backend/app/core/exceptions.py +43 -0
- package/backend/app/core/security.py +32 -0
- package/backend/app/main.py +54 -0
- package/backend/app/modules/__init__.py +0 -0
- package/backend/app/modules/admin/__init__.py +1 -0
- package/backend/app/modules/admin/models.py +15 -0
- package/backend/app/modules/admin/router.py +353 -0
- package/backend/app/modules/auth/__init__.py +0 -0
- package/backend/app/modules/auth/models.py +16 -0
- package/backend/app/modules/auth/router.py +73 -0
- package/backend/app/modules/auth/schemas.py +29 -0
- package/backend/app/modules/cash_sessions/__init__.py +0 -0
- package/backend/app/modules/cash_sessions/models.py +19 -0
- package/backend/app/modules/cash_sessions/router.py +85 -0
- package/backend/app/modules/cash_sessions/schemas.py +31 -0
- package/backend/app/modules/customers/__init__.py +0 -0
- package/backend/app/modules/customers/models.py +37 -0
- package/backend/app/modules/customers/router.py +296 -0
- package/backend/app/modules/customers/schemas.py +72 -0
- package/backend/app/modules/inventory/__init__.py +0 -0
- package/backend/app/modules/inventory/models.py +16 -0
- package/backend/app/modules/inventory/router.py +150 -0
- package/backend/app/modules/inventory/schemas.py +24 -0
- package/backend/app/modules/medicines/__init__.py +0 -0
- package/backend/app/modules/medicines/models.py +54 -0
- package/backend/app/modules/medicines/router.py +631 -0
- package/backend/app/modules/medicines/schemas.py +153 -0
- package/backend/app/modules/medicines/template_generator.py +118 -0
- package/backend/app/modules/notifications/__init__.py +1 -0
- package/backend/app/modules/notifications/models.py +15 -0
- package/backend/app/modules/notifications/router.py +89 -0
- package/backend/app/modules/notifications/schemas.py +22 -0
- package/backend/app/modules/organizations/__init__.py +0 -0
- package/backend/app/modules/organizations/models.py +16 -0
- package/backend/app/modules/organizations/router.py +49 -0
- package/backend/app/modules/organizations/schemas.py +31 -0
- package/backend/app/modules/reports/__init__.py +0 -0
- package/backend/app/modules/reports/router.py +409 -0
- package/backend/app/modules/roles/__init__.py +1 -0
- package/backend/app/modules/roles/models.py +43 -0
- package/backend/app/modules/roles/router.py +89 -0
- package/backend/app/modules/roles/schemas.py +55 -0
- package/backend/app/modules/sales/__init__.py +0 -0
- package/backend/app/modules/sales/models.py +44 -0
- package/backend/app/modules/sales/router.py +63 -0
- package/backend/app/modules/sales/schemas.py +42 -0
- package/backend/app/modules/sales/service.py +128 -0
- package/backend/app/modules/suppliers/__init__.py +0 -0
- package/backend/app/modules/suppliers/models.py +14 -0
- package/backend/app/modules/suppliers/router.py +56 -0
- package/backend/app/modules/suppliers/schemas.py +27 -0
- package/backend/app/modules/users/__init__.py +0 -0
- package/backend/app/modules/users/router.py +74 -0
- package/backend/app/seed.py +275 -0
- package/backend/data/.gitkeep +1 -0
- package/backend/requirements.txt +17 -0
- package/bin/cli.js +288 -0
- package/docker-compose.dev.yml +42 -0
- package/docker-compose.yml +59 -0
- package/frontend/Dockerfile +56 -0
- package/frontend/next.config.ts +19 -0
- package/frontend/package.json +60 -0
- package/frontend/pnpm-lock.yaml +5678 -0
- package/frontend/pnpm-workspace.yaml +6 -0
- package/frontend/postcss.config.mjs +6 -0
- package/frontend/public/logo.png +0 -0
- package/frontend/src/app/[locale]/(app)/admin/page.tsx +1426 -0
- package/frontend/src/app/[locale]/(app)/catalog/products/[id]/page.tsx +505 -0
- package/frontend/src/app/[locale]/(app)/catalog/products/page.tsx +753 -0
- package/frontend/src/app/[locale]/(app)/customers/[id]/page.tsx +500 -0
- package/frontend/src/app/[locale]/(app)/customers/page.tsx +538 -0
- package/frontend/src/app/[locale]/(app)/dashboard/page.tsx +175 -0
- package/frontend/src/app/[locale]/(app)/inventory/page.tsx +765 -0
- package/frontend/src/app/[locale]/(app)/layout.tsx +60 -0
- package/frontend/src/app/[locale]/(app)/purchasing/page.tsx +1 -0
- package/frontend/src/app/[locale]/(app)/reports/page.tsx +794 -0
- package/frontend/src/app/[locale]/(app)/sales/page.tsx +296 -0
- package/frontend/src/app/[locale]/(auth)/login/page.tsx +100 -0
- package/frontend/src/app/[locale]/(pos)/layout.tsx +33 -0
- package/frontend/src/app/[locale]/(pos)/pos/page.tsx +463 -0
- package/frontend/src/app/[locale]/(pos)/pos/pos-data.ts +89 -0
- package/frontend/src/app/[locale]/layout.tsx +30 -0
- package/frontend/src/app/[locale]/page.tsx +6 -0
- package/frontend/src/app/globals.css +77 -0
- package/frontend/src/app/layout.tsx +23 -0
- package/frontend/src/app/print.css +18 -0
- package/frontend/src/components/pos/CartPanel.tsx +525 -0
- package/frontend/src/components/pos/CashSessionGuard.tsx +255 -0
- package/frontend/src/components/pos/CloseSessionModal.tsx +160 -0
- package/frontend/src/components/pos/PaymentModal.tsx +266 -0
- package/frontend/src/components/pos/PaymentSettingsModal.tsx +472 -0
- package/frontend/src/components/pos/ProductCatalog.tsx +140 -0
- package/frontend/src/components/pos/SaleReceipt.tsx +133 -0
- package/frontend/src/components/ui/badge.tsx +28 -0
- package/frontend/src/components/ui/bulk-import-modal.tsx +400 -0
- package/frontend/src/components/ui/button.tsx +60 -0
- package/frontend/src/components/ui/card.tsx +13 -0
- package/frontend/src/components/ui/confirm-dialog.tsx +50 -0
- package/frontend/src/components/ui/input.tsx +32 -0
- package/frontend/src/components/ui/notification-center.tsx +228 -0
- package/frontend/src/components/ui/select.tsx +59 -0
- package/frontend/src/components/ui/sidebar.tsx +91 -0
- package/frontend/src/components/ui/slide-over.tsx +43 -0
- package/frontend/src/components/ui/stat-card.tsx +40 -0
- package/frontend/src/components/ui/switch.tsx +38 -0
- package/frontend/src/components/ui/topbar.tsx +97 -0
- package/frontend/src/i18n/request.ts +13 -0
- package/frontend/src/i18n/routing.ts +9 -0
- package/frontend/src/lib/api.ts +240 -0
- package/frontend/src/lib/constants.ts +30 -0
- package/frontend/src/lib/utils.ts +32 -0
- package/frontend/src/messages/ar.json +106 -0
- package/frontend/src/messages/en.json +106 -0
- package/frontend/src/middleware.ts +8 -0
- package/frontend/src/store/app-store.ts +37 -0
- package/frontend/src/store/auth-store.ts +52 -0
- package/frontend/src/store/payment-methods-store.ts +144 -0
- package/frontend/src/store/pos-store.ts +344 -0
- package/frontend/tailwind.config.ts +85 -0
- package/frontend/tsconfig.json +27 -0
- package/img/backgraund-logo.png +0 -0
- package/img/full-logo.png +0 -0
- package/img/logo.png +0 -0
- package/nginx/nginx.conf +67 -0
- package/package.json +88 -0
- package/start.bat +90 -0
- package/start.sh +64 -0
|
@@ -0,0 +1,1426 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { api } from '@/lib/api';
|
|
5
|
+
import { SlideOver } from '@/components/ui/slide-over';
|
|
6
|
+
import { Select } from '@/components/ui/select';
|
|
7
|
+
import { Badge } from '@/components/ui/badge';
|
|
8
|
+
import { Switch } from '@/components/ui/switch';
|
|
9
|
+
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
|
10
|
+
import { toast } from 'react-hot-toast';
|
|
11
|
+
import {
|
|
12
|
+
Users, Shield, Building2, CreditCard, HardDrive, ScrollText,
|
|
13
|
+
Settings, Info, Plus, Edit, Trash2, Search, ChevronRight,
|
|
14
|
+
UserCheck, UserX, Mail, Phone, Key, Globe, Clock, Bell,
|
|
15
|
+
Database, CheckCircle2, XCircle, Download, Upload, Calendar,
|
|
16
|
+
Lock, Eye, EyeOff, AlertTriangle, RefreshCw, Zap, Heart
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { cn } from '@/lib/utils';
|
|
19
|
+
import { useAppStore } from '@/store/app-store';
|
|
20
|
+
|
|
21
|
+
// ─── Tab Configuration ──────────────────────────────────────────────────
|
|
22
|
+
const TABS = [
|
|
23
|
+
{ id: 'users', label: 'Users', icon: Users, description: 'Manage user accounts and access' },
|
|
24
|
+
{ id: 'roles', label: 'Roles & Permissions', icon: Shield, description: 'Configure role-based access' },
|
|
25
|
+
{ id: 'branches', label: 'Branches', icon: Building2, description: 'Manage pharmacy branches' },
|
|
26
|
+
{ id: 'payment', label: 'Payment Methods', icon: CreditCard, description: 'Accepted payment methods' },
|
|
27
|
+
{ id: 'backup', label: 'Backup & Restore', icon: HardDrive, description: 'Database backup & recovery' },
|
|
28
|
+
{ id: 'audit', label: 'Audit Logs', icon: ScrollText, description: 'Activity & change history' },
|
|
29
|
+
{ id: 'system', label: 'System Settings', icon: Settings, description: 'General configuration' },
|
|
30
|
+
{ id: 'about', label: 'About', icon: Info, description: 'System information' },
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
type TabId = typeof TABS[number]['id'];
|
|
34
|
+
|
|
35
|
+
// ─── Role Colors ────────────────────────────────────────────────────────
|
|
36
|
+
const ROLE_COLORS: Record<string, string> = {
|
|
37
|
+
red: 'bg-red-500/10 text-red-400 border-red-500/20',
|
|
38
|
+
purple: 'bg-purple-500/10 text-purple-400 border-purple-500/20',
|
|
39
|
+
blue: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
|
|
40
|
+
green: 'bg-green-500/10 text-green-400 border-green-500/20',
|
|
41
|
+
amber: 'bg-amber-500/10 text-amber-400 border-amber-500/20',
|
|
42
|
+
teal: 'bg-teal-500/10 text-teal-400 border-teal-500/20',
|
|
43
|
+
indigo: 'bg-indigo-500/10 text-indigo-400 border-indigo-500/20',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// ─── Default Role Definitions (fallback when API unavailable) ───────────
|
|
47
|
+
const DEFAULT_ROLES = [
|
|
48
|
+
{ name: 'SUPER_ADMIN', display_name: 'Super Admin', display_name_ar: 'المدير العام', description: 'Full system access. Can manage everything.', color: 'red', is_system: true, user_count: 1 },
|
|
49
|
+
{ name: 'OWNER', display_name: 'Owner', display_name_ar: 'المالك', description: 'Business owner with access to all features except system settings.', color: 'purple', is_system: true, user_count: 0 },
|
|
50
|
+
{ name: 'BRANCH_MANAGER', display_name: 'Branch Manager', display_name_ar: 'مدير الفرع', description: 'Manages branch operations, inventory, and staff.', color: 'blue', is_system: true, user_count: 0 },
|
|
51
|
+
{ name: 'PHARMACIST', display_name: 'Pharmacist', display_name_ar: 'صيدلي', description: 'Can process sales, view inventory, and manage prescriptions.', color: 'green', is_system: true, user_count: 0 },
|
|
52
|
+
{ name: 'CASHIER', display_name: 'Cashier', display_name_ar: 'كاشير', description: 'POS access for processing sales and returns.', color: 'amber', is_system: true, user_count: 0 },
|
|
53
|
+
{ name: 'INVENTORY_STAFF', display_name: 'Inventory Staff', display_name_ar: 'موظف المخزون', description: 'Manages inventory, receives stock, and handles adjustments.', color: 'teal', is_system: true, user_count: 0 },
|
|
54
|
+
{ name: 'ACCOUNTANT', display_name: 'Accountant', display_name_ar: 'محاسب', description: 'Access to financial reports, sales data, and purchases.', color: 'indigo', is_system: true, user_count: 0 },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// ─── Permission Module Definitions ──────────────────────────────────────
|
|
58
|
+
const PERMISSION_MODULES = [
|
|
59
|
+
{ module: 'dashboard', label: 'Dashboard', actions: ['view'] },
|
|
60
|
+
{ module: 'pos', label: 'Point of Sale', actions: ['view', 'create_sale', 'void_sale', 'apply_discount', 'hold_sale'] },
|
|
61
|
+
{ module: 'sales', label: 'Sales', actions: ['view', 'create', 'void', 'return_sale', 'export'] },
|
|
62
|
+
{ module: 'inventory', label: 'Inventory', actions: ['view', 'adjust', 'transfer', 'receive'] },
|
|
63
|
+
{ module: 'catalog', label: 'Catalog', actions: ['view', 'create', 'edit', 'delete'] },
|
|
64
|
+
{ module: 'customers', label: 'Customers', actions: ['view', 'create', 'edit', 'delete'] },
|
|
65
|
+
{ module: 'reports', label: 'Reports', actions: ['view', 'export'] },
|
|
66
|
+
{ module: 'settings', label: 'Settings', actions: ['view', 'manage_users', 'manage_roles', 'manage_branches', 'manage_system'] },
|
|
67
|
+
{ module: 'purchases', label: 'Purchases', actions: ['view', 'create', 'approve', 'receive'] },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
73
|
+
// MAIN COMPONENT
|
|
74
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
75
|
+
export default function SettingsPage() {
|
|
76
|
+
const { currency, setCurrency } = useAppStore();
|
|
77
|
+
const [activeTab, setActiveTab] = useState<TabId>('users');
|
|
78
|
+
|
|
79
|
+
// ── Users State ──
|
|
80
|
+
const [users, setUsers] = useState<any[]>([]);
|
|
81
|
+
const [usersLoading, setUsersLoading] = useState(true);
|
|
82
|
+
const [userSlideOpen, setUserSlideOpen] = useState(false);
|
|
83
|
+
const [editingUserId, setEditingUserId] = useState<string | null>(null);
|
|
84
|
+
const [userForm, setUserForm] = useState({ full_name: '', email: '', password: '', role: 'CASHIER', phone: '' });
|
|
85
|
+
const [deleteUserDialog, setDeleteUserDialog] = useState({ open: false, id: '', name: '' });
|
|
86
|
+
|
|
87
|
+
// ── Branches State ──
|
|
88
|
+
const [branches, setBranches] = useState<any[]>([]);
|
|
89
|
+
const [branchesLoading, setBranchesLoading] = useState(true);
|
|
90
|
+
const [branchSlideOpen, setBranchSlideOpen] = useState(false);
|
|
91
|
+
const [editingBranchId, setEditingBranchId] = useState<string | null>(null);
|
|
92
|
+
const [branchForm, setBranchForm] = useState({ name: '', name_ar: '', code: '', address: '', phone: '', email: '', currency: 'SDG', timezone: 'Africa/Khartoum' });
|
|
93
|
+
|
|
94
|
+
// ── Roles State ──
|
|
95
|
+
const [roles, setRoles] = useState<any[]>(DEFAULT_ROLES);
|
|
96
|
+
const [expandedRole, setExpandedRole] = useState<string | null>(null);
|
|
97
|
+
|
|
98
|
+
// ── Payment Methods State ──
|
|
99
|
+
const [paymentMethods, setPaymentMethods] = useState([
|
|
100
|
+
{ id: 'cash', name: 'Cash', name_ar: 'نقدي', enabled: true, icon: '💵' },
|
|
101
|
+
{ id: 'card', name: 'Credit/Debit Card', name_ar: 'بطاقة ائتمان', enabled: true, icon: '💳' },
|
|
102
|
+
{ id: 'bank', name: 'Bank Transfer', name_ar: 'حوالة بنكية', enabled: false, icon: '🏦' },
|
|
103
|
+
{ id: 'insurance', name: 'Insurance', name_ar: 'تأمين', enabled: false, icon: '🏥' },
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
// ── System Settings State ──
|
|
107
|
+
const [systemSettings, setSystemSettings] = useState({
|
|
108
|
+
pharmacyName: 'Pharma ERP',
|
|
109
|
+
licenseNumber: 'PH-2026-001',
|
|
110
|
+
phone: '+249 12 345 6789',
|
|
111
|
+
address: 'Khartoum, Sudan',
|
|
112
|
+
currency: currency || 'SDG',
|
|
113
|
+
timezone: 'Africa/Khartoum',
|
|
114
|
+
dateFormat: 'DD/MM/YYYY',
|
|
115
|
+
language: 'en',
|
|
116
|
+
lowStockThreshold: 10,
|
|
117
|
+
expiryWarningDays: 90,
|
|
118
|
+
sessionTimeout: 120,
|
|
119
|
+
enforceStrongPassword: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── General State ──
|
|
123
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
124
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
125
|
+
|
|
126
|
+
// Backup state
|
|
127
|
+
const [backups, setBackups] = useState<any[]>([]);
|
|
128
|
+
const [backupsLoading, setBackupsLoading] = useState(false);
|
|
129
|
+
const [creatingBackup, setCreatingBackup] = useState(false);
|
|
130
|
+
const restoreInputRef = React.useRef<HTMLInputElement>(null);
|
|
131
|
+
|
|
132
|
+
// Audit log state
|
|
133
|
+
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
|
134
|
+
const [auditTotal, setAuditTotal] = useState(0);
|
|
135
|
+
const [auditPage, setAuditPage] = useState(1);
|
|
136
|
+
const [auditPages, setAuditPages] = useState(1);
|
|
137
|
+
const [auditLoading, setAuditLoading] = useState(false);
|
|
138
|
+
const [auditSearch, setAuditSearch] = useState('');
|
|
139
|
+
const [auditModule, setAuditModule] = useState('all');
|
|
140
|
+
const [auditAction, setAuditAction] = useState('all');
|
|
141
|
+
// ── Danger Zone State ──
|
|
142
|
+
const [wipeDataOpen, setWipeDataOpen] = useState(false);
|
|
143
|
+
const [factoryResetOpen, setFactoryResetOpen] = useState(false);
|
|
144
|
+
const [isWiping, setIsWiping] = useState(false);
|
|
145
|
+
const [isResetting, setIsResetting] = useState(false);
|
|
146
|
+
|
|
147
|
+
const handleWipeData = async () => {
|
|
148
|
+
try {
|
|
149
|
+
setIsWiping(true);
|
|
150
|
+
await api.wipeData();
|
|
151
|
+
toast.success('All business data has been wiped.');
|
|
152
|
+
setWipeDataOpen(false);
|
|
153
|
+
} catch (err: any) {
|
|
154
|
+
toast.error(err.message || 'Failed to wipe data');
|
|
155
|
+
} finally {
|
|
156
|
+
setIsWiping(false);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleFactoryReset = async () => {
|
|
161
|
+
try {
|
|
162
|
+
setIsResetting(true);
|
|
163
|
+
await api.factoryReset();
|
|
164
|
+
toast.success('Factory reset complete. Redirecting to login...');
|
|
165
|
+
setTimeout(() => {
|
|
166
|
+
window.location.href = '/en/login';
|
|
167
|
+
}, 1500);
|
|
168
|
+
} catch (err: any) {
|
|
169
|
+
toast.error(err.message || 'Failed to factory reset');
|
|
170
|
+
setIsResetting(false);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Fetch backups
|
|
175
|
+
const fetchBackups = async () => {
|
|
176
|
+
setBackupsLoading(true);
|
|
177
|
+
try {
|
|
178
|
+
const data = await api.getBackups();
|
|
179
|
+
setBackups(Array.isArray(data) ? data : []);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
setBackups([]);
|
|
182
|
+
} finally {
|
|
183
|
+
setBackupsLoading(false);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Fetch audit logs
|
|
188
|
+
const fetchAuditLogs = async () => {
|
|
189
|
+
setAuditLoading(true);
|
|
190
|
+
try {
|
|
191
|
+
const data: any = await api.getAuditLogs({
|
|
192
|
+
page: auditPage,
|
|
193
|
+
per_page: 20,
|
|
194
|
+
module: auditModule,
|
|
195
|
+
action: auditAction,
|
|
196
|
+
search: auditSearch || undefined
|
|
197
|
+
});
|
|
198
|
+
setAuditLogs(data.items || []);
|
|
199
|
+
setAuditTotal(data.total || 0);
|
|
200
|
+
setAuditPages(data.pages || 1);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
setAuditLogs([]);
|
|
203
|
+
} finally {
|
|
204
|
+
setAuditLoading(false);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Handle backup creation
|
|
209
|
+
const handleCreateBackup = async () => {
|
|
210
|
+
setCreatingBackup(true);
|
|
211
|
+
try {
|
|
212
|
+
await api.createBackup();
|
|
213
|
+
toast.success('Backup created successfully');
|
|
214
|
+
fetchBackups();
|
|
215
|
+
} catch (e: any) {
|
|
216
|
+
toast.error(e.message || 'Failed to create backup');
|
|
217
|
+
} finally {
|
|
218
|
+
setCreatingBackup(false);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Handle backup restore
|
|
223
|
+
const handleRestoreBackup = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
224
|
+
const file = e.target.files?.[0];
|
|
225
|
+
if (!file) return;
|
|
226
|
+
if (!file.name.endsWith('.db')) {
|
|
227
|
+
toast.error('Only .db files are accepted');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
await api.restoreBackup(file);
|
|
232
|
+
toast.success('Database restored! Please refresh the page.');
|
|
233
|
+
} catch (err: any) {
|
|
234
|
+
toast.error(err.message || 'Restore failed');
|
|
235
|
+
}
|
|
236
|
+
e.target.value = '';
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Handle backup delete
|
|
240
|
+
const handleDeleteBackup = async (filename: string) => {
|
|
241
|
+
try {
|
|
242
|
+
await api.deleteBackup(filename);
|
|
243
|
+
toast.success('Backup deleted');
|
|
244
|
+
fetchBackups();
|
|
245
|
+
} catch (e: any) {
|
|
246
|
+
toast.error(e.message || 'Failed to delete backup');
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// ── Fetch Data ──
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
fetchUsers();
|
|
253
|
+
fetchBranches();
|
|
254
|
+
fetchRoles();
|
|
255
|
+
}, []);
|
|
256
|
+
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (activeTab === 'backup') fetchBackups();
|
|
259
|
+
}, [activeTab]);
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (activeTab === 'audit') fetchAuditLogs();
|
|
263
|
+
}, [activeTab, auditPage, auditModule, auditAction]);
|
|
264
|
+
|
|
265
|
+
const fetchUsers = async () => {
|
|
266
|
+
try {
|
|
267
|
+
setUsersLoading(true);
|
|
268
|
+
const data = await api.getUsers().catch(() => []);
|
|
269
|
+
setUsers(Array.isArray(data) ? data : []);
|
|
270
|
+
} finally {
|
|
271
|
+
setUsersLoading(false);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const fetchBranches = async () => {
|
|
276
|
+
try {
|
|
277
|
+
setBranchesLoading(true);
|
|
278
|
+
const data = await api.getBranches().catch(() => []);
|
|
279
|
+
setBranches(Array.isArray(data) ? data : []);
|
|
280
|
+
} finally {
|
|
281
|
+
setBranchesLoading(false);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const fetchRoles = async () => {
|
|
286
|
+
try {
|
|
287
|
+
const data = await api.getRoles().catch(() => null);
|
|
288
|
+
if (data && Array.isArray(data) && data.length > 0) {
|
|
289
|
+
setRoles(data);
|
|
290
|
+
}
|
|
291
|
+
} catch {}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// ── User CRUD ──
|
|
295
|
+
const openAddUser = () => {
|
|
296
|
+
setEditingUserId(null);
|
|
297
|
+
setUserForm({ full_name: '', email: '', password: '', role: 'CASHIER', phone: '' });
|
|
298
|
+
setUserSlideOpen(true);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const openEditUser = (user: any) => {
|
|
302
|
+
setEditingUserId(user.id);
|
|
303
|
+
setUserForm({ full_name: user.full_name || '', email: user.email || '', password: '', role: user.role || 'CASHIER', phone: user.phone || '' });
|
|
304
|
+
setUserSlideOpen(true);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const saveUser = async () => {
|
|
308
|
+
if (!userForm.full_name || !userForm.email) {
|
|
309
|
+
toast.error('Name and email are required');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
setIsSaving(true);
|
|
314
|
+
if (editingUserId) {
|
|
315
|
+
const payload: any = { full_name: userForm.full_name, role: userForm.role, phone: userForm.phone };
|
|
316
|
+
await api.updateUser(editingUserId, payload);
|
|
317
|
+
toast.success('User updated');
|
|
318
|
+
} else {
|
|
319
|
+
if (!userForm.password) { toast.error('Password is required for new users'); setIsSaving(false); return; }
|
|
320
|
+
await api.registerUser({ email: userForm.email, password: userForm.password, full_name: userForm.full_name, role: userForm.role, phone: userForm.phone });
|
|
321
|
+
toast.success('User created');
|
|
322
|
+
}
|
|
323
|
+
setUserSlideOpen(false);
|
|
324
|
+
fetchUsers();
|
|
325
|
+
} catch (e: any) {
|
|
326
|
+
toast.error(e.message || 'Failed to save user');
|
|
327
|
+
} finally {
|
|
328
|
+
setIsSaving(false);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const deleteUser = async () => {
|
|
333
|
+
try {
|
|
334
|
+
setIsSaving(true);
|
|
335
|
+
await api.deleteUser(deleteUserDialog.id);
|
|
336
|
+
toast.success('User deactivated');
|
|
337
|
+
setDeleteUserDialog({ open: false, id: '', name: '' });
|
|
338
|
+
fetchUsers();
|
|
339
|
+
} catch (e: any) {
|
|
340
|
+
toast.error(e.message || 'Failed to delete user');
|
|
341
|
+
} finally {
|
|
342
|
+
setIsSaving(false);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// ── Branch CRUD ──
|
|
347
|
+
const openAddBranch = () => {
|
|
348
|
+
setEditingBranchId(null);
|
|
349
|
+
setBranchForm({ name: '', name_ar: '', code: '', address: '', phone: '', email: '', currency: 'SDG', timezone: 'Africa/Khartoum' });
|
|
350
|
+
setBranchSlideOpen(true);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const openEditBranch = (branch: any) => {
|
|
354
|
+
setEditingBranchId(branch.id);
|
|
355
|
+
setBranchForm({
|
|
356
|
+
name: branch.name || '', name_ar: branch.name_ar || '', code: branch.code || '',
|
|
357
|
+
address: branch.address || '', phone: branch.phone || '', email: branch.email || '',
|
|
358
|
+
currency: branch.currency || 'SDG', timezone: branch.timezone || 'Africa/Khartoum',
|
|
359
|
+
});
|
|
360
|
+
setBranchSlideOpen(true);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const saveBranch = async () => {
|
|
364
|
+
if (!branchForm.name || !branchForm.code) {
|
|
365
|
+
toast.error('Name and code are required');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
setIsSaving(true);
|
|
370
|
+
const payload: any = { ...branchForm };
|
|
371
|
+
if (!payload.email) delete payload.email;
|
|
372
|
+
if (!payload.phone) delete payload.phone;
|
|
373
|
+
if (!payload.address) delete payload.address;
|
|
374
|
+
|
|
375
|
+
if (editingBranchId) {
|
|
376
|
+
await api.updateBranch(editingBranchId, payload);
|
|
377
|
+
toast.success('Branch updated');
|
|
378
|
+
} else {
|
|
379
|
+
await api.createBranch(payload);
|
|
380
|
+
toast.success('Branch created');
|
|
381
|
+
}
|
|
382
|
+
setBranchSlideOpen(false);
|
|
383
|
+
fetchBranches();
|
|
384
|
+
} catch (e: any) {
|
|
385
|
+
toast.error(e.message || 'Failed to save branch');
|
|
386
|
+
} finally {
|
|
387
|
+
setIsSaving(false);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
392
|
+
// RENDER
|
|
393
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
394
|
+
return (
|
|
395
|
+
<div className="flex flex-col gap-0 h-full min-h-[calc(100vh-120px)]">
|
|
396
|
+
{/* Header */}
|
|
397
|
+
<header className="flex-none rounded-t-xl border border-border bg-surface p-6 shadow-sm">
|
|
398
|
+
<div className="flex items-center gap-3">
|
|
399
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-brand-500/10">
|
|
400
|
+
<Settings className="h-5 w-5 text-brand-400" />
|
|
401
|
+
</div>
|
|
402
|
+
<div>
|
|
403
|
+
<h1 className="text-2xl font-bold tracking-tight text-foreground">Settings</h1>
|
|
404
|
+
<p className="text-sm text-muted-foreground">Manage your pharmacy system configuration</p>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
</header>
|
|
408
|
+
|
|
409
|
+
{/* Two-panel layout */}
|
|
410
|
+
<div className="flex-1 flex flex-col md:flex-row rounded-b-xl border border-t-0 border-border bg-surface overflow-hidden shadow-sm">
|
|
411
|
+
{/* Left Sidebar Nav */}
|
|
412
|
+
<nav className="w-full md:w-64 flex-none border-b md:border-b-0 md:border-r border-border bg-background/50 p-3 overflow-x-auto md:overflow-y-auto scrollbar-hide">
|
|
413
|
+
<div className="flex md:flex-col gap-1 space-y-0 md:space-y-1">
|
|
414
|
+
{TABS.map((tab) => {
|
|
415
|
+
const Icon = tab.icon;
|
|
416
|
+
const isActive = activeTab === tab.id;
|
|
417
|
+
return (
|
|
418
|
+
<button
|
|
419
|
+
key={tab.id}
|
|
420
|
+
onClick={() => setActiveTab(tab.id)}
|
|
421
|
+
className={cn(
|
|
422
|
+
"flex whitespace-nowrap flex-none w-auto md:w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-medium transition-all duration-150",
|
|
423
|
+
isActive
|
|
424
|
+
? "bg-brand-500/10 text-brand-400 shadow-sm"
|
|
425
|
+
: "text-muted-foreground hover:bg-gray-100 hover:text-foreground dark:hover:bg-gray-800/60 dark:hover:text-gray-200"
|
|
426
|
+
)}
|
|
427
|
+
>
|
|
428
|
+
<Icon className={cn("h-4 w-4 flex-none", isActive ? "text-brand-400" : "text-muted-foreground")} />
|
|
429
|
+
<span className="truncate">{tab.label}</span>
|
|
430
|
+
{isActive && <ChevronRight className="ml-auto h-3.5 w-3.5 text-brand-400" />}
|
|
431
|
+
</button>
|
|
432
|
+
);
|
|
433
|
+
})}
|
|
434
|
+
</div>
|
|
435
|
+
</nav>
|
|
436
|
+
|
|
437
|
+
{/* Right Content Panel */}
|
|
438
|
+
<main className="flex-1 overflow-y-auto p-6">
|
|
439
|
+
{activeTab === 'users' && renderUsersTab()}
|
|
440
|
+
{activeTab === 'roles' && renderRolesTab()}
|
|
441
|
+
{activeTab === 'branches' && renderBranchesTab()}
|
|
442
|
+
{activeTab === 'payment' && renderPaymentTab()}
|
|
443
|
+
{activeTab === 'backup' && renderBackupTab()}
|
|
444
|
+
{activeTab === 'audit' && renderAuditTab()}
|
|
445
|
+
{activeTab === 'system' && renderSystemTab()}
|
|
446
|
+
{activeTab === 'about' && renderAboutTab()}
|
|
447
|
+
</main>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
{/* Slide-overs & Dialogs */}
|
|
451
|
+
{renderUserSlideOver()}
|
|
452
|
+
{renderBranchSlideOver()}
|
|
453
|
+
<ConfirmDialog
|
|
454
|
+
open={deleteUserDialog.open}
|
|
455
|
+
onCancel={() => setDeleteUserDialog({ open: false, id: '', name: '' })}
|
|
456
|
+
onConfirm={deleteUser}
|
|
457
|
+
title="Deactivate User"
|
|
458
|
+
message={`Are you sure you want to deactivate ${deleteUserDialog.name}? They will no longer be able to log in.`}
|
|
459
|
+
isLoading={isSaving}
|
|
460
|
+
/>
|
|
461
|
+
<ConfirmDialog
|
|
462
|
+
open={wipeDataOpen}
|
|
463
|
+
onCancel={() => setWipeDataOpen(false)}
|
|
464
|
+
onConfirm={handleWipeData}
|
|
465
|
+
title="Wipe Business Data"
|
|
466
|
+
message="Are you absolutely sure? This will permanently delete all catalog items, inventory batches, and sales records. This action cannot be undone."
|
|
467
|
+
isLoading={isWiping}
|
|
468
|
+
/>
|
|
469
|
+
<ConfirmDialog
|
|
470
|
+
open={factoryResetOpen}
|
|
471
|
+
onCancel={() => setFactoryResetOpen(false)}
|
|
472
|
+
onConfirm={handleFactoryReset}
|
|
473
|
+
title="Factory Reset"
|
|
474
|
+
message="DANGER: This will erase the ENTIRE database, including all users, roles, and settings. The system will be restored to factory defaults. This action cannot be undone."
|
|
475
|
+
isLoading={isResetting}
|
|
476
|
+
/>
|
|
477
|
+
</div>
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
481
|
+
// TAB: USERS
|
|
482
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
483
|
+
function renderUsersTab() {
|
|
484
|
+
const filteredUsers = users.filter(u =>
|
|
485
|
+
searchQuery === '' ||
|
|
486
|
+
u.full_name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
487
|
+
u.email?.toLowerCase().includes(searchQuery.toLowerCase())
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
return (
|
|
491
|
+
<div className="space-y-6">
|
|
492
|
+
<div className="flex items-center justify-between">
|
|
493
|
+
<div>
|
|
494
|
+
<h2 className="text-lg font-semibold text-foreground">User Management</h2>
|
|
495
|
+
<p className="text-sm text-muted-foreground">{users.length} users registered</p>
|
|
496
|
+
</div>
|
|
497
|
+
<button onClick={openAddUser} className="inline-flex h-9 items-center gap-2 rounded-lg bg-brand-500 px-4 text-sm font-medium text-white transition-colors hover:bg-brand-600 shadow-lg shadow-brand-500/20">
|
|
498
|
+
<Plus className="h-4 w-4" /> Add User
|
|
499
|
+
</button>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
{/* Search */}
|
|
503
|
+
<div className="relative max-w-sm">
|
|
504
|
+
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
505
|
+
<input
|
|
506
|
+
type="text" placeholder="Search users..."
|
|
507
|
+
value={searchQuery} onChange={e => setSearchQuery(e.target.value)}
|
|
508
|
+
className="h-9 w-full rounded-lg border border-border bg-background pl-10 pr-4 text-sm text-foreground placeholder-gray-500 focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
|
|
509
|
+
/>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
{/* Users Table */}
|
|
513
|
+
<div className="rounded-xl border border-border overflow-hidden">
|
|
514
|
+
<table className="w-full text-left text-sm">
|
|
515
|
+
<thead className="bg-background text-xs uppercase text-muted-foreground border-b border-border">
|
|
516
|
+
<tr>
|
|
517
|
+
<th className="px-5 py-3 font-semibold">User</th>
|
|
518
|
+
<th className="px-5 py-3 font-semibold">Role</th>
|
|
519
|
+
<th className="px-5 py-3 font-semibold">Status</th>
|
|
520
|
+
<th className="px-5 py-3 font-semibold">Last Login</th>
|
|
521
|
+
<th className="px-5 py-3 font-semibold text-right">Actions</th>
|
|
522
|
+
</tr>
|
|
523
|
+
</thead>
|
|
524
|
+
<tbody className="divide-y divide-gray-800/50">
|
|
525
|
+
{usersLoading ? (
|
|
526
|
+
[...Array(3)].map((_, i) => (
|
|
527
|
+
<tr key={i} className="animate-pulse">
|
|
528
|
+
<td className="px-5 py-4"><div className="h-4 bg-gray-800 rounded w-3/4" /></td>
|
|
529
|
+
<td className="px-5 py-4"><div className="h-4 bg-gray-800 rounded w-1/2" /></td>
|
|
530
|
+
<td className="px-5 py-4"><div className="h-4 bg-gray-800 rounded w-1/3" /></td>
|
|
531
|
+
<td className="px-5 py-4"><div className="h-4 bg-gray-800 rounded w-1/2" /></td>
|
|
532
|
+
<td className="px-5 py-4"><div className="h-4 bg-gray-800 rounded w-8 ml-auto" /></td>
|
|
533
|
+
</tr>
|
|
534
|
+
))
|
|
535
|
+
) : filteredUsers.length === 0 ? (
|
|
536
|
+
<tr>
|
|
537
|
+
<td colSpan={5} className="px-5 py-12 text-center">
|
|
538
|
+
<Users className="h-10 w-10 mx-auto text-gray-600 mb-3" />
|
|
539
|
+
<p className="text-sm text-muted-foreground">No users found</p>
|
|
540
|
+
</td>
|
|
541
|
+
</tr>
|
|
542
|
+
) : (
|
|
543
|
+
filteredUsers.map(user => (
|
|
544
|
+
<tr key={user.id} className="group transition-colors hover:bg-gray-800/30">
|
|
545
|
+
<td className="px-5 py-4">
|
|
546
|
+
<div className="flex items-center gap-3">
|
|
547
|
+
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-brand-500/10 text-brand-400 font-semibold text-sm">
|
|
548
|
+
{user.full_name?.charAt(0)?.toUpperCase() || '?'}
|
|
549
|
+
</div>
|
|
550
|
+
<div>
|
|
551
|
+
<p className="font-medium text-foreground">{user.full_name}</p>
|
|
552
|
+
<p className="text-xs text-muted-foreground">{user.email}</p>
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
</td>
|
|
556
|
+
<td className="px-5 py-4">
|
|
557
|
+
<Badge variant={user.role === 'admin' || user.role === 'SUPER_ADMIN' ? 'info' : 'default'}>
|
|
558
|
+
{user.role}
|
|
559
|
+
</Badge>
|
|
560
|
+
</td>
|
|
561
|
+
<td className="px-5 py-4">
|
|
562
|
+
<Badge variant={user.is_active ? 'success' : 'danger'}>
|
|
563
|
+
{user.is_active ? 'Active' : 'Inactive'}
|
|
564
|
+
</Badge>
|
|
565
|
+
</td>
|
|
566
|
+
<td className="px-5 py-4 text-sm text-muted-foreground">
|
|
567
|
+
{user.last_login ? new Date(user.last_login).toLocaleDateString() : 'Never'}
|
|
568
|
+
</td>
|
|
569
|
+
<td className="px-5 py-4 text-right">
|
|
570
|
+
<div className="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
571
|
+
<button onClick={() => openEditUser(user)} className="p-1.5 text-muted-foreground hover:text-brand-400 hover:bg-brand-500/10 rounded-md transition-colors" title="Edit">
|
|
572
|
+
<Edit className="h-4 w-4" />
|
|
573
|
+
</button>
|
|
574
|
+
<button onClick={() => setDeleteUserDialog({ open: true, id: user.id, name: user.full_name })} className="p-1.5 text-muted-foreground hover:text-red-400 hover:bg-red-500/10 rounded-md transition-colors" title="Deactivate">
|
|
575
|
+
<UserX className="h-4 w-4" />
|
|
576
|
+
</button>
|
|
577
|
+
</div>
|
|
578
|
+
</td>
|
|
579
|
+
</tr>
|
|
580
|
+
))
|
|
581
|
+
)}
|
|
582
|
+
</tbody>
|
|
583
|
+
</table>
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
590
|
+
// TAB: ROLES & PERMISSIONS
|
|
591
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
592
|
+
function renderRolesTab() {
|
|
593
|
+
return (
|
|
594
|
+
<div className="space-y-6">
|
|
595
|
+
<div>
|
|
596
|
+
<h2 className="text-lg font-semibold text-foreground">Roles & Permissions</h2>
|
|
597
|
+
<p className="text-sm text-muted-foreground">Define access levels for each user role</p>
|
|
598
|
+
</div>
|
|
599
|
+
|
|
600
|
+
<div className="grid gap-4">
|
|
601
|
+
{roles.map((role: any) => {
|
|
602
|
+
const isExpanded = expandedRole === role.name;
|
|
603
|
+
const colorClass = ROLE_COLORS[role.color || 'blue'] || ROLE_COLORS.blue;
|
|
604
|
+
const userCount = users.filter(u => u.role === role.name || (role.name === 'SUPER_ADMIN' && u.role === 'admin')).length;
|
|
605
|
+
|
|
606
|
+
return (
|
|
607
|
+
<div key={role.name} className="rounded-xl border border-border bg-background/50 overflow-hidden transition-all duration-200">
|
|
608
|
+
{/* Role Header */}
|
|
609
|
+
<button
|
|
610
|
+
onClick={() => setExpandedRole(isExpanded ? null : role.name)}
|
|
611
|
+
className="flex w-full items-center gap-4 p-4 text-left hover:bg-gray-800/30 transition-colors"
|
|
612
|
+
>
|
|
613
|
+
<div className={cn("flex h-10 w-10 items-center justify-center rounded-lg border", colorClass)}>
|
|
614
|
+
<Shield className="h-5 w-5" />
|
|
615
|
+
</div>
|
|
616
|
+
<div className="flex-1 min-w-0">
|
|
617
|
+
<div className="flex items-center gap-2">
|
|
618
|
+
<h3 className="font-semibold text-foreground">{role.display_name}</h3>
|
|
619
|
+
{role.is_system && (
|
|
620
|
+
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground bg-gray-800 px-1.5 py-0.5 rounded">System</span>
|
|
621
|
+
)}
|
|
622
|
+
</div>
|
|
623
|
+
<p className="text-xs text-muted-foreground mt-0.5 truncate">{role.description}</p>
|
|
624
|
+
</div>
|
|
625
|
+
<div className="flex items-center gap-3 flex-none">
|
|
626
|
+
<div className="text-center">
|
|
627
|
+
<p className="text-lg font-bold text-foreground">{userCount}</p>
|
|
628
|
+
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">Users</p>
|
|
629
|
+
</div>
|
|
630
|
+
<ChevronRight className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", isExpanded && "rotate-90")} />
|
|
631
|
+
</div>
|
|
632
|
+
</button>
|
|
633
|
+
|
|
634
|
+
{/* Expanded Permission Matrix */}
|
|
635
|
+
{isExpanded && (
|
|
636
|
+
<div className="border-t border-border p-4 bg-background/30 animate-in fade-in slide-in-from-top-2 duration-200">
|
|
637
|
+
<div className="grid gap-3">
|
|
638
|
+
{PERMISSION_MODULES.map(mod => (
|
|
639
|
+
<div key={mod.module} className="flex items-start gap-4 p-3 rounded-lg bg-gray-800/20">
|
|
640
|
+
<div className="w-28 flex-none">
|
|
641
|
+
<p className="text-sm font-medium text-foreground">{mod.label}</p>
|
|
642
|
+
</div>
|
|
643
|
+
<div className="flex flex-wrap gap-2">
|
|
644
|
+
{mod.actions.map(action => {
|
|
645
|
+
const permCode = `${mod.module}.${action}`;
|
|
646
|
+
const hasPermission = role.name === 'SUPER_ADMIN' ||
|
|
647
|
+
(role.permissions && role.permissions.some((p: any) => p.code === permCode));
|
|
648
|
+
return (
|
|
649
|
+
<span
|
|
650
|
+
key={action}
|
|
651
|
+
className={cn(
|
|
652
|
+
"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium border transition-colors",
|
|
653
|
+
hasPermission
|
|
654
|
+
? "bg-green-500/10 text-green-400 border-green-500/20"
|
|
655
|
+
: "bg-gray-800/50 text-gray-500 border-gray-700/50"
|
|
656
|
+
)}
|
|
657
|
+
>
|
|
658
|
+
{hasPermission ? <CheckCircle2 className="h-3 w-3" /> : <XCircle className="h-3 w-3" />}
|
|
659
|
+
{action.replace('_', ' ')}
|
|
660
|
+
</span>
|
|
661
|
+
);
|
|
662
|
+
})}
|
|
663
|
+
</div>
|
|
664
|
+
</div>
|
|
665
|
+
))}
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
)}
|
|
669
|
+
</div>
|
|
670
|
+
);
|
|
671
|
+
})}
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
678
|
+
// TAB: BRANCHES
|
|
679
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
680
|
+
function renderBranchesTab() {
|
|
681
|
+
return (
|
|
682
|
+
<div className="space-y-6">
|
|
683
|
+
<div className="flex items-center justify-between">
|
|
684
|
+
<div>
|
|
685
|
+
<h2 className="text-lg font-semibold text-foreground">Branch Management</h2>
|
|
686
|
+
<p className="text-sm text-muted-foreground">{branches.length} branches configured</p>
|
|
687
|
+
</div>
|
|
688
|
+
<button onClick={openAddBranch} className="inline-flex h-9 items-center gap-2 rounded-lg bg-brand-500 px-4 text-sm font-medium text-white transition-colors hover:bg-brand-600 shadow-lg shadow-brand-500/20">
|
|
689
|
+
<Plus className="h-4 w-4" /> Add Branch
|
|
690
|
+
</button>
|
|
691
|
+
</div>
|
|
692
|
+
|
|
693
|
+
<div className="grid gap-4">
|
|
694
|
+
{branchesLoading ? (
|
|
695
|
+
[...Array(2)].map((_, i) => (
|
|
696
|
+
<div key={i} className="rounded-xl border border-border bg-background/50 p-5 animate-pulse">
|
|
697
|
+
<div className="h-5 bg-gray-800 rounded w-1/3 mb-3" />
|
|
698
|
+
<div className="h-4 bg-gray-800 rounded w-1/2" />
|
|
699
|
+
</div>
|
|
700
|
+
))
|
|
701
|
+
) : branches.length === 0 ? (
|
|
702
|
+
<div className="rounded-xl border border-border bg-background/50 p-12 text-center">
|
|
703
|
+
<Building2 className="h-10 w-10 mx-auto text-gray-600 mb-3" />
|
|
704
|
+
<p className="text-sm text-muted-foreground">No branches configured</p>
|
|
705
|
+
<button onClick={openAddBranch} className="mt-4 text-brand-400 hover:text-brand-300 font-medium text-sm">
|
|
706
|
+
<Plus className="h-4 w-4 inline mr-1" /> Add your first branch
|
|
707
|
+
</button>
|
|
708
|
+
</div>
|
|
709
|
+
) : (
|
|
710
|
+
branches.map(branch => (
|
|
711
|
+
<div key={branch.id} className="group rounded-xl border border-border bg-background/50 p-5 hover:border-brand-500/30 transition-colors">
|
|
712
|
+
<div className="flex items-start justify-between">
|
|
713
|
+
<div className="flex items-center gap-4">
|
|
714
|
+
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-blue-500/10 text-blue-400">
|
|
715
|
+
<Building2 className="h-5 w-5" />
|
|
716
|
+
</div>
|
|
717
|
+
<div>
|
|
718
|
+
<h3 className="font-semibold text-foreground">{branch.name}</h3>
|
|
719
|
+
<div className="flex items-center gap-4 mt-1 text-xs text-muted-foreground">
|
|
720
|
+
<span className="flex items-center gap-1"><Key className="h-3 w-3" /> {branch.code}</span>
|
|
721
|
+
{branch.phone && <span className="flex items-center gap-1"><Phone className="h-3 w-3" /> {branch.phone}</span>}
|
|
722
|
+
{branch.email && <span className="flex items-center gap-1"><Mail className="h-3 w-3" /> {branch.email}</span>}
|
|
723
|
+
<span className="flex items-center gap-1"><Globe className="h-3 w-3" /> {branch.currency || 'SDG'}</span>
|
|
724
|
+
</div>
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
728
|
+
<Badge variant={branch.is_active !== false ? 'success' : 'danger'}>
|
|
729
|
+
{branch.is_active !== false ? 'Active' : 'Inactive'}
|
|
730
|
+
</Badge>
|
|
731
|
+
<button onClick={() => openEditBranch(branch)} className="p-1.5 text-muted-foreground hover:text-brand-400 hover:bg-brand-500/10 rounded-md transition-colors">
|
|
732
|
+
<Edit className="h-4 w-4" />
|
|
733
|
+
</button>
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
</div>
|
|
737
|
+
))
|
|
738
|
+
)}
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
745
|
+
// TAB: PAYMENT METHODS
|
|
746
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
747
|
+
function renderPaymentTab() {
|
|
748
|
+
return (
|
|
749
|
+
<div className="space-y-6">
|
|
750
|
+
<div>
|
|
751
|
+
<h2 className="text-lg font-semibold text-foreground">Payment Methods</h2>
|
|
752
|
+
<p className="text-sm text-muted-foreground">Configure which payment methods are accepted at POS</p>
|
|
753
|
+
</div>
|
|
754
|
+
<div className="grid gap-4">
|
|
755
|
+
{paymentMethods.map(method => (
|
|
756
|
+
<div key={method.id} className={cn(
|
|
757
|
+
"rounded-xl border p-5 transition-all duration-200",
|
|
758
|
+
method.enabled
|
|
759
|
+
? "border-brand-500/30 bg-brand-500/5"
|
|
760
|
+
: "border-border bg-background/50 opacity-60"
|
|
761
|
+
)}>
|
|
762
|
+
<div className="flex items-center justify-between">
|
|
763
|
+
<div className="flex items-center gap-4">
|
|
764
|
+
<span className="text-2xl">{method.icon}</span>
|
|
765
|
+
<div>
|
|
766
|
+
<h3 className="font-semibold text-foreground">{method.name}</h3>
|
|
767
|
+
<p className="text-xs text-muted-foreground">{method.name_ar}</p>
|
|
768
|
+
</div>
|
|
769
|
+
</div>
|
|
770
|
+
<Switch
|
|
771
|
+
checked={method.enabled}
|
|
772
|
+
onCheckedChange={checked => {
|
|
773
|
+
setPaymentMethods(prev => prev.map(m => m.id === method.id ? { ...m, enabled: checked } : m));
|
|
774
|
+
toast.success(`${method.name} ${checked ? 'enabled' : 'disabled'}`);
|
|
775
|
+
}}
|
|
776
|
+
/>
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
))}
|
|
780
|
+
</div>
|
|
781
|
+
<p className="text-xs text-muted-foreground flex items-center gap-1.5">
|
|
782
|
+
<AlertTriangle className="h-3.5 w-3.5 text-amber-400" />
|
|
783
|
+
Payment method changes will take effect on the next POS session.
|
|
784
|
+
</p>
|
|
785
|
+
</div>
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
790
|
+
// TAB: BACKUP & RESTORE
|
|
791
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
792
|
+
function renderBackupTab() {
|
|
793
|
+
return (
|
|
794
|
+
<div className="space-y-6">
|
|
795
|
+
<div>
|
|
796
|
+
<h2 className="text-lg font-semibold text-foreground">Backup & Restore</h2>
|
|
797
|
+
<p className="text-sm text-muted-foreground">Manage database backups and restore points</p>
|
|
798
|
+
</div>
|
|
799
|
+
|
|
800
|
+
{/* Action Cards */}
|
|
801
|
+
<div className="grid grid-cols-2 gap-4">
|
|
802
|
+
<button
|
|
803
|
+
onClick={handleCreateBackup}
|
|
804
|
+
disabled={creatingBackup}
|
|
805
|
+
className="flex items-center gap-4 rounded-xl border border-border bg-background/50 p-5 hover:border-brand-500/30 hover:bg-brand-500/5 transition-all group disabled:opacity-50"
|
|
806
|
+
>
|
|
807
|
+
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-brand-500/10 text-brand-400 group-hover:bg-brand-500/20 transition-colors">
|
|
808
|
+
{creatingBackup ? <RefreshCw className="h-5 w-5 animate-spin" /> : <Download className="h-5 w-5" />}
|
|
809
|
+
</div>
|
|
810
|
+
<div className="text-left">
|
|
811
|
+
<h3 className="font-semibold text-foreground">{creatingBackup ? 'Creating...' : 'Create Backup'}</h3>
|
|
812
|
+
<p className="text-xs text-muted-foreground">Export database snapshot now</p>
|
|
813
|
+
</div>
|
|
814
|
+
</button>
|
|
815
|
+
<button
|
|
816
|
+
onClick={() => restoreInputRef.current?.click()}
|
|
817
|
+
className="flex items-center gap-4 rounded-xl border border-border bg-background/50 p-5 hover:border-amber-500/30 hover:bg-amber-500/5 transition-all group"
|
|
818
|
+
>
|
|
819
|
+
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-amber-500/10 text-amber-400 group-hover:bg-amber-500/20 transition-colors">
|
|
820
|
+
<Upload className="h-5 w-5" />
|
|
821
|
+
</div>
|
|
822
|
+
<div className="text-left">
|
|
823
|
+
<h3 className="font-semibold text-foreground">Restore Backup</h3>
|
|
824
|
+
<p className="text-xs text-muted-foreground">Restore from a backup file</p>
|
|
825
|
+
</div>
|
|
826
|
+
</button>
|
|
827
|
+
<input
|
|
828
|
+
ref={restoreInputRef}
|
|
829
|
+
type="file"
|
|
830
|
+
accept=".db"
|
|
831
|
+
onChange={handleRestoreBackup}
|
|
832
|
+
className="hidden"
|
|
833
|
+
/>
|
|
834
|
+
</div>
|
|
835
|
+
|
|
836
|
+
{/* Backup History */}
|
|
837
|
+
<div>
|
|
838
|
+
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">Backup History</h3>
|
|
839
|
+
{backupsLoading ? (
|
|
840
|
+
<div className="text-center py-8 text-sm text-muted-foreground">Loading backups...</div>
|
|
841
|
+
) : backups.length === 0 ? (
|
|
842
|
+
<div className="flex flex-col items-center justify-center py-12 text-center rounded-xl border border-border">
|
|
843
|
+
<HardDrive className="h-10 w-10 text-foreground/20 mb-3" />
|
|
844
|
+
<h3 className="text-sm font-semibold text-foreground">No backups yet</h3>
|
|
845
|
+
<p className="text-xs text-foreground/50 mt-1">Create your first backup using the button above</p>
|
|
846
|
+
</div>
|
|
847
|
+
) : (
|
|
848
|
+
<div className="rounded-xl border border-border overflow-hidden">
|
|
849
|
+
<table className="w-full text-sm">
|
|
850
|
+
<thead className="bg-background text-xs uppercase text-muted-foreground border-b border-border">
|
|
851
|
+
<tr>
|
|
852
|
+
<th className="px-4 py-3 text-left font-semibold">Filename</th>
|
|
853
|
+
<th className="px-4 py-3 text-left font-semibold">Date</th>
|
|
854
|
+
<th className="px-4 py-3 text-left font-semibold">Size</th>
|
|
855
|
+
<th className="px-4 py-3 text-right font-semibold">Actions</th>
|
|
856
|
+
</tr>
|
|
857
|
+
</thead>
|
|
858
|
+
<tbody className="divide-y divide-border">
|
|
859
|
+
{backups.map((b: any) => (
|
|
860
|
+
<tr key={b.filename} className="hover:bg-surface/50 transition-colors">
|
|
861
|
+
<td className="px-4 py-3 font-medium text-foreground">
|
|
862
|
+
<div className="flex items-center gap-2">
|
|
863
|
+
<Database className="h-4 w-4 text-brand-400" />
|
|
864
|
+
{b.filename}
|
|
865
|
+
</div>
|
|
866
|
+
</td>
|
|
867
|
+
<td className="px-4 py-3 text-muted-foreground">
|
|
868
|
+
{new Date(b.created_at).toLocaleString()}
|
|
869
|
+
</td>
|
|
870
|
+
<td className="px-4 py-3 text-muted-foreground">{b.size_display}</td>
|
|
871
|
+
<td className="px-4 py-3 text-right">
|
|
872
|
+
<div className="flex items-center justify-end gap-2">
|
|
873
|
+
<button
|
|
874
|
+
onClick={() => api.downloadBackup(b.filename)}
|
|
875
|
+
className="inline-flex items-center gap-1.5 rounded-lg bg-brand-500/10 text-brand-400 px-2.5 py-1.5 text-xs font-medium hover:bg-brand-500/20 transition-colors"
|
|
876
|
+
>
|
|
877
|
+
<Download className="h-3.5 w-3.5" /> Download
|
|
878
|
+
</button>
|
|
879
|
+
<button
|
|
880
|
+
onClick={() => handleDeleteBackup(b.filename)}
|
|
881
|
+
className="inline-flex items-center gap-1.5 rounded-lg bg-red-500/10 text-red-400 px-2.5 py-1.5 text-xs font-medium hover:bg-red-500/20 transition-colors"
|
|
882
|
+
>
|
|
883
|
+
<Trash2 className="h-3.5 w-3.5" /> Delete
|
|
884
|
+
</button>
|
|
885
|
+
</div>
|
|
886
|
+
</td>
|
|
887
|
+
</tr>
|
|
888
|
+
))}
|
|
889
|
+
</tbody>
|
|
890
|
+
</table>
|
|
891
|
+
</div>
|
|
892
|
+
)}
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
899
|
+
// TAB: AUDIT LOGS
|
|
900
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
901
|
+
function renderAuditTab() {
|
|
902
|
+
const ACTION_COLORS: Record<string, string> = {
|
|
903
|
+
CREATE: 'bg-green-500/10 text-green-400',
|
|
904
|
+
UPDATE: 'bg-blue-500/10 text-blue-400',
|
|
905
|
+
DELETE: 'bg-red-500/10 text-red-400',
|
|
906
|
+
LOGIN: 'bg-purple-500/10 text-purple-400',
|
|
907
|
+
LOGOUT: 'bg-gray-500/10 text-gray-400',
|
|
908
|
+
IMPORT: 'bg-amber-500/10 text-amber-400',
|
|
909
|
+
WIPE: 'bg-red-500/10 text-red-400',
|
|
910
|
+
BACKUP: 'bg-teal-500/10 text-teal-400',
|
|
911
|
+
RESTORE: 'bg-orange-500/10 text-orange-400',
|
|
912
|
+
RESET: 'bg-red-500/10 text-red-400',
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
return (
|
|
916
|
+
<div className="space-y-6">
|
|
917
|
+
<div>
|
|
918
|
+
<h2 className="text-lg font-semibold text-foreground">Audit Logs</h2>
|
|
919
|
+
<p className="text-sm text-muted-foreground">Track all system activities and changes</p>
|
|
920
|
+
</div>
|
|
921
|
+
|
|
922
|
+
{/* Filters */}
|
|
923
|
+
<div className="flex items-center gap-3">
|
|
924
|
+
<div className="relative flex-1 max-w-xs">
|
|
925
|
+
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
926
|
+
<input
|
|
927
|
+
type="text"
|
|
928
|
+
placeholder="Search logs..."
|
|
929
|
+
value={auditSearch}
|
|
930
|
+
onChange={(e) => setAuditSearch(e.target.value)}
|
|
931
|
+
onKeyDown={(e) => { if (e.key === 'Enter') { setAuditPage(1); fetchAuditLogs(); } }}
|
|
932
|
+
className="h-9 w-full rounded-lg border border-border bg-background pl-10 pr-4 text-sm text-foreground placeholder-gray-500 focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
|
|
933
|
+
/>
|
|
934
|
+
</div>
|
|
935
|
+
<Select
|
|
936
|
+
value={auditModule}
|
|
937
|
+
onChange={(v) => { setAuditModule(v); setAuditPage(1); }}
|
|
938
|
+
options={[
|
|
939
|
+
{ value: 'all', label: 'All Modules' },
|
|
940
|
+
{ value: 'Auth', label: 'Auth' },
|
|
941
|
+
{ value: 'Sales', label: 'Sales' },
|
|
942
|
+
{ value: 'Inventory', label: 'Inventory' },
|
|
943
|
+
{ value: 'Medicine', label: 'Medicine' },
|
|
944
|
+
{ value: 'Customer', label: 'Customer' },
|
|
945
|
+
{ value: 'Admin', label: 'Admin' },
|
|
946
|
+
]}
|
|
947
|
+
className="w-[160px]"
|
|
948
|
+
/>
|
|
949
|
+
<Select
|
|
950
|
+
value={auditAction}
|
|
951
|
+
onChange={(v) => { setAuditAction(v); setAuditPage(1); }}
|
|
952
|
+
options={[
|
|
953
|
+
{ value: 'all', label: 'All Actions' },
|
|
954
|
+
{ value: 'CREATE', label: 'Create' },
|
|
955
|
+
{ value: 'UPDATE', label: 'Update' },
|
|
956
|
+
{ value: 'DELETE', label: 'Delete' },
|
|
957
|
+
{ value: 'LOGIN', label: 'Login' },
|
|
958
|
+
{ value: 'IMPORT', label: 'Import' },
|
|
959
|
+
{ value: 'BACKUP', label: 'Backup' },
|
|
960
|
+
{ value: 'WIPE', label: 'Wipe' },
|
|
961
|
+
]}
|
|
962
|
+
className="w-[160px]"
|
|
963
|
+
/>
|
|
964
|
+
<button
|
|
965
|
+
onClick={() => { setAuditPage(1); fetchAuditLogs(); }}
|
|
966
|
+
className="h-9 px-3 rounded-lg border border-border text-sm font-medium text-foreground hover:bg-surface transition-colors"
|
|
967
|
+
>
|
|
968
|
+
<RefreshCw className="h-4 w-4" />
|
|
969
|
+
</button>
|
|
970
|
+
</div>
|
|
971
|
+
|
|
972
|
+
{/* Logs Table */}
|
|
973
|
+
{auditLoading ? (
|
|
974
|
+
<div className="text-center py-12 text-sm text-muted-foreground">Loading audit logs...</div>
|
|
975
|
+
) : auditLogs.length === 0 ? (
|
|
976
|
+
<div className="flex flex-col items-center justify-center py-12 text-center rounded-xl border border-border">
|
|
977
|
+
<ScrollText className="h-10 w-10 text-foreground/20 mb-3" />
|
|
978
|
+
<h3 className="text-sm font-semibold text-foreground">No audit logs found</h3>
|
|
979
|
+
<p className="text-xs text-foreground/50 mt-1">Activity will be logged as users interact with the system</p>
|
|
980
|
+
</div>
|
|
981
|
+
) : (
|
|
982
|
+
<>
|
|
983
|
+
<div className="rounded-xl border border-border overflow-hidden">
|
|
984
|
+
<table className="w-full text-sm">
|
|
985
|
+
<thead className="bg-background text-xs uppercase text-muted-foreground border-b border-border">
|
|
986
|
+
<tr>
|
|
987
|
+
<th className="px-4 py-3 text-left font-semibold">Timestamp</th>
|
|
988
|
+
<th className="px-4 py-3 text-left font-semibold">User</th>
|
|
989
|
+
<th className="px-4 py-3 text-left font-semibold">Action</th>
|
|
990
|
+
<th className="px-4 py-3 text-left font-semibold">Module</th>
|
|
991
|
+
<th className="px-4 py-3 text-left font-semibold">Details</th>
|
|
992
|
+
<th className="px-4 py-3 text-left font-semibold">IP</th>
|
|
993
|
+
</tr>
|
|
994
|
+
</thead>
|
|
995
|
+
<tbody className="divide-y divide-border">
|
|
996
|
+
{auditLogs.map((log: any) => (
|
|
997
|
+
<tr key={log.id} className="hover:bg-surface/50 transition-colors">
|
|
998
|
+
<td className="px-4 py-3 text-muted-foreground whitespace-nowrap">
|
|
999
|
+
{new Date(log.created_at).toLocaleString()}
|
|
1000
|
+
</td>
|
|
1001
|
+
<td className="px-4 py-3 font-medium text-foreground">{log.user_email || 'System'}</td>
|
|
1002
|
+
<td className="px-4 py-3">
|
|
1003
|
+
<span className={`inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${ACTION_COLORS[log.action] || 'bg-gray-500/10 text-gray-400'}`}>
|
|
1004
|
+
{log.action}
|
|
1005
|
+
</span>
|
|
1006
|
+
</td>
|
|
1007
|
+
<td className="px-4 py-3 text-muted-foreground">{log.module}</td>
|
|
1008
|
+
<td className="px-4 py-3 text-foreground max-w-xs truncate" title={log.details}>{log.details}</td>
|
|
1009
|
+
<td className="px-4 py-3 text-muted-foreground text-xs font-mono">{log.ip_address || '-'}</td>
|
|
1010
|
+
</tr>
|
|
1011
|
+
))}
|
|
1012
|
+
</tbody>
|
|
1013
|
+
</table>
|
|
1014
|
+
</div>
|
|
1015
|
+
|
|
1016
|
+
{/* Pagination */}
|
|
1017
|
+
<div className="flex items-center justify-between">
|
|
1018
|
+
<p className="text-xs text-muted-foreground">Showing page {auditPage} of {auditPages} ({auditTotal} total logs)</p>
|
|
1019
|
+
<div className="flex items-center gap-2">
|
|
1020
|
+
<button
|
|
1021
|
+
onClick={() => setAuditPage(p => Math.max(1, p - 1))}
|
|
1022
|
+
disabled={auditPage <= 1}
|
|
1023
|
+
className="h-8 px-3 rounded-lg border border-border text-xs font-medium text-foreground hover:bg-surface transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
1024
|
+
>
|
|
1025
|
+
Previous
|
|
1026
|
+
</button>
|
|
1027
|
+
<button
|
|
1028
|
+
onClick={() => setAuditPage(p => Math.min(auditPages, p + 1))}
|
|
1029
|
+
disabled={auditPage >= auditPages}
|
|
1030
|
+
className="h-8 px-3 rounded-lg border border-border text-xs font-medium text-foreground hover:bg-surface transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
1031
|
+
>
|
|
1032
|
+
Next
|
|
1033
|
+
</button>
|
|
1034
|
+
</div>
|
|
1035
|
+
</div>
|
|
1036
|
+
</>
|
|
1037
|
+
)}
|
|
1038
|
+
</div>
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1043
|
+
// TAB: SYSTEM SETTINGS
|
|
1044
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1045
|
+
function renderSystemTab() {
|
|
1046
|
+
const InputField = ({ label, value, field, type = 'text' }: { label: string; value: string | number; field: string; type?: string }) => (
|
|
1047
|
+
<div className="space-y-1.5">
|
|
1048
|
+
<label className="text-sm font-medium text-muted-foreground">{label}</label>
|
|
1049
|
+
<input
|
|
1050
|
+
type={type} value={value}
|
|
1051
|
+
onChange={e => setSystemSettings(prev => ({ ...prev, [field]: type === 'number' ? Number(e.target.value) : e.target.value }))}
|
|
1052
|
+
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
|
|
1053
|
+
/>
|
|
1054
|
+
</div>
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
return (
|
|
1058
|
+
<div className="space-y-8">
|
|
1059
|
+
<div className="flex items-center justify-between">
|
|
1060
|
+
<div>
|
|
1061
|
+
<h2 className="text-lg font-semibold text-foreground">System Settings</h2>
|
|
1062
|
+
<p className="text-sm text-muted-foreground">Configure your pharmacy system preferences</p>
|
|
1063
|
+
</div>
|
|
1064
|
+
<button onClick={() => {
|
|
1065
|
+
setCurrency(systemSettings.currency);
|
|
1066
|
+
toast.success('Settings saved');
|
|
1067
|
+
}} className="inline-flex h-9 items-center gap-2 rounded-lg bg-brand-500 px-4 text-sm font-medium text-white transition-colors hover:bg-brand-600">
|
|
1068
|
+
Save Changes
|
|
1069
|
+
</button>
|
|
1070
|
+
</div>
|
|
1071
|
+
|
|
1072
|
+
{/* Pharmacy Info */}
|
|
1073
|
+
<section className="space-y-4">
|
|
1074
|
+
<div className="flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1075
|
+
<Building2 className="h-4 w-4" /> Pharmacy Information
|
|
1076
|
+
</div>
|
|
1077
|
+
<div className="rounded-xl border border-border bg-background/50 p-5 space-y-4">
|
|
1078
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1079
|
+
<InputField label="Pharmacy Name" value={systemSettings.pharmacyName} field="pharmacyName" />
|
|
1080
|
+
<InputField label="License Number" value={systemSettings.licenseNumber} field="licenseNumber" />
|
|
1081
|
+
</div>
|
|
1082
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1083
|
+
<InputField label="Phone" value={systemSettings.phone} field="phone" />
|
|
1084
|
+
<InputField label="Address" value={systemSettings.address} field="address" />
|
|
1085
|
+
</div>
|
|
1086
|
+
</div>
|
|
1087
|
+
</section>
|
|
1088
|
+
|
|
1089
|
+
{/* Regional */}
|
|
1090
|
+
<section className="space-y-4">
|
|
1091
|
+
<div className="flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1092
|
+
<Globe className="h-4 w-4" /> Regional Settings
|
|
1093
|
+
</div>
|
|
1094
|
+
<div className="rounded-xl border border-border bg-background/50 p-5 space-y-4">
|
|
1095
|
+
<div className="grid grid-cols-3 gap-4">
|
|
1096
|
+
<div className="space-y-1.5">
|
|
1097
|
+
<label className="text-sm font-medium text-muted-foreground">Currency</label>
|
|
1098
|
+
<input list="system-currency-options" type="text" value={systemSettings.currency} onChange={e => setSystemSettings(prev => ({ ...prev, currency: e.target.value }))} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" placeholder="e.g. SDG" />
|
|
1099
|
+
<datalist id="system-currency-options">
|
|
1100
|
+
<option value="SDG">SDG - Sudanese Pound</option>
|
|
1101
|
+
<option value="SAR">SAR - Saudi Riyal</option>
|
|
1102
|
+
<option value="AED">AED - Emirati Dirham</option>
|
|
1103
|
+
<option value="KWD">KWD - Kuwaiti Dinar</option>
|
|
1104
|
+
<option value="BHD">BHD - Bahraini Dinar</option>
|
|
1105
|
+
<option value="QAR">QAR - Qatari Riyal</option>
|
|
1106
|
+
<option value="OMR">OMR - Omani Rial</option>
|
|
1107
|
+
<option value="USD">USD - US Dollar</option>
|
|
1108
|
+
</datalist>
|
|
1109
|
+
</div>
|
|
1110
|
+
<div className="space-y-1.5">
|
|
1111
|
+
<label className="text-sm font-medium text-muted-foreground">Timezone</label>
|
|
1112
|
+
<input list="system-timezone-options" type="text" value={systemSettings.timezone} onChange={e => setSystemSettings(prev => ({ ...prev, timezone: e.target.value }))} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" placeholder="e.g. Africa/Khartoum" />
|
|
1113
|
+
<datalist id="system-timezone-options">
|
|
1114
|
+
<option value="Africa/Khartoum">Africa/Khartoum (UTC+2)</option>
|
|
1115
|
+
<option value="Asia/Riyadh">Asia/Riyadh (UTC+3)</option>
|
|
1116
|
+
<option value="Asia/Dubai">Asia/Dubai (UTC+4)</option>
|
|
1117
|
+
<option value="Asia/Kuwait">Asia/Kuwait (UTC+3)</option>
|
|
1118
|
+
<option value="Asia/Bahrain">Asia/Bahrain (UTC+3)</option>
|
|
1119
|
+
</datalist>
|
|
1120
|
+
</div>
|
|
1121
|
+
<div className="space-y-1.5">
|
|
1122
|
+
<label className="text-sm font-medium text-muted-foreground">Date Format</label>
|
|
1123
|
+
<Select value={systemSettings.dateFormat} onChange={v => setSystemSettings(prev => ({ ...prev, dateFormat: v }))} options={[
|
|
1124
|
+
{ value: 'DD/MM/YYYY', label: 'DD/MM/YYYY' },
|
|
1125
|
+
{ value: 'MM/DD/YYYY', label: 'MM/DD/YYYY' },
|
|
1126
|
+
{ value: 'YYYY-MM-DD', label: 'YYYY-MM-DD' },
|
|
1127
|
+
]} />
|
|
1128
|
+
</div>
|
|
1129
|
+
</div>
|
|
1130
|
+
</div>
|
|
1131
|
+
</section>
|
|
1132
|
+
|
|
1133
|
+
{/* Notifications */}
|
|
1134
|
+
<section className="space-y-4">
|
|
1135
|
+
<div className="flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1136
|
+
<Bell className="h-4 w-4" /> Notification Thresholds
|
|
1137
|
+
</div>
|
|
1138
|
+
<div className="rounded-xl border border-border bg-background/50 p-5 space-y-4">
|
|
1139
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1140
|
+
<InputField label="Low Stock Alert Threshold (units)" value={systemSettings.lowStockThreshold} field="lowStockThreshold" type="number" />
|
|
1141
|
+
<InputField label="Expiry Warning (days before)" value={systemSettings.expiryWarningDays} field="expiryWarningDays" type="number" />
|
|
1142
|
+
</div>
|
|
1143
|
+
</div>
|
|
1144
|
+
</section>
|
|
1145
|
+
|
|
1146
|
+
{/* Security */}
|
|
1147
|
+
<section className="space-y-4">
|
|
1148
|
+
<div className="flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1149
|
+
<Lock className="h-4 w-4" /> Security
|
|
1150
|
+
</div>
|
|
1151
|
+
<div className="rounded-xl border border-border bg-background/50 p-5 space-y-4">
|
|
1152
|
+
<InputField label="Session Timeout (minutes)" value={systemSettings.sessionTimeout} field="sessionTimeout" type="number" />
|
|
1153
|
+
<Switch
|
|
1154
|
+
checked={systemSettings.enforceStrongPassword}
|
|
1155
|
+
onCheckedChange={c => setSystemSettings(prev => ({ ...prev, enforceStrongPassword: c }))}
|
|
1156
|
+
label="Enforce Strong Passwords"
|
|
1157
|
+
description="Require minimum 8 characters with uppercase, lowercase, numbers, and symbols."
|
|
1158
|
+
/>
|
|
1159
|
+
</div>
|
|
1160
|
+
</section>
|
|
1161
|
+
|
|
1162
|
+
{/* Danger Zone */}
|
|
1163
|
+
<section className="space-y-4 pt-4 border-t border-border">
|
|
1164
|
+
<div className="flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-red-500">
|
|
1165
|
+
<AlertTriangle className="h-4 w-4" /> Danger Zone
|
|
1166
|
+
</div>
|
|
1167
|
+
<div className="rounded-xl border border-red-500/20 bg-red-500/5 p-5 space-y-4">
|
|
1168
|
+
<div className="flex items-center justify-between">
|
|
1169
|
+
<div>
|
|
1170
|
+
<h4 className="text-sm font-medium text-foreground">Wipe Business Data</h4>
|
|
1171
|
+
<p className="text-sm text-muted-foreground mt-1 max-w-xl">
|
|
1172
|
+
Permanently delete all catalog items, inventory batches, and sales records. Keeps user accounts, branches, and system settings intact.
|
|
1173
|
+
</p>
|
|
1174
|
+
</div>
|
|
1175
|
+
<button
|
|
1176
|
+
onClick={() => setWipeDataOpen(true)}
|
|
1177
|
+
className="inline-flex h-9 items-center gap-2 rounded-lg bg-red-500/10 px-4 text-sm font-medium text-red-600 hover:bg-red-500/20 transition-colors"
|
|
1178
|
+
>
|
|
1179
|
+
<Trash2 className="h-4 w-4" />
|
|
1180
|
+
Wipe Data
|
|
1181
|
+
</button>
|
|
1182
|
+
</div>
|
|
1183
|
+
|
|
1184
|
+
<div className="h-px bg-red-500/10 my-4" />
|
|
1185
|
+
|
|
1186
|
+
<div className="flex items-center justify-between">
|
|
1187
|
+
<div>
|
|
1188
|
+
<h4 className="text-sm font-medium text-foreground">Factory Reset</h4>
|
|
1189
|
+
<p className="text-sm text-muted-foreground mt-1 max-w-xl">
|
|
1190
|
+
Erase EVERYTHING including user accounts, roles, and settings. Restores the system to a completely fresh state and logs you out.
|
|
1191
|
+
</p>
|
|
1192
|
+
</div>
|
|
1193
|
+
<button
|
|
1194
|
+
onClick={() => setFactoryResetOpen(true)}
|
|
1195
|
+
className="inline-flex h-9 items-center gap-2 rounded-lg bg-red-600 px-4 text-sm font-medium text-white hover:bg-red-700 transition-colors"
|
|
1196
|
+
>
|
|
1197
|
+
<AlertTriangle className="h-4 w-4" />
|
|
1198
|
+
Factory Reset
|
|
1199
|
+
</button>
|
|
1200
|
+
</div>
|
|
1201
|
+
</div>
|
|
1202
|
+
</section>
|
|
1203
|
+
</div>
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1208
|
+
// TAB: ABOUT
|
|
1209
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1210
|
+
function renderAboutTab() {
|
|
1211
|
+
return (
|
|
1212
|
+
<div className="space-y-6">
|
|
1213
|
+
<div>
|
|
1214
|
+
<h2 className="text-lg font-semibold text-foreground">About</h2>
|
|
1215
|
+
<p className="text-sm text-muted-foreground">System information and version details</p>
|
|
1216
|
+
</div>
|
|
1217
|
+
|
|
1218
|
+
{/* App Info Card */}
|
|
1219
|
+
<div className="rounded-xl border border-border bg-gradient-to-br from-brand-500/5 to-purple-500/5 p-8">
|
|
1220
|
+
<div className="flex items-center gap-5">
|
|
1221
|
+
<div className="flex h-20 w-20 items-center justify-center rounded-2xl bg-white dark:bg-background border border-border overflow-visible">
|
|
1222
|
+
<img src="/logo.png" alt="Pharma ERP Logo" className="w-20 h-20 object-contain scale-[1.5]" />
|
|
1223
|
+
</div>
|
|
1224
|
+
<div>
|
|
1225
|
+
<h3 className="text-2xl font-bold text-foreground">Pharma ERP</h3>
|
|
1226
|
+
<p className="text-sm text-muted-foreground mt-1">Complete pharmacy management solution</p>
|
|
1227
|
+
<div className="flex items-center gap-3 mt-2">
|
|
1228
|
+
<Badge variant="info">v1.0.0</Badge>
|
|
1229
|
+
<span className="text-xs text-muted-foreground">Build: August 2026</span>
|
|
1230
|
+
</div>
|
|
1231
|
+
</div>
|
|
1232
|
+
</div>
|
|
1233
|
+
</div>
|
|
1234
|
+
|
|
1235
|
+
{/* System Status */}
|
|
1236
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1237
|
+
<div className="rounded-xl border border-border bg-background/50 p-5">
|
|
1238
|
+
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-4">System Status</h4>
|
|
1239
|
+
<div className="space-y-3">
|
|
1240
|
+
<div className="flex items-center justify-between">
|
|
1241
|
+
<span className="text-sm text-muted-foreground flex items-center gap-2"><Database className="h-4 w-4" /> Database</span>
|
|
1242
|
+
<span className="text-sm text-green-400 flex items-center gap-1"><CheckCircle2 className="h-3.5 w-3.5" /> Connected</span>
|
|
1243
|
+
</div>
|
|
1244
|
+
<div className="flex items-center justify-between">
|
|
1245
|
+
<span className="text-sm text-muted-foreground flex items-center gap-2"><Zap className="h-4 w-4" /> API Server</span>
|
|
1246
|
+
<span className="text-sm text-green-400 flex items-center gap-1"><CheckCircle2 className="h-3.5 w-3.5" /> Running</span>
|
|
1247
|
+
</div>
|
|
1248
|
+
<div className="flex items-center justify-between">
|
|
1249
|
+
<span className="text-sm text-muted-foreground flex items-center gap-2"><Clock className="h-4 w-4" /> Uptime</span>
|
|
1250
|
+
<span className="text-sm text-foreground">Active</span>
|
|
1251
|
+
</div>
|
|
1252
|
+
</div>
|
|
1253
|
+
</div>
|
|
1254
|
+
|
|
1255
|
+
<div className="rounded-xl border border-border bg-background/50 p-5">
|
|
1256
|
+
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-4">Technology Stack</h4>
|
|
1257
|
+
<div className="space-y-3">
|
|
1258
|
+
<div className="flex items-center justify-between">
|
|
1259
|
+
<span className="text-sm text-muted-foreground">Frontend</span>
|
|
1260
|
+
<span className="text-sm text-foreground">Next.js 15 + React 19</span>
|
|
1261
|
+
</div>
|
|
1262
|
+
<div className="flex items-center justify-between">
|
|
1263
|
+
<span className="text-sm text-muted-foreground">Backend</span>
|
|
1264
|
+
<span className="text-sm text-foreground">FastAPI + Python</span>
|
|
1265
|
+
</div>
|
|
1266
|
+
<div className="flex items-center justify-between">
|
|
1267
|
+
<span className="text-sm text-muted-foreground">Database</span>
|
|
1268
|
+
<span className="text-sm text-foreground">SQLite (aiosqlite)</span>
|
|
1269
|
+
</div>
|
|
1270
|
+
<div className="flex items-center justify-between">
|
|
1271
|
+
<span className="text-sm text-muted-foreground">Auth</span>
|
|
1272
|
+
<span className="text-sm text-foreground">JWT + Argon2</span>
|
|
1273
|
+
</div>
|
|
1274
|
+
</div>
|
|
1275
|
+
</div>
|
|
1276
|
+
</div>
|
|
1277
|
+
|
|
1278
|
+
{/* Support */}
|
|
1279
|
+
<div className="rounded-xl border border-border bg-background/50 p-5">
|
|
1280
|
+
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">Support & License</h4>
|
|
1281
|
+
<p className="text-sm text-muted-foreground">
|
|
1282
|
+
This system is built for the Gulf/MENA region pharmacy market. For support, contact your system administrator.
|
|
1283
|
+
</p>
|
|
1284
|
+
<div className="flex items-center gap-1.5 mt-3 text-xs text-muted-foreground">
|
|
1285
|
+
<Heart className="h-3.5 w-3.5 text-red-400" />
|
|
1286
|
+
Built with care for modern pharmacy management
|
|
1287
|
+
</div>
|
|
1288
|
+
</div>
|
|
1289
|
+
</div>
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1294
|
+
// SLIDE-OVER: USER FORM
|
|
1295
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1296
|
+
function renderUserSlideOver() {
|
|
1297
|
+
return (
|
|
1298
|
+
<SlideOver
|
|
1299
|
+
open={userSlideOpen}
|
|
1300
|
+
onClose={() => setUserSlideOpen(false)}
|
|
1301
|
+
title={editingUserId ? 'Edit User' : 'Add New User'}
|
|
1302
|
+
footer={
|
|
1303
|
+
<div className="flex justify-end gap-3">
|
|
1304
|
+
<button onClick={() => setUserSlideOpen(false)} className="px-4 py-2 text-sm font-medium text-foreground hover:text-foreground transition-colors">Cancel</button>
|
|
1305
|
+
<button onClick={saveUser} disabled={isSaving} className="inline-flex h-10 items-center justify-center rounded-lg bg-brand-500 px-4 text-sm font-medium text-white transition-colors hover:bg-brand-600 disabled:opacity-50">
|
|
1306
|
+
{isSaving ? 'Saving...' : editingUserId ? 'Update User' : 'Create User'}
|
|
1307
|
+
</button>
|
|
1308
|
+
</div>
|
|
1309
|
+
}
|
|
1310
|
+
>
|
|
1311
|
+
<div className="space-y-5">
|
|
1312
|
+
<div className="space-y-1.5">
|
|
1313
|
+
<label className="text-sm font-medium text-muted-foreground">Full Name *</label>
|
|
1314
|
+
<input type="text" value={userForm.full_name} onChange={e => setUserForm(p => ({ ...p, full_name: e.target.value }))} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" />
|
|
1315
|
+
</div>
|
|
1316
|
+
<div className="space-y-1.5">
|
|
1317
|
+
<label className="text-sm font-medium text-muted-foreground">Email *</label>
|
|
1318
|
+
<input type="email" value={userForm.email} onChange={e => setUserForm(p => ({ ...p, email: e.target.value }))} disabled={!!editingUserId} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500 disabled:opacity-50" />
|
|
1319
|
+
</div>
|
|
1320
|
+
{!editingUserId && (
|
|
1321
|
+
<div className="space-y-1.5">
|
|
1322
|
+
<label className="text-sm font-medium text-muted-foreground">Password *</label>
|
|
1323
|
+
<input type="password" value={userForm.password} onChange={e => setUserForm(p => ({ ...p, password: e.target.value }))} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" />
|
|
1324
|
+
</div>
|
|
1325
|
+
)}
|
|
1326
|
+
<div className="space-y-1.5">
|
|
1327
|
+
<label className="text-sm font-medium text-muted-foreground">Role</label>
|
|
1328
|
+
<Select
|
|
1329
|
+
value={userForm.role}
|
|
1330
|
+
onChange={v => setUserForm(p => ({ ...p, role: v }))}
|
|
1331
|
+
options={[
|
|
1332
|
+
{ value: 'SUPER_ADMIN', label: 'Super Admin' },
|
|
1333
|
+
{ value: 'OWNER', label: 'Owner' },
|
|
1334
|
+
{ value: 'BRANCH_MANAGER', label: 'Branch Manager' },
|
|
1335
|
+
{ value: 'PHARMACIST', label: 'Pharmacist' },
|
|
1336
|
+
{ value: 'CASHIER', label: 'Cashier' },
|
|
1337
|
+
{ value: 'INVENTORY_STAFF', label: 'Inventory Staff' },
|
|
1338
|
+
{ value: 'ACCOUNTANT', label: 'Accountant' },
|
|
1339
|
+
]}
|
|
1340
|
+
placeholder="Select role..."
|
|
1341
|
+
/>
|
|
1342
|
+
</div>
|
|
1343
|
+
<div className="space-y-1.5">
|
|
1344
|
+
<label className="text-sm font-medium text-muted-foreground">Phone</label>
|
|
1345
|
+
<input type="text" value={userForm.phone} onChange={e => setUserForm(p => ({ ...p, phone: e.target.value }))} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" />
|
|
1346
|
+
</div>
|
|
1347
|
+
</div>
|
|
1348
|
+
</SlideOver>
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1353
|
+
// SLIDE-OVER: BRANCH FORM
|
|
1354
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1355
|
+
function renderBranchSlideOver() {
|
|
1356
|
+
return (
|
|
1357
|
+
<SlideOver
|
|
1358
|
+
open={branchSlideOpen}
|
|
1359
|
+
onClose={() => setBranchSlideOpen(false)}
|
|
1360
|
+
title={editingBranchId ? 'Edit Branch' : 'Add New Branch'}
|
|
1361
|
+
footer={
|
|
1362
|
+
<div className="flex justify-end gap-3">
|
|
1363
|
+
<button onClick={() => setBranchSlideOpen(false)} className="px-4 py-2 text-sm font-medium text-foreground hover:text-foreground transition-colors">Cancel</button>
|
|
1364
|
+
<button onClick={saveBranch} disabled={isSaving} className="inline-flex h-10 items-center justify-center rounded-lg bg-brand-500 px-4 text-sm font-medium text-white transition-colors hover:bg-brand-600 disabled:opacity-50">
|
|
1365
|
+
{isSaving ? 'Saving...' : editingBranchId ? 'Update Branch' : 'Create Branch'}
|
|
1366
|
+
</button>
|
|
1367
|
+
</div>
|
|
1368
|
+
}
|
|
1369
|
+
>
|
|
1370
|
+
<div className="space-y-5">
|
|
1371
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1372
|
+
<div className="space-y-1.5">
|
|
1373
|
+
<label className="text-sm font-medium text-muted-foreground">Branch Name *</label>
|
|
1374
|
+
<input type="text" value={branchForm.name} onChange={e => setBranchForm(p => ({ ...p, name: e.target.value }))} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" />
|
|
1375
|
+
</div>
|
|
1376
|
+
<div className="space-y-1.5">
|
|
1377
|
+
<label className="text-sm font-medium text-muted-foreground">Arabic Name</label>
|
|
1378
|
+
<input type="text" value={branchForm.name_ar} onChange={e => setBranchForm(p => ({ ...p, name_ar: e.target.value }))} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" dir="rtl" />
|
|
1379
|
+
</div>
|
|
1380
|
+
</div>
|
|
1381
|
+
<div className="space-y-1.5">
|
|
1382
|
+
<label className="text-sm font-medium text-muted-foreground">Branch Code *</label>
|
|
1383
|
+
<input type="text" value={branchForm.code} onChange={e => setBranchForm(p => ({ ...p, code: e.target.value }))} disabled={!!editingBranchId} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground font-mono focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500 disabled:opacity-50" placeholder="e.g. B001" />
|
|
1384
|
+
</div>
|
|
1385
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1386
|
+
<div className="space-y-1.5">
|
|
1387
|
+
<label className="text-sm font-medium text-muted-foreground">Phone</label>
|
|
1388
|
+
<input type="text" value={branchForm.phone} onChange={e => setBranchForm(p => ({ ...p, phone: e.target.value }))} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" />
|
|
1389
|
+
</div>
|
|
1390
|
+
<div className="space-y-1.5">
|
|
1391
|
+
<label className="text-sm font-medium text-muted-foreground">Email</label>
|
|
1392
|
+
<input type="text" value={branchForm.email} onChange={e => setBranchForm(p => ({ ...p, email: e.target.value }))} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" />
|
|
1393
|
+
</div>
|
|
1394
|
+
</div>
|
|
1395
|
+
<div className="space-y-1.5">
|
|
1396
|
+
<label className="text-sm font-medium text-muted-foreground">Address</label>
|
|
1397
|
+
<input type="text" value={branchForm.address} onChange={e => setBranchForm(p => ({ ...p, address: e.target.value }))} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" />
|
|
1398
|
+
</div>
|
|
1399
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1400
|
+
<div className="space-y-1.5">
|
|
1401
|
+
<label className="text-sm font-medium text-muted-foreground">Currency</label>
|
|
1402
|
+
<input list="currency-options" type="text" value={branchForm.currency} onChange={e => setBranchForm(p => ({ ...p, currency: e.target.value }))} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" placeholder="e.g. SDG" />
|
|
1403
|
+
<datalist id="currency-options">
|
|
1404
|
+
<option value="SDG" />
|
|
1405
|
+
<option value="SAR" />
|
|
1406
|
+
<option value="AED" />
|
|
1407
|
+
<option value="KWD" />
|
|
1408
|
+
<option value="QAR" />
|
|
1409
|
+
<option value="USD" />
|
|
1410
|
+
</datalist>
|
|
1411
|
+
</div>
|
|
1412
|
+
<div className="space-y-1.5">
|
|
1413
|
+
<label className="text-sm font-medium text-muted-foreground">Timezone</label>
|
|
1414
|
+
<input list="timezone-options" type="text" value={branchForm.timezone} onChange={e => setBranchForm(p => ({ ...p, timezone: e.target.value }))} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" placeholder="e.g. Africa/Khartoum" />
|
|
1415
|
+
<datalist id="timezone-options">
|
|
1416
|
+
<option value="Africa/Khartoum" />
|
|
1417
|
+
<option value="Asia/Riyadh" />
|
|
1418
|
+
<option value="Asia/Dubai" />
|
|
1419
|
+
</datalist>
|
|
1420
|
+
</div>
|
|
1421
|
+
</div>
|
|
1422
|
+
</div>
|
|
1423
|
+
</SlideOver>
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
}
|