Antaram commited on
Commit
168917d
·
verified ·
1 Parent(s): 289ef31

Upload 36 files

Browse files
Files changed (21) hide show
  1. .env.local +1 -1
  2. .gitattributes +37 -37
  3. .gitignore +24 -24
  4. App.tsx +48 -48
  5. README.md +10 -10
  6. components/Layout.tsx +122 -122
  7. components/PrintInvoice.tsx +12 -1
  8. index.html +94 -94
  9. index.tsx +14 -14
  10. metadata.json +4 -4
  11. package-lock.json +0 -0
  12. package.json +28 -28
  13. pages/AwaakBill.tsx +865 -858
  14. pages/Dashboard.tsx +399 -399
  15. pages/JawaakBill.tsx +806 -794
  16. pages/PartyLedger.tsx +650 -650
  17. pages/Settings.tsx +316 -316
  18. pages/StockReport.tsx +453 -450
  19. services/db.ts +353 -308
  20. tsconfig.json +28 -28
  21. types.ts +122 -121
.env.local CHANGED
@@ -1 +1 @@
1
- GEMINI_API_KEY=PLACEHOLDER_API_KEY
 
1
+ GEMINI_API_KEY=PLACEHOLDER_API_KEY
.gitattributes CHANGED
@@ -1,37 +1,37 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz 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
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz 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 CHANGED
@@ -1,24 +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?
 
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 CHANGED
@@ -1,49 +1,49 @@
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
- import PWAInstallPrompt from './components/PWAInstallPrompt';
11
- import { PWAProvider } from './context/PWAContext';
12
-
13
- const App = () => {
14
- // Register Service Worker for PWA
15
- useEffect(() => {
16
- if ('serviceWorker' in navigator) {
17
- window.addEventListener('load', () => {
18
- navigator.serviceWorker.register('/service-worker.js')
19
- .then(registration => {
20
- console.log('✅ PWA Service Worker registered:', registration);
21
- })
22
- .catch(error => {
23
- console.log('❌ SW registration failed:', error);
24
- });
25
- });
26
- }
27
- }, []);
28
-
29
- return (
30
- <PWAProvider>
31
- <HashRouter>
32
- <Layout>
33
- <Routes>
34
- <Route path="/" element={<Dashboard />} />
35
- <Route path="/jawaak" element={<JawaakBill />} />
36
- <Route path="/awaak" element={<AwaakBill />} />
37
- <Route path="/stock" element={<StockReport />} />
38
- <Route path="/ledger" element={<PartyLedger />} />
39
- <Route path="/settings" element={<Settings />} />
40
- <Route path="*" element={<Navigate to="/" replace />} />
41
- </Routes>
42
- </Layout>
43
- <PWAInstallPrompt />
44
- </HashRouter>
45
- </PWAProvider>
46
- );
47
- };
48
-
49
  export default App;
 
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
+ import PWAInstallPrompt from './components/PWAInstallPrompt';
11
+ import { PWAProvider } from './context/PWAContext';
12
+
13
+ const App = () => {
14
+ // Register Service Worker for PWA
15
+ useEffect(() => {
16
+ if ('serviceWorker' in navigator) {
17
+ window.addEventListener('load', () => {
18
+ navigator.serviceWorker.register('/service-worker.js')
19
+ .then(registration => {
20
+ console.log('✅ PWA Service Worker registered:', registration);
21
+ })
22
+ .catch(error => {
23
+ console.log('❌ SW registration failed:', error);
24
+ });
25
+ });
26
+ }
27
+ }, []);
28
+
29
+ return (
30
+ <PWAProvider>
31
+ <HashRouter>
32
+ <Layout>
33
+ <Routes>
34
+ <Route path="/" element={<Dashboard />} />
35
+ <Route path="/jawaak" element={<JawaakBill />} />
36
+ <Route path="/awaak" element={<AwaakBill />} />
37
+ <Route path="/stock" element={<StockReport />} />
38
+ <Route path="/ledger" element={<PartyLedger />} />
39
+ <Route path="/settings" element={<Settings />} />
40
+ <Route path="*" element={<Navigate to="/" replace />} />
41
+ </Routes>
42
+ </Layout>
43
+ <PWAInstallPrompt />
44
+ </HashRouter>
45
+ </PWAProvider>
46
+ );
47
+ };
48
+
49
  export default App;
README.md CHANGED
@@ -1,10 +1,10 @@
1
- ---
2
- title: Pratikpattanshetty
3
- emoji: 📊
4
- colorFrom: indigo
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ ---
2
+ title: Pratikpattanshetty
3
+ emoji: 📊
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
components/Layout.tsx CHANGED
@@ -1,123 +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;
 
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 CHANGED
@@ -22,7 +22,10 @@ const PrintInvoice: React.FC<PrintInvoiceProps> = ({ transaction, className }) =
22
  const cess = transaction.expenses?.cess_amount || 0;
23
  const adat = transaction.expenses?.adat_amount || 0;
24
  const hamali = (transaction.expenses?.hamali_amount || 0) + (transaction.expenses?.packaging_hamali_amount || 0);
25
- const grandTotal = transaction.total_amount || 0;
 
 
 
26
  const paidAmount = transaction.paid_amount || 0;
27
  const balanceAmount = transaction.balance_amount || 0;
28
 
@@ -262,6 +265,14 @@ const PrintInvoice: React.FC<PrintInvoiceProps> = ({ transaction, className }) =
262
  <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Hamali</td>
263
  <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(hamali)}</td>
264
  </tr>
 
 
 
 
 
 
 
 
265
  <tr style={{ backgroundColor: '#f3f4f6' }}>
266
  <th style={{ padding: '8px 4px', textAlign: 'left', borderTop: '2px solid #000', fontSize: '14px' }}>Grand Total</th>
267
  <th style={{ padding: '8px 4px', textAlign: 'right', borderTop: '2px solid #000', fontSize: '14px' }}>{formatINR(grandTotal)}</th>
 
22
  const cess = transaction.expenses?.cess_amount || 0;
23
  const adat = transaction.expenses?.adat_amount || 0;
24
  const hamali = (transaction.expenses?.hamali_amount || 0) + (transaction.expenses?.packaging_hamali_amount || 0);
25
+ const gaadiBharni = transaction.expenses?.gaadi_bharni || 0;
26
+ const otherExpenses = transaction.expenses?.other_expenses || 0;
27
+ const grandTotal = transaction.total_amount || (subtotal + packing + cess + adat + hamali + gaadiBharni + otherExpenses);
28
+
29
  const paidAmount = transaction.paid_amount || 0;
30
  const balanceAmount = transaction.balance_amount || 0;
31
 
 
265
  <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Hamali</td>
266
  <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(hamali)}</td>
267
  </tr>
268
+ <tr>
269
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Gaadi Bharni</td>
270
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(gaadiBharni)}</td>
271
+ </tr>
272
+ <tr>
273
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Other Expenses</td>
274
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(otherExpenses)}</td>
275
+ </tr>
276
  <tr style={{ backgroundColor: '#f3f4f6' }}>
277
  <th style={{ padding: '8px 4px', textAlign: 'left', borderTop: '2px solid #000', fontSize: '14px' }}>Grand Total</th>
278
  <th style={{ padding: '8px 4px', textAlign: 'right', borderTop: '2px solid #000', fontSize: '14px' }}>{formatINR(grandTotal)}</th>
index.html CHANGED
@@ -1,95 +1,95 @@
1
- <!DOCTYPE html>
2
- <html lang="mr">
3
-
4
- <head>
5
- <meta charset="UTF-8" />
6
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
7
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
- <title>Pattanshetty Inventory</title>
9
-
10
- <!-- PWA Manifest -->
11
- <link rel="manifest" href="/manifest.json" />
12
- <meta name="theme-color" content="#0d9488" />
13
- <meta name="mobile-web-app-capable" content="yes" />
14
- <meta name="apple-mobile-web-app-capable" content="yes" />
15
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
16
- <meta name="apple-mobile-web-app-title" content="Pattanshetty" />
17
- <link rel="apple-touch-icon" href="/icon-192.png" />
18
- <script src="https://cdn.tailwindcss.com"></script>
19
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
20
- <style>
21
- body {
22
- font-family: 'Inter', sans-serif;
23
- }
24
-
25
- /* Hide scrollbar for Chrome, Safari and Opera */
26
- .no-scrollbar::-webkit-scrollbar {
27
- display: none;
28
- }
29
-
30
- /* Hide scrollbar for IE, Edge and Firefox */
31
- .no-scrollbar {
32
- -ms-overflow-style: none;
33
- /* IE and Edge */
34
- scrollbar-width: none;
35
- /* Firefox */
36
- }
37
-
38
- /* Chrome, Safari, Edge, Opera - Hide number input arrows */
39
- input::-webkit-outer-spin-button,
40
- input::-webkit-inner-spin-button {
41
- -webkit-appearance: none;
42
- margin: 0;
43
- }
44
-
45
- /* Firefox - Hide number input arrows */
46
- input[type=number] {
47
- -moz-appearance: textfield;
48
- }
49
- </style>
50
- <script type="importmap">
51
- {
52
- "imports": {
53
- "react-router-dom": "https://aistudiocdn.com/react-router-dom@^7.9.6",
54
- "react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
55
- "react/": "https://aistudiocdn.com/react@^19.2.0/",
56
- "react": "https://aistudiocdn.com/react@^19.2.0",
57
- "lucide-react": "https://aistudiocdn.com/lucide-react@^0.554.0",
58
- "recharts": "https://aistudiocdn.com/recharts@^3.4.1"
59
- }
60
- }
61
- </script>
62
- <link rel="stylesheet" href="/index.css">
63
- </head>
64
-
65
- <body class="bg-gray-50 text-gray-900">
66
- <div id="root"></div>
67
- <script type="module" src="/index.tsx"></script>
68
-
69
- <!-- Service Worker Registration -->
70
- <script>
71
- if ('serviceWorker' in navigator) {
72
- window.addEventListener('load', () => {
73
- navigator.serviceWorker.register('/service-worker.js')
74
- .then((registration) => {
75
- console.log('[SW] Registered successfully:', registration.scope);
76
-
77
- // Check for updates
78
- registration.addEventListener('updatefound', () => {
79
- const newWorker = registration.installing;
80
- newWorker.addEventListener('statechange', () => {
81
- if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
82
- console.log('[SW] New version available! Refresh to update.');
83
- }
84
- });
85
- });
86
- })
87
- .catch((error) => {
88
- console.error('[SW] Registration failed:', error);
89
- });
90
- });
91
- }
92
- </script>
93
- </body>
94
-
95
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="mr">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>Pattanshetty Inventory</title>
9
+
10
+ <!-- PWA Manifest -->
11
+ <link rel="manifest" href="/manifest.json" />
12
+ <meta name="theme-color" content="#0d9488" />
13
+ <meta name="mobile-web-app-capable" content="yes" />
14
+ <meta name="apple-mobile-web-app-capable" content="yes" />
15
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
16
+ <meta name="apple-mobile-web-app-title" content="Pattanshetty" />
17
+ <link rel="apple-touch-icon" href="/icon-192.png" />
18
+ <script src="https://cdn.tailwindcss.com"></script>
19
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
20
+ <style>
21
+ body {
22
+ font-family: 'Inter', sans-serif;
23
+ }
24
+
25
+ /* Hide scrollbar for Chrome, Safari and Opera */
26
+ .no-scrollbar::-webkit-scrollbar {
27
+ display: none;
28
+ }
29
+
30
+ /* Hide scrollbar for IE, Edge and Firefox */
31
+ .no-scrollbar {
32
+ -ms-overflow-style: none;
33
+ /* IE and Edge */
34
+ scrollbar-width: none;
35
+ /* Firefox */
36
+ }
37
+
38
+ /* Chrome, Safari, Edge, Opera - Hide number input arrows */
39
+ input::-webkit-outer-spin-button,
40
+ input::-webkit-inner-spin-button {
41
+ -webkit-appearance: none;
42
+ margin: 0;
43
+ }
44
+
45
+ /* Firefox - Hide number input arrows */
46
+ input[type=number] {
47
+ -moz-appearance: textfield;
48
+ }
49
+ </style>
50
+ <script type="importmap">
51
+ {
52
+ "imports": {
53
+ "react-router-dom": "https://aistudiocdn.com/react-router-dom@^7.9.6",
54
+ "react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
55
+ "react/": "https://aistudiocdn.com/react@^19.2.0/",
56
+ "react": "https://aistudiocdn.com/react@^19.2.0",
57
+ "lucide-react": "https://aistudiocdn.com/lucide-react@^0.554.0",
58
+ "recharts": "https://aistudiocdn.com/recharts@^3.4.1"
59
+ }
60
+ }
61
+ </script>
62
+ <link rel="stylesheet" href="/index.css">
63
+ </head>
64
+
65
+ <body class="bg-gray-50 text-gray-900">
66
+ <div id="root"></div>
67
+ <script type="module" src="/index.tsx"></script>
68
+
69
+ <!-- Service Worker Registration -->
70
+ <script>
71
+ if ('serviceWorker' in navigator) {
72
+ window.addEventListener('load', () => {
73
+ navigator.serviceWorker.register('/service-worker.js')
74
+ .then((registration) => {
75
+ console.log('[SW] Registered successfully:', registration.scope);
76
+
77
+ // Check for updates
78
+ registration.addEventListener('updatefound', () => {
79
+ const newWorker = registration.installing;
80
+ newWorker.addEventListener('statechange', () => {
81
+ if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
82
+ console.log('[SW] New version available! Refresh to update.');
83
+ }
84
+ });
85
+ });
86
+ })
87
+ .catch((error) => {
88
+ console.error('[SW] Registration failed:', error);
89
+ });
90
+ });
91
+ }
92
+ </script>
93
+ </body>
94
+
95
  </html>
index.tsx CHANGED
@@ -1,15 +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
  );
 
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 CHANGED
@@ -1,5 +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
  }
 
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
  }
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -1,28 +1,28 @@
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
- "html2canvas": "^1.4.1",
13
- "jspdf": "^3.0.4",
14
- "jspdf-autotable": "^5.0.2",
15
- "lucide-react": "^0.554.0",
16
- "react": "^19.2.0",
17
- "react-dom": "^19.2.0",
18
- "react-router-dom": "^7.9.6",
19
- "recharts": "^3.4.1",
20
- "xlsx": "^0.18.5"
21
- },
22
- "devDependencies": {
23
- "@types/node": "^22.14.0",
24
- "@vitejs/plugin-react": "^5.0.0",
25
- "typescript": "~5.8.2",
26
- "vite": "^6.2.0"
27
- }
28
- }
 
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
+ "html2canvas": "^1.4.1",
13
+ "jspdf": "^3.0.4",
14
+ "jspdf-autotable": "^5.0.2",
15
+ "lucide-react": "^0.554.0",
16
+ "react": "^19.2.0",
17
+ "react-dom": "^19.2.0",
18
+ "react-router-dom": "^7.9.6",
19
+ "recharts": "^3.4.1",
20
+ "xlsx": "^0.18.5"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.14.0",
24
+ "@vitejs/plugin-react": "^5.0.0",
25
+ "typescript": "~5.8.2",
26
+ "vite": "^6.2.0"
27
+ }
28
+ }
pages/AwaakBill.tsx CHANGED
@@ -1,859 +1,866 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { useNavigate } from 'react-router-dom';
3
- import {
4
- getParties, getMirchiTypes, generateBillNumber, saveTransaction,
5
- checkLotUnique, getAvailableLotsByMirchi
6
- } from '../services/db';
7
- import {
8
- Party, MirchiType, Lot, Transaction, TransactionItem, BillType, PaymentMode, PartyType
9
- } from '../types';
10
- import { Save, Trash2, Plus, RotateCcw, Printer, Download, ChevronUp, ChevronDown } from 'lucide-react';
11
- import PrintInvoice from '../components/PrintInvoice.tsx';
12
- import PdfInvoice from '../components/PdfInvoice.tsx';
13
-
14
- // Defined outside to prevent re-render focus loss
15
- const SummaryInput = ({ label, value, onChange }: { label: string, value: number, onChange: (val: number) => void }) => (
16
- <div className="flex justify-between items-center mb-1 bg-gray-50 p-1.5 rounded">
17
- <span className="text-gray-600 text-xs">{label}</span>
18
- <input
19
- type="number"
20
- step="0.01"
21
- min="0"
22
- className="w-24 border rounded text-right p-1 text-sm bg-white focus:ring-1 focus:ring-teal-500 outline-none appearance-none"
23
- value={value === 0 ? '' : value}
24
- onChange={e => {
25
- const parsed = parseFloat(e.target.value);
26
- onChange(isNaN(parsed) ? 0 : Math.max(0, parsed));
27
- }}
28
- placeholder="0"
29
- />
30
- </div>
31
- );
32
-
33
- const AwaakBill = () => {
34
- const navigate = useNavigate();
35
-
36
- // Master Data
37
- const [parties, setParties] = useState<Party[]>([]);
38
- const [mirchiTypes, setMirchiTypes] = useState<MirchiType[]>([]);
39
-
40
- // Form State
41
- const [isReturnMode, setIsReturnMode] = useState(false);
42
- const [billDate, setBillDate] = useState(new Date().toISOString().split('T')[0]);
43
- const [billNumber, setBillNumber] = useState('');
44
- const [selectedParty, setSelectedParty] = useState('');
45
-
46
- // Mobile UI State
47
- const [showMobileSummary, setShowMobileSummary] = useState(false);
48
-
49
- const [items, setItems] = useState<Partial<TransactionItem>[]>([{
50
- id: Date.now().toString(),
51
- poti_weights: [],
52
- gross_weight: 0,
53
- poti_count: 0,
54
- total_potya: 0,
55
- net_weight: 0,
56
- rate_per_kg: 0,
57
- item_total: 0
58
- }]);
59
-
60
- // Temp inputs for items row
61
- const [potiInputs, setPotiInputs] = useState<{ [key: string]: string }>({});
62
-
63
- // LOT number state
64
- const [lotInputs, setLotInputs] = useState<{ [key: string]: string }>({});
65
- const [availableLots, setAvailableLots] = useState<{ [key: string]: Lot[] }>({});
66
-
67
- const [expenses, setExpenses] = useState({
68
- cess_percent: 1.0,
69
- cess_amount: 0,
70
- adat_percent: 3.0,
71
- adat_amount: 0,
72
- poti_rate: 0,
73
- poti_amount: 0,
74
- hamali_per_poti: 6,
75
- hamali_amount: 0,
76
- gaadi_bharni: 0,
77
- });
78
-
79
- // Payment State
80
- const [paymentMode, setPaymentMode] = useState<'cash' | 'online' | 'hybrid' | 'due'>('cash');
81
- const [paidAmount, setPaidAmount] = useState(0); // Not used directly in new logic, derived
82
- const [cashAmount, setCashAmount] = useState(0); // For hybrid
83
- const [onlineAmount, setOnlineAmount] = useState(0); // For hybrid & due
84
- const [isSubmitting, setIsSubmitting] = useState(false);
85
- const [savedTransaction, setSavedTransaction] = useState<Transaction | null>(null);
86
- const [isLoading, setIsLoading] = useState(true);
87
- const [error, setError] = useState<string | null>(null);
88
-
89
- // Initial Load & Bill Number Generation
90
- useEffect(() => {
91
- const loadData = async () => {
92
- try {
93
- setIsLoading(true);
94
- setError(null);
95
- const [partiesData, mirchiData] = await Promise.all([
96
- getParties(),
97
- getMirchiTypes()
98
- ]);
99
-
100
- if (!partiesData || partiesData.length === 0) {
101
- setError('No parties found. Please add parties in Settings first.');
102
- }
103
- if (!mirchiData || mirchiData.length === 0) {
104
- setError('No mirchi types found. Please add mirchi types in Settings first.');
105
- }
106
-
107
- // Filter parties for Awaak bills
108
- const filteredParties = partiesData.filter(p =>
109
- p.party_type === PartyType.AWAAK || p.party_type === PartyType.BOTH
110
- );
111
-
112
- setParties(filteredParties || []);
113
- setMirchiTypes(mirchiData || []);
114
- setBillNumber(generateBillNumber(BillType.AWAAK, isReturnMode));
115
- } catch (err: any) {
116
- console.error('Error loading data:', err);
117
- setError('Failed to load data. Please check your connection and try again.');
118
- } finally {
119
- setIsLoading(false);
120
- }
121
- };
122
- loadData();
123
- }, [isReturnMode]);
124
-
125
- // Calculation Logic
126
- const calculateRow = (item: Partial<TransactionItem>, potiString: string) => {
127
- const weights = potiString.split(/[\s,+]+/).map(n => parseFloat(n)).filter(n => !isNaN(n) && n > 0);
128
-
129
- const gross = weights.reduce((a, b) => a + b, 0);
130
- const count = weights.length;
131
- const potya = count * 1; // 1kg deduction per bag
132
- const net = Math.max(0, gross - potya);
133
- const total = net * (item.rate_per_kg || 0);
134
-
135
- return {
136
- ...item,
137
- poti_weights: weights,
138
- gross_weight: gross,
139
- poti_count: count,
140
- total_potya: potya,
141
- net_weight: net,
142
- item_total: total
143
- };
144
- };
145
-
146
- const handleItemChange = (id: string, field: string, value: any) => {
147
- setItems(prev => prev.map(item => {
148
- if (item.id !== id) return item;
149
-
150
- let updatedItem = { ...item, [field]: value };
151
-
152
- if (field === 'rate_per_kg') {
153
- updatedItem.item_total = (updatedItem.net_weight || 0) * Math.max(0, parseFloat(value || 0));
154
- }
155
-
156
- if (field === 'net_weight') {
157
- updatedItem.item_total = Math.max(0, parseFloat(value || 0)) * (updatedItem.rate_per_kg || 0);
158
- }
159
-
160
- return updatedItem;
161
- }));
162
- };
163
-
164
- const handlePotiInputChange = (id: string, value: string) => {
165
- setPotiInputs(prev => ({ ...prev, [id]: value }));
166
- setItems(prev => prev.map(item => {
167
- if (item.id !== id) return item;
168
- return calculateRow(item, value);
169
- }));
170
- };
171
-
172
- // Handle mirchi type change - load available lots
173
- const handleMirchiChange = async (id: string, mirchiTypeId: string) => {
174
- handleItemChange(id, 'mirchi_type_id', mirchiTypeId);
175
-
176
- // Load available lots for this mirchi type
177
- if (mirchiTypeId) {
178
- const lots = await getAvailableLotsByMirchi(mirchiTypeId);
179
- setAvailableLots(prev => ({ ...prev, [id]: lots }));
180
-
181
- // Generate suggested lot number
182
- const mirchi = mirchiTypes.find(m => m.id === mirchiTypeId);
183
- if (mirchi) {
184
- const today = new Date().toISOString().split('T')[0].replace(/-/g, '');
185
- const mirchiCode = mirchi.name.substring(0, 4).toUpperCase();
186
- const suggestedLot = `LOT-${mirchiCode}-${today}-`;
187
- setLotInputs(prev => ({ ...prev, [id]: suggestedLot }));
188
- }
189
- }
190
- };
191
-
192
- // Handle LOT number input change
193
- const handleLotNumberChange = (id: string, value: string) => {
194
- setLotInputs(prev => ({ ...prev, [id]: value }));
195
- handleItemChange(id, 'lot_number', value);
196
- };
197
-
198
- // Handle selecting existing lot
199
- const handleLotSelection = (id: string, lotNumber: string) => {
200
- if (lotNumber) {
201
- setLotInputs(prev => ({ ...prev, [id]: lotNumber }));
202
- handleItemChange(id, 'lot_number', lotNumber);
203
- handleItemChange(id, 'is_existing_lot', true);
204
- } else {
205
- handleItemChange(id, 'is_existing_lot', false);
206
- }
207
- };
208
-
209
- const addItem = () => {
210
- setItems(prev => [...prev, {
211
- id: Date.now().toString(),
212
- poti_weights: [],
213
- gross_weight: 0,
214
- poti_count: 0,
215
- total_potya: 0,
216
- net_weight: 0,
217
- rate_per_kg: 0,
218
- item_total: 0
219
- }]);
220
- };
221
-
222
- const removeItem = (id: string) => {
223
- if (items.length > 1) {
224
- setItems(prev => prev.filter(i => i.id !== id));
225
- const newInputs = { ...potiInputs };
226
- delete newInputs[id];
227
- setPotiInputs(newInputs);
228
- }
229
- };
230
-
231
- // Totals Calculation
232
- const subtotal = items.reduce((acc, item) => acc + (item.item_total || 0), 0);
233
- const totalPoti = items.reduce((acc, item) => acc + (item.poti_count || 0), 0);
234
-
235
- // Derived Expenses - New sequence
236
- const potiAmt = totalPoti * expenses.poti_rate;
237
- const baseForCess = subtotal + potiAmt; // Cess calculated on subtotal + poti
238
- const cessAmt = (baseForCess * expenses.cess_percent) / 100;
239
- const adatAmt = (subtotal * expenses.adat_percent) / 100;
240
- const hamaliAmt = totalPoti * expenses.hamali_per_poti;
241
- const totalExp = potiAmt + cessAmt + adatAmt + hamaliAmt + (expenses.gaadi_bharni || 0);
242
- const grandTotal = subtotal + totalExp;
243
-
244
- // Calculate Payment Details based on Mode
245
- let finalCash = 0;
246
- let finalOnline = 0;
247
- let currentPaid = 0;
248
-
249
- if (paymentMode === 'cash') {
250
- finalCash = grandTotal;
251
- finalOnline = 0;
252
- currentPaid = grandTotal;
253
- } else if (paymentMode === 'online') {
254
- finalCash = 0;
255
- finalOnline = grandTotal;
256
- currentPaid = grandTotal;
257
- } else if (paymentMode === 'hybrid') {
258
- finalCash = cashAmount;
259
- finalOnline = Math.max(0, grandTotal - cashAmount);
260
- currentPaid = grandTotal; // Hybrid assumes full payment
261
- } else if (paymentMode === 'due') {
262
- finalCash = 0;
263
- finalOnline = onlineAmount; // User defined
264
- currentPaid = onlineAmount;
265
- }
266
-
267
- const balance = grandTotal - currentPaid;
268
-
269
- // Validation Error
270
- // Hybrid: Cash cannot exceed Total
271
- // Due: Online cannot exceed Total
272
- const isOverpaid = (paymentMode === 'hybrid' && cashAmount > grandTotal) ||
273
- (paymentMode === 'due' && onlineAmount > grandTotal);
274
-
275
- const validateForm = () => {
276
- if (!selectedParty) {
277
- alert('Please select a Party (पार्टी निवडा)');
278
- return false;
279
- }
280
- for (let i = 0; i < items.length; i++) {
281
- const item = items[i];
282
- if (!item.mirchi_type_id) {
283
- alert(`Row ${i + 1}: Please select Mirchi Type`);
284
- return false;
285
- }
286
- // LOT validation - different for return vs normal mode
287
- if (isReturnMode) {
288
- // Return mode: must select existing lot
289
- if (!item.lot_id || !item.lot_number) {
290
- alert(`Row ${i + 1}: Please select a LOT to return`);
291
- return false;
292
- }
293
- } else {
294
- // Normal mode: must enter new lot number
295
- if (!item.lot_number || item.lot_number.trim() === '') {
296
- alert(`Row ${i + 1}: Please enter LOT number`);
297
- return false;
298
- }
299
- // Validate LOT number format only in normal mode
300
- const lotPattern = /^LOT-[A-Z]{3,4}-\d{8}-.+$/;
301
- if (!lotPattern.test(item.lot_number)) {
302
- alert(`Row ${i + 1}: Invalid LOT format. Use: LOT-XXXX-YYYYMMDD-XXX`);
303
- return false;
304
- }
305
- }
306
- if (!item.poti_weights || item.poti_weights.length === 0) {
307
- alert(`Row ${i + 1}: Please enter weights (e.g. 10, 20)`);
308
- return false;
309
- }
310
- if (!item.rate_per_kg || item.rate_per_kg <= 0) {
311
- alert(`Row ${i + 1}: Rate must be greater than 0`);
312
- return false;
313
- }
314
- }
315
- if (isOverpaid) {
316
- alert('Paid amount cannot be greater than Total Amount');
317
- return false;
318
- }
319
- return true;
320
- };
321
-
322
- const handleSubmit = async () => {
323
- if (isSubmitting) return;
324
- if (!validateForm()) return;
325
-
326
- setIsSubmitting(true);
327
-
328
- // Populate mirchi_name for each item from mirchiTypes
329
- const itemsWithNames = items.map(item => ({
330
- ...item,
331
- mirchi_name: mirchiTypes.find(m => m.id === item.mirchi_type_id)?.name || 'Unknown'
332
- }));
333
-
334
- const transaction: Transaction = {
335
- id: Date.now().toString(),
336
- bill_number: billNumber,
337
- bill_date: billDate,
338
- bill_type: BillType.AWAAK,
339
- is_return: isReturnMode,
340
- party_id: selectedParty,
341
- party_name: parties.find(p => p.id === selectedParty)?.name,
342
- items: itemsWithNames as TransactionItem[],
343
- expenses: {
344
- ...expenses,
345
- cess_amount: cessAmt,
346
- adat_amount: adatAmt,
347
- poti_amount: potiAmt,
348
- hamali_amount: hamaliAmt
349
- },
350
- payments: [
351
- ...(finalCash > 0 ? [{ mode: PaymentMode.CASH, amount: finalCash }] : []),
352
- ...(finalOnline > 0 ? [{ mode: PaymentMode.ONLINE, amount: finalOnline }] : []),
353
- ...(balance > 0 ? [{ mode: PaymentMode.DUE, amount: balance }] : []) // Optional: Track due as a payment entry or just balance? Usually balance is enough. Keeping logic simple.
354
- ],
355
- gross_weight_total: items.reduce((a, i) => a + (i.gross_weight || 0), 0),
356
- net_weight_total: items.reduce((a, i) => a + (i.net_weight || 0), 0),
357
- subtotal: subtotal,
358
- total_expenses: totalExp,
359
- total_amount: grandTotal,
360
- paid_amount: currentPaid,
361
- balance_amount: balance
362
- };
363
-
364
- const result = await saveTransaction(transaction);
365
- if (result.success) {
366
- // Use the complete transaction object we created, not just the API response
367
- // This ensures all items are included for printing
368
- const completeTransaction = result.data ? {
369
- ...transaction,
370
- ...result.data,
371
- items: transaction.items // Ensure items are preserved
372
- } : transaction;
373
- setSavedTransaction(completeTransaction);
374
- alert('Bill Saved Successfully! You can now print the invoice.');
375
- } else {
376
- alert(`Error: ${result.message}`);
377
- }
378
- setIsSubmitting(false);
379
- };
380
-
381
- const handleHybridCashChange = (val: number) => {
382
- setCashAmount(val);
383
- // Online amount is derived in render, no state update needed for it in hybrid
384
- };
385
-
386
- const SummaryContent = () => (
387
- <div className="space-y-3 text-sm">
388
- <div className="flex justify-between">
389
- <span className="text-gray-600">एकुण रक्कम (Subtotal)</span>
390
- <span className="font-semibold">₹{subtotal.toFixed(2)}</span>
391
- </div>
392
-
393
- <div className="pt-2 border-t border-dashed space-y-2">
394
- {/* 1. Poti (Bags) Rate */}
395
- <SummaryInput
396
- label={`पोती (Bags ${totalPoti}) Rate`}
397
- value={expenses.poti_rate}
398
- onChange={val => setExpenses({ ...expenses, poti_rate: val })}
399
- />
400
- <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
401
- <span>Amount:</span>
402
- <span>₹{potiAmt.toFixed(2)}</span>
403
- </div>
404
-
405
- {/* 2. Cess Tax (calculated on subtotal + poti) */}
406
- <SummaryInput
407
- label={`सेस (Cess ${expenses.cess_percent}%) on ₹${baseForCess.toFixed(2)}`}
408
- value={expenses.cess_percent}
409
- onChange={val => setExpenses({ ...expenses, cess_percent: val })}
410
- />
411
- <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
412
- <span>Amount:</span>
413
- <span>₹{cessAmt.toFixed(2)}</span>
414
- </div>
415
-
416
- {/* 3. Adat / Market Yard Tax */}
417
- <SummaryInput
418
- label={`अडत (Adat ${expenses.adat_percent}%)`}
419
- value={expenses.adat_percent}
420
- onChange={val => setExpenses({ ...expenses, adat_percent: val })}
421
- />
422
- <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
423
- <span>Amount:</span>
424
- <span>₹{adatAmt.toFixed(2)}</span>
425
- </div>
426
-
427
- {/* 4. Hamali */}
428
- <SummaryInput
429
- label={`हमाली (Hamali per poti)`}
430
- value={expenses.hamali_per_poti}
431
- onChange={val => setExpenses({ ...expenses, hamali_per_poti: val })}
432
- />
433
- <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
434
- <span>Amount ({expenses.hamali_per_poti} * {totalPoti}):</span>
435
- <span>₹{hamaliAmt.toFixed(2)}</span>
436
- </div>
437
-
438
- {/* 5. Gaadi Bharni */}
439
- <SummaryInput
440
- label="गाडी भरणी"
441
- value={expenses.gaadi_bharni}
442
- onChange={val => setExpenses({ ...expenses, gaadi_bharni: val })}
443
- />
444
- </div>
445
-
446
- {/* 6. Total Price */}
447
- <div className="pt-2 border-t border-gray-200 flex justify-between items-center">
448
- <span className="text-base font-bold text-gray-800">एकुण बिल (Total)</span>
449
- <span className="text-xl font-bold text-red-600">{grandTotal.toFixed(2)}</span>
450
- </div>
451
-
452
- {/* Payment Section */}
453
- <div className="bg-teal-50 p-3 rounded-lg mt-2 border border-teal-100">
454
- <label className="block text-xs font-medium text-teal-800 mb-2">Payment Mode</label>
455
- <div className="flex gap-2 mb-3">
456
- {['cash', 'online', 'hybrid', 'due'].map(mode => (
457
- <button
458
- key={mode}
459
- onClick={() => {
460
- setPaymentMode(mode as any);
461
- setCashAmount(0);
462
- setOnlineAmount(0);
463
- }}
464
- className={`flex-1 py-1 text-xs font-medium rounded border ${paymentMode === mode
465
- ? 'bg-teal-600 text-white border-teal-600'
466
- : 'bg-white text-gray-600 border-gray-200'
467
- } capitalize`}
468
- >
469
- {mode}
470
- </button>
471
- ))}
472
- </div>
473
-
474
- {paymentMode === 'cash' && (
475
- <div className="text-center py-2 text-sm text-gray-600">
476
- Full Payment in Cash: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
477
- </div>
478
- )}
479
-
480
- {paymentMode === 'online' && (
481
- <div className="text-center py-2 text-sm text-gray-600">
482
- Full Payment Online: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
483
- </div>
484
- )}
485
-
486
- {paymentMode === 'hybrid' && (
487
- <div className="space-y-2">
488
- <div>
489
- <label className="text-xs text-gray-600">Cash Amount</label>
490
- <input
491
- type="number"
492
- min="0"
493
- max={grandTotal}
494
- step="0.01"
495
- className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
496
- value={cashAmount === 0 ? '' : cashAmount}
497
- onChange={e => {
498
- const val = parseFloat(e.target.value) || 0;
499
- if (val >= 0 && val <= grandTotal) {
500
- handleHybridCashChange(val);
501
- }
502
- }}
503
- onKeyDown={e => {
504
- // Prevent special characters: -, +, e, E
505
- if (['-', '+', 'e', 'E'].includes(e.key)) {
506
- e.preventDefault();
507
- }
508
- }}
509
- placeholder="Cash"
510
- />
511
- </div>
512
- <div>
513
- <label className="text-xs text-gray-600">Online Amount (Auto)</label>
514
- <input
515
- type="text"
516
- readOnly
517
- className="w-full bg-gray-100 border border-gray-200 rounded p-2 text-right font-medium text-gray-600"
518
- value={(grandTotal - cashAmount).toFixed(2)}
519
- />
520
- </div>
521
- </div>
522
- )}
523
-
524
- {paymentMode === 'due' && (
525
- <div className="space-y-2">
526
- <div>
527
- <label className="text-xs text-gray-600">Online Amount (Partial Payment)</label>
528
- <input
529
- type="number"
530
- min="0"
531
- max={grandTotal}
532
- step="0.01"
533
- className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
534
- value={onlineAmount === 0 ? '' : onlineAmount}
535
- onChange={e => {
536
- const val = parseFloat(e.target.value) || 0;
537
- if (val >= 0 && val <= grandTotal) {
538
- setOnlineAmount(val);
539
- }
540
- }}
541
- onKeyDown={e => {
542
- // Prevent special characters: -, +, e, E
543
- if (['-', '+', 'e', 'E'].includes(e.key)) {
544
- e.preventDefault();
545
- }
546
- }}
547
- placeholder="Enter amount paid online (0 for full due)"
548
- />
549
- </div>
550
- <div>
551
- <label className="text-xs text-gray-600">Due Amount (Auto)</label>
552
- <input
553
- type="text"
554
- readOnly
555
- className="w-full bg-red-50 border border-red-200 rounded p-2 text-right font-bold text-red-600"
556
- value={(grandTotal - onlineAmount).toFixed(2)}
557
- />
558
- </div>
559
- </div>
560
- )}
561
-
562
- {isOverpaid && (
563
- <div className="text-red-500 text-xs mt-2 font-medium text-center">
564
- Error: Amount exceeds Total!
565
- </div>
566
- )}
567
- </div>
568
-
569
- <div className="flex justify-between items-center pt-2">
570
- <span className="text-gray-600 font-medium">बाकी (Balance)</span>
571
- <span className={`font-bold ${balance > 0 ? 'text-red-500' : 'text-green-600'}`}>₹{balance.toFixed(2)}</span>
572
- </div>
573
- </div>
574
- );
575
-
576
- return (
577
- <div className="flex flex-col lg:flex-row gap-4 h-full p-4 bg-gray-50">
578
- {/* Loading State */}
579
- {isLoading && (
580
- <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-gray-100">
581
- <div className="text-center">
582
- <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mb-4"></div>
583
- <p className="text-gray-600">Loading data...</p>
584
- </div>
585
- </div>
586
- )}
587
-
588
- {/* Error State */}
589
- {error && !isLoading && (
590
- <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-red-200">
591
- <div className="text-center p-8">
592
- <div className="text-red-500 text-5xl mb-4">⚠️</div>
593
- <h3 className="text-lg font-semibold text-gray-800 mb-2">Error Loading Data</h3>
594
- <p className="text-gray-600 mb-4">{error}</p>
595
- <button
596
- onClick={() => window.location.reload()}
597
- className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"
598
- >
599
- Retry
600
- </button>
601
- </div>
602
- </div>
603
- )}
604
-
605
- {/* Main Content - Only show when not loading and no error */}
606
- {!isLoading && !error && (
607
- <>
608
- {/* Left: Form */}
609
- <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'}`}>
610
- <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
611
- <h2 className={`text-lg font-bold flex items-center gap-2 ${isReturnMode ? 'text-red-600' : 'text-gray-800'}`}>
612
- {isReturnMode ? <RotateCcw className="text-red-500" /> : <div className="text-teal-600 font-bold">IN</div>}
613
- {isReturnMode ? 'आवक परतावा (Purchase Return)' : 'आवक बिल (Purchase)'}
614
- </h2>
615
-
616
- <div className="flex items-center gap-3">
617
- <div className="flex bg-gray-100 rounded-lg p-1">
618
- <button
619
- onClick={() => setIsReturnMode(false)}
620
- className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${!isReturnMode ? 'bg-white text-teal-700 shadow-sm' : 'text-gray-500'}`}
621
- >
622
- Regular
623
- </button>
624
- <button
625
- onClick={() => setIsReturnMode(true)}
626
- className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${isReturnMode ? 'bg-red-500 text-white shadow-sm' : 'text-gray-500'}`}
627
- >
628
- Return
629
- </button>
630
- </div>
631
-
632
- <input
633
- type="date"
634
- className="bg-gray-100 px-3 py-1 rounded text-sm font-mono text-gray-600 border border-gray-200 hover:border-teal-400 focus:border-teal-500 focus:ring-1 focus:ring-teal-500 outline-none cursor-pointer transition-colors"
635
- value={billDate}
636
- onChange={(e) => setBillDate(e.target.value)}
637
- max={new Date().toISOString().split('T')[0]}
638
- />
639
- </div>
640
- </div>
641
-
642
- {/* Header Fields */}
643
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
644
-
645
- <div>
646
- <label className="block text-sm font-medium text-gray-700 mb-1">पार्टी नाव (Party)</label>
647
- <select
648
- className="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-teal-500 outline-none"
649
- value={selectedParty}
650
- onChange={e => setSelectedParty(e.target.value)}
651
- >
652
- <option value="">Select Party</option>
653
- {parties.map(p => (
654
- <option key={p.id} value={p.id}>{p.name} - {p.city}</option>
655
- ))}
656
- </select>
657
- </div>
658
- </div>
659
-
660
- {/* Items Table */}
661
- <div className="mb-6">
662
- <div className="flex justify-between items-center mb-2">
663
- <h3 className="font-semibold text-gray-700">माल तपशील (Items)</h3>
664
- </div>
665
-
666
- <div className="space-y-4">
667
- {items.map((item, index) => (
668
- <div key={item.id} className="p-4 border rounded-lg bg-gray-50 relative shadow-sm">
669
- <button
670
- onClick={() => removeItem(item.id!)}
671
- className="absolute top-2 right-2 text-red-400 hover:text-red-600 p-1"
672
- >
673
- <Trash2 size={18} />
674
- </button>
675
-
676
- <div className="grid grid-cols-2 md:grid-cols-6 gap-3">
677
- <div className="col-span-2">
678
- <label className="block text-xs font-medium text-gray-500 mb-1">मिरची जात (Type)</label>
679
- <select
680
- className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
681
- value={item.mirchi_type_id || ''}
682
- onChange={e => handleMirchiChange(item.id!, e.target.value)}
683
- >
684
- <option value="">Select Type</option>
685
- {mirchiTypes.map(m => (
686
- <option key={m.id} value={m.id}>{m.name}</option>
687
- ))}
688
- </select>
689
- </div>
690
-
691
- {/* LOT Number Section - Dynamic based on mode */}
692
- {item.mirchi_type_id && (
693
- <div className="col-span-2">
694
- <label className="block text-xs font-medium text-gray-500 mb-1">
695
- LOT Number {isReturnMode && <span className="text-red-500">(Select to Return)</span>}
696
- </label>
697
- {isReturnMode ? (
698
- // RETURN MODE: Select existing lot
699
- <select
700
- className="w-full border border-gray-300 rounded p-2 text-sm font-mono bg-white"
701
- value={item.lot_id || ''}
702
- onChange={e => {
703
- const selectedLot = availableLots[item.id!]?.find(l => l.id === e.target.value);
704
- if (selectedLot) {
705
- handleItemChange(item.id!, 'lot_id', selectedLot.id);
706
- handleItemChange(item.id!, 'lot_number', selectedLot.lot_number);
707
- }
708
- }}
709
- >
710
- <option value="">Select LOT to return</option>
711
- {(availableLots[item.id!] || []).map(lot => (
712
- <option key={lot.id} value={lot.id}>
713
- {lot.lot_number} ({lot.remaining_quantity}kg available)
714
- </option>
715
- ))}
716
- </select>
717
- ) : (
718
- // NORMAL MODE: New lot input
719
- <input
720
- type="text"
721
- className="w-full border border-gray-300 rounded p-2 text-sm font-mono"
722
- placeholder="Enter new LOT number"
723
- value={lotInputs[item.id!] || ''}
724
- onChange={e => {
725
- setLotInputs({ ...lotInputs, [item.id!]: e.target.value });
726
- handleItemChange(item.id!, 'lot_number', e.target.value);
727
- handleItemChange(item.id!, 'is_existing_lot', false);
728
- }}
729
- />
730
- )}
731
- </div>
732
- )}
733
- <div className="col-span-2 md:col-span-4">
734
- <label className="block text-xs font-medium text-gray-500 mb-1">पोटी वजन (Eg: 10,20,30)</label>
735
- <input
736
- type="text"
737
- placeholder="10, 20, 30"
738
- className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
739
- value={potiInputs[item.id!] || ''}
740
- onChange={e => handlePotiInputChange(item.id!, e.target.value)}
741
- />
742
- </div>
743
-
744
- <div>
745
- <label className="block text-xs font-medium text-gray-500 mb-1">Gross</label>
746
- <input type="number" readOnly className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm" value={item.gross_weight} />
747
- </div>
748
- <div>
749
- <label className="block text-xs font-medium text-gray-500 mb-1">Poti (count)</label>
750
- <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} />
751
- </div>
752
- <div>
753
- <label className="block text-xs font-medium text-gray-500 mb-1">Net (Editable)</label>
754
- <input
755
- type="number"
756
- min="0"
757
- className="w-full bg-blue-50 border border-blue-200 rounded p-2 text-sm font-bold text-blue-800 appearance-none"
758
- value={item.net_weight === 0 ? '' : item.net_weight}
759
- onChange={e => handleItemChange(item.id!, 'net_weight', Math.max(0, parseFloat(e.target.value) || 0))}
760
- placeholder="Auto-calculated"
761
- />
762
- </div>
763
-
764
- <div>
765
- <label className="block text-xs font-medium text-gray-500 mb-1">Rate (₹)</label>
766
- <input
767
- type="number"
768
- min="0"
769
- className="w-full border border-gray-300 rounded p-2 text-sm bg-white appearance-none"
770
- onWheel={e => e.currentTarget.blur()}
771
- value={item.rate_per_kg === 0 ? '' : item.rate_per_kg}
772
- onChange={e => handleItemChange(item.id!, 'rate_per_kg', Math.max(0, parseFloat(e.target.value) || 0))}
773
- placeholder="0"
774
- />
775
- </div>
776
- <div className="col-span-2 md:col-span-2">
777
- <label className="block text-xs font-medium text-gray-500 mb-1">Total ()</label>
778
- <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)} />
779
- </div>
780
- </div>
781
- </div>
782
- ))}
783
- </div>
784
- <button
785
- onClick={addItem}
786
- 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"
787
- >
788
- <Plus size={18} /> Add New Item (नवीन माल)
789
- </button>
790
- </div>
791
- </div>
792
-
793
- {/* Desktop: Right Summary Sidebar */}
794
- <div className="hidden lg:flex w-80 flex-col gap-4">
795
- <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-0">
796
- <h3 className="font-bold text-gray-800 mb-4 border-b pb-2">बिल सारांश (Summary)</h3>
797
- {SummaryContent()}
798
- <div className="flex gap-2 mt-6">
799
- {savedTransaction && (
800
- <>
801
- <PrintInvoice transaction={savedTransaction} />
802
- <PdfInvoice transaction={savedTransaction} />
803
- </>
804
- )}
805
- <button
806
- onClick={handleSubmit}
807
- disabled={isSubmitting || items.length === 0}
808
- className={`flex-1 bg-teal-600 text-white py-3 rounded-lg font-bold shadow-lg hover:bg-teal-700 transition-all flex items-center justify-center gap-2 ${isSubmitting || items.length === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
809
- >
810
- <Save size={20} />
811
- {isSubmitting ? 'Saving...' : 'Save Bill'}
812
- </button>
813
- </div>
814
- </div>
815
- </div>
816
-
817
- {/* Mobile: Sticky Bottom Action Bar */}
818
- <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">
819
- <div className="flex items-center justify-between p-4 bg-white z-20 relative">
820
- <div
821
- onClick={() => setShowMobileSummary(!showMobileSummary)}
822
- className="flex flex-col cursor-pointer"
823
- >
824
- <div className="flex items-center gap-1 text-gray-500 text-xs font-medium uppercase tracking-wide">
825
- Summary {showMobileSummary ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
826
- </div>
827
- <div className="text-xl font-bold text-red-600">
828
- {grandTotal.toFixed(2)}
829
- </div>
830
- </div>
831
- <button
832
- onClick={handleSubmit}
833
- disabled={isSubmitting}
834
- 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"
835
- >
836
- <Save size={18} /> {isSubmitting ? '...' : 'Save'}
837
- </button>
838
- </div>
839
-
840
- {showMobileSummary && (
841
- <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">
842
- <div className="mt-4">
843
- {SummaryContent()}
844
- </div>
845
- {savedTransaction && (
846
- <div className="mt-4 flex justify-center">
847
- <PrintInvoice transaction={savedTransaction} />
848
- </div>
849
- )}
850
- </div>
851
- )}
852
- </div>
853
- </>
854
- )}
855
- </div>
856
- );
857
- };
858
-
 
 
 
 
 
 
 
859
  export default AwaakBill;
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import {
4
+ getParties, getMirchiTypes, generateBillNumber, saveTransaction,
5
+ checkLotUnique, getAvailableLotsByMirchi
6
+ } from '../services/db';
7
+ import {
8
+ Party, MirchiType, Lot, Transaction, TransactionItem, BillType, PaymentMode, PartyType
9
+ } from '../types';
10
+ import { Save, Trash2, Plus, RotateCcw, Printer, Download, ChevronUp, ChevronDown } from 'lucide-react';
11
+ import PrintInvoice from '../components/PrintInvoice.tsx';
12
+ import PdfInvoice from '../components/PdfInvoice.tsx';
13
+
14
+ // Defined outside to prevent re-render focus loss
15
+ const SummaryInput = ({ label, value, onChange }: { label: string, value: number, onChange: (val: number) => void }) => (
16
+ <div className="flex justify-between items-center mb-1 bg-gray-50 p-1.5 rounded">
17
+ <span className="text-gray-600 text-xs">{label}</span>
18
+ <input
19
+ type="number"
20
+ step="0.01"
21
+ min="0"
22
+ className="w-24 border rounded text-right p-1 text-sm bg-white focus:ring-1 focus:ring-teal-500 outline-none appearance-none"
23
+ value={value === 0 ? '' : value}
24
+ onChange={e => {
25
+ const parsed = parseFloat(e.target.value);
26
+ onChange(isNaN(parsed) ? 0 : Math.max(0, parsed));
27
+ }}
28
+ placeholder="0"
29
+ />
30
+ </div>
31
+ );
32
+
33
+ const AwaakBill = () => {
34
+ const navigate = useNavigate();
35
+
36
+ // Master Data
37
+ const [parties, setParties] = useState<Party[]>([]);
38
+ const [mirchiTypes, setMirchiTypes] = useState<MirchiType[]>([]);
39
+
40
+ // Form State
41
+ const [isReturnMode, setIsReturnMode] = useState(false);
42
+ const [billDate, setBillDate] = useState(new Date().toISOString().split('T')[0]);
43
+ const [billNumber, setBillNumber] = useState('');
44
+ const [selectedParty, setSelectedParty] = useState('');
45
+
46
+ // Mobile UI State
47
+ const [showMobileSummary, setShowMobileSummary] = useState(false);
48
+
49
+ const [items, setItems] = useState<Partial<TransactionItem>[]>([{
50
+ id: Date.now().toString(),
51
+ poti_weights: [],
52
+ gross_weight: 0,
53
+ poti_count: 0,
54
+ total_potya: 0,
55
+ net_weight: 0,
56
+ rate_per_kg: 0,
57
+ item_total: 0
58
+ }]);
59
+
60
+ // Temp inputs for items row
61
+ const [potiInputs, setPotiInputs] = useState<{ [key: string]: string }>({});
62
+
63
+ // LOT number state
64
+ const [lotInputs, setLotInputs] = useState<{ [key: string]: string }>({});
65
+ const [availableLots, setAvailableLots] = useState<{ [key: string]: Lot[] }>({});
66
+
67
+ const [expenses, setExpenses] = useState({
68
+ cess_percent: 1.0,
69
+ cess_amount: 0,
70
+ adat_percent: 3.0,
71
+ adat_amount: 0,
72
+ poti_rate: 0,
73
+ poti_amount: 0,
74
+ hamali_per_poti: 6,
75
+ hamali_amount: 0,
76
+ gaadi_bharni: 0,
77
+ other_expenses: 0,
78
+ });
79
+
80
+ // Payment State
81
+ const [paymentMode, setPaymentMode] = useState<'cash' | 'online' | 'hybrid' | 'due'>('cash');
82
+ const [paidAmount, setPaidAmount] = useState(0); // Not used directly in new logic, derived
83
+ const [cashAmount, setCashAmount] = useState(0); // For hybrid
84
+ const [onlineAmount, setOnlineAmount] = useState(0); // For hybrid & due
85
+ const [isSubmitting, setIsSubmitting] = useState(false);
86
+ const [savedTransaction, setSavedTransaction] = useState<Transaction | null>(null);
87
+ const [isLoading, setIsLoading] = useState(true);
88
+ const [error, setError] = useState<string | null>(null);
89
+
90
+ // Initial Load & Bill Number Generation
91
+ useEffect(() => {
92
+ const loadData = async () => {
93
+ try {
94
+ setIsLoading(true);
95
+ setError(null);
96
+ const [partiesData, mirchiData] = await Promise.all([
97
+ getParties(),
98
+ getMirchiTypes()
99
+ ]);
100
+
101
+ if (!partiesData || partiesData.length === 0) {
102
+ setError('No parties found. Please add parties in Settings first.');
103
+ }
104
+ if (!mirchiData || mirchiData.length === 0) {
105
+ setError('No mirchi types found. Please add mirchi types in Settings first.');
106
+ }
107
+
108
+ // Filter parties for Awaak bills
109
+ const filteredParties = partiesData.filter(p =>
110
+ p.party_type === PartyType.AWAAK || p.party_type === PartyType.BOTH
111
+ );
112
+
113
+ setParties(filteredParties || []);
114
+ setMirchiTypes(mirchiData || []);
115
+ setBillNumber(generateBillNumber(BillType.AWAAK, isReturnMode));
116
+ } catch (err: any) {
117
+ console.error('Error loading data:', err);
118
+ setError('Failed to load data. Please check your connection and try again.');
119
+ } finally {
120
+ setIsLoading(false);
121
+ }
122
+ };
123
+ loadData();
124
+ }, [isReturnMode]);
125
+
126
+ // Calculation Logic
127
+ const calculateRow = (item: Partial<TransactionItem>, potiString: string) => {
128
+ const weights = potiString.split(/[\s,+]+/).map(n => parseFloat(n)).filter(n => !isNaN(n) && n > 0);
129
+
130
+ const gross = weights.reduce((a, b) => a + b, 0);
131
+ const count = weights.length;
132
+ const potya = count * 1; // 1kg deduction per bag
133
+ const net = Math.max(0, gross - potya);
134
+ const total = net * (item.rate_per_kg || 0);
135
+
136
+ return {
137
+ ...item,
138
+ poti_weights: weights,
139
+ gross_weight: gross,
140
+ poti_count: count,
141
+ total_potya: potya,
142
+ net_weight: net,
143
+ item_total: total
144
+ };
145
+ };
146
+
147
+ const handleItemChange = (id: string, field: string, value: any) => {
148
+ setItems(prev => prev.map(item => {
149
+ if (item.id !== id) return item;
150
+
151
+ let updatedItem = { ...item, [field]: value };
152
+
153
+ if (field === 'rate_per_kg') {
154
+ updatedItem.item_total = (updatedItem.net_weight || 0) * Math.max(0, parseFloat(value || 0));
155
+ }
156
+
157
+ if (field === 'net_weight') {
158
+ updatedItem.item_total = Math.max(0, parseFloat(value || 0)) * (updatedItem.rate_per_kg || 0);
159
+ }
160
+
161
+ return updatedItem;
162
+ }));
163
+ };
164
+
165
+ const handlePotiInputChange = (id: string, value: string) => {
166
+ setPotiInputs(prev => ({ ...prev, [id]: value }));
167
+ setItems(prev => prev.map(item => {
168
+ if (item.id !== id) return item;
169
+ return calculateRow(item, value);
170
+ }));
171
+ };
172
+
173
+ // Handle mirchi type change - load available lots
174
+ const handleMirchiChange = async (id: string, mirchiTypeId: string) => {
175
+ handleItemChange(id, 'mirchi_type_id', mirchiTypeId);
176
+
177
+ // Load available lots for this mirchi type
178
+ if (mirchiTypeId) {
179
+ const lots = await getAvailableLotsByMirchi(mirchiTypeId);
180
+ setAvailableLots(prev => ({ ...prev, [id]: lots }));
181
+
182
+ // Generate suggested lot number
183
+ const mirchi = mirchiTypes.find(m => m.id === mirchiTypeId);
184
+ if (mirchi) {
185
+ const today = new Date().toISOString().split('T')[0].replace(/-/g, '');
186
+ const mirchiCode = mirchi.name.substring(0, 4).toUpperCase();
187
+ const suggestedLot = `LOT-${mirchiCode}-${today}-`;
188
+ setLotInputs(prev => ({ ...prev, [id]: suggestedLot }));
189
+ }
190
+ }
191
+ };
192
+
193
+ // Handle LOT number input change
194
+ const handleLotNumberChange = (id: string, value: string) => {
195
+ setLotInputs(prev => ({ ...prev, [id]: value }));
196
+ handleItemChange(id, 'lot_number', value);
197
+ };
198
+
199
+ // Handle selecting existing lot
200
+ const handleLotSelection = (id: string, lotNumber: string) => {
201
+ if (lotNumber) {
202
+ setLotInputs(prev => ({ ...prev, [id]: lotNumber }));
203
+ handleItemChange(id, 'lot_number', lotNumber);
204
+ handleItemChange(id, 'is_existing_lot', true);
205
+ } else {
206
+ handleItemChange(id, 'is_existing_lot', false);
207
+ }
208
+ };
209
+
210
+ const addItem = () => {
211
+ setItems(prev => [...prev, {
212
+ id: Date.now().toString(),
213
+ poti_weights: [],
214
+ gross_weight: 0,
215
+ poti_count: 0,
216
+ total_potya: 0,
217
+ net_weight: 0,
218
+ rate_per_kg: 0,
219
+ item_total: 0
220
+ }]);
221
+ };
222
+
223
+ const removeItem = (id: string) => {
224
+ if (items.length > 1) {
225
+ setItems(prev => prev.filter(i => i.id !== id));
226
+ const newInputs = { ...potiInputs };
227
+ delete newInputs[id];
228
+ setPotiInputs(newInputs);
229
+ }
230
+ };
231
+
232
+ // Totals Calculation
233
+ const subtotal = items.reduce((acc, item) => acc + (item.item_total || 0), 0);
234
+ const totalPoti = items.reduce((acc, item) => acc + (item.poti_count || 0), 0);
235
+
236
+ // Derived Expenses - New sequence
237
+ const potiAmt = totalPoti * expenses.poti_rate;
238
+ const baseForCess = subtotal + potiAmt; // Cess calculated on subtotal + poti
239
+ const cessAmt = (baseForCess * expenses.cess_percent) / 100;
240
+ const adatAmt = (subtotal * expenses.adat_percent) / 100;
241
+ const hamaliAmt = totalPoti * expenses.hamali_per_poti;
242
+ const totalExp = potiAmt + cessAmt + adatAmt + hamaliAmt + (expenses.gaadi_bharni || 0) + (expenses.other_expenses || 0);
243
+ const grandTotal = subtotal + totalExp;
244
+
245
+ // Calculate Payment Details based on Mode
246
+ let finalCash = 0;
247
+ let finalOnline = 0;
248
+ let currentPaid = 0;
249
+
250
+ if (paymentMode === 'cash') {
251
+ finalCash = grandTotal;
252
+ finalOnline = 0;
253
+ currentPaid = grandTotal;
254
+ } else if (paymentMode === 'online') {
255
+ finalCash = 0;
256
+ finalOnline = grandTotal;
257
+ currentPaid = grandTotal;
258
+ } else if (paymentMode === 'hybrid') {
259
+ finalCash = cashAmount;
260
+ finalOnline = Math.max(0, grandTotal - cashAmount);
261
+ currentPaid = grandTotal; // Hybrid assumes full payment
262
+ } else if (paymentMode === 'due') {
263
+ finalCash = 0;
264
+ finalOnline = onlineAmount; // User defined
265
+ currentPaid = onlineAmount;
266
+ }
267
+
268
+ const balance = grandTotal - currentPaid;
269
+
270
+ // Validation Error
271
+ // Hybrid: Cash cannot exceed Total
272
+ // Due: Online cannot exceed Total
273
+ const isOverpaid = (paymentMode === 'hybrid' && cashAmount > grandTotal) ||
274
+ (paymentMode === 'due' && onlineAmount > grandTotal);
275
+
276
+ const validateForm = () => {
277
+ if (!selectedParty) {
278
+ alert('Please select a Party (पार्टी निवडा)');
279
+
280
+ return false;
281
+ }
282
+ for (let i = 0; i < items.length; i++) {
283
+ const item = items[i];
284
+ if (!item.mirchi_type_id) {
285
+ alert(`Row ${i + 1}: Please select Mirchi Type`);
286
+ return false;
287
+ }
288
+ // LOT validation - different for return vs normal mode
289
+ if (isReturnMode) {
290
+ // Return mode: must select existing lot
291
+ if (!item.lot_id || !item.lot_number) {
292
+ alert(`Row ${i + 1}: Please select a LOT to return`);
293
+ return false;
294
+ }
295
+ } else {
296
+ // Normal mode: must enter new lot number
297
+ if (!item.lot_number || item.lot_number.trim() === '') {
298
+ alert(`Row ${i + 1}: Please enter LOT number`);
299
+ return false;
300
+ }
301
+ }
302
+ if (!item.poti_weights || item.poti_weights.length === 0) {
303
+ alert(`Row ${i + 1}: Please enter weights (e.g. 10, 20)`);
304
+ return false;
305
+ }
306
+ if (!item.rate_per_kg || item.rate_per_kg <= 0) {
307
+ alert(`Row ${i + 1}: Rate must be greater than 0`);
308
+ return false;
309
+ }
310
+ }
311
+ if (isOverpaid) {
312
+ alert('Paid amount cannot be greater than Total Amount');
313
+ return false;
314
+ }
315
+ return true;
316
+ };
317
+
318
+ const handleSubmit = async () => {
319
+ if (isSubmitting) return;
320
+ if (!validateForm()) return;
321
+
322
+ setIsSubmitting(true);
323
+
324
+ // Populate mirchi_name for each item from mirchiTypes
325
+ const itemsWithNames = items.map(item => ({
326
+ ...item,
327
+ mirchi_name: mirchiTypes.find(m => m.id === item.mirchi_type_id)?.name || 'Unknown'
328
+ }));
329
+
330
+ const transaction: Transaction = {
331
+ id: Date.now().toString(),
332
+ bill_number: billNumber,
333
+ bill_date: billDate,
334
+ bill_type: BillType.AWAAK,
335
+ is_return: isReturnMode,
336
+ party_id: selectedParty,
337
+ party_name: parties.find(p => p.id === selectedParty)?.name,
338
+ items: itemsWithNames as TransactionItem[],
339
+ expenses: {
340
+ ...expenses,
341
+ cess_amount: cessAmt,
342
+ adat_amount: adatAmt,
343
+ poti_amount: potiAmt,
344
+ hamali_amount: hamaliAmt
345
+ },
346
+ payments: [
347
+ ...(finalCash > 0 ? [{ mode: PaymentMode.CASH, amount: finalCash }] : []),
348
+ ...(finalOnline > 0 ? [{ mode: PaymentMode.ONLINE, amount: finalOnline }] : []),
349
+ ...(balance > 0 ? [{ mode: PaymentMode.DUE, amount: balance }] : []) // Optional: Track due as a payment entry or just balance? Usually balance is enough. Keeping logic simple.
350
+ ],
351
+ gross_weight_total: items.reduce((a, i) => a + (i.gross_weight || 0), 0),
352
+ net_weight_total: items.reduce((a, i) => a + (i.net_weight || 0), 0),
353
+ subtotal: subtotal,
354
+ total_expenses: totalExp,
355
+ total_amount: grandTotal,
356
+ paid_amount: currentPaid,
357
+ balance_amount: balance
358
+ };
359
+
360
+ const result = await saveTransaction(transaction);
361
+ if (result.success) {
362
+ // Use the complete transaction object we created, not just the API response
363
+ // This ensures all items are included for printing
364
+ const completeTransaction = result.data ? {
365
+ ...transaction,
366
+ ...result.data,
367
+ items: transaction.items, // Ensure items are preserved
368
+ expenses: {
369
+ ...result.data.expenses,
370
+ other_expenses: transaction.expenses.other_expenses,
371
+ },
372
+ } : transaction;
373
+ setSavedTransaction(completeTransaction);
374
+ alert('Bill Saved Successfully! You can now print the invoice.');
375
+ } else {
376
+ alert(`Error: ${result.message}`);
377
+ }
378
+ setIsSubmitting(false);
379
+ };
380
+
381
+ const handleHybridCashChange = (val: number) => {
382
+ setCashAmount(val);
383
+ // Online amount is derived in render, no state update needed for it in hybrid
384
+ };
385
+
386
+ const SummaryContent = () => (
387
+ <div className="space-y-3 text-sm">
388
+ <div className="flex justify-between">
389
+ <span className="text-gray-600">एकुण रक्कम (Subtotal)</span>
390
+ <span className="font-semibold">₹{subtotal.toFixed(2)}</span>
391
+ </div>
392
+
393
+ <div className="pt-2 border-t border-dashed space-y-2">
394
+ {/* 1. Poti (Bags) Rate */}
395
+ <SummaryInput
396
+ label={`पोती (Bags ${totalPoti}) Rate`}
397
+ value={expenses.poti_rate}
398
+ onChange={val => setExpenses({ ...expenses, poti_rate: val })}
399
+ />
400
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
401
+ <span>Amount:</span>
402
+ <span>₹{potiAmt.toFixed(2)}</span>
403
+ </div>
404
+
405
+ {/* 2. Cess Tax (calculated on subtotal + poti) */}
406
+ <SummaryInput
407
+ label={`सेस (Cess ${expenses.cess_percent}%) on ₹${baseForCess.toFixed(2)}`}
408
+ value={expenses.cess_percent}
409
+ onChange={val => setExpenses({ ...expenses, cess_percent: val })}
410
+ />
411
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
412
+ <span>Amount:</span>
413
+ <span>₹{cessAmt.toFixed(2)}</span>
414
+ </div>
415
+
416
+ {/* 3. Adat / Market Yard Tax */}
417
+ <SummaryInput
418
+ label={`अडत (Adat ${expenses.adat_percent}%)`}
419
+ value={expenses.adat_percent}
420
+ onChange={val => setExpenses({ ...expenses, adat_percent: val })}
421
+ />
422
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
423
+ <span>Amount:</span>
424
+ <span>₹{adatAmt.toFixed(2)}</span>
425
+ </div>
426
+
427
+ {/* 4. Hamali */}
428
+ <SummaryInput
429
+ label={`हमाली (Hamali per poti)`}
430
+ value={expenses.hamali_per_poti}
431
+ onChange={val => setExpenses({ ...expenses, hamali_per_poti: val })}
432
+ />
433
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
434
+ <span>Amount ({expenses.hamali_per_poti} * {totalPoti}):</span>
435
+ <span>₹{hamaliAmt.toFixed(2)}</span>
436
+ </div>
437
+
438
+ {/* 5. Gaadi Bharni */}
439
+ <SummaryInput
440
+ label="गाडी भरणी"
441
+ value={expenses.gaadi_bharni}
442
+ onChange={val => setExpenses({ ...expenses, gaadi_bharni: val })}
443
+ />
444
+
445
+ {/* 6. Other Expenses */}
446
+ <SummaryInput
447
+ label="Other Expenses"
448
+ value={expenses.other_expenses}
449
+ onChange={val => setExpenses({ ...expenses, other_expenses: val })}
450
+ />
451
+ </div>
452
+
453
+ {/* 7. Total Price */}
454
+ <div className="pt-2 border-t border-gray-200 flex justify-between items-center">
455
+ <span className="text-base font-bold text-gray-800">एकुण बिल (Total)</span>
456
+ <span className="text-xl font-bold text-red-600">₹{grandTotal.toFixed(2)}</span>
457
+ </div>
458
+
459
+ {/* Payment Section */}
460
+ <div className="bg-teal-50 p-3 rounded-lg mt-2 border border-teal-100">
461
+ <label className="block text-xs font-medium text-teal-800 mb-2">Payment Mode</label>
462
+ <div className="flex gap-2 mb-3">
463
+ {['cash', 'online', 'hybrid', 'due'].map(mode => (
464
+ <button
465
+ key={mode}
466
+ onClick={() => {
467
+ setPaymentMode(mode as any);
468
+ setCashAmount(0);
469
+ setOnlineAmount(0);
470
+ }}
471
+ className={`flex-1 py-1 text-xs font-medium rounded border ${paymentMode === mode
472
+ ? 'bg-teal-600 text-white border-teal-600'
473
+ : 'bg-white text-gray-600 border-gray-200'
474
+ } capitalize`}
475
+ >
476
+ {mode}
477
+ </button>
478
+ ))}
479
+ </div>
480
+
481
+ {paymentMode === 'cash' && (
482
+ <div className="text-center py-2 text-sm text-gray-600">
483
+ Full Payment in Cash: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
484
+ </div>
485
+ )}
486
+
487
+ {paymentMode === 'online' && (
488
+ <div className="text-center py-2 text-sm text-gray-600">
489
+ Full Payment Online: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
490
+ </div>
491
+ )}
492
+
493
+ {paymentMode === 'hybrid' && (
494
+ <div className="space-y-2">
495
+ <div>
496
+ <label className="text-xs text-gray-600">Cash Amount</label>
497
+ <input
498
+ type="number"
499
+ min="0"
500
+ max={grandTotal}
501
+ step="0.01"
502
+ className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
503
+ value={cashAmount === 0 ? '' : cashAmount}
504
+ onChange={e => {
505
+ const val = parseFloat(e.target.value) || 0;
506
+ if (val >= 0 && val <= grandTotal) {
507
+ handleHybridCashChange(val);
508
+ }
509
+ }}
510
+ onKeyDown={e => {
511
+ // Prevent special characters: -, +, e, E
512
+ if (['-', '+', 'e', 'E'].includes(e.key)) {
513
+ e.preventDefault();
514
+ }
515
+ }}
516
+ placeholder="Cash"
517
+ />
518
+ </div>
519
+ <div>
520
+ <label className="text-xs text-gray-600">Online Amount (Auto)</label>
521
+ <input
522
+ type="text"
523
+ readOnly
524
+ className="w-full bg-gray-100 border border-gray-200 rounded p-2 text-right font-medium text-gray-600"
525
+ value={(grandTotal - cashAmount).toFixed(2)}
526
+ />
527
+ </div>
528
+ </div>
529
+ )}
530
+
531
+ {paymentMode === 'due' && (
532
+ <div className="space-y-2">
533
+ <div>
534
+ <label className="text-xs text-gray-600">Online Amount (Partial Payment)</label>
535
+ <input
536
+ type="number"
537
+ min="0"
538
+ max={grandTotal}
539
+ step="0.01"
540
+ className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
541
+ value={onlineAmount === 0 ? '' : onlineAmount}
542
+ onChange={e => {
543
+ const val = parseFloat(e.target.value) || 0;
544
+ if (val >= 0 && val <= grandTotal) {
545
+ setOnlineAmount(val);
546
+ }
547
+ }}
548
+ onKeyDown={e => {
549
+ // Prevent special characters: -, +, e, E
550
+ if (['-', '+', 'e', 'E'].includes(e.key)) {
551
+ e.preventDefault();
552
+ }
553
+ }}
554
+ placeholder="Enter amount paid online (0 for full due)"
555
+ />
556
+ </div>
557
+ <div>
558
+ <label className="text-xs text-gray-600">Due Amount (Auto)</label>
559
+ <input
560
+ type="text"
561
+ readOnly
562
+ className="w-full bg-red-50 border border-red-200 rounded p-2 text-right font-bold text-red-600"
563
+ value={(grandTotal - onlineAmount).toFixed(2)}
564
+ />
565
+ </div>
566
+ </div>
567
+ )}
568
+
569
+ {isOverpaid && (
570
+ <div className="text-red-500 text-xs mt-2 font-medium text-center">
571
+ Error: Amount exceeds Total!
572
+ </div>
573
+ )}
574
+ </div>
575
+
576
+ <div className="flex justify-between items-center pt-2">
577
+ <span className="text-gray-600 font-medium">बाकी (Balance)</span>
578
+ <span className={`font-bold ${balance > 0 ? 'text-red-500' : 'text-green-600'}`}>₹{balance.toFixed(2)}</span>
579
+ </div>
580
+ </div>
581
+ );
582
+
583
+ return (
584
+ <div className="flex flex-col lg:flex-row gap-4 h-full p-4 bg-gray-50">
585
+ {/* Loading State */}
586
+ {isLoading && (
587
+ <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-gray-100">
588
+ <div className="text-center">
589
+ <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mb-4"></div>
590
+ <p className="text-gray-600">Loading data...</p>
591
+ </div>
592
+ </div>
593
+ )}
594
+
595
+ {/* Error State */}
596
+ {error && !isLoading && (
597
+ <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-red-200">
598
+ <div className="text-center p-8">
599
+ <div className="text-red-500 text-5xl mb-4">⚠️</div>
600
+ <h3 className="text-lg font-semibold text-gray-800 mb-2">Error Loading Data</h3>
601
+ <p className="text-gray-600 mb-4">{error}</p>
602
+ <button
603
+ onClick={() => window.location.reload()}
604
+ className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"
605
+ >
606
+ Retry
607
+ </button>
608
+ </div>
609
+ </div>
610
+ )}
611
+
612
+ {/* Main Content - Only show when not loading and no error */}
613
+ {!isLoading && !error && (
614
+ <>
615
+ {/* Left: Form */}
616
+ <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'}`}>
617
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
618
+ <h2 className={`text-lg font-bold flex items-center gap-2 ${isReturnMode ? 'text-red-600' : 'text-gray-800'}`}>
619
+ {isReturnMode ? <RotateCcw className="text-red-500" /> : <div className="text-teal-600 font-bold">IN</div>}
620
+ {isReturnMode ? 'आवक परतावा (Purchase Return)' : 'आवक बिल (Purchase)'}
621
+ </h2>
622
+
623
+ <div className="flex items-center gap-3">
624
+ <div className="flex bg-gray-100 rounded-lg p-1">
625
+ <button
626
+ onClick={() => setIsReturnMode(false)}
627
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${!isReturnMode ? 'bg-white text-teal-700 shadow-sm' : 'text-gray-500'}`}
628
+ >
629
+ Regular
630
+ </button>
631
+ <button
632
+ onClick={() => setIsReturnMode(true)}
633
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${isReturnMode ? 'bg-red-500 text-white shadow-sm' : 'text-gray-500'}`}
634
+ >
635
+ Return
636
+ </button>
637
+ </div>
638
+
639
+ <input
640
+ type="date"
641
+ className="bg-gray-100 px-3 py-1 rounded text-sm font-mono text-gray-600 border border-gray-200 hover:border-teal-400 focus:border-teal-500 focus:ring-1 focus:ring-teal-500 outline-none cursor-pointer transition-colors"
642
+ value={billDate}
643
+ onChange={(e) => setBillDate(e.target.value)}
644
+ max={new Date().toISOString().split('T')[0]}
645
+ />
646
+ </div>
647
+ </div>
648
+
649
+ {/* Header Fields */}
650
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
651
+
652
+ <div>
653
+ <label className="block text-sm font-medium text-gray-700 mb-1">पार्टी नाव (Party)</label>
654
+ <select
655
+ className="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-teal-500 outline-none"
656
+ value={selectedParty}
657
+ onChange={e => setSelectedParty(e.target.value)}
658
+ >
659
+ <option value="">Select Party</option>
660
+ {parties.map(p => (
661
+ <option key={p.id} value={p.id}>{p.name} - {p.city}</option>
662
+ ))}
663
+ </select>
664
+ </div>
665
+ </div>
666
+
667
+ {/* Items Table */}
668
+ <div className="mb-6">
669
+ <div className="flex justify-between items-center mb-2">
670
+ <h3 className="font-semibold text-gray-700">माल तपशील (Items)</h3>
671
+ </div>
672
+
673
+ <div className="space-y-4">
674
+ {items.map((item, index) => (
675
+ <div key={item.id} className="p-4 border rounded-lg bg-gray-50 relative shadow-sm">
676
+ <button
677
+ onClick={() => removeItem(item.id!)}
678
+ className="absolute top-2 right-2 text-red-400 hover:text-red-600 p-1"
679
+ >
680
+ <Trash2 size={18} />
681
+ </button>
682
+
683
+ <div className="grid grid-cols-2 md:grid-cols-6 gap-3">
684
+ <div className="col-span-2">
685
+ <label className="block text-xs font-medium text-gray-500 mb-1">मिरची जात (Type)</label>
686
+ <select
687
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
688
+ value={item.mirchi_type_id || ''}
689
+ onChange={e => handleMirchiChange(item.id!, e.target.value)}
690
+ >
691
+ <option value="">Select Type</option>
692
+ {mirchiTypes.map(m => (
693
+ <option key={m.id} value={m.id}>{m.name}</option>
694
+ ))}
695
+ </select>
696
+ </div>
697
+
698
+ {/* LOT Number Section - Dynamic based on mode */}
699
+ {item.mirchi_type_id && (
700
+ <div className="col-span-2">
701
+ <label className="block text-xs font-medium text-gray-500 mb-1">
702
+ LOT Number {isReturnMode && <span className="text-red-500">(Select to Return)</span>}
703
+ </label>
704
+ {isReturnMode ? (
705
+ // RETURN MODE: Select existing lot
706
+ <select
707
+ className="w-full border border-gray-300 rounded p-2 text-sm font-mono bg-white"
708
+ value={item.lot_id || ''}
709
+ onChange={e => {
710
+ const selectedLot = availableLots[item.id!]?.find(l => l.id === e.target.value);
711
+ if (selectedLot) {
712
+ handleItemChange(item.id!, 'lot_id', selectedLot.id);
713
+ handleItemChange(item.id!, 'lot_number', selectedLot.lot_number);
714
+ }
715
+ }}
716
+ >
717
+ <option value="">Select LOT to return</option>
718
+ {(availableLots[item.id!] || []).map(lot => (
719
+ <option key={lot.id} value={lot.id}>
720
+ {lot.lot_number} ({lot.remaining_quantity}kg available)
721
+ </option>
722
+ ))}
723
+ </select>
724
+ ) : (
725
+ // NORMAL MODE: New lot input
726
+ <input
727
+ type="text"
728
+ className="w-full border border-gray-300 rounded p-2 text-sm font-mono"
729
+ placeholder="Enter new LOT number"
730
+ value={lotInputs[item.id!] || ''}
731
+ onChange={e => {
732
+ setLotInputs({ ...lotInputs, [item.id!]: e.target.value });
733
+ handleItemChange(item.id!, 'lot_number', e.target.value);
734
+ handleItemChange(item.id!, 'is_existing_lot', false);
735
+ }}
736
+ />
737
+ )}
738
+ </div>
739
+ )}
740
+ <div className="col-span-2 md:col-span-4">
741
+ <label className="block text-xs font-medium text-gray-500 mb-1">पोटी वजन (Eg: 10,20,30)</label>
742
+ <input
743
+ type="text"
744
+ placeholder="10, 20, 30"
745
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
746
+ value={potiInputs[item.id!] || ''}
747
+ onChange={e => handlePotiInputChange(item.id!, e.target.value)}
748
+ />
749
+ </div>
750
+
751
+ <div>
752
+ <label className="block text-xs font-medium text-gray-500 mb-1">Gross</label>
753
+ <input type="number" readOnly className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm" value={item.gross_weight} />
754
+ </div>
755
+ <div>
756
+ <label className="block text-xs font-medium text-gray-500 mb-1">Poti (count)</label>
757
+ <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} />
758
+ </div>
759
+ <div>
760
+ <label className="block text-xs font-medium text-gray-500 mb-1">Net (Editable)</label>
761
+ <input
762
+ type="number"
763
+ min="0"
764
+ className="w-full bg-blue-50 border border-blue-200 rounded p-2 text-sm font-bold text-blue-800 appearance-none"
765
+ value={item.net_weight === 0 ? '' : item.net_weight}
766
+ onChange={e => handleItemChange(item.id!, 'net_weight', Math.max(0, parseFloat(e.target.value) || 0))}
767
+ placeholder="Auto-calculated"
768
+ />
769
+ </div>
770
+
771
+ <div>
772
+ <label className="block text-xs font-medium text-gray-500 mb-1">Rate ()</label>
773
+ <input
774
+ type="number"
775
+ min="0"
776
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white appearance-none"
777
+ onWheel={e => e.currentTarget.blur()}
778
+ value={item.rate_per_kg === 0 ? '' : item.rate_per_kg}
779
+ onChange={e => handleItemChange(item.id!, 'rate_per_kg', Math.max(0, parseFloat(e.target.value) || 0))}
780
+ placeholder="0"
781
+ />
782
+ </div>
783
+ <div className="col-span-2 md:col-span-2">
784
+ <label className="block text-xs font-medium text-gray-500 mb-1">Total (₹)</label>
785
+ <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)} />
786
+ </div>
787
+ </div>
788
+ </div>
789
+ ))}
790
+ </div>
791
+ <button
792
+ onClick={addItem}
793
+ 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"
794
+ >
795
+ <Plus size={18} /> Add New Item (नवीन माल)
796
+ </button>
797
+ </div>
798
+ </div>
799
+
800
+ {/* Desktop: Right Summary Sidebar */}
801
+ <div className="hidden lg:flex w-80 flex-col gap-4">
802
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-0">
803
+ <h3 className="font-bold text-gray-800 mb-4 border-b pb-2">बिल सारांश (Summary)</h3>
804
+ {SummaryContent()}
805
+ <div className="flex gap-2 mt-6">
806
+ {savedTransaction && (
807
+ <>
808
+ <PrintInvoice transaction={savedTransaction} />
809
+ <PdfInvoice transaction={savedTransaction} />
810
+ </>
811
+ )}
812
+ <button
813
+ onClick={handleSubmit}
814
+ disabled={isSubmitting || items.length === 0}
815
+ className={`flex-1 bg-teal-600 text-white py-3 rounded-lg font-bold shadow-lg hover:bg-teal-700 transition-all flex items-center justify-center gap-2 ${isSubmitting || items.length === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
816
+ >
817
+ <Save size={20} />
818
+ {isSubmitting ? 'Saving...' : 'Save Bill'}
819
+ </button>
820
+ </div>
821
+ </div>
822
+ </div>
823
+
824
+ {/* Mobile: Sticky Bottom Action Bar */}
825
+ <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">
826
+ <div className="flex items-center justify-between p-4 bg-white z-20 relative">
827
+ <div
828
+ onClick={() => setShowMobileSummary(!showMobileSummary)}
829
+ className="flex flex-col cursor-pointer"
830
+ >
831
+ <div className="flex items-center gap-1 text-gray-500 text-xs font-medium uppercase tracking-wide">
832
+ Summary {showMobileSummary ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
833
+ </div>
834
+ <div className="text-xl font-bold text-red-600">
835
+ ₹{grandTotal.toFixed(2)}
836
+ </div>
837
+ </div>
838
+ <button
839
+ onClick={handleSubmit}
840
+ disabled={isSubmitting}
841
+ 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"
842
+ >
843
+ <Save size={18} /> {isSubmitting ? '...' : 'Save'}
844
+ </button>
845
+ </div>
846
+
847
+ {showMobileSummary && (
848
+ <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">
849
+ <div className="mt-4">
850
+ {SummaryContent()}
851
+ </div>
852
+ {savedTransaction && (
853
+ <div className="mt-4 flex justify-center">
854
+ <PrintInvoice transaction={savedTransaction} />
855
+ </div>
856
+ )}
857
+ </div>
858
+ )}
859
+ </div>
860
+ </>
861
+ )}
862
+ </div>
863
+ );
864
+ };
865
+
866
  export default AwaakBill;
pages/Dashboard.tsx CHANGED
@@ -1,400 +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;
 
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 CHANGED
@@ -1,795 +1,807 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { useNavigate } from 'react-router-dom';
3
- import {
4
- getParties, getMirchiTypes, generateBillNumber, saveTransaction,
5
- getAvailableLotsByMirchi
6
- } from '../services/db';
7
- import {
8
- Party, MirchiType, Lot, Transaction, TransactionItem, BillType, PaymentMode, PartyType
9
- } from '../types';
10
- import { Save, Trash2, Plus, RotateCcw, Printer, Download, ChevronUp, ChevronDown } from 'lucide-react';
11
- import PrintInvoice from '../components/PrintInvoice';
12
- import PdfInvoice from '../components/PdfInvoice';
13
-
14
- const SummaryInput = ({ label, value, onChange }: { label: string, value: number, onChange: (val: number) => void }) => (
15
- <div className="flex justify-between items-center mb-1 bg-gray-50 p-1.5 rounded">
16
- <span className="text-gray-600 text-xs">{label}</span>
17
- <input
18
- type="number"
19
- step="0.01"
20
- min="0"
21
- className="w-24 border rounded text-right p-1 text-sm bg-white focus:ring-1 focus:ring-teal-500 outline-none appearance-none"
22
- value={value === 0 ? '' : value}
23
- onChange={e => {
24
- const parsed = parseFloat(e.target.value);
25
- onChange(isNaN(parsed) ? 0 : Math.max(0, parsed));
26
- }}
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
- // LOT selection state
63
- const [availableLots, setAvailableLots] = useState<{ [key: string]: Lot[] }>({});
64
-
65
- const [expenses, setExpenses] = useState({
66
- cess_percent: 1.0,
67
- cess_amount: 0,
68
- adat_percent: 3.0,
69
- adat_amount: 0,
70
- poti_rate: 18, // Default rate
71
- poti_amount: 0,
72
- hamali_per_poti: 6,
73
- hamali_amount: 0,
74
- packaging_hamali_per_poti: 0,
75
- packaging_hamali_amount: 0,
76
- gaadi_bharni: 0,
77
- });
78
-
79
- // Payment State
80
- const [paymentMode, setPaymentMode] = useState<'cash' | 'online' | 'hybrid' | 'due'>('cash');
81
- const [paidAmount, setPaidAmount] = useState(0); // Not used directly in new logic, derived
82
- const [cashAmount, setCashAmount] = useState(0); // For hybrid
83
- const [onlineAmount, setOnlineAmount] = useState(0); // For hybrid & due
84
- const [isSubmitting, setIsSubmitting] = useState(false);
85
- const [savedTransaction, setSavedTransaction] = useState<Transaction | null>(null);
86
- const [isLoading, setIsLoading] = useState(true);
87
- const [error, setError] = useState<string | null>(null);
88
-
89
- // Initial Load & Bill Number Generation
90
- useEffect(() => {
91
- const loadData = async () => {
92
- try {
93
- setIsLoading(true);
94
- setError(null);
95
- const [partiesData, mirchiData] = await Promise.all([
96
- getParties(),
97
- getMirchiTypes()
98
- ]);
99
-
100
- if (!partiesData || partiesData.length === 0) {
101
- setError('No parties found. Please add parties in Settings first.');
102
- }
103
- if (!mirchiData || mirchiData.length === 0) {
104
- setError('No mirchi types found. Please add mirchi types in Settings first.');
105
- }
106
-
107
- // Filter parties for Jawaak bills
108
- const filteredParties = partiesData.filter(p =>
109
- p.party_type === PartyType.JAWAAK || p.party_type === PartyType.BOTH
110
- );
111
-
112
- setParties(filteredParties || []);
113
- setMirchiTypes(mirchiData || []);
114
- setBillNumber(generateBillNumber(BillType.JAWAAK, isReturnMode));
115
- } catch (err: any) {
116
- console.error('Error loading data:', err);
117
- setError('Failed to load data. Please check your connection and try again.');
118
- } finally {
119
- setIsLoading(false);
120
- }
121
- };
122
- loadData();
123
- }, [isReturnMode]);
124
-
125
- // Calculation Logic
126
- const calculateRow = (item: Partial<TransactionItem>, potiString: string) => {
127
- const weights = potiString.split(/[\s,+]+/).map(n => parseFloat(n)).filter(n => !isNaN(n) && n > 0);
128
-
129
- const gross = weights.reduce((a, b) => a + b, 0);
130
- const count = weights.length;
131
- const potya = count * 1;
132
- const net = Math.max(0, gross - potya);
133
- const total = net * (item.rate_per_kg || 0);
134
-
135
- return {
136
- ...item,
137
- poti_weights: weights,
138
- gross_weight: gross,
139
- poti_count: count,
140
- total_potya: potya,
141
- net_weight: net,
142
- item_total: total
143
- };
144
- };
145
-
146
- const handleItemChange = (id: string, field: string, value: any) => {
147
- setItems(prev => prev.map(item => {
148
- if (item.id !== id) return item;
149
-
150
- let updatedItem = { ...item, [field]: value };
151
-
152
- if (field === 'rate_per_kg') {
153
- updatedItem.item_total = (updatedItem.net_weight || 0) * Math.max(0, parseFloat(value || 0));
154
- }
155
-
156
- return updatedItem;
157
- }));
158
- };
159
-
160
- const handlePotiInputChange = (id: string, value: string) => {
161
- setPotiInputs(prev => ({ ...prev, [id]: value }));
162
- setItems(prev => prev.map(item => {
163
- if (item.id !== id) return item;
164
- return calculateRow(item, value);
165
- }));
166
- };
167
-
168
- // Handle mirchi type change - load available lots
169
- const handleMirchiChange = async (id: string, mirchiTypeId: string) => {
170
- handleItemChange(id, 'mirchi_type_id', mirchiTypeId);
171
-
172
- // Load available lots for this mirchi type
173
- if (mirchiTypeId) {
174
- const lots = await getAvailableLotsByMirchi(mirchiTypeId);
175
- setAvailableLots(prev => ({ ...prev, [id]: lots }));
176
- }
177
- };
178
-
179
- // Handle LOT selection
180
- const handleLotSelection = (id: string, lotId: string) => {
181
- handleItemChange(id, 'lot_id', lotId);
182
-
183
- // Find the selected lot to get its details
184
- const lots = availableLots[id] || [];
185
- const selectedLot = lots.find(lot => lot.id === lotId);
186
- if (selectedLot) {
187
- handleItemChange(id, 'lot_number', selectedLot.lot_number);
188
- }
189
- };
190
-
191
- const addItem = () => {
192
- setItems(prev => [...prev, {
193
- id: Date.now().toString(),
194
- poti_weights: [],
195
- gross_weight: 0,
196
- poti_count: 0,
197
- total_potya: 0,
198
- net_weight: 0,
199
- rate_per_kg: 0,
200
- item_total: 0
201
- }]);
202
- };
203
-
204
- const removeItem = (id: string) => {
205
- if (items.length > 1) {
206
- setItems(prev => prev.filter(i => i.id !== id));
207
- const newInputs = { ...potiInputs };
208
- delete newInputs[id];
209
- setPotiInputs(newInputs);
210
- }
211
- };
212
-
213
- // Totals Calculation
214
- const subtotal = items.reduce((acc, item) => acc + (item.item_total || 0), 0);
215
- const totalPoti = items.reduce((acc, item) => acc + (item.poti_count || 0), 0);
216
-
217
- // Derived Expenses - New sequence
218
- const potiAmt = totalPoti * expenses.poti_rate;
219
- const baseForCess = subtotal + potiAmt; // Cess calculated on subtotal + poti
220
- const cessAmt = (baseForCess * expenses.cess_percent) / 100;
221
- const adatAmt = (subtotal * expenses.adat_percent) / 100;
222
- const hamaliAmt = totalPoti * expenses.hamali_per_poti;
223
- const packagingHamaliAmt = totalPoti * expenses.packaging_hamali_per_poti;
224
- const totalExp = potiAmt + cessAmt + adatAmt + hamaliAmt + packagingHamaliAmt + (expenses.gaadi_bharni || 0);
225
- const grandTotal = subtotal + totalExp;
226
-
227
- // Calculate Payment Details based on Mode
228
- let finalCash = 0;
229
- let finalOnline = 0;
230
- let currentPaid = 0;
231
-
232
- if (paymentMode === 'cash') {
233
- finalCash = grandTotal;
234
- finalOnline = 0;
235
- currentPaid = grandTotal;
236
- } else if (paymentMode === 'online') {
237
- finalCash = 0;
238
- finalOnline = grandTotal;
239
- currentPaid = grandTotal;
240
- } else if (paymentMode === 'hybrid') {
241
- finalCash = cashAmount;
242
- finalOnline = Math.max(0, grandTotal - cashAmount);
243
- currentPaid = grandTotal; // Hybrid assumes full payment
244
- } else if (paymentMode === 'due') {
245
- finalCash = 0;
246
- finalOnline = onlineAmount; // User defined
247
- currentPaid = onlineAmount;
248
- }
249
-
250
- const balance = grandTotal - currentPaid;
251
-
252
- // Validation Error
253
- // Hybrid: Cash cannot exceed Total
254
- // Due: Online cannot exceed Total
255
- const isOverpaid = (paymentMode === 'hybrid' && cashAmount > grandTotal) ||
256
- (paymentMode === 'due' && onlineAmount > grandTotal);
257
-
258
- const validateForm = () => {
259
- if (!selectedParty) {
260
- alert('Please select a Party (पार्टी निवडा)');
261
- return false;
262
- }
263
- for (let i = 0; i < items.length; i++) {
264
- const item = items[i];
265
- if (!item.mirchi_type_id) {
266
- alert(`Row ${i + 1}: Please select Mirchi Type`);
267
- return false;
268
- }
269
- if (!item.poti_weights || item.poti_weights.length === 0) {
270
- alert(`Row ${i + 1}: Please enter weights (e.g. 10, 20)`);
271
- return false;
272
- }
273
- if (!item.rate_per_kg || item.rate_per_kg <= 0) {
274
- alert(`Row ${i + 1}: Rate must be greater than 0`);
275
- return false;
276
- }
277
- }
278
- if (isOverpaid) {
279
- alert('Paid amount cannot be greater than Total Amount');
280
- return false;
281
- }
282
- return true;
283
- };
284
-
285
- const handleSubmit = async () => {
286
- if (isSubmitting) return;
287
- if (!validateForm()) return;
288
-
289
- setIsSubmitting(true);
290
-
291
- // Populate mirchi_name for each item from mirchiTypes
292
- const itemsWithNames = items.map(item => ({
293
- ...item,
294
- mirchi_name: mirchiTypes.find(m => m.id === item.mirchi_type_id)?.name || 'Unknown'
295
- }));
296
-
297
- const transaction: Transaction = {
298
- id: Date.now().toString(),
299
- bill_number: billNumber,
300
- bill_date: billDate,
301
- bill_type: BillType.JAWAAK,
302
- is_return: isReturnMode,
303
- party_id: selectedParty,
304
- party_name: parties.find(p => p.id === selectedParty)?.name,
305
- items: itemsWithNames as TransactionItem[],
306
- expenses: {
307
- ...expenses,
308
- cess_amount: cessAmt,
309
- adat_amount: adatAmt,
310
- poti_amount: potiAmt,
311
- hamali_amount: hamaliAmt,
312
- packaging_hamali_amount: packagingHamaliAmt
313
- },
314
- payments: [
315
- ...(finalCash > 0 ? [{ mode: PaymentMode.CASH, amount: finalCash }] : []),
316
- ...(finalOnline > 0 ? [{ mode: PaymentMode.ONLINE, amount: finalOnline }] : []),
317
- ...(balance > 0 ? [{ mode: PaymentMode.DUE, amount: balance }] : []) // Optional: Track due as a payment entry or just balance? Usually balance is enough. Keeping logic simple.
318
- ],
319
- gross_weight_total: items.reduce((a, i) => a + (i.gross_weight || 0), 0),
320
- net_weight_total: items.reduce((a, i) => a + (i.net_weight || 0), 0),
321
- subtotal: subtotal,
322
- total_expenses: totalExp,
323
- total_amount: grandTotal,
324
- paid_amount: currentPaid,
325
- balance_amount: balance
326
- };
327
-
328
- const result = await saveTransaction(transaction);
329
- if (result.success) {
330
- // Use the complete transaction object we created, not just the API response
331
- // This ensures all items are included for printing
332
- const completeTransaction = result.data ? {
333
- ...transaction,
334
- ...result.data,
335
- items: transaction.items // Ensure items are preserved
336
- } : transaction;
337
- setSavedTransaction(completeTransaction);
338
- alert('Bill Saved Successfully! You can now print the invoice.');
339
- } else {
340
- alert(`Error: ${result.message}`);
341
- }
342
- setIsSubmitting(false);
343
- };
344
-
345
- const handleHybridCashChange = (val: number) => {
346
- setCashAmount(val);
347
- // Online amount is derived in render, no state update needed for it in hybrid
348
- };
349
-
350
- const SummaryContent = () => (
351
- <div className="space-y-3 text-sm">
352
- <div className="flex justify-between">
353
- <span className="text-gray-600">एकुण रक्कम (Subtotal)</span>
354
- <span className="font-semibold">₹{subtotal.toFixed(2)}</span>
355
- </div>
356
-
357
- <div className="pt-2 border-t border-dashed space-y-2">
358
- {/* 1. Poti (Bags) Rate */}
359
- <SummaryInput
360
- label={`पोती (Bags ${totalPoti}) Rate`}
361
- value={expenses.poti_rate}
362
- onChange={val => setExpenses({ ...expenses, poti_rate: val })}
363
- />
364
- <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
365
- <span>Amount:</span>
366
- <span>₹{potiAmt.toFixed(2)}</span>
367
- </div>
368
-
369
- {/* 2. Cess Tax (calculated on subtotal + poti) */}
370
- <SummaryInput
371
- label={`सेस (Cess ${expenses.cess_percent}%) on ${baseForCess.toFixed(2)}`}
372
- value={expenses.cess_percent}
373
- onChange={val => setExpenses({ ...expenses, cess_percent: val })}
374
- />
375
- <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
376
- <span>Amount:</span>
377
- <span>₹{cessAmt.toFixed(2)}</span>
378
- </div>
379
-
380
- {/* 3. Adat / Market Yard Tax */}
381
- <SummaryInput
382
- label={`अडत (Adat ${expenses.adat_percent}%)`}
383
- value={expenses.adat_percent}
384
- onChange={val => setExpenses({ ...expenses, adat_percent: val })}
385
- />
386
- <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
387
- <span>Amount:</span>
388
- <span>₹{adatAmt.toFixed(2)}</span>
389
- </div>
390
-
391
- {/* 4. Hamali */}
392
- <SummaryInput
393
- label={`हमाली (Hamali per poti)`}
394
- value={expenses.hamali_per_poti}
395
- onChange={val => setExpenses({ ...expenses, hamali_per_poti: val })}
396
- />
397
- <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
398
- <span>Amount ({expenses.hamali_per_poti} * {totalPoti}):</span>
399
- <span>₹{hamaliAmt.toFixed(2)}</span>
400
- </div>
401
-
402
- {/* 5. Packaging Hamali */}
403
- <SummaryInput
404
- label="पॅकेजिंग हमाली (Rate)"
405
- value={expenses.packaging_hamali_per_poti}
406
- onChange={val => setExpenses({ ...expenses, packaging_hamali_per_poti: val })}
407
- />
408
- <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
409
- <span>Amount ({totalPoti} * {expenses.packaging_hamali_per_poti}):</span>
410
- <span>₹{packagingHamaliAmt.toFixed(2)}</span>
411
- </div>
412
-
413
- {/* 6. Gaadi Bharni */}
414
- <SummaryInput
415
- label="गाडी भरणी"
416
- value={expenses.gaadi_bharni}
417
- onChange={val => setExpenses({ ...expenses, gaadi_bharni: val })}
418
- />
419
- </div>
420
-
421
- {/* 6. Total Price */}
422
- <div className="pt-2 border-t border-gray-200 flex justify-between items-center">
423
- <span className="text-base font-bold text-gray-800">एकुण बिल (Total)</span>
424
- <span className="text-xl font-bold text-red-600">₹{grandTotal.toFixed(2)}</span>
425
- </div>
426
-
427
- {/* Payment Section */}
428
- <div className="bg-teal-50 p-3 rounded-lg mt-2 border border-teal-100">
429
- <label className="block text-xs font-medium text-teal-800 mb-2">Payment Mode</label>
430
- <div className="flex gap-2 mb-3">
431
- {['cash', 'online', 'hybrid', 'due'].map(mode => (
432
- <button
433
- key={mode}
434
- onClick={() => {
435
- setPaymentMode(mode as any);
436
- setCashAmount(0);
437
- setOnlineAmount(0);
438
- }}
439
- className={`flex-1 py-1 text-xs font-medium rounded border ${paymentMode === mode
440
- ? 'bg-teal-600 text-white border-teal-600'
441
- : 'bg-white text-gray-600 border-gray-200'
442
- } capitalize`}
443
- >
444
- {mode}
445
- </button>
446
- ))}
447
- </div>
448
-
449
- {paymentMode === 'cash' && (
450
- <div className="text-center py-2 text-sm text-gray-600">
451
- Full Payment in Cash: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
452
- </div>
453
- )}
454
-
455
- {paymentMode === 'online' && (
456
- <div className="text-center py-2 text-sm text-gray-600">
457
- Full Payment Online: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
458
- </div>
459
- )}
460
-
461
- {paymentMode === 'hybrid' && (
462
- <div className="space-y-2">
463
- <div>
464
- <label className="text-xs text-gray-600">Cash Amount</label>
465
- <input
466
- type="number"
467
- min="0"
468
- max={grandTotal}
469
- step="0.01"
470
- className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
471
- value={cashAmount === 0 ? '' : cashAmount}
472
- onChange={e => {
473
- const val = parseFloat(e.target.value) || 0;
474
- if (val >= 0 && val <= grandTotal) {
475
- handleHybridCashChange(val);
476
- }
477
- }}
478
- onKeyDown={e => {
479
- // Prevent special characters: -, +, e, E
480
- if (['-', '+', 'e', 'E'].includes(e.key)) {
481
- e.preventDefault();
482
- }
483
- }}
484
- placeholder="Cash"
485
- />
486
- </div>
487
- <div>
488
- <label className="text-xs text-gray-600">Online Amount (Auto)</label>
489
- <input
490
- type="text"
491
- readOnly
492
- className="w-full bg-gray-100 border border-gray-200 rounded p-2 text-right font-medium text-gray-600"
493
- value={(grandTotal - cashAmount).toFixed(2)}
494
- />
495
- </div>
496
- </div>
497
- )}
498
-
499
- {paymentMode === 'due' && (
500
- <div className="space-y-2">
501
- <div>
502
- <label className="text-xs text-gray-600">Online Amount (Partial Payment)</label>
503
- <input
504
- type="number"
505
- min="0"
506
- max={grandTotal}
507
- step="0.01"
508
- className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
509
- value={onlineAmount === 0 ? '' : onlineAmount}
510
- onChange={e => {
511
- const val = parseFloat(e.target.value) || 0;
512
- if (val >= 0 && val <= grandTotal) {
513
- setOnlineAmount(val);
514
- }
515
- }}
516
- onKeyDown={e => {
517
- // Prevent special characters: -, +, e, E
518
- if (['-', '+', 'e', 'E'].includes(e.key)) {
519
- e.preventDefault();
520
- }
521
- }}
522
- placeholder="Enter amount paid online (0 for full due)"
523
- />
524
- </div>
525
- <div>
526
- <label className="text-xs text-gray-600">Due Amount (Auto)</label>
527
- <input
528
- type="text"
529
- readOnly
530
- className="w-full bg-red-50 border border-red-200 rounded p-2 text-right font-bold text-red-600"
531
- value={(grandTotal - onlineAmount).toFixed(2)}
532
- />
533
- </div>
534
- </div>
535
- )}
536
-
537
- {isOverpaid && (
538
- <div className="text-red-500 text-xs mt-2 font-medium text-center">
539
- Error: Amount exceeds Total!
540
- </div>
541
- )}
542
- </div>
543
-
544
- <div className="flex justify-between items-center pt-2">
545
- <span className="text-gray-600 font-medium">बाकी (Balance)</span>
546
- <span className={`font-bold ${balance > 0 ? 'text-red-500' : 'text-green-600'}`}>₹{balance.toFixed(2)}</span>
547
- </div>
548
- </div>
549
- );
550
-
551
- return (
552
- <div className="flex flex-col lg:flex-row gap-4 h-full p-4 bg-gray-50">
553
- {isLoading && (
554
- <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-gray-100">
555
- <div className="text-center">
556
- <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mb-4"></div>
557
- <p className="text-gray-600">Loading data...</p>
558
- </div>
559
- </div>
560
- )}
561
- {error && !isLoading && (
562
- <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-red-200">
563
- <div className="text-center p-8">
564
- <div className="text-red-500 text-5xl mb-4">⚠️</div>
565
- <h3 className="text-lg font-semibold text-gray-800 mb-2">Error Loading Data</h3>
566
- <p className="text-gray-600 mb-4">{error}</p>
567
- <button onClick={() => window.location.reload()} className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700">
568
- Retry
569
- </button>
570
- </div>
571
- </div>
572
- )}
573
- {!isLoading && !error && (
574
- <>
575
- {/* Left: Form */}
576
- <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'}`}>
577
- <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
578
- <h2 className={`text-lg font-bold flex items-center gap-2 ${isReturnMode ? 'text-red-600' : 'text-gray-800'}`}>
579
- {isReturnMode ? <RotateCcw className="text-red-500" /> : <div className="text-teal-600 font-bold">OUT</div>}
580
- {isReturnMode ? 'जावक परतावा (Sales Return)' : 'जावक बिल (Sales)'}
581
- </h2>
582
-
583
- <div className="flex items-center gap-3">
584
- <div className="flex bg-gray-100 rounded-lg p-1">
585
- <button
586
- onClick={() => setIsReturnMode(false)}
587
- className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${!isReturnMode ? 'bg-white text-teal-700 shadow-sm' : 'text-gray-500'}`}
588
- >
589
- Regular
590
- </button>
591
- <button
592
- onClick={() => setIsReturnMode(true)}
593
- className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${isReturnMode ? 'bg-red-500 text-white shadow-sm' : 'text-gray-500'}`}
594
- >
595
- Return
596
- </button>
597
- </div>
598
-
599
- <input
600
- type="date"
601
- className="bg-gray-100 px-3 py-1 rounded text-sm font-mono text-gray-600 border border-gray-200 hover:border-blue-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none cursor-pointer transition-colors"
602
- value={billDate}
603
- onChange={(e) => setBillDate(e.target.value)}
604
- max={new Date().toISOString().split('T')[0]}
605
- />
606
- </div>
607
- </div>
608
-
609
- {/* Header Fields */}
610
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
611
-
612
- <div>
613
- <label className="block text-sm font-medium text-gray-700 mb-1">पार्टी नाव (Party)</label>
614
- <select
615
- className="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-teal-500 outline-none"
616
- value={selectedParty}
617
- onChange={e => setSelectedParty(e.target.value)}
618
- >
619
- <option value="">Select Party</option>
620
- {parties.map(p => (
621
- <option key={p.id} value={p.id}>{p.name} - {p.city}</option>
622
- ))}
623
- </select>
624
- </div>
625
- </div>
626
-
627
- {/* Items Table */}
628
- <div className="mb-6">
629
- <div className="flex justify-between items-center mb-2">
630
- <h3 className="font-semibold text-gray-700">माल तपशील (Items)</h3>
631
- </div>
632
-
633
- <div className="space-y-4">
634
- {items.map((item, index) => (
635
- <div key={item.id} className="p-4 border rounded-lg bg-gray-50 relative shadow-sm">
636
- <button
637
- onClick={() => removeItem(item.id!)}
638
- className="absolute top-2 right-2 text-red-400 hover:text-red-600 p-1"
639
- >
640
- <Trash2 size={18} />
641
- </button>
642
-
643
- <div className="grid grid-cols-2 md:grid-cols-6 gap-3">
644
- <div className="col-span-2">
645
- <label className="block text-xs font-medium text-gray-500 mb-1">मिरची जात (Type)</label>
646
- <select
647
- className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
648
- value={item.mirchi_type_id || ''}
649
- onChange={e => handleMirchiChange(item.id!, e.target.value)}
650
- >
651
- <option value="">Select Type</option>
652
- {mirchiTypes.map(m => (
653
- <option key={m.id} value={m.id}>{m.name}</option>
654
- ))}
655
- </select>
656
- </div>
657
-
658
- {/* LOT Selection */}
659
- {item.mirchi_type_id && (
660
- <div className="col-span-2">
661
- <label className="block text-xs font-medium text-gray-500 mb-1">LOT Number</label>
662
- <select
663
- className="w-full border border-gray-300 rounded p-2 text-sm bg-white font-mono"
664
- value={item.lot_id || ''}
665
- onChange={e => handleLotSelection(item.id!, e.target.value)}
666
- >
667
- <option value="">Select LOT</option>
668
- {(availableLots[item.id!] || []).map(lot => (
669
- <option key={lot.id} value={lot.id}>
670
- {lot.lot_number} ({lot.remaining_quantity}kg available)
671
- </option>
672
- ))}
673
- </select>
674
- </div>
675
- )}
676
-
677
- <div className={item.mirchi_type_id ? "col-span-2" : "col-span-2 md:col-span-4"}>
678
- <label className="block text-xs font-medium text-gray-500 mb-1">पोटी वजन (Eg: 10,20,30)</label>
679
- <input
680
- type="text"
681
- placeholder="10, 20, 30"
682
- className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
683
- value={potiInputs[item.id!] || ''}
684
- onChange={e => handlePotiInputChange(item.id!, e.target.value)}
685
- />
686
- </div>
687
-
688
- <div>
689
- <label className="block text-xs font-medium text-gray-500 mb-1">Gross</label>
690
- <input type="number" readOnly className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm" value={item.gross_weight} />
691
- </div>
692
- <div>
693
- <label className="block text-xs font-medium text-gray-500 mb-1">Poti (count)</label>
694
- <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} />
695
- </div>
696
- <div>
697
- <label className="block text-xs font-medium text-gray-500 mb-1">Net</label>
698
- <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} />
699
- </div>
700
- <div>
701
- <label className="block text-xs font-medium text-gray-500 mb-1">Rate (₹)</label>
702
- <input
703
- type="number"
704
- min="0"
705
- className="w-full border border-gray-300 rounded p-2 text-sm bg-white appearance-none"
706
- onWheel={e => e.currentTarget.blur()}
707
- value={item.rate_per_kg === 0 ? '' : item.rate_per_kg}
708
- onChange={e => handleItemChange(item.id!, 'rate_per_kg', Math.max(0, parseFloat(e.target.value) || 0))}
709
- placeholder="0"
710
- />
711
- </div>
712
- <div className="col-span-2 md:col-span-2">
713
- <label className="block text-xs font-medium text-gray-500 mb-1">Total (₹)</label>
714
- <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)} />
715
- </div>
716
- </div>
717
- </div>
718
- ))}
719
- </div>
720
- <button
721
- onClick={addItem}
722
- 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"
723
- >
724
- <Plus size={18} /> Add New Item (नवीन माल)
725
- </button>
726
- </div>
727
- </div>
728
-
729
- {/* Desktop: Right Summary Sidebar */}
730
- <div className="hidden lg:flex w-80 flex-col gap-4">
731
- <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-0">
732
- <h3 className="font-bold text-gray-800 mb-4 border-b pb-2">बिल सारांश (Summary)</h3>
733
- {SummaryContent()}
734
- <div className="flex gap-2 mt-6">
735
- {savedTransaction && (
736
- <>
737
- <PrintInvoice transaction={savedTransaction} />
738
- <PdfInvoice transaction={savedTransaction} />
739
- </>
740
- )}
741
- <button
742
- onClick={handleSubmit}
743
- disabled={isSubmitting || items.length === 0}
744
- className={`flex-1 bg-teal-600 text-white py-3 rounded-lg font-bold shadow-lg hover:bg-teal-700 transition-all flex items-center justify-center gap-2 ${isSubmitting || items.length === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
745
- >
746
- <Save size={20} />
747
- {isSubmitting ? 'Saving...' : 'Save Bill'}
748
- </button>
749
- </div>
750
- </div>
751
- </div>
752
-
753
- {/* Mobile: Sticky Bottom Action Bar */}
754
- <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">
755
- <div className="flex items-center justify-between p-4 bg-white z-20 relative">
756
- <div
757
- onClick={() => setShowMobileSummary(!showMobileSummary)}
758
- className="flex flex-col cursor-pointer"
759
- >
760
- <div className="flex items-center gap-1 text-gray-500 text-xs font-medium uppercase tracking-wide">
761
- Summary {showMobileSummary ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
762
- </div>
763
- <div className="text-xl font-bold text-red-600">
764
- ₹{grandTotal.toFixed(2)}
765
- </div>
766
- </div>
767
- <button
768
- onClick={handleSubmit}
769
- disabled={isSubmitting}
770
- 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"
771
- >
772
- <Save size={18} /> {isSubmitting ? '...' : 'Save'}
773
- </button>
774
- </div>
775
-
776
- {showMobileSummary && (
777
- <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">
778
- <div className="mt-4">
779
- {SummaryContent()}
780
- </div>
781
- {savedTransaction && (
782
- <div className="mt-4 flex justify-center">
783
- <PrintInvoice transaction={savedTransaction} />
784
- </div>
785
- )}
786
- </div>
787
- )}
788
- </div>
789
- </>
790
- )}
791
- </div>
792
- );
793
- };
794
-
 
 
 
 
 
 
 
 
 
 
 
 
795
  export default JawaakBill;
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import {
4
+ getParties, getMirchiTypes, generateBillNumber, saveTransaction,
5
+ getAvailableLotsByMirchi
6
+ } from '../services/db';
7
+ import {
8
+ Party, MirchiType, Lot, Transaction, TransactionItem, BillType, PaymentMode, PartyType
9
+ } from '../types';
10
+ import { Save, Trash2, Plus, RotateCcw, Printer, Download, ChevronUp, ChevronDown } from 'lucide-react';
11
+ import PrintInvoice from '../components/PrintInvoice';
12
+ import PdfInvoice from '../components/PdfInvoice';
13
+
14
+ const SummaryInput = ({ label, value, onChange }: { label: string, value: number, onChange: (val: number) => void }) => (
15
+ <div className="flex justify-between items-center mb-1 bg-gray-50 p-1.5 rounded">
16
+ <span className="text-gray-600 text-xs">{label}</span>
17
+ <input
18
+ type="number"
19
+ step="0.01"
20
+ min="0"
21
+ className="w-24 border rounded text-right p-1 text-sm bg-white focus:ring-1 focus:ring-teal-500 outline-none appearance-none"
22
+ value={value === 0 ? '' : value}
23
+ onChange={e => {
24
+ const parsed = parseFloat(e.target.value);
25
+ onChange(isNaN(parsed) ? 0 : Math.max(0, parsed));
26
+ }}
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
+ // LOT selection state
63
+ const [availableLots, setAvailableLots] = useState<{ [key: string]: Lot[] }>({});
64
+
65
+ const [expenses, setExpenses] = useState({
66
+ cess_percent: 1.0,
67
+ cess_amount: 0,
68
+ adat_percent: 3.0,
69
+ adat_amount: 0,
70
+ poti_rate: 18, // Default rate
71
+ poti_amount: 0,
72
+ hamali_per_poti: 6,
73
+ hamali_amount: 0,
74
+ packaging_hamali_per_poti: 0,
75
+ packaging_hamali_amount: 0,
76
+ gaadi_bharni: 0,
77
+ other_expenses: 0,
78
+ });
79
+
80
+ // Payment State
81
+ const [paymentMode, setPaymentMode] = useState<'cash' | 'online' | 'hybrid' | 'due'>('cash');
82
+ const [paidAmount, setPaidAmount] = useState(0); // Not used directly in new logic, derived
83
+ const [cashAmount, setCashAmount] = useState(0); // For hybrid
84
+ const [onlineAmount, setOnlineAmount] = useState(0); // For hybrid & due
85
+ const [isSubmitting, setIsSubmitting] = useState(false);
86
+ const [savedTransaction, setSavedTransaction] = useState<Transaction | null>(null);
87
+ const [isLoading, setIsLoading] = useState(true);
88
+ const [error, setError] = useState<string | null>(null);
89
+
90
+ // Initial Load & Bill Number Generation
91
+ useEffect(() => {
92
+ const loadData = async () => {
93
+ try {
94
+ setIsLoading(true);
95
+ setError(null);
96
+ const [partiesData, mirchiData] = await Promise.all([
97
+ getParties(),
98
+ getMirchiTypes()
99
+ ]);
100
+
101
+ if (!partiesData || partiesData.length === 0) {
102
+ setError('No parties found. Please add parties in Settings first.');
103
+ }
104
+ if (!mirchiData || mirchiData.length === 0) {
105
+ setError('No mirchi types found. Please add mirchi types in Settings first.');
106
+ }
107
+
108
+ // Filter parties for Jawaak bills
109
+ const filteredParties = partiesData.filter(p =>
110
+ p.party_type === PartyType.JAWAAK || p.party_type === PartyType.BOTH
111
+ );
112
+
113
+ setParties(filteredParties || []);
114
+ setMirchiTypes(mirchiData || []);
115
+ setBillNumber(generateBillNumber(BillType.JAWAAK, isReturnMode));
116
+ } catch (err: any) {
117
+ console.error('Error loading data:', err);
118
+ setError('Failed to load data. Please check your connection and try again.');
119
+ } finally {
120
+ setIsLoading(false);
121
+ }
122
+ };
123
+ loadData();
124
+ }, [isReturnMode]);
125
+
126
+ // Calculation Logic
127
+ const calculateRow = (item: Partial<TransactionItem>, potiString: string) => {
128
+ const weights = potiString.split(/[\s,+]+/).map(n => parseFloat(n)).filter(n => !isNaN(n) && n > 0);
129
+
130
+ const gross = weights.reduce((a, b) => a + b, 0);
131
+ const count = weights.length;
132
+ const potya = count * 1;
133
+ const net = Math.max(0, gross - potya);
134
+ const total = net * (item.rate_per_kg || 0);
135
+
136
+ return {
137
+ ...item,
138
+ poti_weights: weights,
139
+ gross_weight: gross,
140
+ poti_count: count,
141
+ total_potya: potya,
142
+ net_weight: net,
143
+ item_total: total
144
+ };
145
+ };
146
+
147
+ const handleItemChange = (id: string, field: string, value: any) => {
148
+ setItems(prev => prev.map(item => {
149
+ if (item.id !== id) return item;
150
+
151
+ let updatedItem = { ...item, [field]: value };
152
+
153
+ if (field === 'rate_per_kg') {
154
+ updatedItem.item_total = (updatedItem.net_weight || 0) * Math.max(0, parseFloat(value || 0));
155
+ }
156
+
157
+ return updatedItem;
158
+ }));
159
+ };
160
+
161
+ const handlePotiInputChange = (id: string, value: string) => {
162
+ setPotiInputs(prev => ({ ...prev, [id]: value }));
163
+ setItems(prev => prev.map(item => {
164
+ if (item.id !== id) return item;
165
+ return calculateRow(item, value);
166
+ }));
167
+ };
168
+
169
+ // Handle mirchi type change - load available lots
170
+ const handleMirchiChange = async (id: string, mirchiTypeId: string) => {
171
+ handleItemChange(id, 'mirchi_type_id', mirchiTypeId);
172
+
173
+ // Load available lots for this mirchi type
174
+ if (mirchiTypeId) {
175
+ const lots = await getAvailableLotsByMirchi(mirchiTypeId);
176
+ setAvailableLots(prev => ({ ...prev, [id]: lots }));
177
+ }
178
+ };
179
+
180
+ // Handle LOT selection
181
+ const handleLotSelection = (id: string, lotId: string) => {
182
+ handleItemChange(id, 'lot_id', lotId);
183
+
184
+ // Find the selected lot to get its details
185
+ const lots = availableLots[id] || [];
186
+ const selectedLot = lots.find(lot => lot.id === lotId);
187
+ if (selectedLot) {
188
+ handleItemChange(id, 'lot_number', selectedLot.lot_number);
189
+ }
190
+ };
191
+
192
+ const addItem = () => {
193
+ setItems(prev => [...prev, {
194
+ id: Date.now().toString(),
195
+ poti_weights: [],
196
+ gross_weight: 0,
197
+ poti_count: 0,
198
+ total_potya: 0,
199
+ net_weight: 0,
200
+ rate_per_kg: 0,
201
+ item_total: 0
202
+ }]);
203
+ };
204
+
205
+ const removeItem = (id: string) => {
206
+ if (items.length > 1) {
207
+ setItems(prev => prev.filter(i => i.id !== id));
208
+ const newInputs = { ...potiInputs };
209
+ delete newInputs[id];
210
+ setPotiInputs(newInputs);
211
+ }
212
+ };
213
+
214
+ // Totals Calculation
215
+ const subtotal = items.reduce((acc, item) => acc + (item.item_total || 0), 0);
216
+ const totalPoti = items.reduce((acc, item) => acc + (item.poti_count || 0), 0);
217
+
218
+ // Derived Expenses - New sequence
219
+ const potiAmt = totalPoti * expenses.poti_rate;
220
+ const baseForCess = subtotal + potiAmt; // Cess calculated on subtotal + poti
221
+ const cessAmt = (baseForCess * expenses.cess_percent) / 100;
222
+ const adatAmt = (subtotal * expenses.adat_percent) / 100;
223
+ const hamaliAmt = totalPoti * expenses.hamali_per_poti;
224
+ const packagingHamaliAmt = totalPoti * expenses.packaging_hamali_per_poti;
225
+ const totalExp = potiAmt + cessAmt + adatAmt + hamaliAmt + packagingHamaliAmt + (expenses.gaadi_bharni || 0) + (expenses.other_expenses || 0);
226
+ const grandTotal = subtotal + totalExp;
227
+
228
+ // Calculate Payment Details based on Mode
229
+ let finalCash = 0;
230
+ let finalOnline = 0;
231
+ let currentPaid = 0;
232
+
233
+ if (paymentMode === 'cash') {
234
+ finalCash = grandTotal;
235
+ finalOnline = 0;
236
+ currentPaid = grandTotal;
237
+ } else if (paymentMode === 'online') {
238
+ finalCash = 0;
239
+ finalOnline = grandTotal;
240
+ currentPaid = grandTotal;
241
+ } else if (paymentMode === 'hybrid') {
242
+ finalCash = cashAmount;
243
+ finalOnline = Math.max(0, grandTotal - cashAmount);
244
+ currentPaid = grandTotal; // Hybrid assumes full payment
245
+ } else if (paymentMode === 'due') {
246
+ finalCash = 0;
247
+ finalOnline = onlineAmount; // User defined
248
+ currentPaid = onlineAmount;
249
+ }
250
+
251
+ const balance = grandTotal - currentPaid;
252
+
253
+ // Validation Error
254
+ // Hybrid: Cash cannot exceed Total
255
+ // Due: Online cannot exceed Total
256
+ const isOverpaid = (paymentMode === 'hybrid' && cashAmount > grandTotal) ||
257
+ (paymentMode === 'due' && onlineAmount > grandTotal);
258
+
259
+ const validateForm = () => {
260
+ if (!selectedParty) {
261
+ alert('Please select a Party (पार्टी निवडा)');
262
+ return false;
263
+ }
264
+ for (let i = 0; i < items.length; i++) {
265
+ const item = items[i];
266
+ if (!item.mirchi_type_id) {
267
+ alert(`Row ${i + 1}: Please select Mirchi Type`);
268
+ return false;
269
+ }
270
+ if (!item.poti_weights || item.poti_weights.length === 0) {
271
+ alert(`Row ${i + 1}: Please enter weights (e.g. 10, 20)`);
272
+ return false;
273
+ }
274
+ if (!item.rate_per_kg || item.rate_per_kg <= 0) {
275
+ alert(`Row ${i + 1}: Rate must be greater than 0`);
276
+ return false;
277
+ }
278
+ }
279
+ if (isOverpaid) {
280
+ alert('Paid amount cannot be greater than Total Amount');
281
+ return false;
282
+ }
283
+ return true;
284
+ };
285
+
286
+ const handleSubmit = async () => {
287
+ if (isSubmitting) return;
288
+ if (!validateForm()) return;
289
+
290
+ setIsSubmitting(true);
291
+
292
+ // Populate mirchi_name for each item from mirchiTypes
293
+ const itemsWithNames = items.map(item => ({
294
+ ...item,
295
+ mirchi_name: mirchiTypes.find(m => m.id === item.mirchi_type_id)?.name || 'Unknown'
296
+ }));
297
+
298
+ const transaction: Transaction = {
299
+ id: Date.now().toString(),
300
+ bill_number: billNumber,
301
+ bill_date: billDate,
302
+ bill_type: BillType.JAWAAK,
303
+ is_return: isReturnMode,
304
+ party_id: selectedParty,
305
+ party_name: parties.find(p => p.id === selectedParty)?.name,
306
+ items: itemsWithNames as TransactionItem[],
307
+ expenses: {
308
+ ...expenses,
309
+ cess_amount: cessAmt,
310
+ adat_amount: adatAmt,
311
+ poti_amount: potiAmt,
312
+ hamali_amount: hamaliAmt,
313
+ packaging_hamali_amount: packagingHamaliAmt
314
+ },
315
+ payments: [
316
+ ...(finalCash > 0 ? [{ mode: PaymentMode.CASH, amount: finalCash }] : []),
317
+ ...(finalOnline > 0 ? [{ mode: PaymentMode.ONLINE, amount: finalOnline }] : []),
318
+ ...(balance > 0 ? [{ mode: PaymentMode.DUE, amount: balance }] : []) // Optional: Track due as a payment entry or just balance? Usually balance is enough. Keeping logic simple.
319
+ ],
320
+ gross_weight_total: items.reduce((a, i) => a + (i.gross_weight || 0), 0),
321
+ net_weight_total: items.reduce((a, i) => a + (i.net_weight || 0), 0),
322
+ subtotal: subtotal,
323
+ total_expenses: totalExp,
324
+ total_amount: grandTotal,
325
+ paid_amount: currentPaid,
326
+ balance_amount: balance
327
+ };
328
+
329
+ const result = await saveTransaction(transaction);
330
+ if (result.success) {
331
+ // Use the complete transaction object we created, not just the API response
332
+ // This ensures all items are included for printing
333
+ const completeTransaction = result.data ? {
334
+ ...transaction,
335
+ ...result.data,
336
+ items: transaction.items, // Ensure items are preserved
337
+ expenses: {
338
+ ...result.data.expenses,
339
+ other_expenses: transaction.expenses.other_expenses,
340
+ },
341
+ } : transaction;
342
+ setSavedTransaction(completeTransaction);
343
+ alert('Bill Saved Successfully! You can now print the invoice.');
344
+ } else {
345
+ alert(`Error: ${result.message}`);
346
+ }
347
+ setIsSubmitting(false);
348
+ };
349
+
350
+ const handleHybridCashChange = (val: number) => {
351
+ setCashAmount(val);
352
+ // Online amount is derived in render, no state update needed for it in hybrid
353
+ };
354
+
355
+ const SummaryContent = () => (
356
+ <div className="space-y-3 text-sm">
357
+ <div className="flex justify-between">
358
+ <span className="text-gray-600">एकुण रक्कम (Subtotal)</span>
359
+ <span className="font-semibold">₹{subtotal.toFixed(2)}</span>
360
+ </div>
361
+
362
+ <div className="pt-2 border-t border-dashed space-y-2">
363
+ {/* 1. Poti (Bags) Rate */}
364
+ <SummaryInput
365
+ label={`पोती (Bags ${totalPoti}) Rate`}
366
+ value={expenses.poti_rate}
367
+ onChange={val => setExpenses({ ...expenses, poti_rate: val })}
368
+ />
369
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
370
+ <span>Amount:</span>
371
+ <span>₹{potiAmt.toFixed(2)}</span>
372
+ </div>
373
+
374
+ {/* 2. Cess Tax (calculated on subtotal + poti) */}
375
+ <SummaryInput
376
+ label={`सेस (Cess ${expenses.cess_percent}%) on ₹${baseForCess.toFixed(2)}`}
377
+ value={expenses.cess_percent}
378
+ onChange={val => setExpenses({ ...expenses, cess_percent: val })}
379
+ />
380
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
381
+ <span>Amount:</span>
382
+ <span>₹{cessAmt.toFixed(2)}</span>
383
+ </div>
384
+
385
+ {/* 3. Adat / Market Yard Tax */}
386
+ <SummaryInput
387
+ label={`अडत (Adat ${expenses.adat_percent}%)`}
388
+ value={expenses.adat_percent}
389
+ onChange={val => setExpenses({ ...expenses, adat_percent: val })}
390
+ />
391
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
392
+ <span>Amount:</span>
393
+ <span>₹{adatAmt.toFixed(2)}</span>
394
+ </div>
395
+
396
+ {/* 4. Hamali */}
397
+ <SummaryInput
398
+ label={`हमाली (Hamali per poti)`}
399
+ value={expenses.hamali_per_poti}
400
+ onChange={val => setExpenses({ ...expenses, hamali_per_poti: val })}
401
+ />
402
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
403
+ <span>Amount ({expenses.hamali_per_poti} * {totalPoti}):</span>
404
+ <span>₹{hamaliAmt.toFixed(2)}</span>
405
+ </div>
406
+
407
+ {/* 5. Packaging Hamali */}
408
+ <SummaryInput
409
+ label="पॅकेजिंग हमाली (Rate)"
410
+ value={expenses.packaging_hamali_per_poti}
411
+ onChange={val => setExpenses({ ...expenses, packaging_hamali_per_poti: val })}
412
+ />
413
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
414
+ <span>Amount ({totalPoti} * {expenses.packaging_hamali_per_poti}):</span>
415
+ <span>₹{packagingHamaliAmt.toFixed(2)}</span>
416
+ </div>
417
+
418
+ {/* 6. Gaadi Bharni */}
419
+ <SummaryInput
420
+ label="गाडी भरणी"
421
+ value={expenses.gaadi_bharni}
422
+ onChange={val => setExpenses({ ...expenses, gaadi_bharni: val })}
423
+ />
424
+
425
+ {/* 7. Other Expenses */}
426
+ <SummaryInput
427
+ label="Other Expenses"
428
+ value={expenses.other_expenses}
429
+ onChange={val => setExpenses({ ...expenses, other_expenses: val })}
430
+ />
431
+ </div>
432
+
433
+ {/* 8. Total Price */}
434
+ <div className="pt-2 border-t border-gray-200 flex justify-between items-center">
435
+ <span className="text-base font-bold text-gray-800">एकुण बिल (Total)</span>
436
+ <span className="text-xl font-bold text-red-600">₹{grandTotal.toFixed(2)}</span>
437
+ </div>
438
+
439
+ {/* Payment Section */}
440
+ <div className="bg-teal-50 p-3 rounded-lg mt-2 border border-teal-100">
441
+ <label className="block text-xs font-medium text-teal-800 mb-2">Payment Mode</label>
442
+ <div className="flex gap-2 mb-3">
443
+ {['cash', 'online', 'hybrid', 'due'].map(mode => (
444
+ <button
445
+ key={mode}
446
+ onClick={() => {
447
+ setPaymentMode(mode as any);
448
+ setCashAmount(0);
449
+ setOnlineAmount(0);
450
+ }}
451
+ className={`flex-1 py-1 text-xs font-medium rounded border ${paymentMode === mode
452
+ ? 'bg-teal-600 text-white border-teal-600'
453
+ : 'bg-white text-gray-600 border-gray-200'
454
+ } capitalize`}
455
+ >
456
+ {mode}
457
+ </button>
458
+ ))}
459
+ </div>
460
+
461
+ {paymentMode === 'cash' && (
462
+ <div className="text-center py-2 text-sm text-gray-600">
463
+ Full Payment in Cash: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
464
+ </div>
465
+ )}
466
+
467
+ {paymentMode === 'online' && (
468
+ <div className="text-center py-2 text-sm text-gray-600">
469
+ Full Payment Online: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
470
+ </div>
471
+ )}
472
+
473
+ {paymentMode === 'hybrid' && (
474
+ <div className="space-y-2">
475
+ <div>
476
+ <label className="text-xs text-gray-600">Cash Amount</label>
477
+ <input
478
+ type="number"
479
+ min="0"
480
+ max={grandTotal}
481
+ step="0.01"
482
+ className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
483
+ value={cashAmount === 0 ? '' : cashAmount}
484
+ onChange={e => {
485
+ const val = parseFloat(e.target.value) || 0;
486
+ if (val >= 0 && val <= grandTotal) {
487
+ handleHybridCashChange(val);
488
+ }
489
+ }}
490
+ onKeyDown={e => {
491
+ // Prevent special characters: -, +, e, E
492
+ if (['-', '+', 'e', 'E'].includes(e.key)) {
493
+ e.preventDefault();
494
+ }
495
+ }}
496
+ placeholder="Cash"
497
+ />
498
+ </div>
499
+ <div>
500
+ <label className="text-xs text-gray-600">Online Amount (Auto)</label>
501
+ <input
502
+ type="text"
503
+ readOnly
504
+ className="w-full bg-gray-100 border border-gray-200 rounded p-2 text-right font-medium text-gray-600"
505
+ value={(grandTotal - cashAmount).toFixed(2)}
506
+ />
507
+ </div>
508
+ </div>
509
+ )}
510
+
511
+ {paymentMode === 'due' && (
512
+ <div className="space-y-2">
513
+ <div>
514
+ <label className="text-xs text-gray-600">Online Amount (Partial Payment)</label>
515
+ <input
516
+ type="number"
517
+ min="0"
518
+ max={grandTotal}
519
+ step="0.01"
520
+ className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
521
+ value={onlineAmount === 0 ? '' : onlineAmount}
522
+ onChange={e => {
523
+ const val = parseFloat(e.target.value) || 0;
524
+ if (val >= 0 && val <= grandTotal) {
525
+ setOnlineAmount(val);
526
+ }
527
+ }}
528
+ onKeyDown={e => {
529
+ // Prevent special characters: -, +, e, E
530
+ if (['-', '+', 'e', 'E'].includes(e.key)) {
531
+ e.preventDefault();
532
+ }
533
+ }}
534
+ placeholder="Enter amount paid online (0 for full due)"
535
+ />
536
+ </div>
537
+ <div>
538
+ <label className="text-xs text-gray-600">Due Amount (Auto)</label>
539
+ <input
540
+ type="text"
541
+ readOnly
542
+ className="w-full bg-red-50 border border-red-200 rounded p-2 text-right font-bold text-red-600"
543
+ value={(grandTotal - onlineAmount).toFixed(2)}
544
+ />
545
+ </div>
546
+ </div>
547
+ )}
548
+
549
+ {isOverpaid && (
550
+ <div className="text-red-500 text-xs mt-2 font-medium text-center">
551
+ Error: Amount exceeds Total!
552
+ </div>
553
+ )}
554
+ </div>
555
+
556
+ <div className="flex justify-between items-center pt-2">
557
+ <span className="text-gray-600 font-medium">बाकी (Balance)</span>
558
+ <span className={`font-bold ${balance > 0 ? 'text-red-500' : 'text-green-600'}`}>₹{balance.toFixed(2)}</span>
559
+ </div>
560
+ </div>
561
+ );
562
+
563
+ return (
564
+ <div className="flex flex-col lg:flex-row gap-4 h-full p-4 bg-gray-50">
565
+ {isLoading && (
566
+ <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-gray-100">
567
+ <div className="text-center">
568
+ <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mb-4"></div>
569
+ <p className="text-gray-600">Loading data...</p>
570
+ </div>
571
+ </div>
572
+ )}
573
+ {error && !isLoading && (
574
+ <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-red-200">
575
+ <div className="text-center p-8">
576
+ <div className="text-red-500 text-5xl mb-4">⚠️</div>
577
+ <h3 className="text-lg font-semibold text-gray-800 mb-2">Error Loading Data</h3>
578
+ <p className="text-gray-600 mb-4">{error}</p>
579
+ <button onClick={() => window.location.reload()} className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700">
580
+ Retry
581
+ </button>
582
+ </div>
583
+ </div>
584
+ )}
585
+ {!isLoading && !error && (
586
+ <>
587
+ {/* Left: Form */}
588
+ <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'}`}>
589
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
590
+ <h2 className={`text-lg font-bold flex items-center gap-2 ${isReturnMode ? 'text-red-600' : 'text-gray-800'}`}>
591
+ {isReturnMode ? <RotateCcw className="text-red-500" /> : <div className="text-teal-600 font-bold">OUT</div>}
592
+ {isReturnMode ? 'जावक परतावा (Sales Return)' : 'जावक बिल (Sales)'}
593
+ </h2>
594
+
595
+ <div className="flex items-center gap-3">
596
+ <div className="flex bg-gray-100 rounded-lg p-1">
597
+ <button
598
+ onClick={() => setIsReturnMode(false)}
599
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${!isReturnMode ? 'bg-white text-teal-700 shadow-sm' : 'text-gray-500'}`}
600
+ >
601
+ Regular
602
+ </button>
603
+ <button
604
+ onClick={() => setIsReturnMode(true)}
605
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${isReturnMode ? 'bg-red-500 text-white shadow-sm' : 'text-gray-500'}`}
606
+ >
607
+ Return
608
+ </button>
609
+ </div>
610
+
611
+ <input
612
+ type="date"
613
+ className="bg-gray-100 px-3 py-1 rounded text-sm font-mono text-gray-600 border border-gray-200 hover:border-blue-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none cursor-pointer transition-colors"
614
+ value={billDate}
615
+ onChange={(e) => setBillDate(e.target.value)}
616
+ max={new Date().toISOString().split('T')[0]}
617
+ />
618
+ </div>
619
+ </div>
620
+
621
+ {/* Header Fields */}
622
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
623
+
624
+ <div>
625
+ <label className="block text-sm font-medium text-gray-700 mb-1">पार्टी नाव (Party)</label>
626
+ <select
627
+ className="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-teal-500 outline-none"
628
+ value={selectedParty}
629
+ onChange={e => setSelectedParty(e.target.value)}
630
+ >
631
+ <option value="">Select Party</option>
632
+ {parties.map(p => (
633
+ <option key={p.id} value={p.id}>{p.name} - {p.city}</option>
634
+ ))}
635
+ </select>
636
+ </div>
637
+ </div>
638
+
639
+ {/* Items Table */}
640
+ <div className="mb-6">
641
+ <div className="flex justify-between items-center mb-2">
642
+ <h3 className="font-semibold text-gray-700">माल तपशील (Items)</h3>
643
+ </div>
644
+
645
+ <div className="space-y-4">
646
+ {items.map((item, index) => (
647
+ <div key={item.id} className="p-4 border rounded-lg bg-gray-50 relative shadow-sm">
648
+ <button
649
+ onClick={() => removeItem(item.id!)}
650
+ className="absolute top-2 right-2 text-red-400 hover:text-red-600 p-1"
651
+ >
652
+ <Trash2 size={18} />
653
+ </button>
654
+
655
+ <div className="grid grid-cols-2 md:grid-cols-6 gap-3">
656
+ <div className="col-span-2">
657
+ <label className="block text-xs font-medium text-gray-500 mb-1">मिरची जात (Type)</label>
658
+ <select
659
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
660
+ value={item.mirchi_type_id || ''}
661
+ onChange={e => handleMirchiChange(item.id!, e.target.value)}
662
+ >
663
+ <option value="">Select Type</option>
664
+ {mirchiTypes.map(m => (
665
+ <option key={m.id} value={m.id}>{m.name}</option>
666
+ ))}
667
+ </select>
668
+ </div>
669
+
670
+ {/* LOT Selection */}
671
+ {item.mirchi_type_id && (
672
+ <div className="col-span-2">
673
+ <label className="block text-xs font-medium text-gray-500 mb-1">LOT Number</label>
674
+ <select
675
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white font-mono"
676
+ value={item.lot_id || ''}
677
+ onChange={e => handleLotSelection(item.id!, e.target.value)}
678
+ >
679
+ <option value="">Select LOT</option>
680
+ {(availableLots[item.id!] || []).map(lot => (
681
+ <option key={lot.id} value={lot.id}>
682
+ {lot.lot_number} ({lot.remaining_quantity}kg available)
683
+ </option>
684
+ ))}
685
+ </select>
686
+ </div>
687
+ )}
688
+
689
+ <div className={item.mirchi_type_id ? "col-span-2" : "col-span-2 md:col-span-4"}>
690
+ <label className="block text-xs font-medium text-gray-500 mb-1">पोटी वजन (Eg: 10,20,30)</label>
691
+ <input
692
+ type="text"
693
+ placeholder="10, 20, 30"
694
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
695
+ value={potiInputs[item.id!] || ''}
696
+ onChange={e => handlePotiInputChange(item.id!, e.target.value)}
697
+ />
698
+ </div>
699
+
700
+ <div>
701
+ <label className="block text-xs font-medium text-gray-500 mb-1">Gross</label>
702
+ <input type="number" readOnly className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm" value={item.gross_weight} />
703
+ </div>
704
+ <div>
705
+ <label className="block text-xs font-medium text-gray-500 mb-1">Poti (count)</label>
706
+ <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} />
707
+ </div>
708
+ <div>
709
+ <label className="block text-xs font-medium text-gray-500 mb-1">Net</label>
710
+ <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} />
711
+ </div>
712
+ <div>
713
+ <label className="block text-xs font-medium text-gray-500 mb-1">Rate (₹)</label>
714
+ <input
715
+ type="number"
716
+ min="0"
717
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white appearance-none"
718
+ onWheel={e => e.currentTarget.blur()}
719
+ value={item.rate_per_kg === 0 ? '' : item.rate_per_kg}
720
+ onChange={e => handleItemChange(item.id!, 'rate_per_kg', Math.max(0, parseFloat(e.target.value) || 0))}
721
+ placeholder="0"
722
+ />
723
+ </div>
724
+ <div className="col-span-2 md:col-span-2">
725
+ <label className="block text-xs font-medium text-gray-500 mb-1">Total (₹)</label>
726
+ <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)} />
727
+ </div>
728
+ </div>
729
+ </div>
730
+ ))}
731
+ </div>
732
+ <button
733
+ onClick={addItem}
734
+ 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"
735
+ >
736
+ <Plus size={18} /> Add New Item (नवीन माल)
737
+ </button>
738
+ </div>
739
+ </div>
740
+
741
+ {/* Desktop: Right Summary Sidebar */}
742
+ <div className="hidden lg:flex w-80 flex-col gap-4">
743
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-0">
744
+ <h3 className="font-bold text-gray-800 mb-4 border-b pb-2">बिल सारांश (Summary)</h3>
745
+ {SummaryContent()}
746
+ <div className="flex gap-2 mt-6">
747
+ {savedTransaction && (
748
+ <>
749
+ <PrintInvoice transaction={savedTransaction} />
750
+ <PdfInvoice transaction={savedTransaction} />
751
+ </>
752
+ )}
753
+ <button
754
+ onClick={handleSubmit}
755
+ disabled={isSubmitting || items.length === 0}
756
+ className={`flex-1 bg-teal-600 text-white py-3 rounded-lg font-bold shadow-lg hover:bg-teal-700 transition-all flex items-center justify-center gap-2 ${isSubmitting || items.length === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
757
+ >
758
+ <Save size={20} />
759
+ {isSubmitting ? 'Saving...' : 'Save Bill'}
760
+ </button>
761
+ </div>
762
+ </div>
763
+ </div>
764
+
765
+ {/* Mobile: Sticky Bottom Action Bar */}
766
+ <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">
767
+ <div className="flex items-center justify-between p-4 bg-white z-20 relative">
768
+ <div
769
+ onClick={() => setShowMobileSummary(!showMobileSummary)}
770
+ className="flex flex-col cursor-pointer"
771
+ >
772
+ <div className="flex items-center gap-1 text-gray-500 text-xs font-medium uppercase tracking-wide">
773
+ Summary {showMobileSummary ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
774
+ </div>
775
+ <div className="text-xl font-bold text-red-600">
776
+ {grandTotal.toFixed(2)}
777
+ </div>
778
+ </div>
779
+ <button
780
+ onClick={handleSubmit}
781
+ disabled={isSubmitting}
782
+ 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"
783
+ >
784
+ <Save size={18} /> {isSubmitting ? '...' : 'Save'}
785
+ </button>
786
+ </div>
787
+
788
+ {showMobileSummary && (
789
+ <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">
790
+ <div className="mt-4">
791
+ {SummaryContent()}
792
+ </div>
793
+ {savedTransaction && (
794
+ <div className="mt-4 flex justify-center">
795
+ <PrintInvoice transaction={savedTransaction} />
796
+ </div>
797
+ )}
798
+ </div>
799
+ )}
800
+ </div>
801
+ </>
802
+ )}
803
+ </div>
804
+ );
805
+ };
806
+
807
  export default JawaakBill;
pages/PartyLedger.tsx CHANGED
@@ -1,651 +1,651 @@
1
- import React, { useEffect, useState, useMemo } from 'react';
2
- import { getParties, getTransactions, updateTransactionPayment } from '../services/db';
3
- import { Party, Transaction, PartyType, BillType } from '../types';
4
- import { Users, Filter, Search, ArrowLeft, Eye, Edit, Save, X, Printer, Download } from 'lucide-react';
5
- import PrintInvoice from '../components/PrintInvoice.tsx';
6
- import PdfInvoice from '../components/PdfInvoice.tsx';
7
- import { exportPartyLedger } from '../utils/exportToExcel';
8
-
9
- const PartyLedger = () => {
10
- const [parties, setParties] = useState<Party[]>([]);
11
- const [transactions, setTransactions] = useState<Transaction[]>([]);
12
- const [activeTab, setActiveTab] = useState<'awaak' | 'jawaak'>('awaak');
13
- const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
14
- const [selectedParty, setSelectedParty] = useState<Party | null>(null);
15
- const [searchQuery, setSearchQuery] = useState('');
16
-
17
- // Update Payment State
18
- const [editingTxId, setEditingTxId] = useState<string | null>(null);
19
- const [paymentAmount, setPaymentAmount] = useState<string>('');
20
-
21
- // Multi-select for combined invoice
22
- const [selectedTransactions, setSelectedTransactions] = useState<string[]>([]);
23
-
24
- const loadData = async () => {
25
- setParties(await getParties());
26
- setTransactions(await getTransactions());
27
- };
28
-
29
- useEffect(() => {
30
- loadData();
31
- }, []);
32
-
33
- // Filter Parties based on Tab and Search
34
- const filteredParties = parties.filter(p => {
35
- const matchesTab = activeTab === 'awaak'
36
- ? (p.party_type === PartyType.AWAAK || p.party_type === PartyType.BOTH)
37
- : (p.party_type === PartyType.JAWAAK || p.party_type === PartyType.BOTH);
38
-
39
- const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
40
- p.phone.includes(searchQuery) ||
41
- p.city.toLowerCase().includes(searchQuery.toLowerCase());
42
-
43
- return matchesTab && matchesSearch;
44
- });
45
-
46
- // Get Transactions for Selected Party
47
- const partyTransactions = selectedParty
48
- ? transactions.filter(t => t.party_id === selectedParty.id).sort((a, b) => new Date(b.bill_date).getTime() - new Date(a.bill_date).getTime())
49
- : [];
50
-
51
- // Memoize combined transaction for PDF generation
52
- const combinedTransaction = useMemo(() => {
53
- if (!selectedParty || selectedTransactions.length === 0) return null;
54
-
55
- const selected = partyTransactions.filter(t => selectedTransactions.includes(t.id));
56
- if (selected.length === 0) return null;
57
-
58
- return {
59
- ...selected[0], // Use first transaction as base
60
- id: `combined-${selectedParty.id}-${Date.now()}`, // Unique ID for combined
61
- bill_number: `COMBINED-${selected.length}-BILLS`,
62
- items: selected.flatMap(t => t.items),
63
- subtotal: selected.reduce((sum, t) => sum + (t.subtotal || 0), 0),
64
- total_expenses: selected.reduce((sum, t) => sum + (t.total_expenses || 0), 0),
65
- total_amount: selected.reduce((sum, t) => sum + t.total_amount, 0),
66
- paid_amount: selected.reduce((sum, t) => sum + t.paid_amount, 0),
67
- balance_amount: selected.reduce((sum, t) => sum + t.balance_amount, 0),
68
- gross_weight_total: selected.reduce((sum, t) => sum + (t.gross_weight_total || 0), 0),
69
- net_weight_total: selected.reduce((sum, t) => sum + (t.net_weight_total || 0), 0),
70
- };
71
- }, [selectedTransactions, partyTransactions, selectedParty]);
72
-
73
- // Calculate Totals for List View
74
- const getPartyStats = (partyId: string) => {
75
- const txs = transactions.filter(t => t.party_id === partyId);
76
-
77
- // Calculate totals - returns should subtract from totals
78
- const totalBill = txs.reduce((sum, t) => {
79
- if (t.is_return) {
80
- return sum - t.total_amount; // Subtract returns
81
- }
82
- return sum + t.total_amount; // Add normal transactions
83
- }, 0);
84
-
85
- const totalPaid = txs.reduce((sum, t) => {
86
- if (t.is_return) {
87
- return sum - t.paid_amount; // Subtract returns
88
- }
89
- return sum + t.paid_amount; // Add normal transactions
90
- }, 0);
91
-
92
- // Balance is directly from party object for accuracy
93
- const party = parties.find(p => p.id === partyId);
94
- return {
95
- totalBill,
96
- totalPaid,
97
- balance: party?.current_balance || 0
98
- };
99
- };
100
-
101
- const handleUpdatePayment = async (tx: Transaction) => {
102
- const amount = parseFloat(paymentAmount);
103
- if (isNaN(amount) || amount <= 0) {
104
- alert('Please enter a valid amount');
105
- return;
106
- }
107
- if (amount > tx.balance_amount) {
108
- alert('Amount cannot exceed due balance');
109
- return;
110
- }
111
-
112
- const res = await updateTransactionPayment(tx.id, amount);
113
- if (res.success) {
114
- // Update local state for demo/sample data
115
- setTransactions((prev) =>
116
- prev.map((t) =>
117
- t.id === tx.id
118
- ? {
119
- ...t,
120
- paid_amount: t.paid_amount + amount,
121
- balance_amount: t.balance_amount - amount,
122
- }
123
- : t,
124
- ),
125
- );
126
- setEditingTxId(null);
127
- setPaymentAmount('');
128
- // Reload data to refresh party balances
129
- loadData();
130
- } else {
131
- alert(res.message);
132
- }
133
- };
134
-
135
-
136
- return (
137
- <div className="space-y-6">
138
- {/* Header & Controls */}
139
- <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">
140
- {viewMode === 'detail' && selectedParty ? (
141
- <>
142
- {/* Header with Party Info */}
143
- <div className="flex items-center justify-between w-full">
144
- <div className="flex items-center gap-4">
145
- <button
146
- onClick={() => { setViewMode('list'); setSelectedParty(null); setSelectedTransactions([]); }}
147
- className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
148
- >
149
- <ArrowLeft size={20} className="text-gray-600" />
150
- </button>
151
- <div>
152
- <h2 className="text-2xl font-bold text-gray-800">{selectedParty.name}</h2>
153
- <p className="text-sm text-gray-500">{selectedParty.city} • {selectedParty.phone}</p>
154
- </div>
155
- </div>
156
- {selectedTransactions.length > 0 && combinedTransaction && (
157
- <PrintInvoice
158
- transaction={combinedTransaction}
159
- className="px-2 sm:px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-1 sm:gap-2"
160
- >
161
- <Printer size={18} className="shrink-0" />
162
- <span className="hidden sm:inline">Print {selectedTransactions.length} Selected</span>
163
- </PrintInvoice>
164
- )}
165
- {combinedTransaction && (
166
- <PdfInvoice
167
- transaction={combinedTransaction}
168
- className="px-2 sm:px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center gap-1 sm:gap-2"
169
- >
170
- <Download size={18} className="shrink-0" />
171
- <span className="hidden sm:inline">PDF {selectedTransactions.length} Selected</span>
172
- </PdfInvoice>
173
- )}
174
- <button
175
- onClick={() => exportPartyLedger(selectedParty, partyTransactions)}
176
- className="px-2 sm:px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-1 sm:gap-2"
177
- title="Export to Excel"
178
- >
179
- <Download size={18} className="shrink-0" />
180
- <span className="hidden sm:inline">Export to Excel</span>
181
- </button>
182
- </div>
183
- </>
184
- ) : (
185
- <div className="flex items-center gap-2">
186
- <Users className="text-teal-600" />
187
- <h2 className="text-lg font-bold">
188
- पार्टी लेजर (Party Ledger)
189
- </h2>
190
- </div>
191
- )}
192
-
193
- {viewMode === 'list' && (
194
- <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
195
- {/* Tabs */}
196
- <div className="flex bg-gray-100 p-1 rounded-lg">
197
- <button
198
- onClick={() => setActiveTab('awaak')}
199
- 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'}`}
200
- >
201
- Awaak (Purchase)
202
- </button>
203
- <button
204
- onClick={() => setActiveTab('jawaak')}
205
- 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'}`}
206
- >
207
- Jawaak (Sales)
208
- </button>
209
- </div>
210
-
211
- {/* Search */}
212
- <div className="relative w-full md:w-auto mt-2 md:mt-0">
213
- <Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
214
- <input
215
- type="text"
216
- placeholder="Search Party..."
217
- 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"
218
- value={searchQuery}
219
- onChange={e => setSearchQuery(e.target.value)}
220
- />
221
- </div>
222
- </div>
223
- )}
224
- </div>
225
-
226
- {/* Content Area */}
227
- <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
228
- {viewMode === 'list' ? (
229
- <>
230
- {/* Desktop Table View */}
231
- <div className="hidden md:block overflow-x-auto">
232
- <table className="w-full text-sm text-left">
233
- <thead className="bg-gray-50 text-gray-500 border-b border-gray-100">
234
- <tr>
235
- <th className="px-6 py-4 font-medium">Party Name</th>
236
- <th className="px-6 py-4 font-medium text-right">Total Bill Amount</th>
237
- <th className="px-6 py-4 font-medium text-right">Jama (Paid)</th>
238
- <th className="px-6 py-4 font-medium text-right">Baki (Balance)</th>
239
- <th className="px-6 py-4 font-medium text-center">Action</th>
240
- </tr>
241
- </thead>
242
- <tbody className="divide-y divide-gray-100">
243
- {filteredParties.length > 0 ? (
244
- filteredParties.map(party => {
245
- const stats = getPartyStats(party.id);
246
- return (
247
- <tr key={party.id} className="hover:bg-gray-50 transition-colors">
248
- <td className="px-6 py-4 font-medium text-gray-900">{party.name}</td>
249
- <td className="px-6 py-4 text-right font-medium">₹{stats.totalBill.toLocaleString()}</td>
250
- <td className="px-6 py-4 text-right text-green-600 font-medium">₹{stats.totalPaid.toLocaleString()}</td>
251
- <td className={`px-6 py-4 text-right font-bold ${stats.balance !== 0 ? 'text-red-500' : 'text-gray-400'}`}>
252
- ₹{Math.abs(stats.balance).toLocaleString()} {stats.balance > 0 ? '(Dr)' : stats.balance < 0 ? '(Cr)' : ''}
253
- </td>
254
- <td className="px-6 py-4 text-center">
255
- <button
256
- onClick={() => {
257
- setSelectedParty(party);
258
- setViewMode('detail');
259
- // If this party has no transactions, add some sample data for demo
260
- const hasTx = transactions.some(t => t.party_id === party.id);
261
- if (!hasTx) {
262
- const sampleTxs = [
263
- {
264
- id: `${party.id}-tx1`,
265
- party_id: party.id,
266
- bill_date: new Date().toISOString().split('T')[0],
267
- bill_number: 'SAMPLE001',
268
- items: [{ mirchi_name: 'Red Chili' }],
269
- is_return: false,
270
- total_amount: 5000,
271
- paid_amount: 2000,
272
- balance_amount: 3000,
273
- bill_type: PartyType.AWAAK,
274
- },
275
- {
276
- id: `${party.id}-tx2`,
277
- party_id: party.id,
278
- bill_date: new Date().toISOString().split('T')[0],
279
- bill_number: 'SAMPLE002',
280
- items: [{ mirchi_name: 'Green Chili' }],
281
- is_return: false,
282
- total_amount: 3000,
283
- paid_amount: 3000,
284
- balance_amount: 0,
285
- bill_type: PartyType.AWAAK,
286
- },
287
- ];
288
- setTransactions(prev => [...prev, ...sampleTxs]);
289
- }
290
- }}
291
- className="p-2 hover:bg-teal-50 text-teal-600 rounded-full transition-colors"
292
- title="View Ledger"
293
- >
294
- <Eye size={18} />
295
- </button>
296
- </td>
297
- </tr>
298
- );
299
- })
300
- ) : (
301
- <tr>
302
- <td colSpan={5} className="px-6 py-8 text-center text-gray-500">
303
- No parties found.
304
- </td>
305
- </tr>
306
- )}
307
- </tbody>
308
- </table>
309
- </div>
310
-
311
- {/* Mobile Card View */}
312
- <div className="md:hidden flex flex-col divide-y divide-gray-100">
313
- {filteredParties.length > 0 ? (
314
- filteredParties.map(party => {
315
- const stats = getPartyStats(party.id);
316
- return (
317
- <div key={party.id} className="p-4 space-y-3">
318
- <div className="flex justify-between items-start">
319
- <div>
320
- <h3 className="font-bold text-gray-900">{party.name}</h3>
321
- </div>
322
- <button
323
- onClick={() => {
324
- setSelectedParty(party);
325
- setViewMode('detail');
326
- // If this party has no transactions, add sample data for demo
327
- const hasTx = transactions.some(t => t.party_id === party.id);
328
- if (!hasTx) {
329
- const sampleTxs = [
330
- {
331
- id: `${party.id}-tx1`,
332
- party_id: party.id,
333
- bill_date: new Date().toISOString().split('T')[0],
334
- bill_number: 'SAMPLE001',
335
- items: [{ mirchi_name: 'Red Chili' }],
336
- is_return: false,
337
- total_amount: 5000,
338
- paid_amount: 2000,
339
- balance_amount: 3000,
340
- bill_type: PartyType.AWAAK,
341
- },
342
- {
343
- id: `${party.id}-tx2`,
344
- party_id: party.id,
345
- bill_date: new Date().toISOString().split('T')[0],
346
- bill_number: 'SAMPLE002',
347
- items: [{ mirchi_name: 'Green Chili' }],
348
- is_return: false,
349
- total_amount: 3000,
350
- paid_amount: 3000,
351
- balance_amount: 0,
352
- bill_type: PartyType.AWAAK,
353
- },
354
- ];
355
- setTransactions(prev => [...prev, ...sampleTxs]);
356
- }
357
- }}
358
- className="p-2 bg-teal-50 text-teal-600 rounded-lg"
359
- >
360
- <Eye size={18} />
361
- </button>
362
- </div>
363
- <div className="grid grid-cols-3 gap-2 text-xs">
364
- <div className="bg-gray-50 p-2 rounded">
365
- <div className="text-gray-500 mb-1">Total Bill</div>
366
- <div className="font-medium">₹{stats.totalBill.toLocaleString()}</div>
367
- </div>
368
- <div className="bg-green-50 p-2 rounded">
369
- <div className="text-green-600 mb-1">Paid</div>
370
- <div className="font-medium text-green-700">₹{stats.totalPaid.toLocaleString()}</div>
371
- </div>
372
- <div className="bg-red-50 p-2 rounded">
373
- <div className="text-red-500 mb-1">Balance</div>
374
- <div className="font-bold text-red-600">
375
- ₹{Math.abs(stats.balance).toLocaleString()} {stats.balance > 0 ? 'Dr' : stats.balance < 0 ? 'Cr' : ''}
376
- </div>
377
- </div>
378
- </div>
379
- </div>
380
- );
381
- })
382
- ) : (
383
- <div className="p-8 text-center text-gray-500">
384
- No parties found.
385
- </div>
386
- )}
387
- </div>
388
- </>
389
- ) : (
390
- // Detail View
391
- <>
392
- {/* Desktop Table View */}
393
- <div className="hidden md:block overflow-x-auto">
394
- <table className="w-full text-sm text-left">
395
- <thead className="bg-gray-50 text-gray-500 border-b border-gray-100">
396
- <tr>
397
- <th className="px-6 py-4 font-medium text-center">
398
- <input
399
- type="checkbox"
400
- checked={selectedTransactions.length === partyTransactions.length && partyTransactions.length > 0}
401
- onChange={(e) => {
402
- if (e.target.checked) {
403
- setSelectedTransactions(partyTransactions.map(t => t.id));
404
- } else {
405
- setSelectedTransactions([]);
406
- }
407
- }}
408
- className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500"
409
- />
410
- </th>
411
- <th className="px-6 py-4 font-medium">Date</th>
412
- <th className="px-6 py-4 font-medium">Bill No</th>
413
- <th className="px-6 py-4 font-medium">Mirchi Type</th>
414
- <th className="px-6 py-4 font-medium">Remark</th>
415
- <th className="px-6 py-4 font-medium text-right">Bill Amount</th>
416
- <th className="px-6 py-4 font-medium text-right">Paid</th>
417
- <th className="px-6 py-4 font-medium text-right">Due Balance</th>
418
- <th className="px-6 py-4 font-medium text-center">Action</th>
419
- <th className="px-6 py-4 font-medium text-center">Print</th>
420
- </tr>
421
- </thead>
422
- <tbody className="divide-y divide-gray-100">
423
- {partyTransactions.length > 0 ? (
424
- partyTransactions.map(tx => (
425
- <tr key={tx.id} className="hover:bg-gray-50">
426
- <td className="px-6 py-4 text-center">
427
- <input
428
- type="checkbox"
429
- checked={selectedTransactions.includes(tx.id)}
430
- onChange={(e) => {
431
- if (e.target.checked) {
432
- setSelectedTransactions([...selectedTransactions, tx.id]);
433
- } else {
434
- setSelectedTransactions(selectedTransactions.filter(id => id !== tx.id));
435
- }
436
- }}
437
- className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500"
438
- />
439
- </td>
440
- <td className="px-6 py-4 text-gray-600">{tx.bill_date?.split('T')[0] || tx.bill_date}</td>
441
- <td className="px-6 py-4 font-mono text-gray-500">{tx.bill_number}</td>
442
- <td className="px-6 py-4">
443
- {tx.items.map((i, idx) => (
444
- <div key={idx} className="text-sm">
445
- {i.mirchi_name}
446
- {i.lot_number && (
447
- <span className="ml-2 text-xs font-mono text-gray-500">({i.lot_number})</span>
448
- )}
449
- </div>
450
- ))}
451
- </td>
452
- <td className="px-6 py-4">
453
- {tx.is_return ? (
454
- <span className="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs font-semibold">Returned</span>
455
- ) : (
456
- <span className="text-gray-400">-</span>
457
- )}
458
- </td>
459
- <td className="px-6 py-4 text-right font-medium">₹{tx.total_amount.toLocaleString()}</td>
460
- <td className="px-6 py-4 text-right text-green-600">₹{tx.paid_amount.toLocaleString()}</td>
461
- <td className="px-6 py-4 text-right font-bold text-red-500">₹{tx.balance_amount.toLocaleString()}</td>
462
- <td className="px-6 py-4 text-center">
463
- {tx.balance_amount > 0 ? (
464
- editingTxId === tx.id ? (
465
- <div className="flex items-center gap-2 justify-end">
466
- <input
467
- type="number"
468
- className="w-24 border border-teal-300 rounded px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-teal-500"
469
- placeholder="Amount"
470
- value={paymentAmount}
471
- onChange={e => setPaymentAmount(e.target.value)}
472
- autoFocus
473
- />
474
- <button
475
- onClick={() => handleUpdatePayment(tx)}
476
- className="p-2 bg-teal-600 text-white rounded hover:bg-teal-700 disabled:opacity-50"
477
- title="Save"
478
- disabled={Number(paymentAmount) > tx.balance_amount || Number(paymentAmount) <= 0}
479
- >
480
- <Save size={16} />
481
- </button>
482
- <button
483
- onClick={() => { setEditingTxId(null); setPaymentAmount(''); }}
484
- className="p-1 bg-gray-200 text-gray-600 rounded hover:bg-gray-300"
485
- title="Cancel"
486
- >
487
- <X size={14} />
488
- </button>
489
- </div>
490
- ) : (
491
- <button
492
- onClick={() => { setEditingTxId(tx.id); setPaymentAmount(''); }}
493
- className="px-3 py-1 border border-teal-600 text-teal-600 rounded-md text-xs font-medium hover:bg-teal-50 transition-colors"
494
- >
495
- Update Due
496
- </button>
497
- )
498
- ) : (
499
- <span className="text-green-600 text-xs font-bold flex items-center justify-center gap-1">
500
- Paid
501
- </span>
502
- )}
503
- </td>
504
- <td className="px-6 py-4 text-center">
505
- <div className="flex gap-2 justify-center items-center">
506
- <PrintInvoice
507
- transaction={tx}
508
- className="p-2 bg-white text-teal-600 border border-teal-600 rounded-md hover:bg-teal-50 transition-colors flex items-center justify-center shadow-sm"
509
- />
510
- <PdfInvoice
511
- transaction={tx}
512
- className="p-2 bg-white text-red-600 border border-red-600 rounded-md hover:bg-red-50 transition-colors flex items-center justify-center shadow-sm"
513
- >
514
- <Download size={16} />
515
- </PdfInvoice>
516
- </div>
517
- </td>
518
- </tr>
519
- ))
520
- ) : (
521
- <tr>
522
- <td colSpan={8} className="px-6 py-8 text-center text-gray-500">
523
- No transactions found for this party.
524
- </td>
525
- </tr>
526
- )}
527
- </tbody>
528
- </table>
529
- </div>
530
-
531
- {/* Mobile Card View for Detail */}
532
- <div className="md:hidden flex flex-col divide-y divide-gray-100">
533
- {partyTransactions.length > 0 ? (
534
- partyTransactions.map(tx => (
535
- <div key={tx.id} className="p-4 space-y-3">
536
- <div className="flex justify-between items-start">
537
- <div className="flex items-start gap-3">
538
- <input
539
- type="checkbox"
540
- checked={selectedTransactions.includes(tx.id)}
541
- onChange={(e) => {
542
- if (e.target.checked) {
543
- setSelectedTransactions([...selectedTransactions, tx.id]);
544
- } else {
545
- setSelectedTransactions(selectedTransactions.filter(id => id !== tx.id));
546
- }
547
- }}
548
- className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500 mt-1"
549
- />
550
- <div>
551
- <div className="text-xs text-gray-500">{tx.bill_date?.split('T')[0] || tx.bill_date}</div>
552
- <div className="font-mono text-sm font-medium text-gray-800">{tx.bill_number}</div>
553
- </div>
554
- </div>
555
- {tx.is_return && (
556
- <span className="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs font-semibold">Returned</span>
557
- )}
558
- </div>
559
-
560
- <div className="text-sm text-gray-600">
561
- <span className="font-medium text-gray-500 text-xs block mb-1">Items:</span>
562
- {tx.items.map((i, idx) => (
563
- <div key={idx}>
564
- {i.mirchi_name}
565
- {i.lot_number && (
566
- <span className="ml-2 text-xs font-mono text-gray-500">({i.lot_number})</span>
567
- )}
568
- </div>
569
- ))}
570
- </div>
571
-
572
- <div className="grid grid-cols-3 gap-2 text-xs pt-2 border-t border-gray-50">
573
- <div>
574
- <div className="text-gray-500">Bill Amount</div>
575
- <div className="font-medium">₹{tx.total_amount.toLocaleString()}</div>
576
- </div>
577
- <div>
578
- <div className="text-gray-500">Paid</div>
579
- <div className="font-medium text-green-600">₹{tx.paid_amount.toLocaleString()}</div>
580
- </div>
581
- <div>
582
- <div className="text-gray-500">Due</div>
583
- <div className="font-bold text-red-500">₹{tx.balance_amount.toLocaleString()}</div>
584
- </div>
585
- </div>
586
-
587
- {tx.balance_amount > 0 && (
588
- <div className="pt-2">
589
- {editingTxId === tx.id ? (
590
- <div className="flex items-center gap-2">
591
- <input
592
- type="number"
593
- className="flex-1 border border-teal-300 rounded px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-teal-500"
594
- placeholder="Enter Amount"
595
- value={paymentAmount}
596
- onChange={e => setPaymentAmount(e.target.value)}
597
- autoFocus
598
- />
599
- <button
600
- onClick={() => handleUpdatePayment(tx)}
601
- className="p-2 bg-teal-600 text-white rounded hover:bg-teal-700"
602
- >
603
- <Save size={16} />
604
- </button>
605
- <button
606
- onClick={() => { setEditingTxId(null); setPaymentAmount(''); }}
607
- className="p-2 bg-gray-200 text-gray-600 rounded hover:bg-gray-300"
608
- >
609
- <X size={16} />
610
- </button>
611
- </div>
612
- ) : (
613
- <button
614
- onClick={() => { setEditingTxId(tx.id); setPaymentAmount(''); }}
615
- className="w-full py-2 border border-teal-600 text-teal-600 rounded-lg text-sm font-medium hover:bg-teal-50 transition-colors"
616
- >
617
- Update Due
618
- </button>
619
- )}
620
- </div>
621
- )}
622
-
623
- {/* Print Button for Mobile */}
624
- <div className="pt-2 border-t border-gray-100 mt-2 flex gap-3 items-center">
625
- <PrintInvoice
626
- transaction={tx}
627
- className="p-2 bg-white text-teal-600 border border-teal-600 rounded-md hover:bg-teal-50 transition-colors flex items-center justify-center shadow-sm"
628
- />
629
- <PdfInvoice
630
- transaction={tx}
631
- className="p-2 bg-white text-red-600 border border-red-600 rounded-md hover:bg-red-50 transition-colors flex items-center justify-center shadow-sm"
632
- >
633
- <Download size={16} />
634
- </PdfInvoice>
635
- </div>
636
- </div>
637
- ))
638
- ) : (
639
- <div className="p-8 text-center text-gray-500">
640
- No transactions found.
641
- </div>
642
- )}
643
- </div>
644
- </>
645
- )}
646
- </div>
647
- </div >
648
- );
649
- };
650
-
651
  export default PartyLedger;
 
1
+ import React, { useEffect, useState, useMemo } from 'react';
2
+ import { getParties, getTransactions, updateTransactionPayment } from '../services/db';
3
+ import { Party, Transaction, PartyType, BillType } from '../types';
4
+ import { Users, Filter, Search, ArrowLeft, Eye, Edit, Save, X, Printer, Download } from 'lucide-react';
5
+ import PrintInvoice from '../components/PrintInvoice.tsx';
6
+ import PdfInvoice from '../components/PdfInvoice.tsx';
7
+ import { exportPartyLedger } from '../utils/exportToExcel';
8
+
9
+ const PartyLedger = () => {
10
+ const [parties, setParties] = useState<Party[]>([]);
11
+ const [transactions, setTransactions] = useState<Transaction[]>([]);
12
+ const [activeTab, setActiveTab] = useState<'awaak' | 'jawaak'>('awaak');
13
+ const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
14
+ const [selectedParty, setSelectedParty] = useState<Party | null>(null);
15
+ const [searchQuery, setSearchQuery] = useState('');
16
+
17
+ // Update Payment State
18
+ const [editingTxId, setEditingTxId] = useState<string | null>(null);
19
+ const [paymentAmount, setPaymentAmount] = useState<string>('');
20
+
21
+ // Multi-select for combined invoice
22
+ const [selectedTransactions, setSelectedTransactions] = useState<string[]>([]);
23
+
24
+ const loadData = async () => {
25
+ setParties(await getParties());
26
+ setTransactions(await getTransactions());
27
+ };
28
+
29
+ useEffect(() => {
30
+ loadData();
31
+ }, []);
32
+
33
+ // Filter Parties based on Tab and Search
34
+ const filteredParties = parties.filter(p => {
35
+ const matchesTab = activeTab === 'awaak'
36
+ ? (p.party_type === PartyType.AWAAK || p.party_type === PartyType.BOTH)
37
+ : (p.party_type === PartyType.JAWAAK || p.party_type === PartyType.BOTH);
38
+
39
+ const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
40
+ p.phone.includes(searchQuery) ||
41
+ p.city.toLowerCase().includes(searchQuery.toLowerCase());
42
+
43
+ return matchesTab && matchesSearch;
44
+ });
45
+
46
+ // Get Transactions for Selected Party
47
+ const partyTransactions = selectedParty
48
+ ? transactions.filter(t => t.party_id === selectedParty.id).sort((a, b) => new Date(b.bill_date).getTime() - new Date(a.bill_date).getTime())
49
+ : [];
50
+
51
+ // Memoize combined transaction for PDF generation
52
+ const combinedTransaction = useMemo(() => {
53
+ if (!selectedParty || selectedTransactions.length === 0) return null;
54
+
55
+ const selected = partyTransactions.filter(t => selectedTransactions.includes(t.id));
56
+ if (selected.length === 0) return null;
57
+
58
+ return {
59
+ ...selected[0], // Use first transaction as base
60
+ id: `combined-${selectedParty.id}-${Date.now()}`, // Unique ID for combined
61
+ bill_number: `COMBINED-${selected.length}-BILLS`,
62
+ items: selected.flatMap(t => t.items),
63
+ subtotal: selected.reduce((sum, t) => sum + (t.subtotal || 0), 0),
64
+ total_expenses: selected.reduce((sum, t) => sum + (t.total_expenses || 0), 0),
65
+ total_amount: selected.reduce((sum, t) => sum + t.total_amount, 0),
66
+ paid_amount: selected.reduce((sum, t) => sum + t.paid_amount, 0),
67
+ balance_amount: selected.reduce((sum, t) => sum + t.balance_amount, 0),
68
+ gross_weight_total: selected.reduce((sum, t) => sum + (t.gross_weight_total || 0), 0),
69
+ net_weight_total: selected.reduce((sum, t) => sum + (t.net_weight_total || 0), 0),
70
+ };
71
+ }, [selectedTransactions, partyTransactions, selectedParty]);
72
+
73
+ // Calculate Totals for List View
74
+ const getPartyStats = (partyId: string) => {
75
+ const txs = transactions.filter(t => t.party_id === partyId);
76
+
77
+ // Calculate totals - returns should subtract from totals
78
+ const totalBill = txs.reduce((sum, t) => {
79
+ if (t.is_return) {
80
+ return sum - t.total_amount; // Subtract returns
81
+ }
82
+ return sum + t.total_amount; // Add normal transactions
83
+ }, 0);
84
+
85
+ const totalPaid = txs.reduce((sum, t) => {
86
+ if (t.is_return) {
87
+ return sum - t.paid_amount; // Subtract returns
88
+ }
89
+ return sum + t.paid_amount; // Add normal transactions
90
+ }, 0);
91
+
92
+ // Balance is directly from party object for accuracy
93
+ const party = parties.find(p => p.id === partyId);
94
+ return {
95
+ totalBill,
96
+ totalPaid,
97
+ balance: party?.current_balance || 0
98
+ };
99
+ };
100
+
101
+ const handleUpdatePayment = async (tx: Transaction) => {
102
+ const amount = parseFloat(paymentAmount);
103
+ if (isNaN(amount) || amount <= 0) {
104
+ alert('Please enter a valid amount');
105
+ return;
106
+ }
107
+ if (amount > tx.balance_amount) {
108
+ alert('Amount cannot exceed due balance');
109
+ return;
110
+ }
111
+
112
+ const res = await updateTransactionPayment(tx.id, amount);
113
+ if (res.success) {
114
+ // Update local state for demo/sample data
115
+ setTransactions((prev) =>
116
+ prev.map((t) =>
117
+ t.id === tx.id
118
+ ? {
119
+ ...t,
120
+ paid_amount: t.paid_amount + amount,
121
+ balance_amount: t.balance_amount - amount,
122
+ }
123
+ : t,
124
+ ),
125
+ );
126
+ setEditingTxId(null);
127
+ setPaymentAmount('');
128
+ // Reload data to refresh party balances
129
+ loadData();
130
+ } else {
131
+ alert(res.message);
132
+ }
133
+ };
134
+
135
+
136
+ return (
137
+ <div className="space-y-6">
138
+ {/* Header & Controls */}
139
+ <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">
140
+ {viewMode === 'detail' && selectedParty ? (
141
+ <>
142
+ {/* Header with Party Info */}
143
+ <div className="flex items-center justify-between w-full">
144
+ <div className="flex items-center gap-4">
145
+ <button
146
+ onClick={() => { setViewMode('list'); setSelectedParty(null); setSelectedTransactions([]); }}
147
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
148
+ >
149
+ <ArrowLeft size={20} className="text-gray-600" />
150
+ </button>
151
+ <div>
152
+ <h2 className="text-2xl font-bold text-gray-800">{selectedParty.name}</h2>
153
+ <p className="text-sm text-gray-500">{selectedParty.city} • {selectedParty.phone}</p>
154
+ </div>
155
+ </div>
156
+ {selectedTransactions.length > 0 && combinedTransaction && (
157
+ <PrintInvoice
158
+ transaction={combinedTransaction}
159
+ className="px-2 sm:px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-1 sm:gap-2"
160
+ >
161
+ <Printer size={18} className="shrink-0" />
162
+ <span className="hidden sm:inline">Print {selectedTransactions.length} Selected</span>
163
+ </PrintInvoice>
164
+ )}
165
+ {combinedTransaction && (
166
+ <PdfInvoice
167
+ transaction={combinedTransaction}
168
+ className="px-2 sm:px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center gap-1 sm:gap-2"
169
+ >
170
+ <Download size={18} className="shrink-0" />
171
+ <span className="hidden sm:inline">PDF {selectedTransactions.length} Selected</span>
172
+ </PdfInvoice>
173
+ )}
174
+ <button
175
+ onClick={() => exportPartyLedger(selectedParty, partyTransactions)}
176
+ className="px-2 sm:px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-1 sm:gap-2"
177
+ title="Export to Excel"
178
+ >
179
+ <Download size={18} className="shrink-0" />
180
+ <span className="hidden sm:inline">Export to Excel</span>
181
+ </button>
182
+ </div>
183
+ </>
184
+ ) : (
185
+ <div className="flex items-center gap-2">
186
+ <Users className="text-teal-600" />
187
+ <h2 className="text-lg font-bold">
188
+ पार्टी लेजर (Party Ledger)
189
+ </h2>
190
+ </div>
191
+ )}
192
+
193
+ {viewMode === 'list' && (
194
+ <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
195
+ {/* Tabs */}
196
+ <div className="flex bg-gray-100 p-1 rounded-lg">
197
+ <button
198
+ onClick={() => setActiveTab('awaak')}
199
+ 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'}`}
200
+ >
201
+ Awaak (Purchase)
202
+ </button>
203
+ <button
204
+ onClick={() => setActiveTab('jawaak')}
205
+ 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'}`}
206
+ >
207
+ Jawaak (Sales)
208
+ </button>
209
+ </div>
210
+
211
+ {/* Search */}
212
+ <div className="relative w-full md:w-auto mt-2 md:mt-0">
213
+ <Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
214
+ <input
215
+ type="text"
216
+ placeholder="Search Party..."
217
+ 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"
218
+ value={searchQuery}
219
+ onChange={e => setSearchQuery(e.target.value)}
220
+ />
221
+ </div>
222
+ </div>
223
+ )}
224
+ </div>
225
+
226
+ {/* Content Area */}
227
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
228
+ {viewMode === 'list' ? (
229
+ <>
230
+ {/* Desktop Table View */}
231
+ <div className="hidden md:block overflow-x-auto">
232
+ <table className="w-full text-sm text-left">
233
+ <thead className="bg-gray-50 text-gray-500 border-b border-gray-100">
234
+ <tr>
235
+ <th className="px-6 py-4 font-medium">Party Name</th>
236
+ <th className="px-6 py-4 font-medium text-right">Total Bill Amount</th>
237
+ <th className="px-6 py-4 font-medium text-right">Jama (Paid)</th>
238
+ <th className="px-6 py-4 font-medium text-right">Baki (Balance)</th>
239
+ <th className="px-6 py-4 font-medium text-center">Action</th>
240
+ </tr>
241
+ </thead>
242
+ <tbody className="divide-y divide-gray-100">
243
+ {filteredParties.length > 0 ? (
244
+ filteredParties.map(party => {
245
+ const stats = getPartyStats(party.id);
246
+ return (
247
+ <tr key={party.id} className="hover:bg-gray-50 transition-colors">
248
+ <td className="px-6 py-4 font-medium text-gray-900">{party.name}</td>
249
+ <td className="px-6 py-4 text-right font-medium">₹{stats.totalBill.toLocaleString()}</td>
250
+ <td className="px-6 py-4 text-right text-green-600 font-medium">₹{stats.totalPaid.toLocaleString()}</td>
251
+ <td className={`px-6 py-4 text-right font-bold ${stats.balance !== 0 ? 'text-red-500' : 'text-gray-400'}`}>
252
+ ₹{Math.abs(stats.balance).toLocaleString()} {stats.balance > 0 ? '(Dr)' : stats.balance < 0 ? '(Cr)' : ''}
253
+ </td>
254
+ <td className="px-6 py-4 text-center">
255
+ <button
256
+ onClick={() => {
257
+ setSelectedParty(party);
258
+ setViewMode('detail');
259
+ // If this party has no transactions, add some sample data for demo
260
+ const hasTx = transactions.some(t => t.party_id === party.id);
261
+ if (!hasTx) {
262
+ const sampleTxs = [
263
+ {
264
+ id: `${party.id}-tx1`,
265
+ party_id: party.id,
266
+ bill_date: new Date().toISOString().split('T')[0],
267
+ bill_number: 'SAMPLE001',
268
+ items: [{ mirchi_name: 'Red Chili' }],
269
+ is_return: false,
270
+ total_amount: 5000,
271
+ paid_amount: 2000,
272
+ balance_amount: 3000,
273
+ bill_type: PartyType.AWAAK,
274
+ },
275
+ {
276
+ id: `${party.id}-tx2`,
277
+ party_id: party.id,
278
+ bill_date: new Date().toISOString().split('T')[0],
279
+ bill_number: 'SAMPLE002',
280
+ items: [{ mirchi_name: 'Green Chili' }],
281
+ is_return: false,
282
+ total_amount: 3000,
283
+ paid_amount: 3000,
284
+ balance_amount: 0,
285
+ bill_type: PartyType.AWAAK,
286
+ },
287
+ ];
288
+ setTransactions(prev => [...prev, ...sampleTxs]);
289
+ }
290
+ }}
291
+ className="p-2 hover:bg-teal-50 text-teal-600 rounded-full transition-colors"
292
+ title="View Ledger"
293
+ >
294
+ <Eye size={18} />
295
+ </button>
296
+ </td>
297
+ </tr>
298
+ );
299
+ })
300
+ ) : (
301
+ <tr>
302
+ <td colSpan={5} className="px-6 py-8 text-center text-gray-500">
303
+ No parties found.
304
+ </td>
305
+ </tr>
306
+ )}
307
+ </tbody>
308
+ </table>
309
+ </div>
310
+
311
+ {/* Mobile Card View */}
312
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
313
+ {filteredParties.length > 0 ? (
314
+ filteredParties.map(party => {
315
+ const stats = getPartyStats(party.id);
316
+ return (
317
+ <div key={party.id} className="p-4 space-y-3">
318
+ <div className="flex justify-between items-start">
319
+ <div>
320
+ <h3 className="font-bold text-gray-900">{party.name}</h3>
321
+ </div>
322
+ <button
323
+ onClick={() => {
324
+ setSelectedParty(party);
325
+ setViewMode('detail');
326
+ // If this party has no transactions, add sample data for demo
327
+ const hasTx = transactions.some(t => t.party_id === party.id);
328
+ if (!hasTx) {
329
+ const sampleTxs = [
330
+ {
331
+ id: `${party.id}-tx1`,
332
+ party_id: party.id,
333
+ bill_date: new Date().toISOString().split('T')[0],
334
+ bill_number: 'SAMPLE001',
335
+ items: [{ mirchi_name: 'Red Chili' }],
336
+ is_return: false,
337
+ total_amount: 5000,
338
+ paid_amount: 2000,
339
+ balance_amount: 3000,
340
+ bill_type: PartyType.AWAAK,
341
+ },
342
+ {
343
+ id: `${party.id}-tx2`,
344
+ party_id: party.id,
345
+ bill_date: new Date().toISOString().split('T')[0],
346
+ bill_number: 'SAMPLE002',
347
+ items: [{ mirchi_name: 'Green Chili' }],
348
+ is_return: false,
349
+ total_amount: 3000,
350
+ paid_amount: 3000,
351
+ balance_amount: 0,
352
+ bill_type: PartyType.AWAAK,
353
+ },
354
+ ];
355
+ setTransactions(prev => [...prev, ...sampleTxs]);
356
+ }
357
+ }}
358
+ className="p-2 bg-teal-50 text-teal-600 rounded-lg"
359
+ >
360
+ <Eye size={18} />
361
+ </button>
362
+ </div>
363
+ <div className="grid grid-cols-3 gap-2 text-xs">
364
+ <div className="bg-gray-50 p-2 rounded">
365
+ <div className="text-gray-500 mb-1">Total Bill</div>
366
+ <div className="font-medium">₹{stats.totalBill.toLocaleString()}</div>
367
+ </div>
368
+ <div className="bg-green-50 p-2 rounded">
369
+ <div className="text-green-600 mb-1">Paid</div>
370
+ <div className="font-medium text-green-700">₹{stats.totalPaid.toLocaleString()}</div>
371
+ </div>
372
+ <div className="bg-red-50 p-2 rounded">
373
+ <div className="text-red-500 mb-1">Balance</div>
374
+ <div className="font-bold text-red-600">
375
+ ₹{Math.abs(stats.balance).toLocaleString()} {stats.balance > 0 ? 'Dr' : stats.balance < 0 ? 'Cr' : ''}
376
+ </div>
377
+ </div>
378
+ </div>
379
+ </div>
380
+ );
381
+ })
382
+ ) : (
383
+ <div className="p-8 text-center text-gray-500">
384
+ No parties found.
385
+ </div>
386
+ )}
387
+ </div>
388
+ </>
389
+ ) : (
390
+ // Detail View
391
+ <>
392
+ {/* Desktop Table View */}
393
+ <div className="hidden md:block overflow-x-auto">
394
+ <table className="w-full text-sm text-left">
395
+ <thead className="bg-gray-50 text-gray-500 border-b border-gray-100">
396
+ <tr>
397
+ <th className="px-6 py-4 font-medium text-center">
398
+ <input
399
+ type="checkbox"
400
+ checked={selectedTransactions.length === partyTransactions.length && partyTransactions.length > 0}
401
+ onChange={(e) => {
402
+ if (e.target.checked) {
403
+ setSelectedTransactions(partyTransactions.map(t => t.id));
404
+ } else {
405
+ setSelectedTransactions([]);
406
+ }
407
+ }}
408
+ className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500"
409
+ />
410
+ </th>
411
+ <th className="px-6 py-4 font-medium">Date</th>
412
+ <th className="px-6 py-4 font-medium">Bill No</th>
413
+ <th className="px-6 py-4 font-medium">Mirchi Type</th>
414
+ <th className="px-6 py-4 font-medium">Remark</th>
415
+ <th className="px-6 py-4 font-medium text-right">Bill Amount</th>
416
+ <th className="px-6 py-4 font-medium text-right">Paid</th>
417
+ <th className="px-6 py-4 font-medium text-right">Due Balance</th>
418
+ <th className="px-6 py-4 font-medium text-center">Action</th>
419
+ <th className="px-6 py-4 font-medium text-center">Print</th>
420
+ </tr>
421
+ </thead>
422
+ <tbody className="divide-y divide-gray-100">
423
+ {partyTransactions.length > 0 ? (
424
+ partyTransactions.map(tx => (
425
+ <tr key={tx.id} className="hover:bg-gray-50">
426
+ <td className="px-6 py-4 text-center">
427
+ <input
428
+ type="checkbox"
429
+ checked={selectedTransactions.includes(tx.id)}
430
+ onChange={(e) => {
431
+ if (e.target.checked) {
432
+ setSelectedTransactions([...selectedTransactions, tx.id]);
433
+ } else {
434
+ setSelectedTransactions(selectedTransactions.filter(id => id !== tx.id));
435
+ }
436
+ }}
437
+ className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500"
438
+ />
439
+ </td>
440
+ <td className="px-6 py-4 text-gray-600">{tx.bill_date?.split('T')[0] || tx.bill_date}</td>
441
+ <td className="px-6 py-4 font-mono text-gray-500">{tx.bill_number}</td>
442
+ <td className="px-6 py-4">
443
+ {tx.items.map((i, idx) => (
444
+ <div key={idx} className="text-sm">
445
+ {i.mirchi_name}
446
+ {i.lot_number && (
447
+ <span className="ml-2 text-xs font-mono text-gray-500">({i.lot_number})</span>
448
+ )}
449
+ </div>
450
+ ))}
451
+ </td>
452
+ <td className="px-6 py-4">
453
+ {tx.is_return ? (
454
+ <span className="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs font-semibold">Returned</span>
455
+ ) : (
456
+ <span className="text-gray-400">-</span>
457
+ )}
458
+ </td>
459
+ <td className="px-6 py-4 text-right font-medium">₹{tx.total_amount.toLocaleString()}</td>
460
+ <td className="px-6 py-4 text-right text-green-600">₹{tx.paid_amount.toLocaleString()}</td>
461
+ <td className="px-6 py-4 text-right font-bold text-red-500">₹{tx.balance_amount.toLocaleString()}</td>
462
+ <td className="px-6 py-4 text-center">
463
+ {tx.balance_amount > 0 ? (
464
+ editingTxId === tx.id ? (
465
+ <div className="flex items-center gap-2 justify-end">
466
+ <input
467
+ type="number"
468
+ className="w-24 border border-teal-300 rounded px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-teal-500"
469
+ placeholder="Amount"
470
+ value={paymentAmount}
471
+ onChange={e => setPaymentAmount(e.target.value)}
472
+ autoFocus
473
+ />
474
+ <button
475
+ onClick={() => handleUpdatePayment(tx)}
476
+ className="p-2 bg-teal-600 text-white rounded hover:bg-teal-700 disabled:opacity-50"
477
+ title="Save"
478
+ disabled={Number(paymentAmount) > tx.balance_amount || Number(paymentAmount) <= 0}
479
+ >
480
+ <Save size={16} />
481
+ </button>
482
+ <button
483
+ onClick={() => { setEditingTxId(null); setPaymentAmount(''); }}
484
+ className="p-1 bg-gray-200 text-gray-600 rounded hover:bg-gray-300"
485
+ title="Cancel"
486
+ >
487
+ <X size={14} />
488
+ </button>
489
+ </div>
490
+ ) : (
491
+ <button
492
+ onClick={() => { setEditingTxId(tx.id); setPaymentAmount(''); }}
493
+ className="px-3 py-1 border border-teal-600 text-teal-600 rounded-md text-xs font-medium hover:bg-teal-50 transition-colors"
494
+ >
495
+ Update Due
496
+ </button>
497
+ )
498
+ ) : (
499
+ <span className="text-green-600 text-xs font-bold flex items-center justify-center gap-1">
500
+ Paid
501
+ </span>
502
+ )}
503
+ </td>
504
+ <td className="px-6 py-4 text-center">
505
+ <div className="flex gap-2 justify-center items-center">
506
+ <PrintInvoice
507
+ transaction={tx}
508
+ className="p-2 bg-white text-teal-600 border border-teal-600 rounded-md hover:bg-teal-50 transition-colors flex items-center justify-center shadow-sm"
509
+ />
510
+ <PdfInvoice
511
+ transaction={tx}
512
+ className="p-2 bg-white text-red-600 border border-red-600 rounded-md hover:bg-red-50 transition-colors flex items-center justify-center shadow-sm"
513
+ >
514
+ <Download size={16} />
515
+ </PdfInvoice>
516
+ </div>
517
+ </td>
518
+ </tr>
519
+ ))
520
+ ) : (
521
+ <tr>
522
+ <td colSpan={8} className="px-6 py-8 text-center text-gray-500">
523
+ No transactions found for this party.
524
+ </td>
525
+ </tr>
526
+ )}
527
+ </tbody>
528
+ </table>
529
+ </div>
530
+
531
+ {/* Mobile Card View for Detail */}
532
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
533
+ {partyTransactions.length > 0 ? (
534
+ partyTransactions.map(tx => (
535
+ <div key={tx.id} className="p-4 space-y-3">
536
+ <div className="flex justify-between items-start">
537
+ <div className="flex items-start gap-3">
538
+ <input
539
+ type="checkbox"
540
+ checked={selectedTransactions.includes(tx.id)}
541
+ onChange={(e) => {
542
+ if (e.target.checked) {
543
+ setSelectedTransactions([...selectedTransactions, tx.id]);
544
+ } else {
545
+ setSelectedTransactions(selectedTransactions.filter(id => id !== tx.id));
546
+ }
547
+ }}
548
+ className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500 mt-1"
549
+ />
550
+ <div>
551
+ <div className="text-xs text-gray-500">{tx.bill_date?.split('T')[0] || tx.bill_date}</div>
552
+ <div className="font-mono text-sm font-medium text-gray-800">{tx.bill_number}</div>
553
+ </div>
554
+ </div>
555
+ {tx.is_return && (
556
+ <span className="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs font-semibold">Returned</span>
557
+ )}
558
+ </div>
559
+
560
+ <div className="text-sm text-gray-600">
561
+ <span className="font-medium text-gray-500 text-xs block mb-1">Items:</span>
562
+ {tx.items.map((i, idx) => (
563
+ <div key={idx}>
564
+ {i.mirchi_name}
565
+ {i.lot_number && (
566
+ <span className="ml-2 text-xs font-mono text-gray-500">({i.lot_number})</span>
567
+ )}
568
+ </div>
569
+ ))}
570
+ </div>
571
+
572
+ <div className="grid grid-cols-3 gap-2 text-xs pt-2 border-t border-gray-50">
573
+ <div>
574
+ <div className="text-gray-500">Bill Amount</div>
575
+ <div className="font-medium">₹{tx.total_amount.toLocaleString()}</div>
576
+ </div>
577
+ <div>
578
+ <div className="text-gray-500">Paid</div>
579
+ <div className="font-medium text-green-600">₹{tx.paid_amount.toLocaleString()}</div>
580
+ </div>
581
+ <div>
582
+ <div className="text-gray-500">Due</div>
583
+ <div className="font-bold text-red-500">₹{tx.balance_amount.toLocaleString()}</div>
584
+ </div>
585
+ </div>
586
+
587
+ {tx.balance_amount > 0 && (
588
+ <div className="pt-2">
589
+ {editingTxId === tx.id ? (
590
+ <div className="flex items-center gap-2">
591
+ <input
592
+ type="number"
593
+ className="flex-1 border border-teal-300 rounded px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-teal-500"
594
+ placeholder="Enter Amount"
595
+ value={paymentAmount}
596
+ onChange={e => setPaymentAmount(e.target.value)}
597
+ autoFocus
598
+ />
599
+ <button
600
+ onClick={() => handleUpdatePayment(tx)}
601
+ className="p-2 bg-teal-600 text-white rounded hover:bg-teal-700"
602
+ >
603
+ <Save size={16} />
604
+ </button>
605
+ <button
606
+ onClick={() => { setEditingTxId(null); setPaymentAmount(''); }}
607
+ className="p-2 bg-gray-200 text-gray-600 rounded hover:bg-gray-300"
608
+ >
609
+ <X size={16} />
610
+ </button>
611
+ </div>
612
+ ) : (
613
+ <button
614
+ onClick={() => { setEditingTxId(tx.id); setPaymentAmount(''); }}
615
+ className="w-full py-2 border border-teal-600 text-teal-600 rounded-lg text-sm font-medium hover:bg-teal-50 transition-colors"
616
+ >
617
+ Update Due
618
+ </button>
619
+ )}
620
+ </div>
621
+ )}
622
+
623
+ {/* Print Button for Mobile */}
624
+ <div className="pt-2 border-t border-gray-100 mt-2 flex gap-3 items-center">
625
+ <PrintInvoice
626
+ transaction={tx}
627
+ className="p-2 bg-white text-teal-600 border border-teal-600 rounded-md hover:bg-teal-50 transition-colors flex items-center justify-center shadow-sm"
628
+ />
629
+ <PdfInvoice
630
+ transaction={tx}
631
+ className="p-2 bg-white text-red-600 border border-red-600 rounded-md hover:bg-red-50 transition-colors flex items-center justify-center shadow-sm"
632
+ >
633
+ <Download size={16} />
634
+ </PdfInvoice>
635
+ </div>
636
+ </div>
637
+ ))
638
+ ) : (
639
+ <div className="p-8 text-center text-gray-500">
640
+ No transactions found.
641
+ </div>
642
+ )}
643
+ </div>
644
+ </>
645
+ )}
646
+ </div>
647
+ </div >
648
+ );
649
+ };
650
+
651
  export default PartyLedger;
pages/Settings.tsx CHANGED
@@ -1,317 +1,317 @@
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
- import { usePWA } from '../context/PWAContext';
11
-
12
- const Settings = () => {
13
- const [activeTab, setActiveTab] = useState<'parties' | 'mirchi' | 'general'>('parties');
14
- const [parties, setParties] = useState<Party[]>([]);
15
- const [mirchiTypes, setMirchiTypes] = useState<MirchiType[]>([]);
16
-
17
- // PWA Install
18
- const { isInstallable, installApp } = usePWA();
19
-
20
- // Form States
21
- const [newParty, setNewParty] = useState<Partial<Party>>({
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
-
46
- const handleSaveParty = async () => {
47
- if (!newParty.name) {
48
- setPartyMessage({ type: 'error', text: 'Party Name is required.' });
49
- return;
50
- }
51
-
52
- const party: Party = {
53
- id: newParty.id || `p-${Date.now()}`,
54
- name: newParty.name,
55
- city: newParty.city || '',
56
- phone: newParty.phone || '',
57
- party_type: newParty.party_type as PartyType,
58
- current_balance: parseFloat(String(newParty.current_balance || 0))
59
- };
60
-
61
- await saveParty(party);
62
- setNewParty({ name: '', city: '', phone: '', party_type: PartyType.BOTH, current_balance: 0 });
63
- loadData();
64
- setPartyMessage({ type: 'success', text: 'Party saved successfully.' });
65
- };
66
-
67
- const handleSaveMirchi = async () => {
68
- if (!newMirchi.name) {
69
- setMirchiMessage({ type: 'error', text: 'Mirchi type name is required.' });
70
- return;
71
- }
72
-
73
- const type: MirchiType = {
74
- id: newMirchi.id || `m-${Date.now()}`,
75
- name: newMirchi.name,
76
- current_rate: 0
77
- };
78
-
79
- const result = await apiSaveMirchiType(type);
80
- if (!result.success) {
81
- setMirchiMessage({ type: 'error', text: result.message || 'Error saving mirchi type.' });
82
- return;
83
- }
84
- setNewMirchi({ name: '' });
85
- loadData();
86
- setMirchiMessage({ type: 'success', text: 'Mirchi type saved successfully.' });
87
- };
88
-
89
- const loadData = async () => {
90
- setParties(await getParties());
91
- setMirchiTypes(await apiGetMirchiTypes());
92
- };
93
-
94
- const handleInstallClick = async () => {
95
- await installApp();
96
- };
97
-
98
- return (
99
- <div className="flex flex-col md:flex-row h-full gap-6">
100
- {/* Sidebar / Tabs */}
101
- <div className="w-full md:w-64 bg-white rounded-xl shadow-sm border border-gray-100 p-4 h-fit">
102
- <h2 className="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
103
- <SettingsIcon className="text-teal-600" size={20} /> सेटिंग्स
104
- </h2>
105
- <div className="space-y-2">
106
- <button
107
- onClick={() => setActiveTab('parties')}
108
- 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'}`}
109
- >
110
- <Users size={18} /> पार्टी व्यवस्थापन
111
- </button>
112
- <button
113
- onClick={() => setActiveTab('mirchi')}
114
- 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'}`}
115
- >
116
- <Sprout size={18} /> मिरची दर & प्रकार
117
- </button>
118
- <button
119
- onClick={() => setActiveTab('general')}
120
- 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'}`}
121
- >
122
- <Bell size={18} /> Alerts & Rules
123
- </button>
124
- </div>
125
- </div>
126
-
127
- {/* Content Area */}
128
- <div className="flex-1 bg-white rounded-xl shadow-sm border border-gray-100 p-6 overflow-y-auto no-scrollbar">
129
-
130
- {/* PARTIES TAB */}
131
- {activeTab === 'parties' && (
132
- <div className="space-y-6">
133
- <div className="border-b pb-4">
134
- <h3 className="text-lg font-bold text-gray-800">Party व्यवस्थापन (Party Management)</h3>
135
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
136
- <input
137
- placeholder="Party Name"
138
- className="border rounded p-2"
139
- value={newParty.name}
140
- onChange={e => setNewParty({ ...newParty, name: e.target.value })}
141
- />
142
- <input
143
- placeholder="Phone"
144
- className="border rounded p-2"
145
- value={newParty.phone}
146
- onChange={e => setNewParty({ ...newParty, phone: e.target.value })}
147
- />
148
- <select
149
- className="border rounded p-2"
150
- value={newParty.party_type}
151
- onChange={e => setNewParty({ ...newParty, party_type: e.target.value as PartyType })}
152
- >
153
- <option value={PartyType.BOTH}>Both (Purchase & Sales)</option>
154
- <option value={PartyType.AWAAK}>Only Awaak (Purchase)</option>
155
- <option value={PartyType.JAWAAK}>Only Jawaak (Sales)</option>
156
- </select>
157
- <button
158
- onClick={handleSaveParty}
159
- className="bg-teal-600 text-white rounded p-2 flex items-center justify-center gap-2 font-medium hover:bg-teal-700"
160
- >
161
- <Plus size={18} /> Save Party
162
- </button>
163
- </div>
164
- {partyMessage && (
165
- <div
166
- className={`mt-3 text-sm rounded px-3 py-2 ${partyMessage.type === 'error'
167
- ? 'bg-red-50 text-red-700 border border-red-100'
168
- : 'bg-green-50 text-green-700 border border-green-100'
169
- }`}
170
- >
171
- {partyMessage.text}
172
- </div>
173
- )}
174
- </div>
175
-
176
- <div>
177
- <h3 className="font-semibold text-gray-700 mb-3">All Parties</h3>
178
- <div className="overflow-x-auto">
179
- <table className="w-full text-sm text-left">
180
- <thead className="bg-gray-50 text-gray-500">
181
- <tr>
182
- <th className="p-2">Name</th>
183
- <th className="p-2">Phone</th>
184
- <th className="p-2">Type</th>
185
- <th className="p-2 text-center">Action</th>
186
- </tr>
187
- </thead>
188
- <tbody className="divide-y">
189
- {parties.map(p => (
190
- <tr key={p.id}>
191
- <td className="p-2">{p.name}</td>
192
- <td className="p-2">{p.phone}</td>
193
- <td className="p-2">{p.party_type}</td>
194
- <td className="p-2 text-center">
195
- <button
196
- onClick={() => setNewParty(p)}
197
- className="text-blue-600 hover:underline text-xs"
198
- >
199
- Edit
200
- </button>
201
- </td>
202
- </tr>
203
- ))}
204
- </tbody>
205
- </table>
206
- </div>
207
- </div>
208
- </div>
209
- )}
210
-
211
- {/* MIRCHI TAB */}
212
- {activeTab === 'mirchi' && (
213
- <div className="space-y-6">
214
- <div className="border-b pb-4">
215
- <h3 className="text-lg font-bold text-gray-800">मिरची प्रकार (Mirchi Types)</h3>
216
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
217
- <input
218
- placeholder="Variety Name (e.g. Teja)"
219
- className="border rounded p-2"
220
- value={newMirchi.name}
221
- onChange={e => setNewMirchi({ ...newMirchi, name: e.target.value })}
222
- />
223
- <button
224
- onClick={handleSaveMirchi}
225
- className="bg-teal-600 text-white rounded p-2 flex items-center justify-center gap-2 font-medium hover:bg-teal-700"
226
- >
227
- <Plus size={18} /> Save Type
228
- </button>
229
- </div>
230
- {mirchiMessage && (
231
- <div
232
- className={`mt-3 text-sm rounded px-3 py-2 ${mirchiMessage.type === 'error'
233
- ? 'bg-red-50 text-red-700 border border-red-100'
234
- : 'bg-green-50 text-green-700 border border-green-100'
235
- }`}
236
- >
237
- {mirchiMessage.text}
238
- </div>
239
- )}
240
- </div>
241
-
242
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
243
- {mirchiTypes.map(m => (
244
- <div key={m.id} className="p-4 border rounded-lg hover:shadow-md transition bg-gray-50">
245
- <div className="flex justify-between items-start mb-2">
246
- <h4 className="font-bold text-gray-800">{m.name}</h4>
247
- <button
248
- onClick={() => setNewMirchi(m)}
249
- className="text-teal-600 text-xs font-medium"
250
- >
251
- Edit
252
- </button>
253
- </div>
254
- <div className="text-xs text-gray-500 mt-1">Mirchi Type</div>
255
- </div>
256
- ))}
257
- </div>
258
- </div>
259
- )}
260
-
261
- {/* General / Alerts Tab */}
262
- {activeTab === 'general' && (
263
- <div className="space-y-6">
264
- <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
265
- <h3 className="text-lg font-semibold text-gray-800 mb-4">Install as App</h3>
266
- <p className="text-sm text-gray-600 mb-4">
267
- Install this application on your device for quick access and offline support.
268
- </p>
269
- {isInstallable ? (
270
- <button
271
- onClick={handleInstallClick}
272
- 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"
273
- >
274
- <Download size={20} />
275
- Install App
276
- </button>
277
- ) : (
278
- <div className="text-sm text-gray-500 bg-gray-50 p-4 rounded-lg border border-gray-200">
279
- ✅ App is already installed or not available for installation on this device.
280
- </div>
281
- )}
282
- </div>
283
-
284
- <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
285
- <h3 className="text-lg font-semibold text-gray-800 mb-4">Alert Configuration</h3>
286
- <div className="space-y-4">
287
- <div>
288
- <label className="block text-sm font-medium text-gray-700 mb-2">Low Stock Threshold (kg)</label>
289
- <input
290
- type="number"
291
- value={config.lowStockThreshold}
292
- onChange={(e) => setConfig({ ...config, lowStockThreshold: parseFloat(e.target.value) })}
293
- className="w-full border border-gray-300 rounded-lg p-2"
294
- />
295
- </div>
296
- <div className="flex items-center gap-3">
297
- <input
298
- type="checkbox"
299
- checked={config.enableNotifications}
300
- onChange={(e) => setConfig({ ...config, enableNotifications: e.target.checked })}
301
- className="w-4 h-4 text-teal-600 rounded"
302
- />
303
- <label className="text-sm text-gray-700">Enable Notifications</label>
304
- </div>
305
- <button className="bg-gray-800 text-white px-4 py-2 rounded text-sm hover:bg-gray-900">
306
- Save Config
307
- </button>
308
- </div>
309
- </div>
310
- </div>
311
- )}
312
- </div>
313
- </div>
314
- );
315
- };
316
-
317
  export default Settings;
 
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
+ import { usePWA } from '../context/PWAContext';
11
+
12
+ const Settings = () => {
13
+ const [activeTab, setActiveTab] = useState<'parties' | 'mirchi' | 'general'>('parties');
14
+ const [parties, setParties] = useState<Party[]>([]);
15
+ const [mirchiTypes, setMirchiTypes] = useState<MirchiType[]>([]);
16
+
17
+ // PWA Install
18
+ const { isInstallable, installApp } = usePWA();
19
+
20
+ // Form States
21
+ const [newParty, setNewParty] = useState<Partial<Party>>({
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
+
46
+ const handleSaveParty = async () => {
47
+ if (!newParty.name) {
48
+ setPartyMessage({ type: 'error', text: 'Party Name is required.' });
49
+ return;
50
+ }
51
+
52
+ const party: Party = {
53
+ id: newParty.id || `p-${Date.now()}`,
54
+ name: newParty.name,
55
+ city: newParty.city || '',
56
+ phone: newParty.phone || '',
57
+ party_type: newParty.party_type as PartyType,
58
+ current_balance: parseFloat(String(newParty.current_balance || 0))
59
+ };
60
+
61
+ await saveParty(party);
62
+ setNewParty({ name: '', city: '', phone: '', party_type: PartyType.BOTH, current_balance: 0 });
63
+ loadData();
64
+ setPartyMessage({ type: 'success', text: 'Party saved successfully.' });
65
+ };
66
+
67
+ const handleSaveMirchi = async () => {
68
+ if (!newMirchi.name) {
69
+ setMirchiMessage({ type: 'error', text: 'Mirchi type name is required.' });
70
+ return;
71
+ }
72
+
73
+ const type: MirchiType = {
74
+ id: newMirchi.id || `m-${Date.now()}`,
75
+ name: newMirchi.name,
76
+ current_rate: 0
77
+ };
78
+
79
+ const result = await apiSaveMirchiType(type);
80
+ if (!result.success) {
81
+ setMirchiMessage({ type: 'error', text: result.message || 'Error saving mirchi type.' });
82
+ return;
83
+ }
84
+ setNewMirchi({ name: '' });
85
+ loadData();
86
+ setMirchiMessage({ type: 'success', text: 'Mirchi type saved successfully.' });
87
+ };
88
+
89
+ const loadData = async () => {
90
+ setParties(await getParties());
91
+ setMirchiTypes(await apiGetMirchiTypes());
92
+ };
93
+
94
+ const handleInstallClick = async () => {
95
+ await installApp();
96
+ };
97
+
98
+ return (
99
+ <div className="flex flex-col md:flex-row h-full gap-6">
100
+ {/* Sidebar / Tabs */}
101
+ <div className="w-full md:w-64 bg-white rounded-xl shadow-sm border border-gray-100 p-4 h-fit">
102
+ <h2 className="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
103
+ <SettingsIcon className="text-teal-600" size={20} /> सेटिंग्स
104
+ </h2>
105
+ <div className="space-y-2">
106
+ <button
107
+ onClick={() => setActiveTab('parties')}
108
+ 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'}`}
109
+ >
110
+ <Users size={18} /> पार्टी व्यवस्थापन
111
+ </button>
112
+ <button
113
+ onClick={() => setActiveTab('mirchi')}
114
+ 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'}`}
115
+ >
116
+ <Sprout size={18} /> मिरची दर & प्रकार
117
+ </button>
118
+ <button
119
+ onClick={() => setActiveTab('general')}
120
+ 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'}`}
121
+ >
122
+ <Bell size={18} /> Alerts & Rules
123
+ </button>
124
+ </div>
125
+ </div>
126
+
127
+ {/* Content Area */}
128
+ <div className="flex-1 bg-white rounded-xl shadow-sm border border-gray-100 p-6 overflow-y-auto no-scrollbar">
129
+
130
+ {/* PARTIES TAB */}
131
+ {activeTab === 'parties' && (
132
+ <div className="space-y-6">
133
+ <div className="border-b pb-4">
134
+ <h3 className="text-lg font-bold text-gray-800">Party व्यवस्थापन (Party Management)</h3>
135
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
136
+ <input
137
+ placeholder="Party Name"
138
+ className="border rounded p-2"
139
+ value={newParty.name}
140
+ onChange={e => setNewParty({ ...newParty, name: e.target.value })}
141
+ />
142
+ <input
143
+ placeholder="Phone"
144
+ className="border rounded p-2"
145
+ value={newParty.phone}
146
+ onChange={e => setNewParty({ ...newParty, phone: e.target.value })}
147
+ />
148
+ <select
149
+ className="border rounded p-2"
150
+ value={newParty.party_type}
151
+ onChange={e => setNewParty({ ...newParty, party_type: e.target.value as PartyType })}
152
+ >
153
+ <option value={PartyType.BOTH}>Both (Purchase & Sales)</option>
154
+ <option value={PartyType.AWAAK}>Only Awaak (Purchase)</option>
155
+ <option value={PartyType.JAWAAK}>Only Jawaak (Sales)</option>
156
+ </select>
157
+ <button
158
+ onClick={handleSaveParty}
159
+ className="bg-teal-600 text-white rounded p-2 flex items-center justify-center gap-2 font-medium hover:bg-teal-700"
160
+ >
161
+ <Plus size={18} /> Save Party
162
+ </button>
163
+ </div>
164
+ {partyMessage && (
165
+ <div
166
+ className={`mt-3 text-sm rounded px-3 py-2 ${partyMessage.type === 'error'
167
+ ? 'bg-red-50 text-red-700 border border-red-100'
168
+ : 'bg-green-50 text-green-700 border border-green-100'
169
+ }`}
170
+ >
171
+ {partyMessage.text}
172
+ </div>
173
+ )}
174
+ </div>
175
+
176
+ <div>
177
+ <h3 className="font-semibold text-gray-700 mb-3">All Parties</h3>
178
+ <div className="overflow-x-auto">
179
+ <table className="w-full text-sm text-left">
180
+ <thead className="bg-gray-50 text-gray-500">
181
+ <tr>
182
+ <th className="p-2">Name</th>
183
+ <th className="p-2">Phone</th>
184
+ <th className="p-2">Type</th>
185
+ <th className="p-2 text-center">Action</th>
186
+ </tr>
187
+ </thead>
188
+ <tbody className="divide-y">
189
+ {parties.map(p => (
190
+ <tr key={p.id}>
191
+ <td className="p-2">{p.name}</td>
192
+ <td className="p-2">{p.phone}</td>
193
+ <td className="p-2">{p.party_type}</td>
194
+ <td className="p-2 text-center">
195
+ <button
196
+ onClick={() => setNewParty(p)}
197
+ className="text-blue-600 hover:underline text-xs"
198
+ >
199
+ Edit
200
+ </button>
201
+ </td>
202
+ </tr>
203
+ ))}
204
+ </tbody>
205
+ </table>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ )}
210
+
211
+ {/* MIRCHI TAB */}
212
+ {activeTab === 'mirchi' && (
213
+ <div className="space-y-6">
214
+ <div className="border-b pb-4">
215
+ <h3 className="text-lg font-bold text-gray-800">मिरची प्रकार (Mirchi Types)</h3>
216
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
217
+ <input
218
+ placeholder="Variety Name (e.g. Teja)"
219
+ className="border rounded p-2"
220
+ value={newMirchi.name}
221
+ onChange={e => setNewMirchi({ ...newMirchi, name: e.target.value })}
222
+ />
223
+ <button
224
+ onClick={handleSaveMirchi}
225
+ className="bg-teal-600 text-white rounded p-2 flex items-center justify-center gap-2 font-medium hover:bg-teal-700"
226
+ >
227
+ <Plus size={18} /> Save Type
228
+ </button>
229
+ </div>
230
+ {mirchiMessage && (
231
+ <div
232
+ className={`mt-3 text-sm rounded px-3 py-2 ${mirchiMessage.type === 'error'
233
+ ? 'bg-red-50 text-red-700 border border-red-100'
234
+ : 'bg-green-50 text-green-700 border border-green-100'
235
+ }`}
236
+ >
237
+ {mirchiMessage.text}
238
+ </div>
239
+ )}
240
+ </div>
241
+
242
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
243
+ {mirchiTypes.map(m => (
244
+ <div key={m.id} className="p-4 border rounded-lg hover:shadow-md transition bg-gray-50">
245
+ <div className="flex justify-between items-start mb-2">
246
+ <h4 className="font-bold text-gray-800">{m.name}</h4>
247
+ <button
248
+ onClick={() => setNewMirchi(m)}
249
+ className="text-teal-600 text-xs font-medium"
250
+ >
251
+ Edit
252
+ </button>
253
+ </div>
254
+ <div className="text-xs text-gray-500 mt-1">Mirchi Type</div>
255
+ </div>
256
+ ))}
257
+ </div>
258
+ </div>
259
+ )}
260
+
261
+ {/* General / Alerts Tab */}
262
+ {activeTab === 'general' && (
263
+ <div className="space-y-6">
264
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
265
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">Install as App</h3>
266
+ <p className="text-sm text-gray-600 mb-4">
267
+ Install this application on your device for quick access and offline support.
268
+ </p>
269
+ {isInstallable ? (
270
+ <button
271
+ onClick={handleInstallClick}
272
+ 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"
273
+ >
274
+ <Download size={20} />
275
+ Install App
276
+ </button>
277
+ ) : (
278
+ <div className="text-sm text-gray-500 bg-gray-50 p-4 rounded-lg border border-gray-200">
279
+ ✅ App is already installed or not available for installation on this device.
280
+ </div>
281
+ )}
282
+ </div>
283
+
284
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
285
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">Alert Configuration</h3>
286
+ <div className="space-y-4">
287
+ <div>
288
+ <label className="block text-sm font-medium text-gray-700 mb-2">Low Stock Threshold (kg)</label>
289
+ <input
290
+ type="number"
291
+ value={config.lowStockThreshold}
292
+ onChange={(e) => setConfig({ ...config, lowStockThreshold: parseFloat(e.target.value) })}
293
+ className="w-full border border-gray-300 rounded-lg p-2"
294
+ />
295
+ </div>
296
+ <div className="flex items-center gap-3">
297
+ <input
298
+ type="checkbox"
299
+ checked={config.enableNotifications}
300
+ onChange={(e) => setConfig({ ...config, enableNotifications: e.target.checked })}
301
+ className="w-4 h-4 text-teal-600 rounded"
302
+ />
303
+ <label className="text-sm text-gray-700">Enable Notifications</label>
304
+ </div>
305
+ <button className="bg-gray-800 text-white px-4 py-2 rounded text-sm hover:bg-gray-900">
306
+ Save Config
307
+ </button>
308
+ </div>
309
+ </div>
310
+ </div>
311
+ )}
312
+ </div>
313
+ </div>
314
+ );
315
+ };
316
+
317
  export default Settings;
pages/StockReport.tsx CHANGED
@@ -1,451 +1,454 @@
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 aggregatedByLot = useMemo(() => {
35
- // Calculate bag count for each lot from transactions
36
- return filteredLots.map(lot => {
37
- let totalBags = 0;
38
-
39
- // Calculate bags from all transactions for this lot
40
- transactions.forEach(tx => {
41
- tx.items.forEach(item => {
42
- if (item.lot_id === lot.id) {
43
- if (tx.bill_type === BillType.AWAAK) {
44
- // Purchase: Add bags
45
- totalBags += tx.is_return ? -item.poti_count : item.poti_count;
46
- } else {
47
- // Sale: Subtract bags
48
- totalBags += tx.is_return ? item.poti_count : -item.poti_count;
49
- }
50
- }
51
- });
52
- });
53
-
54
- return {
55
- lotId: lot.id,
56
- lotNumber: lot.lot_number,
57
- mirchiName: lot.mirchi_name,
58
- totalQuantity: lot.total_quantity,
59
- remainingQty: lot.remaining_quantity,
60
- totalBags: Math.max(0, totalBags),
61
- status: lot.status,
62
- purchaseDate: lot.purchase_date,
63
- avgRate: lot.avg_rate
64
- };
65
- });
66
- }, [filteredLots, transactions]);
67
-
68
- const detailMovements = useMemo(() => {
69
- if (!selectedMirchiId) return [];
70
-
71
- const rows: {
72
- id: string;
73
- date: string;
74
- billNo: string;
75
- partyName: string;
76
- inQty: number;
77
- outQty: number;
78
- inBags: number;
79
- outBags: number;
80
- typeLabel: string;
81
- isReturn: boolean;
82
- }[] = [];
83
-
84
- transactions.forEach((tx) => {
85
- tx.items
86
- .filter((item) => item.lot_id === selectedMirchiId) // Filter by lot_id instead of mirchi_type_id
87
- .forEach((item) => {
88
- const party = parties.find((p) => p.id === tx.party_id);
89
- let inQty = 0;
90
- let outQty = 0;
91
- let inBags = 0;
92
- let outBags = 0;
93
- let typeLabel = '';
94
-
95
- if (tx.bill_type === BillType.AWAAK) {
96
- // Awaak = Purchase / Stock IN
97
- if (tx.is_return) {
98
- // Purchase Return: Stock OUT
99
- outQty = item.net_weight;
100
- outBags = item.poti_count;
101
- typeLabel = 'Purchase Return';
102
- } else {
103
- inQty = item.net_weight;
104
- inBags = item.poti_count;
105
- typeLabel = 'Purchase';
106
- }
107
- } else {
108
- // Jawaak = Sales / Stock OUT
109
- if (tx.is_return) {
110
- // Sales Return: Stock IN
111
- inQty = item.net_weight;
112
- inBags = item.poti_count;
113
- typeLabel = 'Sales Return';
114
- } else {
115
- outQty = item.net_weight;
116
- outBags = item.poti_count;
117
- typeLabel = 'Sale';
118
- }
119
- }
120
-
121
- rows.push({
122
- id: `${tx.id}-${item.id}`,
123
- date: tx.bill_date,
124
- billNo: tx.bill_number,
125
- partyName: party?.name || tx.party_name || 'Unknown Party',
126
- inQty,
127
- outQty,
128
- inBags,
129
- outBags,
130
- typeLabel,
131
- isReturn: tx.is_return,
132
- });
133
- });
134
- });
135
-
136
- // Sort latest first
137
- rows.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
138
- return rows;
139
- }, [selectedMirchiId, transactions, parties]);
140
-
141
- return (
142
- <div className="space-y-4">
143
- <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
144
- <div className="p-4 md:p-6 border-b flex flex-col md:flex-row md:items-center justify-between gap-4">
145
- <div className="flex items-center gap-2">
146
- {viewMode === 'detail' && (
147
- <button
148
- onClick={() => {
149
- setViewMode('list');
150
- setSelectedMirchiId(null);
151
- setSelectedMirchiName(null);
152
- }}
153
- className="mr-1 p-1 hover:bg-gray-100 rounded-full"
154
- >
155
- <ArrowLeft size={20} className="text-gray-600" />
156
- </button>
157
- )}
158
- <Package className="text-teal-600" />
159
- <h2 className="text-lg md:text-xl font-bold text-gray-800">
160
- {viewMode === 'list'
161
- ? 'स्टॉक रिपोर्ट (Stock Inventory)'
162
- : `${selectedMirchiName ?? ''} - Stock Detail`}
163
- </h2>
164
- </div>
165
-
166
- {viewMode === 'list' && (
167
- <div className="relative w-full md:w-auto">
168
- <Search
169
- className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
170
- size={18}
171
- />
172
- <input
173
- type="text"
174
- placeholder="Search Mirchi Jaat / Type..."
175
- 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"
176
- value={searchTerm}
177
- onChange={(e) => setSearchTerm(e.target.value)}
178
- />
179
- </div>
180
- )}
181
- </div>
182
-
183
- {viewMode === 'list' ? (
184
- <>
185
- {/* Desktop table */}
186
- <div className="hidden md:block overflow-x-auto">
187
- <table className="w-full text-sm text-left">
188
- <thead className="bg-gray-50 text-gray-500 font-medium">
189
- <tr>
190
- <th className="px-6 py-4">LOT Number</th>
191
- <th className="px-6 py-4">मिरची जात (Mirchi Type)</th>
192
- <th className="px-6 py-4 text-right">बॅग/पोती (Bags)</th>
193
- <th className="px-6 py-4 text-right">शिल्लक वजन (Remaining Qty)</th>
194
- <th className="px-6 py-4 text-center">स्थिी (Status)</th>
195
- </tr>
196
- </thead>
197
- <tbody className="divide-y divide-gray-100">
198
- {aggregatedByLot.length === 0 ? (
199
- <tr>
200
- <td
201
- colSpan={5}
202
- className="text-center py-8 text-gray-500"
203
- >
204
- No active stock found
205
- </td>
206
- </tr>
207
- ) : (
208
- aggregatedByLot.map((row) => {
209
- const isLow = row.remainingQty < LOW_STOCK_THRESHOLD;
210
- const statusLabel =
211
- row.remainingQty === 0
212
- ? 'Out of Stock'
213
- : isLow
214
- ? 'Low Stock'
215
- : 'Available';
216
- const statusClasses =
217
- row.remainingQty === 0
218
- ? 'bg-red-100 text-red-700'
219
- : isLow
220
- ? 'bg-orange-100 text-orange-700'
221
- : 'bg-green-100 text-green-700';
222
-
223
- return (
224
- <tr
225
- key={row.lotId}
226
- className="hover:bg-gray-50 transition-colors cursor-pointer"
227
- onClick={() => {
228
- setSelectedMirchiId(row.lotId);
229
- setSelectedMirchiName(row.lotNumber);
230
- setViewMode('detail');
231
- }}
232
- >
233
- <td className="px-6 py-4 font-mono text-sm font-medium text-teal-700">
234
- {row.lotNumber}
235
- </td>
236
- <td className="px-6 py-4">
237
- {row.mirchiName}
238
- </td>
239
- <td className="px-6 py-4 text-right">
240
- <span className="font-medium text-gray-700">
241
- {row.totalBags} bags
242
- </span>
243
- </td>
244
- <td className="px-6 py-4 text-right">
245
- <span
246
- className={`font-bold ${isLow ? 'text-red-500' : 'text-gray-800'
247
- }`}
248
- >
249
- {row.remainingQty} kg
250
- </span>
251
- </td>
252
- <td className="px-6 py-4 text-center">
253
- <span
254
- className={`inline-block px-2 py-1 rounded-full text-xs font-semibold ${statusClasses}`}
255
- >
256
- {statusLabel}
257
- </span>
258
- </td>
259
- </tr>
260
- );
261
- })
262
- )}
263
- </tbody>
264
- </table>
265
- </div>
266
-
267
- {/* Mobile cards */}
268
- <div className="md:hidden flex flex-col divide-y divide-gray-100">
269
- {aggregatedByLot.length === 0 ? (
270
- <div className="p-6 text-center text-gray-500 text-sm">
271
- No active stock found
272
- </div>
273
- ) : (
274
- aggregatedByLot.map((row) => {
275
- const isLow = row.remainingQty < LOW_STOCK_THRESHOLD;
276
- const statusLabel =
277
- row.remainingQty === 0
278
- ? 'Out of Stock'
279
- : isLow
280
- ? 'Low Stock'
281
- : 'Available';
282
- const statusClasses =
283
- row.remainingQty === 0
284
- ? 'bg-red-100 text-red-700'
285
- : isLow
286
- ? 'bg-orange-100 text-orange-700'
287
- : 'bg-green-100 text-green-700';
288
-
289
- return (
290
- <button
291
- key={row.lotId}
292
- className="text-left p-4 space-y-2 hover:bg-gray-50 transition-colors"
293
- onClick={() => {
294
- setSelectedMirchiId(row.lotId);
295
- setSelectedMirchiName(row.lotNumber);
296
- setViewMode('detail');
297
- }}
298
- >
299
- <div className="flex items-center justify-between gap-2">
300
- <div>
301
- <div className="text-sm font-mono font-semibold text-teal-700">
302
- {row.lotNumber}
303
- </div>
304
- <div className="text-xs text-gray-500">
305
- {row.mirchiName}
306
- </div>
307
- </div>
308
- <span
309
- className={`px-2 py-1 rounded-full text-[10px] font-semibold ${statusClasses}`}
310
- >
311
- {statusLabel}
312
- </span>
313
- </div>
314
- <div className="grid grid-cols-2 gap-2 text-xs">
315
- <div className="bg-gray-50 p-2 rounded">
316
- <div className="text-gray-500">Bags</div>
317
- <div className="font-medium">{row.totalBags} bags</div>
318
- </div>
319
- <div className="bg-gray-50 p-2 rounded">
320
- <div className="text-gray-500">Remaining Qty</div>
321
- <div
322
- className={`font-bold ${isLow ? 'text-red-500' : 'text-gray-800'
323
- }`}
324
- >
325
- {row.remainingQty} kg
326
- </div>
327
- </div>
328
- </div>
329
- </button>
330
- );
331
- })
332
- )}
333
- </div>
334
- </>
335
- ) : (
336
- <>
337
- {/* Desktop detail table */}
338
- <div className="hidden md:block overflow-x-auto">
339
- <table className="w-full text-sm text-left">
340
- <thead className="bg-gray-50 text-gray-500 font-medium">
341
- <tr>
342
- <th className="px-6 py-4">तारीख (Date)</th>
343
- <th className="px-6 py-4">बिल नंबर (Bill No)</th>
344
- <th className="px-6 py-4">पार्टी (Party)</th>
345
- <th className="px-6 py-4 text-right">बॅग इन (In Bags)</th>
346
- <th className="px-6 py-4 text-right">बॅग आउट (Out Bags)</th>
347
- <th className="px-6 py-4 text-right"> इन (In Qty)</th>
348
- <th className="px-6 py-4 text-right">माल आउट (Out Qty)</th>
349
- <th className="px-6 py-4 text-center">टाइप (Type)</th>
350
- </tr>
351
- </thead>
352
- <tbody className="divide-y divide-gray-100">
353
- {detailMovements.length === 0 ? (
354
- <tr>
355
- <td colSpan={8} className="px-6 py-8 text-center text-gray-500">
356
- No movements found for this Mirchi type.
357
- </td>
358
- </tr>
359
- ) : (
360
- detailMovements.map((row) => (
361
- <tr key={row.id} className="hover:bg-gray-50">
362
- <td className="px-6 py-4 text-gray-600">{row.date}</td>
363
- <td className="px-6 py-4 font-mono text-gray-500">{row.billNo}</td>
364
- <td className="px-6 py-4 text-gray-700">{row.partyName}</td>
365
- <td className="px-6 py-4 text-right text-green-600 font-medium">
366
- {row.inBags > 0 ? `${row.inBags} bags` : '-'}
367
- </td>
368
- <td className="px-6 py-4 text-right text-red-600 font-medium">
369
- {row.outBags > 0 ? `${row.outBags} bags` : '-'}
370
- </td>
371
- <td className="px-6 py-4 text-right text-green-600 font-medium">
372
- {row.inQty > 0 ? `${row.inQty} kg` : '-'}
373
- </td>
374
- <td className="px-6 py-4 text-right text-red-600 font-medium">
375
- {row.outQty > 0 ? `${row.outQty} kg` : '-'}
376
- </td>
377
- <td className="px-6 py-4 text-center">
378
- <span
379
- className={`inline-block px-2 py-1 rounded-full text-xs font-semibold ${row.typeLabel.includes('Return')
380
- ? 'bg-orange-100 text-orange-700'
381
- : row.typeLabel === 'Purchase'
382
- ? 'bg-teal-100 text-teal-700'
383
- : 'bg-blue-100 text-blue-700'
384
- }`}
385
- >
386
- {row.typeLabel}
387
- </span>
388
- </td>
389
- </tr>
390
- ))
391
- )}
392
- </tbody>
393
- </table>
394
- </div>
395
-
396
- {/* Mobile detail cards */}
397
- <div className="md:hidden flex flex-col divide-y divide-gray-100">
398
- {detailMovements.length === 0 ? (
399
- <div className="p-6 text-center text-gray-500 text-sm">
400
- No movements found for this Mirchi type.
401
- </div>
402
- ) : (
403
- detailMovements.map((row) => (
404
- <div key={row.id} className="p-4 space-y-2">
405
- <div className="flex justify-between items-start gap-2">
406
- <div>
407
- <div className="text-xs text-gray-500">{row.date}</div>
408
- <div className="font-mono text-sm font-medium text-gray-800">
409
- {row.billNo}
410
- </div>
411
- <div className="text-xs text-gray-600 mt-1">
412
- {row.partyName}
413
- </div>
414
- </div>
415
- <span
416
- className={`px-2 py-1 rounded-full text-[10px] font-semibold ${row.typeLabel.includes('Return')
417
- ? 'bg-orange-100 text-orange-700'
418
- : row.typeLabel === 'Purchase'
419
- ? 'bg-teal-100 text-teal-700'
420
- : 'bg-blue-100 text-blue-700'
421
- }`}
422
- >
423
- {row.typeLabel}
424
- </span>
425
- </div>
426
- <div className="grid grid-cols-2 gap-2 text-xs pt-2 border-t border-gray-50">
427
- <div className="bg-gray-50 p-2 rounded">
428
- <div className="text-gray-500">माल इन (In)</div>
429
- <div className="font-medium text-green-700">
430
- {row.inQty > 0 ? `${row.inQty} kg` : '-'}
431
- </div>
432
- </div>
433
- <div className="bg-gray-50 p-2 rounded">
434
- <div className="text-gray-500">माल आउट (Out)</div>
435
- <div className="font-medium text-red-600">
436
- {row.outQty > 0 ? `${row.outQty} kg` : '-'}
437
- </div>
438
- </div>
439
- </div>
440
- </div>
441
- ))
442
- )}
443
- </div>
444
- </>
445
- )}
446
- </div>
447
- </div>
448
- );
449
- };
450
-
 
 
 
451
  export default StockReport;
 
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 aggregatedByLot = useMemo(() => {
35
+ // Calculate bag count for each lot from transactions
36
+ return filteredLots.map(lot => {
37
+ let totalBags = 0;
38
+
39
+ // Calculate bags from all transactions for this lot
40
+ transactions.forEach(tx => {
41
+ tx.items.forEach(item => {
42
+ if (item.lot_id === lot.id) {
43
+ if (tx.bill_type === BillType.AWAAK) {
44
+ // Purchase: Add bags
45
+ totalBags += tx.is_return ? -item.poti_count : item.poti_count;
46
+ } else {
47
+ // Sale: Subtract bags
48
+ totalBags += tx.is_return ? item.poti_count : -item.poti_count;
49
+ }
50
+ }
51
+ });
52
+ });
53
+
54
+ return {
55
+ lotId: lot.id,
56
+ lotNumber: lot.lot_number,
57
+ mirchiName: lot.mirchi_name,
58
+ totalQuantity: lot.total_quantity,
59
+ remainingQty: lot.remaining_quantity,
60
+ totalBags: Math.max(0, totalBags),
61
+ status: lot.status,
62
+ purchaseDate: lot.purchase_date,
63
+ avgRate: lot.avg_rate
64
+ };
65
+ });
66
+ }, [filteredLots, transactions]);
67
+
68
+ const detailMovements = useMemo(() => {
69
+ if (!selectedMirchiId) return [];
70
+
71
+ const rows: {
72
+ id: string;
73
+ date: string;
74
+ billNo: string;
75
+ partyName: string;
76
+ inQty: number;
77
+ outQty: number;
78
+ inBags: number;
79
+ outBags: number;
80
+ typeLabel: string;
81
+ isReturn: boolean;
82
+ }[] = [];
83
+
84
+ transactions.forEach((tx) => {
85
+ tx.items
86
+ .filter((item) => item.lot_id === selectedMirchiId) // Filter by lot_id instead of mirchi_type_id
87
+ .forEach((item) => {
88
+ const party = parties.find((p) => p.id === tx.party_id);
89
+
90
+ let inQty = 0;
91
+ let outQty = 0;
92
+ let inBags = 0;
93
+ let outBags = 0;
94
+ let typeLabel = '';
95
+
96
+ if (tx.bill_type === BillType.AWAAK) {
97
+ // Awaak = Purchase / Stock IN
98
+ if (tx.is_return) {
99
+ // Purchase Return: Stock OUT
100
+ outQty = item.net_weight;
101
+ outBags = item.poti_count;
102
+ typeLabel = 'Purchase Return';
103
+ } else {
104
+ inQty = item.net_weight;
105
+ inBags = item.poti_count;
106
+ typeLabel = 'Purchase';
107
+ }
108
+ } else {
109
+ // Jawaak = Sales / Stock OUT
110
+ if (tx.is_return) {
111
+ // Sales Return: Stock IN
112
+ inQty = item.net_weight;
113
+ inBags = item.poti_count;
114
+ typeLabel = 'Sales Return';
115
+ } else {
116
+ outQty = item.net_weight;
117
+ outBags = item.poti_count;
118
+ typeLabel = 'Sale';
119
+ }
120
+ }
121
+
122
+ const displayDate = tx.bill_date ? tx.bill_date.split('T')[0] : tx.bill_date;
123
+
124
+ rows.push({
125
+ id: `${tx.id}-${item.id}`,
126
+ date: displayDate,
127
+ billNo: tx.bill_number,
128
+ partyName: party?.name || tx.party_name || 'Unknown Party',
129
+ inQty,
130
+ outQty,
131
+ inBags,
132
+ outBags,
133
+ typeLabel,
134
+ isReturn: tx.is_return,
135
+ });
136
+ });
137
+ });
138
+
139
+ // Sort latest first
140
+ rows.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
141
+ return rows;
142
+ }, [selectedMirchiId, transactions, parties]);
143
+
144
+ return (
145
+ <div className="space-y-4">
146
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
147
+ <div className="p-4 md:p-6 border-b flex flex-col md:flex-row md:items-center justify-between gap-4">
148
+ <div className="flex items-center gap-2">
149
+ {viewMode === 'detail' && (
150
+ <button
151
+ onClick={() => {
152
+ setViewMode('list');
153
+ setSelectedMirchiId(null);
154
+ setSelectedMirchiName(null);
155
+ }}
156
+ className="mr-1 p-1 hover:bg-gray-100 rounded-full"
157
+ >
158
+ <ArrowLeft size={20} className="text-gray-600" />
159
+ </button>
160
+ )}
161
+ <Package className="text-teal-600" />
162
+ <h2 className="text-lg md:text-xl font-bold text-gray-800">
163
+ {viewMode === 'list'
164
+ ? 'स्टॉक रिपोर्ट (Stock Inventory)'
165
+ : `${selectedMirchiName ?? ''} - Stock Detail`}
166
+ </h2>
167
+ </div>
168
+
169
+ {viewMode === 'list' && (
170
+ <div className="relative w-full md:w-auto">
171
+ <Search
172
+ className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
173
+ size={18}
174
+ />
175
+ <input
176
+ type="text"
177
+ placeholder="Search Mirchi Jaat / Type..."
178
+ 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"
179
+ value={searchTerm}
180
+ onChange={(e) => setSearchTerm(e.target.value)}
181
+ />
182
+ </div>
183
+ )}
184
+ </div>
185
+
186
+ {viewMode === 'list' ? (
187
+ <>
188
+ {/* Desktop table */}
189
+ <div className="hidden md:block overflow-x-auto">
190
+ <table className="w-full text-sm text-left">
191
+ <thead className="bg-gray-50 text-gray-500 font-medium">
192
+ <tr>
193
+ <th className="px-6 py-4">LOT Number</th>
194
+ <th className="px-6 py-4">िरचजात (Mirchi Type)</th>
195
+ <th className="px-6 py-4 text-right">बॅग/पोती (Bags)</th>
196
+ <th className="px-6 py-4 text-right">शिल्लक वजन (Remaining Qty)</th>
197
+ <th className="px-6 py-4 text-center">स्थिती (Status)</th>
198
+ </tr>
199
+ </thead>
200
+ <tbody className="divide-y divide-gray-100">
201
+ {aggregatedByLot.length === 0 ? (
202
+ <tr>
203
+ <td
204
+ colSpan={5}
205
+ className="text-center py-8 text-gray-500"
206
+ >
207
+ No active stock found
208
+ </td>
209
+ </tr>
210
+ ) : (
211
+ aggregatedByLot.map((row) => {
212
+ const isLow = row.remainingQty < LOW_STOCK_THRESHOLD;
213
+ const statusLabel =
214
+ row.remainingQty === 0
215
+ ? 'Out of Stock'
216
+ : isLow
217
+ ? 'Low Stock'
218
+ : 'Available';
219
+ const statusClasses =
220
+ row.remainingQty === 0
221
+ ? 'bg-red-100 text-red-700'
222
+ : isLow
223
+ ? 'bg-orange-100 text-orange-700'
224
+ : 'bg-green-100 text-green-700';
225
+
226
+ return (
227
+ <tr
228
+ key={row.lotId}
229
+ className="hover:bg-gray-50 transition-colors cursor-pointer"
230
+ onClick={() => {
231
+ setSelectedMirchiId(row.lotId);
232
+ setSelectedMirchiName(row.lotNumber);
233
+ setViewMode('detail');
234
+ }}
235
+ >
236
+ <td className="px-6 py-4 font-mono text-sm font-medium text-teal-700">
237
+ {row.lotNumber}
238
+ </td>
239
+ <td className="px-6 py-4">
240
+ {row.mirchiName}
241
+ </td>
242
+ <td className="px-6 py-4 text-right">
243
+ <span className="font-medium text-gray-700">
244
+ {row.totalBags} bags
245
+ </span>
246
+ </td>
247
+ <td className="px-6 py-4 text-right">
248
+ <span
249
+ className={`font-bold ${isLow ? 'text-red-500' : 'text-gray-800'
250
+ }`}
251
+ >
252
+ {row.remainingQty} kg
253
+ </span>
254
+ </td>
255
+ <td className="px-6 py-4 text-center">
256
+ <span
257
+ className={`inline-block px-2 py-1 rounded-full text-xs font-semibold ${statusClasses}`}
258
+ >
259
+ {statusLabel}
260
+ </span>
261
+ </td>
262
+ </tr>
263
+ );
264
+ })
265
+ )}
266
+ </tbody>
267
+ </table>
268
+ </div>
269
+
270
+ {/* Mobile cards */}
271
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
272
+ {aggregatedByLot.length === 0 ? (
273
+ <div className="p-6 text-center text-gray-500 text-sm">
274
+ No active stock found
275
+ </div>
276
+ ) : (
277
+ aggregatedByLot.map((row) => {
278
+ const isLow = row.remainingQty < LOW_STOCK_THRESHOLD;
279
+ const statusLabel =
280
+ row.remainingQty === 0
281
+ ? 'Out of Stock'
282
+ : isLow
283
+ ? 'Low Stock'
284
+ : 'Available';
285
+ const statusClasses =
286
+ row.remainingQty === 0
287
+ ? 'bg-red-100 text-red-700'
288
+ : isLow
289
+ ? 'bg-orange-100 text-orange-700'
290
+ : 'bg-green-100 text-green-700';
291
+
292
+ return (
293
+ <button
294
+ key={row.lotId}
295
+ className="text-left p-4 space-y-2 hover:bg-gray-50 transition-colors"
296
+ onClick={() => {
297
+ setSelectedMirchiId(row.lotId);
298
+ setSelectedMirchiName(row.lotNumber);
299
+ setViewMode('detail');
300
+ }}
301
+ >
302
+ <div className="flex items-center justify-between gap-2">
303
+ <div>
304
+ <div className="text-sm font-mono font-semibold text-teal-700">
305
+ {row.lotNumber}
306
+ </div>
307
+ <div className="text-xs text-gray-500">
308
+ {row.mirchiName}
309
+ </div>
310
+ </div>
311
+ <span
312
+ className={`px-2 py-1 rounded-full text-[10px] font-semibold ${statusClasses}`}
313
+ >
314
+ {statusLabel}
315
+ </span>
316
+ </div>
317
+ <div className="grid grid-cols-2 gap-2 text-xs">
318
+ <div className="bg-gray-50 p-2 rounded">
319
+ <div className="text-gray-500">Bags</div>
320
+ <div className="font-medium">{row.totalBags} bags</div>
321
+ </div>
322
+ <div className="bg-gray-50 p-2 rounded">
323
+ <div className="text-gray-500">Remaining Qty</div>
324
+ <div
325
+ className={`font-bold ${isLow ? 'text-red-500' : 'text-gray-800'
326
+ }`}
327
+ >
328
+ {row.remainingQty} kg
329
+ </div>
330
+ </div>
331
+ </div>
332
+ </button>
333
+ );
334
+ })
335
+ )}
336
+ </div>
337
+ </>
338
+ ) : (
339
+ <>
340
+ {/* Desktop detail table */}
341
+ <div className="hidden md:block overflow-x-auto">
342
+ <table className="w-full text-sm text-left">
343
+ <thead className="bg-gray-50 text-gray-500 font-medium">
344
+ <tr>
345
+ <th className="px-6 py-4">तारीख (Date)</th>
346
+ <th className="px-6 py-4">बिल नंबर (Bill No)</th>
347
+ <th className="px-6 py-4">र्टी (Party)</th>
348
+ <th className="px-6 py-4 text-right">बॅग इन (In Bags)</th>
349
+ <th className="px-6 py-4 text-right">बॅग आउट (Out Bags)</th>
350
+ <th className="px-6 py-4 text-right">माल इन (In Qty)</th>
351
+ <th className="px-6 py-4 text-right">माल ���उट (Out Qty)</th>
352
+ <th className="px-6 py-4 text-center">टाइप (Type)</th>
353
+ </tr>
354
+ </thead>
355
+ <tbody className="divide-y divide-gray-100">
356
+ {detailMovements.length === 0 ? (
357
+ <tr>
358
+ <td colSpan={8} className="px-6 py-8 text-center text-gray-500">
359
+ No movements found for this Mirchi type.
360
+ </td>
361
+ </tr>
362
+ ) : (
363
+ detailMovements.map((row) => (
364
+ <tr key={row.id} className="hover:bg-gray-50">
365
+ <td className="px-6 py-4 text-gray-600">{row.date}</td>
366
+ <td className="px-6 py-4 font-mono text-gray-500">{row.billNo}</td>
367
+ <td className="px-6 py-4 text-gray-700">{row.partyName}</td>
368
+ <td className="px-6 py-4 text-right text-green-600 font-medium">
369
+ {row.inBags > 0 ? `${row.inBags} bags` : '-'}
370
+ </td>
371
+ <td className="px-6 py-4 text-right text-red-600 font-medium">
372
+ {row.outBags > 0 ? `${row.outBags} bags` : '-'}
373
+ </td>
374
+ <td className="px-6 py-4 text-right text-green-600 font-medium">
375
+ {row.inQty > 0 ? `${row.inQty} kg` : '-'}
376
+ </td>
377
+ <td className="px-6 py-4 text-right text-red-600 font-medium">
378
+ {row.outQty > 0 ? `${row.outQty} kg` : '-'}
379
+ </td>
380
+ <td className="px-6 py-4 text-center">
381
+ <span
382
+ className={`inline-block px-2 py-1 rounded-full text-xs font-semibold ${row.typeLabel.includes('Return')
383
+ ? 'bg-orange-100 text-orange-700'
384
+ : row.typeLabel === 'Purchase'
385
+ ? 'bg-teal-100 text-teal-700'
386
+ : 'bg-blue-100 text-blue-700'
387
+ }`}
388
+ >
389
+ {row.typeLabel}
390
+ </span>
391
+ </td>
392
+ </tr>
393
+ ))
394
+ )}
395
+ </tbody>
396
+ </table>
397
+ </div>
398
+
399
+ {/* Mobile detail cards */}
400
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
401
+ {detailMovements.length === 0 ? (
402
+ <div className="p-6 text-center text-gray-500 text-sm">
403
+ No movements found for this Mirchi type.
404
+ </div>
405
+ ) : (
406
+ detailMovements.map((row) => (
407
+ <div key={row.id} className="p-4 space-y-2">
408
+ <div className="flex justify-between items-start gap-2">
409
+ <div>
410
+ <div className="text-xs text-gray-500">{row.date}</div>
411
+ <div className="font-mono text-sm font-medium text-gray-800">
412
+ {row.billNo}
413
+ </div>
414
+ <div className="text-xs text-gray-600 mt-1">
415
+ {row.partyName}
416
+ </div>
417
+ </div>
418
+ <span
419
+ className={`px-2 py-1 rounded-full text-[10px] font-semibold ${row.typeLabel.includes('Return')
420
+ ? 'bg-orange-100 text-orange-700'
421
+ : row.typeLabel === 'Purchase'
422
+ ? 'bg-teal-100 text-teal-700'
423
+ : 'bg-blue-100 text-blue-700'
424
+ }`}
425
+ >
426
+ {row.typeLabel}
427
+ </span>
428
+ </div>
429
+ <div className="grid grid-cols-2 gap-2 text-xs pt-2 border-t border-gray-50">
430
+ <div className="bg-gray-50 p-2 rounded">
431
+ <div className="text-gray-500">माल इन (In)</div>
432
+ <div className="font-medium text-green-700">
433
+ {row.inQty > 0 ? `${row.inQty} kg` : '-'}
434
+ </div>
435
+ </div>
436
+ <div className="bg-gray-50 p-2 rounded">
437
+ <div className="text-gray-500">माल आउट (Out)</div>
438
+ <div className="font-medium text-red-600">
439
+ {row.outQty > 0 ? `${row.outQty} kg` : '-'}
440
+ </div>
441
+ </div>
442
+ </div>
443
+ </div>
444
+ ))
445
+ )}
446
+ </div>
447
+ </>
448
+ )}
449
+ </div>
450
+ </div>
451
+ );
452
+ };
453
+
454
  export default StockReport;
services/db.ts CHANGED
@@ -1,309 +1,354 @@
1
- import {
2
- Party, MirchiType, Lot, Transaction, PartyType,
3
- LotStatus, BillType, ApiResponse, PaymentMode
4
- } from '../types';
5
-
6
- const API_BASE = '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
- // Check if lot number is unique
189
- export const checkLotUnique = async (lotNumber: string): Promise<boolean> => {
190
- try {
191
- const res = await fetch(`${API_BASE}/lots/check-unique`, {
192
- method: 'POST',
193
- headers: { 'Content-Type': 'application/json' },
194
- body: JSON.stringify({ lot_number: lotNumber }),
195
- });
196
- if (!res.ok) throw new Error('Failed to check lot uniqueness');
197
- const data = await res.json();
198
- return !data.exists; // Return true if unique (not exists)
199
- } catch (error) {
200
- console.error('Error checking lot uniqueness:', error);
201
- return false;
202
- }
203
- };
204
-
205
- // Get available lots for a specific mirchi type
206
- export const getAvailableLotsByMirchi = async (mirchiTypeId: string): Promise<Lot[]> => {
207
- try {
208
- const res = await fetch(`${API_BASE}/lots/available/${mirchiTypeId}`);
209
- if (!res.ok) throw new Error('Failed to load available lots');
210
- const data = await res.json();
211
- return data.map((l: any) => ({
212
- ...l,
213
- total_quantity: parseNumeric(l.total_quantity),
214
- remaining_quantity: parseNumeric(l.remaining_quantity),
215
- avg_rate: parseNumeric(l.avg_rate)
216
- }));
217
- } catch (error) {
218
- console.error('Error fetching available lots:', error);
219
- return [];
220
- }
221
- };
222
-
223
- // Transactions API
224
- export const getTransactions = async (): Promise<Transaction[]> => {
225
- try {
226
- const res = await fetch(`${API_BASE}/transactions`);
227
- if (!res.ok) throw new Error('Failed to load transactions');
228
- const data = await res.json();
229
- return data.map(normalizeTransaction);
230
- } catch (error) {
231
- console.error('Error fetching transactions:', error);
232
- return [];
233
- }
234
- };
235
-
236
- export const generateBillNumber = (type: BillType, isReturn: boolean = false): string => {
237
- const prefix = type === BillType.JAWAAK ? (isReturn ? 'JAWAAK-RET' : 'JAWAAK') : (isReturn ? 'AWAAK-RET' : 'AWAAK');
238
- const timestamp = Date.now();
239
- return `${prefix}-${new Date().getFullYear()}-${String(timestamp).slice(-4)}`;
240
- };
241
-
242
- export const saveTransaction = async (transaction: Transaction): Promise<ApiResponse<Transaction>> => {
243
- try {
244
- // Validate Payload
245
- if (!transaction.party_id) return { success: false, message: "Party is required" };
246
- if (!transaction.items || transaction.items.length === 0) return { success: false, message: "At least one item is required" };
247
-
248
- console.log('=== SAVING TRANSACTION ===');
249
- console.log('Transaction ID:', transaction.id);
250
- console.log('Items count:', transaction.items?.length);
251
- console.log('Items data:', JSON.stringify(transaction.items, null, 2));
252
-
253
- const res = await fetch(`${API_BASE}/transactions`, {
254
- method: 'POST',
255
- headers: { 'Content-Type': 'application/json' },
256
- body: JSON.stringify(transaction)
257
- });
258
-
259
- if (!res.ok) {
260
- const error = await res.json();
261
- console.error('Save transaction error:', error);
262
- throw new Error(error.message || 'Failed to save transaction');
263
- }
264
-
265
- const result = await res.json();
266
- console.log('=== SAVE RESPONSE ===');
267
- console.log('Response data:', result.data);
268
- console.log('Response items count:', result.data?.items?.length);
269
- console.log('Response items:', JSON.stringify(result.data?.items, null, 2));
270
-
271
- return {
272
- success: true,
273
- data: normalizeTransaction(result.data),
274
- message: result.message
275
- };
276
- } catch (error: any) {
277
- console.error('Error saving transaction:', error);
278
- return {
279
- success: false,
280
- message: error.message || 'Failed to save transaction'
281
- };
282
- }
283
- };
284
-
285
- export const updateTransactionPayment = async (transactionId: string, amount: number): Promise<ApiResponse<Transaction>> => {
286
- try {
287
- // Validate amount
288
- if (amount <= 0) return { success: false, message: "Amount must be greater than 0" };
289
-
290
- const res = await fetch(`${API_BASE}/transactions/${transactionId}/payment`, {
291
- method: 'PATCH',
292
- headers: { 'Content-Type': 'application/json' },
293
- body: JSON.stringify({ amount }),
294
- });
295
-
296
- const data = await res.json();
297
- if (!res.ok || !data.success) {
298
- return { success: false, message: data?.message || 'Error updating payment' };
299
- }
300
-
301
- return {
302
- success: true,
303
- message: data.message || 'Payment updated successfully'
304
- };
305
- } catch (e: any) {
306
- console.error('Error updating payment:', e);
307
- return { success: false, message: e.message || 'Error updating payment' };
308
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  };
 
1
+ import {
2
+ Party, MirchiType, Lot, Transaction, PartyType,
3
+ LotStatus, BillType, ApiResponse, PaymentMode
4
+ } from '../types';
5
+
6
+ const API_BASE = '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
+ const grossWeightTotal = parseNumeric(tx.gross_weight_total);
37
+ const netWeightTotal = parseNumeric(tx.net_weight_total);
38
+ const subtotal = parseNumeric(tx.subtotal);
39
+ const totalExpenses = parseNumeric(tx.total_expenses);
40
+ const totalAmount = parseNumeric(tx.total_amount);
41
+ const paidAmount = parseNumeric(tx.paid_amount);
42
+ const balanceAmount = parseNumeric(tx.balance_amount);
43
+
44
+ // Normalize expenses and reconstruct other_expenses if backend didn't send it
45
+ let normalizedExpenses;
46
+ if (tx.expenses) {
47
+ const hasOtherExpensesField = Object.prototype.hasOwnProperty.call(tx.expenses, 'other_expenses');
48
+
49
+ const baseExpenses = {
50
+ cess_percent: parseNumeric(tx.expenses.cess_percent),
51
+ cess_amount: parseNumeric(tx.expenses.cess_amount),
52
+ adat_percent: parseNumeric(tx.expenses.adat_percent),
53
+ adat_amount: parseNumeric(tx.expenses.adat_amount),
54
+ poti_rate: parseNumeric(tx.expenses.poti_rate),
55
+ poti_amount: parseNumeric(tx.expenses.poti_amount),
56
+ hamali_per_poti: parseNumeric(tx.expenses.hamali_per_poti),
57
+ hamali_amount: parseNumeric(tx.expenses.hamali_amount),
58
+ packaging_hamali_per_poti: parseNumeric(tx.expenses.packaging_hamali_per_poti),
59
+ packaging_hamali_amount: parseNumeric(tx.expenses.packaging_hamali_amount),
60
+ gaadi_bharni: parseNumeric(tx.expenses.gaadi_bharni),
61
+ };
62
+
63
+ let otherExpensesValue = hasOtherExpensesField
64
+ ? parseNumeric(tx.expenses.other_expenses)
65
+ : 0;
66
+
67
+ // If backend doesn't provide other_expenses but total_expenses is known,
68
+ // infer other_expenses as the remaining part after subtracting known components.
69
+ if (!hasOtherExpensesField && totalExpenses) {
70
+ const knownComponents =
71
+ baseExpenses.poti_amount +
72
+ baseExpenses.cess_amount +
73
+ baseExpenses.adat_amount +
74
+ baseExpenses.hamali_amount +
75
+ baseExpenses.packaging_hamali_amount +
76
+ baseExpenses.gaadi_bharni;
77
+
78
+ const diff = totalExpenses - knownComponents;
79
+ if (diff > 0) {
80
+ otherExpensesValue = diff;
81
+ }
82
+ }
83
+
84
+ normalizedExpenses = {
85
+ ...baseExpenses,
86
+ other_expenses: otherExpensesValue,
87
+ };
88
+ } else {
89
+ normalizedExpenses = {
90
+ cess_percent: 0,
91
+ cess_amount: 0,
92
+ adat_percent: 0,
93
+ adat_amount: 0,
94
+ poti_rate: 0,
95
+ poti_amount: 0,
96
+ hamali_per_poti: 0,
97
+ hamali_amount: 0,
98
+ packaging_hamali_per_poti: 0,
99
+ packaging_hamali_amount: 0,
100
+ gaadi_bharni: 0,
101
+ other_expenses: 0,
102
+ };
103
+ }
104
+
105
+ return {
106
+ ...tx,
107
+ gross_weight_total: grossWeightTotal,
108
+ net_weight_total: netWeightTotal,
109
+ subtotal,
110
+ total_expenses: totalExpenses,
111
+ total_amount: totalAmount,
112
+ paid_amount: paidAmount,
113
+ balance_amount: balanceAmount,
114
+ items: validItems.map((item: any) => ({
115
+ ...item,
116
+ poti_weights: parsePotiWeights(item.poti_weights),
117
+ gross_weight: parseNumeric(item.gross_weight),
118
+ poti_count: parseNumeric(item.poti_count),
119
+ total_potya: parseNumeric(item.total_potya),
120
+ net_weight: parseNumeric(item.net_weight),
121
+ rate_per_kg: parseNumeric(item.rate_per_kg),
122
+ item_total: parseNumeric(item.item_total),
123
+ })),
124
+ expenses: normalizedExpenses,
125
+ payments: tx.payments || [],
126
+ };
127
+ };
128
+
129
+ // Parties API
130
+ export const getParties = async (): Promise<Party[]> => {
131
+ try {
132
+ const res = await fetch(`${API_BASE}/parties`);
133
+ if (!res.ok) throw new Error('Failed to load parties');
134
+ const data = await res.json();
135
+ return data.map((p: any) => ({
136
+ ...p,
137
+ current_balance: parseNumeric(p.current_balance)
138
+ }));
139
+ } catch (error) {
140
+ console.error('Error fetching parties:', error);
141
+ return [];
142
+ }
143
+ };
144
+
145
+ export const saveParty = async (party: Party): Promise<ApiResponse<Party>> => {
146
+ try {
147
+ const res = await fetch(`${API_BASE}/parties`, {
148
+ method: 'POST',
149
+ headers: { 'Content-Type': 'application/json' },
150
+ body: JSON.stringify(party),
151
+ });
152
+
153
+ const data = await res.json();
154
+ if (!res.ok || !data.success) {
155
+ return { success: false, message: data?.message || 'Error saving party' };
156
+ }
157
+
158
+ return {
159
+ success: true,
160
+ data: {
161
+ ...data.data,
162
+ current_balance: parseNumeric(data.data.current_balance)
163
+ },
164
+ message: data.message || 'Party saved successfully'
165
+ };
166
+ } catch (e: any) {
167
+ return { success: false, message: e.message || 'Database error' };
168
+ }
169
+ };
170
+
171
+ // Mirchi Types API
172
+ export const apiGetMirchiTypes = async (): Promise<MirchiType[]> => {
173
+ try {
174
+ const res = await fetch(`${API_BASE}/mirchi-types`);
175
+ if (!res.ok) throw new Error('Failed to load mirchi types');
176
+ const data = await res.json();
177
+ return data.map((m: any) => ({
178
+ ...m,
179
+ current_rate: parseNumeric(m.current_rate)
180
+ }));
181
+ } catch (error) {
182
+ console.error('Error fetching mirchi types:', error);
183
+ return [];
184
+ }
185
+ };
186
+
187
+ export const apiSaveMirchiType = async (type: MirchiType): Promise<ApiResponse<MirchiType>> => {
188
+ try {
189
+ const res = await fetch(`${API_BASE}/mirchi-types`, {
190
+ method: 'POST',
191
+ headers: { 'Content-Type': 'application/json' },
192
+ body: JSON.stringify(type),
193
+ });
194
+ const data = await res.json();
195
+ if (!res.ok || !data.success) {
196
+ return { success: false, message: data?.message || 'Error saving type' };
197
+ }
198
+ return {
199
+ success: true,
200
+ data: {
201
+ ...data.data,
202
+ current_rate: parseNumeric(data.data.current_rate)
203
+ },
204
+ message: data.message
205
+ };
206
+ } catch (e: any) {
207
+ return { success: false, message: e.message || 'Error saving type' };
208
+ }
209
+ };
210
+
211
+ // Alias for compatibility
212
+ export const getMirchiTypes = apiGetMirchiTypes;
213
+ export const saveMirchiType = apiSaveMirchiType;
214
+
215
+ // Lots API
216
+ export const getActiveLots = async (): Promise<Lot[]> => {
217
+ try {
218
+ const res = await fetch(`${API_BASE}/lots/active`);
219
+ if (!res.ok) throw new Error('Failed to load active lots');
220
+ const data = await res.json();
221
+ return data.map((l: any) => ({
222
+ ...l,
223
+ total_quantity: parseNumeric(l.total_quantity),
224
+ remaining_quantity: parseNumeric(l.remaining_quantity),
225
+ avg_rate: parseNumeric(l.avg_rate)
226
+ }));
227
+ } catch (error) {
228
+ console.error('Error fetching active lots:', error);
229
+ return [];
230
+ }
231
+ };
232
+
233
+ // Check if lot number is unique
234
+ export const checkLotUnique = async (lotNumber: string): Promise<boolean> => {
235
+ try {
236
+ const res = await fetch(`${API_BASE}/lots/check-unique`, {
237
+ method: 'POST',
238
+ headers: { 'Content-Type': 'application/json' },
239
+ body: JSON.stringify({ lot_number: lotNumber }),
240
+ });
241
+ if (!res.ok) throw new Error('Failed to check lot uniqueness');
242
+ const data = await res.json();
243
+ return !data.exists; // Return true if unique (not exists)
244
+ } catch (error) {
245
+ console.error('Error checking lot uniqueness:', error);
246
+ return false;
247
+ }
248
+ };
249
+
250
+ // Get available lots for a specific mirchi type
251
+ export const getAvailableLotsByMirchi = async (mirchiTypeId: string): Promise<Lot[]> => {
252
+ try {
253
+ const res = await fetch(`${API_BASE}/lots/available/${mirchiTypeId}`);
254
+ if (!res.ok) throw new Error('Failed to load available lots');
255
+ const data = await res.json();
256
+ return data.map((l: any) => ({
257
+ ...l,
258
+ total_quantity: parseNumeric(l.total_quantity),
259
+ remaining_quantity: parseNumeric(l.remaining_quantity),
260
+ avg_rate: parseNumeric(l.avg_rate)
261
+ }));
262
+ } catch (error) {
263
+ console.error('Error fetching available lots:', error);
264
+ return [];
265
+ }
266
+ };
267
+
268
+ // Transactions API
269
+ export const getTransactions = async (): Promise<Transaction[]> => {
270
+ try {
271
+ const res = await fetch(`${API_BASE}/transactions`);
272
+ if (!res.ok) throw new Error('Failed to load transactions');
273
+ const data = await res.json();
274
+ return data.map(normalizeTransaction);
275
+ } catch (error) {
276
+ console.error('Error fetching transactions:', error);
277
+ return [];
278
+ }
279
+ };
280
+
281
+ export const generateBillNumber = (type: BillType, isReturn: boolean = false): string => {
282
+ const prefix = type === BillType.JAWAAK ? (isReturn ? 'JAWAAK-RET' : 'JAWAAK') : (isReturn ? 'AWAAK-RET' : 'AWAAK');
283
+ const timestamp = Date.now();
284
+ return `${prefix}-${new Date().getFullYear()}-${String(timestamp).slice(-4)}`;
285
+ };
286
+
287
+ export const saveTransaction = async (transaction: Transaction): Promise<ApiResponse<Transaction>> => {
288
+ try {
289
+ // Validate Payload
290
+ if (!transaction.party_id) return { success: false, message: "Party is required" };
291
+ if (!transaction.items || transaction.items.length === 0) return { success: false, message: "At least one item is required" };
292
+
293
+ console.log('=== SAVING TRANSACTION ===');
294
+ console.log('Transaction ID:', transaction.id);
295
+ console.log('Items count:', transaction.items?.length);
296
+ console.log('Items data:', JSON.stringify(transaction.items, null, 2));
297
+
298
+ const res = await fetch(`${API_BASE}/transactions`, {
299
+ method: 'POST',
300
+ headers: { 'Content-Type': 'application/json' },
301
+ body: JSON.stringify(transaction)
302
+ });
303
+
304
+ if (!res.ok) {
305
+ const error = await res.json();
306
+ console.error('Save transaction error:', error);
307
+ throw new Error(error.message || 'Failed to save transaction');
308
+ }
309
+
310
+ const result = await res.json();
311
+ console.log('=== SAVE RESPONSE ===');
312
+ console.log('Response data:', result.data);
313
+ console.log('Response items count:', result.data?.items?.length);
314
+ console.log('Response items:', JSON.stringify(result.data?.items, null, 2));
315
+
316
+ return {
317
+ success: true,
318
+ data: normalizeTransaction(result.data),
319
+ message: result.message
320
+ };
321
+ } catch (error: any) {
322
+ console.error('Error saving transaction:', error);
323
+ return {
324
+ success: false,
325
+ message: error.message || 'Failed to save transaction'
326
+ };
327
+ }
328
+ };
329
+
330
+ export const updateTransactionPayment = async (transactionId: string, amount: number): Promise<ApiResponse<Transaction>> => {
331
+ try {
332
+ // Validate amount
333
+ if (amount <= 0) return { success: false, message: "Amount must be greater than 0" };
334
+
335
+ const res = await fetch(`${API_BASE}/transactions/${transactionId}/payment`, {
336
+ method: 'PATCH',
337
+ headers: { 'Content-Type': 'application/json' },
338
+ body: JSON.stringify({ amount }),
339
+ });
340
+
341
+ const data = await res.json();
342
+ if (!res.ok || !data.success) {
343
+ return { success: false, message: data?.message || 'Error updating payment' };
344
+ }
345
+
346
+ return {
347
+ success: true,
348
+ message: data.message || 'Payment updated successfully'
349
+ };
350
+ } catch (e: any) {
351
+ console.error('Error updating payment:', e);
352
+ return { success: false, message: e.message || 'Error updating payment' };
353
+ }
354
  };
tsconfig.json CHANGED
@@ -1,29 +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
  }
 
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 CHANGED
@@ -1,122 +1,123 @@
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
  }
 
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
+ other_expenses: number;
83
+ }
84
+
85
+ export interface TransactionItem {
86
+ id: string;
87
+ mirchi_type_id: string;
88
+ mirchi_name?: string;
89
+ quality: string;
90
+ lot_id?: string; // Optional for Jawaak (created auto), Required for Awaak
91
+ poti_weights: number[]; // Array of weights [10, 20, 30]
92
+ gross_weight: number;
93
+ poti_count: number;
94
+ total_potya: number; // Deduction
95
+ net_weight: number;
96
+ rate_per_kg: number;
97
+ item_total: number;
98
+ }
99
+
100
+ export interface Transaction {
101
+ id: string;
102
+ bill_number: string;
103
+ bill_date: string;
104
+ bill_type: BillType;
105
+ is_return: boolean; // True for Purchase Return or Sales Return
106
+ party_id: string;
107
+ party_name?: string;
108
+ items: TransactionItem[];
109
+ expenses: Expenses;
110
+ payments: Payment[];
111
+
112
+ // Totals
113
+ gross_weight_total: number;
114
+ net_weight_total: number;
115
+ subtotal: number;
116
+ total_expenses: number;
117
+ total_amount: number;
118
+ paid_amount: number;
119
+ balance_amount: number;
120
+
121
+ created_at?: string;
122
+ updated_at?: string;
123
  }