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,296 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { api } from '@/lib/api';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import { toast } from 'react-hot-toast';
|
|
7
|
+
import { useAppStore } from '@/store/app-store';
|
|
8
|
+
import { format } from 'date-fns';
|
|
9
|
+
import {
|
|
10
|
+
Search, Calendar, Filter, Receipt, ArrowLeftRight,
|
|
11
|
+
ChevronRight, AlertCircle, RefreshCw, X
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
import { cn } from '@/lib/utils';
|
|
14
|
+
import * as Dialog from '@radix-ui/react-dialog';
|
|
15
|
+
|
|
16
|
+
export default function SalesHistoryPage() {
|
|
17
|
+
const [sales, setSales] = useState<any[]>([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const { currency } = useAppStore();
|
|
20
|
+
|
|
21
|
+
// Filters
|
|
22
|
+
const [search, setSearch] = useState('');
|
|
23
|
+
const [statusFilter, setStatusFilter] = useState('ALL');
|
|
24
|
+
|
|
25
|
+
// Details Slide-over
|
|
26
|
+
const [selectedSale, setSelectedSale] = useState<any | null>(null);
|
|
27
|
+
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
28
|
+
const [isReturning, setIsReturning] = useState(false);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
fetchSales();
|
|
32
|
+
}, [statusFilter]);
|
|
33
|
+
|
|
34
|
+
const fetchSales = async () => {
|
|
35
|
+
setLoading(true);
|
|
36
|
+
try {
|
|
37
|
+
const data = await api.getSales({ status: statusFilter, search: search || undefined });
|
|
38
|
+
setSales(Array.isArray(data) ? data : []);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
toast.error('Failed to load sales history');
|
|
41
|
+
} finally {
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleSearchSubmit = (e: React.FormEvent) => {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
fetchSales();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const openSaleDetails = async (sale: any) => {
|
|
52
|
+
setSelectedSale(sale);
|
|
53
|
+
setDetailsOpen(true);
|
|
54
|
+
// Fetch full details if items are missing
|
|
55
|
+
if (!sale.items || sale.items.length === 0) {
|
|
56
|
+
try {
|
|
57
|
+
const fullSale = await api.getSaleById(sale.id);
|
|
58
|
+
setSelectedSale(fullSale);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
toast.error('Failed to load sale details');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleProcessReturn = async () => {
|
|
66
|
+
if (!selectedSale) return;
|
|
67
|
+
|
|
68
|
+
if (confirm(`Are you sure you want to refund Invoice ${selectedSale.invoice_number}? This action cannot be undone.`)) {
|
|
69
|
+
setIsReturning(true);
|
|
70
|
+
try {
|
|
71
|
+
const result = await api.returnSale(selectedSale.id);
|
|
72
|
+
toast.success(`Invoice ${selectedSale.invoice_number} has been refunded.`);
|
|
73
|
+
setSelectedSale(result); // Update local details
|
|
74
|
+
fetchSales(); // Refresh table
|
|
75
|
+
} catch (error) {
|
|
76
|
+
toast.error('Failed to process return. Ensure it is not already returned.');
|
|
77
|
+
} finally {
|
|
78
|
+
setIsReturning(false);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="flex flex-col h-full min-h-[calc(100vh-120px)] bg-background">
|
|
85
|
+
{/* Header & Filters */}
|
|
86
|
+
<div className="p-6 pb-0 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
|
87
|
+
<div>
|
|
88
|
+
<h1 className="text-2xl font-bold tracking-tight text-foreground">Sales History</h1>
|
|
89
|
+
<p className="text-sm text-muted-foreground mt-1">View all past transactions and process refunds</p>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div className="flex flex-wrap items-center gap-3 w-full sm:w-auto">
|
|
93
|
+
{/* Search */}
|
|
94
|
+
<form onSubmit={handleSearchSubmit} className="relative w-full sm:w-64">
|
|
95
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
96
|
+
<input
|
|
97
|
+
type="text"
|
|
98
|
+
placeholder="Search invoice..."
|
|
99
|
+
value={search}
|
|
100
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
101
|
+
className="w-full pl-9 pr-4 py-2 bg-surface border border-border rounded-lg text-sm focus:outline-none focus:ring-1 focus:ring-brand-500 text-foreground"
|
|
102
|
+
/>
|
|
103
|
+
</form>
|
|
104
|
+
|
|
105
|
+
{/* Status Filter */}
|
|
106
|
+
<div className="flex items-center p-1 bg-surface border border-border rounded-lg">
|
|
107
|
+
<button
|
|
108
|
+
onClick={() => setStatusFilter('ALL')}
|
|
109
|
+
className={cn("px-3 py-1 text-sm font-medium rounded-md transition-colors", statusFilter === 'ALL' ? "bg-brand-500 text-white" : "text-muted-foreground hover:text-foreground")}
|
|
110
|
+
>
|
|
111
|
+
All
|
|
112
|
+
</button>
|
|
113
|
+
<button
|
|
114
|
+
onClick={() => setStatusFilter('COMPLETED')}
|
|
115
|
+
className={cn("px-3 py-1 text-sm font-medium rounded-md transition-colors", statusFilter === 'COMPLETED' ? "bg-brand-500 text-white" : "text-muted-foreground hover:text-foreground")}
|
|
116
|
+
>
|
|
117
|
+
Completed
|
|
118
|
+
</button>
|
|
119
|
+
<button
|
|
120
|
+
onClick={() => setStatusFilter('RETURNED')}
|
|
121
|
+
className={cn("px-3 py-1 text-sm font-medium rounded-md transition-colors", statusFilter === 'RETURNED' ? "bg-brand-500 text-white" : "text-muted-foreground hover:text-foreground")}
|
|
122
|
+
>
|
|
123
|
+
Returned
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{/* Data Table */}
|
|
130
|
+
<div className="p-6 flex-1">
|
|
131
|
+
<div className="rounded-xl border border-border bg-surface overflow-hidden shadow-sm h-full flex flex-col">
|
|
132
|
+
<div className="overflow-x-auto">
|
|
133
|
+
<table className="w-full text-left text-sm whitespace-nowrap">
|
|
134
|
+
<thead className="bg-background text-xs uppercase text-muted-foreground border-b border-border">
|
|
135
|
+
<tr>
|
|
136
|
+
<th className="px-6 py-4 font-semibold">Invoice</th>
|
|
137
|
+
<th className="px-6 py-4 font-semibold hidden md:table-cell">Date</th>
|
|
138
|
+
<th className="px-6 py-4 font-semibold">Customer</th>
|
|
139
|
+
<th className="px-6 py-4 font-semibold text-center hidden md:table-cell">Items</th>
|
|
140
|
+
<th className="px-6 py-4 font-semibold text-right">Total</th>
|
|
141
|
+
<th className="px-6 py-4 font-semibold text-center hidden md:table-cell">Payment</th>
|
|
142
|
+
<th className="px-6 py-4 font-semibold text-right">Status</th>
|
|
143
|
+
</tr>
|
|
144
|
+
</thead>
|
|
145
|
+
<tbody className="divide-y divide-border">
|
|
146
|
+
{loading ? (
|
|
147
|
+
<tr><td colSpan={7} className="px-6 py-12 text-center text-muted-foreground">Loading sales...</td></tr>
|
|
148
|
+
) : sales.length === 0 ? (
|
|
149
|
+
<tr><td colSpan={7} className="px-6 py-12 text-center text-muted-foreground flex flex-col items-center justify-center">
|
|
150
|
+
<Receipt className="h-10 w-10 mb-3 opacity-20" />
|
|
151
|
+
No sales found for the selected filters.
|
|
152
|
+
</td></tr>
|
|
153
|
+
) : (
|
|
154
|
+
sales.map(sale => (
|
|
155
|
+
<tr
|
|
156
|
+
key={sale.id}
|
|
157
|
+
onClick={() => openSaleDetails(sale)}
|
|
158
|
+
className="hover:bg-gray-800/30 cursor-pointer transition-colors group"
|
|
159
|
+
>
|
|
160
|
+
<td className="px-6 py-4 font-medium text-foreground flex items-center gap-2">
|
|
161
|
+
<Receipt className="h-4 w-4 text-brand-400 opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
162
|
+
{sale.invoice_number}
|
|
163
|
+
</td>
|
|
164
|
+
<td className="px-6 py-4 text-muted-foreground hidden md:table-cell">
|
|
165
|
+
{sale.created_at ? format(new Date(sale.created_at), 'MMM dd, yyyy - hh:mm a') : '-'}
|
|
166
|
+
</td>
|
|
167
|
+
<td className="px-6 py-4 text-foreground">
|
|
168
|
+
{sale.customer?.name || <span className="text-muted-foreground italic">Walk-in</span>}
|
|
169
|
+
</td>
|
|
170
|
+
<td className="px-6 py-4 text-center text-muted-foreground hidden md:table-cell">
|
|
171
|
+
{sale.items?.length || 0}
|
|
172
|
+
</td>
|
|
173
|
+
<td className="px-6 py-4 text-right font-bold text-foreground">
|
|
174
|
+
{currency} {Number(sale.total_amount).toFixed(2)}
|
|
175
|
+
</td>
|
|
176
|
+
<td className="px-6 py-4 text-center hidden md:table-cell">
|
|
177
|
+
<Badge variant="default" className="uppercase text-xs">{sale.payment_method}</Badge>
|
|
178
|
+
</td>
|
|
179
|
+
<td className="px-6 py-4 text-right">
|
|
180
|
+
{sale.status === 'COMPLETED' ? (
|
|
181
|
+
<Badge variant="success">Completed</Badge>
|
|
182
|
+
) : (
|
|
183
|
+
<Badge variant="danger">Returned</Badge>
|
|
184
|
+
)}
|
|
185
|
+
</td>
|
|
186
|
+
</tr>
|
|
187
|
+
))
|
|
188
|
+
)}
|
|
189
|
+
</tbody>
|
|
190
|
+
</table>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Sale Details Slide-over */}
|
|
196
|
+
<Dialog.Root open={detailsOpen} onOpenChange={setDetailsOpen}>
|
|
197
|
+
<Dialog.Portal>
|
|
198
|
+
<Dialog.Overlay className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
|
|
199
|
+
<Dialog.Content className="fixed right-0 top-0 bottom-0 w-full max-w-md bg-surface border-l border-border shadow-2xl z-50 flex flex-col data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right duration-300">
|
|
200
|
+
{selectedSale && (
|
|
201
|
+
<>
|
|
202
|
+
<div className="flex-none flex items-center justify-between p-6 border-b border-border bg-background/50">
|
|
203
|
+
<div>
|
|
204
|
+
<Dialog.Title className="text-xl font-bold text-foreground flex items-center gap-2">
|
|
205
|
+
{selectedSale.invoice_number}
|
|
206
|
+
{selectedSale.status === 'RETURNED' && <Badge variant="danger">Refunded</Badge>}
|
|
207
|
+
</Dialog.Title>
|
|
208
|
+
<Dialog.Description className="text-sm text-muted-foreground mt-1">
|
|
209
|
+
{selectedSale.created_at ? format(new Date(selectedSale.created_at), 'PPP at p') : ''}
|
|
210
|
+
</Dialog.Description>
|
|
211
|
+
</div>
|
|
212
|
+
<Dialog.Close asChild>
|
|
213
|
+
<button className="h-8 w-8 rounded-full bg-gray-800 flex items-center justify-center text-muted-foreground hover:text-white hover:bg-gray-700 transition-colors">
|
|
214
|
+
<X size={18} />
|
|
215
|
+
</button>
|
|
216
|
+
</Dialog.Close>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div className="flex-1 overflow-y-auto p-6 space-y-8">
|
|
220
|
+
{/* Summary Cards */}
|
|
221
|
+
<div className="grid grid-cols-2 gap-4">
|
|
222
|
+
<div className="bg-background rounded-lg p-4 border border-border">
|
|
223
|
+
<p className="text-xs font-medium text-muted-foreground uppercase mb-1">Customer</p>
|
|
224
|
+
<p className="text-sm font-semibold text-foreground truncate">
|
|
225
|
+
{selectedSale.customer?.name || 'Walk-in Customer'}
|
|
226
|
+
</p>
|
|
227
|
+
</div>
|
|
228
|
+
<div className="bg-background rounded-lg p-4 border border-border">
|
|
229
|
+
<p className="text-xs font-medium text-muted-foreground uppercase mb-1">Payment</p>
|
|
230
|
+
<p className="text-sm font-semibold text-foreground uppercase">
|
|
231
|
+
{selectedSale.payment_method}
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* Items List */}
|
|
237
|
+
<div>
|
|
238
|
+
<h3 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-4">Receipt Items</h3>
|
|
239
|
+
<div className="space-y-4">
|
|
240
|
+
{selectedSale.items?.map((item: any, i: number) => (
|
|
241
|
+
<div key={i} className="flex justify-between items-start py-2 border-b border-border/50 last:border-0">
|
|
242
|
+
<div>
|
|
243
|
+
<p className="text-sm font-medium text-foreground">Medicine ID: {item.medicine_id.substring(0,8)}</p>
|
|
244
|
+
<p className="text-xs text-muted-foreground">{item.quantity} x {currency} {Number(item.unit_price).toFixed(2)}</p>
|
|
245
|
+
</div>
|
|
246
|
+
<p className="text-sm font-bold text-foreground">{currency} {Number(item.line_total).toFixed(2)}</p>
|
|
247
|
+
</div>
|
|
248
|
+
))}
|
|
249
|
+
|
|
250
|
+
{!selectedSale.items?.length && (
|
|
251
|
+
<div className="py-4 text-center text-sm text-muted-foreground">Loading items...</div>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
{/* Totals */}
|
|
257
|
+
<div className="bg-background rounded-lg p-5 border border-border">
|
|
258
|
+
<div className="flex justify-between items-center mb-2">
|
|
259
|
+
<span className="text-sm text-muted-foreground">Subtotal</span>
|
|
260
|
+
<span className="text-sm text-foreground">{currency} {Number(selectedSale.total_amount).toFixed(2)}</span>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="flex justify-between items-center py-2 border-t border-border mt-2">
|
|
263
|
+
<span className="text-lg font-bold text-foreground">Total</span>
|
|
264
|
+
<span className="text-lg font-black text-brand-400">{currency} {Number(selectedSale.total_amount).toFixed(2)}</span>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Footer Action */}
|
|
270
|
+
<div className="flex-none p-6 border-t border-border bg-background/50">
|
|
271
|
+
{selectedSale.status === 'COMPLETED' ? (
|
|
272
|
+
<button
|
|
273
|
+
onClick={handleProcessReturn}
|
|
274
|
+
disabled={isReturning}
|
|
275
|
+
className="w-full flex items-center justify-center gap-2 bg-red-500/10 hover:bg-red-500/20 text-red-500 font-medium py-3 rounded-xl transition-colors disabled:opacity-50"
|
|
276
|
+
>
|
|
277
|
+
{isReturning ? (
|
|
278
|
+
<><RefreshCw className="h-5 w-5 animate-spin" /> Processing...</>
|
|
279
|
+
) : (
|
|
280
|
+
<><ArrowLeftRight className="h-5 w-5" /> Process Full Refund</>
|
|
281
|
+
)}
|
|
282
|
+
</button>
|
|
283
|
+
) : (
|
|
284
|
+
<div className="w-full flex items-center justify-center gap-2 bg-gray-800 text-muted-foreground font-medium py-3 rounded-xl">
|
|
285
|
+
<AlertCircle className="h-5 w-5" /> Already Refunded
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
</>
|
|
290
|
+
)}
|
|
291
|
+
</Dialog.Content>
|
|
292
|
+
</Dialog.Portal>
|
|
293
|
+
</Dialog.Root>
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useTranslations } from 'next-intl';
|
|
3
|
+
import Image from 'next/image';
|
|
4
|
+
import { useRouter } from '@/i18n/routing';
|
|
5
|
+
import { useAuthStore } from '@/store/auth-store';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Input } from '@/components/ui/input';
|
|
8
|
+
import { Card } from '@/components/ui/card';
|
|
9
|
+
import { Activity, Eye, EyeOff } from 'lucide-react';
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import toast from 'react-hot-toast';
|
|
12
|
+
|
|
13
|
+
export default function LoginPage() {
|
|
14
|
+
const t = useTranslations('auth');
|
|
15
|
+
const login = useAuthStore(state => state.login);
|
|
16
|
+
const isLoading = useAuthStore(state => state.isLoading);
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
const [email, setEmail] = useState('');
|
|
19
|
+
const [password, setPassword] = useState('');
|
|
20
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
21
|
+
const [rememberMe, setRememberMe] = useState(false);
|
|
22
|
+
|
|
23
|
+
const handleLogin = async (e: React.FormEvent) => {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
try {
|
|
26
|
+
await login({ email, password, remember_me: rememberMe });
|
|
27
|
+
router.push('/dashboard');
|
|
28
|
+
} catch (error: any) {
|
|
29
|
+
toast.error(error.message || 'Invalid credentials');
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="min-h-screen flex items-center justify-center bg-surface relative overflow-hidden">
|
|
35
|
+
{/* Background gradient */}
|
|
36
|
+
<div className="absolute top-0 -left-1/4 w-1/2 h-1/2 bg-brand-500/20 blur-[120px] rounded-full pointer-events-none" />
|
|
37
|
+
<div className="absolute bottom-0 -right-1/4 w-1/2 h-1/2 bg-teal-500/20 blur-[120px] rounded-full pointer-events-none" />
|
|
38
|
+
|
|
39
|
+
<div className="z-10 w-full max-w-md p-6">
|
|
40
|
+
<div className="flex flex-col items-center mb-8 text-center">
|
|
41
|
+
<div className="w-24 h-24 bg-white dark:bg-background rounded-2xl flex items-center justify-center mb-4 shadow-xl border border-border overflow-visible">
|
|
42
|
+
<Image src="/logo.png" alt="Pharma ERP Logo" width={100} height={100} className="object-contain scale-[1.7]" />
|
|
43
|
+
</div>
|
|
44
|
+
<h1 className="text-2xl font-bold text-foreground">Pharma ERP</h1>
|
|
45
|
+
<p className="text-gray-500 mt-2">{t('welcome')}</p>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<Card className="p-6 backdrop-blur-xl bg-background/80 shadow-2xl border-white/10">
|
|
49
|
+
<form onSubmit={handleLogin} className="space-y-4">
|
|
50
|
+
<div className="space-y-2">
|
|
51
|
+
<label className="text-sm font-medium text-foreground">Username or Email</label>
|
|
52
|
+
<Input
|
|
53
|
+
type="text"
|
|
54
|
+
placeholder="admin"
|
|
55
|
+
value={email}
|
|
56
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
57
|
+
required
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="space-y-2">
|
|
61
|
+
<label className="text-sm font-medium text-foreground">{t('password')}</label>
|
|
62
|
+
<div className="relative">
|
|
63
|
+
<Input
|
|
64
|
+
type={showPassword ? "text" : "password"}
|
|
65
|
+
placeholder="••••••••"
|
|
66
|
+
value={password}
|
|
67
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
68
|
+
required
|
|
69
|
+
className="pr-10"
|
|
70
|
+
/>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
74
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
|
75
|
+
>
|
|
76
|
+
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="flex items-center space-x-2 pt-2">
|
|
81
|
+
<input
|
|
82
|
+
type="checkbox"
|
|
83
|
+
id="remember"
|
|
84
|
+
checked={rememberMe}
|
|
85
|
+
onChange={(e) => setRememberMe(e.target.checked)}
|
|
86
|
+
className="rounded border-gray-300 text-brand-500 shadow-sm focus:border-brand-500 focus:ring-brand-500 h-4 w-4 bg-background"
|
|
87
|
+
/>
|
|
88
|
+
<label htmlFor="remember" className="text-sm text-foreground cursor-pointer">
|
|
89
|
+
Stay signed in
|
|
90
|
+
</label>
|
|
91
|
+
</div>
|
|
92
|
+
<Button type="submit" className="w-full mt-6" loading={isLoading}>
|
|
93
|
+
{t('login')}
|
|
94
|
+
</Button>
|
|
95
|
+
</form>
|
|
96
|
+
</Card>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import '@/app/print.css';
|
|
4
|
+
import { useEffect } from 'react';
|
|
5
|
+
import { useAppStore } from '@/store/app-store';
|
|
6
|
+
|
|
7
|
+
export default function PosLayout({ children }: { children: React.ReactNode }) {
|
|
8
|
+
const { theme } = useAppStore();
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
document.documentElement.setAttribute('data-density', 'touch');
|
|
12
|
+
return () => {
|
|
13
|
+
document.documentElement.removeAttribute('data-density');
|
|
14
|
+
};
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
// Apply dark class (same as app layout)
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (theme === 'dark') {
|
|
20
|
+
document.documentElement.classList.add('dark');
|
|
21
|
+
} else {
|
|
22
|
+
document.documentElement.classList.remove('dark');
|
|
23
|
+
}
|
|
24
|
+
}, [theme]);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex flex-col h-screen overflow-hidden bg-surface">
|
|
28
|
+
<main className="flex-1 overflow-hidden">
|
|
29
|
+
{children}
|
|
30
|
+
</main>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|