Antaram commited on
Commit
8dcac3a
·
verified ·
1 Parent(s): c69c07d

Upload 29 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+ build/
7
+
8
+ # Environment files (will be set in HF Spaces)
9
+ .env.local
10
+ .env.development
11
+
12
+ # Git
13
+ .git/
14
+ .gitignore
15
+
16
+ # Logs
17
+ *.log
18
+ npm-debug.log*
19
+
20
+ # IDE
21
+ .vscode/
22
+ .idea/
23
+
24
+ # Test files
25
+ *.test.ts
26
+ *.test.tsx
27
+ __tests__/
28
+
29
+ # Documentation
30
+ README.md
31
+ docs/
.env ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Environment Variables
2
+
3
+ # Backend API URL
4
+ VITE_API_URL=https://antaram-pattanshettybackend.hf.space/api
.env.local ADDED
@@ -0,0 +1 @@
 
 
1
+ GEMINI_API_KEY=PLACEHOLDER_API_KEY
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ public/icon-192.png filter=lfs diff=lfs merge=lfs -text
37
+ public/icon-512.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
App.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect } from 'react';
2
+ import { HashRouter, Routes, Route, Navigate } from 'react-router-dom';
3
+ import Layout from './components/Layout';
4
+ import Dashboard from './pages/Dashboard';
5
+ import JawaakBill from './pages/JawaakBill';
6
+ 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
13
+ useEffect(() => {
14
+ if ('serviceWorker' in navigator) {
15
+ window.addEventListener('load', () => {
16
+ navigator.serviceWorker.register('/service-worker.js')
17
+ .then(registration => {
18
+ console.log('✅ PWA Service Worker registered:', registration);
19
+ })
20
+ .catch(error => {
21
+ console.log('❌ SW registration failed:', error);
22
+ });
23
+ });
24
+ }
25
+ }, []);
26
+
27
+ return (
28
+ <HashRouter>
29
+ <Layout>
30
+ <Routes>
31
+ <Route path="/" element={<Dashboard />} />
32
+ <Route path="/jawaak" element={<JawaakBill />} />
33
+ <Route path="/awaak" element={<AwaakBill />} />
34
+ <Route path="/stock" element={<StockReport />} />
35
+ <Route path="/ledger" element={<PartyLedger />} />
36
+ <Route path="/settings" element={<Settings />} />
37
+ <Route path="*" element={<Navigate to="/" replace />} />
38
+ </Routes>
39
+ </Layout>
40
+ </HashRouter>
41
+ );
42
+ };
43
+
44
+ export default App;
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage build for Vite React app
2
+
3
+ # Stage 1: Build
4
+ FROM node:18-alpine AS builder
5
+
6
+ WORKDIR /app
7
+
8
+ # Copy package files
9
+ COPY package*.json ./
10
+
11
+ # Install dependencies
12
+ RUN npm ci
13
+
14
+ # Copy source code
15
+ COPY . .
16
+
17
+ # Build the app
18
+ RUN npm run build
19
+
20
+ # Stage 2: Serve with nginx
21
+ FROM nginx:alpine
22
+
23
+ # Copy built files from builder
24
+ COPY --from=builder /app/dist /usr/share/nginx/html
25
+
26
+ # Copy nginx configuration
27
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
28
+
29
+ # Expose Hugging Face port
30
+ EXPOSE 7860
31
+
32
+ # Start nginx
33
+ CMD ["nginx", "-g", "daemon off;"]
components/Layout.tsx ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Link, useLocation } from 'react-router-dom';
3
+ import {
4
+ Home,
5
+ FileInput,
6
+ FileOutput,
7
+ Users,
8
+ Package,
9
+ Settings,
10
+ Menu,
11
+ X,
12
+ TrendingUp
13
+ } from 'lucide-react';
14
+
15
+ interface LayoutProps {
16
+ children: React.ReactNode;
17
+ }
18
+
19
+ const Layout: React.FC<LayoutProps> = ({ children }) => {
20
+ const [isSidebarOpen, setSidebarOpen] = useState(false);
21
+ const location = useLocation();
22
+
23
+ const navItems = [
24
+ { path: '/', label: 'डॅशबोर्ड (Home)', icon: Home },
25
+ { path: '/jawaak', label: 'जावक बिल (Sales)', icon: FileOutput },
26
+ { path: '/awaak', label: 'आवक बिल (Purchase)', icon: FileInput },
27
+ { path: '/ledger', label: 'पार्टी लेजर (Ledger)', icon: Users },
28
+ { path: '/stock', label: 'स्टॉक रिपोर्ट (Stock)', icon: Package },
29
+ { path: '/settings', label: 'सेटिंग्स (Settings)', icon: Settings },
30
+ ];
31
+
32
+ const isActive = (path: string) => location.pathname === path;
33
+
34
+ return (
35
+ <div className="flex h-screen bg-gray-50 overflow-hidden">
36
+ {/* Mobile Sidebar Overlay */}
37
+ {isSidebarOpen && (
38
+ <div
39
+ className="fixed inset-0 bg-black bg-opacity-50 z-20 lg:hidden"
40
+ onClick={() => setSidebarOpen(false)}
41
+ />
42
+ )}
43
+
44
+ {/* Sidebar */}
45
+ <aside
46
+ className={`fixed lg:static inset-y-0 left-0 w-64 bg-teal-800 text-white transform transition-transform duration-200 ease-in-out z-30 ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
47
+ }`}
48
+ >
49
+ <div className="flex items-center justify-between p-4 border-b border-teal-700 h-16">
50
+ <div className="flex items-center gap-2">
51
+ <TrendingUp className="w-6 h-6 text-teal-300" />
52
+ <span className="font-bold text-xl">Mirchi Vyapar</span>
53
+ </div>
54
+ <button
55
+ className="lg:hidden p-1 hover:bg-teal-700 rounded"
56
+ onClick={() => setSidebarOpen(false)}
57
+ >
58
+ <X size={24} />
59
+ </button>
60
+ </div>
61
+
62
+ <nav className="p-4 space-y-1">
63
+ {navItems.map((item) => (
64
+ <Link
65
+ key={item.path}
66
+ to={item.path}
67
+ onClick={() => setSidebarOpen(false)}
68
+ className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(item.path)
69
+ ? 'bg-teal-900 text-teal-100 shadow-sm'
70
+ : 'text-teal-100 hover:bg-teal-700'
71
+ }`}
72
+ >
73
+ <item.icon size={20} />
74
+ <span className="font-medium">{item.label}</span>
75
+ </Link>
76
+ ))}
77
+ </nav>
78
+
79
+ <div className="absolute bottom-0 w-full p-4 border-t border-teal-700 bg-teal-800">
80
+ <div className="flex items-center gap-3">
81
+ <div className="w-8 h-8 rounded-full bg-teal-600 flex items-center justify-center font-bold">
82
+ A
83
+ </div>
84
+ <div>
85
+ <p className="text-sm font-medium">Admin User</p>
86
+ <p className="text-xs text-teal-300">Mirchi Mandi</p>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </aside>
91
+
92
+ {/* Main Content */}
93
+ <main className="flex-1 flex flex-col overflow-hidden">
94
+ {/* Top Header */}
95
+ <header className="h-16 bg-white border-b flex items-center justify-between px-4 lg:px-8">
96
+ <button
97
+ className="lg:hidden p-2 -ml-2 text-gray-600 hover:bg-gray-100 rounded-lg"
98
+ onClick={() => setSidebarOpen(true)}
99
+ >
100
+ <Menu size={24} />
101
+ </button>
102
+
103
+ <h1 className="text-xl font-semibold text-gray-800 ml-2 lg:ml-0">
104
+ {navItems.find(i => isActive(i.path))?.label.split(' (')[0] || 'Mirchi Vyapar'}
105
+ </h1>
106
+
107
+ <div className="flex items-center gap-4">
108
+ <span className="text-sm text-gray-500 hidden md:block">
109
+ {new Date().toLocaleDateString('mr-IN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
110
+ </span>
111
+ </div>
112
+ </header>
113
+
114
+ {/* Page Content */}
115
+ <div className="flex-1 overflow-auto p-4 lg:p-6 pb-20 lg:pb-6">
116
+ {children}
117
+ </div>
118
+ </main>
119
+ </div>
120
+ );
121
+ };
122
+
123
+ export default Layout;
components/PrintInvoice.tsx ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef } from 'react';
2
+ import { Transaction, BillType } from '../types';
3
+
4
+ interface PrintInvoiceProps {
5
+ transaction: Transaction;
6
+ businessName?: string;
7
+ businessAddress?: string;
8
+ businessGSTIN?: string;
9
+ businessPhone?: string;
10
+ }
11
+
12
+ const PrintInvoice: React.FC<PrintInvoiceProps> = ({
13
+ transaction,
14
+ businessName = 'Pattanshetty Traders',
15
+ businessAddress = 'Market Yard, Sangli',
16
+ businessGSTIN = 'GSTIN: 27ABCDE1234F1Z5',
17
+ businessPhone = 'फोन: 98765 43210'
18
+ }) => {
19
+ const printRef = useRef<HTMLDivElement>(null);
20
+
21
+ // Debug logging
22
+ React.useEffect(() => {
23
+ console.log('PrintInvoice - Transaction:', transaction);
24
+ console.log('PrintInvoice - Items count:', transaction.items?.length);
25
+ console.log('PrintInvoice - Items:', transaction.items);
26
+ }, [transaction]);
27
+
28
+ const handlePrint = () => {
29
+ const printContent = printRef.current;
30
+ if (!printContent) return;
31
+
32
+ const printWindow = window.open('', '', 'width=800,height=600');
33
+ if (!printWindow) return;
34
+
35
+ printWindow.document.write(`
36
+ <!DOCTYPE html>
37
+ <html>
38
+ <head>
39
+ <title>Invoice - ${transaction.bill_number}</title>
40
+ <style>
41
+ @media print {
42
+ @page {
43
+ size: A4;
44
+ margin: 10mm 8mm;
45
+ }
46
+ body {
47
+ margin: 0;
48
+ padding: 0;
49
+ }
50
+ }
51
+ * {
52
+ margin: 0;
53
+ padding: 0;
54
+ box-sizing: border-box;
55
+ }
56
+ body {
57
+ font-family: Arial, sans-serif;
58
+ font-size: 11pt;
59
+ line-height: 1.3;
60
+ color: #000;
61
+ }
62
+ .invoice-container {
63
+ width: 210mm;
64
+ height: 148.5mm;
65
+ padding: 8mm 10mm;
66
+ page-break-after: always;
67
+ }
68
+ .header {
69
+ padding-bottom: 8px;
70
+ margin-bottom: 10px;
71
+ }
72
+ .header-top {
73
+ display: flex;
74
+ justify-content: space-between;
75
+ align-items: flex-start;
76
+ margin-bottom: 4px;
77
+ }
78
+ .business-name {
79
+ font-size: 16pt;
80
+ font-weight: bold;
81
+ }
82
+ .business-details {
83
+ font-size: 9pt;
84
+ color: #333;
85
+ }
86
+ .bill-info {
87
+ text-align: right;
88
+ font-size: 9pt;
89
+ }
90
+ .bill-info div {
91
+ margin-bottom: 2px;
92
+ }
93
+ .party-section {
94
+ display: flex;
95
+ justify-content: space-between;
96
+ margin: 8px 0;
97
+ padding: 6px 8px;
98
+ background-color: #f9f9f9;
99
+ border-radius: 4px;
100
+ font-size: 9pt;
101
+ }
102
+ .party-details {
103
+ flex: 1;
104
+ }
105
+ .party-label {
106
+ font-weight: bold;
107
+ margin-bottom: 2px;
108
+ font-size: 10pt;
109
+ }
110
+ .payment-info {
111
+ text-align: right;
112
+ }
113
+ table {
114
+ width: 100%;
115
+ border-collapse: collapse;
116
+ margin: 6px 0;
117
+ font-size: 9pt;
118
+ }
119
+ th {
120
+ background-color: #e8e8e8;
121
+ border: 1px solid #333;
122
+ padding: 3px 4px;
123
+ text-align: left;
124
+ font-weight: bold;
125
+ font-size: 9pt;
126
+ }
127
+ td {
128
+ border: 1px solid #666;
129
+ padding: 3px 4px;
130
+ font-size: 9pt;
131
+ }
132
+ .text-right {
133
+ text-align: right;
134
+ }
135
+ .text-center {
136
+ text-align: center;
137
+ }
138
+ .summary-section {
139
+ margin-top: 6px;
140
+ display: flex;
141
+ justify-content: space-between;
142
+ gap: 10px;
143
+ }
144
+ .summary-left {
145
+ flex: 1;
146
+ padding: 6px;
147
+ background-color: #f9f9f9;
148
+ border-radius: 4px;
149
+ }
150
+ .summary-right {
151
+ width: 180px;
152
+ border: 1px solid #333;
153
+ padding: 6px 8px;
154
+ background-color: #fff;
155
+ }
156
+ .summary-row {
157
+ display: flex;
158
+ justify-content: space-between;
159
+ padding: 2px 0;
160
+ font-size: 9pt;
161
+ }
162
+ .summary-row.total {
163
+ border-top: 2px solid #000;
164
+ margin-top: 4px;
165
+ padding-top: 4px;
166
+ font-weight: bold;
167
+ font-size: 10pt;
168
+ }
169
+ .footer {
170
+ margin-top: 12px;
171
+ font-size: 9pt;
172
+ color: #666;
173
+ }
174
+ .signature {
175
+ text-align: right;
176
+ margin-top: 20px;
177
+ font-size: 10pt;
178
+ }
179
+ </style>
180
+ </head>
181
+ <body>
182
+ ${printContent.innerHTML}
183
+ </body>
184
+ </html>
185
+ `);
186
+
187
+ printWindow.document.close();
188
+ setTimeout(() => {
189
+ printWindow.print();
190
+ printWindow.close();
191
+ }, 250);
192
+ };
193
+
194
+ const formatDate = (dateString: string) => {
195
+ const date = new Date(dateString);
196
+ const day = String(date.getDate()).padStart(2, '0');
197
+ const month = String(date.getMonth() + 1).padStart(2, '0');
198
+ const year = date.getFullYear();
199
+ return `${day}/${month}/${year}`;
200
+ };
201
+
202
+ const billTypeLabel = transaction.bill_type === BillType.JAWAAK
203
+ ? (transaction.is_return ? 'खरेदी परतावा' : 'खरेदी बिल')
204
+ : (transaction.is_return ? 'विक्री परतावा' : 'विक्री बिल');
205
+
206
+ return (
207
+ <>
208
+ {/* Hidden print content */}
209
+ <div style={{ display: 'none' }}>
210
+ <div ref={printRef} className="invoice-container">
211
+ {/* Header */}
212
+ <div className="header">
213
+ <div className="header-top">
214
+ <div>
215
+ <div className="business-name">{businessName}</div>
216
+ <div className="business-details">{businessAddress} • {businessGSTIN}</div>
217
+ <div className="business-details">{businessPhone}</div>
218
+ </div>
219
+ <div className="bill-info">
220
+ <div><strong>बिल दिनांक:</strong> {formatDate(transaction.bill_date)}</div>
221
+ <div><strong>बिल प्रकार:</strong> {billTypeLabel}</div>
222
+ <div><strong>बिल नंबर:</strong> {transaction.bill_number}</div>
223
+ </div>
224
+ </div>
225
+ </div>
226
+
227
+ {/* Party Details */}
228
+ <div className="party-section">
229
+ <div className="party-details">
230
+ <div className="party-label">पार्टी माहिती</div>
231
+ <div><strong>{transaction.party_name}</strong></div>
232
+ <div style={{ fontSize: '9pt', color: '#666' }}>Party ID: {transaction.party_id}</div>
233
+ </div>
234
+ <div className="payment-info">
235
+ <div><strong>दिलेली रक्कम:</strong> ₹{transaction.paid_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</div>
236
+ <div><strong>बाकी रक्कम:</strong> ₹{transaction.balance_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</div>
237
+ </div>
238
+ </div>
239
+
240
+ {/* Items Table */}
241
+ <table>
242
+ <thead>
243
+ <tr>
244
+ <th className="text-center">#</th>
245
+ <th>जात / Particular</th>
246
+ <th className="text-center">Lot</th>
247
+ <th className="text-center">Bags</th>
248
+ <th className="text-right">Gross (kg)</th>
249
+ <th className="text-right">Net (kg)</th>
250
+ <th className="text-right">Rate (₹)</th>
251
+ <th className="text-right">Amount (₹)</th>
252
+ </tr>
253
+ </thead>
254
+ <tbody>
255
+ {transaction.items && transaction.items.length > 0 ? (
256
+ transaction.items.map((item, index) => (
257
+ <tr key={item.id || index}>
258
+ <td className="text-center">{index + 1}</td>
259
+ <td>{item.mirchi_name || 'N/A'}</td>
260
+ <td className="text-center">{item.lot_id ? item.lot_id.substring(0, 8) : '-'}</td>
261
+ <td className="text-center">{item.poti_count || 0}</td>
262
+ <td className="text-right">{(item.gross_weight || 0).toFixed(2)}</td>
263
+ <td className="text-right">{(item.net_weight || 0).toFixed(2)}</td>
264
+ <td className="text-right">{(item.rate_per_kg || 0).toFixed(2)}</td>
265
+ <td className="text-right">₹{(item.item_total || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
266
+ </tr>
267
+ ))
268
+ ) : (
269
+ <tr>
270
+ <td colSpan={8} className="text-center">No items found</td>
271
+ </tr>
272
+ )}
273
+ </tbody>
274
+ </table>
275
+
276
+ {/* Summary Section */}
277
+ <div className="summary-section">
278
+ <div className="summary-left">
279
+ <div style={{ fontSize: '10pt', fontWeight: 'bold', marginBottom: '4px' }}>वजन सारांश</div>
280
+ <div style={{ fontSize: '9pt' }}>
281
+ <div>पोत्या: {transaction.items.reduce((sum, i) => sum + i.poti_count, 0)}</div>
282
+ <div>एकूण वजन: {transaction.gross_weight_total.toFixed(2)} kg</div>
283
+ <div>निव्वळ वजन: {transaction.net_weight_total.toFixed(2)} kg</div>
284
+ </div>
285
+ </div>
286
+ <div className="summary-right">
287
+ <div className="summary-row">
288
+ <span>मूळ रक्कम:</span>
289
+ <span>₹{transaction.subtotal.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
290
+ </div>
291
+ {transaction.expenses.cess_amount > 0 && (
292
+ <div className="summary-row">
293
+ <span>सेस ({transaction.expenses.cess_percent}%):</span>
294
+ <span>₹{transaction.expenses.cess_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
295
+ </div>
296
+ )}
297
+ {transaction.expenses.adat_amount > 0 && (
298
+ <div className="summary-row">
299
+ <span>अडत ({transaction.expenses.adat_percent}%):</span>
300
+ <span>₹{transaction.expenses.adat_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
301
+ </div>
302
+ )}
303
+ {transaction.expenses.poti_amount > 0 && (
304
+ <div className="summary-row">
305
+ <span>पोती:</span>
306
+ <span>₹{transaction.expenses.poti_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
307
+ </div>
308
+ )}
309
+ {transaction.expenses.hamali_amount > 0 && (
310
+ <div className="summary-row">
311
+ <span>हमाली:</span>
312
+ <span>₹{transaction.expenses.hamali_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
313
+ </div>
314
+ )}
315
+ {transaction.expenses.packaging_hamali_amount > 0 && (
316
+ <div className="summary-row">
317
+ <span>पॅकेजिंग हमाली:</span>
318
+ <span>₹{transaction.expenses.packaging_hamali_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
319
+ </div>
320
+ )}
321
+ {transaction.expenses.gaadi_bharni > 0 && (
322
+ <div className="summary-row">
323
+ <span>गाडी भरणी:</span>
324
+ <span>₹{transaction.expenses.gaadi_bharni.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
325
+ </div>
326
+ )}
327
+ <div className="summary-row total">
328
+ <span>एकूण रक्कम:</span>
329
+ <span>₹{transaction.total_amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
330
+ </div>
331
+ </div>
332
+ </div>
333
+
334
+ {/* Footer */}
335
+ <div className="footer">
336
+ नोंद: कृपया माल प्राप्त झाल्यानंतर वजन व रक्कम तपासा. कोणतीही तक्रार 24 तासात नोंदवा.
337
+ </div>
338
+
339
+ <div className="signature">
340
+ (अधिकृत सही)
341
+ </div>
342
+ </div>
343
+ </div>
344
+
345
+ {/* Print Button */}
346
+ <button
347
+ onClick={handlePrint}
348
+ className="px-3 py-1 border border-teal-600 text-teal-600 rounded-md text-xs font-medium hover:bg-teal-50 transition-colors inline-flex items-center gap-1"
349
+ title="Print Invoice"
350
+ >
351
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
352
+ <path fillRule="evenodd" d="M5 4v3H4a2 2 0 00-2 2v3a2 2 0 002 2h1v2a2 2 0 002 2h6a2 2 0 002-2v-2h1a2 2 0 002-2V9a2 2 0 00-2-2h-1V4a2 2 0 00-2-2H7a2 2 0 00-2 2zm8 0H7v3h6V4zm0 8H7v4h6v-4z" clipRule="evenodd" />
353
+ </svg>
354
+ Print
355
+ </button>
356
+ </>
357
+ );
358
+ };
359
+
360
+ export default PrintInvoice;
index.html ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="mr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Pattanshetty Inventory</title>
8
+
9
+ <!-- PWA Manifest -->
10
+ <link rel="manifest" href="/manifest.json" />
11
+ <meta name="theme-color" content="#0d9488" />
12
+ <meta name="apple-mobile-web-app-capable" content="yes" />
13
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
14
+ <meta name="apple-mobile-web-app-title" content="Pattanshetty" />
15
+ <link rel="apple-touch-icon" href="/icon-192.png" />
16
+ <script src="https://cdn.tailwindcss.com"></script>
17
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
18
+ <style>
19
+ body { font-family: 'Inter', sans-serif; }
20
+
21
+ /* Hide scrollbar for Chrome, Safari and Opera */
22
+ .no-scrollbar::-webkit-scrollbar {
23
+ display: none;
24
+ }
25
+ /* Hide scrollbar for IE, Edge and Firefox */
26
+ .no-scrollbar {
27
+ -ms-overflow-style: none; /* IE and Edge */
28
+ scrollbar-width: none; /* Firefox */
29
+ }
30
+
31
+ /* Chrome, Safari, Edge, Opera - Hide number input arrows */
32
+ input::-webkit-outer-spin-button,
33
+ input::-webkit-inner-spin-button {
34
+ -webkit-appearance: none;
35
+ margin: 0;
36
+ }
37
+
38
+ /* Firefox - Hide number input arrows */
39
+ input[type=number] {
40
+ -moz-appearance: textfield;
41
+ }
42
+ </style>
43
+ <script type="importmap">
44
+ {
45
+ "imports": {
46
+ "react-router-dom": "https://aistudiocdn.com/react-router-dom@^7.9.6",
47
+ "react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
48
+ "react/": "https://aistudiocdn.com/react@^19.2.0/",
49
+ "react": "https://aistudiocdn.com/react@^19.2.0",
50
+ "lucide-react": "https://aistudiocdn.com/lucide-react@^0.554.0",
51
+ "recharts": "https://aistudiocdn.com/recharts@^3.4.1"
52
+ }
53
+ }
54
+ </script>
55
+ <link rel="stylesheet" href="/index.css">
56
+ </head>
57
+ <body class="bg-gray-50 text-gray-900">
58
+ <div id="root"></div>
59
+ <script type="module" src="/index.tsx"></script>
60
+ </body>
61
+ </html>
index.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+
5
+ const rootElement = document.getElementById('root');
6
+ if (!rootElement) {
7
+ throw new Error("Could not find root element to mount to");
8
+ }
9
+
10
+ const root = ReactDOM.createRoot(rootElement);
11
+ root.render(
12
+ <React.StrictMode>
13
+ <App />
14
+ </React.StrictMode>
15
+ );
metadata.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "name": "Mirchi Vyapar Manager",
3
+ "description": "A comprehensive Chili Trading Management System for managing Purchases (Jawaak), Sales (Awaak), Inventory (Stock), and Financial Ledgers with Marathi interface support.",
4
+ "requestFramePermissions": []
5
+ }
nginx.conf ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ server {
2
+ listen 7860;
3
+ server_name _;
4
+
5
+ root /usr/share/nginx/html;
6
+ index index.html;
7
+
8
+ # Enable gzip compression
9
+ gzip on;
10
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
11
+
12
+ # Handle client-side routing
13
+ location / {
14
+ try_files $uri $uri/ /index.html;
15
+ }
16
+
17
+ # Cache static assets
18
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
19
+ expires 1y;
20
+ add_header Cache-Control "public, immutable";
21
+ }
22
+
23
+ # Security headers
24
+ add_header X-Frame-Options "SAMEORIGIN" always;
25
+ add_header X-Content-Type-Options "nosniff" always;
26
+ add_header X-XSS-Protection "1; mode=block" always;
27
+ }
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "mirchi-vyapar-manager",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "lucide-react": "^0.554.0",
13
+ "react": "^19.2.0",
14
+ "react-dom": "^19.2.0",
15
+ "react-router-dom": "^7.9.6",
16
+ "recharts": "^3.4.1",
17
+ "xlsx": "^0.18.5"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.14.0",
21
+ "@vitejs/plugin-react": "^5.0.0",
22
+ "typescript": "~5.8.2",
23
+ "vite": "^6.2.0"
24
+ }
25
+ }
pages/AwaakBill.tsx ADDED
@@ -0,0 +1,681 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import {
4
+ getParties, getMirchiTypes, generateBillNumber, saveTransaction
5
+ } from '../services/db';
6
+ import {
7
+ Party, MirchiType, Transaction, TransactionItem, BillType, PaymentMode, PartyType
8
+ } from '../types';
9
+ import { Plus, Trash2, Save, ChevronUp, ChevronDown, RotateCcw } from 'lucide-react';
10
+ import PrintInvoice from '../components/PrintInvoice';
11
+
12
+ // Defined outside to prevent re-render focus loss
13
+ const SummaryInput = ({ label, value, onChange }: { label: string, value: number, onChange: (val: number) => void }) => (
14
+ <div className="flex justify-between items-center mb-1 bg-gray-50 p-1.5 rounded">
15
+ <span className="text-gray-600 text-xs">{label}</span>
16
+ <input
17
+ type="number"
18
+ step="0.01"
19
+ min="0"
20
+ className="w-24 border rounded text-right p-1 text-sm bg-white focus:ring-1 focus:ring-teal-500 outline-none appearance-none"
21
+ value={value === 0 ? '' : value}
22
+ onChange={e => {
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>
30
+ );
31
+
32
+ const AwaakBill = () => {
33
+ const navigate = useNavigate();
34
+
35
+ // Master Data
36
+ const [parties, setParties] = useState<Party[]>([]);
37
+ const [mirchiTypes, setMirchiTypes] = useState<MirchiType[]>([]);
38
+
39
+ // Form State
40
+ const [isReturnMode, setIsReturnMode] = useState(false);
41
+ const [billDate, setBillDate] = useState(new Date().toISOString().split('T')[0]);
42
+ const [billNumber, setBillNumber] = useState('');
43
+ const [selectedParty, setSelectedParty] = useState('');
44
+
45
+ // Mobile UI State
46
+ const [showMobileSummary, setShowMobileSummary] = useState(false);
47
+
48
+ const [items, setItems] = useState<Partial<TransactionItem>[]>([{
49
+ id: Date.now().toString(),
50
+ poti_weights: [],
51
+ gross_weight: 0,
52
+ poti_count: 0,
53
+ total_potya: 0,
54
+ net_weight: 0,
55
+ rate_per_kg: 0,
56
+ item_total: 0
57
+ }]);
58
+
59
+ // Temp inputs for items row
60
+ const [potiInputs, setPotiInputs] = useState<{ [key: string]: string }>({});
61
+
62
+ const [expenses, setExpenses] = useState({
63
+ cess_percent: 2.0,
64
+ cess_amount: 0,
65
+ adat_percent: 3.0,
66
+ adat_amount: 0,
67
+ hamali_per_poti: 6,
68
+ hamali_amount: 0,
69
+ gaadi_bharni: 0,
70
+ });
71
+
72
+ // Payment State
73
+ const [paymentMode, setPaymentMode] = useState<'cash' | 'online' | 'hybrid' | 'due'>('cash');
74
+ const [paidAmount, setPaidAmount] = useState(0); // Not used directly in new logic, derived
75
+ const [cashAmount, setCashAmount] = useState(0); // For hybrid
76
+ const [onlineAmount, setOnlineAmount] = useState(0); // For hybrid & due
77
+ const [isSubmitting, setIsSubmitting] = useState(false);
78
+ const [savedTransaction, setSavedTransaction] = useState<Transaction | null>(null);
79
+ const [isLoading, setIsLoading] = useState(true);
80
+ const [error, setError] = useState<string | null>(null);
81
+
82
+ // Initial Load & Bill Number Generation
83
+ useEffect(() => {
84
+ const loadData = async () => {
85
+ try {
86
+ setIsLoading(true);
87
+ setError(null);
88
+ const [partiesData, mirchiData] = await Promise.all([
89
+ getParties(),
90
+ getMirchiTypes()
91
+ ]);
92
+
93
+ if (!partiesData || partiesData.length === 0) {
94
+ setError('No parties found. Please add parties in Settings first.');
95
+ }
96
+ if (!mirchiData || mirchiData.length === 0) {
97
+ setError('No mirchi types found. Please add mirchi types in Settings first.');
98
+ }
99
+
100
+ // Filter parties for Awaak bills
101
+ const filteredParties = partiesData.filter(p =>
102
+ p.party_type === PartyType.AWAAK || p.party_type === PartyType.BOTH
103
+ );
104
+
105
+ setParties(filteredParties || []);
106
+ setMirchiTypes(mirchiData || []);
107
+ setBillNumber(generateBillNumber(BillType.AWAAK, isReturnMode));
108
+ } catch (err: any) {
109
+ console.error('Error loading data:', err);
110
+ setError('Failed to load data. Please check your connection and try again.');
111
+ } finally {
112
+ setIsLoading(false);
113
+ }
114
+ };
115
+ loadData();
116
+ }, [isReturnMode]);
117
+
118
+ // Calculation Logic
119
+ const calculateRow = (item: Partial<TransactionItem>, potiString: string) => {
120
+ const weights = potiString.split(/[\s,+]+/).map(n => parseFloat(n)).filter(n => !isNaN(n) && n > 0);
121
+
122
+ const gross = weights.reduce((a, b) => a + b, 0);
123
+ const count = weights.length;
124
+ const potya = 0; // No deduction for Awaak
125
+ const net = gross; // Net is same as Gross
126
+ const total = net * (item.rate_per_kg || 0);
127
+
128
+ return {
129
+ ...item,
130
+ poti_weights: weights,
131
+ gross_weight: gross,
132
+ poti_count: count,
133
+ total_potya: potya,
134
+ net_weight: net,
135
+ item_total: total
136
+ };
137
+ };
138
+
139
+ const handleItemChange = (id: string, field: string, value: any) => {
140
+ setItems(prev => prev.map(item => {
141
+ if (item.id !== id) return item;
142
+
143
+ let updatedItem = { ...item, [field]: value };
144
+
145
+ if (field === 'rate_per_kg') {
146
+ updatedItem.item_total = (updatedItem.net_weight || 0) * Math.max(0, parseFloat(value || 0));
147
+ }
148
+
149
+ return updatedItem;
150
+ }));
151
+ };
152
+
153
+ const handlePotiInputChange = (id: string, value: string) => {
154
+ setPotiInputs(prev => ({ ...prev, [id]: value }));
155
+ setItems(prev => prev.map(item => {
156
+ if (item.id !== id) return item;
157
+ return calculateRow(item, value);
158
+ }));
159
+ };
160
+
161
+ const addItem = () => {
162
+ setItems(prev => [...prev, {
163
+ id: Date.now().toString(),
164
+ poti_weights: [],
165
+ gross_weight: 0,
166
+ poti_count: 0,
167
+ total_potya: 0,
168
+ net_weight: 0,
169
+ rate_per_kg: 0,
170
+ item_total: 0
171
+ }]);
172
+ };
173
+
174
+ const removeItem = (id: string) => {
175
+ if (items.length > 1) {
176
+ setItems(prev => prev.filter(i => i.id !== id));
177
+ const newInputs = { ...potiInputs };
178
+ delete newInputs[id];
179
+ setPotiInputs(newInputs);
180
+ }
181
+ };
182
+
183
+ // Totals Calculation
184
+ const subtotal = items.reduce((acc, item) => acc + (item.item_total || 0), 0);
185
+ const totalPoti = items.reduce((acc, item) => acc + (item.poti_count || 0), 0);
186
+
187
+ // Derived Expenses (0 if Return Mode usually, but keeping logic consistent)
188
+ // Derived Expenses
189
+ const cessAmt = (subtotal * expenses.cess_percent) / 100;
190
+ const adatAmt = (subtotal * expenses.adat_percent) / 100;
191
+ const hamaliAmt = totalPoti * expenses.hamali_per_poti;
192
+ const totalExp = cessAmt + adatAmt + hamaliAmt + (expenses.gaadi_bharni || 0);
193
+ const grandTotal = subtotal + totalExp;
194
+
195
+ // Calculate Payment Details based on Mode
196
+ let finalCash = 0;
197
+ let finalOnline = 0;
198
+ let currentPaid = 0;
199
+
200
+ if (paymentMode === 'cash') {
201
+ finalCash = grandTotal;
202
+ finalOnline = 0;
203
+ currentPaid = grandTotal;
204
+ } else if (paymentMode === 'online') {
205
+ finalCash = 0;
206
+ finalOnline = grandTotal;
207
+ currentPaid = grandTotal;
208
+ } else if (paymentMode === 'hybrid') {
209
+ finalCash = cashAmount;
210
+ finalOnline = Math.max(0, grandTotal - cashAmount);
211
+ currentPaid = grandTotal; // Hybrid assumes full payment
212
+ } else if (paymentMode === 'due') {
213
+ finalCash = 0;
214
+ finalOnline = onlineAmount; // User defined
215
+ currentPaid = onlineAmount;
216
+ }
217
+
218
+ const balance = grandTotal - currentPaid;
219
+
220
+ // Validation Error
221
+ // Hybrid: Cash cannot exceed Total
222
+ // Due: Online cannot exceed Total
223
+ const isOverpaid = (paymentMode === 'hybrid' && cashAmount > grandTotal) ||
224
+ (paymentMode === 'due' && onlineAmount > grandTotal);
225
+
226
+ const validateForm = () => {
227
+ if (!selectedParty) {
228
+ alert('Please select a Party (पार्टी निवडा)');
229
+ return false;
230
+ }
231
+ for (let i = 0; i < items.length; i++) {
232
+ const item = items[i];
233
+ if (!item.mirchi_type_id) {
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;
240
+ }
241
+ if (!item.rate_per_kg || item.rate_per_kg <= 0) {
242
+ alert(`Row ${i + 1}: Rate must be greater than 0`);
243
+ return false;
244
+ }
245
+ }
246
+ if (isOverpaid) {
247
+ alert('Paid amount cannot be greater than Total Amount');
248
+ return false;
249
+ }
250
+ return true;
251
+ };
252
+
253
+ const handleSubmit = async () => {
254
+ if (isSubmitting) return;
255
+ if (!validateForm()) return;
256
+
257
+ setIsSubmitting(true);
258
+
259
+ // Populate mirchi_name for each item from mirchiTypes
260
+ const itemsWithNames = items.map(item => ({
261
+ ...item,
262
+ mirchi_name: mirchiTypes.find(m => m.id === item.mirchi_type_id)?.name || 'Unknown'
263
+ }));
264
+
265
+ const transaction: Transaction = {
266
+ id: Date.now().toString(),
267
+ bill_number: billNumber,
268
+ bill_date: billDate,
269
+ bill_type: BillType.AWAAK,
270
+ is_return: isReturnMode,
271
+ party_id: selectedParty,
272
+ party_name: parties.find(p => p.id === selectedParty)?.name,
273
+ items: itemsWithNames as TransactionItem[],
274
+ expenses: {
275
+ ...expenses,
276
+ cess_amount: cessAmt,
277
+ adat_amount: adatAmt,
278
+ hamali_amount: hamaliAmt
279
+ },
280
+ payments: [
281
+ ...(finalCash > 0 ? [{ mode: PaymentMode.CASH, amount: finalCash }] : []),
282
+ ...(finalOnline > 0 ? [{ mode: PaymentMode.ONLINE, amount: finalOnline }] : []),
283
+ ...(balance > 0 ? [{ mode: PaymentMode.DUE, amount: balance }] : []) // Optional: Track due as a payment entry or just balance? Usually balance is enough. Keeping logic simple.
284
+ ],
285
+ gross_weight_total: items.reduce((a, i) => a + (i.gross_weight || 0), 0),
286
+ net_weight_total: items.reduce((a, i) => a + (i.net_weight || 0), 0),
287
+ subtotal: subtotal,
288
+ total_expenses: totalExp,
289
+ total_amount: grandTotal,
290
+ paid_amount: currentPaid,
291
+ balance_amount: balance
292
+ };
293
+
294
+ const result = await saveTransaction(transaction);
295
+ if (result.success) {
296
+ // Use the complete transaction object we created, not just the API response
297
+ // This ensures all items are included for printing
298
+ const completeTransaction = result.data ? {
299
+ ...transaction,
300
+ ...result.data,
301
+ items: transaction.items // Ensure items are preserved
302
+ } : transaction;
303
+ setSavedTransaction(completeTransaction);
304
+ alert('Bill Saved Successfully! You can now print the invoice.');
305
+ } else {
306
+ alert(`Error: ${result.message}`);
307
+ }
308
+ setIsSubmitting(false);
309
+ };
310
+
311
+ const handleHybridCashChange = (val: number) => {
312
+ setCashAmount(val);
313
+ // Online amount is derived in render, no state update needed for it in hybrid
314
+ };
315
+
316
+ const SummaryContent = () => (
317
+ <div className="space-y-3 text-sm">
318
+ <div className="flex justify-between">
319
+ <span className="text-gray-600">एकुण रक्कम (Subtotal)</span>
320
+ <span className="font-semibold">₹{subtotal.toFixed(2)}</span>
321
+ </div>
322
+
323
+ <div className="pt-2 border-t border-dashed space-y-2">
324
+ {/* 1. Cess Tax */}
325
+ <div className="flex justify-between items-center">
326
+ <span className="text-gray-600 text-xs">सेस (Cess {expenses.cess_percent}%)</span>
327
+ <span className="text-gray-800 font-medium">₹{cessAmt.toFixed(2)}</span>
328
+ </div>
329
+
330
+ {/* 2. Adat / Market Yard Tax */}
331
+ <SummaryInput
332
+ label={`अडत (Adat ${expenses.adat_percent}%)`}
333
+ value={expenses.adat_percent}
334
+ onChange={val => setExpenses({ ...expenses, adat_percent: val })}
335
+ />
336
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
337
+ <span>Amount:</span>
338
+ <span>₹{adatAmt.toFixed(2)}</span>
339
+ </div>
340
+
341
+ {/* 3. Poti (Packet) / Bag */}
342
+ <div className="flex justify-between items-center">
343
+ <span className="text-gray-600 text-xs">पोती (Bags)</span>
344
+ <span className="text-gray-800 font-medium">{totalPoti}</span>
345
+ </div>
346
+
347
+ {/* 4. Hamali */}
348
+ <div className="flex justify-between items-center">
349
+ <span className="text-gray-600 text-xs">हमाली ({expenses.hamali_per_poti} * {totalPoti})</span>
350
+ <span className="text-gray-800 font-medium">₹{hamaliAmt.toFixed(2)}</span>
351
+ </div>
352
+
353
+ {/* 5. Gaadi Bharni */}
354
+ <SummaryInput
355
+ label="गाडी भरणी"
356
+ value={expenses.gaadi_bharni}
357
+ onChange={val => setExpenses({ ...expenses, gaadi_bharni: val })}
358
+ />
359
+ </div>
360
+
361
+ {/* 6. Total Price */}
362
+ <div className="pt-2 border-t border-gray-200 flex justify-between items-center">
363
+ <span className="text-base font-bold text-gray-800">एकुण बिल (Total)</span>
364
+ <span className="text-xl font-bold text-red-600">₹{grandTotal.toFixed(2)}</span>
365
+ </div>
366
+
367
+ {/* Payment Section */}
368
+ <div className="bg-teal-50 p-3 rounded-lg mt-2 border border-teal-100">
369
+ <label className="block text-xs font-medium text-teal-800 mb-2">Payment Mode</label>
370
+ <div className="flex gap-2 mb-3">
371
+ {['cash', 'online', 'hybrid', 'due'].map(mode => (
372
+ <button
373
+ key={mode}
374
+ onClick={() => {
375
+ setPaymentMode(mode as any);
376
+ setCashAmount(0);
377
+ setOnlineAmount(0);
378
+ }}
379
+ className={`flex-1 py-1 text-xs font-medium rounded border ${paymentMode === mode
380
+ ? 'bg-teal-600 text-white border-teal-600'
381
+ : 'bg-white text-gray-600 border-gray-200'
382
+ } capitalize`}
383
+ >
384
+ {mode}
385
+ </button>
386
+ ))}
387
+ </div>
388
+
389
+ {paymentMode === 'cash' && (
390
+ <div className="text-center py-2 text-sm text-gray-600">
391
+ Full Payment in Cash: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
392
+ </div>
393
+ )}
394
+
395
+ {paymentMode === 'online' && (
396
+ <div className="text-center py-2 text-sm text-gray-600">
397
+ Full Payment Online: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
398
+ </div>
399
+ )}
400
+
401
+ {paymentMode === 'hybrid' && (
402
+ <div className="space-y-2">
403
+ <div>
404
+ <label className="text-xs text-gray-600">Cash Amount</label>
405
+ <input
406
+ type="number"
407
+ className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
408
+ value={cashAmount === 0 ? '' : cashAmount}
409
+ onChange={e => handleHybridCashChange(parseFloat(e.target.value) || 0)}
410
+ placeholder="Cash"
411
+ />
412
+ </div>
413
+ <div>
414
+ <label className="text-xs text-gray-600">Online Amount (Auto)</label>
415
+ <input
416
+ type="text"
417
+ readOnly
418
+ className="w-full bg-gray-100 border border-gray-200 rounded p-2 text-right font-medium text-gray-600"
419
+ value={(grandTotal - cashAmount).toFixed(2)}
420
+ />
421
+ </div>
422
+ </div>
423
+ )}
424
+
425
+ {paymentMode === 'due' && (
426
+ <div className="space-y-2">
427
+ <div>
428
+ <label className="text-xs text-gray-600">Online Amount (Partial Payment)</label>
429
+ <input
430
+ type="number"
431
+ className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
432
+ value={onlineAmount === 0 ? '' : onlineAmount}
433
+ onChange={e => setOnlineAmount(parseFloat(e.target.value) || 0)}
434
+ placeholder="Enter amount paid online (0 for full due)"
435
+ />
436
+ </div>
437
+ <div>
438
+ <label className="text-xs text-gray-600">Due Amount (Auto)</label>
439
+ <input
440
+ type="text"
441
+ readOnly
442
+ className="w-full bg-red-50 border border-red-200 rounded p-2 text-right font-bold text-red-600"
443
+ value={(grandTotal - onlineAmount).toFixed(2)}
444
+ />
445
+ </div>
446
+ </div>
447
+ )}
448
+
449
+ {isOverpaid && (
450
+ <div className="text-red-500 text-xs mt-2 font-medium text-center">
451
+ Error: Amount exceeds Total!
452
+ </div>
453
+ )}
454
+ </div>
455
+
456
+ <div className="flex justify-between items-center pt-2">
457
+ <span className="text-gray-600 font-medium">बाकी (Balance)</span>
458
+ <span className={`font-bold ${balance > 0 ? 'text-red-500' : 'text-green-600'}`}>₹{balance.toFixed(2)}</span>
459
+ </div>
460
+ </div>
461
+ );
462
+
463
+ return (
464
+ <div className="flex flex-col lg:flex-row gap-4 h-full p-4 bg-gray-50">
465
+ {/* Loading State */}
466
+ {isLoading && (
467
+ <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-gray-100">
468
+ <div className="text-center">
469
+ <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mb-4"></div>
470
+ <p className="text-gray-600">Loading data...</p>
471
+ </div>
472
+ </div>
473
+ )}
474
+
475
+ {/* Error State */}
476
+ {error && !isLoading && (
477
+ <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-red-200">
478
+ <div className="text-center p-8">
479
+ <div className="text-red-500 text-5xl mb-4">⚠️</div>
480
+ <h3 className="text-lg font-semibold text-gray-800 mb-2">Error Loading Data</h3>
481
+ <p className="text-gray-600 mb-4">{error}</p>
482
+ <button
483
+ onClick={() => window.location.reload()}
484
+ className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"
485
+ >
486
+ Retry
487
+ </button>
488
+ </div>
489
+ </div>
490
+ )}
491
+
492
+ {/* Main Content - Only show when not loading and no error */}
493
+ {!isLoading && !error && (
494
+ <>
495
+ {/* Left: Form */}
496
+ <div className={`flex-1 bg-white rounded-xl shadow-sm border p-4 lg:p-6 lg:overflow-y-auto lg:h-full no-scrollbar pb-40 lg:pb-6 ${isReturnMode ? 'border-red-200' : 'border-gray-100'}`}>
497
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
498
+ <h2 className={`text-lg font-bold flex items-center gap-2 ${isReturnMode ? 'text-red-600' : 'text-gray-800'}`}>
499
+ {isReturnMode ? <RotateCcw className="text-red-500" /> : <div className="text-teal-600 font-bold">IN</div>}
500
+ {isReturnMode ? 'आवक परतावा (Purchase Return)' : 'आवक बिल (Purchase)'}
501
+ </h2>
502
+
503
+ <div className="flex items-center gap-3">
504
+ <div className="flex bg-gray-100 rounded-lg p-1">
505
+ <button
506
+ onClick={() => setIsReturnMode(false)}
507
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${!isReturnMode ? 'bg-white text-teal-700 shadow-sm' : 'text-gray-500'}`}
508
+ >
509
+ Regular
510
+ </button>
511
+ <button
512
+ onClick={() => setIsReturnMode(true)}
513
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${isReturnMode ? 'bg-red-500 text-white shadow-sm' : 'text-gray-500'}`}
514
+ >
515
+ Return
516
+ </button>
517
+ </div>
518
+
519
+ <div className="bg-gray-100 px-3 py-1 rounded text-sm font-mono text-gray-600">
520
+ {billDate}
521
+ </div>
522
+ </div>
523
+ </div>
524
+
525
+ {/* Header Fields */}
526
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
527
+
528
+ <div>
529
+ <label className="block text-sm font-medium text-gray-700 mb-1">पार्टी नाव (Party)</label>
530
+ <select
531
+ className="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-teal-500 outline-none"
532
+ value={selectedParty}
533
+ onChange={e => setSelectedParty(e.target.value)}
534
+ >
535
+ <option value="">Select Party</option>
536
+ {parties.map(p => (
537
+ <option key={p.id} value={p.id}>{p.name} - {p.city}</option>
538
+ ))}
539
+ </select>
540
+ </div>
541
+ </div>
542
+
543
+ {/* Items Table */}
544
+ <div className="mb-6">
545
+ <div className="flex justify-between items-center mb-2">
546
+ <h3 className="font-semibold text-gray-700">माल तपशील (Items)</h3>
547
+ </div>
548
+
549
+ <div className="space-y-4">
550
+ {items.map((item, index) => (
551
+ <div key={item.id} className="p-4 border rounded-lg bg-gray-50 relative shadow-sm">
552
+ <button
553
+ onClick={() => removeItem(item.id!)}
554
+ className="absolute top-2 right-2 text-red-400 hover:text-red-600 p-1"
555
+ >
556
+ <Trash2 size={18} />
557
+ </button>
558
+
559
+ <div className="grid grid-cols-2 md:grid-cols-6 gap-3">
560
+ <div className="col-span-2">
561
+ <label className="block text-xs font-medium text-gray-500 mb-1">मिरची जात (Type)</label>
562
+ <select
563
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
564
+ value={item.mirchi_type_id || ''}
565
+ onChange={e => handleItemChange(item.id!, 'mirchi_type_id', e.target.value)}
566
+ >
567
+ <option value="">Select Type</option>
568
+ {mirchiTypes.map(m => (
569
+ <option key={m.id} value={m.id}>{m.name}</option>
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
576
+ type="text"
577
+ placeholder="10, 20, 30"
578
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
579
+ value={potiInputs[item.id!] || ''}
580
+ onChange={e => handlePotiInputChange(item.id!, e.target.value)}
581
+ />
582
+ </div>
583
+
584
+ <div>
585
+ <label className="block text-xs font-medium text-gray-500 mb-1">Gross</label>
586
+ <input type="number" readOnly className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm" value={item.gross_weight} />
587
+ </div>
588
+ {/* Removed Deduct and Net columns */}
589
+
590
+ <div>
591
+ <label className="block text-xs font-medium text-gray-500 mb-1">Rate (₹)</label>
592
+ <input
593
+ type="number"
594
+ min="0"
595
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white appearance-none"
596
+ onWheel={e => e.currentTarget.blur()}
597
+ value={item.rate_per_kg === 0 ? '' : item.rate_per_kg}
598
+ onChange={e => handleItemChange(item.id!, 'rate_per_kg', Math.max(0, parseFloat(e.target.value) || 0))}
599
+ placeholder="0"
600
+ />
601
+ </div>
602
+ <div className="col-span-2 md:col-span-2">
603
+ <label className="block text-xs font-medium text-gray-500 mb-1">Total (₹)</label>
604
+ <input type="number" readOnly className="w-full bg-green-50 border border-green-200 rounded p-2 text-sm font-bold text-right text-green-800" value={item.item_total?.toFixed(2)} />
605
+ </div>
606
+ </div>
607
+ </div>
608
+ ))}
609
+ </div>
610
+ <button
611
+ onClick={addItem}
612
+ className="w-full mt-4 flex justify-center items-center gap-2 text-teal-600 font-medium bg-teal-50 hover:bg-teal-100 py-3 rounded-lg border border-teal-100 transition"
613
+ >
614
+ <Plus size={18} /> Add New Item (नवीन माल)
615
+ </button>
616
+ </div>
617
+ </div>
618
+
619
+ {/* Desktop: Right Summary Sidebar */}
620
+ <div className="hidden lg:flex w-80 flex-col gap-4">
621
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-0">
622
+ <h3 className="font-bold text-gray-800 mb-4 border-b pb-2">बिल सारांश (Summary)</h3>
623
+ {SummaryContent()}
624
+ <button
625
+ onClick={handleSubmit}
626
+ disabled={isSubmitting || isOverpaid}
627
+ className="w-full mt-6 bg-teal-600 text-white py-3 rounded-lg font-semibold hover:bg-teal-700 flex justify-center items-center gap-2 shadow-lg shadow-teal-100 disabled:opacity-50 disabled:bg-gray-400"
628
+ >
629
+ <Save size={20} /> {isSubmitting ? 'Saving...' : 'जतन करा (Save)'}
630
+ </button>
631
+ {savedTransaction && (
632
+ <div className="mt-4">
633
+ <PrintInvoice transaction={savedTransaction} />
634
+ </div>
635
+ )}
636
+ </div>
637
+ </div>
638
+
639
+ {/* Mobile: Sticky Bottom Action Bar */}
640
+ <div className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-[0_-4px_10px_rgba(0,0,0,0.1)] z-20">
641
+ <div className="flex items-center justify-between p-4 bg-white z-20 relative">
642
+ <div
643
+ onClick={() => setShowMobileSummary(!showMobileSummary)}
644
+ className="flex flex-col cursor-pointer"
645
+ >
646
+ <div className="flex items-center gap-1 text-gray-500 text-xs font-medium uppercase tracking-wide">
647
+ Summary {showMobileSummary ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
648
+ </div>
649
+ <div className="text-xl font-bold text-red-600">
650
+ ₹{grandTotal.toFixed(2)}
651
+ </div>
652
+ </div>
653
+ <button
654
+ onClick={handleSubmit}
655
+ disabled={isSubmitting}
656
+ className="bg-teal-600 text-white px-6 py-2.5 rounded-lg font-semibold flex items-center gap-2 shadow-sm active:bg-teal-700 disabled:opacity-50"
657
+ >
658
+ <Save size={18} /> {isSubmitting ? '...' : 'Save'}
659
+ </button>
660
+ </div>
661
+
662
+ {showMobileSummary && (
663
+ <div className="px-6 pb-6 pt-0 bg-white border-t border-dashed border-gray-200 max-h-[60vh] overflow-y-auto animate-in slide-in-from-bottom-2">
664
+ <div className="mt-4">
665
+ {SummaryContent()}
666
+ </div>
667
+ {savedTransaction && (
668
+ <div className="mt-4 flex justify-center">
669
+ <PrintInvoice transaction={savedTransaction} />
670
+ </div>
671
+ )}
672
+ </div>
673
+ )}
674
+ </div>
675
+ </>
676
+ )}
677
+ </div>
678
+ );
679
+ };
680
+
681
+ export default AwaakBill;
pages/Dashboard.tsx ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from 'react';
2
+ import { getTransactions, getActiveLots, getParties } from '../services/db';
3
+ import { Transaction, Lot, Party } from '../types';
4
+ import {
5
+ TrendingUp,
6
+ Package,
7
+ AlertCircle,
8
+ Bell,
9
+ IndianRupee,
10
+ ArrowUpRight,
11
+ ArrowDownLeft
12
+ } from 'lucide-react';
13
+ import {
14
+ BarChart,
15
+ Bar,
16
+ XAxis,
17
+ Tooltip,
18
+ ResponsiveContainer
19
+ } from 'recharts';
20
+ import { Link } from 'react-router-dom';
21
+
22
+ const Dashboard = () => {
23
+ const [recentTx, setRecentTx] = useState<Transaction[]>([]);
24
+ const [totalStock, setTotalStock] = useState(0);
25
+ const [stockValue, setStockValue] = useState(0);
26
+ const [activeLots, setActiveLots] = useState<Lot[]>([]);
27
+ const [dueParties, setDueParties] = useState<Party[]>([]);
28
+ const [notifications, setNotifications] = useState<string[]>([]);
29
+ const [showNotif, setShowNotif] = useState(false);
30
+
31
+ useEffect(() => {
32
+ const loadData = async () => {
33
+ const txs = await getTransactions();
34
+ const lots = await getActiveLots();
35
+ const parties = await getParties();
36
+
37
+ setRecentTx(txs.slice(0, 5));
38
+ setActiveLots(lots);
39
+
40
+ const stockQty = lots.reduce((acc, lot) => acc + lot.remaining_quantity, 0);
41
+ const stockVal = lots.reduce((acc, lot) => acc + (lot.remaining_quantity * lot.avg_rate), 0);
42
+
43
+ setTotalStock(stockQty);
44
+ setStockValue(stockVal);
45
+
46
+ const dues = parties.filter(p => p.current_balance !== 0);
47
+ setDueParties(dues);
48
+
49
+ const alerts = [];
50
+ const lowStock = lots.filter(l => l.remaining_quantity < 50).length;
51
+ if (lowStock > 0) alerts.push(`${lowStock} lots are running low on stock.`);
52
+ const paymentPending = dues.length;
53
+ if (paymentPending > 0) alerts.push(`${paymentPending} parties have pending payments.`);
54
+ if (txs.length === 0) alerts.push("Welcome! Create your first bill to get started.");
55
+
56
+ setNotifications(alerts);
57
+ };
58
+ loadData();
59
+ }, []);
60
+
61
+ const chartData = activeLots.map(lot => ({
62
+ name: lot.mirchi_name,
63
+ qty: lot.remaining_quantity
64
+ }));
65
+
66
+ const aggregatedChartData = Object.values(chartData.reduce((acc: any, curr) => {
67
+ if (!acc[curr.name]) acc[curr.name] = { name: curr.name, qty: 0 };
68
+ acc[curr.name].qty += curr.qty;
69
+ return acc;
70
+ }, {}));
71
+
72
+ return (
73
+ <div className="space-y-4 md:space-y-8 pb-6 md:pb-10">
74
+ {/* Header Section with Separated Notification Bell */}
75
+ <div className="flex flex-col md:flex-row justify-between items-stretch gap-3 md:gap-6">
76
+ {/* Welcome Card */}
77
+ <div className="flex-1 bg-gradient-to-r from-teal-800 to-teal-700 rounded-xl p-4 md:p-6 text-white shadow-md relative overflow-hidden min-h-[110px] md:min-h-[120px] flex flex-col justify-center">
78
+ <div className="z-10 relative">
79
+ <h1 className="text-xl md:text-2xl font-bold">नमस्कार, Admin! 👋</h1>
80
+ <p className="text-teal-100 mt-2 opacity-90 max-w-lg text-sm md:text-base">
81
+ तुमच्या व्यवसायाचा आजचा आढावा येथे आहे. (Here is your business overview for today.)
82
+ </p>
83
+ </div>
84
+ {/* Decorative Background Elements */}
85
+ <div className="absolute right-0 top-0 h-full w-1/3 bg-white/10 skew-x-12 z-0 pointer-events-none"></div>
86
+ <div className="absolute -bottom-8 -right-8 w-32 h-32 bg-teal-500/20 rounded-full blur-2xl z-0 pointer-events-none"></div>
87
+ </div>
88
+
89
+ {/* Actions / Notifications */}
90
+ <div className="flex items-center justify-between md:justify-center gap-2 md:gap-4 bg-white px-3 py-2 md:p-4 rounded-xl shadow-sm border border-gray-100 h-auto self-stretch">
91
+ <div className="flex flex-col text-left text-xs md:text-sm">
92
+ <span className="text-xs text-gray-500 font-medium uppercase">Current Date</span>
93
+ <span className="font-bold text-gray-800">{new Date().toLocaleDateString('en-IN', { day: 'numeric', month: 'short' })}</span>
94
+ </div>
95
+ <div className="hidden md:block h-10 w-px" />
96
+ <div className="relative flex-shrink-0 ml-auto">
97
+ <button
98
+ onClick={() => setShowNotif(!showNotif)}
99
+ className="p-3 bg-gray-50 hover:bg-teal-50 text-gray-600 hover:text-teal-700 rounded-full transition-colors relative border border-gray-200"
100
+ aria-label="Notifications"
101
+ >
102
+ <Bell size={24} />
103
+ {notifications.length > 0 && (
104
+ <span className="absolute top-0 right-0 w-3 h-3 bg-red-500 rounded-full border-2 border-white animate-pulse"></span>
105
+ )}
106
+ </button>
107
+
108
+ {/* Notification Dropdown - aligned to bell */}
109
+ {showNotif && (
110
+ <div
111
+ className="absolute top-full mt-2 w-72 sm:w-80 bg-white rounded-xl shadow-2xl border border-gray-100 text-gray-800 z-50 animate-in slide-in-from-top-2 origin-top right-0"
112
+ >
113
+ <div className="p-4 border-b bg-gray-50 rounded-t-xl font-semibold flex justify-between items-center">
114
+ <span>सूचना (Notifications)</span>
115
+ <span className="text-xs bg-teal-100 text-teal-800 px-2 py-1 rounded-full font-bold">{notifications.length}</span>
116
+ </div>
117
+ <div className="max-h-72 overflow-y-auto custom-scrollbar">
118
+ {notifications.length === 0 ? (
119
+ <div className="p-6 text-center text-gray-400 text-sm">No new notifications</div>
120
+ ) : (
121
+ <div className="divide-y divide-gray-100">
122
+ {notifications.map((note, i) => (
123
+ <div key={i} className="p-4 hover:bg-gray-50 text-sm flex items-start gap-3 transition-colors">
124
+ <div className="w-2 h-2 mt-1.5 rounded-full bg-orange-500 shrink-0"></div>
125
+ <p className="text-gray-600 leading-relaxed">{note}</p>
126
+ </div>
127
+ ))}
128
+ </div>
129
+ )}
130
+ </div>
131
+ <div className="p-2 border-t text-center">
132
+ <button onClick={() => setShowNotif(false)} className="text-xs text-teal-600 font-medium hover:underline">Close</button>
133
+ </div>
134
+ </div>
135
+ )}
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ {/* KPI Cards - mobile swipeable row, desktop grid */}
141
+ {/* Mobile: horizontal scroll */}
142
+ <div className="md:hidden -mx-4 px-4 mt-1">
143
+ <div className="flex gap-3 overflow-x-auto pb-1 no-scrollbar snap-x snap-mandatory">
144
+ <div className="min-w-[75%] snap-start bg-white p-4 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
145
+ <div className="flex items-center justify-between">
146
+ <div>
147
+ <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Total Stock</p>
148
+ <h3 className="text-xl font-bold text-gray-800 mt-1 group-hover:text-teal-600 transition-colors">{totalStock.toLocaleString()} <span className="text-xs font-normal text-gray-400">kg</span></h3>
149
+ </div>
150
+ <div className="p-3 bg-blue-50 text-blue-600 rounded-2xl group-hover:bg-blue-100 transition-colors">
151
+ <Package size={20} />
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <div className="min-w-[75%] snap-start bg-white p-4 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
157
+ <div className="flex items-center justify-between">
158
+ <div>
159
+ <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Est. Value</p>
160
+ <h3 className="text-xl font-bold text-gray-800 mt-1 group-hover:text-green-600 transition-colors">₹ {(stockValue / 100000).toFixed(2)} L</h3>
161
+ </div>
162
+ <div className="p-3 bg-green-50 text-green-600 rounded-2xl group-hover:bg-green-100 transition-colors">
163
+ <IndianRupee size={20} />
164
+ </div>
165
+ </div>
166
+ </div>
167
+
168
+ <div className="min-w-[75%] snap-start bg-white p-4 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
169
+ <div className="flex items-center justify-between">
170
+ <div>
171
+ <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Receivables (येणे)</p>
172
+ <h3 className="text-xl font-bold text-teal-600 mt-1">
173
+ ₹ {dueParties.filter(p => p.current_balance > 0).reduce((a,b) => a + b.current_balance, 0).toLocaleString()}
174
+ </h3>
175
+ </div>
176
+ <div className="p-3 bg-teal-50 text-teal-600 rounded-2xl group-hover:bg-teal-100 transition-colors">
177
+ <ArrowDownLeft size={20} />
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ <div className="min-w-[75%] snap-start bg-white p-4 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
183
+ <div className="flex items-center justify-between">
184
+ <div>
185
+ <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Payables (देणे)</p>
186
+ <h3 className="text-xl font-bold text-red-500 mt-1">
187
+ ₹ {Math.abs(dueParties.filter(p => p.current_balance < 0).reduce((a,b) => a + b.current_balance, 0)).toLocaleString()}
188
+ </h3>
189
+ </div>
190
+ <div className="p-3 bg-red-50 text-red-600 rounded-2xl group-hover:bg-red-100 transition-colors">
191
+ <ArrowUpRight size={20} />
192
+ </div>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+
198
+ {/* Desktop / tablet: original grid */}
199
+ <div className="hidden md:grid grid-cols-2 lg:grid-cols-4 gap-6">
200
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
201
+ <div className="flex items-center justify-between">
202
+ <div>
203
+ <p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Total Stock</p>
204
+ <h3 className="text-2xl font-bold text-gray-800 mt-2 group-hover:text-teal-600 transition-colors">{totalStock.toLocaleString()} <span className="text-sm font-normal text-gray-400">kg</span></h3>
205
+ </div>
206
+ <div className="p-4 bg-blue-50 text-blue-600 rounded-2xl group-hover:bg-blue-100 transition-colors">
207
+ <Package size={24} />
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
213
+ <div className="flex items-center justify-between">
214
+ <div>
215
+ <p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Est. Value</p>
216
+ <h3 className="text-2xl font-bold text-gray-800 mt-2 group-hover:text-green-600 transition-colors">₹ {(stockValue / 100000).toFixed(2)} L</h3>
217
+ </div>
218
+ <div className="p-4 bg-green-50 text-green-600 rounded-2xl group-hover:bg-green-100 transition-colors">
219
+ <IndianRupee size={24} />
220
+ </div>
221
+ </div>
222
+ </div>
223
+
224
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
225
+ <div className="flex items-center justify-between">
226
+ <div>
227
+ <p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Receivables (येणे)</p>
228
+ <h3 className="text-2xl font-bold text-teal-600 mt-2">
229
+ ₹ {dueParties.filter(p => p.current_balance > 0).reduce((a,b) => a + b.current_balance, 0).toLocaleString()}
230
+ </h3>
231
+ </div>
232
+ <div className="p-4 bg-teal-50 text-teal-600 rounded-2xl group-hover:bg-teal-100 transition-colors">
233
+ <ArrowDownLeft size={24} />
234
+ </div>
235
+ </div>
236
+ </div>
237
+
238
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
239
+ <div className="flex items-center justify-between">
240
+ <div>
241
+ <p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Payables (देणे)</p>
242
+ <h3 className="text-2xl font-bold text-red-500 mt-2">
243
+ ₹ {Math.abs(dueParties.filter(p => p.current_balance < 0).reduce((a,b) => a + b.current_balance, 0)).toLocaleString()}
244
+ </h3>
245
+ </div>
246
+ <div className="p-4 bg-red-50 text-red-600 rounded-2xl group-hover:bg-red-100 transition-colors">
247
+ <ArrowUpRight size={24} />
248
+ </div>
249
+ </div>
250
+ </div>
251
+ </div>
252
+
253
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 md:gap-8">
254
+ {/* Recent Transactions */}
255
+ <div className="lg:col-span-2 flex flex-col gap-6">
256
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 flex flex-col h-full">
257
+ <div className="p-3 md:p-4 border-b border-gray-100 flex items-center justify-between">
258
+ <h3 className="font-bold text-gray-800 flex items-center gap-2">
259
+ <TrendingUp size={20} className="text-gray-400" />
260
+ अलीकडील व्यवहार (Recent)
261
+ </h3>
262
+ <Link to="/ledger" className="text-xs md:text-sm text-teal-600 font-bold hover:bg-teal-50 px-3 py-1.5 rounded-lg transition">View All</Link>
263
+ </div>
264
+ {/* Desktop table */}
265
+ <div className="hidden md:block overflow-x-auto flex-1">
266
+ <table className="w-full text-sm text-left">
267
+ <thead className="bg-gray-50 text-gray-500 uppercase text-xs tracking-wider">
268
+ <tr>
269
+ <th className="px-6 py-4">Bill No</th>
270
+ <th className="px-6 py-4">Party</th>
271
+ <th className="px-6 py-4">Type</th>
272
+ <th className="px-6 py-4 text-right">Amount</th>
273
+ </tr>
274
+ </thead>
275
+ <tbody className="divide-y divide-gray-100">
276
+ {recentTx.length === 0 ? (
277
+ <tr><td colSpan={4} className="text-center py-12 text-gray-400">No transactions recorded yet.</td></tr>
278
+ ) : (
279
+ recentTx.map(tx => (
280
+ <tr key={tx.id} className="hover:bg-gray-50 transition-colors">
281
+ <td className="px-6 py-4">
282
+ <div className="font-bold text-gray-700">{tx.bill_number}</div>
283
+ <div className="text-xs text-gray-400 mt-0.5">{tx.bill_date}</div>
284
+ </td>
285
+ <td className="px-6 py-4 font-medium text-gray-900">{tx.party_name}</td>
286
+ <td className="px-6 py-4">
287
+ <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold capitalize ${
288
+ tx.is_return
289
+ ? 'bg-orange-100 text-orange-800'
290
+ : tx.bill_type === 'jawaak'
291
+ ? 'bg-blue-100 text-blue-800'
292
+ : 'bg-green-100 text-green-800'
293
+ }`}>
294
+ {tx.is_return ? 'Return' : (tx.bill_type === 'jawaak' ? 'Purchase' : 'Sale')}
295
+ </span>
296
+ </td>
297
+ <td className="px-6 py-4 text-right font-bold text-gray-700">₹{tx.total_amount.toLocaleString()}</td>
298
+ </tr>
299
+ ))
300
+ )}
301
+ </tbody>
302
+ </table>
303
+ </div>
304
+
305
+ {/* Mobile cards */}
306
+ <div className="md:hidden flex-1 p-3 space-y-2.5">
307
+ {recentTx.length === 0 ? (
308
+ <div className="text-center py-8 text-gray-400 text-sm">No transactions recorded yet.</div>
309
+ ) : (
310
+ recentTx.map(tx => (
311
+ <div
312
+ key={tx.id}
313
+ className="border border-gray-100 rounded-lg p-3 hover:bg-gray-50 transition-colors flex flex-col gap-2"
314
+ >
315
+ <div className="flex justify-between items-start gap-2">
316
+ <div>
317
+ <div className="text-sm font-semibold text-gray-800">{tx.bill_number}</div>
318
+ <div className="text-[11px] text-gray-400 mt-0.5">{tx.bill_date}</div>
319
+ </div>
320
+ <span
321
+ className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold capitalize ${
322
+ tx.is_return
323
+ ? 'bg-orange-100 text-orange-800'
324
+ : tx.bill_type === 'jawaak'
325
+ ? 'bg-blue-100 text-blue-800'
326
+ : 'bg-green-100 text-green-800'
327
+ }`}
328
+ >
329
+ {tx.is_return ? 'Return' : tx.bill_type === 'jawaak' ? 'Purchase' : 'Sale'}
330
+ </span>
331
+ </div>
332
+ <div className="flex justify-between items-center text-xs mt-1">
333
+ <div className="text-gray-600 font-medium truncate max-w-[60%]">
334
+ {tx.party_name || 'Unknown Party'}
335
+ </div>
336
+ <div className="text-gray-900 font-bold text-sm">
337
+ ₹{tx.total_amount.toLocaleString()}
338
+ </div>
339
+ </div>
340
+ </div>
341
+ ))
342
+ )}
343
+ </div>
344
+ </div>
345
+ </div>
346
+
347
+ {/* Right Column */}
348
+ <div className="flex flex-col gap-6">
349
+ {/* Pending Payments List */}
350
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 flex flex-col max-h-[360px] md:max-h-[400px]">
351
+ <div className="p-3 md:p-4 border-b border-gray-100">
352
+ <h3 className="font-bold text-gray-800 flex items-center gap-2">
353
+ <AlertCircle size={20} className="text-orange-500" />
354
+ पेमेंट बाकी (Pending)
355
+ </h3>
356
+ </div>
357
+ <div className="overflow-y-auto custom-scrollbar p-2 space-y-2">
358
+ {dueParties.length === 0 ? (
359
+ <div className="text-center py-6 md:py-8 text-gray-400 text-sm">
360
+ <p>All payments settled! 🎉</p>
361
+ </div>
362
+ ) : (
363
+ dueParties.map(p => (
364
+ <div key={p.id} className="flex justify-between items-center p-4 hover:bg-gray-50 rounded-lg border border-transparent hover:border-gray-100 transition-all">
365
+ <div>
366
+ <p className="font-bold text-sm text-gray-800">{p.name}</p>
367
+ <p className="text-xs text-gray-500 mt-0.5">{p.city}</p>
368
+ </div>
369
+ <div className={`text-sm font-bold ${p.current_balance > 0 ? 'text-teal-600' : 'text-red-500'}`}>
370
+ {p.current_balance > 0 ? '+' : ''}{p.current_balance.toLocaleString()}
371
+ </div>
372
+ </div>
373
+ ))
374
+ )}
375
+ </div>
376
+ </div>
377
+
378
+ {/* Stock Chart */}
379
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 md:p-5">
380
+ <h3 className="font-bold text-gray-800 mb-4 md:mb-5 text-sm md:text-base">स्टॉक (Quantity)</h3>
381
+ <div className="h-40 md:h-48 w-full">
382
+ <ResponsiveContainer width="100%" height="100%">
383
+ <BarChart data={aggregatedChartData}>
384
+ <XAxis dataKey="name" axisLine={false} tickLine={false} tick={{fontSize: 12, fill: '#6b7280'}} />
385
+ <Tooltip
386
+ contentStyle={{borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'}}
387
+ cursor={{fill: '#f3f4f6'}}
388
+ />
389
+ <Bar dataKey="qty" fill="#0d9488" radius={[4, 4, 0, 0]} barSize={40} />
390
+ </BarChart>
391
+ </ResponsiveContainer>
392
+ </div>
393
+ </div>
394
+ </div>
395
+ </div>
396
+ </div>
397
+ );
398
+ };
399
+
400
+ export default Dashboard;
pages/JawaakBill.tsx ADDED
@@ -0,0 +1,703 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import {
4
+ getParties, getMirchiTypes, generateBillNumber, saveTransaction
5
+ } from '../services/db';
6
+ import {
7
+ Party, MirchiType, Transaction, TransactionItem, BillType, PaymentMode, PartyType
8
+ } from '../types';
9
+ import { Plus, Trash2, Save, ChevronUp, ChevronDown, RotateCcw } from 'lucide-react';
10
+ import PrintInvoice from '../components/PrintInvoice';
11
+
12
+ // Defined outside to prevent re-render focus loss
13
+ const SummaryInput = ({ label, value, onChange }: { label: string, value: number, onChange: (val: number) => void }) => (
14
+ <div className="flex justify-between items-center mb-1 bg-gray-50 p-1.5 rounded">
15
+ <span className="text-gray-600 text-xs">{label}</span>
16
+ <input
17
+ type="number"
18
+ step="0.01"
19
+ min="0"
20
+ className="w-24 border rounded text-right p-1 text-sm bg-white focus:ring-1 focus:ring-teal-500 outline-none appearance-none"
21
+ value={value === 0 ? '' : value}
22
+ onChange={e => {
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>
30
+ );
31
+
32
+ const JawaakBill = () => {
33
+ const navigate = useNavigate();
34
+
35
+ // Master Data
36
+ const [parties, setParties] = useState<Party[]>([]);
37
+ const [mirchiTypes, setMirchiTypes] = useState<MirchiType[]>([]);
38
+
39
+ // Form State
40
+ const [isReturnMode, setIsReturnMode] = useState(false);
41
+ const [billDate, setBillDate] = useState(new Date().toISOString().split('T')[0]);
42
+ const [billNumber, setBillNumber] = useState('');
43
+ const [selectedParty, setSelectedParty] = useState('');
44
+
45
+ // Mobile UI State
46
+ const [showMobileSummary, setShowMobileSummary] = useState(false);
47
+
48
+ const [items, setItems] = useState<Partial<TransactionItem>[]>([{
49
+ id: Date.now().toString(),
50
+ poti_weights: [],
51
+ gross_weight: 0,
52
+ poti_count: 0,
53
+ total_potya: 0,
54
+ net_weight: 0,
55
+ rate_per_kg: 0,
56
+ item_total: 0
57
+ }]);
58
+
59
+ // Temp inputs for items row
60
+ const [potiInputs, setPotiInputs] = useState<{ [key: string]: string }>({});
61
+
62
+ const [expenses, setExpenses] = useState({
63
+ cess_percent: 2.0,
64
+ cess_amount: 0,
65
+ adat_percent: 3.0,
66
+ adat_amount: 0,
67
+ poti_rate: 18, // Default rate
68
+ poti_amount: 0,
69
+ hamali_per_poti: 6,
70
+ hamali_amount: 0,
71
+ packaging_hamali_per_poti: 0,
72
+ packaging_hamali_amount: 0,
73
+ gaadi_bharni: 0,
74
+ });
75
+
76
+ // Payment State
77
+ const [paymentMode, setPaymentMode] = useState<'cash' | 'online' | 'hybrid' | 'due'>('cash');
78
+ const [paidAmount, setPaidAmount] = useState(0); // Not used directly in new logic, derived
79
+ const [cashAmount, setCashAmount] = useState(0); // For hybrid
80
+ const [onlineAmount, setOnlineAmount] = useState(0); // For hybrid & due
81
+ const [isSubmitting, setIsSubmitting] = useState(false);
82
+ const [savedTransaction, setSavedTransaction] = useState<Transaction | null>(null);
83
+ const [isLoading, setIsLoading] = useState(true);
84
+ const [error, setError] = useState<string | null>(null);
85
+
86
+ // Initial Load & Bill Number Generation
87
+ useEffect(() => {
88
+ const loadData = async () => {
89
+ try {
90
+ setIsLoading(true);
91
+ setError(null);
92
+ const [partiesData, mirchiData] = await Promise.all([
93
+ getParties(),
94
+ getMirchiTypes()
95
+ ]);
96
+
97
+ if (!partiesData || partiesData.length === 0) {
98
+ setError('No parties found. Please add parties in Settings first.');
99
+ }
100
+ if (!mirchiData || mirchiData.length === 0) {
101
+ setError('No mirchi types found. Please add mirchi types in Settings first.');
102
+ }
103
+
104
+ // Filter parties for Jawaak bills
105
+ const filteredParties = partiesData.filter(p =>
106
+ p.party_type === PartyType.JAWAAK || p.party_type === PartyType.BOTH
107
+ );
108
+
109
+ setParties(filteredParties || []);
110
+ setMirchiTypes(mirchiData || []);
111
+ setBillNumber(generateBillNumber(BillType.JAWAAK, isReturnMode));
112
+ } catch (err: any) {
113
+ console.error('Error loading data:', err);
114
+ setError('Failed to load data. Please check your connection and try again.');
115
+ } finally {
116
+ setIsLoading(false);
117
+ }
118
+ };
119
+ loadData();
120
+ }, [isReturnMode]);
121
+
122
+ // Calculation Logic
123
+ const calculateRow = (item: Partial<TransactionItem>, potiString: string) => {
124
+ const weights = potiString.split(/[\s,+]+/).map(n => parseFloat(n)).filter(n => !isNaN(n) && n > 0);
125
+
126
+ const gross = weights.reduce((a, b) => a + b, 0);
127
+ const count = weights.length;
128
+ const potya = count * 1;
129
+ const net = Math.max(0, gross - potya);
130
+ const total = net * (item.rate_per_kg || 0);
131
+
132
+ return {
133
+ ...item,
134
+ poti_weights: weights,
135
+ gross_weight: gross,
136
+ poti_count: count,
137
+ total_potya: potya,
138
+ net_weight: net,
139
+ item_total: total
140
+ };
141
+ };
142
+
143
+ const handleItemChange = (id: string, field: string, value: any) => {
144
+ setItems(prev => prev.map(item => {
145
+ if (item.id !== id) return item;
146
+
147
+ let updatedItem = { ...item, [field]: value };
148
+
149
+ if (field === 'rate_per_kg') {
150
+ updatedItem.item_total = (updatedItem.net_weight || 0) * Math.max(0, parseFloat(value || 0));
151
+ }
152
+
153
+ return updatedItem;
154
+ }));
155
+ };
156
+
157
+ const handlePotiInputChange = (id: string, value: string) => {
158
+ setPotiInputs(prev => ({ ...prev, [id]: value }));
159
+ setItems(prev => prev.map(item => {
160
+ if (item.id !== id) return item;
161
+ return calculateRow(item, value);
162
+ }));
163
+ };
164
+
165
+ const addItem = () => {
166
+ setItems(prev => [...prev, {
167
+ id: Date.now().toString(),
168
+ poti_weights: [],
169
+ gross_weight: 0,
170
+ poti_count: 0,
171
+ total_potya: 0,
172
+ net_weight: 0,
173
+ rate_per_kg: 0,
174
+ item_total: 0
175
+ }]);
176
+ };
177
+
178
+ const removeItem = (id: string) => {
179
+ if (items.length > 1) {
180
+ setItems(prev => prev.filter(i => i.id !== id));
181
+ const newInputs = { ...potiInputs };
182
+ delete newInputs[id];
183
+ setPotiInputs(newInputs);
184
+ }
185
+ };
186
+
187
+ // Totals Calculation
188
+ const subtotal = items.reduce((acc, item) => acc + (item.item_total || 0), 0);
189
+ const totalPoti = items.reduce((acc, item) => acc + (item.poti_count || 0), 0);
190
+
191
+ // Derived Expenses (0 if Return Mode usually, but keeping logic consistent)
192
+ // Derived Expenses
193
+ const cessAmt = (subtotal * expenses.cess_percent) / 100;
194
+ const adatAmt = (subtotal * expenses.adat_percent) / 100;
195
+ const potiAmt = totalPoti * expenses.poti_rate;
196
+ const hamaliAmt = totalPoti * expenses.hamali_per_poti;
197
+ const packagingHamaliAmt = totalPoti * expenses.packaging_hamali_per_poti;
198
+ const totalExp = cessAmt + adatAmt + potiAmt + hamaliAmt + packagingHamaliAmt + (expenses.gaadi_bharni || 0);
199
+ const grandTotal = subtotal + totalExp;
200
+
201
+ // Calculate Payment Details based on Mode
202
+ let finalCash = 0;
203
+ let finalOnline = 0;
204
+ let currentPaid = 0;
205
+
206
+ if (paymentMode === 'cash') {
207
+ finalCash = grandTotal;
208
+ finalOnline = 0;
209
+ currentPaid = grandTotal;
210
+ } else if (paymentMode === 'online') {
211
+ finalCash = 0;
212
+ finalOnline = grandTotal;
213
+ currentPaid = grandTotal;
214
+ } else if (paymentMode === 'hybrid') {
215
+ finalCash = cashAmount;
216
+ finalOnline = Math.max(0, grandTotal - cashAmount);
217
+ currentPaid = grandTotal; // Hybrid assumes full payment
218
+ } else if (paymentMode === 'due') {
219
+ finalCash = 0;
220
+ finalOnline = onlineAmount; // User defined
221
+ currentPaid = onlineAmount;
222
+ }
223
+
224
+ const balance = grandTotal - currentPaid;
225
+
226
+ // Validation Error
227
+ // Hybrid: Cash cannot exceed Total
228
+ // Due: Online cannot exceed Total
229
+ const isOverpaid = (paymentMode === 'hybrid' && cashAmount > grandTotal) ||
230
+ (paymentMode === 'due' && onlineAmount > grandTotal);
231
+
232
+ const validateForm = () => {
233
+ if (!selectedParty) {
234
+ alert('Please select a Party (पार्टी निवडा)');
235
+ return false;
236
+ }
237
+ for (let i = 0; i < items.length; i++) {
238
+ const item = items[i];
239
+ if (!item.mirchi_type_id) {
240
+ alert(`Row ${i + 1}: Please select Mirchi Type`);
241
+ return false;
242
+ }
243
+ if (!item.poti_weights || item.poti_weights.length === 0) {
244
+ alert(`Row ${i + 1}: Please enter weights (e.g. 10, 20)`);
245
+ return false;
246
+ }
247
+ if (!item.rate_per_kg || item.rate_per_kg <= 0) {
248
+ alert(`Row ${i + 1}: Rate must be greater than 0`);
249
+ return false;
250
+ }
251
+ }
252
+ if (isOverpaid) {
253
+ alert('Paid amount cannot be greater than Total Amount');
254
+ return false;
255
+ }
256
+ return true;
257
+ };
258
+
259
+ const handleSubmit = async () => {
260
+ if (isSubmitting) return;
261
+ if (!validateForm()) return;
262
+
263
+ setIsSubmitting(true);
264
+
265
+ // Populate mirchi_name for each item from mirchiTypes
266
+ const itemsWithNames = items.map(item => ({
267
+ ...item,
268
+ mirchi_name: mirchiTypes.find(m => m.id === item.mirchi_type_id)?.name || 'Unknown'
269
+ }));
270
+
271
+ const transaction: Transaction = {
272
+ id: Date.now().toString(),
273
+ bill_number: billNumber,
274
+ bill_date: billDate,
275
+ bill_type: BillType.JAWAAK,
276
+ is_return: isReturnMode,
277
+ party_id: selectedParty,
278
+ party_name: parties.find(p => p.id === selectedParty)?.name,
279
+ items: itemsWithNames as TransactionItem[],
280
+ expenses: {
281
+ ...expenses,
282
+ cess_amount: cessAmt,
283
+ adat_amount: adatAmt,
284
+ poti_amount: potiAmt,
285
+ hamali_amount: hamaliAmt,
286
+ packaging_hamali_amount: packagingHamaliAmt
287
+ },
288
+ payments: [
289
+ ...(finalCash > 0 ? [{ mode: PaymentMode.CASH, amount: finalCash }] : []),
290
+ ...(finalOnline > 0 ? [{ mode: PaymentMode.ONLINE, amount: finalOnline }] : []),
291
+ ...(balance > 0 ? [{ mode: PaymentMode.DUE, amount: balance }] : []) // Optional: Track due as a payment entry or just balance? Usually balance is enough. Keeping logic simple.
292
+ ],
293
+ gross_weight_total: items.reduce((a, i) => a + (i.gross_weight || 0), 0),
294
+ net_weight_total: items.reduce((a, i) => a + (i.net_weight || 0), 0),
295
+ subtotal: subtotal,
296
+ total_expenses: totalExp,
297
+ total_amount: grandTotal,
298
+ paid_amount: currentPaid,
299
+ balance_amount: balance
300
+ };
301
+
302
+ const result = await saveTransaction(transaction);
303
+ if (result.success) {
304
+ // Use the complete transaction object we created, not just the API response
305
+ // This ensures all items are included for printing
306
+ const completeTransaction = result.data ? {
307
+ ...transaction,
308
+ ...result.data,
309
+ items: transaction.items // Ensure items are preserved
310
+ } : transaction;
311
+ setSavedTransaction(completeTransaction);
312
+ alert('Bill Saved Successfully! You can now print the invoice.');
313
+ } else {
314
+ alert(`Error: ${result.message}`);
315
+ }
316
+ setIsSubmitting(false);
317
+ };
318
+
319
+ const handleHybridCashChange = (val: number) => {
320
+ setCashAmount(val);
321
+ // Online amount is derived in render, no state update needed for it in hybrid
322
+ };
323
+
324
+ const SummaryContent = () => (
325
+ <div className="space-y-3 text-sm">
326
+ <div className="flex justify-between">
327
+ <span className="text-gray-600">एकुण रक्कम (Subtotal)</span>
328
+ <span className="font-semibold">₹{subtotal.toFixed(2)}</span>
329
+ </div>
330
+
331
+ <div className="pt-2 border-t border-dashed space-y-2">
332
+ {/* 1. Cess Tax */}
333
+ <div className="flex justify-between items-center">
334
+ <span className="text-gray-600 text-xs">सेस (Cess {expenses.cess_percent}%)</span>
335
+ <span className="text-gray-800 font-medium">₹{cessAmt.toFixed(2)}</span>
336
+ </div>
337
+
338
+ {/* 2. Adat / Market Yard Tax */}
339
+ <SummaryInput
340
+ label={`अडत (Adat ${expenses.adat_percent}%)`}
341
+ value={expenses.adat_percent}
342
+ onChange={val => setExpenses({ ...expenses, adat_percent: val })}
343
+ />
344
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
345
+ <span>Amount:</span>
346
+ <span>₹{adatAmt.toFixed(2)}</span>
347
+ </div>
348
+
349
+ {/* 3. Poti (Packet) / Bag */}
350
+ <SummaryInput
351
+ label={`पोती (Bags ${totalPoti}) Rate`}
352
+ value={expenses.poti_rate}
353
+ onChange={val => setExpenses({ ...expenses, poti_rate: val })}
354
+ />
355
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
356
+ <span>Amount:</span>
357
+ <span>₹{potiAmt.toFixed(2)}</span>
358
+ </div>
359
+
360
+ {/* 4. Hamali */}
361
+ <div className="flex justify-between items-center">
362
+ <span className="text-gray-600 text-xs">हमाली ({expenses.hamali_per_poti} * {totalPoti})</span>
363
+ <span className="text-gray-800 font-medium">₹{hamaliAmt.toFixed(2)}</span>
364
+ </div>
365
+
366
+ {/* 5. Packaging Hamali */}
367
+ <SummaryInput
368
+ label="पॅकेजिंग हमाली (Rate)"
369
+ value={expenses.packaging_hamali_per_poti}
370
+ onChange={val => setExpenses({ ...expenses, packaging_hamali_per_poti: val })}
371
+ />
372
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
373
+ <span>Amount ({totalPoti} * {expenses.packaging_hamali_per_poti}):</span>
374
+ <span>₹{packagingHamaliAmt.toFixed(2)}</span>
375
+ </div>
376
+
377
+ {/* 6. Gaadi Bharni */}
378
+ <SummaryInput
379
+ label="गाडी भरणी"
380
+ value={expenses.gaadi_bharni}
381
+ onChange={val => setExpenses({ ...expenses, gaadi_bharni: val })}
382
+ />
383
+ </div>
384
+
385
+ {/* 6. Total Price */}
386
+ <div className="pt-2 border-t border-gray-200 flex justify-between items-center">
387
+ <span className="text-base font-bold text-gray-800">एकुण बिल (Total)</span>
388
+ <span className="text-xl font-bold text-red-600">₹{grandTotal.toFixed(2)}</span>
389
+ </div>
390
+
391
+ {/* Payment Section */}
392
+ <div className="bg-teal-50 p-3 rounded-lg mt-2 border border-teal-100">
393
+ <label className="block text-xs font-medium text-teal-800 mb-2">Payment Mode</label>
394
+ <div className="flex gap-2 mb-3">
395
+ {['cash', 'online', 'hybrid', 'due'].map(mode => (
396
+ <button
397
+ key={mode}
398
+ onClick={() => {
399
+ setPaymentMode(mode as any);
400
+ setCashAmount(0);
401
+ setOnlineAmount(0);
402
+ }}
403
+ className={`flex-1 py-1 text-xs font-medium rounded border ${paymentMode === mode
404
+ ? 'bg-teal-600 text-white border-teal-600'
405
+ : 'bg-white text-gray-600 border-gray-200'
406
+ } capitalize`}
407
+ >
408
+ {mode}
409
+ </button>
410
+ ))}
411
+ </div>
412
+
413
+ {paymentMode === 'cash' && (
414
+ <div className="text-center py-2 text-sm text-gray-600">
415
+ Full Payment in Cash: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
416
+ </div>
417
+ )}
418
+
419
+ {paymentMode === 'online' && (
420
+ <div className="text-center py-2 text-sm text-gray-600">
421
+ Full Payment Online: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
422
+ </div>
423
+ )}
424
+
425
+ {paymentMode === 'hybrid' && (
426
+ <div className="space-y-2">
427
+ <div>
428
+ <label className="text-xs text-gray-600">Cash Amount</label>
429
+ <input
430
+ type="number"
431
+ className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
432
+ value={cashAmount === 0 ? '' : cashAmount}
433
+ onChange={e => handleHybridCashChange(parseFloat(e.target.value) || 0)}
434
+ placeholder="Cash"
435
+ />
436
+ </div>
437
+ <div>
438
+ <label className="text-xs text-gray-600">Online Amount (Auto)</label>
439
+ <input
440
+ type="text"
441
+ readOnly
442
+ className="w-full bg-gray-100 border border-gray-200 rounded p-2 text-right font-medium text-gray-600"
443
+ value={(grandTotal - cashAmount).toFixed(2)}
444
+ />
445
+ </div>
446
+ </div>
447
+ )}
448
+
449
+ {paymentMode === 'due' && (
450
+ <div className="space-y-2">
451
+ <div>
452
+ <label className="text-xs text-gray-600">Online Amount (Partial Payment)</label>
453
+ <input
454
+ type="number"
455
+ className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
456
+ value={onlineAmount === 0 ? '' : onlineAmount}
457
+ onChange={e => setOnlineAmount(parseFloat(e.target.value) || 0)}
458
+ placeholder="Enter amount paid online (0 for full due)"
459
+ />
460
+ </div>
461
+ <div>
462
+ <label className="text-xs text-gray-600">Due Amount (Auto)</label>
463
+ <input
464
+ type="text"
465
+ readOnly
466
+ className="w-full bg-red-50 border border-red-200 rounded p-2 text-right font-bold text-red-600"
467
+ value={(grandTotal - onlineAmount).toFixed(2)}
468
+ />
469
+ </div>
470
+ </div>
471
+ )}
472
+
473
+ {isOverpaid && (
474
+ <div className="text-red-500 text-xs mt-2 font-medium text-center">
475
+ Error: Amount exceeds Total!
476
+ </div>
477
+ )}
478
+ </div>
479
+
480
+ <div className="flex justify-between items-center pt-2">
481
+ <span className="text-gray-600 font-medium">बाकी (Balance)</span>
482
+ <span className={`font-bold ${balance > 0 ? 'text-red-500' : 'text-green-600'}`}>₹{balance.toFixed(2)}</span>
483
+ </div>
484
+ </div>
485
+ );
486
+
487
+ return (
488
+ <div className="flex flex-col lg:flex-row gap-4 h-full p-4 bg-gray-50">
489
+ {isLoading && (
490
+ <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-gray-100">
491
+ <div className="text-center">
492
+ <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mb-4"></div>
493
+ <p className="text-gray-600">Loading data...</p>
494
+ </div>
495
+ </div>
496
+ )}
497
+ {error && !isLoading && (
498
+ <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-red-200">
499
+ <div className="text-center p-8">
500
+ <div className="text-red-500 text-5xl mb-4">⚠️</div>
501
+ <h3 className="text-lg font-semibold text-gray-800 mb-2">Error Loading Data</h3>
502
+ <p className="text-gray-600 mb-4">{error}</p>
503
+ <button onClick={() => window.location.reload()} className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700">
504
+ Retry
505
+ </button>
506
+ </div>
507
+ </div>
508
+ )}
509
+ {!isLoading && !error && (
510
+ <>
511
+ {/* Left: Form */}
512
+ <div className={`flex-1 bg-white rounded-xl shadow-sm border p-4 lg:p-6 lg:overflow-y-auto lg:h-full no-scrollbar pb-40 lg:pb-6 ${isReturnMode ? 'border-red-200' : 'border-gray-100'}`}>
513
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
514
+ <h2 className={`text-lg font-bold flex items-center gap-2 ${isReturnMode ? 'text-red-600' : 'text-gray-800'}`}>
515
+ {isReturnMode ? <RotateCcw className="text-red-500" /> : <div className="text-teal-600 font-bold">OUT</div>}
516
+ {isReturnMode ? 'जावक परतावा (Sales Return)' : 'जावक बिल (Sales)'}
517
+ </h2>
518
+
519
+ <div className="flex items-center gap-3">
520
+ <div className="flex bg-gray-100 rounded-lg p-1">
521
+ <button
522
+ onClick={() => setIsReturnMode(false)}
523
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${!isReturnMode ? 'bg-white text-teal-700 shadow-sm' : 'text-gray-500'}`}
524
+ >
525
+ Regular
526
+ </button>
527
+ <button
528
+ onClick={() => setIsReturnMode(true)}
529
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${isReturnMode ? 'bg-red-500 text-white shadow-sm' : 'text-gray-500'}`}
530
+ >
531
+ Return
532
+ </button>
533
+ </div>
534
+
535
+ <div className="bg-gray-100 px-3 py-1 rounded text-sm font-mono text-gray-600">
536
+ {billDate}
537
+ </div>
538
+ </div>
539
+ </div>
540
+
541
+ {/* Header Fields */}
542
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
543
+
544
+ <div>
545
+ <label className="block text-sm font-medium text-gray-700 mb-1">पार्टी नाव (Party)</label>
546
+ <select
547
+ className="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-teal-500 outline-none"
548
+ value={selectedParty}
549
+ onChange={e => setSelectedParty(e.target.value)}
550
+ >
551
+ <option value="">Select Party</option>
552
+ {parties.map(p => (
553
+ <option key={p.id} value={p.id}>{p.name} - {p.city}</option>
554
+ ))}
555
+ </select>
556
+ </div>
557
+ </div>
558
+
559
+ {/* Items Table */}
560
+ <div className="mb-6">
561
+ <div className="flex justify-between items-center mb-2">
562
+ <h3 className="font-semibold text-gray-700">माल तपशील (Items)</h3>
563
+ </div>
564
+
565
+ <div className="space-y-4">
566
+ {items.map((item, index) => (
567
+ <div key={item.id} className="p-4 border rounded-lg bg-gray-50 relative shadow-sm">
568
+ <button
569
+ onClick={() => removeItem(item.id!)}
570
+ className="absolute top-2 right-2 text-red-400 hover:text-red-600 p-1"
571
+ >
572
+ <Trash2 size={18} />
573
+ </button>
574
+
575
+ <div className="grid grid-cols-2 md:grid-cols-6 gap-3">
576
+ <div className="col-span-2">
577
+ <label className="block text-xs font-medium text-gray-500 mb-1">मिरची जात (Type)</label>
578
+ <select
579
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
580
+ value={item.mirchi_type_id || ''}
581
+ onChange={e => handleItemChange(item.id!, 'mirchi_type_id', e.target.value)}
582
+ >
583
+ <option value="">Select Type</option>
584
+ {mirchiTypes.map(m => (
585
+ <option key={m.id} value={m.id}>{m.name}</option>
586
+ ))}
587
+ </select>
588
+ </div>
589
+ <div className="col-span-2 md:col-span-4">
590
+ <label className="block text-xs font-medium text-gray-500 mb-1">पोटी वजन (Eg: 10,20,30)</label>
591
+ <input
592
+ type="text"
593
+ placeholder="10, 20, 30"
594
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
595
+ value={potiInputs[item.id!] || ''}
596
+ onChange={e => handlePotiInputChange(item.id!, e.target.value)}
597
+ />
598
+ </div>
599
+
600
+ <div>
601
+ <label className="block text-xs font-medium text-gray-500 mb-1">Gross</label>
602
+ <input type="number" readOnly className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm" value={item.gross_weight} />
603
+ </div>
604
+ <div>
605
+ <label className="block text-xs font-medium text-gray-500 mb-1">Poti (count)</label>
606
+ <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} />
607
+ </div>
608
+ <div>
609
+ <label className="block text-xs font-medium text-gray-500 mb-1">Net</label>
610
+ <input type="number" readOnly className="w-full bg-blue-50 border border-blue-200 rounded p-2 text-sm font-bold text-blue-800" value={item.net_weight} />
611
+ </div>
612
+ <div>
613
+ <label className="block text-xs font-medium text-gray-500 mb-1">Rate (₹)</label>
614
+ <input
615
+ type="number"
616
+ min="0"
617
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white appearance-none"
618
+ onWheel={e => e.currentTarget.blur()}
619
+ value={item.rate_per_kg === 0 ? '' : item.rate_per_kg}
620
+ onChange={e => handleItemChange(item.id!, 'rate_per_kg', Math.max(0, parseFloat(e.target.value) || 0))}
621
+ placeholder="0"
622
+ />
623
+ </div>
624
+ <div className="col-span-2 md:col-span-2">
625
+ <label className="block text-xs font-medium text-gray-500 mb-1">Total (₹)</label>
626
+ <input type="number" readOnly className="w-full bg-green-50 border border-green-200 rounded p-2 text-sm font-bold text-right text-green-800" value={item.item_total?.toFixed(2)} />
627
+ </div>
628
+ </div>
629
+ </div>
630
+ ))}
631
+ </div>
632
+ <button
633
+ onClick={addItem}
634
+ className="w-full mt-4 flex justify-center items-center gap-2 text-teal-600 font-medium bg-teal-50 hover:bg-teal-100 py-3 rounded-lg border border-teal-100 transition"
635
+ >
636
+ <Plus size={18} /> Add New Item (नवीन माल)
637
+ </button>
638
+ </div>
639
+ </div>
640
+
641
+ {/* Desktop: Right Summary Sidebar */}
642
+ <div className="hidden lg:flex w-80 flex-col gap-4">
643
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-0">
644
+ <h3 className="font-bold text-gray-800 mb-4 border-b pb-2">बिल सारांश (Summary)</h3>
645
+ {SummaryContent()}
646
+ <button
647
+ onClick={handleSubmit}
648
+ disabled={isSubmitting || isOverpaid}
649
+ className="w-full mt-6 bg-teal-600 text-white py-3 rounded-lg font-semibold hover:bg-teal-700 flex justify-center items-center gap-2 shadow-lg shadow-teal-100 disabled:opacity-50 disabled:bg-gray-400"
650
+ >
651
+ <Save size={20} /> {isSubmitting ? 'Saving...' : 'जतन करा (Save)'}
652
+ </button>
653
+ {savedTransaction && (
654
+ <div className="mt-4">
655
+ <PrintInvoice transaction={savedTransaction} />
656
+ </div>
657
+ )}
658
+ </div>
659
+ </div>
660
+
661
+ {/* Mobile: Sticky Bottom Action Bar */}
662
+ <div className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-[0_-4px_10px_rgba(0,0,0,0.1)] z-20">
663
+ <div className="flex items-center justify-between p-4 bg-white z-20 relative">
664
+ <div
665
+ onClick={() => setShowMobileSummary(!showMobileSummary)}
666
+ className="flex flex-col cursor-pointer"
667
+ >
668
+ <div className="flex items-center gap-1 text-gray-500 text-xs font-medium uppercase tracking-wide">
669
+ Summary {showMobileSummary ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
670
+ </div>
671
+ <div className="text-xl font-bold text-red-600">
672
+ ₹{grandTotal.toFixed(2)}
673
+ </div>
674
+ </div>
675
+ <button
676
+ onClick={handleSubmit}
677
+ disabled={isSubmitting}
678
+ className="bg-teal-600 text-white px-6 py-2.5 rounded-lg font-semibold flex items-center gap-2 shadow-sm active:bg-teal-700 disabled:opacity-50"
679
+ >
680
+ <Save size={18} /> {isSubmitting ? '...' : 'Save'}
681
+ </button>
682
+ </div>
683
+
684
+ {showMobileSummary && (
685
+ <div className="px-6 pb-6 pt-0 bg-white border-t border-dashed border-gray-200 max-h-[60vh] overflow-y-auto animate-in slide-in-from-bottom-2">
686
+ <div className="mt-4">
687
+ {SummaryContent()}
688
+ </div>
689
+ {savedTransaction && (
690
+ <div className="mt-4 flex justify-center">
691
+ <PrintInvoice transaction={savedTransaction} />
692
+ </div>
693
+ )}
694
+ </div>
695
+ )}
696
+ </div>
697
+ </>
698
+ )}
699
+ </div>
700
+ );
701
+ };
702
+
703
+ export default JawaakBill;
pages/PartyLedger.tsx ADDED
@@ -0,0 +1,596 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = () => {
9
+ const [parties, setParties] = useState<Party[]>([]);
10
+ const [transactions, setTransactions] = useState<Transaction[]>([]);
11
+ const [activeTab, setActiveTab] = useState<'awaak' | 'jawaak'>('awaak');
12
+ const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
13
+ const [selectedParty, setSelectedParty] = useState<Party | null>(null);
14
+ const [searchQuery, setSearchQuery] = useState('');
15
+
16
+ // Update Payment State
17
+ const [editingTxId, setEditingTxId] = useState<string | null>(null);
18
+ const [paymentAmount, setPaymentAmount] = useState<string>('');
19
+
20
+ // Multi-select for combined invoice
21
+ const [selectedTransactions, setSelectedTransactions] = useState<string[]>([]);
22
+
23
+ const loadData = async () => {
24
+ setParties(await getParties());
25
+ setTransactions(await getTransactions());
26
+ };
27
+
28
+ useEffect(() => {
29
+ loadData();
30
+ }, []);
31
+
32
+ // Filter Parties based on Tab and Search
33
+ const filteredParties = parties.filter(p => {
34
+ const matchesTab = activeTab === 'awaak'
35
+ ? (p.party_type === PartyType.AWAAK || p.party_type === PartyType.BOTH)
36
+ : (p.party_type === PartyType.JAWAAK || p.party_type === PartyType.BOTH);
37
+
38
+ const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
39
+ p.phone.includes(searchQuery) ||
40
+ p.city.toLowerCase().includes(searchQuery.toLowerCase());
41
+
42
+ return matchesTab && matchesSearch;
43
+ });
44
+
45
+ // Get Transactions for Selected Party
46
+ const partyTransactions = selectedParty
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);
53
+ const totalBill = txs.reduce((sum, t) => sum + t.total_amount, 0);
54
+ const totalPaid = txs.reduce((sum, t) => sum + t.paid_amount, 0);
55
+ // Balance is directly from party object for accuracy, or calculated?
56
+ // Using party.current_balance is better as it's the source of truth for ledger
57
+ const party = parties.find(p => p.id === partyId);
58
+ return {
59
+ totalBill,
60
+ totalPaid,
61
+ balance: party?.current_balance || 0
62
+ };
63
+ };
64
+
65
+ const handleUpdatePayment = async (tx: Transaction) => {
66
+ const amount = parseFloat(paymentAmount);
67
+ if (isNaN(amount) || amount <= 0) {
68
+ alert('Please enter a valid amount');
69
+ return;
70
+ }
71
+ if (amount > tx.balance_amount) {
72
+ alert('Amount cannot exceed due balance');
73
+ return;
74
+ }
75
+
76
+ const res = await updateTransactionPayment(tx.id, amount);
77
+ if (res.success) {
78
+ // Update local state for demo/sample data
79
+ setTransactions((prev) =>
80
+ prev.map((t) =>
81
+ t.id === tx.id
82
+ ? {
83
+ ...t,
84
+ paid_amount: t.paid_amount + amount,
85
+ balance_amount: t.balance_amount - amount,
86
+ }
87
+ : t,
88
+ ),
89
+ );
90
+ setEditingTxId(null);
91
+ setPaymentAmount('');
92
+ // Reload data to refresh party balances
93
+ loadData();
94
+ } else {
95
+ alert(res.message);
96
+ }
97
+ };
98
+
99
+
100
+ return (
101
+ <div className="space-y-6">
102
+ {/* Header & Controls */}
103
+ <div className="flex flex-col md:flex-row justify-between items-center bg-white p-4 rounded-xl border border-gray-100 shadow-sm gap-4">
104
+ {viewMode === 'detail' && selectedParty ? (
105
+ <>
106
+ {/* Header with Party Info */}
107
+ <div className="flex items-center justify-between w-full">
108
+ <div className="flex items-center gap-4">
109
+ <button
110
+ onClick={() => { setViewMode('list'); setSelectedParty(null); setSelectedTransactions([]); }}
111
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
112
+ >
113
+ <ArrowLeft size={20} className="text-gray-600" />
114
+ </button>
115
+ <div>
116
+ <h2 className="text-2xl font-bold text-gray-800">{selectedParty.name}</h2>
117
+ <p className="text-sm text-gray-500">{selectedParty.city} • {selectedParty.phone}</p>
118
+ </div>
119
+ </div>
120
+ {selectedTransactions.length > 0 && (
121
+ <button
122
+ onClick={() => {
123
+ const selected = partyTransactions.filter(t => selectedTransactions.includes(t.id));
124
+ if (selected.length > 0) {
125
+ // Create combined transaction for printing
126
+ const combinedTransaction = {
127
+ ...selected[0], // Use first transaction as base
128
+ id: `combined-${selectedParty.id}-${Date.now()}`, // Unique ID for combined
129
+ bill_number: `COMBINED-${selected.length}-BILLS`,
130
+ items: selected.flatMap(t => t.items),
131
+ subtotal: selected.reduce((sum, t) => sum + (t.subtotal || 0), 0),
132
+ total_expenses: selected.reduce((sum, t) => sum + (t.total_expenses || 0), 0),
133
+ total_amount: selected.reduce((sum, t) => sum + t.total_amount, 0),
134
+ paid_amount: selected.reduce((sum, t) => sum + t.paid_amount, 0),
135
+ balance_amount: selected.reduce((sum, t) => sum + t.balance_amount, 0),
136
+ gross_weight_total: selected.reduce((sum, t) => sum + (t.gross_weight_total || 0), 0),
137
+ net_weight_total: selected.reduce((sum, t) => sum + (t.net_weight_total || 0), 0),
138
+ };
139
+ // Trigger print for combined transaction
140
+ const printComponent = document.createElement('div');
141
+ document.body.appendChild(printComponent);
142
+ // Use PrintInvoice component
143
+ import('../components/PrintInvoice').then(({ default: PrintInvoice }) => {
144
+ const React = require('react');
145
+ const ReactDOM = require('react-dom/client');
146
+ const root = ReactDOM.createRoot(printComponent);
147
+ root.render(React.createElement(PrintInvoice, { transaction: combinedTransaction }));
148
+ setTimeout(() => {
149
+ const printBtn = printComponent.querySelector('button');
150
+ if (printBtn) printBtn.click();
151
+ setTimeout(() => {
152
+ root.unmount();
153
+ document.body.removeChild(printComponent);
154
+ }, 1000);
155
+ }, 500);
156
+ });
157
+ }
158
+ }}
159
+ className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-2"
160
+ >
161
+ <Printer size={18} />
162
+ Print {selectedTransactions.length} Selected
163
+ </button>
164
+ )}
165
+ <button
166
+ onClick={() => exportPartyLedger(selectedParty, partyTransactions)}
167
+ className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2"
168
+ >
169
+ <Download size={18} />
170
+ Export to Excel
171
+ </button>
172
+ </div>
173
+ </>
174
+ ) : (
175
+ <div className="flex items-center gap-2">
176
+ <Users className="text-teal-600" />
177
+ <h2 className="text-lg font-bold">
178
+ पार्टी लेजर (Party Ledger)
179
+ </h2>
180
+ </div>
181
+ )}
182
+
183
+ {viewMode === 'list' && (
184
+ <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
185
+ {/* Tabs */}
186
+ <div className="flex bg-gray-100 p-1 rounded-lg">
187
+ <button
188
+ onClick={() => setActiveTab('awaak')}
189
+ className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${activeTab === 'awaak' ? 'bg-white text-teal-700 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
190
+ >
191
+ Awaak (Purchase)
192
+ </button>
193
+ <button
194
+ onClick={() => setActiveTab('jawaak')}
195
+ className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${activeTab === 'jawaak' ? 'bg-white text-blue-700 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
196
+ >
197
+ Jawaak (Sales)
198
+ </button>
199
+ </div>
200
+
201
+ {/* Search */}
202
+ <div className="relative w-full md:w-auto mt-2 md:mt-0">
203
+ <Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
204
+ <input
205
+ type="text"
206
+ placeholder="Search Party..."
207
+ className="pl-10 pr-4 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500 outline-none w-full md:w-64"
208
+ value={searchQuery}
209
+ onChange={e => setSearchQuery(e.target.value)}
210
+ />
211
+ </div>
212
+ </div>
213
+ )}
214
+ </div>
215
+
216
+ {/* Content Area */}
217
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
218
+ {viewMode === 'list' ? (
219
+ <>
220
+ {/* Desktop Table View */}
221
+ <div className="hidden md:block overflow-x-auto">
222
+ <table className="w-full text-sm text-left">
223
+ <thead className="bg-gray-50 text-gray-500 border-b border-gray-100">
224
+ <tr>
225
+ <th className="px-6 py-4 font-medium">Party Name</th>
226
+ <th className="px-6 py-4 font-medium">City</th>
227
+ <th className="px-6 py-4 font-medium text-right">Total Bill Amount</th>
228
+ <th className="px-6 py-4 font-medium text-right">Jama (Paid)</th>
229
+ <th className="px-6 py-4 font-medium text-right">Baki (Balance)</th>
230
+ <th className="px-6 py-4 font-medium text-center">Action</th>
231
+ </tr>
232
+ </thead>
233
+ <tbody className="divide-y divide-gray-100">
234
+ {filteredParties.length > 0 ? (
235
+ filteredParties.map(party => {
236
+ const stats = getPartyStats(party.id);
237
+ return (
238
+ <tr key={party.id} className="hover:bg-gray-50 transition-colors">
239
+ <td className="px-6 py-4 font-medium text-gray-900">{party.name}</td>
240
+ <td className="px-6 py-4 text-gray-500">{party.city}</td>
241
+ <td className="px-6 py-4 text-right font-medium">₹{stats.totalBill.toLocaleString()}</td>
242
+ <td className="px-6 py-4 text-right text-green-600 font-medium">₹{stats.totalPaid.toLocaleString()}</td>
243
+ <td className={`px-6 py-4 text-right font-bold ${stats.balance !== 0 ? 'text-red-500' : 'text-gray-400'}`}>
244
+ ₹{Math.abs(stats.balance).toLocaleString()} {stats.balance > 0 ? '(Dr)' : stats.balance < 0 ? '(Cr)' : ''}
245
+ </td>
246
+ <td className="px-6 py-4 text-center">
247
+ <button
248
+ onClick={() => {
249
+ setSelectedParty(party);
250
+ setViewMode('detail');
251
+ // If this party has no transactions, add some sample data for demo
252
+ const hasTx = transactions.some(t => t.party_id === party.id);
253
+ if (!hasTx) {
254
+ const sampleTxs = [
255
+ {
256
+ id: `${party.id}-tx1`,
257
+ party_id: party.id,
258
+ bill_date: new Date().toISOString().split('T')[0],
259
+ bill_number: 'SAMPLE001',
260
+ items: [{ mirchi_name: 'Red Chili' }],
261
+ is_return: false,
262
+ total_amount: 5000,
263
+ paid_amount: 2000,
264
+ balance_amount: 3000,
265
+ bill_type: PartyType.AWAAK,
266
+ },
267
+ {
268
+ id: `${party.id}-tx2`,
269
+ party_id: party.id,
270
+ bill_date: new Date().toISOString().split('T')[0],
271
+ bill_number: 'SAMPLE002',
272
+ items: [{ mirchi_name: 'Green Chili' }],
273
+ is_return: false,
274
+ total_amount: 3000,
275
+ paid_amount: 3000,
276
+ balance_amount: 0,
277
+ bill_type: PartyType.AWAAK,
278
+ },
279
+ ];
280
+ setTransactions(prev => [...prev, ...sampleTxs]);
281
+ }
282
+ }}
283
+ className="p-2 hover:bg-teal-50 text-teal-600 rounded-full transition-colors"
284
+ title="View Ledger"
285
+ >
286
+ <Eye size={18} />
287
+ </button>
288
+ </td>
289
+ </tr>
290
+ );
291
+ })
292
+ ) : (
293
+ <tr>
294
+ <td colSpan={6} className="px-6 py-8 text-center text-gray-500">
295
+ No parties found.
296
+ </td>
297
+ </tr>
298
+ )}
299
+ </tbody>
300
+ </table>
301
+ </div>
302
+
303
+ {/* Mobile Card View */}
304
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
305
+ {filteredParties.length > 0 ? (
306
+ filteredParties.map(party => {
307
+ const stats = getPartyStats(party.id);
308
+ return (
309
+ <div key={party.id} className="p-4 space-y-3">
310
+ <div className="flex justify-between items-start">
311
+ <div>
312
+ <h3 className="font-bold text-gray-900">{party.name}</h3>
313
+ <p className="text-xs text-gray-500">{party.city}</p>
314
+ </div>
315
+ <button
316
+ onClick={() => {
317
+ setSelectedParty(party);
318
+ setViewMode('detail');
319
+ // If this party has no transactions, add sample data for demo
320
+ const hasTx = transactions.some(t => t.party_id === party.id);
321
+ if (!hasTx) {
322
+ const sampleTxs = [
323
+ {
324
+ id: `${party.id}-tx1`,
325
+ party_id: party.id,
326
+ bill_date: new Date().toISOString().split('T')[0],
327
+ bill_number: 'SAMPLE001',
328
+ items: [{ mirchi_name: 'Red Chili' }],
329
+ is_return: false,
330
+ total_amount: 5000,
331
+ paid_amount: 2000,
332
+ balance_amount: 3000,
333
+ bill_type: PartyType.AWAAK,
334
+ },
335
+ {
336
+ id: `${party.id}-tx2`,
337
+ party_id: party.id,
338
+ bill_date: new Date().toISOString().split('T')[0],
339
+ bill_number: 'SAMPLE002',
340
+ items: [{ mirchi_name: 'Green Chili' }],
341
+ is_return: false,
342
+ total_amount: 3000,
343
+ paid_amount: 3000,
344
+ balance_amount: 0,
345
+ bill_type: PartyType.AWAAK,
346
+ },
347
+ ];
348
+ setTransactions(prev => [...prev, ...sampleTxs]);
349
+ }
350
+ }}
351
+ className="p-2 bg-teal-50 text-teal-600 rounded-lg"
352
+ >
353
+ <Eye size={18} />
354
+ </button>
355
+ </div>
356
+ <div className="grid grid-cols-3 gap-2 text-xs">
357
+ <div className="bg-gray-50 p-2 rounded">
358
+ <div className="text-gray-500 mb-1">Total Bill</div>
359
+ <div className="font-medium">₹{stats.totalBill.toLocaleString()}</div>
360
+ </div>
361
+ <div className="bg-green-50 p-2 rounded">
362
+ <div className="text-green-600 mb-1">Paid</div>
363
+ <div className="font-medium text-green-700">₹{stats.totalPaid.toLocaleString()}</div>
364
+ </div>
365
+ <div className="bg-red-50 p-2 rounded">
366
+ <div className="text-red-500 mb-1">Balance</div>
367
+ <div className="font-bold text-red-600">
368
+ ₹{Math.abs(stats.balance).toLocaleString()} {stats.balance > 0 ? 'Dr' : stats.balance < 0 ? 'Cr' : ''}
369
+ </div>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ );
374
+ })
375
+ ) : (
376
+ <div className="p-8 text-center text-gray-500">
377
+ No parties found.
378
+ </div>
379
+ )}
380
+ </div>
381
+ </>
382
+ ) : (
383
+ // Detail View
384
+ <>
385
+ {/* Desktop Table View */}
386
+ <div className="hidden md:block overflow-x-auto">
387
+ <table className="w-full text-sm text-left">
388
+ <thead className="bg-gray-50 text-gray-500 border-b border-gray-100">
389
+ <tr>
390
+ <th className="px-6 py-4 font-medium text-center">
391
+ <input
392
+ type="checkbox"
393
+ checked={selectedTransactions.length === partyTransactions.length && partyTransactions.length > 0}
394
+ onChange={(e) => {
395
+ if (e.target.checked) {
396
+ setSelectedTransactions(partyTransactions.map(t => t.id));
397
+ } else {
398
+ setSelectedTransactions([]);
399
+ }
400
+ }}
401
+ className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500"
402
+ />
403
+ </th>
404
+ <th className="px-6 py-4 font-medium">Date</th>
405
+ <th className="px-6 py-4 font-medium">Bill No</th>
406
+ <th className="px-6 py-4 font-medium">Mirchi Type</th>
407
+ <th className="px-6 py-4 font-medium">Remark</th>
408
+ <th className="px-6 py-4 font-medium text-right">Bill Amount</th>
409
+ <th className="px-6 py-4 font-medium text-right">Paid</th>
410
+ <th className="px-6 py-4 font-medium text-right">Due Balance</th>
411
+ <th className="px-6 py-4 font-medium text-center">Action</th>
412
+ <th className="px-6 py-4 font-medium text-center">Print</th>
413
+ </tr>
414
+ </thead>
415
+ <tbody className="divide-y divide-gray-100">
416
+ {partyTransactions.length > 0 ? (
417
+ partyTransactions.map(tx => (
418
+ <tr key={tx.id} className="hover:bg-gray-50">
419
+ <td className="px-6 py-4 text-center">
420
+ <input
421
+ type="checkbox"
422
+ checked={selectedTransactions.includes(tx.id)}
423
+ onChange={(e) => {
424
+ if (e.target.checked) {
425
+ setSelectedTransactions([...selectedTransactions, tx.id]);
426
+ } else {
427
+ setSelectedTransactions(selectedTransactions.filter(id => id !== tx.id));
428
+ }
429
+ }}
430
+ className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500"
431
+ />
432
+ </td>
433
+ <td className="px-6 py-4 text-gray-600">{tx.bill_date}</td>
434
+ <td className="px-6 py-4 font-mono text-gray-500">{tx.bill_number}</td>
435
+ <td className="px-6 py-4">
436
+ {tx.items.map(i => i.mirchi_name).join(', ')}
437
+ </td>
438
+ <td className="px-6 py-4">
439
+ {tx.is_return ? (
440
+ <span className="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs font-semibold">Returned</span>
441
+ ) : (
442
+ <span className="text-gray-400">-</span>
443
+ )}
444
+ </td>
445
+ <td className="px-6 py-4 text-right font-medium">₹{tx.total_amount.toLocaleString()}</td>
446
+ <td className="px-6 py-4 text-right text-green-600">₹{tx.paid_amount.toLocaleString()}</td>
447
+ <td className="px-6 py-4 text-right font-bold text-red-500">₹{tx.balance_amount.toLocaleString()}</td>
448
+ <td className="px-6 py-4 text-center">
449
+ {tx.balance_amount > 0 ? (
450
+ editingTxId === tx.id ? (
451
+ <div className="flex items-center gap-2 justify-end">
452
+ <input
453
+ type="number"
454
+ className="w-24 border border-teal-300 rounded px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-teal-500"
455
+ placeholder="Amount"
456
+ value={paymentAmount}
457
+ onChange={e => setPaymentAmount(e.target.value)}
458
+ autoFocus
459
+ />
460
+ <button
461
+ onClick={() => handleUpdatePayment(tx)}
462
+ className="p-2 bg-teal-600 text-white rounded hover:bg-teal-700 disabled:opacity-50"
463
+ title="Save"
464
+ disabled={Number(paymentAmount) > tx.balance_amount || Number(paymentAmount) <= 0}
465
+ >
466
+ <Save size={16} />
467
+ </button>
468
+ <button
469
+ onClick={() => { setEditingTxId(null); setPaymentAmount(''); }}
470
+ className="p-1 bg-gray-200 text-gray-600 rounded hover:bg-gray-300"
471
+ title="Cancel"
472
+ >
473
+ <X size={14} />
474
+ </button>
475
+ </div>
476
+ ) : (
477
+ <button
478
+ onClick={() => { setEditingTxId(tx.id); setPaymentAmount(''); }}
479
+ className="px-3 py-1 border border-teal-600 text-teal-600 rounded-md text-xs font-medium hover:bg-teal-50 transition-colors"
480
+ >
481
+ Update Due
482
+ </button>
483
+ )
484
+ ) : (
485
+ <span className="text-green-600 text-xs font-bold flex items-center justify-center gap-1">
486
+ Paid
487
+ </span>
488
+ )}
489
+ </td>
490
+ <td className="px-6 py-4 text-center">
491
+ <PrintInvoice transaction={tx} />
492
+ </td>
493
+ </tr>
494
+ ))
495
+ ) : (
496
+ <tr>
497
+ <td colSpan={8} className="px-6 py-8 text-center text-gray-500">
498
+ No transactions found for this party.
499
+ </td>
500
+ </tr>
501
+ )}
502
+ </tbody>
503
+ </table>
504
+ </div>
505
+
506
+ {/* Mobile Card View for Detail */}
507
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
508
+ {partyTransactions.length > 0 ? (
509
+ partyTransactions.map(tx => (
510
+ <div key={tx.id} className="p-4 space-y-3">
511
+ <div className="flex justify-between items-start">
512
+ <div>
513
+ <div className="text-xs text-gray-500">{tx.bill_date}</div>
514
+ <div className="font-mono text-sm font-medium text-gray-800">{tx.bill_number}</div>
515
+ </div>
516
+ {tx.is_return && (
517
+ <span className="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs font-semibold">Returned</span>
518
+ )}
519
+ </div>
520
+
521
+ <div className="text-sm text-gray-600">
522
+ <span className="font-medium text-gray-500 text-xs block mb-1">Items:</span>
523
+ {tx.items.map(i => i.mirchi_name).join(', ')}
524
+ </div>
525
+
526
+ <div className="grid grid-cols-3 gap-2 text-xs pt-2 border-t border-gray-50">
527
+ <div>
528
+ <div className="text-gray-500">Bill Amount</div>
529
+ <div className="font-medium">₹{tx.total_amount.toLocaleString()}</div>
530
+ </div>
531
+ <div>
532
+ <div className="text-gray-500">Paid</div>
533
+ <div className="font-medium text-green-600">₹{tx.paid_amount.toLocaleString()}</div>
534
+ </div>
535
+ <div>
536
+ <div className="text-gray-500">Due</div>
537
+ <div className="font-bold text-red-500">₹{tx.balance_amount.toLocaleString()}</div>
538
+ </div>
539
+ </div>
540
+
541
+ {tx.balance_amount > 0 && (
542
+ <div className="pt-2">
543
+ {editingTxId === tx.id ? (
544
+ <div className="flex items-center gap-2">
545
+ <input
546
+ type="number"
547
+ className="flex-1 border border-teal-300 rounded px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-teal-500"
548
+ placeholder="Enter Amount"
549
+ value={paymentAmount}
550
+ onChange={e => setPaymentAmount(e.target.value)}
551
+ autoFocus
552
+ />
553
+ <button
554
+ onClick={() => handleUpdatePayment(tx)}
555
+ className="p-2 bg-teal-600 text-white rounded hover:bg-teal-700"
556
+ >
557
+ <Save size={16} />
558
+ </button>
559
+ <button
560
+ onClick={() => { setEditingTxId(null); setPaymentAmount(''); }}
561
+ className="p-2 bg-gray-200 text-gray-600 rounded hover:bg-gray-300"
562
+ >
563
+ <X size={16} />
564
+ </button>
565
+ </div>
566
+ ) : (
567
+ <button
568
+ onClick={() => { setEditingTxId(tx.id); setPaymentAmount(''); }}
569
+ className="w-full py-2 border border-teal-600 text-teal-600 rounded-lg text-sm font-medium hover:bg-teal-50 transition-colors"
570
+ >
571
+ Update Due
572
+ </button>
573
+ )}
574
+ </div>
575
+ )}
576
+
577
+ {/* Print Button for Mobile */}
578
+ <div className="pt-2 border-t border-gray-100 mt-2">
579
+ <PrintInvoice transaction={tx} />
580
+ </div>
581
+ </div>
582
+ ))
583
+ ) : (
584
+ <div className="p-8 text-center text-gray-500">
585
+ No transactions found.
586
+ </div>
587
+ )}
588
+ </div>
589
+ </>
590
+ )}
591
+ </div>
592
+ </div>
593
+ );
594
+ };
595
+
596
+ export default PartyLedger;
pages/Settings.tsx ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ getParties,
4
+ saveParty,
5
+ apiGetMirchiTypes,
6
+ apiSaveMirchiType,
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');
13
+ const [parties, setParties] = useState<Party[]>([]);
14
+ const [mirchiTypes, setMirchiTypes] = useState<MirchiType[]>([]);
15
+
16
+ // PWA Install
17
+ const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
18
+ const [isInstallable, setIsInstallable] = useState(false);
19
+
20
+ // Form States
21
+ const [newParty, setNewParty] = useState<Partial<Party>>({
22
+ name: '', city: '', phone: '', party_type: PartyType.BOTH, current_balance: 0
23
+ });
24
+ const [newMirchi, setNewMirchi] = useState<Partial<MirchiType>>({
25
+ name: ''
26
+ });
27
+
28
+ // Inline feedback
29
+ const [partyMessage, setPartyMessage] = useState<{ type: 'error' | 'success'; text: string } | null>(null);
30
+ const [mirchiMessage, setMirchiMessage] = useState<{ type: 'error' | 'success'; text: string } | null>(null);
31
+
32
+ // Alert Config (Mock)
33
+ const [config, setConfig] = useState({
34
+ lowStockThreshold: 50,
35
+ enableNotifications: true
36
+ });
37
+
38
+ useEffect(() => {
39
+ const loadData = async () => {
40
+ setParties(await getParties());
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 () => {
60
+ if (!newParty.name) {
61
+ setPartyMessage({ type: 'error', text: 'Party Name is required.' });
62
+ return;
63
+ }
64
+
65
+ const party: Party = {
66
+ id: newParty.id || `p-${Date.now()}`,
67
+ name: newParty.name,
68
+ city: newParty.city || '',
69
+ phone: newParty.phone || '',
70
+ party_type: newParty.party_type as PartyType,
71
+ current_balance: parseFloat(String(newParty.current_balance || 0))
72
+ };
73
+
74
+ await saveParty(party);
75
+ setNewParty({ name: '', city: '', phone: '', party_type: PartyType.BOTH, current_balance: 0 });
76
+ loadData();
77
+ setPartyMessage({ type: 'success', text: 'Party saved successfully.' });
78
+ };
79
+
80
+ const handleSaveMirchi = async () => {
81
+ if (!newMirchi.name) {
82
+ setMirchiMessage({ type: 'error', text: 'Mirchi type name is required.' });
83
+ return;
84
+ }
85
+
86
+ const type: MirchiType = {
87
+ id: newMirchi.id || `m-${Date.now()}`,
88
+ name: newMirchi.name,
89
+ current_rate: 0
90
+ };
91
+
92
+ const result = await apiSaveMirchiType(type);
93
+ if (!result.success) {
94
+ setMirchiMessage({ type: 'error', text: result.message || 'Error saving mirchi type.' });
95
+ return;
96
+ }
97
+ setNewMirchi({ name: '' });
98
+ loadData();
99
+ setMirchiMessage({ type: 'success', text: 'Mirchi type saved successfully.' });
100
+ };
101
+
102
+ const loadData = async () => {
103
+ setParties(await getParties());
104
+ setMirchiTypes(await apiGetMirchiTypes());
105
+ };
106
+
107
+ const handleInstallClick = async () => {
108
+ if (!deferredPrompt) return;
109
+
110
+ deferredPrompt.prompt();
111
+ const { outcome } = await deferredPrompt.userChoice;
112
+
113
+ if (outcome === 'accepted') {
114
+ console.log('✅ PWA installed');
115
+ }
116
+
117
+ setDeferredPrompt(null);
118
+ setIsInstallable(false);
119
+ };
120
+
121
+ return (
122
+ <div className="flex flex-col md:flex-row h-full gap-6">
123
+ {/* Sidebar / Tabs */}
124
+ <div className="w-full md:w-64 bg-white rounded-xl shadow-sm border border-gray-100 p-4 h-fit">
125
+ <h2 className="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
126
+ <SettingsIcon className="text-teal-600" size={20} /> सेटिंग्स
127
+ </h2>
128
+ <div className="space-y-2">
129
+ <button
130
+ onClick={() => setActiveTab('parties')}
131
+ className={`w-full text-left px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-3 transition ${activeTab === 'parties' ? 'bg-teal-50 text-teal-700' : 'text-gray-600 hover:bg-gray-50'}`}
132
+ >
133
+ <Users size={18} /> पार्टी व्यवस्थापन
134
+ </button>
135
+ <button
136
+ onClick={() => setActiveTab('mirchi')}
137
+ className={`w-full text-left px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-3 transition ${activeTab === 'mirchi' ? 'bg-teal-50 text-teal-700' : 'text-gray-600 hover:bg-gray-50'}`}
138
+ >
139
+ <Sprout size={18} /> मिरची दर & प्रकार
140
+ </button>
141
+ <button
142
+ onClick={() => setActiveTab('general')}
143
+ className={`w-full text-left px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-3 transition ${activeTab === 'general' ? 'bg-teal-50 text-teal-700' : 'text-gray-600 hover:bg-gray-50'}`}
144
+ >
145
+ <Bell size={18} /> Alerts & Rules
146
+ </button>
147
+ </div>
148
+ </div>
149
+
150
+ {/* Content Area */}
151
+ <div className="flex-1 bg-white rounded-xl shadow-sm border border-gray-100 p-6 overflow-y-auto no-scrollbar">
152
+
153
+ {/* PARTIES TAB */}
154
+ {activeTab === 'parties' && (
155
+ <div className="space-y-6">
156
+ <div className="border-b pb-4">
157
+ <h3 className="text-lg font-bold text-gray-800">Party व्यवस्थापन (Party Management)</h3>
158
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
159
+ <input
160
+ placeholder="Party Name"
161
+ className="border rounded p-2"
162
+ value={newParty.name}
163
+ onChange={e => setNewParty({ ...newParty, name: e.target.value })}
164
+ />
165
+ <input
166
+ placeholder="Phone"
167
+ className="border rounded p-2"
168
+ value={newParty.phone}
169
+ onChange={e => setNewParty({ ...newParty, phone: e.target.value })}
170
+ />
171
+ <select
172
+ className="border rounded p-2"
173
+ value={newParty.party_type}
174
+ onChange={e => setNewParty({ ...newParty, party_type: e.target.value as PartyType })}
175
+ >
176
+ <option value={PartyType.BOTH}>Both (Purchase & Sales)</option>
177
+ <option value={PartyType.AWAAK}>Only Awaak (Purchase)</option>
178
+ <option value={PartyType.JAWAAK}>Only Jawaak (Sales)</option>
179
+ </select>
180
+ <button
181
+ onClick={handleSaveParty}
182
+ className="bg-teal-600 text-white rounded p-2 flex items-center justify-center gap-2 font-medium hover:bg-teal-700"
183
+ >
184
+ <Plus size={18} /> Save Party
185
+ </button>
186
+ </div>
187
+ {partyMessage && (
188
+ <div
189
+ className={`mt-3 text-sm rounded px-3 py-2 ${partyMessage.type === 'error'
190
+ ? 'bg-red-50 text-red-700 border border-red-100'
191
+ : 'bg-green-50 text-green-700 border border-green-100'
192
+ }`}
193
+ >
194
+ {partyMessage.text}
195
+ </div>
196
+ )}
197
+ </div>
198
+
199
+ <div>
200
+ <h3 className="font-semibold text-gray-700 mb-3">All Parties</h3>
201
+ <div className="overflow-x-auto">
202
+ <table className="w-full text-sm text-left">
203
+ <thead className="bg-gray-50 text-gray-500">
204
+ <tr>
205
+ <th className="p-2">Name</th>
206
+ <th className="p-2">Phone</th>
207
+ <th className="p-2">Type</th>
208
+ <th className="p-2 text-center">Action</th>
209
+ </tr>
210
+ </thead>
211
+ <tbody className="divide-y">
212
+ {parties.map(p => (
213
+ <tr key={p.id}>
214
+ <td className="p-2">{p.name}</td>
215
+ <td className="p-2">{p.phone}</td>
216
+ <td className="p-2">{p.party_type}</td>
217
+ <td className="p-2 text-center">
218
+ <button
219
+ onClick={() => setNewParty(p)}
220
+ className="text-blue-600 hover:underline text-xs"
221
+ >
222
+ Edit
223
+ </button>
224
+ </td>
225
+ </tr>
226
+ ))}
227
+ </tbody>
228
+ </table>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ )}
233
+
234
+ {/* MIRCHI TAB */}
235
+ {activeTab === 'mirchi' && (
236
+ <div className="space-y-6">
237
+ <div className="border-b pb-4">
238
+ <h3 className="text-lg font-bold text-gray-800">मिरची प्रकार (Mirchi Types)</h3>
239
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
240
+ <input
241
+ placeholder="Variety Name (e.g. Teja)"
242
+ className="border rounded p-2"
243
+ value={newMirchi.name}
244
+ onChange={e => setNewMirchi({ ...newMirchi, name: e.target.value })}
245
+ />
246
+ <button
247
+ onClick={handleSaveMirchi}
248
+ className="bg-teal-600 text-white rounded p-2 flex items-center justify-center gap-2 font-medium hover:bg-teal-700"
249
+ >
250
+ <Plus size={18} /> Save Type
251
+ </button>
252
+ </div>
253
+ {mirchiMessage && (
254
+ <div
255
+ className={`mt-3 text-sm rounded px-3 py-2 ${mirchiMessage.type === 'error'
256
+ ? 'bg-red-50 text-red-700 border border-red-100'
257
+ : 'bg-green-50 text-green-700 border border-green-100'
258
+ }`}
259
+ >
260
+ {mirchiMessage.text}
261
+ </div>
262
+ )}
263
+ </div>
264
+
265
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
266
+ {mirchiTypes.map(m => (
267
+ <div key={m.id} className="p-4 border rounded-lg hover:shadow-md transition bg-gray-50">
268
+ <div className="flex justify-between items-start mb-2">
269
+ <h4 className="font-bold text-gray-800">{m.name}</h4>
270
+ <button
271
+ onClick={() => setNewMirchi(m)}
272
+ className="text-teal-600 text-xs font-medium"
273
+ >
274
+ Edit
275
+ </button>
276
+ </div>
277
+ <div className="text-xs text-gray-500 mt-1">Mirchi Type</div>
278
+ </div>
279
+ ))}
280
+ </div>
281
+ </div>
282
+ )}
283
+
284
+ {/* General / Alerts Tab */}
285
+ {activeTab === 'general' && (
286
+ <div className="space-y-6">
287
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
288
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">Install as App</h3>
289
+ <p className="text-sm text-gray-600 mb-4">
290
+ Install this application on your device for quick access and offline support.
291
+ </p>
292
+ {isInstallable ? (
293
+ <button
294
+ onClick={handleInstallClick}
295
+ className="px-6 py-3 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-2 font-medium"
296
+ >
297
+ <Download size={20} />
298
+ Install App
299
+ </button>
300
+ ) : (
301
+ <div className="text-sm text-gray-500 bg-gray-50 p-4 rounded-lg border border-gray-200">
302
+ ✅ App is already installed or not available for installation on this device.
303
+ </div>
304
+ )}
305
+ </div>
306
+
307
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
308
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">Alert Configuration</h3>
309
+ <div className="space-y-4">
310
+ <div>
311
+ <label className="block text-sm font-medium text-gray-700 mb-2">Low Stock Threshold (kg)</label>
312
+ <input
313
+ type="number"
314
+ value={config.lowStockThreshold}
315
+ onChange={(e) => setConfig({ ...config, lowStockThreshold: parseFloat(e.target.value) })}
316
+ className="w-full border border-gray-300 rounded-lg p-2"
317
+ />
318
+ </div>
319
+ <div className="flex items-center gap-3">
320
+ <input
321
+ type="checkbox"
322
+ checked={config.enableNotifications}
323
+ onChange={(e) => setConfig({ ...config, enableNotifications: e.target.checked })}
324
+ className="w-4 h-4 text-teal-600 rounded"
325
+ />
326
+ <label className="text-sm text-gray-700">Enable Notifications</label>
327
+ </div>
328
+ <button className="bg-gray-800 text-white px-4 py-2 rounded text-sm hover:bg-gray-900">
329
+ Save Config
330
+ </button>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ )}
335
+ </div>
336
+ </div>
337
+ );
338
+ };
339
+
340
+ export default Settings;
pages/StockReport.tsx ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { getActiveLots, getParties, getTransactions } from '../services/db';
3
+ import { BillType, Lot, Party, Transaction } from '../types';
4
+ import { Package, Search, ArrowLeft } from 'lucide-react';
5
+
6
+ const LOW_STOCK_THRESHOLD = 50; // kg
7
+
8
+ const StockReport = () => {
9
+ const [lots, setLots] = useState<Lot[]>([]);
10
+ const [parties, setParties] = useState<Party[]>([]);
11
+ const [transactions, setTransactions] = useState<Transaction[]>([]);
12
+ const [searchTerm, setSearchTerm] = useState('');
13
+ const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
14
+ const [selectedMirchiId, setSelectedMirchiId] = useState<string | null>(null);
15
+ const [selectedMirchiName, setSelectedMirchiName] = useState<string | null>(null);
16
+
17
+ useEffect(() => {
18
+ const fetch = async () => {
19
+ setLots(await getActiveLots());
20
+ setParties(await getParties());
21
+ setTransactions(await getTransactions());
22
+ };
23
+ fetch();
24
+ }, []);
25
+
26
+ const filteredLots = useMemo(
27
+ () =>
28
+ lots.filter((l) =>
29
+ l.mirchi_name.toLowerCase().includes(searchTerm.toLowerCase())
30
+ ),
31
+ [lots, searchTerm]
32
+ );
33
+
34
+ const aggregatedByMirchi = useMemo(() => {
35
+ const map = new Map<string, { mirchiId: string; mirchiName: string; totalQty: number; remainingQty: number }>();
36
+
37
+ filteredLots.forEach((lot) => {
38
+ const key = lot.mirchi_type_id;
39
+ const existing = map.get(key) || {
40
+ mirchiId: lot.mirchi_type_id,
41
+ mirchiName: lot.mirchi_name,
42
+ totalQty: 0,
43
+ remainingQty: 0,
44
+ };
45
+ existing.totalQty += lot.total_quantity;
46
+ existing.remainingQty += lot.remaining_quantity;
47
+ map.set(key, existing);
48
+ });
49
+
50
+ return Array.from(map.values());
51
+ }, [filteredLots]);
52
+
53
+ const detailMovements = useMemo(() => {
54
+ if (!selectedMirchiId) return [];
55
+
56
+ const rows: {
57
+ id: string;
58
+ date: string;
59
+ billNo: string;
60
+ partyName: string;
61
+ inQty: number;
62
+ outQty: number;
63
+ typeLabel: string;
64
+ isReturn: boolean;
65
+ }[] = [];
66
+
67
+ transactions.forEach((tx) => {
68
+ tx.items
69
+ .filter((item) => item.mirchi_type_id === selectedMirchiId)
70
+ .forEach((item) => {
71
+ const party = parties.find((p) => p.id === tx.party_id);
72
+ let inQty = 0;
73
+ let outQty = 0;
74
+ let typeLabel = '';
75
+
76
+ if (tx.bill_type === BillType.JAWAAK) {
77
+ // Jawaak = Purchase / Stock IN
78
+ if (tx.is_return) {
79
+ // Purchase Return: Stock OUT
80
+ outQty = item.net_weight;
81
+ typeLabel = 'Purchase Return';
82
+ } else {
83
+ inQty = item.net_weight;
84
+ typeLabel = 'Purchase';
85
+ }
86
+ } else {
87
+ // Awaak = Sales / Stock OUT
88
+ if (tx.is_return) {
89
+ // Sales Return: Stock IN
90
+ inQty = item.net_weight;
91
+ typeLabel = 'Sales Return';
92
+ } else {
93
+ outQty = item.net_weight;
94
+ typeLabel = 'Sale';
95
+ }
96
+ }
97
+
98
+ rows.push({
99
+ id: `${tx.id}-${item.id}`,
100
+ date: tx.bill_date,
101
+ billNo: tx.bill_number,
102
+ partyName: party?.name || tx.party_name || 'Unknown Party',
103
+ inQty,
104
+ outQty,
105
+ typeLabel,
106
+ isReturn: tx.is_return,
107
+ });
108
+ });
109
+ });
110
+
111
+ // Sort latest first
112
+ rows.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
113
+ return rows;
114
+ }, [selectedMirchiName, transactions, parties]);
115
+
116
+ return (
117
+ <div className="space-y-4">
118
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
119
+ <div className="p-4 md:p-6 border-b flex flex-col md:flex-row md:items-center justify-between gap-4">
120
+ <div className="flex items-center gap-2">
121
+ {viewMode === 'detail' && (
122
+ <button
123
+ onClick={() => {
124
+ setViewMode('list');
125
+ setSelectedMirchiId(null);
126
+ setSelectedMirchiName(null);
127
+ }}
128
+ className="mr-1 p-1 hover:bg-gray-100 rounded-full"
129
+ >
130
+ <ArrowLeft size={20} className="text-gray-600" />
131
+ </button>
132
+ )}
133
+ <Package className="text-teal-600" />
134
+ <h2 className="text-lg md:text-xl font-bold text-gray-800">
135
+ {viewMode === 'list'
136
+ ? 'स्टॉक रिपोर्ट (Stock Inventory)'
137
+ : `${selectedMirchiName ?? ''} - Stock Detail`}
138
+ </h2>
139
+ </div>
140
+
141
+ {viewMode === 'list' && (
142
+ <div className="relative w-full md:w-auto">
143
+ <Search
144
+ className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
145
+ size={18}
146
+ />
147
+ <input
148
+ type="text"
149
+ placeholder="Search Mirchi Jaat / Type..."
150
+ className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 outline-none w-full md:w-64 text-sm"
151
+ value={searchTerm}
152
+ onChange={(e) => setSearchTerm(e.target.value)}
153
+ />
154
+ </div>
155
+ )}
156
+ </div>
157
+
158
+ {viewMode === 'list' ? (
159
+ <>
160
+ {/* Desktop table */}
161
+ <div className="hidden md:block overflow-x-auto">
162
+ <table className="w-full text-sm text-left">
163
+ <thead className="bg-gray-50 text-gray-500 font-medium">
164
+ <tr>
165
+ <th className="px-6 py-4">मिरची जात (Mirchi Type)</th>
166
+ <th className="px-6 py-4 text-right">एकूण क्वांटिटी (Total Qty)</th>
167
+ <th className="px-6 py-4 text-right">शिल्लक (Remaining)</th>
168
+ <th className="px-6 py-4 text-center">स्थिती (Status)</th>
169
+ </tr>
170
+ </thead>
171
+ <tbody className="divide-y divide-gray-100">
172
+ {aggregatedByMirchi.length === 0 ? (
173
+ <tr>
174
+ <td
175
+ colSpan={4}
176
+ className="text-center py-8 text-gray-500"
177
+ >
178
+ No active stock found
179
+ </td>
180
+ </tr>
181
+ ) : (
182
+ aggregatedByMirchi.map((row) => {
183
+ const isLow = row.remainingQty < LOW_STOCK_THRESHOLD;
184
+ const statusLabel =
185
+ row.remainingQty === 0
186
+ ? 'Out of Stock'
187
+ : isLow
188
+ ? 'Low Stock'
189
+ : 'Available';
190
+ const statusClasses =
191
+ row.remainingQty === 0
192
+ ? 'bg-red-100 text-red-700'
193
+ : isLow
194
+ ? 'bg-orange-100 text-orange-700'
195
+ : 'bg-green-100 text-green-700';
196
+
197
+ return (
198
+ <tr
199
+ key={row.mirchiId}
200
+ className="hover:bg-gray-50 transition-colors cursor-pointer"
201
+ onClick={() => {
202
+ setSelectedMirchiId(row.mirchiId);
203
+ setSelectedMirchiName(row.mirchiName);
204
+ setViewMode('detail');
205
+ }}
206
+ >
207
+ <td className="px-6 py-4 font-medium text-teal-700">
208
+ {row.mirchiName}
209
+ </td>
210
+ <td className="px-6 py-4 text-right">
211
+ {row.totalQty} kg
212
+ </td>
213
+ <td className="px-6 py-4 text-right">
214
+ <span
215
+ className={`font-bold ${
216
+ isLow ? 'text-red-500' : 'text-gray-800'
217
+ }`}
218
+ >
219
+ {row.remainingQty} kg
220
+ </span>
221
+ </td>
222
+ <td className="px-6 py-4 text-center">
223
+ <span
224
+ className={`inline-block px-2 py-1 rounded-full text-xs font-semibold ${statusClasses}`}
225
+ >
226
+ {statusLabel}
227
+ </span>
228
+ </td>
229
+ </tr>
230
+ );
231
+ })
232
+ )}
233
+ </tbody>
234
+ </table>
235
+ </div>
236
+
237
+ {/* Mobile cards */}
238
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
239
+ {aggregatedByMirchi.length === 0 ? (
240
+ <div className="p-6 text-center text-gray-500 text-sm">
241
+ No active stock found
242
+ </div>
243
+ ) : (
244
+ aggregatedByMirchi.map((row) => {
245
+ const isLow = row.remainingQty < LOW_STOCK_THRESHOLD;
246
+ const statusLabel =
247
+ row.remainingQty === 0
248
+ ? 'Out of Stock'
249
+ : isLow
250
+ ? 'Low Stock'
251
+ : 'Available';
252
+ const statusClasses =
253
+ row.remainingQty === 0
254
+ ? 'bg-red-100 text-red-700'
255
+ : isLow
256
+ ? 'bg-orange-100 text-orange-700'
257
+ : 'bg-green-100 text-green-700';
258
+
259
+ return (
260
+ <button
261
+ key={row.mirchiName}
262
+ className="text-left p-4 space-y-2 hover:bg-gray-50 transition-colors"
263
+ onClick={() => {
264
+ setSelectedMirchiId(row.mirchiId);
265
+ setSelectedMirchiName(row.mirchiName);
266
+ setViewMode('detail');
267
+ }}
268
+ >
269
+ <div className="flex items-center justify-between gap-2">
270
+ <div>
271
+ <div className="text-sm font-semibold text-gray-900">
272
+ {row.mirchiName}
273
+ </div>
274
+ <div className="text-xs text-gray-500">
275
+ Mirchi Jaat / Type
276
+ </div>
277
+ </div>
278
+ <span
279
+ className={`px-2 py-1 rounded-full text-[10px] font-semibold ${statusClasses}`}
280
+ >
281
+ {statusLabel}
282
+ </span>
283
+ </div>
284
+ <div className="grid grid-cols-2 gap-2 text-xs mt-1">
285
+ <div className="bg-gray-50 p-2 rounded">
286
+ <div className="text-gray-500">Total Qty</div>
287
+ <div className="font-medium">{row.totalQty} kg</div>
288
+ </div>
289
+ <div className="bg-gray-50 p-2 rounded">
290
+ <div className="text-gray-500">Remaining</div>
291
+ <div
292
+ className={`font-bold ${
293
+ isLow ? 'text-red-500' : 'text-gray-800'
294
+ }`}
295
+ >
296
+ {row.remainingQty} kg
297
+ </div>
298
+ </div>
299
+ </div>
300
+ </button>
301
+ );
302
+ })
303
+ )}
304
+ </div>
305
+ </>
306
+ ) : (
307
+ <>
308
+ {/* Desktop detail table */}
309
+ <div className="hidden md:block overflow-x-auto">
310
+ <table className="w-full text-sm text-left">
311
+ <thead className="bg-gray-50 text-gray-500 font-medium">
312
+ <tr>
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>
319
+ </tr>
320
+ </thead>
321
+ <tbody className="divide-y divide-gray-100">
322
+ {detailMovements.length === 0 ? (
323
+ <tr>
324
+ <td colSpan={6} className="px-6 py-8 text-center text-gray-500">
325
+ No movements found for this Mirchi type.
326
+ </td>
327
+ </tr>
328
+ ) : (
329
+ detailMovements.map((row) => (
330
+ <tr key={row.id} className="hover:bg-gray-50">
331
+ <td className="px-6 py-4 text-gray-600">{row.date}</td>
332
+ <td className="px-6 py-4 font-mono text-gray-500">{row.billNo}</td>
333
+ <td className="px-6 py-4 text-gray-800">{row.partyName}</td>
334
+ <td className="px-6 py-4 text-right text-green-700 font-medium">
335
+ {row.inQty > 0 ? `${row.inQty} kg` : '-'}
336
+ </td>
337
+ <td className="px-6 py-4 text-right text-red-600 font-medium">
338
+ {row.outQty > 0 ? `${row.outQty} kg` : '-'}
339
+ </td>
340
+ <td className="px-6 py-4 text-center">
341
+ <span
342
+ className={`inline-block px-2 py-1 rounded-full text-xs font-semibold ${
343
+ row.typeLabel.includes('Return')
344
+ ? 'bg-orange-100 text-orange-700'
345
+ : row.typeLabel === 'Purchase'
346
+ ? 'bg-teal-100 text-teal-700'
347
+ : 'bg-blue-100 text-blue-700'
348
+ }`}
349
+ >
350
+ {row.typeLabel}
351
+ </span>
352
+ </td>
353
+ </tr>
354
+ ))
355
+ )}
356
+ </tbody>
357
+ </table>
358
+ </div>
359
+
360
+ {/* Mobile detail cards */}
361
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
362
+ {detailMovements.length === 0 ? (
363
+ <div className="p-6 text-center text-gray-500 text-sm">
364
+ No movements found for this Mirchi type.
365
+ </div>
366
+ ) : (
367
+ detailMovements.map((row) => (
368
+ <div key={row.id} className="p-4 space-y-2">
369
+ <div className="flex justify-between items-start gap-2">
370
+ <div>
371
+ <div className="text-xs text-gray-500">{row.date}</div>
372
+ <div className="font-mono text-sm font-medium text-gray-800">
373
+ {row.billNo}
374
+ </div>
375
+ <div className="text-xs text-gray-600 mt-1">
376
+ {row.partyName}
377
+ </div>
378
+ </div>
379
+ <span
380
+ className={`px-2 py-1 rounded-full text-[10px] font-semibold ${
381
+ row.typeLabel.includes('Return')
382
+ ? 'bg-orange-100 text-orange-700'
383
+ : row.typeLabel === 'Purchase'
384
+ ? 'bg-teal-100 text-teal-700'
385
+ : 'bg-blue-100 text-blue-700'
386
+ }`}
387
+ >
388
+ {row.typeLabel}
389
+ </span>
390
+ </div>
391
+ <div className="grid grid-cols-2 gap-2 text-xs pt-2 border-t border-gray-50">
392
+ <div className="bg-gray-50 p-2 rounded">
393
+ <div className="text-gray-500">माल इन (In)</div>
394
+ <div className="font-medium text-green-700">
395
+ {row.inQty > 0 ? `${row.inQty} kg` : '-'}
396
+ </div>
397
+ </div>
398
+ <div className="bg-gray-50 p-2 rounded">
399
+ <div className="text-gray-500">माल आउट (Out)</div>
400
+ <div className="font-medium text-red-600">
401
+ {row.outQty > 0 ? `${row.outQty} kg` : '-'}
402
+ </div>
403
+ </div>
404
+ </div>
405
+ </div>
406
+ ))
407
+ )}
408
+ </div>
409
+ </>
410
+ )}
411
+ </div>
412
+ </div>
413
+ );
414
+ };
415
+
416
+ export default StockReport;
public/icon-192.png ADDED

Git LFS Details

  • SHA256: 752add4a62055788955c12d11b499e3f8c087cc112635b9c1a25fc8b8e97f49b
  • Pointer size: 131 Bytes
  • Size of remote file: 388 kB
public/icon-512.png ADDED

Git LFS Details

  • SHA256: df87a32301726f75e84cf90101da2089eae3509a50f0a508f7782dd3b513c3c7
  • Pointer size: 131 Bytes
  • Size of remote file: 360 kB
public/manifest.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Pattanshetty Inventory",
3
+ "short_name": "Pattanshetty",
4
+ "description": "Inventory Management System for Mirchi Trading",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#f9fafb",
8
+ "theme_color": "#0d9488",
9
+ "orientation": "portrait-primary",
10
+ "icons": [
11
+ {
12
+ "src": "/icon-192.png",
13
+ "sizes": "192x192",
14
+ "type": "image/png",
15
+ "purpose": "any maskable"
16
+ },
17
+ {
18
+ "src": "/icon-512.png",
19
+ "sizes": "512x512",
20
+ "type": "image/png",
21
+ "purpose": "any maskable"
22
+ }
23
+ ],
24
+ "categories": [
25
+ "business",
26
+ "productivity"
27
+ ],
28
+ "screenshots": [],
29
+ "shortcuts": [
30
+ {
31
+ "name": "New Purchase Bill",
32
+ "short_name": "Purchase",
33
+ "description": "Create new purchase bill",
34
+ "url": "/awaak",
35
+ "icons": []
36
+ },
37
+ {
38
+ "name": "New Sales Bill",
39
+ "short_name": "Sales",
40
+ "description": "Create new sales bill",
41
+ "url": "/jawaak",
42
+ "icons": []
43
+ },
44
+ {
45
+ "name": "Party Ledger",
46
+ "short_name": "Ledger",
47
+ "description": "View party ledger",
48
+ "url": "/ledger",
49
+ "icons": []
50
+ }
51
+ ]
52
+ }
public/service-worker.js ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const CACHE_NAME = 'pattanshetty-v1';
2
+ const urlsToCache = [
3
+ '/',
4
+ '/index.html',
5
+ '/src/main.tsx',
6
+ '/src/App.tsx',
7
+ ];
8
+
9
+ // Install event - cache resources
10
+ self.addEventListener('install', (event) => {
11
+ event.waitUntil(
12
+ caches.open(CACHE_NAME)
13
+ .then((cache) => {
14
+ console.log('Opened cache');
15
+ return cache.addAll(urlsToCache);
16
+ })
17
+ );
18
+ self.skipWaiting();
19
+ });
20
+
21
+ // Activate event - clean up old caches
22
+ self.addEventListener('activate', (event) => {
23
+ event.waitUntil(
24
+ caches.keys().then((cacheNames) => {
25
+ return Promise.all(
26
+ cacheNames.map((cacheName) => {
27
+ if (cacheName !== CACHE_NAME) {
28
+ console.log('Deleting old cache:', cacheName);
29
+ return caches.delete(cacheName);
30
+ }
31
+ })
32
+ );
33
+ })
34
+ );
35
+ self.clients.claim();
36
+ });
37
+
38
+ // Fetch event - serve from cache, fallback to network
39
+ self.addEventListener('fetch', (event) => {
40
+ event.respondWith(
41
+ caches.match(event.request)
42
+ .then((response) => {
43
+ // Cache hit - return response
44
+ if (response) {
45
+ return response;
46
+ }
47
+
48
+ return fetch(event.request).then(
49
+ (response) => {
50
+ // Check if valid response
51
+ if (!response || response.status !== 200 || response.type !== 'basic') {
52
+ return response;
53
+ }
54
+
55
+ // Clone the response
56
+ const responseToCache = response.clone();
57
+
58
+ caches.open(CACHE_NAME)
59
+ .then((cache) => {
60
+ cache.put(event.request, responseToCache);
61
+ });
62
+
63
+ return response;
64
+ }
65
+ );
66
+ })
67
+ );
68
+ });
services/db.ts ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Party, MirchiType, Lot, Transaction, TransactionItem, PartyType,
3
+ LotStatus, BillType, ApiResponse, PaymentMode
4
+ } from '../types';
5
+
6
+ const API_BASE = import.meta.env.VITE_API_URL || 'https://antaram-pattanshettybackend.hf.space/api';
7
+
8
+ // Helper function to parse numeric values from API
9
+ const parseNumeric = (value: any): number => {
10
+ if (typeof value === 'string') {
11
+ return parseFloat(value);
12
+ }
13
+ return value || 0;
14
+ };
15
+
16
+ // Helper function to parse poti_weights from string to array
17
+ const parsePotiWeights = (weights: any): number[] => {
18
+ if (typeof weights === 'string') {
19
+ try {
20
+ return JSON.parse(weights);
21
+ } catch {
22
+ return [];
23
+ }
24
+ }
25
+ return weights || [];
26
+ };
27
+
28
+ // Helper function to normalize transaction data from API
29
+ const normalizeTransaction = (tx: any): Transaction => {
30
+ // Filter out null items that come from PostgreSQL json_agg when there are no items
31
+ const rawItems = tx.items || [];
32
+ const validItems = Array.isArray(rawItems)
33
+ ? rawItems.filter((item: any) => item && item.id !== null)
34
+ : [];
35
+
36
+ return {
37
+ ...tx,
38
+ gross_weight_total: parseNumeric(tx.gross_weight_total),
39
+ net_weight_total: parseNumeric(tx.net_weight_total),
40
+ subtotal: parseNumeric(tx.subtotal),
41
+ total_expenses: parseNumeric(tx.total_expenses),
42
+ total_amount: parseNumeric(tx.total_amount),
43
+ paid_amount: parseNumeric(tx.paid_amount),
44
+ balance_amount: parseNumeric(tx.balance_amount),
45
+ items: validItems.map((item: any) => ({
46
+ ...item,
47
+ poti_weights: parsePotiWeights(item.poti_weights),
48
+ gross_weight: parseNumeric(item.gross_weight),
49
+ poti_count: parseNumeric(item.poti_count),
50
+ total_potya: parseNumeric(item.total_potya),
51
+ net_weight: parseNumeric(item.net_weight),
52
+ rate_per_kg: parseNumeric(item.rate_per_kg),
53
+ item_total: parseNumeric(item.item_total),
54
+ })),
55
+ expenses: tx.expenses ? {
56
+ cess_percent: parseNumeric(tx.expenses.cess_percent),
57
+ cess_amount: parseNumeric(tx.expenses.cess_amount),
58
+ adat_percent: parseNumeric(tx.expenses.adat_percent),
59
+ adat_amount: parseNumeric(tx.expenses.adat_amount),
60
+ poti_rate: parseNumeric(tx.expenses.poti_rate),
61
+ poti_amount: parseNumeric(tx.expenses.poti_amount),
62
+ hamali_per_poti: parseNumeric(tx.expenses.hamali_per_poti),
63
+ hamali_amount: parseNumeric(tx.expenses.hamali_amount),
64
+ packaging_hamali_per_poti: parseNumeric(tx.expenses.packaging_hamali_per_poti),
65
+ packaging_hamali_amount: parseNumeric(tx.expenses.packaging_hamali_amount),
66
+ gaadi_bharni: parseNumeric(tx.expenses.gaadi_bharni),
67
+ } : {
68
+ cess_percent: 0,
69
+ cess_amount: 0,
70
+ adat_percent: 0,
71
+ adat_amount: 0,
72
+ poti_rate: 0,
73
+ poti_amount: 0,
74
+ hamali_per_poti: 0,
75
+ hamali_amount: 0,
76
+ packaging_hamali_per_poti: 0,
77
+ packaging_hamali_amount: 0,
78
+ gaadi_bharni: 0,
79
+ },
80
+ payments: tx.payments || [],
81
+ };
82
+ };
83
+
84
+ // Parties API
85
+ export const getParties = async (): Promise<Party[]> => {
86
+ try {
87
+ const res = await fetch(`${API_BASE}/parties`);
88
+ if (!res.ok) throw new Error('Failed to load parties');
89
+ const data = await res.json();
90
+ return data.map((p: any) => ({
91
+ ...p,
92
+ current_balance: parseNumeric(p.current_balance)
93
+ }));
94
+ } catch (error) {
95
+ console.error('Error fetching parties:', error);
96
+ return [];
97
+ }
98
+ };
99
+
100
+ export const saveParty = async (party: Party): Promise<ApiResponse<Party>> => {
101
+ try {
102
+ const res = await fetch(`${API_BASE}/parties`, {
103
+ method: 'POST',
104
+ headers: { 'Content-Type': 'application/json' },
105
+ body: JSON.stringify(party),
106
+ });
107
+
108
+ const data = await res.json();
109
+ if (!res.ok || !data.success) {
110
+ return { success: false, message: data?.message || 'Error saving party' };
111
+ }
112
+
113
+ return {
114
+ success: true,
115
+ data: {
116
+ ...data.data,
117
+ current_balance: parseNumeric(data.data.current_balance)
118
+ },
119
+ message: data.message || 'Party saved successfully'
120
+ };
121
+ } catch (e: any) {
122
+ return { success: false, message: e.message || 'Database error' };
123
+ }
124
+ };
125
+
126
+ // Mirchi Types API
127
+ export const apiGetMirchiTypes = async (): Promise<MirchiType[]> => {
128
+ try {
129
+ const res = await fetch(`${API_BASE}/mirchi-types`);
130
+ if (!res.ok) throw new Error('Failed to load mirchi types');
131
+ const data = await res.json();
132
+ return data.map((m: any) => ({
133
+ ...m,
134
+ current_rate: parseNumeric(m.current_rate)
135
+ }));
136
+ } catch (error) {
137
+ console.error('Error fetching mirchi types:', error);
138
+ return [];
139
+ }
140
+ };
141
+
142
+ export const apiSaveMirchiType = async (type: MirchiType): Promise<ApiResponse<MirchiType>> => {
143
+ try {
144
+ const res = await fetch(`${API_BASE}/mirchi-types`, {
145
+ method: 'POST',
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: JSON.stringify(type),
148
+ });
149
+ const data = await res.json();
150
+ if (!res.ok || !data.success) {
151
+ return { success: false, message: data?.message || 'Error saving type' };
152
+ }
153
+ return {
154
+ success: true,
155
+ data: {
156
+ ...data.data,
157
+ current_rate: parseNumeric(data.data.current_rate)
158
+ },
159
+ message: data.message
160
+ };
161
+ } catch (e: any) {
162
+ return { success: false, message: e.message || 'Error saving type' };
163
+ }
164
+ };
165
+
166
+ // Alias for compatibility
167
+ export const getMirchiTypes = apiGetMirchiTypes;
168
+ export const saveMirchiType = apiSaveMirchiType;
169
+
170
+ // Lots API
171
+ export const getActiveLots = async (): Promise<Lot[]> => {
172
+ try {
173
+ const res = await fetch(`${API_BASE}/lots/active`);
174
+ if (!res.ok) throw new Error('Failed to load active lots');
175
+ const data = await res.json();
176
+ return data.map((l: any) => ({
177
+ ...l,
178
+ total_quantity: parseNumeric(l.total_quantity),
179
+ remaining_quantity: parseNumeric(l.remaining_quantity),
180
+ avg_rate: parseNumeric(l.avg_rate)
181
+ }));
182
+ } catch (error) {
183
+ console.error('Error fetching active lots:', error);
184
+ return [];
185
+ }
186
+ };
187
+
188
+ // Transactions API
189
+ export const getTransactions = async (): Promise<Transaction[]> => {
190
+ try {
191
+ const res = await fetch(`${API_BASE}/transactions`);
192
+ if (!res.ok) throw new Error('Failed to load transactions');
193
+ const data = await res.json();
194
+ return data.map(normalizeTransaction);
195
+ } catch (error) {
196
+ console.error('Error fetching transactions:', error);
197
+ return [];
198
+ }
199
+ };
200
+
201
+ export const generateBillNumber = (type: BillType, isReturn: boolean = false): string => {
202
+ const prefix = type === BillType.JAWAAK ? (isReturn ? 'JAWAAK-RET' : 'JAWAAK') : (isReturn ? 'AWAAK-RET' : 'AWAAK');
203
+ const timestamp = Date.now();
204
+ return `${prefix}-${new Date().getFullYear()}-${String(timestamp).slice(-4)}`;
205
+ };
206
+
207
+ export const saveTransaction = async (transaction: Transaction): Promise<ApiResponse<Transaction>> => {
208
+ try {
209
+ // Validate Payload
210
+ if (!transaction.party_id) return { success: false, message: "Party is required" };
211
+ if (!transaction.items || transaction.items.length === 0) return { success: false, message: "At least one item is required" };
212
+
213
+ console.log('=== SAVING TRANSACTION ===');
214
+ console.log('Transaction ID:', transaction.id);
215
+ console.log('Items count:', transaction.items?.length);
216
+ console.log('Items data:', JSON.stringify(transaction.items, null, 2));
217
+
218
+ const res = await fetch(`${API_BASE}/transactions`, {
219
+ method: 'POST',
220
+ headers: { 'Content-Type': 'application/json' },
221
+ body: JSON.stringify(transaction)
222
+ });
223
+
224
+ if (!res.ok) {
225
+ const error = await res.json();
226
+ console.error('Save transaction error:', error);
227
+ throw new Error(error.message || 'Failed to save transaction');
228
+ }
229
+
230
+ const result = await res.json();
231
+ console.log('=== SAVE RESPONSE ===');
232
+ console.log('Response data:', result.data);
233
+ console.log('Response items count:', result.data?.items?.length);
234
+ console.log('Response items:', JSON.stringify(result.data?.items, null, 2));
235
+
236
+ return {
237
+ success: true,
238
+ data: normalizeTransaction(result.data),
239
+ message: result.message
240
+ };
241
+ } catch (error: any) {
242
+ console.error('Error saving transaction:', error);
243
+ return {
244
+ success: false,
245
+ message: error.message || 'Failed to save transaction'
246
+ };
247
+ }
248
+ };
249
+
250
+ export const updateTransactionPayment = async (transactionId: string, amount: number): Promise<ApiResponse<Transaction>> => {
251
+ try {
252
+ // Validate amount
253
+ if (amount <= 0) return { success: false, message: "Amount must be greater than 0" };
254
+
255
+ const res = await fetch(`${API_BASE}/transactions/${transactionId}/payment`, {
256
+ method: 'PATCH',
257
+ headers: { 'Content-Type': 'application/json' },
258
+ body: JSON.stringify({ amount }),
259
+ });
260
+
261
+ const data = await res.json();
262
+ if (!res.ok || !data.success) {
263
+ return { success: false, message: data?.message || 'Error updating payment' };
264
+ }
265
+
266
+ return {
267
+ success: true,
268
+ message: data.message || 'Payment updated successfully'
269
+ };
270
+ } catch (e: any) {
271
+ console.error('Error updating payment:', e);
272
+ return { success: false, message: e.message || 'Error updating payment' };
273
+ }
274
+ };
tsconfig.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "experimentalDecorators": true,
5
+ "useDefineForClassFields": false,
6
+ "module": "ESNext",
7
+ "lib": [
8
+ "ES2022",
9
+ "DOM",
10
+ "DOM.Iterable"
11
+ ],
12
+ "skipLibCheck": true,
13
+ "types": [
14
+ "node"
15
+ ],
16
+ "moduleResolution": "bundler",
17
+ "isolatedModules": true,
18
+ "moduleDetection": "force",
19
+ "allowJs": true,
20
+ "jsx": "react-jsx",
21
+ "paths": {
22
+ "@/*": [
23
+ "./*"
24
+ ]
25
+ },
26
+ "allowImportingTsExtensions": true,
27
+ "noEmit": true
28
+ }
29
+ }
types.ts ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Enums
2
+ // NOTE:
3
+ // - AWAAK = Purchase (Incoming stock)
4
+ // - JAWAAK = Sale (Outgoing stock)
5
+ export enum BillType {
6
+ JAWAAK = 'jawaak', // Sale / Outgoing
7
+ AWAAK = 'awaak' // Purchase / Incoming
8
+ }
9
+
10
+ export enum PartyType {
11
+ JAWAAK = 'jawaak',
12
+ AWAAK = 'awaak',
13
+ BOTH = 'both'
14
+ }
15
+
16
+ export enum PaymentMode {
17
+ CASH = 'cash',
18
+ ONLINE = 'online',
19
+ CHEQUE = 'cheque',
20
+ DUE = 'due'
21
+ }
22
+
23
+ export enum LotStatus {
24
+ ACTIVE = 'active',
25
+ SOLD_OUT = 'sold_out'
26
+ }
27
+
28
+ // API Response Wrapper (Backend Ready)
29
+ export interface ApiResponse<T> {
30
+ success: boolean;
31
+ data?: T;
32
+ message?: string;
33
+ errors?: string[];
34
+ }
35
+
36
+ // Interfaces
37
+ export interface Party {
38
+ id: string;
39
+ name: string;
40
+ phone: string;
41
+ city: string;
42
+ party_type: PartyType;
43
+ current_balance: number; // +ve = They owe us, -ve = We owe them
44
+ }
45
+
46
+ export interface MirchiType {
47
+ id: string;
48
+ name: string;
49
+ current_rate: number;
50
+ }
51
+
52
+ export interface Lot {
53
+ id: string;
54
+ lot_number: string;
55
+ mirchi_type_id: string;
56
+ mirchi_name: string; // Denormalized for display
57
+ total_quantity: number;
58
+ remaining_quantity: number;
59
+ purchase_date: string;
60
+ status: LotStatus;
61
+ avg_rate: number;
62
+ }
63
+
64
+ export interface Payment {
65
+ mode: PaymentMode;
66
+ amount: number;
67
+ reference?: string;
68
+ }
69
+
70
+ export interface Expenses {
71
+ cess_percent: number;
72
+ cess_amount: number;
73
+ adat_percent: number;
74
+ adat_amount: number;
75
+ poti_rate: number;
76
+ poti_amount: number;
77
+ hamali_per_poti: number;
78
+ hamali_amount: number;
79
+ packaging_hamali_per_poti: number;
80
+ packaging_hamali_amount: number;
81
+ gaadi_bharni: number;
82
+ }
83
+
84
+ export interface TransactionItem {
85
+ id: string;
86
+ mirchi_type_id: string;
87
+ mirchi_name?: string;
88
+ quality: string;
89
+ lot_id?: string; // Optional for Jawaak (created auto), Required for Awaak
90
+ poti_weights: number[]; // Array of weights [10, 20, 30]
91
+ gross_weight: number;
92
+ poti_count: number;
93
+ total_potya: number; // Deduction
94
+ net_weight: number;
95
+ rate_per_kg: number;
96
+ item_total: number;
97
+ }
98
+
99
+ export interface Transaction {
100
+ id: string;
101
+ bill_number: string;
102
+ bill_date: string;
103
+ bill_type: BillType;
104
+ is_return: boolean; // True for Purchase Return or Sales Return
105
+ party_id: string;
106
+ party_name?: string;
107
+ items: TransactionItem[];
108
+ expenses: Expenses;
109
+ payments: Payment[];
110
+
111
+ // Totals
112
+ gross_weight_total: number;
113
+ net_weight_total: number;
114
+ subtotal: number;
115
+ total_expenses: number;
116
+ total_amount: number;
117
+ paid_amount: number;
118
+ balance_amount: number;
119
+
120
+ created_at?: string;
121
+ updated_at?: string;
122
+ }
utils/exportToExcel.ts ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as XLSX from 'xlsx';
2
+ import { Transaction, Party } from '../types';
3
+
4
+ /**
5
+ * Export party ledger to Excel
6
+ */
7
+ export const exportPartyLedger = (
8
+ party: Party,
9
+ transactions: Transaction[],
10
+ dateRange?: { from: string; to: string }
11
+ ) => {
12
+ // Filter transactions by date range if provided
13
+ const filteredTransactions = dateRange
14
+ ? transactions.filter(t => {
15
+ const txDate = new Date(t.bill_date);
16
+ return txDate >= new Date(dateRange.from) && txDate <= new Date(dateRange.to);
17
+ })
18
+ : transactions;
19
+
20
+ // Prepare data for Excel
21
+ const data = filteredTransactions.map(tx => ({
22
+ 'Date': new Date(tx.bill_date).toLocaleDateString('en-IN'),
23
+ 'Bill Number': tx.bill_number,
24
+ 'Type': tx.bill_type.toUpperCase(),
25
+ 'Return': tx.is_return ? 'Yes' : 'No',
26
+ 'Items': tx.items.map(i => i.mirchi_name).join(', '),
27
+ 'Gross Weight (kg)': tx.gross_weight_total.toFixed(2),
28
+ 'Net Weight (kg)': tx.net_weight_total.toFixed(2),
29
+ 'Subtotal (₹)': tx.subtotal.toFixed(2),
30
+ 'Expenses (₹)': tx.total_expenses.toFixed(2),
31
+ 'Total Amount (₹)': tx.total_amount.toFixed(2),
32
+ 'Paid (₹)': tx.paid_amount.toFixed(2),
33
+ 'Balance (₹)': tx.balance_amount.toFixed(2),
34
+ }));
35
+
36
+ // Add summary row
37
+ const totalAmount = filteredTransactions.reduce((sum, tx) => sum + tx.total_amount, 0);
38
+ const totalPaid = filteredTransactions.reduce((sum, tx) => sum + tx.paid_amount, 0);
39
+ const totalBalance = filteredTransactions.reduce((sum, tx) => sum + tx.balance_amount, 0);
40
+
41
+ data.push({
42
+ 'Date': '',
43
+ 'Bill Number': '',
44
+ 'Type': '',
45
+ 'Return': '',
46
+ 'Items': 'TOTAL',
47
+ 'Gross Weight (kg)': '',
48
+ 'Net Weight (kg)': '',
49
+ 'Subtotal (₹)': '',
50
+ 'Expenses (₹)': '',
51
+ 'Total Amount (₹)': totalAmount.toFixed(2),
52
+ 'Paid (₹)': totalPaid.toFixed(2),
53
+ 'Balance (₹)': totalBalance.toFixed(2),
54
+ } as any);
55
+
56
+ // Create worksheet
57
+ const ws = XLSX.utils.json_to_sheet(data);
58
+
59
+ // Set column widths
60
+ ws['!cols'] = [
61
+ { wch: 12 }, // Date
62
+ { wch: 20 }, // Bill Number
63
+ { wch: 10 }, // Type
64
+ { wch: 8 }, // Return
65
+ { wch: 30 }, // Items
66
+ { wch: 15 }, // Gross Weight
67
+ { wch: 15 }, // Net Weight
68
+ { wch: 15 }, // Subtotal
69
+ { wch: 15 }, // Expenses
70
+ { wch: 15 }, // Total Amount
71
+ { wch: 15 }, // Paid
72
+ { wch: 15 }, // Balance
73
+ ];
74
+
75
+ // Create workbook
76
+ const wb = XLSX.utils.book_new();
77
+ XLSX.utils.book_append_sheet(wb, ws, 'Ledger');
78
+
79
+ // Add party info sheet
80
+ const partyInfo = [
81
+ { Field: 'Party Name', Value: party.name },
82
+ { Field: 'Phone', Value: party.phone || '-' },
83
+ { Field: 'City', Value: party.city || '-' },
84
+ { Field: 'Type', Value: party.party_type },
85
+ { Field: 'Current Balance', Value: `₹${party.current_balance.toFixed(2)}` },
86
+ { Field: '', Value: '' },
87
+ { Field: 'Export Date', Value: new Date().toLocaleString('en-IN') },
88
+ { Field: 'Total Transactions', Value: filteredTransactions.length.toString() },
89
+ ];
90
+ const wsInfo = XLSX.utils.json_to_sheet(partyInfo);
91
+ XLSX.utils.book_append_sheet(wb, wsInfo, 'Party Info');
92
+
93
+ // Generate filename
94
+ const filename = `${party.name.replace(/\s+/g, '_')}_Ledger_${new Date().toISOString().split('T')[0]}.xlsx`;
95
+
96
+ // Download file
97
+ XLSX.writeFile(wb, filename);
98
+ };
99
+
100
+ /**
101
+ * Export all parties to Excel
102
+ */
103
+ export const exportAllParties = (parties: Party[]) => {
104
+ const data = parties.map(p => ({
105
+ 'Party Name': p.name,
106
+ 'Phone': p.phone || '-',
107
+ 'City': p.city || '-',
108
+ 'Type': p.party_type.toUpperCase(),
109
+ 'Current Balance (₹)': p.current_balance.toFixed(2),
110
+ 'Status': p.current_balance > 0 ? 'Receivable' : p.current_balance < 0 ? 'Payable' : 'Settled',
111
+ }));
112
+
113
+ // Add summary
114
+ const totalReceivable = parties.filter(p => p.current_balance > 0).reduce((sum, p) => sum + p.current_balance, 0);
115
+ const totalPayable = parties.filter(p => p.current_balance < 0).reduce((sum, p) => sum + Math.abs(p.current_balance), 0);
116
+
117
+ data.push({
118
+ 'Party Name': '',
119
+ 'Phone': '',
120
+ 'City': '',
121
+ 'Type': 'SUMMARY',
122
+ 'Current Balance (₹)': '',
123
+ 'Status': '',
124
+ } as any);
125
+
126
+ data.push({
127
+ 'Party Name': '',
128
+ 'Phone': '',
129
+ 'City': '',
130
+ 'Type': 'Total Receivable',
131
+ 'Current Balance (₹)': totalReceivable.toFixed(2),
132
+ 'Status': '',
133
+ } as any);
134
+
135
+ data.push({
136
+ 'Party Name': '',
137
+ 'Phone': '',
138
+ 'City': '',
139
+ 'Type': 'Total Payable',
140
+ 'Current Balance (₹)': totalPayable.toFixed(2),
141
+ 'Status': '',
142
+ } as any);
143
+
144
+ const ws = XLSX.utils.json_to_sheet(data);
145
+ ws['!cols'] = [
146
+ { wch: 30 }, // Party Name
147
+ { wch: 15 }, // Phone
148
+ { wch: 20 }, // City
149
+ { wch: 15 }, // Type
150
+ { wch: 20 }, // Balance
151
+ { wch: 15 }, // Status
152
+ ];
153
+
154
+ const wb = XLSX.utils.book_new();
155
+ XLSX.utils.book_append_sheet(wb, ws, 'All Parties');
156
+
157
+ const filename = `All_Parties_${new Date().toISOString().split('T')[0]}.xlsx`;
158
+ XLSX.writeFile(wb, filename);
159
+ };
160
+
161
+ /**
162
+ * Export all transactions to Excel
163
+ */
164
+ export const exportAllTransactions = (
165
+ transactions: Transaction[],
166
+ dateRange?: { from: string; to: string }
167
+ ) => {
168
+ const filteredTransactions = dateRange
169
+ ? transactions.filter(t => {
170
+ const txDate = new Date(t.bill_date);
171
+ return txDate >= new Date(dateRange.from) && txDate <= new Date(dateRange.to);
172
+ })
173
+ : transactions;
174
+
175
+ const data = filteredTransactions.map(tx => ({
176
+ 'Date': new Date(tx.bill_date).toLocaleDateString('en-IN'),
177
+ 'Bill Number': tx.bill_number,
178
+ 'Party': tx.party_name,
179
+ 'Type': tx.bill_type.toUpperCase(),
180
+ 'Return': tx.is_return ? 'Yes' : 'No',
181
+ 'Items': tx.items.map(i => `${i.mirchi_name} (${i.net_weight}kg)`).join(', '),
182
+ 'Total Amount (₹)': tx.total_amount.toFixed(2),
183
+ 'Paid (₹)': tx.paid_amount.toFixed(2),
184
+ 'Balance (₹)': tx.balance_amount.toFixed(2),
185
+ }));
186
+
187
+ const ws = XLSX.utils.json_to_sheet(data);
188
+ ws['!cols'] = [
189
+ { wch: 12 },
190
+ { wch: 20 },
191
+ { wch: 25 },
192
+ { wch: 10 },
193
+ { wch: 8 },
194
+ { wch: 40 },
195
+ { wch: 15 },
196
+ { wch: 15 },
197
+ { wch: 15 },
198
+ ];
199
+
200
+ const wb = XLSX.utils.book_new();
201
+ XLSX.utils.book_append_sheet(wb, ws, 'Transactions');
202
+
203
+ const filename = `All_Transactions_${new Date().toISOString().split('T')[0]}.xlsx`;
204
+ XLSX.writeFile(wb, filename);
205
+ };
vite.config.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+ import { defineConfig, loadEnv } from 'vite';
3
+ import react from '@vitejs/plugin-react';
4
+
5
+ export default defineConfig(({ mode }) => {
6
+ const env = loadEnv(mode, '.', '');
7
+ return {
8
+ server: {
9
+ port: 3000,
10
+ host: '0.0.0.0',
11
+ },
12
+ preview: {
13
+ port: 7860,
14
+ host: '0.0.0.0',
15
+ },
16
+ plugins: [react()],
17
+ define: {
18
+ 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
19
+ 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
20
+ // Expose VITE_ prefixed env variables
21
+ 'import.meta.env.VITE_API_URL': JSON.stringify(env.VITE_API_URL)
22
+ },
23
+ resolve: {
24
+ alias: {
25
+ '@': path.resolve(__dirname, '.'),
26
+ }
27
+ }
28
+ };
29
+ });