Spaces:
Sleeping
Sleeping
Upload 28 files
Browse files- App.tsx +18 -13
- components/PWAInstallPrompt.tsx +98 -0
- components/PdfInvoice.tsx +219 -0
- components/PrintInvoice.tsx +268 -332
- context/PWAContext.tsx +83 -0
- index.html +77 -43
- package-lock.json +232 -0
- package.json +3 -0
- pages/AwaakBill.tsx +158 -43
- pages/JawaakBill.tsx +109 -45
- pages/PartyLedger.tsx +102 -57
- pages/Settings.tsx +3 -26
- pages/StockReport.tsx +103 -68
- public/service-worker.js +50 -26
- services/db.ts +35 -0
- utils/dateFormatter.ts +56 -0
- vite.config.ts +16 -16
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 |
-
<
|
| 29 |
-
<
|
| 30 |
-
<
|
| 31 |
-
<
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
| 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, {
|
| 2 |
-
import { Transaction
|
|
|
|
| 3 |
|
| 4 |
interface PrintInvoiceProps {
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
businessAddress?: string;
|
| 8 |
-
businessGSTIN?: string;
|
| 9 |
-
businessPhone?: string;
|
| 10 |
}
|
| 11 |
|
| 12 |
-
const PrintInvoice: React.FC<PrintInvoiceProps> = ({
|
| 13 |
-
|
| 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 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
}, [transaction]);
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
setTimeout(() => {
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
}
|
|
|
|
| 193 |
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
| 201 |
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
<
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
</div>
|
|
|
|
| 226 |
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
</div>
|
|
|
|
| 239 |
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
</table>
|
| 275 |
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
</div>
|
| 333 |
|
| 334 |
-
{
|
| 335 |
-
|
| 336 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
</div>
|
|
|
|
| 338 |
|
| 339 |
-
|
| 340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
</div>
|
|
|
|
|
|
|
| 342 |
</div>
|
|
|
|
| 343 |
</div>
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 58 |
-
|
|
|
|
| 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 {
|
| 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:
|
| 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 =
|
| 125 |
-
const net = 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
|
| 188 |
-
|
| 189 |
-
const
|
|
|
|
| 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.
|
| 325 |
-
<
|
| 326 |
-
|
| 327 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
</div>
|
| 329 |
|
| 330 |
-
{/*
|
| 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 |
-
<
|
| 349 |
-
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
</div>
|
| 352 |
|
| 353 |
{/* 5. Gaadi Bharni */}
|
|
@@ -516,9 +591,13 @@ const AwaakBill = () => {
|
|
| 516 |
</button>
|
| 517 |
</div>
|
| 518 |
|
| 519 |
-
<
|
| 520 |
-
|
| 521 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =>
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 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 {
|
| 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:
|
| 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
|
| 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 =
|
| 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.
|
| 333 |
-
<
|
| 334 |
-
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
</div>
|
| 337 |
|
| 338 |
-
{/* 2.
|
| 339 |
<SummaryInput
|
| 340 |
-
label={`
|
| 341 |
-
value={expenses.
|
| 342 |
-
onChange={val => setExpenses({ ...expenses,
|
| 343 |
/>
|
| 344 |
<div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
|
| 345 |
<span>Amount:</span>
|
| 346 |
-
<span>₹{
|
| 347 |
</div>
|
| 348 |
|
| 349 |
-
{/* 3.
|
| 350 |
<SummaryInput
|
| 351 |
-
label={`
|
| 352 |
-
value={expenses.
|
| 353 |
-
onChange={val => setExpenses({ ...expenses,
|
| 354 |
/>
|
| 355 |
<div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
|
| 356 |
<span>Amount:</span>
|
| 357 |
-
<span>₹{
|
| 358 |
</div>
|
| 359 |
|
| 360 |
{/* 4. Hamali */}
|
| 361 |
-
<
|
| 362 |
-
|
| 363 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
</div>
|
| 365 |
|
| 366 |
{/* 5. Packaging Hamali */}
|
|
@@ -532,9 +568,13 @@ const JawaakBill = () => {
|
|
| 532 |
</button>
|
| 533 |
</div>
|
| 534 |
|
| 535 |
-
<
|
| 536 |
-
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =>
|
| 582 |
>
|
| 583 |
<option value="">Select Type</option>
|
| 584 |
{mirchiTypes.map(m => (
|
|
@@ -586,7 +626,27 @@ const JawaakBill = () => {
|
|
| 586 |
))}
|
| 587 |
</select>
|
| 588 |
</div>
|
| 589 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 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 |
-
<
|
| 122 |
-
|
| 123 |
-
|
| 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 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 514 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 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
|
| 35 |
-
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
mirchiName: lot.mirchi_name,
|
| 42 |
-
|
| 43 |
-
remainingQty:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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.
|
| 77 |
-
//
|
| 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 |
-
//
|
| 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 |
-
}, [
|
| 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">
|
| 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 |
-
{
|
| 173 |
<tr>
|
| 174 |
<td
|
| 175 |
-
colSpan={
|
| 176 |
className="text-center py-8 text-gray-500"
|
| 177 |
>
|
| 178 |
No active stock found
|
| 179 |
</td>
|
| 180 |
</tr>
|
| 181 |
) : (
|
| 182 |
-
|
| 183 |
const isLow = row.remainingQty < LOW_STOCK_THRESHOLD;
|
| 184 |
const statusLabel =
|
| 185 |
row.remainingQty === 0
|
| 186 |
? 'Out of Stock'
|
| 187 |
: isLow
|
| 188 |
-
|
| 189 |
-
|
| 190 |
const statusClasses =
|
| 191 |
row.remainingQty === 0
|
| 192 |
? 'bg-red-100 text-red-700'
|
| 193 |
: isLow
|
| 194 |
-
|
| 195 |
-
|
| 196 |
|
| 197 |
return (
|
| 198 |
<tr
|
| 199 |
-
key={row.
|
| 200 |
className="hover:bg-gray-50 transition-colors cursor-pointer"
|
| 201 |
onClick={() => {
|
| 202 |
-
setSelectedMirchiId(row.
|
| 203 |
-
setSelectedMirchiName(row.
|
| 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 |
-
|
|
|
|
|
|
|
| 212 |
</td>
|
| 213 |
<td className="px-6 py-4 text-right">
|
| 214 |
<span
|
| 215 |
-
className={`font-bold ${
|
| 216 |
-
|
| 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 |
-
{
|
| 240 |
<div className="p-6 text-center text-gray-500 text-sm">
|
| 241 |
No active stock found
|
| 242 |
</div>
|
| 243 |
) : (
|
| 244 |
-
|
| 245 |
const isLow = row.remainingQty < LOW_STOCK_THRESHOLD;
|
| 246 |
const statusLabel =
|
| 247 |
row.remainingQty === 0
|
| 248 |
? 'Out of Stock'
|
| 249 |
: isLow
|
| 250 |
-
|
| 251 |
-
|
| 252 |
const statusClasses =
|
| 253 |
row.remainingQty === 0
|
| 254 |
? 'bg-red-100 text-red-700'
|
| 255 |
: isLow
|
| 256 |
-
|
| 257 |
-
|
| 258 |
|
| 259 |
return (
|
| 260 |
<button
|
| 261 |
-
key={row.
|
| 262 |
className="text-left p-4 space-y-2 hover:bg-gray-50 transition-colors"
|
| 263 |
onClick={() => {
|
| 264 |
-
setSelectedMirchiId(row.
|
| 265 |
-
setSelectedMirchiName(row.
|
| 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-
|
| 272 |
-
{row.
|
| 273 |
</div>
|
| 274 |
<div className="text-xs text-gray-500">
|
| 275 |
-
|
| 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
|
| 285 |
<div className="bg-gray-50 p-2 rounded">
|
| 286 |
-
<div className="text-gray-500">
|
| 287 |
-
<div className="font-medium">{row.
|
| 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 |
-
|
| 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={
|
| 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-
|
| 334 |
-
<td className="px-6 py-4 text-right text-green-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 344 |
-
|
| 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 |
-
|
| 382 |
-
|
| 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-
|
| 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('
|
| 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 -
|
| 39 |
self.addEventListener('fetch', (event) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
event.respondWith(
|
| 41 |
-
|
| 42 |
.then((response) => {
|
| 43 |
-
//
|
| 44 |
-
if (response) {
|
| 45 |
return response;
|
| 46 |
}
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 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 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
-
|
| 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 |
+
})
|