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,175 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useTranslations } from 'next-intl';
|
|
4
|
+
import { useParams } from 'next/navigation';
|
|
5
|
+
import { StatCard } from '@/components/ui/stat-card';
|
|
6
|
+
import { Card } from '@/components/ui/card';
|
|
7
|
+
import { useAppStore } from '@/store/app-store';
|
|
8
|
+
import { formatMoney } from '@/lib/utils';
|
|
9
|
+
import { api } from '@/lib/api';
|
|
10
|
+
import { CreditCard, Package, AlertTriangle, TrendingUp } from 'lucide-react';
|
|
11
|
+
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export default function DashboardPage() {
|
|
15
|
+
const t = useTranslations();
|
|
16
|
+
const params = useParams();
|
|
17
|
+
const locale = params.locale as string;
|
|
18
|
+
const { currency } = useAppStore();
|
|
19
|
+
|
|
20
|
+
const [stats, setStats] = useState({
|
|
21
|
+
todaySales: 0,
|
|
22
|
+
revenue: 0,
|
|
23
|
+
outOfStock: 0,
|
|
24
|
+
lowStock: 0,
|
|
25
|
+
expiring: 0
|
|
26
|
+
});
|
|
27
|
+
const [chartData, setChartData] = useState<{name: string; revenue: number}[]>([]);
|
|
28
|
+
const [recentSales, setRecentSales] = useState<any[]>([]);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
async function loadData() {
|
|
32
|
+
try {
|
|
33
|
+
const today = new Date();
|
|
34
|
+
const todayStr = today.toISOString().split('T')[0];
|
|
35
|
+
|
|
36
|
+
// Calculate start of current week (Sunday)
|
|
37
|
+
const startOfWeek = new Date(today);
|
|
38
|
+
startOfWeek.setDate(today.getDate() - today.getDay());
|
|
39
|
+
const weekStartStr = startOfWeek.toISOString().split('T')[0];
|
|
40
|
+
|
|
41
|
+
const [outOfStock, lowStock, expiring, recent, dailySales, weeklyRange] = await Promise.all([
|
|
42
|
+
api.getOutOfStock().catch(() => []),
|
|
43
|
+
api.getLowStock().catch(() => []),
|
|
44
|
+
api.getExpiring().catch(() => []),
|
|
45
|
+
api.getSales().catch(() => []),
|
|
46
|
+
api.getDailySales(todayStr).catch(() => ({ total_sales_count: 0, total_amount: 0 })),
|
|
47
|
+
api.getSalesRange(weekStartStr, todayStr).catch(() => null)
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
setStats({
|
|
51
|
+
todaySales: (dailySales as any)?.total_amount || 0,
|
|
52
|
+
revenue: (weeklyRange as any)?.total_revenue || (dailySales as any)?.total_amount || 0,
|
|
53
|
+
outOfStock: Array.isArray(outOfStock) ? outOfStock.length : 0,
|
|
54
|
+
lowStock: Array.isArray(lowStock) ? lowStock.length : 0,
|
|
55
|
+
expiring: Array.isArray(expiring) ? expiring.length : 0
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const recentArray = Array.isArray(recent) ? recent : (recent as any)?.items || [];
|
|
59
|
+
setRecentSales(recentArray.slice(0, 5));
|
|
60
|
+
|
|
61
|
+
// Build weekly chart data
|
|
62
|
+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
63
|
+
const weekData = dayNames.map((name, i) => {
|
|
64
|
+
const dayDate = new Date(startOfWeek);
|
|
65
|
+
dayDate.setDate(startOfWeek.getDate() + i);
|
|
66
|
+
return { name, revenue: 0, date: dayDate.toISOString().split('T')[0] };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if ((weeklyRange as any)?.daily_breakdown) {
|
|
70
|
+
(weeklyRange as any).daily_breakdown.forEach((d: any) => {
|
|
71
|
+
const found = weekData.find(w => w.date === d.date);
|
|
72
|
+
if (found) found.revenue = d.revenue || d.total_revenue || 0;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setChartData(weekData.map(({ name, revenue }) => ({ name, revenue })));
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Failed to load dashboard data', error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
loadData();
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="space-y-6">
|
|
86
|
+
<div className="flex justify-between items-end">
|
|
87
|
+
<div>
|
|
88
|
+
<h1 className="text-2xl font-bold text-foreground">Dashboard</h1>
|
|
89
|
+
<p className="text-gray-500">Welcome back. Here is what's happening today.</p>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
|
|
94
|
+
<StatCard
|
|
95
|
+
title={t('dashboard.todaySales')}
|
|
96
|
+
value={formatMoney(stats.todaySales, currency)}
|
|
97
|
+
icon={<CreditCard size={20} />}
|
|
98
|
+
/>
|
|
99
|
+
<StatCard
|
|
100
|
+
title={t('dashboard.revenue')}
|
|
101
|
+
value={formatMoney(stats.revenue, currency)}
|
|
102
|
+
icon={<TrendingUp size={20} />}
|
|
103
|
+
/>
|
|
104
|
+
<StatCard
|
|
105
|
+
title="Out of Stock"
|
|
106
|
+
value={`${stats.outOfStock} Items`}
|
|
107
|
+
icon={<Package size={20} className="text-danger" />}
|
|
108
|
+
/>
|
|
109
|
+
<StatCard
|
|
110
|
+
title={t('dashboard.lowStock')}
|
|
111
|
+
value={`${stats.lowStock} Items`}
|
|
112
|
+
icon={<AlertTriangle size={20} className="text-warning" />}
|
|
113
|
+
/>
|
|
114
|
+
<StatCard
|
|
115
|
+
title={t('dashboard.expiringMedicines')}
|
|
116
|
+
value={`${stats.expiring} Items`}
|
|
117
|
+
icon={<AlertTriangle size={20} className="text-danger" />}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
122
|
+
<Card className="col-span-1 lg:col-span-2 p-4 md:p-6">
|
|
123
|
+
<h3 className="font-semibold text-lg mb-4">Sales Overview</h3>
|
|
124
|
+
<div className="h-64 md:h-72 w-full">
|
|
125
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
126
|
+
<AreaChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
|
|
127
|
+
<defs>
|
|
128
|
+
<linearGradient id="colorSales" x1="0" y1="0" x2="0" y2="1">
|
|
129
|
+
<stop offset="5%" stopColor="var(--brand-primary)" stopOpacity={0.3}/>
|
|
130
|
+
<stop offset="95%" stopColor="var(--brand-primary)" stopOpacity={0}/>
|
|
131
|
+
</linearGradient>
|
|
132
|
+
</defs>
|
|
133
|
+
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--border)" />
|
|
134
|
+
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{fill: 'var(--foreground)', opacity: 0.5}} dy={10} />
|
|
135
|
+
<YAxis axisLine={false} tickLine={false} tick={{fill: 'var(--foreground)', opacity: 0.5}} tickFormatter={(value) => `${value}`} />
|
|
136
|
+
<Tooltip
|
|
137
|
+
contentStyle={{ backgroundColor: 'var(--background)', borderColor: 'var(--border)', borderRadius: '8px' }}
|
|
138
|
+
itemStyle={{ color: 'var(--brand-primary)' }}
|
|
139
|
+
formatter={(value: number) => formatMoney(value, currency)}
|
|
140
|
+
/>
|
|
141
|
+
<Area type="monotone" dataKey="revenue" name="Revenue" stroke="var(--brand-primary)" strokeWidth={2} fillOpacity={1} fill="url(#colorSales)" />
|
|
142
|
+
</AreaChart>
|
|
143
|
+
</ResponsiveContainer>
|
|
144
|
+
</div>
|
|
145
|
+
</Card>
|
|
146
|
+
|
|
147
|
+
<Card className="p-6 flex flex-col">
|
|
148
|
+
<h3 className="font-semibold text-lg mb-4">{t('dashboard.recentTransactions')}</h3>
|
|
149
|
+
<div className="space-y-4 flex-1 overflow-y-auto">
|
|
150
|
+
{recentSales.length > 0 ? (
|
|
151
|
+
recentSales.map((sale: any) => (
|
|
152
|
+
<div key={sale.id} className="flex justify-between items-center p-3 hover:bg-surface rounded-lg transition-colors border border-transparent hover:border-border">
|
|
153
|
+
<div>
|
|
154
|
+
<div className="font-medium text-sm">{sale.invoice_number || sale.id.substring(0, 8)}</div>
|
|
155
|
+
<div className="text-xs text-gray-500">{sale.customer?.name || 'Walk-in Customer'}</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div className="text-right">
|
|
158
|
+
<div className="font-medium text-sm">{formatMoney(sale.total_amount || 0, currency)}</div>
|
|
159
|
+
<div className="text-xs text-success">Completed</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
))
|
|
163
|
+
) : (
|
|
164
|
+
<div className="flex-1 flex flex-col items-center justify-center text-center py-8">
|
|
165
|
+
<CreditCard size={32} className="text-foreground/20 mb-2" />
|
|
166
|
+
<p className="text-sm text-foreground/40">No recent transactions</p>
|
|
167
|
+
<p className="text-xs text-foreground/30 mt-1">Sales will appear here once processed</p>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</Card>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|