Skip to content

Commit 2746889

Browse files
committed
feat: add ConfirmationModal component and integrate it into RefillerDashboard and StockOut views for stock entry confirmation
1 parent 04d107a commit 2746889

3 files changed

Lines changed: 327 additions & 16 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import React from 'react';
2+
import { X, CheckCircle, Package } from 'lucide-react';
3+
4+
export interface ConfirmationItem {
5+
id: string;
6+
name: string;
7+
brand: string;
8+
mrp: number;
9+
quantity: number;
10+
amount: number;
11+
}
12+
13+
interface ConfirmationModalProps {
14+
isOpen: boolean;
15+
onClose: () => void;
16+
onConfirm: () => void;
17+
items: ConfirmationItem[];
18+
title: string;
19+
subtitle?: string;
20+
confirmButtonText?: string;
21+
confirmButtonColor?: 'indigo' | 'rose';
22+
isProcessing?: boolean;
23+
}
24+
25+
const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
26+
isOpen,
27+
onClose,
28+
onConfirm,
29+
items,
30+
title,
31+
subtitle,
32+
confirmButtonText = 'Confirm',
33+
confirmButtonColor = 'indigo',
34+
isProcessing = false
35+
}) => {
36+
if (!isOpen) return null;
37+
38+
const totalQuantity = items.reduce((sum, item) => sum + item.quantity, 0);
39+
const totalAmount = items.reduce((sum, item) => sum + item.amount, 0);
40+
41+
// Group items by brand
42+
const itemsByBrand = items.reduce((acc, item) => {
43+
if (!acc[item.brand]) {
44+
acc[item.brand] = [];
45+
}
46+
acc[item.brand].push(item);
47+
return acc;
48+
}, {} as Record<string, ConfirmationItem[]>);
49+
50+
const colorClasses = {
51+
indigo: {
52+
gradient: 'from-indigo-500 to-violet-600',
53+
shadow: 'shadow-indigo-500/30',
54+
button: 'bg-indigo-600 hover:bg-indigo-700 shadow-indigo-200',
55+
icon: 'bg-indigo-100 text-indigo-600',
56+
badge: 'bg-indigo-100/80 text-indigo-700 border-indigo-200'
57+
},
58+
rose: {
59+
gradient: 'from-rose-500 to-pink-600',
60+
shadow: 'shadow-rose-500/30',
61+
button: 'bg-rose-600 hover:bg-rose-700 shadow-rose-200',
62+
icon: 'bg-rose-100 text-rose-600',
63+
badge: 'bg-rose-100/80 text-rose-700 border-rose-200'
64+
}
65+
};
66+
67+
const colors = colorClasses[confirmButtonColor];
68+
69+
return (
70+
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-slate-900/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
71+
<div className="bg-white rounded-[24px] shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden ring-1 ring-white/20 flex flex-col">
72+
{/* Header */}
73+
<div className="p-6 border-b border-slate-100 flex items-center justify-between bg-slate-50/50 shrink-0">
74+
<div className="flex items-center gap-4">
75+
<div className={`bg-gradient-to-br ${colors.gradient} p-3 rounded-xl text-white shadow-lg ${colors.shadow}`}>
76+
<CheckCircle size={24} />
77+
</div>
78+
<div>
79+
<h3 className="text-2xl font-bold text-slate-800">{title}</h3>
80+
{subtitle && <p className="text-sm text-slate-500 mt-1">{subtitle}</p>}
81+
</div>
82+
</div>
83+
<button
84+
onClick={onClose}
85+
className="p-2 rounded-full hover:bg-slate-200 text-slate-400 transition-colors"
86+
disabled={isProcessing}
87+
>
88+
<X size={20} />
89+
</button>
90+
</div>
91+
92+
{/* Summary Stats */}
93+
<div className="p-6 bg-gradient-to-r from-slate-50/50 to-slate-100/30 border-b border-slate-100 shrink-0">
94+
<div className="grid grid-cols-3 gap-4">
95+
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
96+
<div className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Total Items</div>
97+
<div className="text-2xl font-black text-slate-800">{items.length}</div>
98+
</div>
99+
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
100+
<div className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Total Quantity</div>
101+
<div className="text-2xl font-black text-slate-800">{totalQuantity}</div>
102+
</div>
103+
<div className="bg-white rounded-xl p-4 shadow-sm border border-slate-100">
104+
<div className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Total Amount</div>
105+
<div className="text-2xl font-black text-slate-800">{totalAmount.toFixed(2)}</div>
106+
</div>
107+
</div>
108+
</div>
109+
110+
{/* Items List - Scrollable */}
111+
<div className="flex-1 overflow-y-auto p-6 space-y-6">
112+
{(Object.entries(itemsByBrand) as [string, ConfirmationItem[]][]).map(([brand, brandItems]) => {
113+
const brandTotal = brandItems.reduce((sum, item) => sum + item.amount, 0);
114+
return (
115+
<div key={brand} className="glass-panel rounded-[20px] overflow-hidden">
116+
<div className="bg-slate-900/5 px-6 py-4 flex items-center justify-between border-b border-white/10">
117+
<div className="flex items-center gap-3">
118+
<Package size={16} className="text-slate-500" />
119+
<span className="text-slate-800 font-bold tracking-wide">{brand}</span>
120+
</div>
121+
<div className="text-sm font-bold text-slate-600">
122+
{brandTotal.toFixed(2)}
123+
</div>
124+
</div>
125+
<div className="overflow-x-auto">
126+
<table className="w-full text-left border-collapse">
127+
<thead>
128+
<tr className="border-b border-slate-100 text-[10px] uppercase font-bold text-slate-400 tracking-wider">
129+
<th className="px-6 py-3 w-12">#</th>
130+
<th className="px-6 py-3 min-w-[200px]">Item Name</th>
131+
<th className="px-6 py-3 w-32 text-center">MRP (₹)</th>
132+
<th className="px-6 py-3 w-32 text-center">Quantity</th>
133+
<th className="px-6 py-3 w-40 text-right">Amount (₹)</th>
134+
</tr>
135+
</thead>
136+
<tbody className="divide-y divide-slate-50/50">
137+
{brandItems.map((item, idx) => (
138+
<tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
139+
<td className="px-6 py-3 text-xs text-slate-300 font-mono">{idx + 1}</td>
140+
<td className="px-6 py-3 text-sm font-semibold text-slate-700">{item.name}</td>
141+
<td className="px-6 py-3 text-sm text-center font-mono text-slate-500">
142+
{item.mrp.toFixed(2)}
143+
</td>
144+
<td className="px-6 py-3 text-center">
145+
<span className={`inline-block px-3 py-1 rounded-full text-xs font-bold ${colors.badge} border`}>
146+
{item.quantity} units
147+
</span>
148+
</td>
149+
<td className="px-6 py-3 text-sm text-right font-bold text-slate-800">
150+
{item.amount.toFixed(2)}
151+
</td>
152+
</tr>
153+
))}
154+
</tbody>
155+
</table>
156+
</div>
157+
</div>
158+
);
159+
})}
160+
</div>
161+
162+
{/* Footer Actions */}
163+
<div className="p-6 bg-slate-50 flex gap-3 justify-end border-t border-slate-200 shrink-0">
164+
<button
165+
onClick={onClose}
166+
disabled={isProcessing}
167+
className="px-6 py-3 rounded-xl font-bold text-slate-600 hover:bg-white hover:shadow-sm border border-transparent hover:border-slate-200 transition-all disabled:opacity-50"
168+
>
169+
Cancel
170+
</button>
171+
<button
172+
onClick={onConfirm}
173+
disabled={isProcessing}
174+
className={`px-8 py-3 rounded-xl font-bold text-white ${colors.button} shadow-lg transition-all active:translate-y-0.5 disabled:opacity-50 flex items-center gap-2`}
175+
>
176+
{isProcessing ? (
177+
<>
178+
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
179+
Processing...
180+
</>
181+
) : (
182+
<>
183+
<CheckCircle size={20} />
184+
{confirmButtonText}
185+
</>
186+
)}
187+
</button>
188+
</div>
189+
</div>
190+
</div>
191+
);
192+
};
193+
194+
export default ConfirmationModal;

client/views/RefillerDashboard.tsx

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { User, StockEntry, Product } from '../types';
33
import { Save, Calendar, Store, Info, Plus, Trash2, PackagePlus, Layers, Edit2, AlertTriangle, X } from 'lucide-react';
44
import { useToast } from '../context/ToastContext';
55
import { api } from '../services/api';
6+
import ConfirmationModal, { ConfirmationItem } from '../components/ConfirmationModal';
67

78
interface RefillerDashboardProps {
89
user: User;
@@ -44,6 +45,10 @@ const RefillerDashboard: React.FC<RefillerDashboardProps> = ({ user, products, o
4445
const [isProcessing, setIsProcessing] = useState(false);
4546
const [editForm, setEditForm] = useState({ name: '', brand: '', mrp: '' });
4647

48+
// --- Confirmation Modal State ---
49+
const [showConfirmation, setShowConfirmation] = useState(false);
50+
const [confirmationItems, setConfirmationItems] = useState<ConfirmationItem[]>([]);
51+
4752
const productsByBrand = useMemo(() => {
4853
const grouped: Record<string, Product[]> = {};
4954
products.forEach(p => {
@@ -165,6 +170,68 @@ const RefillerDashboard: React.FC<RefillerDashboardProps> = ({ user, products, o
165170

166171
// --- SAVE ---
167172
const handleSave = () => {
173+
const confirmItems: ConfirmationItem[] = [];
174+
175+
// 1. Process Standard Entries (Existing Products)
176+
(Object.entries(formData) as [string, { qty: string, amt: string }][]).forEach(([productId, data]) => {
177+
if (data.qty && data.amt) {
178+
const product = products.find(p => p.id === productId);
179+
if (product) {
180+
confirmItems.push({
181+
id: productId,
182+
name: product.name,
183+
brand: product.brand,
184+
mrp: product.mrp,
185+
quantity: parseFloat(data.qty),
186+
amount: parseFloat(data.amt)
187+
});
188+
}
189+
}
190+
});
191+
192+
// 2. Process New Items in EXISTING Brands
193+
(Object.entries(newItemsByBrand) as [string, CustomRow[]][]).forEach(([brand, rows]) => {
194+
rows.forEach(row => {
195+
if (row.name && row.qty && row.amt) {
196+
confirmItems.push({
197+
id: row.id,
198+
name: row.name,
199+
brand: brand,
200+
mrp: parseFloat(row.mrp) || 0,
201+
quantity: parseFloat(row.qty),
202+
amount: parseFloat(row.amt)
203+
});
204+
}
205+
});
206+
});
207+
208+
// 3. Process Custom Tables (New Categories + New Products)
209+
customTables.forEach(table => {
210+
table.rows.forEach(row => {
211+
if (row.name && row.qty && row.amt) {
212+
confirmItems.push({
213+
id: row.id,
214+
name: row.name,
215+
brand: table.title || 'Uncategorized',
216+
mrp: parseFloat(row.mrp) || 0,
217+
quantity: parseFloat(row.qty),
218+
amount: parseFloat(row.amt)
219+
});
220+
}
221+
});
222+
});
223+
224+
if (confirmItems.length === 0) {
225+
addToast("Please enter at least one valid stock entry before submitting.", "error");
226+
return;
227+
}
228+
229+
// Show confirmation modal
230+
setConfirmationItems(confirmItems);
231+
setShowConfirmation(true);
232+
};
233+
234+
const handleConfirmSave = () => {
168235
const newEntries: StockEntry[] = [];
169236
const newProducts: Product[] = [];
170237

@@ -234,15 +301,11 @@ const RefillerDashboard: React.FC<RefillerDashboardProps> = ({ user, products, o
234301
});
235302
});
236303

237-
if (newEntries.length === 0) {
238-
addToast("Please enter at least one valid stock entry before submitting.", "error");
239-
return;
240-
}
241-
242304
onAddBatch(newEntries, newProducts);
243305
setFormData({});
244306
setCustomTables([]);
245307
setNewItemsByBrand({});
308+
setShowConfirmation(false);
246309
addToast(`Success! Processing ${newEntries.length} entries...`, "success");
247310
};
248311

@@ -294,6 +357,18 @@ const RefillerDashboard: React.FC<RefillerDashboardProps> = ({ user, products, o
294357
<div className="space-y-8 pb-20 relative">
295358

296359
{/* --- Modals --- */}
360+
{/* Confirmation Modal */}
361+
<ConfirmationModal
362+
isOpen={showConfirmation}
363+
onClose={() => setShowConfirmation(false)}
364+
onConfirm={handleConfirmSave}
365+
items={confirmationItems}
366+
title="Confirm Stock Entry"
367+
subtitle="Please review the items you're about to submit"
368+
confirmButtonText="Submit Batch"
369+
confirmButtonColor="indigo"
370+
/>
371+
297372
{/* Edit Modal */}
298373
{editingProduct && (
299374
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-slate-900/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">

0 commit comments

Comments
 (0)