Spaces:
Sleeping
Sleeping
Upload 36 files
Browse files- .env.local +1 -1
- .gitattributes +37 -37
- .gitignore +24 -24
- App.tsx +48 -48
- README.md +10 -10
- components/Layout.tsx +122 -122
- components/PrintInvoice.tsx +12 -1
- index.html +94 -94
- index.tsx +14 -14
- metadata.json +4 -4
- package-lock.json +0 -0
- package.json +28 -28
- pages/AwaakBill.tsx +865 -858
- pages/Dashboard.tsx +399 -399
- pages/JawaakBill.tsx +806 -794
- pages/PartyLedger.tsx +650 -650
- pages/Settings.tsx +316 -316
- pages/StockReport.tsx +453 -450
- services/db.ts +353 -308
- tsconfig.json +28 -28
- 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
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 80 |
-
|
| 81 |
-
const [
|
| 82 |
-
const [
|
| 83 |
-
const [
|
| 84 |
-
const [
|
| 85 |
-
const [
|
| 86 |
-
const [
|
| 87 |
-
const [
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
const
|
| 131 |
-
const
|
| 132 |
-
const
|
| 133 |
-
const
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
const
|
| 186 |
-
const
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
handleItemChange(id, '
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
const
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
const
|
| 238 |
-
const
|
| 239 |
-
const
|
| 240 |
-
const
|
| 241 |
-
const
|
| 242 |
-
const
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
let
|
| 247 |
-
let
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
//
|
| 271 |
-
//
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
}
|
| 306 |
-
if (!item.
|
| 307 |
-
alert(`Row ${i + 1}:
|
| 308 |
-
return false;
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
}
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 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 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
<
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
<div
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
</
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
<
|
| 664 |
-
</div>
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
<
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
}
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
/>
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
<
|
| 752 |
-
|
| 753 |
-
<
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
<
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
<
|
| 811 |
-
|
| 812 |
-
<
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
<div
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
className="
|
| 835 |
-
|
| 836 |
-
<
|
| 837 |
-
</
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 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 |
-
|
| 80 |
-
|
| 81 |
-
const [
|
| 82 |
-
const [
|
| 83 |
-
const [
|
| 84 |
-
const [
|
| 85 |
-
const [
|
| 86 |
-
const [
|
| 87 |
-
const [
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
const
|
| 131 |
-
const
|
| 132 |
-
const
|
| 133 |
-
const
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
const
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
const
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
const
|
| 220 |
-
const
|
| 221 |
-
const
|
| 222 |
-
const
|
| 223 |
-
const
|
| 224 |
-
const
|
| 225 |
-
const
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
let
|
| 230 |
-
let
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
//
|
| 254 |
-
//
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
if (
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
...(
|
| 317 |
-
...(
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
//
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
...
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
};
|
| 349 |
-
|
| 350 |
-
const
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
<div className="
|
| 358 |
-
|
| 359 |
-
<
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
/
|
| 364 |
-
<
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
/
|
| 375 |
-
<
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
/
|
| 386 |
-
<
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
/
|
| 397 |
-
<
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
/
|
| 408 |
-
<
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
/
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
{paymentMode === '
|
| 462 |
-
<div className="
|
| 463 |
-
<
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
}
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
}
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
</
|
| 570 |
-
</div>
|
| 571 |
-
</div>
|
| 572 |
-
)}
|
| 573 |
-
{
|
| 574 |
-
<>
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
<
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
<
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
<
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
</
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
<
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
<div>
|
| 701 |
-
<label className="block text-xs font-medium text-gray-500 mb-1">
|
| 702 |
-
<input
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
/>
|
| 711 |
-
</div>
|
| 712 |
-
<div
|
| 713 |
-
<label className="block text-xs font-medium text-gray-500 mb-1">
|
| 714 |
-
<input
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
<
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
<
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
className="
|
| 771 |
-
>
|
| 772 |
-
<
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
{
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 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 |
-
|
| 90 |
-
let
|
| 91 |
-
let
|
| 92 |
-
let
|
| 93 |
-
let
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
<th className="px-6 py-4
|
| 194 |
-
<th className="px-6 py-4
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
<td className="px-6 py-4">
|
| 237 |
-
{row.
|
| 238 |
-
</td>
|
| 239 |
-
<td className="px-6 py-4
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
<span
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
{
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
<span
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
<div
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
<div className="text-
|
| 305 |
-
{row.
|
| 306 |
-
</div>
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
>
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
<
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
<
|
| 319 |
-
|
| 320 |
-
<div className="
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
{
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
<th className="px-6 py-4
|
| 346 |
-
<th className="px-6 py-4
|
| 347 |
-
<th className="px-6 py-4
|
| 348 |
-
<th className="px-6 py-4 text-right">
|
| 349 |
-
<th className="px-6 py-4 text-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
<td className="px-6 py-4 text-
|
| 366 |
-
|
| 367 |
-
</td>
|
| 368 |
-
<td className="px-6 py-4 text-right text-
|
| 369 |
-
{row.
|
| 370 |
-
</td>
|
| 371 |
-
<td className="px-6 py-4 text-right text-
|
| 372 |
-
{row.
|
| 373 |
-
</td>
|
| 374 |
-
<td className="px-6 py-4 text-right text-
|
| 375 |
-
{row.
|
| 376 |
-
</td>
|
| 377 |
-
<td className="px-6 py-4 text-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
<div
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
</div>
|
| 411 |
-
<div className="
|
| 412 |
-
{row.
|
| 413 |
-
</div>
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
<
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
</div>
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
<
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
</div>
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 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 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
return
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
return
|
| 230 |
-
}
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
};
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
const res = await fetch(`${API_BASE}/
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 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 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
| 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 |
}
|