Antaram commited on
Commit
b69b439
·
verified ·
1 Parent(s): 95aa713

Upload 28 files

Browse files
App.tsx CHANGED
@@ -7,6 +7,8 @@ import AwaakBill from './pages/AwaakBill';
7
  import StockReport from './pages/StockReport';
8
  import PartyLedger from './pages/PartyLedger';
9
  import Settings from './pages/Settings';
 
 
10
 
11
  const App = () => {
12
  // Register Service Worker for PWA
@@ -25,19 +27,22 @@ const App = () => {
25
  }, []);
26
 
27
  return (
28
- <HashRouter>
29
- <Layout>
30
- <Routes>
31
- <Route path="/" element={<Dashboard />} />
32
- <Route path="/jawaak" element={<JawaakBill />} />
33
- <Route path="/awaak" element={<AwaakBill />} />
34
- <Route path="/stock" element={<StockReport />} />
35
- <Route path="/ledger" element={<PartyLedger />} />
36
- <Route path="/settings" element={<Settings />} />
37
- <Route path="*" element={<Navigate to="/" replace />} />
38
- </Routes>
39
- </Layout>
40
- </HashRouter>
 
 
 
41
  );
42
  };
43
 
 
7
  import StockReport from './pages/StockReport';
8
  import PartyLedger from './pages/PartyLedger';
9
  import Settings from './pages/Settings';
10
+ import PWAInstallPrompt from './components/PWAInstallPrompt';
11
+ import { PWAProvider } from './context/PWAContext';
12
 
13
  const App = () => {
14
  // Register Service Worker for PWA
 
27
  }, []);
28
 
29
  return (
30
+ <PWAProvider>
31
+ <HashRouter>
32
+ <Layout>
33
+ <Routes>
34
+ <Route path="/" element={<Dashboard />} />
35
+ <Route path="/jawaak" element={<JawaakBill />} />
36
+ <Route path="/awaak" element={<AwaakBill />} />
37
+ <Route path="/stock" element={<StockReport />} />
38
+ <Route path="/ledger" element={<PartyLedger />} />
39
+ <Route path="/settings" element={<Settings />} />
40
+ <Route path="*" element={<Navigate to="/" replace />} />
41
+ </Routes>
42
+ </Layout>
43
+ <PWAInstallPrompt />
44
+ </HashRouter>
45
+ </PWAProvider>
46
  );
47
  };
48
 
components/PWAInstallPrompt.tsx ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Download, X } from 'lucide-react';
3
+ import { usePWA } from '../context/PWAContext';
4
+
5
+ const PWAInstallPrompt: React.FC = () => {
6
+ const { deferredPrompt, isInstallable, isIOS, isStandalone, installApp } = usePWA();
7
+ const [showInstallPrompt, setShowInstallPrompt] = useState(false);
8
+
9
+ useEffect(() => {
10
+ // Show install prompt if installable and not already installed
11
+ if (isInstallable && !isStandalone) {
12
+ setShowInstallPrompt(true);
13
+ }
14
+
15
+ // For iOS, show prompt if not installed
16
+ if (isIOS && !isStandalone) {
17
+ setShowInstallPrompt(true);
18
+ }
19
+ }, [isInstallable, isStandalone, isIOS]);
20
+
21
+ const handleInstallClick = async () => {
22
+ if (isIOS) {
23
+ // iOS doesn't support programmatic install, show instructions
24
+ return;
25
+ }
26
+
27
+ await installApp();
28
+ setShowInstallPrompt(false);
29
+ };
30
+
31
+ const handleDismiss = () => {
32
+ setShowInstallPrompt(false);
33
+ // Remember dismissal for 7 days
34
+ localStorage.setItem('pwa-install-dismissed', Date.now().toString());
35
+ };
36
+
37
+ // Don't show if already installed
38
+ if (isStandalone) {
39
+ return null;
40
+ }
41
+
42
+ // Check if dismissed recently (within 7 days)
43
+ const dismissedTime = localStorage.getItem('pwa-install-dismissed');
44
+ if (dismissedTime) {
45
+ const daysSinceDismissed = (Date.now() - parseInt(dismissedTime)) / (1000 * 60 * 60 * 24);
46
+ if (daysSinceDismissed < 7) {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ if (!showInstallPrompt) {
52
+ return null;
53
+ }
54
+
55
+ return (
56
+ <div className="fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-96 bg-white rounded-lg shadow-2xl border-2 border-teal-600 p-4 z-50 animate-slide-up">
57
+ <button
58
+ onClick={handleDismiss}
59
+ className="absolute top-2 right-2 text-gray-400 hover:text-gray-600"
60
+ >
61
+ <X size={20} />
62
+ </button>
63
+
64
+ <div className="flex items-start gap-3">
65
+ <div className="p-2 bg-teal-100 rounded-lg">
66
+ <Download className="text-teal-600" size={24} />
67
+ </div>
68
+ <div className="flex-1">
69
+ <h3 className="font-bold text-gray-900 mb-1">Install App</h3>
70
+ {isIOS ? (
71
+ <div className="text-sm text-gray-600">
72
+ <p className="mb-2">Install this app on your iPhone:</p>
73
+ <ol className="list-decimal list-inside space-y-1 text-xs">
74
+ <li>Tap the Share button <span className="inline-block">⎋</span></li>
75
+ <li>Scroll and tap "Add to Home Screen"</li>
76
+ <li>Tap "Add" in the top right</li>
77
+ </ol>
78
+ </div>
79
+ ) : (
80
+ <>
81
+ <p className="text-sm text-gray-600 mb-3">
82
+ Install our app for a better experience and offline access!
83
+ </p>
84
+ <button
85
+ onClick={handleInstallClick}
86
+ className="w-full bg-teal-600 text-white py-2 px-4 rounded-lg font-medium hover:bg-teal-700 transition-colors"
87
+ >
88
+ Install Now
89
+ </button>
90
+ </>
91
+ )}
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ };
97
+
98
+ export default PWAInstallPrompt;
components/PdfInvoice.tsx ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Transaction } from '../types';
3
+ import { Download, X, Eye } from 'lucide-react';
4
+ import jsPDF from 'jspdf';
5
+ import 'jspdf-autotable';
6
+
7
+ interface PdfInvoiceProps {
8
+ transaction: Transaction;
9
+ className?: string;
10
+ children?: React.ReactNode;
11
+ }
12
+
13
+ const PdfInvoice: React.FC<PdfInvoiceProps> = ({ transaction, className, children }) => {
14
+ const [showPreview, setShowPreview] = useState(false);
15
+ const [pdfUrl, setPdfUrl] = useState<string>('');
16
+
17
+ const formatINR = (v: number) => `₹${v.toFixed(2)}`;
18
+ const formatDate = (dateStr: string) => {
19
+ const date = new Date(dateStr);
20
+ return date.toLocaleDateString('en-IN', { day: '2-digit', month: '2-digit', year: 'numeric' });
21
+ };
22
+
23
+ const generatePDF = (download: boolean = false) => {
24
+ const doc = new jsPDF();
25
+
26
+ const subtotal = transaction.subtotal || 0;
27
+ const packing = transaction.expenses?.poti_amount || 0;
28
+ const cess = transaction.expenses?.cess_amount || 0;
29
+ const adat = transaction.expenses?.adat_amount || 0;
30
+ const hamali = (transaction.expenses?.hamali_amount || 0) + (transaction.expenses?.packaging_hamali_amount || 0);
31
+ const grandTotal = transaction.total_amount || 0;
32
+ const paidAmount = transaction.paid_amount || 0;
33
+ const balanceAmount = transaction.balance_amount || 0;
34
+
35
+ // Header - Invoice Date on left, Bill No on right
36
+ doc.setFontSize(13);
37
+ doc.setFont('helvetica', 'bold');
38
+ doc.text(`Invoice Date: ${formatDate(transaction.bill_date)}`, 14, 15);
39
+ doc.text(`Bill No: ${transaction.bill_number}`, 196, 15, { align: 'right' });
40
+
41
+ // Party Details Section - LEFT ALIGNED
42
+ doc.setFontSize(11);
43
+ doc.setFont('helvetica', 'bold');
44
+ doc.text('Party Details', 14, 25);
45
+ doc.setLineWidth(0.5);
46
+ doc.line(14, 26, 55, 26);
47
+
48
+ // Party info - explicitly left aligned at x=14
49
+ doc.setFont('helvetica', 'normal');
50
+ doc.setFontSize(9);
51
+
52
+ const partyName = transaction.party_name || '-';
53
+ const partyCity = transaction.party_city || '-';
54
+ const partyPhone = transaction.party_phone || '-';
55
+
56
+ doc.text(`Name: ${partyName}`, 14, 31, { align: 'left' });
57
+ doc.text(`Place: ${partyCity}`, 14, 36, { align: 'left' });
58
+ doc.text(`Mobile: ${partyPhone}`, 14, 41, { align: 'left' });
59
+
60
+ // Items Table
61
+ const tableData = transaction.items.map((item, idx) => {
62
+ const potiWeights = Array.isArray(item.poti_weights) ? item.poti_weights.join(', ') : (typeof item.poti_weights === 'string' ? item.poti_weights : '-');
63
+ const amount = item.net_weight * item.rate_per_kg;
64
+ return [idx + 1, `${item.mirchi_name}\n${potiWeights}`, item.poti_count, item.net_weight, item.rate_per_kg, formatINR(amount)];
65
+ });
66
+
67
+ (doc as any).autoTable({
68
+ startY: 47,
69
+ head: [['#', 'Product Type', 'Bags', 'Net (kg)', 'Rate (₹)', 'Amount (₹)']],
70
+ body: tableData,
71
+ theme: 'grid',
72
+ styles: { fontSize: 8, cellPadding: 2, lineWidth: 0.5, lineColor: [0, 0, 0] },
73
+ headStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontStyle: 'bold' },
74
+ bodyStyles: { textColor: [0, 0, 0] },
75
+ columnStyles: {
76
+ 0: { halign: 'center', cellWidth: 10 },
77
+ 1: { halign: 'left', cellWidth: 60 },
78
+ 2: { halign: 'center', cellWidth: 20 },
79
+ 3: { halign: 'center', cellWidth: 25 },
80
+ 4: { halign: 'center', cellWidth: 25 },
81
+ 5: { halign: 'right', cellWidth: 35 }
82
+ }
83
+ });
84
+
85
+ // Summary Table - Right side
86
+ const finalY = (doc as any).lastAutoTable.finalY + 5;
87
+ const summaryData = [
88
+ ['Sub Total', formatINR(subtotal)],
89
+ ['Packing', formatINR(packing)],
90
+ ['CESS', formatINR(cess)],
91
+ ['Adat', formatINR(adat)],
92
+ ['Hamali', formatINR(hamali)],
93
+ ['Total Amount', formatINR(grandTotal)]
94
+ ];
95
+
96
+ (doc as any).autoTable({
97
+ startY: finalY,
98
+ body: summaryData,
99
+ theme: 'grid',
100
+ styles: { fontSize: 8, cellPadding: 2, lineWidth: 0.5, lineColor: [0, 0, 0] },
101
+ bodyStyles: { textColor: [0, 0, 0] },
102
+ columnStyles: {
103
+ 0: { halign: 'left', cellWidth: 50 },
104
+ 1: { halign: 'right', cellWidth: 35 }
105
+ },
106
+ margin: { left: 111 }
107
+ });
108
+
109
+ // Payment Status Section - LEFT ALIGNED
110
+ const paymentY = (doc as any).lastAutoTable.finalY + 10;
111
+ doc.setFontSize(11);
112
+ doc.setFont('helvetica', 'bold');
113
+ doc.text('Payment Status', 14, paymentY, { align: 'left' });
114
+ doc.setLineWidth(0.5);
115
+ doc.line(14, paymentY + 1, 60, paymentY + 1);
116
+
117
+ doc.setFont('helvetica', 'normal');
118
+ doc.setFontSize(9);
119
+
120
+ let currentY = paymentY + 6;
121
+
122
+ if (transaction.payments && transaction.payments.length > 0) {
123
+ transaction.payments.forEach((payment) => {
124
+ const mode = payment.mode === 'cash' ? 'Cash' : payment.mode === 'online' ? 'Online' : payment.mode;
125
+ doc.text(`${mode}: ${formatINR(payment.amount)}`, 14, currentY, { align: 'left' });
126
+ currentY += 5;
127
+ });
128
+ doc.setFont('helvetica', 'bold');
129
+ doc.text(`Total Paid: ${formatINR(paidAmount)}`, 14, currentY, { align: 'left' });
130
+ currentY += 5;
131
+ } else {
132
+ doc.text(`Paid Amount: ${formatINR(paidAmount)}`, 14, currentY, { align: 'left' });
133
+ currentY += 5;
134
+ }
135
+
136
+ if (balanceAmount > 0) {
137
+ doc.setFont('helvetica', 'bold');
138
+ doc.setTextColor(211, 47, 47);
139
+ doc.text(`Due Amount: ${formatINR(balanceAmount)}`, 14, currentY, { align: 'left' });
140
+ doc.setTextColor(0, 0, 0);
141
+ }
142
+
143
+ // Signature - Right side
144
+ doc.setFont('helvetica', 'normal');
145
+ doc.setFontSize(9);
146
+ doc.text('(Authorised Signatory)', 196, paymentY + 20, { align: 'right' });
147
+
148
+ if (download) {
149
+ doc.save(`Invoice_${transaction.bill_number}.pdf`);
150
+ } else {
151
+ const pdfBlob = doc.output('blob');
152
+ const url = URL.createObjectURL(pdfBlob);
153
+ setPdfUrl(url);
154
+ setShowPreview(true);
155
+ }
156
+ };
157
+
158
+ const handleDownload = () => {
159
+ generatePDF(true);
160
+ setShowPreview(false);
161
+ if (pdfUrl) {
162
+ URL.revokeObjectURL(pdfUrl);
163
+ }
164
+ };
165
+
166
+ const handleClose = () => {
167
+ setShowPreview(false);
168
+ if (pdfUrl) {
169
+ URL.revokeObjectURL(pdfUrl);
170
+ }
171
+ };
172
+
173
+ return (
174
+ <>
175
+ <button
176
+ onClick={() => generatePDF(false)}
177
+ className={className || "p-2 bg-white text-red-600 border border-red-600 rounded-md hover:bg-red-50 transition-colors flex items-center justify-center shadow-sm"}
178
+ title="Preview & Download PDF"
179
+ >
180
+ {children || <Eye size={16} />}
181
+ </button>
182
+
183
+ {showPreview && (
184
+ <div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
185
+ <div className="bg-white rounded-lg shadow-2xl max-w-6xl w-full max-h-[90vh] flex flex-col">
186
+ <div className="border-b border-gray-200 px-6 py-4 flex justify-between items-center">
187
+ <h2 className="text-xl font-bold text-gray-800">PDF Preview</h2>
188
+ <div className="flex gap-2">
189
+ <button
190
+ onClick={handleDownload}
191
+ className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center gap-2"
192
+ >
193
+ <Download size={18} />
194
+ Download PDF
195
+ </button>
196
+ <button
197
+ onClick={handleClose}
198
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
199
+ >
200
+ <X size={24} className="text-gray-600" />
201
+ </button>
202
+ </div>
203
+ </div>
204
+
205
+ <div className="flex-1 overflow-auto p-4 bg-gray-100">
206
+ <iframe
207
+ src={pdfUrl}
208
+ className="w-full h-full min-h-[600px] bg-white rounded shadow-lg"
209
+ title="PDF Preview"
210
+ />
211
+ </div>
212
+ </div>
213
+ </div>
214
+ )}
215
+ </>
216
+ );
217
+ };
218
+
219
+ export default PdfInvoice;
components/PrintInvoice.tsx CHANGED
@@ -1,360 +1,296 @@
1
- import React, { useRef } from 'react';
2
- import { Transaction, BillType } from '../types';
 
3
 
4
  interface PrintInvoiceProps {
5
- transaction: Transaction;
6
- businessName?: string;
7
- businessAddress?: string;
8
- businessGSTIN?: string;
9
- businessPhone?: string;
10
  }
11
 
12
- const PrintInvoice: React.FC<PrintInvoiceProps> = ({
13
- transaction,
14
- businessName = 'Pattanshetty Traders',
15
- businessAddress = 'Market Yard, Sangli',
16
- businessGSTIN = 'GSTIN: 27ABCDE1234F1Z5',
17
- businessPhone = 'फोन: 98765 43210'
18
- }) => {
19
- const printRef = useRef<HTMLDivElement>(null);
20
 
21
- // Debug logging
22
- React.useEffect(() => {
23
- console.log('PrintInvoice - Transaction:', transaction);
24
- console.log('PrintInvoice - Items count:', transaction.items?.length);
25
- console.log('PrintInvoice - Items:', transaction.items);
26
- }, [transaction]);
27
 
28
- const handlePrint = () => {
29
- const printContent = printRef.current;
30
- if (!printContent) return;
 
 
 
 
 
 
31
 
32
- const printWindow = window.open('', '', 'width=800,height=600');
33
- if (!printWindow) return;
 
34
 
35
- printWindow.document.write(`
36
- <!DOCTYPE html>
37
- <html>
38
- <head>
39
- <title>Invoice - ${transaction.bill_number}</title>
40
- <style>
41
- @media print {
42
- @page {
43
- size: A4;
44
- margin: 10mm 8mm;
45
- }
46
- body {
47
- margin: 0;
48
- padding: 0;
49
- }
50
- }
51
- * {
52
- margin: 0;
53
- padding: 0;
54
- box-sizing: border-box;
55
- }
56
- body {
57
- font-family: Arial, sans-serif;
58
- font-size: 11pt;
59
- line-height: 1.3;
60
- color: #000;
61
- }
62
- .invoice-container {
63
- width: 210mm;
64
- height: 148.5mm;
65
- padding: 8mm 10mm;
66
- page-break-after: always;
67
- }
68
- .header {
69
- padding-bottom: 8px;
70
- margin-bottom: 10px;
71
- }
72
- .header-top {
73
- display: flex;
74
- justify-content: space-between;
75
- align-items: flex-start;
76
- margin-bottom: 4px;
77
- }
78
- .business-name {
79
- font-size: 16pt;
80
- font-weight: bold;
81
- }
82
- .business-details {
83
- font-size: 9pt;
84
- color: #333;
85
- }
86
- .bill-info {
87
- text-align: right;
88
- font-size: 9pt;
89
- }
90
- .bill-info div {
91
- margin-bottom: 2px;
92
- }
93
- .party-section {
94
- display: flex;
95
- justify-content: space-between;
96
- margin: 8px 0;
97
- padding: 6px 8px;
98
- background-color: #f9f9f9;
99
- border-radius: 4px;
100
- font-size: 9pt;
101
- }
102
- .party-details {
103
- flex: 1;
104
- }
105
- .party-label {
106
- font-weight: bold;
107
- margin-bottom: 2px;
108
- font-size: 10pt;
109
- }
110
- .payment-info {
111
- text-align: right;
112
- }
113
- table {
114
- width: 100%;
115
- border-collapse: collapse;
116
- margin: 6px 0;
117
- font-size: 9pt;
118
- }
119
- th {
120
- background-color: #e8e8e8;
121
- border: 1px solid #333;
122
- padding: 3px 4px;
123
- text-align: left;
124
- font-weight: bold;
125
- font-size: 9pt;
126
- }
127
- td {
128
- border: 1px solid #666;
129
- padding: 3px 4px;
130
- font-size: 9pt;
131
- }
132
- .text-right {
133
- text-align: right;
134
- }
135
- .text-center {
136
- text-align: center;
137
- }
138
- .summary-section {
139
- margin-top: 6px;
140
- display: flex;
141
- justify-content: space-between;
142
- gap: 10px;
143
- }
144
- .summary-left {
145
- flex: 1;
146
- padding: 6px;
147
- background-color: #f9f9f9;
148
- border-radius: 4px;
149
- }
150
- .summary-right {
151
- width: 180px;
152
- border: 1px solid #333;
153
- padding: 6px 8px;
154
- background-color: #fff;
155
- }
156
- .summary-row {
157
- display: flex;
158
- justify-content: space-between;
159
- padding: 2px 0;
160
- font-size: 9pt;
161
- }
162
- .summary-row.total {
163
- border-top: 2px solid #000;
164
- margin-top: 4px;
165
- padding-top: 4px;
166
- font-weight: bold;
167
- font-size: 10pt;
168
- }
169
- .footer {
170
- margin-top: 12px;
171
- font-size: 9pt;
172
- color: #666;
173
- }
174
- .signature {
175
- text-align: right;
176
- margin-top: 20px;
177
- font-size: 10pt;
178
- }
179
- </style>
180
- </head>
181
- <body>
182
- ${printContent.innerHTML}
183
- </body>
184
- </html>
185
- `);
186
 
187
- printWindow.document.close();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  setTimeout(() => {
189
- printWindow.print();
190
- printWindow.close();
191
- }, 250);
192
- };
 
193
 
194
- const formatDate = (dateString: string) => {
195
- const date = new Date(dateString);
196
- const day = String(date.getDate()).padStart(2, '0');
197
- const month = String(date.getMonth() + 1).padStart(2, '0');
198
- const year = date.getFullYear();
199
- return `${day}/${month}/${year}`;
200
- };
 
 
201
 
202
- const billTypeLabel = transaction.bill_type === BillType.JAWAAK
203
- ? (transaction.is_return ? 'खरेदी परतावा' : 'खरेदी बिल')
204
- : (transaction.is_return ? 'विक्री परतावा' : 'विक्री बिल');
205
 
206
- return (
207
- <>
208
- {/* Hidden print content */}
209
- <div style={{ display: 'none' }}>
210
- <div ref={printRef} className="invoice-container">
211
- {/* Header */}
212
- <div className="header">
213
- <div className="header-top">
214
- <div>
215
- <div className="business-name">{businessName}</div>
216
- <div className="business-details">{businessAddress} • {businessGSTIN}</div>
217
- <div className="business-details">{businessPhone}</div>
218
- </div>
219
- <div className="bill-info">
220
- <div><strong>बिल दिनांक:</strong> {formatDate(transaction.bill_date)}</div>
221
- <div><strong>बिल प्रकार:</strong> {billTypeLabel}</div>
222
- <div><strong>बिल नंबर:</strong> {transaction.bill_number}</div>
223
- </div>
224
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  </div>
 
226
 
227
- {/* Party Details */}
228
- <div className="party-section">
229
- <div className="party-details">
230
- <div className="party-label">पार्टी माहिती</div>
231
- <div><strong>{transaction.party_name}</strong></div>
232
- <div style={{ fontSize: '9pt', color: '#666' }}>Party ID: {transaction.party_id}</div>
233
- </div>
234
- <div className="payment-info">
235
- <div><strong>दिलेली रक्कम:</strong> ₹{transaction.paid_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</div>
236
- <div><strong>बाकी रक्कम:</strong> ₹{transaction.balance_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</div>
237
- </div>
 
 
 
 
 
238
  </div>
 
239
 
240
- {/* Items Table */}
241
- <table>
242
- <thead>
243
- <tr>
244
- <th className="text-center">#</th>
245
- <th>जात / Particular</th>
246
- <th className="text-center">Lot</th>
247
- <th className="text-center">Bags</th>
248
- <th className="text-right">Gross (kg)</th>
249
- <th className="text-right">Net (kg)</th>
250
- <th className="text-right">Rate (₹)</th>
251
- <th className="text-right">Amount (₹)</th>
252
- </tr>
253
- </thead>
254
- <tbody>
255
- {transaction.items && transaction.items.length > 0 ? (
256
- transaction.items.map((item, index) => (
257
- <tr key={item.id || index}>
258
- <td className="text-center">{index + 1}</td>
259
- <td>{item.mirchi_name || 'N/A'}</td>
260
- <td className="text-center">{item.lot_id ? item.lot_id.substring(0, 8) : '-'}</td>
261
- <td className="text-center">{item.poti_count || 0}</td>
262
- <td className="text-right">{(item.gross_weight || 0).toFixed(2)}</td>
263
- <td className="text-right">{(item.net_weight || 0).toFixed(2)}</td>
264
- <td className="text-right">{(item.rate_per_kg || 0).toFixed(2)}</td>
265
- <td className="text-right">₹{(item.item_total || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
266
- </tr>
267
- ))
268
- ) : (
269
- <tr>
270
- <td colSpan={8} className="text-center">No items found</td>
271
- </tr>
272
- )}
273
- </tbody>
274
- </table>
275
 
276
- {/* Summary Section */}
277
- <div className="summary-section">
278
- <div className="summary-left">
279
- <div style={{ fontSize: '10pt', fontWeight: 'bold', marginBottom: '4px' }}>वजन सारांश</div>
280
- <div style={{ fontSize: '9pt' }}>
281
- <div>पोत्या: {transaction.items.reduce((sum, i) => sum + i.poti_count, 0)}</div>
282
- <div>एकूण वजन: {transaction.gross_weight_total.toFixed(2)} kg</div>
283
- <div>निव्वळ वजन: {transaction.net_weight_total.toFixed(2)} kg</div>
284
- </div>
285
- </div>
286
- <div className="summary-right">
287
- <div className="summary-row">
288
- <span>मूळ रक्कम:</span>
289
- <span>₹{transaction.subtotal.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
290
- </div>
291
- {transaction.expenses.cess_amount > 0 && (
292
- <div className="summary-row">
293
- <span>सेस ({transaction.expenses.cess_percent}%):</span>
294
- <span>₹{transaction.expenses.cess_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
295
- </div>
296
- )}
297
- {transaction.expenses.adat_amount > 0 && (
298
- <div className="summary-row">
299
- <span>अडत ({transaction.expenses.adat_percent}%):</span>
300
- <span>₹{transaction.expenses.adat_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
301
- </div>
302
- )}
303
- {transaction.expenses.poti_amount > 0 && (
304
- <div className="summary-row">
305
- <span>पोती:</span>
306
- <span>₹{transaction.expenses.poti_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
307
- </div>
308
- )}
309
- {transaction.expenses.hamali_amount > 0 && (
310
- <div className="summary-row">
311
- <span>हमाली:</span>
312
- <span>₹{transaction.expenses.hamali_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
313
- </div>
314
- )}
315
- {transaction.expenses.packaging_hamali_amount > 0 && (
316
- <div className="summary-row">
317
- <span>पॅकेजिंग हमाली:</span>
318
- <span>₹{transaction.expenses.packaging_hamali_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
319
- </div>
320
- )}
321
- {transaction.expenses.gaadi_bharni > 0 && (
322
- <div className="summary-row">
323
- <span>गाडी भरणी:</span>
324
- <span>₹{transaction.expenses.gaadi_bharni.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
325
- </div>
326
- )}
327
- <div className="summary-row total">
328
- <span>एकूण रक्कम:</span>
329
- <span>₹{transaction.total_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
330
  </div>
331
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  </div>
333
 
334
- {/* Footer */}
335
- <div className="footer">
336
- नोंद: कृपया माल प्राप्त झाल्यानंतर वजन व रक्कम तपासा. कोणतीही तक्रार 24 तासात नोंदवा.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  </div>
 
338
 
339
- <div className="signature">
340
- (अधिकृत सही)
 
 
 
 
 
 
 
341
  </div>
 
 
342
  </div>
 
343
  </div>
344
-
345
- {/* Print Button */}
346
- <button
347
- onClick={handlePrint}
348
- className="px-3 py-1 border border-teal-600 text-teal-600 rounded-md text-xs font-medium hover:bg-teal-50 transition-colors inline-flex items-center gap-1"
349
- title="Print Invoice"
350
- >
351
- <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
352
- <path fillRule="evenodd" d="M5 4v3H4a2 2 0 00-2 2v3a2 2 0 002 2h1v2a2 2 0 002 2h6a2 2 0 002-2v-2h1a2 2 0 002-2V9a2 2 0 00-2-2h-1V4a2 2 0 00-2-2H7a2 2 0 00-2 2zm8 0H7v3h6V4zm0 8H7v4h6v-4z" clipRule="evenodd" />
353
- </svg>
354
- Print
355
- </button>
356
- </>
357
- );
358
  };
359
 
360
- export default PrintInvoice;
 
1
+ import React, { useState } from 'react';
2
+ import { Transaction } from '../types';
3
+ import { Printer, X } from 'lucide-react';
4
 
5
  interface PrintInvoiceProps {
6
+ transaction: Transaction;
7
+ className?: string;
 
 
 
8
  }
9
 
10
+ const PrintInvoice: React.FC<PrintInvoiceProps> = ({ transaction, className }) => {
11
+ const [showPreview, setShowPreview] = useState(false);
 
 
 
 
 
 
12
 
13
+ const formatINR = (v: number) => `₹${v.toFixed(2)}`;
14
+ const formatDate = (dateStr: string) => {
15
+ const date = new Date(dateStr);
16
+ return date.toLocaleDateString('en-IN', { day: '2-digit', month: '2-digit', year: 'numeric' });
17
+ };
 
18
 
19
+ // Calculate totals
20
+ const subtotal = transaction.subtotal || 0;
21
+ const packing = transaction.expenses?.poti_amount || 0;
22
+ const cess = transaction.expenses?.cess_amount || 0;
23
+ const adat = transaction.expenses?.adat_amount || 0;
24
+ const hamali = (transaction.expenses?.hamali_amount || 0) + (transaction.expenses?.packaging_hamali_amount || 0);
25
+ const grandTotal = transaction.total_amount || 0;
26
+ const paidAmount = transaction.paid_amount || 0;
27
+ const balanceAmount = transaction.balance_amount || 0;
28
 
29
+ const handlePrint = () => {
30
+ const printContent = document.getElementById('invoice-content');
31
+ if (!printContent) return;
32
 
33
+ const iframe = document.createElement('iframe');
34
+ iframe.style.position = 'absolute';
35
+ iframe.style.width = '0';
36
+ iframe.style.height = '0';
37
+ iframe.style.border = 'none';
38
+ document.body.appendChild(iframe);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ const doc = iframe.contentWindow?.document;
41
+ if (doc) {
42
+ doc.open();
43
+ doc.write(`
44
+ <html>
45
+ <head>
46
+ <title>Invoice-${transaction.bill_number}</title>
47
+ <style>
48
+ /* Reset margins to absolute zero */
49
+ @page { size: A4; margin: 0; }
50
+ body { margin: 0; padding: 0; background-color: white; font-family: Helvetica, Arial, sans-serif; }
51
+
52
+ /* Overwrite the container style for printing */
53
+ #invoice-container-inner {
54
+ width: 210mm !important;
55
+ /* Reducing slightly from 297mm to 295mm prevents the 'spillover' blank page */
56
+ min-height: 295mm !important;
57
+ padding: 15mm !important;
58
+ margin: 0 auto !important;
59
+ box-sizing: border-box !important;
60
+ overflow: hidden !important; /* Cut off any rogue pixels */
61
+ }
62
+
63
+ /* Helper to hide non-print elements if any sneak in */
64
+ .print-hidden { display: none !important; }
65
+ </style>
66
+ </head>
67
+ <body>
68
+ ${printContent.innerHTML}
69
+ </body>
70
+ </html>
71
+ `);
72
+ doc.close();
73
+
74
+ iframe.contentWindow?.focus();
75
+ setTimeout(() => {
76
+ iframe.contentWindow?.print();
77
  setTimeout(() => {
78
+ document.body.removeChild(iframe);
79
+ }, 1000);
80
+ }, 500);
81
+ }
82
+ };
83
 
84
+ return (
85
+ <>
86
+ <button
87
+ onClick={() => setShowPreview(true)}
88
+ className={className || "p-2 bg-white text-teal-600 border border-teal-600 rounded-md hover:bg-teal-50 transition-colors flex items-center justify-center shadow-sm"}
89
+ title="Print Invoice"
90
+ >
91
+ <Printer size={16} />
92
+ </button>
93
 
94
+ {showPreview && (
95
+ <div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-2 sm:p-4">
96
+ <div className="bg-white rounded-lg shadow-2xl max-w-4xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden flex flex-col">
97
 
98
+ {/* Header */}
99
+ <div className="sticky top-0 bg-white border-b border-gray-200 px-3 py-3 sm:px-6 sm:py-4 flex justify-between items-center z-10 shrink-0">
100
+ <h2 className="text-lg sm:text-xl font-bold text-gray-800 truncate mr-2">Invoice Preview</h2>
101
+ <div className="flex gap-2 items-center">
102
+ <button
103
+ onClick={handlePrint}
104
+ className="px-3 py-1.5 sm:px-4 sm:py-2 text-sm sm:text-base bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-1 sm:gap-2"
105
+ >
106
+ <Printer size={16} className="sm:w-[18px] sm:h-[18px]" />
107
+ <span>Print</span>
108
+ </button>
109
+ <button
110
+ onClick={() => setShowPreview(false)}
111
+ className="p-1.5 sm:p-2 hover:bg-gray-100 rounded-lg transition-colors"
112
+ >
113
+ <X size={20} className="text-gray-600 sm:w-6 sm:h-6" />
114
+ </button>
115
+ </div>
116
+ </div>
117
+
118
+ {/* Scrollable Area */}
119
+ <div className="overflow-auto flex-1 p-2 sm:p-8 bg-gray-100">
120
+ <div className="mb-4 text-xs text-gray-500 text-center sm:hidden">
121
+ Scroll horizontally to view full invoice
122
+ </div>
123
+
124
+ {/* Wrapper for ID targeting */}
125
+ <div id="invoice-content">
126
+ <div
127
+ id="invoice-container-inner"
128
+ className="bg-white mx-auto shadow-sm"
129
+ style={{
130
+ width: '210mm',
131
+ minHeight: '297mm', /* This stays 297mm for screen preview, but Print overrides it to 295mm */
132
+ padding: '15mm',
133
+ margin: 'auto',
134
+ fontFamily: 'Helvetica, Arial, sans-serif',
135
+ fontSize: '12px',
136
+ color: '#000',
137
+ backgroundColor: '#fff',
138
+ boxSizing: 'border-box',
139
+ position: 'relative'
140
+ }}
141
+ >
142
+
143
+ {/* Invoice Header */}
144
+ <div style={{ borderBottom: '2px solid #333', paddingBottom: '10px', marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
145
+ <div>
146
+ <h1 style={{ margin: 0, fontSize: '24px', fontWeight: 'bold', letterSpacing: '1px' }}>INVOICE</h1>
147
+ </div>
148
+ <div style={{ textAlign: 'right' }}>
149
+ <div style={{ fontSize: '14px', fontWeight: 'bold' }}>Bill No: {transaction.bill_number}</div>
150
+ <div style={{ fontSize: '12px', marginTop: '4px' }}>Date: {formatDate(transaction.bill_date)}</div>
151
  </div>
152
+ </div>
153
 
154
+ {/* Info Section */}
155
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
156
+ <div style={{ width: '55%', textAlign: 'left' }}>
157
+ <div style={{
158
+ textTransform: 'uppercase',
159
+ fontSize: '10px',
160
+ color: '#555',
161
+ fontWeight: 'bold',
162
+ marginBottom: '4px',
163
+ borderBottom: '1px solid #eee',
164
+ paddingBottom: '2px',
165
+ width: '100%'
166
+ }}>Billed To</div>
167
+
168
+ <div style={{ fontSize: '14px', fontWeight: 'bold', marginTop: '5px' }}>{transaction.party_name}</div>
169
+ <div style={{ marginTop: '2px' }}>Phone: {transaction.party_phone || '-'}</div>
170
  </div>
171
+ </div>
172
 
173
+ {/* Table */}
174
+ <table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: '20px' }}>
175
+ <thead>
176
+ <tr style={{ backgroundColor: '#f3f4f6' }}>
177
+ <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'center', width: '5%' }}>#</th>
178
+ <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'left', width: '40%' }}>Item Description</th>
179
+ <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'center', width: '10%' }}>Bags</th>
180
+ <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'center', width: '15%' }}>Net Weight</th>
181
+ <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'right', width: '15%' }}>Rate</th>
182
+ <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'right', width: '15%' }}>Amount</th>
183
+ </tr>
184
+ </thead>
185
+ <tbody>
186
+ {transaction.items.map((item, idx) => {
187
+ const potiWeights = Array.isArray(item.poti_weights) ? item.poti_weights.join(', ') : (typeof item.poti_weights === 'string' ? item.poti_weights : '-');
188
+ const amount = item.net_weight * item.rate_per_kg;
189
+ return (
190
+ <tr key={idx}>
191
+ <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'center' }}>{idx + 1}</td>
192
+ <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'left' }}>
193
+ <div style={{ fontWeight: 'bold' }}>{item.mirchi_name}</div>
194
+ <div style={{ fontSize: '9px', color: '#666', marginTop: '2px', lineHeight: '1.2' }}>
195
+ Weights: {potiWeights}
196
+ </div>
197
+ </td>
198
+ <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'center' }}>{item.poti_count}</td>
199
+ <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'center' }}>{item.net_weight} kg</td>
200
+ <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'right' }}>{formatINR(item.rate_per_kg)}</td>
201
+ <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'right' }}>{formatINR(amount)}</td>
202
+ </tr>
203
+ );
204
+ })}
205
+ </tbody>
206
+ </table>
 
207
 
208
+ {/* Totals Section */}
209
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
210
+ <div style={{ width: '50%', paddingRight: '20px', textAlign: 'left' }}>
211
+ <div style={{ border: '1px solid #ccc', padding: '10px', borderRadius: '4px' }}>
212
+ <div style={{ fontWeight: 'bold', borderBottom: '1px solid #eee', marginBottom: '8px', paddingBottom: '4px' }}>Payment Status</div>
213
+ {transaction.payments && transaction.payments.length > 0 ? (
214
+ <>
215
+ {transaction.payments.map((payment, idx) => (
216
+ <div key={idx} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '11px', marginBottom: '2px' }}>
217
+ <span>{payment.mode === 'cash' ? 'Cash' : payment.mode === 'online' ? 'Online' : payment.mode}:</span>
218
+ <span>{formatINR(payment.amount)}</span>
219
+ </div>
220
+ ))}
221
+ <div style={{ borderTop: '1px dashed #ccc', marginTop: '6px', paddingTop: '6px', display: 'flex', justifyContent: 'space-between', fontWeight: 'bold' }}>
222
+ <span>Total Paid:</span>
223
+ <span>{formatINR(paidAmount)}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  </div>
225
+ </>
226
+ ) : (
227
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
228
+ <span>Total Paid:</span>
229
+ <span>{formatINR(paidAmount)}</span>
230
+ </div>
231
+ )}
232
+
233
+ {balanceAmount > 0 && (
234
+ <div style={{ marginTop: '8px', color: '#c62828', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', fontSize: '13px' }}>
235
+ <span>Due Amount:</span>
236
+ <span>{formatINR(balanceAmount)}</span>
237
+ </div>
238
+ )}
239
+ </div>
240
  </div>
241
 
242
+ <div style={{ width: '45%' }}>
243
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
244
+ <tbody>
245
+ <tr>
246
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee' }}>Sub Total</td>
247
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee', fontWeight: 'bold' }}>{formatINR(subtotal)}</td>
248
+ </tr>
249
+ <tr>
250
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Packing Expenses</td>
251
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(packing)}</td>
252
+ </tr>
253
+ <tr>
254
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>CESS</td>
255
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(cess)}</td>
256
+ </tr>
257
+ <tr>
258
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Adat</td>
259
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(adat)}</td>
260
+ </tr>
261
+ <tr>
262
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Hamali</td>
263
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(hamali)}</td>
264
+ </tr>
265
+ <tr style={{ backgroundColor: '#f3f4f6' }}>
266
+ <th style={{ padding: '8px 4px', textAlign: 'left', borderTop: '2px solid #000', fontSize: '14px' }}>Grand Total</th>
267
+ <th style={{ padding: '8px 4px', textAlign: 'right', borderTop: '2px solid #000', fontSize: '14px' }}>{formatINR(grandTotal)}</th>
268
+ </tr>
269
+ </tbody>
270
+ </table>
271
  </div>
272
+ </div>
273
 
274
+ {/* Signatures */}
275
+ <div style={{ marginTop: '50px', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
276
+ <div style={{ textAlign: 'left', fontSize: '10px', color: '#666' }}>
277
+ <p>Thank you for your business.</p>
278
+ </div>
279
+ <div style={{ textAlign: 'center' }}>
280
+ <div style={{ marginBottom: '40px', fontSize: '10px' }}>For Authorised Signatory</div>
281
+ <div style={{ borderTop: '1px solid #000', width: '150px', margin: 'auto' }}></div>
282
+ <div style={{ fontSize: '10px', marginTop: '4px' }}>(Signature)</div>
283
  </div>
284
+ </div>
285
+
286
  </div>
287
+ </div>
288
  </div>
289
+ </div>
290
+ </div>
291
+ )}
292
+ </>
293
+ );
 
 
 
 
 
 
 
 
 
294
  };
295
 
296
+ export default PrintInvoice;
context/PWAContext.tsx ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { createContext, useContext, useEffect, useState } from 'react';
2
+
3
+ interface PWAContextType {
4
+ deferredPrompt: any;
5
+ isInstallable: boolean;
6
+ isIOS: boolean;
7
+ isStandalone: boolean;
8
+ installApp: () => Promise<void>;
9
+ }
10
+
11
+ const PWAContext = createContext<PWAContextType | undefined>(undefined);
12
+
13
+ export const PWAProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
14
+ const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
15
+ const [isInstallable, setIsInstallable] = useState(false);
16
+ const [isIOS, setIsIOS] = useState(false);
17
+ const [isStandalone, setIsStandalone] = useState(false);
18
+
19
+ useEffect(() => {
20
+ // Check if already installed
21
+ const standalone = window.matchMedia('(display-mode: standalone)').matches ||
22
+ (window.navigator as any).standalone ||
23
+ document.referrer.includes('android-app://');
24
+
25
+ setIsStandalone(standalone);
26
+
27
+ // Check if iOS
28
+ const ios = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
29
+ setIsIOS(ios);
30
+
31
+ // Listen for beforeinstallprompt event
32
+ const handleBeforeInstallPrompt = (e: Event) => {
33
+ // Prevent the mini-infobar from appearing on mobile
34
+ e.preventDefault();
35
+ // Stash the event so it can be triggered later.
36
+ setDeferredPrompt(e);
37
+ setIsInstallable(true);
38
+ console.log('✅ PWA Install Prompt captured');
39
+ };
40
+
41
+ window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
42
+
43
+ return () => {
44
+ window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
45
+ };
46
+ }, []);
47
+
48
+ const installApp = async () => {
49
+ if (!deferredPrompt) {
50
+ return;
51
+ }
52
+
53
+ // Show the install prompt
54
+ deferredPrompt.prompt();
55
+
56
+ // Wait for the user to respond to the prompt
57
+ const { outcome } = await deferredPrompt.userChoice;
58
+
59
+ if (outcome === 'accepted') {
60
+ console.log('User accepted the install prompt');
61
+ } else {
62
+ console.log('User dismissed the install prompt');
63
+ }
64
+
65
+ // We've used the prompt, so clear it
66
+ setDeferredPrompt(null);
67
+ setIsInstallable(false);
68
+ };
69
+
70
+ return (
71
+ <PWAContext.Provider value={{ deferredPrompt, isInstallable, isIOS, isStandalone, installApp }}>
72
+ {children}
73
+ </PWAContext.Provider>
74
+ );
75
+ };
76
+
77
+ export const usePWA = () => {
78
+ const context = useContext(PWAContext);
79
+ if (context === undefined) {
80
+ throw new Error('usePWA must be used within a PWAProvider');
81
+ }
82
+ return context;
83
+ };
index.html CHANGED
@@ -1,45 +1,52 @@
1
  <!DOCTYPE html>
2
  <html lang="mr">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Pattanshetty Inventory</title>
8
-
9
- <!-- PWA Manifest -->
10
- <link rel="manifest" href="/manifest.json" />
11
- <meta name="theme-color" content="#0d9488" />
12
- <meta name="apple-mobile-web-app-capable" content="yes" />
13
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
14
- <meta name="apple-mobile-web-app-title" content="Pattanshetty" />
15
- <link rel="apple-touch-icon" href="/icon-192.png" />
16
- <script src="https://cdn.tailwindcss.com"></script>
17
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
18
- <style>
19
- body { font-family: 'Inter', sans-serif; }
20
-
21
- /* Hide scrollbar for Chrome, Safari and Opera */
22
- .no-scrollbar::-webkit-scrollbar {
23
- display: none;
24
- }
25
- /* Hide scrollbar for IE, Edge and Firefox */
26
- .no-scrollbar {
27
- -ms-overflow-style: none; /* IE and Edge */
28
- scrollbar-width: none; /* Firefox */
29
- }
30
-
31
- /* Chrome, Safari, Edge, Opera - Hide number input arrows */
32
- input::-webkit-outer-spin-button,
33
- input::-webkit-inner-spin-button {
34
- -webkit-appearance: none;
35
- margin: 0;
36
- }
37
-
38
- /* Firefox - Hide number input arrows */
39
- input[type=number] {
40
- -moz-appearance: textfield;
41
- }
42
- </style>
 
 
 
 
 
 
 
43
  <script type="importmap">
44
  {
45
  "imports": {
@@ -52,10 +59,37 @@
52
  }
53
  }
54
  </script>
55
- <link rel="stylesheet" href="/index.css">
56
  </head>
57
- <body class="bg-gray-50 text-gray-900">
58
- <div id="root"></div>
 
59
  <script type="module" src="/index.tsx"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  </body>
 
61
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="mr">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>Pattanshetty Inventory</title>
9
+
10
+ <!-- PWA Manifest -->
11
+ <link rel="manifest" href="/manifest.json" />
12
+ <meta name="theme-color" content="#0d9488" />
13
+ <meta name="mobile-web-app-capable" content="yes" />
14
+ <meta name="apple-mobile-web-app-capable" content="yes" />
15
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
16
+ <meta name="apple-mobile-web-app-title" content="Pattanshetty" />
17
+ <link rel="apple-touch-icon" href="/icon-192.png" />
18
+ <script src="https://cdn.tailwindcss.com"></script>
19
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
20
+ <style>
21
+ body {
22
+ font-family: 'Inter', sans-serif;
23
+ }
24
+
25
+ /* Hide scrollbar for Chrome, Safari and Opera */
26
+ .no-scrollbar::-webkit-scrollbar {
27
+ display: none;
28
+ }
29
+
30
+ /* Hide scrollbar for IE, Edge and Firefox */
31
+ .no-scrollbar {
32
+ -ms-overflow-style: none;
33
+ /* IE and Edge */
34
+ scrollbar-width: none;
35
+ /* Firefox */
36
+ }
37
+
38
+ /* Chrome, Safari, Edge, Opera - Hide number input arrows */
39
+ input::-webkit-outer-spin-button,
40
+ input::-webkit-inner-spin-button {
41
+ -webkit-appearance: none;
42
+ margin: 0;
43
+ }
44
+
45
+ /* Firefox - Hide number input arrows */
46
+ input[type=number] {
47
+ -moz-appearance: textfield;
48
+ }
49
+ </style>
50
  <script type="importmap">
51
  {
52
  "imports": {
 
59
  }
60
  }
61
  </script>
62
+ <link rel="stylesheet" href="/index.css">
63
  </head>
64
+
65
+ <body class="bg-gray-50 text-gray-900">
66
+ <div id="root"></div>
67
  <script type="module" src="/index.tsx"></script>
68
+
69
+ <!-- Service Worker Registration -->
70
+ <script>
71
+ if ('serviceWorker' in navigator) {
72
+ window.addEventListener('load', () => {
73
+ navigator.serviceWorker.register('/service-worker.js')
74
+ .then((registration) => {
75
+ console.log('[SW] Registered successfully:', registration.scope);
76
+
77
+ // Check for updates
78
+ registration.addEventListener('updatefound', () => {
79
+ const newWorker = registration.installing;
80
+ newWorker.addEventListener('statechange', () => {
81
+ if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
82
+ console.log('[SW] New version available! Refresh to update.');
83
+ }
84
+ });
85
+ });
86
+ })
87
+ .catch((error) => {
88
+ console.error('[SW] Registration failed:', error);
89
+ });
90
+ });
91
+ }
92
+ </script>
93
  </body>
94
+
95
  </html>
package-lock.json CHANGED
@@ -8,6 +8,9 @@
8
  "name": "mirchi-vyapar-manager",
9
  "version": "0.0.0",
10
  "dependencies": {
 
 
 
11
  "lucide-react": "^0.554.0",
12
  "react": "^19.2.0",
13
  "react-dom": "^19.2.0",
@@ -256,6 +259,15 @@
256
  "@babel/core": "^7.0.0-0"
257
  }
258
  },
 
 
 
 
 
 
 
 
 
259
  "node_modules/@babel/template": {
260
  "version": "7.27.2",
261
  "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -1476,6 +1488,26 @@
1476
  "undici-types": "~6.21.0"
1477
  }
1478
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1479
  "node_modules/@types/use-sync-external-store": {
1480
  "version": "0.0.6",
1481
  "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@@ -1582,6 +1614,15 @@
1582
  "license": "MIT",
1583
  "peer": true
1584
  },
 
 
 
 
 
 
 
 
 
1585
  "node_modules/baseline-browser-mapping": {
1586
  "version": "2.8.30",
1587
  "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz",
@@ -1668,6 +1709,26 @@
1668
  ],
1669
  "license": "CC-BY-4.0"
1670
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1671
  "node_modules/cfb": {
1672
  "version": "1.2.2",
1673
  "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
@@ -1759,6 +1820,18 @@
1759
  "node": ">=18"
1760
  }
1761
  },
 
 
 
 
 
 
 
 
 
 
 
 
1762
  "node_modules/crc-32": {
1763
  "version": "1.2.2",
1764
  "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
@@ -1786,6 +1859,15 @@
1786
  "node": ">= 8"
1787
  }
1788
  },
 
 
 
 
 
 
 
 
 
1789
  "node_modules/d3-array": {
1790
  "version": "3.2.4",
1791
  "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
@@ -1937,6 +2019,16 @@
1937
  "license": "MIT",
1938
  "peer": true
1939
  },
 
 
 
 
 
 
 
 
 
 
1940
  "node_modules/electron-to-chromium": {
1941
  "version": "1.5.259",
1942
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz",
@@ -2212,6 +2304,17 @@
2212
  "license": "MIT",
2213
  "peer": true
2214
  },
 
 
 
 
 
 
 
 
 
 
 
2215
  "node_modules/fdir": {
2216
  "version": "6.5.0",
2217
  "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -2230,6 +2333,12 @@
2230
  }
2231
  }
2232
  },
 
 
 
 
 
 
2233
  "node_modules/file-entry-cache": {
2234
  "version": "8.0.0",
2235
  "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -2351,6 +2460,19 @@
2351
  "node": ">=8"
2352
  }
2353
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
2354
  "node_modules/ignore": {
2355
  "version": "5.3.2",
2356
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2407,6 +2529,12 @@
2407
  "node": ">=12"
2408
  }
2409
  },
 
 
 
 
 
 
2410
  "node_modules/is-extglob": {
2411
  "version": "2.1.1",
2412
  "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2504,6 +2632,32 @@
2504
  "node": ">=6"
2505
  }
2506
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2507
  "node_modules/keyv": {
2508
  "version": "4.5.4",
2509
  "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -2672,6 +2826,12 @@
2672
  "url": "https://github.com/sponsors/sindresorhus"
2673
  }
2674
  },
 
 
 
 
 
 
2675
  "node_modules/parent-module": {
2676
  "version": "1.0.1",
2677
  "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -2705,6 +2865,13 @@
2705
  "node": ">=8"
2706
  }
2707
  },
 
 
 
 
 
 
 
2708
  "node_modules/picocolors": {
2709
  "version": "1.1.1",
2710
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -2774,6 +2941,16 @@
2774
  "node": ">=6"
2775
  }
2776
  },
 
 
 
 
 
 
 
 
 
 
2777
  "node_modules/react": {
2778
  "version": "19.2.0",
2779
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
@@ -2919,6 +3096,13 @@
2919
  "redux": "^5.0.0"
2920
  }
2921
  },
 
 
 
 
 
 
 
2922
  "node_modules/reselect": {
2923
  "version": "5.1.1",
2924
  "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
@@ -2935,6 +3119,16 @@
2935
  "node": ">=4"
2936
  }
2937
  },
 
 
 
 
 
 
 
 
 
 
2938
  "node_modules/rollup": {
2939
  "version": "4.53.3",
2940
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
@@ -3044,6 +3238,16 @@
3044
  "node": ">=0.8"
3045
  }
3046
  },
 
 
 
 
 
 
 
 
 
 
3047
  "node_modules/strip-json-comments": {
3048
  "version": "3.1.1",
3049
  "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -3070,6 +3274,25 @@
3070
  "node": ">=8"
3071
  }
3072
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3073
  "node_modules/tiny-invariant": {
3074
  "version": "1.3.3",
3075
  "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -3177,6 +3400,15 @@
3177
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
3178
  }
3179
  },
 
 
 
 
 
 
 
 
 
3180
  "node_modules/victory-vendor": {
3181
  "version": "37.3.6",
3182
  "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
 
8
  "name": "mirchi-vyapar-manager",
9
  "version": "0.0.0",
10
  "dependencies": {
11
+ "html2canvas": "^1.4.1",
12
+ "jspdf": "^3.0.4",
13
+ "jspdf-autotable": "^5.0.2",
14
  "lucide-react": "^0.554.0",
15
  "react": "^19.2.0",
16
  "react-dom": "^19.2.0",
 
259
  "@babel/core": "^7.0.0-0"
260
  }
261
  },
262
+ "node_modules/@babel/runtime": {
263
+ "version": "7.28.4",
264
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
265
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
266
+ "license": "MIT",
267
+ "engines": {
268
+ "node": ">=6.9.0"
269
+ }
270
+ },
271
  "node_modules/@babel/template": {
272
  "version": "7.27.2",
273
  "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
 
1488
  "undici-types": "~6.21.0"
1489
  }
1490
  },
1491
+ "node_modules/@types/pako": {
1492
+ "version": "2.0.4",
1493
+ "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
1494
+ "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
1495
+ "license": "MIT"
1496
+ },
1497
+ "node_modules/@types/raf": {
1498
+ "version": "3.4.3",
1499
+ "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
1500
+ "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
1501
+ "license": "MIT",
1502
+ "optional": true
1503
+ },
1504
+ "node_modules/@types/trusted-types": {
1505
+ "version": "2.0.7",
1506
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
1507
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
1508
+ "license": "MIT",
1509
+ "optional": true
1510
+ },
1511
  "node_modules/@types/use-sync-external-store": {
1512
  "version": "0.0.6",
1513
  "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
 
1614
  "license": "MIT",
1615
  "peer": true
1616
  },
1617
+ "node_modules/base64-arraybuffer": {
1618
+ "version": "1.0.2",
1619
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
1620
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
1621
+ "license": "MIT",
1622
+ "engines": {
1623
+ "node": ">= 0.6.0"
1624
+ }
1625
+ },
1626
  "node_modules/baseline-browser-mapping": {
1627
  "version": "2.8.30",
1628
  "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz",
 
1709
  ],
1710
  "license": "CC-BY-4.0"
1711
  },
1712
+ "node_modules/canvg": {
1713
+ "version": "3.0.11",
1714
+ "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
1715
+ "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
1716
+ "license": "MIT",
1717
+ "optional": true,
1718
+ "dependencies": {
1719
+ "@babel/runtime": "^7.12.5",
1720
+ "@types/raf": "^3.4.0",
1721
+ "core-js": "^3.8.3",
1722
+ "raf": "^3.4.1",
1723
+ "regenerator-runtime": "^0.13.7",
1724
+ "rgbcolor": "^1.0.1",
1725
+ "stackblur-canvas": "^2.0.0",
1726
+ "svg-pathdata": "^6.0.3"
1727
+ },
1728
+ "engines": {
1729
+ "node": ">=10.0.0"
1730
+ }
1731
+ },
1732
  "node_modules/cfb": {
1733
  "version": "1.2.2",
1734
  "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
 
1820
  "node": ">=18"
1821
  }
1822
  },
1823
+ "node_modules/core-js": {
1824
+ "version": "3.47.0",
1825
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
1826
+ "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
1827
+ "hasInstallScript": true,
1828
+ "license": "MIT",
1829
+ "optional": true,
1830
+ "funding": {
1831
+ "type": "opencollective",
1832
+ "url": "https://opencollective.com/core-js"
1833
+ }
1834
+ },
1835
  "node_modules/crc-32": {
1836
  "version": "1.2.2",
1837
  "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
 
1859
  "node": ">= 8"
1860
  }
1861
  },
1862
+ "node_modules/css-line-break": {
1863
+ "version": "2.1.0",
1864
+ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
1865
+ "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
1866
+ "license": "MIT",
1867
+ "dependencies": {
1868
+ "utrie": "^1.0.2"
1869
+ }
1870
+ },
1871
  "node_modules/d3-array": {
1872
  "version": "3.2.4",
1873
  "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
 
2019
  "license": "MIT",
2020
  "peer": true
2021
  },
2022
+ "node_modules/dompurify": {
2023
+ "version": "3.3.0",
2024
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
2025
+ "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==",
2026
+ "license": "(MPL-2.0 OR Apache-2.0)",
2027
+ "optional": true,
2028
+ "optionalDependencies": {
2029
+ "@types/trusted-types": "^2.0.7"
2030
+ }
2031
+ },
2032
  "node_modules/electron-to-chromium": {
2033
  "version": "1.5.259",
2034
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz",
 
2304
  "license": "MIT",
2305
  "peer": true
2306
  },
2307
+ "node_modules/fast-png": {
2308
+ "version": "6.4.0",
2309
+ "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
2310
+ "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
2311
+ "license": "MIT",
2312
+ "dependencies": {
2313
+ "@types/pako": "^2.0.3",
2314
+ "iobuffer": "^5.3.2",
2315
+ "pako": "^2.1.0"
2316
+ }
2317
+ },
2318
  "node_modules/fdir": {
2319
  "version": "6.5.0",
2320
  "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
 
2333
  }
2334
  }
2335
  },
2336
+ "node_modules/fflate": {
2337
+ "version": "0.8.2",
2338
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
2339
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
2340
+ "license": "MIT"
2341
+ },
2342
  "node_modules/file-entry-cache": {
2343
  "version": "8.0.0",
2344
  "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
 
2460
  "node": ">=8"
2461
  }
2462
  },
2463
+ "node_modules/html2canvas": {
2464
+ "version": "1.4.1",
2465
+ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
2466
+ "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
2467
+ "license": "MIT",
2468
+ "dependencies": {
2469
+ "css-line-break": "^2.1.0",
2470
+ "text-segmentation": "^1.0.3"
2471
+ },
2472
+ "engines": {
2473
+ "node": ">=8.0.0"
2474
+ }
2475
+ },
2476
  "node_modules/ignore": {
2477
  "version": "5.3.2",
2478
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
 
2529
  "node": ">=12"
2530
  }
2531
  },
2532
+ "node_modules/iobuffer": {
2533
+ "version": "5.4.0",
2534
+ "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
2535
+ "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
2536
+ "license": "MIT"
2537
+ },
2538
  "node_modules/is-extglob": {
2539
  "version": "2.1.1",
2540
  "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
 
2632
  "node": ">=6"
2633
  }
2634
  },
2635
+ "node_modules/jspdf": {
2636
+ "version": "3.0.4",
2637
+ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
2638
+ "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
2639
+ "license": "MIT",
2640
+ "dependencies": {
2641
+ "@babel/runtime": "^7.28.4",
2642
+ "fast-png": "^6.2.0",
2643
+ "fflate": "^0.8.1"
2644
+ },
2645
+ "optionalDependencies": {
2646
+ "canvg": "^3.0.11",
2647
+ "core-js": "^3.6.0",
2648
+ "dompurify": "^3.2.4",
2649
+ "html2canvas": "^1.0.0-rc.5"
2650
+ }
2651
+ },
2652
+ "node_modules/jspdf-autotable": {
2653
+ "version": "5.0.2",
2654
+ "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
2655
+ "integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
2656
+ "license": "MIT",
2657
+ "peerDependencies": {
2658
+ "jspdf": "^2 || ^3"
2659
+ }
2660
+ },
2661
  "node_modules/keyv": {
2662
  "version": "4.5.4",
2663
  "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
 
2826
  "url": "https://github.com/sponsors/sindresorhus"
2827
  }
2828
  },
2829
+ "node_modules/pako": {
2830
+ "version": "2.1.0",
2831
+ "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
2832
+ "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
2833
+ "license": "(MIT AND Zlib)"
2834
+ },
2835
  "node_modules/parent-module": {
2836
  "version": "1.0.1",
2837
  "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
 
2865
  "node": ">=8"
2866
  }
2867
  },
2868
+ "node_modules/performance-now": {
2869
+ "version": "2.1.0",
2870
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
2871
+ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
2872
+ "license": "MIT",
2873
+ "optional": true
2874
+ },
2875
  "node_modules/picocolors": {
2876
  "version": "1.1.1",
2877
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
 
2941
  "node": ">=6"
2942
  }
2943
  },
2944
+ "node_modules/raf": {
2945
+ "version": "3.4.1",
2946
+ "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
2947
+ "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
2948
+ "license": "MIT",
2949
+ "optional": true,
2950
+ "dependencies": {
2951
+ "performance-now": "^2.1.0"
2952
+ }
2953
+ },
2954
  "node_modules/react": {
2955
  "version": "19.2.0",
2956
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
 
3096
  "redux": "^5.0.0"
3097
  }
3098
  },
3099
+ "node_modules/regenerator-runtime": {
3100
+ "version": "0.13.11",
3101
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
3102
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
3103
+ "license": "MIT",
3104
+ "optional": true
3105
+ },
3106
  "node_modules/reselect": {
3107
  "version": "5.1.1",
3108
  "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
 
3119
  "node": ">=4"
3120
  }
3121
  },
3122
+ "node_modules/rgbcolor": {
3123
+ "version": "1.0.1",
3124
+ "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
3125
+ "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
3126
+ "license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
3127
+ "optional": true,
3128
+ "engines": {
3129
+ "node": ">= 0.8.15"
3130
+ }
3131
+ },
3132
  "node_modules/rollup": {
3133
  "version": "4.53.3",
3134
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
 
3238
  "node": ">=0.8"
3239
  }
3240
  },
3241
+ "node_modules/stackblur-canvas": {
3242
+ "version": "2.7.0",
3243
+ "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
3244
+ "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
3245
+ "license": "MIT",
3246
+ "optional": true,
3247
+ "engines": {
3248
+ "node": ">=0.1.14"
3249
+ }
3250
+ },
3251
  "node_modules/strip-json-comments": {
3252
  "version": "3.1.1",
3253
  "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
 
3274
  "node": ">=8"
3275
  }
3276
  },
3277
+ "node_modules/svg-pathdata": {
3278
+ "version": "6.0.3",
3279
+ "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
3280
+ "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
3281
+ "license": "MIT",
3282
+ "optional": true,
3283
+ "engines": {
3284
+ "node": ">=12.0.0"
3285
+ }
3286
+ },
3287
+ "node_modules/text-segmentation": {
3288
+ "version": "1.0.3",
3289
+ "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
3290
+ "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
3291
+ "license": "MIT",
3292
+ "dependencies": {
3293
+ "utrie": "^1.0.2"
3294
+ }
3295
+ },
3296
  "node_modules/tiny-invariant": {
3297
  "version": "1.3.3",
3298
  "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
 
3400
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
3401
  }
3402
  },
3403
+ "node_modules/utrie": {
3404
+ "version": "1.0.2",
3405
+ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
3406
+ "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
3407
+ "license": "MIT",
3408
+ "dependencies": {
3409
+ "base64-arraybuffer": "^1.0.2"
3410
+ }
3411
+ },
3412
  "node_modules/victory-vendor": {
3413
  "version": "37.3.6",
3414
  "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
package.json CHANGED
@@ -9,6 +9,9 @@
9
  "preview": "vite preview"
10
  },
11
  "dependencies": {
 
 
 
12
  "lucide-react": "^0.554.0",
13
  "react": "^19.2.0",
14
  "react-dom": "^19.2.0",
 
9
  "preview": "vite preview"
10
  },
11
  "dependencies": {
12
+ "html2canvas": "^1.4.1",
13
+ "jspdf": "^3.0.4",
14
+ "jspdf-autotable": "^5.0.2",
15
  "lucide-react": "^0.554.0",
16
  "react": "^19.2.0",
17
  "react-dom": "^19.2.0",
pages/AwaakBill.tsx CHANGED
@@ -1,13 +1,15 @@
1
  import React, { useState, useEffect } from 'react';
2
  import { useNavigate } from 'react-router-dom';
3
  import {
4
- getParties, getMirchiTypes, generateBillNumber, saveTransaction
 
5
  } from '../services/db';
6
  import {
7
- Party, MirchiType, Transaction, TransactionItem, BillType, PaymentMode, PartyType
8
  } from '../types';
9
- import { Plus, Trash2, Save, ChevronUp, ChevronDown, RotateCcw } from 'lucide-react';
10
- import PrintInvoice from '../components/PrintInvoice';
 
11
 
12
  // Defined outside to prevent re-render focus loss
13
  const SummaryInput = ({ label, value, onChange }: { label: string, value: number, onChange: (val: number) => void }) => (
@@ -23,7 +25,6 @@ const SummaryInput = ({ label, value, onChange }: { label: string, value: number
23
  const parsed = parseFloat(e.target.value);
24
  onChange(isNaN(parsed) ? 0 : Math.max(0, parsed));
25
  }}
26
- onWheel={e => e.currentTarget.blur()}
27
  placeholder="0"
28
  />
29
  </div>
@@ -59,11 +60,17 @@ const AwaakBill = () => {
59
  // Temp inputs for items row
60
  const [potiInputs, setPotiInputs] = useState<{ [key: string]: string }>({});
61
 
 
 
 
 
62
  const [expenses, setExpenses] = useState({
63
- cess_percent: 2.0,
64
  cess_amount: 0,
65
  adat_percent: 3.0,
66
  adat_amount: 0,
 
 
67
  hamali_per_poti: 6,
68
  hamali_amount: 0,
69
  gaadi_bharni: 0,
@@ -121,8 +128,8 @@ const AwaakBill = () => {
121
 
122
  const gross = weights.reduce((a, b) => a + b, 0);
123
  const count = weights.length;
124
- const potya = 0; // No deduction for Awaak
125
- const net = gross; // Net is same as Gross
126
  const total = net * (item.rate_per_kg || 0);
127
 
128
  return {
@@ -146,6 +153,10 @@ const AwaakBill = () => {
146
  updatedItem.item_total = (updatedItem.net_weight || 0) * Math.max(0, parseFloat(value || 0));
147
  }
148
 
 
 
 
 
149
  return updatedItem;
150
  }));
151
  };
@@ -158,6 +169,43 @@ const AwaakBill = () => {
158
  }));
159
  };
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  const addItem = () => {
162
  setItems(prev => [...prev, {
163
  id: Date.now().toString(),
@@ -184,12 +232,13 @@ const AwaakBill = () => {
184
  const subtotal = items.reduce((acc, item) => acc + (item.item_total || 0), 0);
185
  const totalPoti = items.reduce((acc, item) => acc + (item.poti_count || 0), 0);
186
 
187
- // Derived Expenses (0 if Return Mode usually, but keeping logic consistent)
188
- // Derived Expenses
189
- const cessAmt = (subtotal * expenses.cess_percent) / 100;
 
190
  const adatAmt = (subtotal * expenses.adat_percent) / 100;
191
  const hamaliAmt = totalPoti * expenses.hamali_per_poti;
192
- const totalExp = cessAmt + adatAmt + hamaliAmt + (expenses.gaadi_bharni || 0);
193
  const grandTotal = subtotal + totalExp;
194
 
195
  // Calculate Payment Details based on Mode
@@ -234,6 +283,16 @@ const AwaakBill = () => {
234
  alert(`Row ${i + 1}: Please select Mirchi Type`);
235
  return false;
236
  }
 
 
 
 
 
 
 
 
 
 
237
  if (!item.poti_weights || item.poti_weights.length === 0) {
238
  alert(`Row ${i + 1}: Please enter weights (e.g. 10, 20)`);
239
  return false;
@@ -275,6 +334,7 @@ const AwaakBill = () => {
275
  ...expenses,
276
  cess_amount: cessAmt,
277
  adat_amount: adatAmt,
 
278
  hamali_amount: hamaliAmt
279
  },
280
  payments: [
@@ -321,13 +381,29 @@ const AwaakBill = () => {
321
  </div>
322
 
323
  <div className="pt-2 border-t border-dashed space-y-2">
324
- {/* 1. Cess Tax */}
325
- <div className="flex justify-between items-center">
326
- <span className="text-gray-600 text-xs">सेस (Cess {expenses.cess_percent}%)</span>
327
- <span className="text-gray-800 font-medium">₹{cessAmt.toFixed(2)}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  </div>
329
 
330
- {/* 2. Adat / Market Yard Tax */}
331
  <SummaryInput
332
  label={`अडत (Adat ${expenses.adat_percent}%)`}
333
  value={expenses.adat_percent}
@@ -338,16 +414,15 @@ const AwaakBill = () => {
338
  <span>₹{adatAmt.toFixed(2)}</span>
339
  </div>
340
 
341
- {/* 3. Poti (Packet) / Bag */}
342
- <div className="flex justify-between items-center">
343
- <span className="text-gray-600 text-xs">पोती (Bags)</span>
344
- <span className="text-gray-800 font-medium">{totalPoti}</span>
345
- </div>
346
-
347
  {/* 4. Hamali */}
348
- <div className="flex justify-between items-center">
349
- <span className="text-gray-600 text-xs">हमाली ({expenses.hamali_per_poti} * {totalPoti})</span>
350
- <span className="text-gray-800 font-medium">₹{hamaliAmt.toFixed(2)}</span>
 
 
 
 
 
351
  </div>
352
 
353
  {/* 5. Gaadi Bharni */}
@@ -516,9 +591,13 @@ const AwaakBill = () => {
516
  </button>
517
  </div>
518
 
519
- <div className="bg-gray-100 px-3 py-1 rounded text-sm font-mono text-gray-600">
520
- {billDate}
521
- </div>
 
 
 
 
522
  </div>
523
  </div>
524
 
@@ -562,7 +641,7 @@ const AwaakBill = () => {
562
  <select
563
  className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
564
  value={item.mirchi_type_id || ''}
565
- onChange={e => handleItemChange(item.id!, 'mirchi_type_id', e.target.value)}
566
  >
567
  <option value="">Select Type</option>
568
  {mirchiTypes.map(m => (
@@ -570,6 +649,24 @@ const AwaakBill = () => {
570
  ))}
571
  </select>
572
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  <div className="col-span-2 md:col-span-4">
574
  <label className="block text-xs font-medium text-gray-500 mb-1">पोटी वजन (Eg: 10,20,30)</label>
575
  <input
@@ -585,7 +682,21 @@ const AwaakBill = () => {
585
  <label className="block text-xs font-medium text-gray-500 mb-1">Gross</label>
586
  <input type="number" readOnly className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm" value={item.gross_weight} />
587
  </div>
588
- {/* Removed Deduct and Net columns */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
 
590
  <div>
591
  <label className="block text-xs font-medium text-gray-500 mb-1">Rate (₹)</label>
@@ -621,18 +732,22 @@ const AwaakBill = () => {
621
  <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-0">
622
  <h3 className="font-bold text-gray-800 mb-4 border-b pb-2">बिल सारांश (Summary)</h3>
623
  {SummaryContent()}
624
- <button
625
- onClick={handleSubmit}
626
- disabled={isSubmitting || isOverpaid}
627
- className="w-full mt-6 bg-teal-600 text-white py-3 rounded-lg font-semibold hover:bg-teal-700 flex justify-center items-center gap-2 shadow-lg shadow-teal-100 disabled:opacity-50 disabled:bg-gray-400"
628
- >
629
- <Save size={20} /> {isSubmitting ? 'Saving...' : 'जतन करा (Save)'}
630
- </button>
631
- {savedTransaction && (
632
- <div className="mt-4">
633
- <PrintInvoice transaction={savedTransaction} />
634
- </div>
635
- )}
 
 
 
 
636
  </div>
637
  </div>
638
 
 
1
  import React, { useState, useEffect } from 'react';
2
  import { useNavigate } from 'react-router-dom';
3
  import {
4
+ getParties, getMirchiTypes, generateBillNumber, saveTransaction,
5
+ checkLotUnique, getAvailableLotsByMirchi
6
  } from '../services/db';
7
  import {
8
+ Party, MirchiType, Lot, Transaction, TransactionItem, BillType, PaymentMode, PartyType
9
  } from '../types';
10
+ import { Save, Trash2, Plus, RotateCcw, Printer, Download, ChevronUp, ChevronDown } from 'lucide-react';
11
+ import PrintInvoice from '../components/PrintInvoice.tsx';
12
+ import PdfInvoice from '../components/PdfInvoice.tsx';
13
 
14
  // Defined outside to prevent re-render focus loss
15
  const SummaryInput = ({ label, value, onChange }: { label: string, value: number, onChange: (val: number) => void }) => (
 
25
  const parsed = parseFloat(e.target.value);
26
  onChange(isNaN(parsed) ? 0 : Math.max(0, parsed));
27
  }}
 
28
  placeholder="0"
29
  />
30
  </div>
 
60
  // Temp inputs for items row
61
  const [potiInputs, setPotiInputs] = useState<{ [key: string]: string }>({});
62
 
63
+ // LOT number state
64
+ const [lotInputs, setLotInputs] = useState<{ [key: string]: string }>({});
65
+ const [availableLots, setAvailableLots] = useState<{ [key: string]: Lot[] }>({});
66
+
67
  const [expenses, setExpenses] = useState({
68
+ cess_percent: 1.0,
69
  cess_amount: 0,
70
  adat_percent: 3.0,
71
  adat_amount: 0,
72
+ poti_rate: 0,
73
+ poti_amount: 0,
74
  hamali_per_poti: 6,
75
  hamali_amount: 0,
76
  gaadi_bharni: 0,
 
128
 
129
  const gross = weights.reduce((a, b) => a + b, 0);
130
  const count = weights.length;
131
+ const potya = count * 1; // 1kg deduction per bag
132
+ const net = Math.max(0, gross - potya);
133
  const total = net * (item.rate_per_kg || 0);
134
 
135
  return {
 
153
  updatedItem.item_total = (updatedItem.net_weight || 0) * Math.max(0, parseFloat(value || 0));
154
  }
155
 
156
+ if (field === 'net_weight') {
157
+ updatedItem.item_total = Math.max(0, parseFloat(value || 0)) * (updatedItem.rate_per_kg || 0);
158
+ }
159
+
160
  return updatedItem;
161
  }));
162
  };
 
169
  }));
170
  };
171
 
172
+ // Handle mirchi type change - load available lots
173
+ const handleMirchiChange = async (id: string, mirchiTypeId: string) => {
174
+ handleItemChange(id, 'mirchi_type_id', mirchiTypeId);
175
+
176
+ // Load available lots for this mirchi type
177
+ if (mirchiTypeId) {
178
+ const lots = await getAvailableLotsByMirchi(mirchiTypeId);
179
+ setAvailableLots(prev => ({ ...prev, [id]: lots }));
180
+
181
+ // Generate suggested lot number
182
+ const mirchi = mirchiTypes.find(m => m.id === mirchiTypeId);
183
+ if (mirchi) {
184
+ const today = new Date().toISOString().split('T')[0].replace(/-/g, '');
185
+ const mirchiCode = mirchi.name.substring(0, 4).toUpperCase();
186
+ const suggestedLot = `LOT-${mirchiCode}-${today}-`;
187
+ setLotInputs(prev => ({ ...prev, [id]: suggestedLot }));
188
+ }
189
+ }
190
+ };
191
+
192
+ // Handle LOT number input change
193
+ const handleLotNumberChange = (id: string, value: string) => {
194
+ setLotInputs(prev => ({ ...prev, [id]: value }));
195
+ handleItemChange(id, 'lot_number', value);
196
+ };
197
+
198
+ // Handle selecting existing lot
199
+ const handleLotSelection = (id: string, lotNumber: string) => {
200
+ if (lotNumber) {
201
+ setLotInputs(prev => ({ ...prev, [id]: lotNumber }));
202
+ handleItemChange(id, 'lot_number', lotNumber);
203
+ handleItemChange(id, 'is_existing_lot', true);
204
+ } else {
205
+ handleItemChange(id, 'is_existing_lot', false);
206
+ }
207
+ };
208
+
209
  const addItem = () => {
210
  setItems(prev => [...prev, {
211
  id: Date.now().toString(),
 
232
  const subtotal = items.reduce((acc, item) => acc + (item.item_total || 0), 0);
233
  const totalPoti = items.reduce((acc, item) => acc + (item.poti_count || 0), 0);
234
 
235
+ // Derived Expenses - New sequence
236
+ const potiAmt = totalPoti * expenses.poti_rate;
237
+ const baseForCess = subtotal + potiAmt; // Cess calculated on subtotal + poti
238
+ const cessAmt = (baseForCess * expenses.cess_percent) / 100;
239
  const adatAmt = (subtotal * expenses.adat_percent) / 100;
240
  const hamaliAmt = totalPoti * expenses.hamali_per_poti;
241
+ const totalExp = potiAmt + cessAmt + adatAmt + hamaliAmt + (expenses.gaadi_bharni || 0);
242
  const grandTotal = subtotal + totalExp;
243
 
244
  // Calculate Payment Details based on Mode
 
283
  alert(`Row ${i + 1}: Please select Mirchi Type`);
284
  return false;
285
  }
286
+ if (!item.lot_number || item.lot_number.trim() === '') {
287
+ alert(`Row ${i + 1}: Please enter LOT number`);
288
+ return false;
289
+ }
290
+ // Validate LOT number format
291
+ const lotPattern = /^LOT-[A-Z]{3,4}-\d{8}-.+$/;
292
+ if (!lotPattern.test(item.lot_number)) {
293
+ alert(`Row ${i + 1}: Invalid LOT format. Use: LOT-XXXX-YYYYMMDD-XXX`);
294
+ return false;
295
+ }
296
  if (!item.poti_weights || item.poti_weights.length === 0) {
297
  alert(`Row ${i + 1}: Please enter weights (e.g. 10, 20)`);
298
  return false;
 
334
  ...expenses,
335
  cess_amount: cessAmt,
336
  adat_amount: adatAmt,
337
+ poti_amount: potiAmt,
338
  hamali_amount: hamaliAmt
339
  },
340
  payments: [
 
381
  </div>
382
 
383
  <div className="pt-2 border-t border-dashed space-y-2">
384
+ {/* 1. Poti (Bags) Rate */}
385
+ <SummaryInput
386
+ label={`पोती (Bags ${totalPoti}) Rate`}
387
+ value={expenses.poti_rate}
388
+ onChange={val => setExpenses({ ...expenses, poti_rate: val })}
389
+ />
390
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
391
+ <span>Amount:</span>
392
+ <span>₹{potiAmt.toFixed(2)}</span>
393
+ </div>
394
+
395
+ {/* 2. Cess Tax (calculated on subtotal + poti) */}
396
+ <SummaryInput
397
+ label={`सेस (Cess ${expenses.cess_percent}%) on ₹${baseForCess.toFixed(2)}`}
398
+ value={expenses.cess_percent}
399
+ onChange={val => setExpenses({ ...expenses, cess_percent: val })}
400
+ />
401
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
402
+ <span>Amount:</span>
403
+ <span>₹{cessAmt.toFixed(2)}</span>
404
  </div>
405
 
406
+ {/* 3. Adat / Market Yard Tax */}
407
  <SummaryInput
408
  label={`अडत (Adat ${expenses.adat_percent}%)`}
409
  value={expenses.adat_percent}
 
414
  <span>₹{adatAmt.toFixed(2)}</span>
415
  </div>
416
 
 
 
 
 
 
 
417
  {/* 4. Hamali */}
418
+ <SummaryInput
419
+ label={`हमाली (Hamali per poti)`}
420
+ value={expenses.hamali_per_poti}
421
+ onChange={val => setExpenses({ ...expenses, hamali_per_poti: val })}
422
+ />
423
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
424
+ <span>Amount ({expenses.hamali_per_poti} * {totalPoti}):</span>
425
+ <span>₹{hamaliAmt.toFixed(2)}</span>
426
  </div>
427
 
428
  {/* 5. Gaadi Bharni */}
 
591
  </button>
592
  </div>
593
 
594
+ <input
595
+ type="date"
596
+ className="bg-gray-100 px-3 py-1 rounded text-sm font-mono text-gray-600 border border-gray-200 hover:border-teal-400 focus:border-teal-500 focus:ring-1 focus:ring-teal-500 outline-none cursor-pointer transition-colors"
597
+ value={billDate}
598
+ onChange={(e) => setBillDate(e.target.value)}
599
+ max={new Date().toISOString().split('T')[0]}
600
+ />
601
  </div>
602
  </div>
603
 
 
641
  <select
642
  className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
643
  value={item.mirchi_type_id || ''}
644
+ onChange={e => handleMirchiChange(item.id!, e.target.value)}
645
  >
646
  <option value="">Select Type</option>
647
  {mirchiTypes.map(m => (
 
649
  ))}
650
  </select>
651
  </div>
652
+
653
+ {/* LOT Number Section - Single Dynamic Field */}
654
+ {item.mirchi_type_id && (
655
+ <div className="col-span-2">
656
+ <label className="block text-xs font-medium text-gray-500 mb-1">LOT Number</label>
657
+ <input
658
+ type="text"
659
+ className="w-full border border-gray-300 rounded p-2 text-sm font-mono"
660
+ placeholder="Enter new LOT number"
661
+ value={lotInputs[item.id!] || ''}
662
+ onChange={e => {
663
+ setLotInputs({ ...lotInputs, [item.id!]: e.target.value });
664
+ handleItemChange(item.id!, 'lot_number', e.target.value);
665
+ handleItemChange(item.id!, 'is_existing_lot', false);
666
+ }}
667
+ />
668
+ </div>
669
+ )}
670
  <div className="col-span-2 md:col-span-4">
671
  <label className="block text-xs font-medium text-gray-500 mb-1">पोटी वजन (Eg: 10,20,30)</label>
672
  <input
 
682
  <label className="block text-xs font-medium text-gray-500 mb-1">Gross</label>
683
  <input type="number" readOnly className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm" value={item.gross_weight} />
684
  </div>
685
+ <div>
686
+ <label className="block text-xs font-medium text-gray-500 mb-1">Poti (count)</label>
687
+ <input type="number" readOnly className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm text-red-500" value={item.total_potya} />
688
+ </div>
689
+ <div>
690
+ <label className="block text-xs font-medium text-gray-500 mb-1">Net (Editable)</label>
691
+ <input
692
+ type="number"
693
+ min="0"
694
+ className="w-full bg-blue-50 border border-blue-200 rounded p-2 text-sm font-bold text-blue-800 appearance-none"
695
+ value={item.net_weight === 0 ? '' : item.net_weight}
696
+ onChange={e => handleItemChange(item.id!, 'net_weight', Math.max(0, parseFloat(e.target.value) || 0))}
697
+ placeholder="Auto-calculated"
698
+ />
699
+ </div>
700
 
701
  <div>
702
  <label className="block text-xs font-medium text-gray-500 mb-1">Rate (₹)</label>
 
732
  <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-0">
733
  <h3 className="font-bold text-gray-800 mb-4 border-b pb-2">बिल सारांश (Summary)</h3>
734
  {SummaryContent()}
735
+ <div className="flex gap-2 mt-6">
736
+ {savedTransaction && (
737
+ <>
738
+ <PrintInvoice transaction={savedTransaction} />
739
+ <PdfInvoice transaction={savedTransaction} />
740
+ </>
741
+ )}
742
+ <button
743
+ onClick={handleSubmit}
744
+ disabled={isSubmitting || items.length === 0}
745
+ className={`flex-1 bg-teal-600 text-white py-3 rounded-lg font-bold shadow-lg hover:bg-teal-700 transition-all flex items-center justify-center gap-2 ${isSubmitting || items.length === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
746
+ >
747
+ <Save size={20} />
748
+ {isSubmitting ? 'Saving...' : 'Save Bill'}
749
+ </button>
750
+ </div>
751
  </div>
752
  </div>
753
 
pages/JawaakBill.tsx CHANGED
@@ -1,15 +1,16 @@
1
  import React, { useState, useEffect } from 'react';
2
  import { useNavigate } from 'react-router-dom';
3
  import {
4
- getParties, getMirchiTypes, generateBillNumber, saveTransaction
 
5
  } from '../services/db';
6
  import {
7
- Party, MirchiType, Transaction, TransactionItem, BillType, PaymentMode, PartyType
8
  } from '../types';
9
- import { Plus, Trash2, Save, ChevronUp, ChevronDown, RotateCcw } from 'lucide-react';
10
  import PrintInvoice from '../components/PrintInvoice';
 
11
 
12
- // Defined outside to prevent re-render focus loss
13
  const SummaryInput = ({ label, value, onChange }: { label: string, value: number, onChange: (val: number) => void }) => (
14
  <div className="flex justify-between items-center mb-1 bg-gray-50 p-1.5 rounded">
15
  <span className="text-gray-600 text-xs">{label}</span>
@@ -23,7 +24,6 @@ const SummaryInput = ({ label, value, onChange }: { label: string, value: number
23
  const parsed = parseFloat(e.target.value);
24
  onChange(isNaN(parsed) ? 0 : Math.max(0, parsed));
25
  }}
26
- onWheel={e => e.currentTarget.blur()}
27
  placeholder="0"
28
  />
29
  </div>
@@ -59,8 +59,11 @@ const JawaakBill = () => {
59
  // Temp inputs for items row
60
  const [potiInputs, setPotiInputs] = useState<{ [key: string]: string }>({});
61
 
 
 
 
62
  const [expenses, setExpenses] = useState({
63
- cess_percent: 2.0,
64
  cess_amount: 0,
65
  adat_percent: 3.0,
66
  adat_amount: 0,
@@ -162,6 +165,29 @@ const JawaakBill = () => {
162
  }));
163
  };
164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  const addItem = () => {
166
  setItems(prev => [...prev, {
167
  id: Date.now().toString(),
@@ -188,14 +214,14 @@ const JawaakBill = () => {
188
  const subtotal = items.reduce((acc, item) => acc + (item.item_total || 0), 0);
189
  const totalPoti = items.reduce((acc, item) => acc + (item.poti_count || 0), 0);
190
 
191
- // Derived Expenses (0 if Return Mode usually, but keeping logic consistent)
192
- // Derived Expenses
193
- const cessAmt = (subtotal * expenses.cess_percent) / 100;
194
- const adatAmt = (subtotal * expenses.adat_percent) / 100;
195
  const potiAmt = totalPoti * expenses.poti_rate;
 
 
 
196
  const hamaliAmt = totalPoti * expenses.hamali_per_poti;
197
  const packagingHamaliAmt = totalPoti * expenses.packaging_hamali_per_poti;
198
- const totalExp = cessAmt + adatAmt + potiAmt + hamaliAmt + packagingHamaliAmt + (expenses.gaadi_bharni || 0);
199
  const grandTotal = subtotal + totalExp;
200
 
201
  // Calculate Payment Details based on Mode
@@ -329,38 +355,48 @@ const JawaakBill = () => {
329
  </div>
330
 
331
  <div className="pt-2 border-t border-dashed space-y-2">
332
- {/* 1. Cess Tax */}
333
- <div className="flex justify-between items-center">
334
- <span className="text-gray-600 text-xs">सेस (Cess {expenses.cess_percent}%)</span>
335
- <span className="text-gray-800 font-medium">₹{cessAmt.toFixed(2)}</span>
 
 
 
 
 
336
  </div>
337
 
338
- {/* 2. Adat / Market Yard Tax */}
339
  <SummaryInput
340
- label={`अडत (Adat ${expenses.adat_percent}%)`}
341
- value={expenses.adat_percent}
342
- onChange={val => setExpenses({ ...expenses, adat_percent: val })}
343
  />
344
  <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
345
  <span>Amount:</span>
346
- <span>₹{adatAmt.toFixed(2)}</span>
347
  </div>
348
 
349
- {/* 3. Poti (Packet) / Bag */}
350
  <SummaryInput
351
- label={`पो (Bags ${totalPoti}) Rate`}
352
- value={expenses.poti_rate}
353
- onChange={val => setExpenses({ ...expenses, poti_rate: val })}
354
  />
355
  <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
356
  <span>Amount:</span>
357
- <span>₹{potiAmt.toFixed(2)}</span>
358
  </div>
359
 
360
  {/* 4. Hamali */}
361
- <div className="flex justify-between items-center">
362
- <span className="text-gray-600 text-xs">हमाली ({expenses.hamali_per_poti} * {totalPoti})</span>
363
- <span className="text-gray-800 font-medium">₹{hamaliAmt.toFixed(2)}</span>
 
 
 
 
 
364
  </div>
365
 
366
  {/* 5. Packaging Hamali */}
@@ -532,9 +568,13 @@ const JawaakBill = () => {
532
  </button>
533
  </div>
534
 
535
- <div className="bg-gray-100 px-3 py-1 rounded text-sm font-mono text-gray-600">
536
- {billDate}
537
- </div>
 
 
 
 
538
  </div>
539
  </div>
540
 
@@ -578,7 +618,7 @@ const JawaakBill = () => {
578
  <select
579
  className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
580
  value={item.mirchi_type_id || ''}
581
- onChange={e => handleItemChange(item.id!, 'mirchi_type_id', e.target.value)}
582
  >
583
  <option value="">Select Type</option>
584
  {mirchiTypes.map(m => (
@@ -586,7 +626,27 @@ const JawaakBill = () => {
586
  ))}
587
  </select>
588
  </div>
589
- <div className="col-span-2 md:col-span-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
590
  <label className="block text-xs font-medium text-gray-500 mb-1">पोटी वजन (Eg: 10,20,30)</label>
591
  <input
592
  type="text"
@@ -643,18 +703,22 @@ const JawaakBill = () => {
643
  <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-0">
644
  <h3 className="font-bold text-gray-800 mb-4 border-b pb-2">बिल सारांश (Summary)</h3>
645
  {SummaryContent()}
646
- <button
647
- onClick={handleSubmit}
648
- disabled={isSubmitting || isOverpaid}
649
- className="w-full mt-6 bg-teal-600 text-white py-3 rounded-lg font-semibold hover:bg-teal-700 flex justify-center items-center gap-2 shadow-lg shadow-teal-100 disabled:opacity-50 disabled:bg-gray-400"
650
- >
651
- <Save size={20} /> {isSubmitting ? 'Saving...' : 'जतन करा (Save)'}
652
- </button>
653
- {savedTransaction && (
654
- <div className="mt-4">
655
- <PrintInvoice transaction={savedTransaction} />
656
- </div>
657
- )}
 
 
 
 
658
  </div>
659
  </div>
660
 
 
1
  import React, { useState, useEffect } from 'react';
2
  import { useNavigate } from 'react-router-dom';
3
  import {
4
+ getParties, getMirchiTypes, generateBillNumber, saveTransaction,
5
+ getAvailableLotsByMirchi
6
  } from '../services/db';
7
  import {
8
+ Party, MirchiType, Lot, Transaction, TransactionItem, BillType, PaymentMode, PartyType
9
  } from '../types';
10
+ import { Save, Trash2, Plus, RotateCcw, Printer, Download, ChevronUp, ChevronDown } from 'lucide-react';
11
  import PrintInvoice from '../components/PrintInvoice';
12
+ import PdfInvoice from '../components/PdfInvoice';
13
 
 
14
  const SummaryInput = ({ label, value, onChange }: { label: string, value: number, onChange: (val: number) => void }) => (
15
  <div className="flex justify-between items-center mb-1 bg-gray-50 p-1.5 rounded">
16
  <span className="text-gray-600 text-xs">{label}</span>
 
24
  const parsed = parseFloat(e.target.value);
25
  onChange(isNaN(parsed) ? 0 : Math.max(0, parsed));
26
  }}
 
27
  placeholder="0"
28
  />
29
  </div>
 
59
  // Temp inputs for items row
60
  const [potiInputs, setPotiInputs] = useState<{ [key: string]: string }>({});
61
 
62
+ // LOT selection state
63
+ const [availableLots, setAvailableLots] = useState<{ [key: string]: Lot[] }>({});
64
+
65
  const [expenses, setExpenses] = useState({
66
+ cess_percent: 1.0,
67
  cess_amount: 0,
68
  adat_percent: 3.0,
69
  adat_amount: 0,
 
165
  }));
166
  };
167
 
168
+ // Handle mirchi type change - load available lots
169
+ const handleMirchiChange = async (id: string, mirchiTypeId: string) => {
170
+ handleItemChange(id, 'mirchi_type_id', mirchiTypeId);
171
+
172
+ // Load available lots for this mirchi type
173
+ if (mirchiTypeId) {
174
+ const lots = await getAvailableLotsByMirchi(mirchiTypeId);
175
+ setAvailableLots(prev => ({ ...prev, [id]: lots }));
176
+ }
177
+ };
178
+
179
+ // Handle LOT selection
180
+ const handleLotSelection = (id: string, lotId: string) => {
181
+ handleItemChange(id, 'lot_id', lotId);
182
+
183
+ // Find the selected lot to get its details
184
+ const lots = availableLots[id] || [];
185
+ const selectedLot = lots.find(lot => lot.id === lotId);
186
+ if (selectedLot) {
187
+ handleItemChange(id, 'lot_number', selectedLot.lot_number);
188
+ }
189
+ };
190
+
191
  const addItem = () => {
192
  setItems(prev => [...prev, {
193
  id: Date.now().toString(),
 
214
  const subtotal = items.reduce((acc, item) => acc + (item.item_total || 0), 0);
215
  const totalPoti = items.reduce((acc, item) => acc + (item.poti_count || 0), 0);
216
 
217
+ // Derived Expenses - New sequence
 
 
 
218
  const potiAmt = totalPoti * expenses.poti_rate;
219
+ const baseForCess = subtotal + potiAmt; // Cess calculated on subtotal + poti
220
+ const cessAmt = (baseForCess * expenses.cess_percent) / 100;
221
+ const adatAmt = (subtotal * expenses.adat_percent) / 100;
222
  const hamaliAmt = totalPoti * expenses.hamali_per_poti;
223
  const packagingHamaliAmt = totalPoti * expenses.packaging_hamali_per_poti;
224
+ const totalExp = potiAmt + cessAmt + adatAmt + hamaliAmt + packagingHamaliAmt + (expenses.gaadi_bharni || 0);
225
  const grandTotal = subtotal + totalExp;
226
 
227
  // Calculate Payment Details based on Mode
 
355
  </div>
356
 
357
  <div className="pt-2 border-t border-dashed space-y-2">
358
+ {/* 1. Poti (Bags) Rate */}
359
+ <SummaryInput
360
+ label={`पोती (Bags ${totalPoti}) Rate`}
361
+ value={expenses.poti_rate}
362
+ onChange={val => setExpenses({ ...expenses, poti_rate: val })}
363
+ />
364
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
365
+ <span>Amount:</span>
366
+ <span>₹{potiAmt.toFixed(2)}</span>
367
  </div>
368
 
369
+ {/* 2. Cess Tax (calculated on subtotal + poti) */}
370
  <SummaryInput
371
+ label={`सेस (Cess ${expenses.cess_percent}%) on ₹${baseForCess.toFixed(2)}`}
372
+ value={expenses.cess_percent}
373
+ onChange={val => setExpenses({ ...expenses, cess_percent: val })}
374
  />
375
  <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
376
  <span>Amount:</span>
377
+ <span>₹{cessAmt.toFixed(2)}</span>
378
  </div>
379
 
380
+ {/* 3. Adat / Market Yard Tax */}
381
  <SummaryInput
382
+ label={`अडत (Adat ${expenses.adat_percent}%)`}
383
+ value={expenses.adat_percent}
384
+ onChange={val => setExpenses({ ...expenses, adat_percent: val })}
385
  />
386
  <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
387
  <span>Amount:</span>
388
+ <span>₹{adatAmt.toFixed(2)}</span>
389
  </div>
390
 
391
  {/* 4. Hamali */}
392
+ <SummaryInput
393
+ label={`हमाली (Hamali per poti)`}
394
+ value={expenses.hamali_per_poti}
395
+ onChange={val => setExpenses({ ...expenses, hamali_per_poti: val })}
396
+ />
397
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
398
+ <span>Amount ({expenses.hamali_per_poti} * {totalPoti}):</span>
399
+ <span>₹{hamaliAmt.toFixed(2)}</span>
400
  </div>
401
 
402
  {/* 5. Packaging Hamali */}
 
568
  </button>
569
  </div>
570
 
571
+ <input
572
+ type="date"
573
+ className="bg-gray-100 px-3 py-1 rounded text-sm font-mono text-gray-600 border border-gray-200 hover:border-blue-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none cursor-pointer transition-colors"
574
+ value={billDate}
575
+ onChange={(e) => setBillDate(e.target.value)}
576
+ max={new Date().toISOString().split('T')[0]}
577
+ />
578
  </div>
579
  </div>
580
 
 
618
  <select
619
  className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
620
  value={item.mirchi_type_id || ''}
621
+ onChange={e => handleMirchiChange(item.id!, e.target.value)}
622
  >
623
  <option value="">Select Type</option>
624
  {mirchiTypes.map(m => (
 
626
  ))}
627
  </select>
628
  </div>
629
+
630
+ {/* LOT Selection */}
631
+ {item.mirchi_type_id && (
632
+ <div className="col-span-2">
633
+ <label className="block text-xs font-medium text-gray-500 mb-1">LOT Number</label>
634
+ <select
635
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white font-mono"
636
+ value={item.lot_id || ''}
637
+ onChange={e => handleLotSelection(item.id!, e.target.value)}
638
+ >
639
+ <option value="">Select LOT</option>
640
+ {(availableLots[item.id!] || []).map(lot => (
641
+ <option key={lot.id} value={lot.id}>
642
+ {lot.lot_number} ({lot.remaining_quantity}kg available)
643
+ </option>
644
+ ))}
645
+ </select>
646
+ </div>
647
+ )}
648
+
649
+ <div className={item.mirchi_type_id ? "col-span-2" : "col-span-2 md:col-span-4"}>
650
  <label className="block text-xs font-medium text-gray-500 mb-1">पोटी वजन (Eg: 10,20,30)</label>
651
  <input
652
  type="text"
 
703
  <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-0">
704
  <h3 className="font-bold text-gray-800 mb-4 border-b pb-2">बिल सारांश (Summary)</h3>
705
  {SummaryContent()}
706
+ <div className="flex gap-2 mt-6">
707
+ {savedTransaction && (
708
+ <>
709
+ <PrintInvoice transaction={savedTransaction} />
710
+ <PdfInvoice transaction={savedTransaction} />
711
+ </>
712
+ )}
713
+ <button
714
+ onClick={handleSubmit}
715
+ disabled={isSubmitting || items.length === 0}
716
+ className={`flex-1 bg-teal-600 text-white py-3 rounded-lg font-bold shadow-lg hover:bg-teal-700 transition-all flex items-center justify-center gap-2 ${isSubmitting || items.length === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
717
+ >
718
+ <Save size={20} />
719
+ {isSubmitting ? 'Saving...' : 'Save Bill'}
720
+ </button>
721
+ </div>
722
  </div>
723
  </div>
724
 
pages/PartyLedger.tsx CHANGED
@@ -1,8 +1,9 @@
1
- import React, { useEffect, useState } from 'react';
2
  import { getParties, getTransactions, updateTransactionPayment } from '../services/db';
3
  import { Party, Transaction, PartyType, BillType } from '../types';
4
  import { Users, Filter, Search, ArrowLeft, Eye, Edit, Save, X, Printer, Download } from 'lucide-react';
5
- import PrintInvoice from '../components/PrintInvoice';
 
6
  import { exportPartyLedger } from '../utils/exportToExcel';
7
 
8
  const PartyLedger = () => {
@@ -47,6 +48,28 @@ const PartyLedger = () => {
47
  ? transactions.filter(t => t.party_id === selectedParty.id).sort((a, b) => new Date(b.bill_date).getTime() - new Date(a.bill_date).getTime())
48
  : [];
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  // Calculate Totals for List View
51
  const getPartyStats = (partyId: string) => {
52
  const txs = transactions.filter(t => t.party_id === partyId);
@@ -117,57 +140,31 @@ const PartyLedger = () => {
117
  <p className="text-sm text-gray-500">{selectedParty.city} • {selectedParty.phone}</p>
118
  </div>
119
  </div>
120
- {selectedTransactions.length > 0 && (
121
- <button
122
- onClick={() => {
123
- const selected = partyTransactions.filter(t => selectedTransactions.includes(t.id));
124
- if (selected.length > 0) {
125
- // Create combined transaction for printing
126
- const combinedTransaction = {
127
- ...selected[0], // Use first transaction as base
128
- id: `combined-${selectedParty.id}-${Date.now()}`, // Unique ID for combined
129
- bill_number: `COMBINED-${selected.length}-BILLS`,
130
- items: selected.flatMap(t => t.items),
131
- subtotal: selected.reduce((sum, t) => sum + (t.subtotal || 0), 0),
132
- total_expenses: selected.reduce((sum, t) => sum + (t.total_expenses || 0), 0),
133
- total_amount: selected.reduce((sum, t) => sum + t.total_amount, 0),
134
- paid_amount: selected.reduce((sum, t) => sum + t.paid_amount, 0),
135
- balance_amount: selected.reduce((sum, t) => sum + t.balance_amount, 0),
136
- gross_weight_total: selected.reduce((sum, t) => sum + (t.gross_weight_total || 0), 0),
137
- net_weight_total: selected.reduce((sum, t) => sum + (t.net_weight_total || 0), 0),
138
- };
139
- // Trigger print for combined transaction
140
- const printComponent = document.createElement('div');
141
- document.body.appendChild(printComponent);
142
- // Use PrintInvoice component
143
- import('../components/PrintInvoice').then(({ default: PrintInvoice }) => {
144
- const React = require('react');
145
- const ReactDOM = require('react-dom/client');
146
- const root = ReactDOM.createRoot(printComponent);
147
- root.render(React.createElement(PrintInvoice, { transaction: combinedTransaction }));
148
- setTimeout(() => {
149
- const printBtn = printComponent.querySelector('button');
150
- if (printBtn) printBtn.click();
151
- setTimeout(() => {
152
- root.unmount();
153
- document.body.removeChild(printComponent);
154
- }, 1000);
155
- }, 500);
156
- });
157
- }
158
- }}
159
- className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-2"
160
  >
161
- <Printer size={18} />
162
- Print {selectedTransactions.length} Selected
163
- </button>
 
 
 
 
 
 
 
 
 
164
  )}
165
  <button
166
  onClick={() => exportPartyLedger(selectedParty, partyTransactions)}
167
- className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2"
 
168
  >
169
- <Download size={18} />
170
- Export to Excel
171
  </button>
172
  </div>
173
  </>
@@ -433,7 +430,14 @@ const PartyLedger = () => {
433
  <td className="px-6 py-4 text-gray-600">{tx.bill_date}</td>
434
  <td className="px-6 py-4 font-mono text-gray-500">{tx.bill_number}</td>
435
  <td className="px-6 py-4">
436
- {tx.items.map(i => i.mirchi_name).join(', ')}
 
 
 
 
 
 
 
437
  </td>
438
  <td className="px-6 py-4">
439
  {tx.is_return ? (
@@ -488,7 +492,18 @@ const PartyLedger = () => {
488
  )}
489
  </td>
490
  <td className="px-6 py-4 text-center">
491
- <PrintInvoice transaction={tx} />
 
 
 
 
 
 
 
 
 
 
 
492
  </td>
493
  </tr>
494
  ))
@@ -509,9 +524,23 @@ const PartyLedger = () => {
509
  partyTransactions.map(tx => (
510
  <div key={tx.id} className="p-4 space-y-3">
511
  <div className="flex justify-between items-start">
512
- <div>
513
- <div className="text-xs text-gray-500">{tx.bill_date}</div>
514
- <div className="font-mono text-sm font-medium text-gray-800">{tx.bill_number}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  </div>
516
  {tx.is_return && (
517
  <span className="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs font-semibold">Returned</span>
@@ -520,7 +549,14 @@ const PartyLedger = () => {
520
 
521
  <div className="text-sm text-gray-600">
522
  <span className="font-medium text-gray-500 text-xs block mb-1">Items:</span>
523
- {tx.items.map(i => i.mirchi_name).join(', ')}
 
 
 
 
 
 
 
524
  </div>
525
 
526
  <div className="grid grid-cols-3 gap-2 text-xs pt-2 border-t border-gray-50">
@@ -575,8 +611,17 @@ const PartyLedger = () => {
575
  )}
576
 
577
  {/* Print Button for Mobile */}
578
- <div className="pt-2 border-t border-gray-100 mt-2">
579
- <PrintInvoice transaction={tx} />
 
 
 
 
 
 
 
 
 
580
  </div>
581
  </div>
582
  ))
@@ -589,7 +634,7 @@ const PartyLedger = () => {
589
  </>
590
  )}
591
  </div>
592
- </div>
593
  );
594
  };
595
 
 
1
+ import React, { useEffect, useState, useMemo } from 'react';
2
  import { getParties, getTransactions, updateTransactionPayment } from '../services/db';
3
  import { Party, Transaction, PartyType, BillType } from '../types';
4
  import { Users, Filter, Search, ArrowLeft, Eye, Edit, Save, X, Printer, Download } from 'lucide-react';
5
+ import PrintInvoice from '../components/PrintInvoice.tsx';
6
+ import PdfInvoice from '../components/PdfInvoice.tsx';
7
  import { exportPartyLedger } from '../utils/exportToExcel';
8
 
9
  const PartyLedger = () => {
 
48
  ? transactions.filter(t => t.party_id === selectedParty.id).sort((a, b) => new Date(b.bill_date).getTime() - new Date(a.bill_date).getTime())
49
  : [];
50
 
51
+ // Memoize combined transaction for PDF generation
52
+ const combinedTransaction = useMemo(() => {
53
+ if (!selectedParty || selectedTransactions.length === 0) return null;
54
+
55
+ const selected = partyTransactions.filter(t => selectedTransactions.includes(t.id));
56
+ if (selected.length === 0) return null;
57
+
58
+ return {
59
+ ...selected[0], // Use first transaction as base
60
+ id: `combined-${selectedParty.id}-${Date.now()}`, // Unique ID for combined
61
+ bill_number: `COMBINED-${selected.length}-BILLS`,
62
+ items: selected.flatMap(t => t.items),
63
+ subtotal: selected.reduce((sum, t) => sum + (t.subtotal || 0), 0),
64
+ total_expenses: selected.reduce((sum, t) => sum + (t.total_expenses || 0), 0),
65
+ total_amount: selected.reduce((sum, t) => sum + t.total_amount, 0),
66
+ paid_amount: selected.reduce((sum, t) => sum + t.paid_amount, 0),
67
+ balance_amount: selected.reduce((sum, t) => sum + t.balance_amount, 0),
68
+ gross_weight_total: selected.reduce((sum, t) => sum + (t.gross_weight_total || 0), 0),
69
+ net_weight_total: selected.reduce((sum, t) => sum + (t.net_weight_total || 0), 0),
70
+ };
71
+ }, [selectedTransactions, partyTransactions, selectedParty]);
72
+
73
  // Calculate Totals for List View
74
  const getPartyStats = (partyId: string) => {
75
  const txs = transactions.filter(t => t.party_id === partyId);
 
140
  <p className="text-sm text-gray-500">{selectedParty.city} • {selectedParty.phone}</p>
141
  </div>
142
  </div>
143
+ {selectedTransactions.length > 0 && combinedTransaction && (
144
+ <PrintInvoice
145
+ transaction={combinedTransaction}
146
+ className="px-2 sm:px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-1 sm:gap-2"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  >
148
+ <Printer size={18} className="shrink-0" />
149
+ <span className="hidden sm:inline">Print {selectedTransactions.length} Selected</span>
150
+ </PrintInvoice>
151
+ )}
152
+ {combinedTransaction && (
153
+ <PdfInvoice
154
+ transaction={combinedTransaction}
155
+ className="px-2 sm:px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center gap-1 sm:gap-2"
156
+ >
157
+ <Download size={18} className="shrink-0" />
158
+ <span className="hidden sm:inline">PDF {selectedTransactions.length} Selected</span>
159
+ </PdfInvoice>
160
  )}
161
  <button
162
  onClick={() => exportPartyLedger(selectedParty, partyTransactions)}
163
+ className="px-2 sm:px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-1 sm:gap-2"
164
+ title="Export to Excel"
165
  >
166
+ <Download size={18} className="shrink-0" />
167
+ <span className="hidden sm:inline">Export to Excel</span>
168
  </button>
169
  </div>
170
  </>
 
430
  <td className="px-6 py-4 text-gray-600">{tx.bill_date}</td>
431
  <td className="px-6 py-4 font-mono text-gray-500">{tx.bill_number}</td>
432
  <td className="px-6 py-4">
433
+ {tx.items.map((i, idx) => (
434
+ <div key={idx} className="text-sm">
435
+ {i.mirchi_name}
436
+ {i.lot_number && (
437
+ <span className="ml-2 text-xs font-mono text-gray-500">({i.lot_number})</span>
438
+ )}
439
+ </div>
440
+ ))}
441
  </td>
442
  <td className="px-6 py-4">
443
  {tx.is_return ? (
 
492
  )}
493
  </td>
494
  <td className="px-6 py-4 text-center">
495
+ <div className="flex gap-2 justify-center items-center">
496
+ <PrintInvoice
497
+ transaction={tx}
498
+ className="p-2 bg-white text-teal-600 border border-teal-600 rounded-md hover:bg-teal-50 transition-colors flex items-center justify-center shadow-sm"
499
+ />
500
+ <PdfInvoice
501
+ transaction={tx}
502
+ className="p-2 bg-white text-red-600 border border-red-600 rounded-md hover:bg-red-50 transition-colors flex items-center justify-center shadow-sm"
503
+ >
504
+ <Download size={16} />
505
+ </PdfInvoice>
506
+ </div>
507
  </td>
508
  </tr>
509
  ))
 
524
  partyTransactions.map(tx => (
525
  <div key={tx.id} className="p-4 space-y-3">
526
  <div className="flex justify-between items-start">
527
+ <div className="flex items-start gap-3">
528
+ <input
529
+ type="checkbox"
530
+ checked={selectedTransactions.includes(tx.id)}
531
+ onChange={(e) => {
532
+ if (e.target.checked) {
533
+ setSelectedTransactions([...selectedTransactions, tx.id]);
534
+ } else {
535
+ setSelectedTransactions(selectedTransactions.filter(id => id !== tx.id));
536
+ }
537
+ }}
538
+ className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500 mt-1"
539
+ />
540
+ <div>
541
+ <div className="text-xs text-gray-500">{tx.bill_date}</div>
542
+ <div className="font-mono text-sm font-medium text-gray-800">{tx.bill_number}</div>
543
+ </div>
544
  </div>
545
  {tx.is_return && (
546
  <span className="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs font-semibold">Returned</span>
 
549
 
550
  <div className="text-sm text-gray-600">
551
  <span className="font-medium text-gray-500 text-xs block mb-1">Items:</span>
552
+ {tx.items.map((i, idx) => (
553
+ <div key={idx}>
554
+ {i.mirchi_name}
555
+ {i.lot_number && (
556
+ <span className="ml-2 text-xs font-mono text-gray-500">({i.lot_number})</span>
557
+ )}
558
+ </div>
559
+ ))}
560
  </div>
561
 
562
  <div className="grid grid-cols-3 gap-2 text-xs pt-2 border-t border-gray-50">
 
611
  )}
612
 
613
  {/* Print Button for Mobile */}
614
+ <div className="pt-2 border-t border-gray-100 mt-2 flex gap-3 items-center">
615
+ <PrintInvoice
616
+ transaction={tx}
617
+ className="p-2 bg-white text-teal-600 border border-teal-600 rounded-md hover:bg-teal-50 transition-colors flex items-center justify-center shadow-sm"
618
+ />
619
+ <PdfInvoice
620
+ transaction={tx}
621
+ className="p-2 bg-white text-red-600 border border-red-600 rounded-md hover:bg-red-50 transition-colors flex items-center justify-center shadow-sm"
622
+ >
623
+ <Download size={16} />
624
+ </PdfInvoice>
625
  </div>
626
  </div>
627
  ))
 
634
  </>
635
  )}
636
  </div>
637
+ </div >
638
  );
639
  };
640
 
pages/Settings.tsx CHANGED
@@ -7,6 +7,7 @@ import {
7
  } from '../services/db';
8
  import { Party, MirchiType, PartyType } from '../types';
9
  import { Save, Plus, Settings as SettingsIcon, Users, Sprout, Bell, Download } from 'lucide-react';
 
10
 
11
  const Settings = () => {
12
  const [activeTab, setActiveTab] = useState<'parties' | 'mirchi' | 'general'>('parties');
@@ -14,8 +15,7 @@ const Settings = () => {
14
  const [mirchiTypes, setMirchiTypes] = useState<MirchiType[]>([]);
15
 
16
  // PWA Install
17
- const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
18
- const [isInstallable, setIsInstallable] = useState(false);
19
 
20
  // Form States
21
  const [newParty, setNewParty] = useState<Partial<Party>>({
@@ -41,19 +41,6 @@ const Settings = () => {
41
  setMirchiTypes(await apiGetMirchiTypes());
42
  };
43
  loadData();
44
-
45
- // Listen for PWA install prompt
46
- const handleBeforeInstallPrompt = (e: any) => {
47
- e.preventDefault();
48
- setDeferredPrompt(e);
49
- setIsInstallable(true);
50
- };
51
-
52
- window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
53
-
54
- return () => {
55
- window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
56
- };
57
  }, []);
58
 
59
  const handleSaveParty = async () => {
@@ -105,17 +92,7 @@ const Settings = () => {
105
  };
106
 
107
  const handleInstallClick = async () => {
108
- if (!deferredPrompt) return;
109
-
110
- deferredPrompt.prompt();
111
- const { outcome } = await deferredPrompt.userChoice;
112
-
113
- if (outcome === 'accepted') {
114
- console.log('✅ PWA installed');
115
- }
116
-
117
- setDeferredPrompt(null);
118
- setIsInstallable(false);
119
  };
120
 
121
  return (
 
7
  } from '../services/db';
8
  import { Party, MirchiType, PartyType } from '../types';
9
  import { Save, Plus, Settings as SettingsIcon, Users, Sprout, Bell, Download } from 'lucide-react';
10
+ import { usePWA } from '../context/PWAContext';
11
 
12
  const Settings = () => {
13
  const [activeTab, setActiveTab] = useState<'parties' | 'mirchi' | 'general'>('parties');
 
15
  const [mirchiTypes, setMirchiTypes] = useState<MirchiType[]>([]);
16
 
17
  // PWA Install
18
+ const { isInstallable, installApp } = usePWA();
 
19
 
20
  // Form States
21
  const [newParty, setNewParty] = useState<Partial<Party>>({
 
41
  setMirchiTypes(await apiGetMirchiTypes());
42
  };
43
  loadData();
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }, []);
45
 
46
  const handleSaveParty = async () => {
 
92
  };
93
 
94
  const handleInstallClick = async () => {
95
+ await installApp();
 
 
 
 
 
 
 
 
 
 
96
  };
97
 
98
  return (
pages/StockReport.tsx CHANGED
@@ -31,24 +31,39 @@ const StockReport = () => {
31
  [lots, searchTerm]
32
  );
33
 
34
- const aggregatedByMirchi = useMemo(() => {
35
- const map = new Map<string, { mirchiId: string; mirchiName: string; totalQty: number; remainingQty: number }>();
 
 
36
 
37
- filteredLots.forEach((lot) => {
38
- const key = lot.mirchi_type_id;
39
- const existing = map.get(key) || {
40
- mirchiId: lot.mirchi_type_id,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  mirchiName: lot.mirchi_name,
42
- totalQty: 0,
43
- remainingQty: 0,
 
 
 
 
44
  };
45
- existing.totalQty += lot.total_quantity;
46
- existing.remainingQty += lot.remaining_quantity;
47
- map.set(key, existing);
48
  });
49
-
50
- return Array.from(map.values());
51
- }, [filteredLots]);
52
 
53
  const detailMovements = useMemo(() => {
54
  if (!selectedMirchiId) return [];
@@ -60,37 +75,45 @@ const StockReport = () => {
60
  partyName: string;
61
  inQty: number;
62
  outQty: number;
 
 
63
  typeLabel: string;
64
  isReturn: boolean;
65
  }[] = [];
66
 
67
  transactions.forEach((tx) => {
68
  tx.items
69
- .filter((item) => item.mirchi_type_id === selectedMirchiId)
70
  .forEach((item) => {
71
  const party = parties.find((p) => p.id === tx.party_id);
72
  let inQty = 0;
73
  let outQty = 0;
 
 
74
  let typeLabel = '';
75
 
76
- if (tx.bill_type === BillType.JAWAAK) {
77
- // Jawaak = Purchase / Stock IN
78
  if (tx.is_return) {
79
  // Purchase Return: Stock OUT
80
  outQty = item.net_weight;
 
81
  typeLabel = 'Purchase Return';
82
  } else {
83
  inQty = item.net_weight;
 
84
  typeLabel = 'Purchase';
85
  }
86
  } else {
87
- // Awaak = Sales / Stock OUT
88
  if (tx.is_return) {
89
  // Sales Return: Stock IN
90
  inQty = item.net_weight;
 
91
  typeLabel = 'Sales Return';
92
  } else {
93
  outQty = item.net_weight;
 
94
  typeLabel = 'Sale';
95
  }
96
  }
@@ -102,6 +125,8 @@ const StockReport = () => {
102
  partyName: party?.name || tx.party_name || 'Unknown Party',
103
  inQty,
104
  outQty,
 
 
105
  typeLabel,
106
  isReturn: tx.is_return,
107
  });
@@ -111,7 +136,7 @@ const StockReport = () => {
111
  // Sort latest first
112
  rows.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
113
  return rows;
114
- }, [selectedMirchiName, transactions, parties]);
115
 
116
  return (
117
  <div className="space-y-4">
@@ -162,59 +187,64 @@ const StockReport = () => {
162
  <table className="w-full text-sm text-left">
163
  <thead className="bg-gray-50 text-gray-500 font-medium">
164
  <tr>
 
165
  <th className="px-6 py-4">मिरची जात (Mirchi Type)</th>
166
- <th className="px-6 py-4 text-right">एकूण क्वांटिटी (Total Qty)</th>
167
- <th className="px-6 py-4 text-right">शिल्लक (Remaining)</th>
168
  <th className="px-6 py-4 text-center">स्थिती (Status)</th>
169
  </tr>
170
  </thead>
171
  <tbody className="divide-y divide-gray-100">
172
- {aggregatedByMirchi.length === 0 ? (
173
  <tr>
174
  <td
175
- colSpan={4}
176
  className="text-center py-8 text-gray-500"
177
  >
178
  No active stock found
179
  </td>
180
  </tr>
181
  ) : (
182
- aggregatedByMirchi.map((row) => {
183
  const isLow = row.remainingQty < LOW_STOCK_THRESHOLD;
184
  const statusLabel =
185
  row.remainingQty === 0
186
  ? 'Out of Stock'
187
  : isLow
188
- ? 'Low Stock'
189
- : 'Available';
190
  const statusClasses =
191
  row.remainingQty === 0
192
  ? 'bg-red-100 text-red-700'
193
  : isLow
194
- ? 'bg-orange-100 text-orange-700'
195
- : 'bg-green-100 text-green-700';
196
 
197
  return (
198
  <tr
199
- key={row.mirchiId}
200
  className="hover:bg-gray-50 transition-colors cursor-pointer"
201
  onClick={() => {
202
- setSelectedMirchiId(row.mirchiId);
203
- setSelectedMirchiName(row.mirchiName);
204
  setViewMode('detail');
205
  }}
206
  >
207
- <td className="px-6 py-4 font-medium text-teal-700">
 
 
 
208
  {row.mirchiName}
209
  </td>
210
  <td className="px-6 py-4 text-right">
211
- {row.totalQty} kg
 
 
212
  </td>
213
  <td className="px-6 py-4 text-right">
214
  <span
215
- className={`font-bold ${
216
- isLow ? 'text-red-500' : 'text-gray-800'
217
- }`}
218
  >
219
  {row.remainingQty} kg
220
  </span>
@@ -236,43 +266,43 @@ const StockReport = () => {
236
 
237
  {/* Mobile cards */}
238
  <div className="md:hidden flex flex-col divide-y divide-gray-100">
239
- {aggregatedByMirchi.length === 0 ? (
240
  <div className="p-6 text-center text-gray-500 text-sm">
241
  No active stock found
242
  </div>
243
  ) : (
244
- aggregatedByMirchi.map((row) => {
245
  const isLow = row.remainingQty < LOW_STOCK_THRESHOLD;
246
  const statusLabel =
247
  row.remainingQty === 0
248
  ? 'Out of Stock'
249
  : isLow
250
- ? 'Low Stock'
251
- : 'Available';
252
  const statusClasses =
253
  row.remainingQty === 0
254
  ? 'bg-red-100 text-red-700'
255
  : isLow
256
- ? 'bg-orange-100 text-orange-700'
257
- : 'bg-green-100 text-green-700';
258
 
259
  return (
260
  <button
261
- key={row.mirchiName}
262
  className="text-left p-4 space-y-2 hover:bg-gray-50 transition-colors"
263
  onClick={() => {
264
- setSelectedMirchiId(row.mirchiId);
265
- setSelectedMirchiName(row.mirchiName);
266
  setViewMode('detail');
267
  }}
268
  >
269
  <div className="flex items-center justify-between gap-2">
270
  <div>
271
- <div className="text-sm font-semibold text-gray-900">
272
- {row.mirchiName}
273
  </div>
274
  <div className="text-xs text-gray-500">
275
- Mirchi Jaat / Type
276
  </div>
277
  </div>
278
  <span
@@ -281,17 +311,16 @@ const StockReport = () => {
281
  {statusLabel}
282
  </span>
283
  </div>
284
- <div className="grid grid-cols-2 gap-2 text-xs mt-1">
285
  <div className="bg-gray-50 p-2 rounded">
286
- <div className="text-gray-500">Total Qty</div>
287
- <div className="font-medium">{row.totalQty} kg</div>
288
  </div>
289
  <div className="bg-gray-50 p-2 rounded">
290
- <div className="text-gray-500">Remaining</div>
291
  <div
292
- className={`font-bold ${
293
- isLow ? 'text-red-500' : 'text-gray-800'
294
- }`}
295
  >
296
  {row.remainingQty} kg
297
  </div>
@@ -313,6 +342,8 @@ const StockReport = () => {
313
  <th className="px-6 py-4">तारीख (Date)</th>
314
  <th className="px-6 py-4">बिल नंबर (Bill No)</th>
315
  <th className="px-6 py-4">पार्टी (Party)</th>
 
 
316
  <th className="px-6 py-4 text-right">माल इन (In Qty)</th>
317
  <th className="px-6 py-4 text-right">माल आउट (Out Qty)</th>
318
  <th className="px-6 py-4 text-center">टाइप (Type)</th>
@@ -321,7 +352,7 @@ const StockReport = () => {
321
  <tbody className="divide-y divide-gray-100">
322
  {detailMovements.length === 0 ? (
323
  <tr>
324
- <td colSpan={6} className="px-6 py-8 text-center text-gray-500">
325
  No movements found for this Mirchi type.
326
  </td>
327
  </tr>
@@ -330,8 +361,14 @@ const StockReport = () => {
330
  <tr key={row.id} className="hover:bg-gray-50">
331
  <td className="px-6 py-4 text-gray-600">{row.date}</td>
332
  <td className="px-6 py-4 font-mono text-gray-500">{row.billNo}</td>
333
- <td className="px-6 py-4 text-gray-800">{row.partyName}</td>
334
- <td className="px-6 py-4 text-right text-green-700 font-medium">
 
 
 
 
 
 
335
  {row.inQty > 0 ? `${row.inQty} kg` : '-'}
336
  </td>
337
  <td className="px-6 py-4 text-right text-red-600 font-medium">
@@ -339,13 +376,12 @@ const StockReport = () => {
339
  </td>
340
  <td className="px-6 py-4 text-center">
341
  <span
342
- className={`inline-block px-2 py-1 rounded-full text-xs font-semibold ${
343
- row.typeLabel.includes('Return')
344
- ? 'bg-orange-100 text-orange-700'
345
- : row.typeLabel === 'Purchase'
346
  ? 'bg-teal-100 text-teal-700'
347
  : 'bg-blue-100 text-blue-700'
348
- }`}
349
  >
350
  {row.typeLabel}
351
  </span>
@@ -377,13 +413,12 @@ const StockReport = () => {
377
  </div>
378
  </div>
379
  <span
380
- className={`px-2 py-1 rounded-full text-[10px] font-semibold ${
381
- row.typeLabel.includes('Return')
382
- ? 'bg-orange-100 text-orange-700'
383
- : row.typeLabel === 'Purchase'
384
  ? 'bg-teal-100 text-teal-700'
385
  : 'bg-blue-100 text-blue-700'
386
- }`}
387
  >
388
  {row.typeLabel}
389
  </span>
 
31
  [lots, searchTerm]
32
  );
33
 
34
+ const aggregatedByLot = useMemo(() => {
35
+ // Calculate bag count for each lot from transactions
36
+ return filteredLots.map(lot => {
37
+ let totalBags = 0;
38
 
39
+ // Calculate bags from all transactions for this lot
40
+ transactions.forEach(tx => {
41
+ tx.items.forEach(item => {
42
+ if (item.lot_id === lot.id) {
43
+ if (tx.bill_type === BillType.AWAAK) {
44
+ // Purchase: Add bags
45
+ totalBags += tx.is_return ? -item.poti_count : item.poti_count;
46
+ } else {
47
+ // Sale: Subtract bags
48
+ totalBags += tx.is_return ? item.poti_count : -item.poti_count;
49
+ }
50
+ }
51
+ });
52
+ });
53
+
54
+ return {
55
+ lotId: lot.id,
56
+ lotNumber: lot.lot_number,
57
  mirchiName: lot.mirchi_name,
58
+ totalQuantity: lot.total_quantity,
59
+ remainingQty: lot.remaining_quantity,
60
+ totalBags: Math.max(0, totalBags),
61
+ status: lot.status,
62
+ purchaseDate: lot.purchase_date,
63
+ avgRate: lot.avg_rate
64
  };
 
 
 
65
  });
66
+ }, [filteredLots, transactions]);
 
 
67
 
68
  const detailMovements = useMemo(() => {
69
  if (!selectedMirchiId) return [];
 
75
  partyName: string;
76
  inQty: number;
77
  outQty: number;
78
+ inBags: number;
79
+ outBags: number;
80
  typeLabel: string;
81
  isReturn: boolean;
82
  }[] = [];
83
 
84
  transactions.forEach((tx) => {
85
  tx.items
86
+ .filter((item) => item.lot_id === selectedMirchiId) // Filter by lot_id instead of mirchi_type_id
87
  .forEach((item) => {
88
  const party = parties.find((p) => p.id === tx.party_id);
89
  let inQty = 0;
90
  let outQty = 0;
91
+ let inBags = 0;
92
+ let outBags = 0;
93
  let typeLabel = '';
94
 
95
+ if (tx.bill_type === BillType.AWAAK) {
96
+ // Awaak = Purchase / Stock IN
97
  if (tx.is_return) {
98
  // Purchase Return: Stock OUT
99
  outQty = item.net_weight;
100
+ outBags = item.poti_count;
101
  typeLabel = 'Purchase Return';
102
  } else {
103
  inQty = item.net_weight;
104
+ inBags = item.poti_count;
105
  typeLabel = 'Purchase';
106
  }
107
  } else {
108
+ // Jawaak = Sales / Stock OUT
109
  if (tx.is_return) {
110
  // Sales Return: Stock IN
111
  inQty = item.net_weight;
112
+ inBags = item.poti_count;
113
  typeLabel = 'Sales Return';
114
  } else {
115
  outQty = item.net_weight;
116
+ outBags = item.poti_count;
117
  typeLabel = 'Sale';
118
  }
119
  }
 
125
  partyName: party?.name || tx.party_name || 'Unknown Party',
126
  inQty,
127
  outQty,
128
+ inBags,
129
+ outBags,
130
  typeLabel,
131
  isReturn: tx.is_return,
132
  });
 
136
  // Sort latest first
137
  rows.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
138
  return rows;
139
+ }, [selectedMirchiId, transactions, parties]);
140
 
141
  return (
142
  <div className="space-y-4">
 
187
  <table className="w-full text-sm text-left">
188
  <thead className="bg-gray-50 text-gray-500 font-medium">
189
  <tr>
190
+ <th className="px-6 py-4">LOT Number</th>
191
  <th className="px-6 py-4">मिरची जात (Mirchi Type)</th>
192
+ <th className="px-6 py-4 text-right">बॅग/पोती (Bags)</th>
193
+ <th className="px-6 py-4 text-right">शिल्लक वजन (Remaining Qty)</th>
194
  <th className="px-6 py-4 text-center">स्थिती (Status)</th>
195
  </tr>
196
  </thead>
197
  <tbody className="divide-y divide-gray-100">
198
+ {aggregatedByLot.length === 0 ? (
199
  <tr>
200
  <td
201
+ colSpan={5}
202
  className="text-center py-8 text-gray-500"
203
  >
204
  No active stock found
205
  </td>
206
  </tr>
207
  ) : (
208
+ aggregatedByLot.map((row) => {
209
  const isLow = row.remainingQty < LOW_STOCK_THRESHOLD;
210
  const statusLabel =
211
  row.remainingQty === 0
212
  ? 'Out of Stock'
213
  : isLow
214
+ ? 'Low Stock'
215
+ : 'Available';
216
  const statusClasses =
217
  row.remainingQty === 0
218
  ? 'bg-red-100 text-red-700'
219
  : isLow
220
+ ? 'bg-orange-100 text-orange-700'
221
+ : 'bg-green-100 text-green-700';
222
 
223
  return (
224
  <tr
225
+ key={row.lotId}
226
  className="hover:bg-gray-50 transition-colors cursor-pointer"
227
  onClick={() => {
228
+ setSelectedMirchiId(row.lotId);
229
+ setSelectedMirchiName(row.lotNumber);
230
  setViewMode('detail');
231
  }}
232
  >
233
+ <td className="px-6 py-4 font-mono text-sm font-medium text-teal-700">
234
+ {row.lotNumber}
235
+ </td>
236
+ <td className="px-6 py-4">
237
  {row.mirchiName}
238
  </td>
239
  <td className="px-6 py-4 text-right">
240
+ <span className="font-medium text-gray-700">
241
+ {row.totalBags} bags
242
+ </span>
243
  </td>
244
  <td className="px-6 py-4 text-right">
245
  <span
246
+ className={`font-bold ${isLow ? 'text-red-500' : 'text-gray-800'
247
+ }`}
 
248
  >
249
  {row.remainingQty} kg
250
  </span>
 
266
 
267
  {/* Mobile cards */}
268
  <div className="md:hidden flex flex-col divide-y divide-gray-100">
269
+ {aggregatedByLot.length === 0 ? (
270
  <div className="p-6 text-center text-gray-500 text-sm">
271
  No active stock found
272
  </div>
273
  ) : (
274
+ aggregatedByLot.map((row) => {
275
  const isLow = row.remainingQty < LOW_STOCK_THRESHOLD;
276
  const statusLabel =
277
  row.remainingQty === 0
278
  ? 'Out of Stock'
279
  : isLow
280
+ ? 'Low Stock'
281
+ : 'Available';
282
  const statusClasses =
283
  row.remainingQty === 0
284
  ? 'bg-red-100 text-red-700'
285
  : isLow
286
+ ? 'bg-orange-100 text-orange-700'
287
+ : 'bg-green-100 text-green-700';
288
 
289
  return (
290
  <button
291
+ key={row.lotId}
292
  className="text-left p-4 space-y-2 hover:bg-gray-50 transition-colors"
293
  onClick={() => {
294
+ setSelectedMirchiId(row.lotId);
295
+ setSelectedMirchiName(row.lotNumber);
296
  setViewMode('detail');
297
  }}
298
  >
299
  <div className="flex items-center justify-between gap-2">
300
  <div>
301
+ <div className="text-sm font-mono font-semibold text-teal-700">
302
+ {row.lotNumber}
303
  </div>
304
  <div className="text-xs text-gray-500">
305
+ {row.mirchiName}
306
  </div>
307
  </div>
308
  <span
 
311
  {statusLabel}
312
  </span>
313
  </div>
314
+ <div className="grid grid-cols-2 gap-2 text-xs">
315
  <div className="bg-gray-50 p-2 rounded">
316
+ <div className="text-gray-500">Bags</div>
317
+ <div className="font-medium">{row.totalBags} bags</div>
318
  </div>
319
  <div className="bg-gray-50 p-2 rounded">
320
+ <div className="text-gray-500">Remaining Qty</div>
321
  <div
322
+ className={`font-bold ${isLow ? 'text-red-500' : 'text-gray-800'
323
+ }`}
 
324
  >
325
  {row.remainingQty} kg
326
  </div>
 
342
  <th className="px-6 py-4">तारीख (Date)</th>
343
  <th className="px-6 py-4">बिल नंबर (Bill No)</th>
344
  <th className="px-6 py-4">पार्टी (Party)</th>
345
+ <th className="px-6 py-4 text-right">बॅग इन (In Bags)</th>
346
+ <th className="px-6 py-4 text-right">बॅग आउट (Out Bags)</th>
347
  <th className="px-6 py-4 text-right">माल इन (In Qty)</th>
348
  <th className="px-6 py-4 text-right">माल आउट (Out Qty)</th>
349
  <th className="px-6 py-4 text-center">टाइप (Type)</th>
 
352
  <tbody className="divide-y divide-gray-100">
353
  {detailMovements.length === 0 ? (
354
  <tr>
355
+ <td colSpan={8} className="px-6 py-8 text-center text-gray-500">
356
  No movements found for this Mirchi type.
357
  </td>
358
  </tr>
 
361
  <tr key={row.id} className="hover:bg-gray-50">
362
  <td className="px-6 py-4 text-gray-600">{row.date}</td>
363
  <td className="px-6 py-4 font-mono text-gray-500">{row.billNo}</td>
364
+ <td className="px-6 py-4 text-gray-700">{row.partyName}</td>
365
+ <td className="px-6 py-4 text-right text-green-600 font-medium">
366
+ {row.inBags > 0 ? `${row.inBags} bags` : '-'}
367
+ </td>
368
+ <td className="px-6 py-4 text-right text-red-600 font-medium">
369
+ {row.outBags > 0 ? `${row.outBags} bags` : '-'}
370
+ </td>
371
+ <td className="px-6 py-4 text-right text-green-600 font-medium">
372
  {row.inQty > 0 ? `${row.inQty} kg` : '-'}
373
  </td>
374
  <td className="px-6 py-4 text-right text-red-600 font-medium">
 
376
  </td>
377
  <td className="px-6 py-4 text-center">
378
  <span
379
+ className={`inline-block px-2 py-1 rounded-full text-xs font-semibold ${row.typeLabel.includes('Return')
380
+ ? 'bg-orange-100 text-orange-700'
381
+ : row.typeLabel === 'Purchase'
 
382
  ? 'bg-teal-100 text-teal-700'
383
  : 'bg-blue-100 text-blue-700'
384
+ }`}
385
  >
386
  {row.typeLabel}
387
  </span>
 
413
  </div>
414
  </div>
415
  <span
416
+ className={`px-2 py-1 rounded-full text-[10px] font-semibold ${row.typeLabel.includes('Return')
417
+ ? 'bg-orange-100 text-orange-700'
418
+ : row.typeLabel === 'Purchase'
 
419
  ? 'bg-teal-100 text-teal-700'
420
  : 'bg-blue-100 text-blue-700'
421
+ }`}
422
  >
423
  {row.typeLabel}
424
  </span>
public/service-worker.js CHANGED
@@ -1,18 +1,22 @@
1
- const CACHE_NAME = 'pattanshetty-v1';
2
  const urlsToCache = [
3
  '/',
4
  '/index.html',
5
- '/src/main.tsx',
6
- '/src/App.tsx',
7
  ];
8
 
9
  // Install event - cache resources
10
  self.addEventListener('install', (event) => {
 
11
  event.waitUntil(
12
  caches.open(CACHE_NAME)
13
  .then((cache) => {
14
- console.log('Opened cache');
15
- return cache.addAll(urlsToCache);
 
 
 
 
 
16
  })
17
  );
18
  self.skipWaiting();
@@ -20,49 +24,69 @@ self.addEventListener('install', (event) => {
20
 
21
  // Activate event - clean up old caches
22
  self.addEventListener('activate', (event) => {
 
23
  event.waitUntil(
24
  caches.keys().then((cacheNames) => {
25
  return Promise.all(
26
  cacheNames.map((cacheName) => {
27
  if (cacheName !== CACHE_NAME) {
28
- console.log('Deleting old cache:', cacheName);
29
  return caches.delete(cacheName);
30
  }
31
  })
32
  );
 
 
33
  })
34
  );
35
  self.clients.claim();
36
  });
37
 
38
- // Fetch event - serve from cache, fallback to network
39
  self.addEventListener('fetch', (event) => {
 
 
 
 
 
40
  event.respondWith(
41
- caches.match(event.request)
42
  .then((response) => {
43
- // Cache hit - return response
44
- if (response) {
45
  return response;
46
  }
47
 
48
- return fetch(event.request).then(
49
- (response) => {
50
- // Check if valid response
51
- if (!response || response.status !== 200 || response.type !== 'basic') {
52
- return response;
53
- }
54
-
55
- // Clone the response
56
- const responseToCache = response.clone();
57
 
58
- caches.open(CACHE_NAME)
59
- .then((cache) => {
60
- cache.put(event.request, responseToCache);
61
- });
 
 
 
62
 
63
- return response;
64
- }
65
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  })
67
  );
68
  });
 
1
+ const CACHE_NAME = 'pattanshetty-v2';
2
  const urlsToCache = [
3
  '/',
4
  '/index.html',
 
 
5
  ];
6
 
7
  // Install event - cache resources
8
  self.addEventListener('install', (event) => {
9
+ console.log('[ServiceWorker] Installing...');
10
  event.waitUntil(
11
  caches.open(CACHE_NAME)
12
  .then((cache) => {
13
+ console.log('[ServiceWorker] Caching app shell');
14
+ return cache.addAll(urlsToCache).catch((err) => {
15
+ console.error('[ServiceWorker] Cache addAll failed:', err);
16
+ });
17
+ })
18
+ .catch((err) => {
19
+ console.error('[ServiceWorker] Cache open failed:', err);
20
  })
21
  );
22
  self.skipWaiting();
 
24
 
25
  // Activate event - clean up old caches
26
  self.addEventListener('activate', (event) => {
27
+ console.log('[ServiceWorker] Activating...');
28
  event.waitUntil(
29
  caches.keys().then((cacheNames) => {
30
  return Promise.all(
31
  cacheNames.map((cacheName) => {
32
  if (cacheName !== CACHE_NAME) {
33
+ console.log('[ServiceWorker] Deleting old cache:', cacheName);
34
  return caches.delete(cacheName);
35
  }
36
  })
37
  );
38
+ }).catch((err) => {
39
+ console.error('[ServiceWorker] Activation failed:', err);
40
  })
41
  );
42
  self.clients.claim();
43
  });
44
 
45
+ // Fetch event - Network first, fallback to cache
46
  self.addEventListener('fetch', (event) => {
47
+ // Skip non-GET requests
48
+ if (event.request.method !== 'GET') {
49
+ return;
50
+ }
51
+
52
  event.respondWith(
53
+ fetch(event.request)
54
  .then((response) => {
55
+ // Check if valid response
56
+ if (!response || response.status !== 200 || response.type === 'error') {
57
  return response;
58
  }
59
 
60
+ // Clone the response
61
+ const responseToCache = response.clone();
 
 
 
 
 
 
 
62
 
63
+ caches.open(CACHE_NAME)
64
+ .then((cache) => {
65
+ cache.put(event.request, responseToCache);
66
+ })
67
+ .catch((err) => {
68
+ console.error('[ServiceWorker] Cache put failed:', err);
69
+ });
70
 
71
+ return response;
72
+ })
73
+ .catch((err) => {
74
+ console.log('[ServiceWorker] Fetch failed, trying cache:', err);
75
+ // Network failed, try cache
76
+ return caches.match(event.request)
77
+ .then((cachedResponse) => {
78
+ if (cachedResponse) {
79
+ return cachedResponse;
80
+ }
81
+ // Return offline page or error
82
+ return new Response('Offline - No cached version available', {
83
+ status: 503,
84
+ statusText: 'Service Unavailable',
85
+ headers: new Headers({
86
+ 'Content-Type': 'text/plain'
87
+ })
88
+ });
89
+ });
90
  })
91
  );
92
  });
services/db.ts CHANGED
@@ -185,6 +185,41 @@ export const getActiveLots = async (): Promise<Lot[]> => {
185
  }
186
  };
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  // Transactions API
189
  export const getTransactions = async (): Promise<Transaction[]> => {
190
  try {
 
185
  }
186
  };
187
 
188
+ // Check if lot number is unique
189
+ export const checkLotUnique = async (lotNumber: string): Promise<boolean> => {
190
+ try {
191
+ const res = await fetch(`${API_BASE}/lots/check-unique`, {
192
+ method: 'POST',
193
+ headers: { 'Content-Type': 'application/json' },
194
+ body: JSON.stringify({ lot_number: lotNumber }),
195
+ });
196
+ if (!res.ok) throw new Error('Failed to check lot uniqueness');
197
+ const data = await res.json();
198
+ return !data.exists; // Return true if unique (not exists)
199
+ } catch (error) {
200
+ console.error('Error checking lot uniqueness:', error);
201
+ return false;
202
+ }
203
+ };
204
+
205
+ // Get available lots for a specific mirchi type
206
+ export const getAvailableLotsByMirchi = async (mirchiTypeId: string): Promise<Lot[]> => {
207
+ try {
208
+ const res = await fetch(`${API_BASE}/lots/available/${mirchiTypeId}`);
209
+ if (!res.ok) throw new Error('Failed to load available lots');
210
+ const data = await res.json();
211
+ return data.map((l: any) => ({
212
+ ...l,
213
+ total_quantity: parseNumeric(l.total_quantity),
214
+ remaining_quantity: parseNumeric(l.remaining_quantity),
215
+ avg_rate: parseNumeric(l.avg_rate)
216
+ }));
217
+ } catch (error) {
218
+ console.error('Error fetching available lots:', error);
219
+ return [];
220
+ }
221
+ };
222
+
223
  // Transactions API
224
  export const getTransactions = async (): Promise<Transaction[]> => {
225
  try {
utils/dateFormatter.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Date formatting utility for consistent date display across the app
2
+
3
+ /**
4
+ * Formats a date string to DD/MM/YYYY format
5
+ * @param dateStr - Date string in any format
6
+ * @returns Formatted date string (DD/MM/YYYY)
7
+ */
8
+ export const formatDate = (dateStr: string | Date): string => {
9
+ const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr;
10
+
11
+ if (isNaN(date.getTime())) {
12
+ return '-';
13
+ }
14
+
15
+ return date.toLocaleDateString('en-IN', {
16
+ day: '2-digit',
17
+ month: '2-digit',
18
+ year: 'numeric'
19
+ });
20
+ };
21
+
22
+ /**
23
+ * Formats a date string to YYYY-MM-DD format (for input fields)
24
+ * @param dateStr - Date string in any format
25
+ * @returns Formatted date string (YYYY-MM-DD)
26
+ */
27
+ export const formatDateForInput = (dateStr: string | Date): string => {
28
+ const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr;
29
+
30
+ if (isNaN(date.getTime())) {
31
+ return '';
32
+ }
33
+
34
+ const year = date.getFullYear();
35
+ const month = String(date.getMonth() + 1).padStart(2, '0');
36
+ const day = String(date.getDate()).padStart(2, '0');
37
+
38
+ return `${year}-${month}-${day}`;
39
+ };
40
+
41
+ /**
42
+ * Gets today's date in YYYY-MM-DD format
43
+ * @returns Today's date string
44
+ */
45
+ export const getTodayDate = (): string => {
46
+ return formatDateForInput(new Date());
47
+ };
48
+
49
+ /**
50
+ * Formats a date to display only date (no time)
51
+ * @param dateStr - Date string
52
+ * @returns Formatted date string
53
+ */
54
+ export const formatDateOnly = (dateStr: string | Date): string => {
55
+ return formatDate(dateStr);
56
+ };
vite.config.ts CHANGED
@@ -1,16 +1,16 @@
1
- import { defineConfig } from 'vite'
2
- import react from '@vitejs/plugin-react'
3
-
4
- export default defineConfig({
5
- plugins: [react()],
6
- server: {
7
- host: true, // Listen on all addresses
8
- port: 3000
9
- },
10
- preview: {
11
- host: true, // Listen on all addresses
12
- port: 7860,
13
- strictPort: true,
14
- allowedHosts: ['antaram-pratikpattanshetty.hf.space'] // Add this line
15
- }
16
- })
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ host: true, // Listen on all addresses
8
+ port: 3000
9
+ },
10
+ preview: {
11
+ host: true, // Listen on all addresses
12
+ port: 7860,
13
+ strictPort: true,
14
+ allowedHosts: ['antaram-pratikpattanshetty.hf.space'] // Add this line
15
+ }
16
+ })