Antaram commited on
Commit
6234767
·
verified ·
1 Parent(s): 0d459ce

Upload 49 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+ build/
7
+
8
+ # Environment files (will be set in HF Spaces)
9
+ .env.local
10
+ .env.development
11
+
12
+ # Git
13
+ .git/
14
+ .gitignore
15
+
16
+ # Logs
17
+ *.log
18
+ npm-debug.log*
19
+
20
+ # IDE
21
+ .vscode/
22
+ .idea/
23
+
24
+ # Test files
25
+ *.test.ts
26
+ *.test.tsx
27
+ __tests__/
28
+
29
+ # Documentation
30
+ README.md
31
+ docs/
.env ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Environment Variables
2
+
3
+ # Backend API URL
4
+ VITE_API_URL=https://antaram-pattanshettybackend.hf.space/api
.env.local ADDED
@@ -0,0 +1 @@
 
 
1
+ GEMINI_API_KEY=PLACEHOLDER_API_KEY
.firebaserc ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "projects": {
3
+ "default": "pattanshetti-03-12"
4
+ }
5
+ }
.gitattributes CHANGED
@@ -1,35 +1,39 @@
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
 
 
 
 
 
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
38
+ dist/icon-192.png filter=lfs diff=lfs merge=lfs -text
39
+ dist/icon-512.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
App.tsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Analysis from './pages/Analysis';
11
+ import PWAInstallPrompt from './components/PWAInstallPrompt';
12
+ import { PWAProvider } from './context/PWAContext';
13
+
14
+ const App = () => {
15
+ // Register Service Worker for PWA
16
+ useEffect(() => {
17
+ if ('serviceWorker' in navigator) {
18
+ window.addEventListener('load', () => {
19
+ navigator.serviceWorker.register('/service-worker.js')
20
+ .then(registration => {
21
+ console.log('✅ PWA Service Worker registered:', registration);
22
+ })
23
+ .catch(error => {
24
+ console.log('❌ SW registration failed:', error);
25
+ });
26
+ });
27
+ }
28
+ }, []);
29
+
30
+ return (
31
+ <PWAProvider>
32
+ <HashRouter>
33
+ <Layout>
34
+ <Routes>
35
+ <Route path="/" element={<Dashboard />} />
36
+ <Route path="/jawaak" element={<JawaakBill />} />
37
+ <Route path="/awaak" element={<AwaakBill />} />
38
+ <Route path="/stock" element={<StockReport />} />
39
+ <Route path="/ledger" element={<PartyLedger />} />
40
+ <Route path="/analysis" element={<Analysis />} />
41
+ <Route path="/settings" element={<Settings />} />
42
+ <Route path="*" element={<Navigate to="/" replace />} />
43
+ </Routes>
44
+ </Layout>
45
+ <PWAInstallPrompt />
46
+ </HashRouter>
47
+ </PWAProvider>
48
+ );
49
+ };
50
+
51
+ export default App;
DEPLOYMENT.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Spaces Deployment
2
+
3
+ ## Frontend Dockerfile
4
+
5
+ Simple Dockerfile for deploying the React frontend to Hugging Face Spaces.
6
+
7
+ ### Build & Run Locally
8
+
9
+ ```bash
10
+ # Build the Docker image
11
+ docker build -t pattanshetty-frontend .
12
+
13
+ # Run the container
14
+ docker run -p 7860:7860 pattanshetty-frontend
15
+ ```
16
+
17
+ ### Deploy to Hugging Face Spaces
18
+
19
+ 1. Create a new Space on Hugging Face
20
+ 2. Choose "Docker" as the SDK
21
+ 3. Upload the entire `frontend` folder
22
+ 4. The Dockerfile will automatically build and deploy
23
+
24
+ ### Port Configuration
25
+
26
+ - **Port 7860** - Hugging Face Spaces default port
27
+ - The app will be accessible at your Space URL
28
+
29
+ ### Environment Variables
30
+
31
+ If you need to set environment variables (like API URL), create a `.env` file or set them in Hugging Face Spaces settings.
32
+
33
+ ### Notes
34
+
35
+ - Uses `npm run preview` which serves the production build
36
+ - No nginx needed - Vite's preview server handles everything
37
+ - Lightweight Alpine Linux base image
38
+ - Production-ready build
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy package files
6
+ COPY package*.json ./
7
+
8
+ # Install dependencies
9
+ RUN npm install
10
+
11
+ # Copy all files
12
+ COPY . .
13
+
14
+ # Build the app
15
+ RUN npm run build
16
+
17
+ # Expose port 7860 (Hugging Face Spaces default)
18
+ EXPOSE 7860
19
+
20
+ # Start the preview server
21
+ CMD ["npm", "run", "preview", "--", "--port", "7860", "--host", "0.0.0.0"]
components/Layout.tsx ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: '/analysis', label: 'अ‍ॅनालिसीस (Analysis)', icon: TrendingUp },
30
+ { path: '/settings', label: 'सेटिंग्स (Settings)', icon: Settings },
31
+ ];
32
+
33
+ const isActive = (path: string) => location.pathname === path;
34
+
35
+ return (
36
+ <div className="flex h-screen bg-gray-50 overflow-hidden">
37
+ {/* Mobile Sidebar Overlay */}
38
+ {isSidebarOpen && (
39
+ <div
40
+ className="fixed inset-0 bg-black bg-opacity-50 z-20 lg:hidden"
41
+ onClick={() => setSidebarOpen(false)}
42
+ />
43
+ )}
44
+
45
+ {/* Sidebar */}
46
+ <aside
47
+ 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'
48
+ }`}
49
+ >
50
+ <div className="flex items-center justify-between p-4 border-b border-teal-700 h-16">
51
+ <div className="flex items-center gap-2">
52
+ <TrendingUp className="w-6 h-6 text-teal-300" />
53
+ <span className="font-bold text-xl">Mirchi Vyapar</span>
54
+ </div>
55
+ <button
56
+ className="lg:hidden p-1 hover:bg-teal-700 rounded"
57
+ onClick={() => setSidebarOpen(false)}
58
+ >
59
+ <X size={24} />
60
+ </button>
61
+ </div>
62
+
63
+ <nav className="p-4 space-y-1">
64
+ {navItems.map((item) => (
65
+ <Link
66
+ key={item.path}
67
+ to={item.path}
68
+ onClick={() => setSidebarOpen(false)}
69
+ className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(item.path)
70
+ ? 'bg-teal-900 text-teal-100 shadow-sm'
71
+ : 'text-teal-100 hover:bg-teal-700'
72
+ }`}
73
+ >
74
+ <item.icon size={20} />
75
+ <span className="font-medium">{item.label}</span>
76
+ </Link>
77
+ ))}
78
+ </nav>
79
+
80
+ <div className="absolute bottom-0 w-full p-4 border-t border-teal-700 bg-teal-800">
81
+ <div className="flex items-center gap-3">
82
+ <div className="w-8 h-8 rounded-full bg-teal-600 flex items-center justify-center font-bold">
83
+ A
84
+ </div>
85
+ <div>
86
+ <p className="text-sm font-medium">Admin User</p>
87
+ <p className="text-xs text-teal-300">Mirchi Mandi</p>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </aside>
92
+
93
+ {/* Main Content */}
94
+ <main className="flex-1 flex flex-col overflow-hidden">
95
+ {/* Top Header */}
96
+ <header className="h-16 bg-white border-b flex items-center justify-between px-4 lg:px-8">
97
+ <button
98
+ className="lg:hidden p-2 -ml-2 text-gray-600 hover:bg-gray-100 rounded-lg"
99
+ onClick={() => setSidebarOpen(true)}
100
+ >
101
+ <Menu size={24} />
102
+ </button>
103
+
104
+ <h1 className="text-xl font-semibold text-gray-800 ml-2 lg:ml-0">
105
+ {navItems.find(i => isActive(i.path))?.label.split(' (')[0] || 'Mirchi Vyapar'}
106
+ </h1>
107
+
108
+ <div className="flex items-center gap-4">
109
+ <span className="text-sm text-gray-500 hidden md:block">
110
+ {new Date().toLocaleDateString('mr-IN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
111
+ </span>
112
+ </div>
113
+ </header>
114
+
115
+ {/* Page Content */}
116
+ <div className="flex-1 overflow-auto p-4 lg:p-6 pb-20 lg:pb-6">
117
+ {children}
118
+ </div>
119
+ </main>
120
+ </div>
121
+ );
122
+ };
123
+
124
+ export default Layout;
components/PWAInstallPrompt.tsx ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Download, X } from 'lucide-react';
3
+ import { usePWA } from '../context/PWAContext';
4
+
5
+ const PWAInstallPrompt: React.FC = () => {
6
+ const { deferredPrompt, isInstallable, isIOS, isStandalone, installApp } = usePWA();
7
+ const [showInstallPrompt, setShowInstallPrompt] = useState(false);
8
+
9
+ useEffect(() => {
10
+ // Show install prompt if installable and not already installed
11
+ if (isInstallable && !isStandalone) {
12
+ setShowInstallPrompt(true);
13
+ }
14
+
15
+ // For iOS, show prompt if not installed
16
+ if (isIOS && !isStandalone) {
17
+ setShowInstallPrompt(true);
18
+ }
19
+ }, [isInstallable, isStandalone, isIOS]);
20
+
21
+ const handleInstallClick = async () => {
22
+ if (isIOS) {
23
+ // iOS doesn't support programmatic install, show instructions
24
+ return;
25
+ }
26
+
27
+ await installApp();
28
+ setShowInstallPrompt(false);
29
+ };
30
+
31
+ const handleDismiss = () => {
32
+ setShowInstallPrompt(false);
33
+ // Remember dismissal for 7 days
34
+ localStorage.setItem('pwa-install-dismissed', Date.now().toString());
35
+ };
36
+
37
+ // Don't show if already installed
38
+ if (isStandalone) {
39
+ return null;
40
+ }
41
+
42
+ // Check if dismissed recently (within 7 days)
43
+ const dismissedTime = localStorage.getItem('pwa-install-dismissed');
44
+ if (dismissedTime) {
45
+ const daysSinceDismissed = (Date.now() - parseInt(dismissedTime)) / (1000 * 60 * 60 * 24);
46
+ if (daysSinceDismissed < 7) {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ if (!showInstallPrompt) {
52
+ return null;
53
+ }
54
+
55
+ return (
56
+ <div className="fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-96 bg-white rounded-lg shadow-2xl border-2 border-teal-600 p-4 z-50 animate-slide-up">
57
+ <button
58
+ onClick={handleDismiss}
59
+ className="absolute top-2 right-2 text-gray-400 hover:text-gray-600"
60
+ >
61
+ <X size={20} />
62
+ </button>
63
+
64
+ <div className="flex items-start gap-3">
65
+ <div className="p-2 bg-teal-100 rounded-lg">
66
+ <Download className="text-teal-600" size={24} />
67
+ </div>
68
+ <div className="flex-1">
69
+ <h3 className="font-bold text-gray-900 mb-1">Install App</h3>
70
+ {isIOS ? (
71
+ <div className="text-sm text-gray-600">
72
+ <p className="mb-2">Install this app on your iPhone:</p>
73
+ <ol className="list-decimal list-inside space-y-1 text-xs">
74
+ <li>Tap the Share button <span className="inline-block">⎋</span></li>
75
+ <li>Scroll and tap "Add to Home Screen"</li>
76
+ <li>Tap "Add" in the top right</li>
77
+ </ol>
78
+ </div>
79
+ ) : (
80
+ <>
81
+ <p className="text-sm text-gray-600 mb-3">
82
+ Install our app for a better experience and offline access!
83
+ </p>
84
+ <button
85
+ onClick={handleInstallClick}
86
+ className="w-full bg-teal-600 text-white py-2 px-4 rounded-lg font-medium hover:bg-teal-700 transition-colors"
87
+ >
88
+ Install Now
89
+ </button>
90
+ </>
91
+ )}
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ };
97
+
98
+ export default PWAInstallPrompt;
components/PdfInvoice.tsx ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Transaction } from '../types';
3
+ import { Download, X, Eye } from 'lucide-react';
4
+ import jsPDF from 'jspdf';
5
+ import 'jspdf-autotable';
6
+
7
+ interface PdfInvoiceProps {
8
+ transaction: Transaction;
9
+ className?: string;
10
+ children?: React.ReactNode;
11
+ }
12
+
13
+ const PdfInvoice: React.FC<PdfInvoiceProps> = ({ transaction, className, children }) => {
14
+ const [showPreview, setShowPreview] = useState(false);
15
+ const [pdfUrl, setPdfUrl] = useState<string>('');
16
+
17
+ const formatINR = (v: number) => `₹${v.toFixed(2)}`;
18
+ const formatDate = (dateStr: string) => {
19
+ const date = new Date(dateStr);
20
+ return date.toLocaleDateString('en-IN', { day: '2-digit', month: '2-digit', year: 'numeric' });
21
+ };
22
+
23
+ const generatePDF = (download: boolean = false) => {
24
+ const doc = new jsPDF();
25
+
26
+ const subtotal = transaction.subtotal || 0;
27
+ const packing = transaction.expenses?.poti_amount || 0;
28
+ const cess = transaction.expenses?.cess_amount || 0;
29
+ const adat = transaction.expenses?.adat_amount || 0;
30
+ const hamali = (transaction.expenses?.hamali_amount || 0) + (transaction.expenses?.packaging_hamali_amount || 0);
31
+ const grandTotal = transaction.total_amount || 0;
32
+ const paidAmount = transaction.paid_amount || 0;
33
+ const balanceAmount = transaction.balance_amount || 0;
34
+
35
+ // Header - Invoice Date on left, Bill No on right
36
+ doc.setFontSize(13);
37
+ doc.setFont('helvetica', 'bold');
38
+ doc.text(`Invoice Date: ${formatDate(transaction.bill_date)}`, 14, 15);
39
+ doc.text(`Bill No: ${transaction.bill_number}`, 196, 15, { align: 'right' });
40
+
41
+ // Party Details Section - LEFT ALIGNED
42
+ doc.setFontSize(11);
43
+ doc.setFont('helvetica', 'bold');
44
+ doc.text('Party Details', 14, 25);
45
+ doc.setLineWidth(0.5);
46
+ doc.line(14, 26, 55, 26);
47
+
48
+ // Party info - explicitly left aligned at x=14
49
+ doc.setFont('helvetica', 'normal');
50
+ doc.setFontSize(9);
51
+
52
+ const partyName = transaction.party_name || '-';
53
+ const partyCity = transaction.party_city || '-';
54
+ const partyPhone = transaction.party_phone || '-';
55
+
56
+ doc.text(`Name: ${partyName}`, 14, 31, { align: 'left' });
57
+ doc.text(`Place: ${partyCity}`, 14, 36, { align: 'left' });
58
+ doc.text(`Mobile: ${partyPhone}`, 14, 41, { align: 'left' });
59
+
60
+ // Items Table
61
+ const tableData = transaction.items.map((item, idx) => {
62
+ const potiWeights = Array.isArray(item.poti_weights) ? item.poti_weights.join(', ') : (typeof item.poti_weights === 'string' ? item.poti_weights : '-');
63
+ const amount = item.net_weight * item.rate_per_kg;
64
+ return [idx + 1, `${item.mirchi_name}\n${potiWeights}`, item.poti_count, item.net_weight, item.rate_per_kg, formatINR(amount)];
65
+ });
66
+
67
+ (doc as any).autoTable({
68
+ startY: 47,
69
+ head: [['#', 'Product Type', 'Bags', 'Net (kg)', 'Rate (₹)', 'Amount (₹)']],
70
+ body: tableData,
71
+ theme: 'grid',
72
+ styles: { fontSize: 8, cellPadding: 2, lineWidth: 0.5, lineColor: [0, 0, 0] },
73
+ headStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontStyle: 'bold' },
74
+ bodyStyles: { textColor: [0, 0, 0] },
75
+ columnStyles: {
76
+ 0: { halign: 'center', cellWidth: 10 },
77
+ 1: { halign: 'left', cellWidth: 60 },
78
+ 2: { halign: 'center', cellWidth: 20 },
79
+ 3: { halign: 'center', cellWidth: 25 },
80
+ 4: { halign: 'center', cellWidth: 25 },
81
+ 5: { halign: 'right', cellWidth: 35 }
82
+ }
83
+ });
84
+
85
+ // Summary Table - Right side
86
+ const finalY = (doc as any).lastAutoTable.finalY + 5;
87
+ const summaryData = [
88
+ ['Sub Total', formatINR(subtotal)],
89
+ ['Packing', formatINR(packing)],
90
+ ['CESS', formatINR(cess)],
91
+ ['Adat', formatINR(adat)],
92
+ ['Hamali', formatINR(hamali)],
93
+ ['Total Amount', formatINR(grandTotal)]
94
+ ];
95
+
96
+ (doc as any).autoTable({
97
+ startY: finalY,
98
+ body: summaryData,
99
+ theme: 'grid',
100
+ styles: { fontSize: 8, cellPadding: 2, lineWidth: 0.5, lineColor: [0, 0, 0] },
101
+ bodyStyles: { textColor: [0, 0, 0] },
102
+ columnStyles: {
103
+ 0: { halign: 'left', cellWidth: 50 },
104
+ 1: { halign: 'right', cellWidth: 35 }
105
+ },
106
+ margin: { left: 111 }
107
+ });
108
+
109
+ // Payment Status Section - LEFT ALIGNED
110
+ const paymentY = (doc as any).lastAutoTable.finalY + 10;
111
+ doc.setFontSize(11);
112
+ doc.setFont('helvetica', 'bold');
113
+ doc.text('Payment Status', 14, paymentY, { align: 'left' });
114
+ doc.setLineWidth(0.5);
115
+ doc.line(14, paymentY + 1, 60, paymentY + 1);
116
+
117
+ doc.setFont('helvetica', 'normal');
118
+ doc.setFontSize(9);
119
+
120
+ let currentY = paymentY + 6;
121
+
122
+ if (transaction.payments && transaction.payments.length > 0) {
123
+ transaction.payments.forEach((payment) => {
124
+ const mode = payment.mode === 'cash' ? 'Cash' : payment.mode === 'online' ? 'Online' : payment.mode;
125
+ doc.text(`${mode}: ${formatINR(payment.amount)}`, 14, currentY, { align: 'left' });
126
+ currentY += 5;
127
+ });
128
+ doc.setFont('helvetica', 'bold');
129
+ doc.text(`Total Paid: ${formatINR(paidAmount)}`, 14, currentY, { align: 'left' });
130
+ currentY += 5;
131
+ } else {
132
+ doc.text(`Paid Amount: ${formatINR(paidAmount)}`, 14, currentY, { align: 'left' });
133
+ currentY += 5;
134
+ }
135
+
136
+ if (balanceAmount > 0) {
137
+ doc.setFont('helvetica', 'bold');
138
+ doc.setTextColor(211, 47, 47);
139
+ doc.text(`Due Amount: ${formatINR(balanceAmount)}`, 14, currentY, { align: 'left' });
140
+ doc.setTextColor(0, 0, 0);
141
+ }
142
+
143
+ // Signature - Right side
144
+ doc.setFont('helvetica', 'normal');
145
+ doc.setFontSize(9);
146
+ doc.text('(Authorised Signatory)', 196, paymentY + 20, { align: 'right' });
147
+
148
+ if (download) {
149
+ doc.save(`Invoice_${transaction.bill_number}.pdf`);
150
+ } else {
151
+ const pdfBlob = doc.output('blob');
152
+ const url = URL.createObjectURL(pdfBlob);
153
+ setPdfUrl(url);
154
+ setShowPreview(true);
155
+ }
156
+ };
157
+
158
+ const handleDownload = () => {
159
+ generatePDF(true);
160
+ setShowPreview(false);
161
+ if (pdfUrl) {
162
+ URL.revokeObjectURL(pdfUrl);
163
+ }
164
+ };
165
+
166
+ const handleClose = () => {
167
+ setShowPreview(false);
168
+ if (pdfUrl) {
169
+ URL.revokeObjectURL(pdfUrl);
170
+ }
171
+ };
172
+
173
+ return (
174
+ <>
175
+ <button
176
+ onClick={() => generatePDF(false)}
177
+ className={className || "p-2 bg-white text-red-600 border border-red-600 rounded-md hover:bg-red-50 transition-colors flex items-center justify-center shadow-sm"}
178
+ title="Preview & Download PDF"
179
+ >
180
+ {children || <Eye size={16} />}
181
+ </button>
182
+
183
+ {showPreview && (
184
+ <div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
185
+ <div className="bg-white rounded-lg shadow-2xl max-w-6xl w-full max-h-[90vh] flex flex-col">
186
+ <div className="border-b border-gray-200 px-6 py-4 flex justify-between items-center">
187
+ <h2 className="text-xl font-bold text-gray-800">PDF Preview</h2>
188
+ <div className="flex gap-2">
189
+ <button
190
+ onClick={handleDownload}
191
+ className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center gap-2"
192
+ >
193
+ <Download size={18} />
194
+ Download PDF
195
+ </button>
196
+ <button
197
+ onClick={handleClose}
198
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
199
+ >
200
+ <X size={24} className="text-gray-600" />
201
+ </button>
202
+ </div>
203
+ </div>
204
+
205
+ <div className="flex-1 overflow-auto p-4 bg-gray-100">
206
+ <iframe
207
+ src={pdfUrl}
208
+ className="w-full h-full min-h-[600px] bg-white rounded shadow-lg"
209
+ title="PDF Preview"
210
+ />
211
+ </div>
212
+ </div>
213
+ </div>
214
+ )}
215
+ </>
216
+ );
217
+ };
218
+
219
+ export default PdfInvoice;
components/PrintInvoice.tsx ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Transaction } from '../types';
3
+ import { Printer, X } from 'lucide-react';
4
+
5
+ interface PrintInvoiceProps {
6
+ transaction: Transaction;
7
+ className?: string;
8
+ }
9
+
10
+ const PrintInvoice: React.FC<PrintInvoiceProps> = ({ transaction, className }) => {
11
+ const [showPreview, setShowPreview] = useState(false);
12
+
13
+ const formatINR = (v: number) => `₹${v.toFixed(2)}`;
14
+ const formatDate = (dateStr: string) => {
15
+ const date = new Date(dateStr);
16
+ return date.toLocaleDateString('en-IN', { day: '2-digit', month: '2-digit', year: 'numeric' });
17
+ };
18
+
19
+ // Calculate totals
20
+ const subtotal = transaction.subtotal || 0;
21
+ const packing = transaction.expenses?.poti_amount || 0;
22
+ const cess = transaction.expenses?.cess_amount || 0;
23
+ const adat = transaction.expenses?.adat_amount || 0;
24
+ const hamali = (transaction.expenses?.hamali_amount || 0) + (transaction.expenses?.packaging_hamali_amount || 0);
25
+ const 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
+
32
+ const handlePrint = () => {
33
+ const printContent = document.getElementById('invoice-content');
34
+ if (!printContent) return;
35
+
36
+ const iframe = document.createElement('iframe');
37
+ iframe.style.position = 'absolute';
38
+ iframe.style.width = '0';
39
+ iframe.style.height = '0';
40
+ iframe.style.border = 'none';
41
+ document.body.appendChild(iframe);
42
+
43
+ const doc = iframe.contentWindow?.document;
44
+ if (doc) {
45
+ doc.open();
46
+ doc.write(`
47
+ <html>
48
+ <head>
49
+ <title>Invoice-${transaction.bill_number}</title>
50
+ <style>
51
+ /* Reset margins to absolute zero */
52
+ @page { size: A4; margin: 0; }
53
+ body { margin: 0; padding: 0; background-color: white; font-family: Helvetica, Arial, sans-serif; }
54
+
55
+ /* Overwrite the container style for printing */
56
+ #invoice-container-inner {
57
+ width: 210mm !important;
58
+ /* Reducing slightly from 297mm to 295mm prevents the 'spillover' blank page */
59
+ min-height: 295mm !important;
60
+ padding: 15mm !important;
61
+ margin: 0 auto !important;
62
+ box-sizing: border-box !important;
63
+ overflow: hidden !important; /* Cut off any rogue pixels */
64
+ }
65
+
66
+ /* Helper to hide non-print elements if any sneak in */
67
+ .print-hidden { display: none !important; }
68
+ </style>
69
+ </head>
70
+ <body>
71
+ ${printContent.innerHTML}
72
+ </body>
73
+ </html>
74
+ `);
75
+ doc.close();
76
+
77
+ iframe.contentWindow?.focus();
78
+ setTimeout(() => {
79
+ iframe.contentWindow?.print();
80
+ setTimeout(() => {
81
+ document.body.removeChild(iframe);
82
+ }, 1000);
83
+ }, 500);
84
+ }
85
+ };
86
+
87
+ return (
88
+ <>
89
+ <button
90
+ onClick={() => setShowPreview(true)}
91
+ className={className || "p-2 bg-white text-teal-600 border border-teal-600 rounded-md hover:bg-teal-50 transition-colors flex items-center justify-center shadow-sm"}
92
+ title="Print Invoice"
93
+ >
94
+ <Printer size={16} />
95
+ </button>
96
+
97
+ {showPreview && (
98
+ <div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-2 sm:p-4">
99
+ <div className="bg-white rounded-lg shadow-2xl max-w-4xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden flex flex-col">
100
+
101
+ {/* Header */}
102
+ <div className="sticky top-0 bg-white border-b border-gray-200 px-3 py-3 sm:px-6 sm:py-4 flex justify-between items-center z-10 shrink-0">
103
+ <h2 className="text-lg sm:text-xl font-bold text-gray-800 truncate mr-2">Invoice Preview</h2>
104
+ <div className="flex gap-2 items-center">
105
+ <button
106
+ onClick={handlePrint}
107
+ className="px-3 py-1.5 sm:px-4 sm:py-2 text-sm sm:text-base bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-1 sm:gap-2"
108
+ >
109
+ <Printer size={16} className="sm:w-[18px] sm:h-[18px]" />
110
+ <span>Print</span>
111
+ </button>
112
+ <button
113
+ onClick={() => setShowPreview(false)}
114
+ className="p-1.5 sm:p-2 hover:bg-gray-100 rounded-lg transition-colors"
115
+ >
116
+ <X size={20} className="text-gray-600 sm:w-6 sm:h-6" />
117
+ </button>
118
+ </div>
119
+ </div>
120
+
121
+ {/* Scrollable Area */}
122
+ <div className="overflow-auto flex-1 p-2 sm:p-8 bg-gray-100">
123
+ <div className="mb-4 text-xs text-gray-500 text-center sm:hidden">
124
+ Scroll horizontally to view full invoice
125
+ </div>
126
+
127
+ {/* Wrapper for ID targeting */}
128
+ <div id="invoice-content">
129
+ <div
130
+ id="invoice-container-inner"
131
+ className="bg-white mx-auto shadow-sm"
132
+ style={{
133
+ width: '210mm',
134
+ minHeight: '297mm', /* This stays 297mm for screen preview, but Print overrides it to 295mm */
135
+ padding: '15mm',
136
+ margin: 'auto',
137
+ fontFamily: 'Helvetica, Arial, sans-serif',
138
+ fontSize: '12px',
139
+ color: '#000',
140
+ backgroundColor: '#fff',
141
+ boxSizing: 'border-box',
142
+ position: 'relative'
143
+ }}
144
+ >
145
+
146
+ {/* Invoice Header */}
147
+ <div style={{ borderBottom: '2px solid #333', paddingBottom: '10px', marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
148
+ <div>
149
+ <h1 style={{ margin: 0, fontSize: '24px', fontWeight: 'bold', letterSpacing: '1px' }}>INVOICE</h1>
150
+ </div>
151
+ <div style={{ textAlign: 'right' }}>
152
+ {/* <div style={{ fontSize: '14px', fontWeight: 'bold' }}>Bill No: {transaction.bill_number}</div> */}
153
+ <div style={{ fontSize: '12px', marginTop: '4px' }}>Date: {formatDate(transaction.bill_date)}</div>
154
+ </div>
155
+ </div>
156
+
157
+ {/* Info Section */}
158
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
159
+ <div style={{ width: '55%', textAlign: 'left' }}>
160
+ <div style={{
161
+ textTransform: 'uppercase',
162
+ fontSize: '10px',
163
+ color: '#555',
164
+ fontWeight: 'bold',
165
+ marginBottom: '4px',
166
+ borderBottom: '1px solid #eee',
167
+ paddingBottom: '2px',
168
+ width: '100%'
169
+ }}>Billed To</div>
170
+
171
+ <div style={{ fontSize: '14px', fontWeight: 'bold', marginTop: '5px' }}>{transaction.party_name}</div>
172
+ <div style={{ marginTop: '2px' }}>Phone: {transaction.party_phone || '-'}</div>
173
+ </div>
174
+ </div>
175
+
176
+ {/* Table */}
177
+ <table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: '20px' }}>
178
+ <thead>
179
+ <tr style={{ backgroundColor: '#f3f4f6' }}>
180
+ <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'center', width: '5%' }}>#</th>
181
+ <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'left', width: '40%' }}>Item Description</th>
182
+ <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'center', width: '10%' }}>Bags</th>
183
+ <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'center', width: '15%' }}>Net Weight</th>
184
+ <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'right', width: '15%' }}>Rate</th>
185
+ <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'right', width: '15%' }}>Amount</th>
186
+ </tr>
187
+ </thead>
188
+ <tbody>
189
+ {transaction.items.map((item, idx) => {
190
+ const potiWeights = Array.isArray(item.poti_weights) ? item.poti_weights.join(', ') : (typeof item.poti_weights === 'string' ? item.poti_weights : '-');
191
+ const amount = item.net_weight * item.rate_per_kg;
192
+ return (
193
+ <tr key={idx}>
194
+ <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'center' }}>{idx + 1}</td>
195
+ <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'left' }}>
196
+ <div style={{ fontWeight: 'bold' }}>{item.mirchi_name}</div>
197
+ <div style={{ fontSize: '9px', color: '#666', marginTop: '2px', lineHeight: '1.2' }}>
198
+ Weights: {potiWeights}
199
+ </div>
200
+ </td>
201
+ <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'center' }}>{item.poti_count}</td>
202
+ <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'center' }}>{item.net_weight} kg</td>
203
+ <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'right' }}>{formatINR(item.rate_per_kg)}</td>
204
+ <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'right' }}>{formatINR(amount)}</td>
205
+ </tr>
206
+ );
207
+ })}
208
+ </tbody>
209
+ </table>
210
+
211
+ {/* Totals Section */}
212
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
213
+ <div style={{ width: '50%', paddingRight: '20px', textAlign: 'left' }}>
214
+ <div style={{ border: '1px solid #ccc', padding: '10px', borderRadius: '4px' }}>
215
+ <div style={{ fontWeight: 'bold', borderBottom: '1px solid #eee', marginBottom: '8px', paddingBottom: '4px' }}>Payment Status</div>
216
+ {transaction.payments && transaction.payments.length > 0 ? (
217
+ <>
218
+ {transaction.payments.map((payment, idx) => (
219
+ <div key={idx} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '11px', marginBottom: '2px' }}>
220
+ <span>{payment.mode === 'cash' ? 'Cash' : payment.mode === 'online' ? 'Online' : payment.mode}:</span>
221
+ <span>{formatINR(payment.amount)}</span>
222
+ </div>
223
+ ))}
224
+ <div style={{ borderTop: '1px dashed #ccc', marginTop: '6px', paddingTop: '6px', display: 'flex', justifyContent: 'space-between', fontWeight: 'bold' }}>
225
+ <span>Total Paid:</span>
226
+ <span>{formatINR(paidAmount)}</span>
227
+ </div>
228
+ </>
229
+ ) : (
230
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
231
+ <span>Total Paid:</span>
232
+ <span>{formatINR(paidAmount)}</span>
233
+ </div>
234
+ )}
235
+
236
+ {balanceAmount > 0 && (
237
+ <div style={{ marginTop: '8px', color: '#c62828', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', fontSize: '13px' }}>
238
+ <span>Due Amount:</span>
239
+ <span>{formatINR(balanceAmount)}</span>
240
+ </div>
241
+ )}
242
+ </div>
243
+ </div>
244
+
245
+ <div style={{ width: '45%' }}>
246
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
247
+ <tbody>
248
+ {/* Renamed Sub Total to Total */}
249
+ {/* <tr>
250
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee' }}>Total</td>
251
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee', fontWeight: 'bold' }}>{formatINR(subtotal)}</td>
252
+ </tr> */}
253
+ <tr>
254
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Packing Expenses</td>
255
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(packing)}</td>
256
+ </tr>
257
+
258
+ {/* New Row: Total + Packing Calculation */}
259
+ <tr>
260
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', fontWeight: 'bold' }}>Sub Total</td>
261
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee', fontWeight: 'bold' }}>{formatINR(subtotal + packing)}</td>
262
+ </tr>
263
+
264
+ <tr>
265
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>CESS</td>
266
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(cess)}</td>
267
+ </tr>
268
+ <tr>
269
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Adat</td>
270
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(adat)}</td>
271
+ </tr>
272
+ <tr>
273
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Hamali</td>
274
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(hamali)}</td>
275
+ </tr>
276
+ <tr>
277
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Gaadi Bharni</td>
278
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(gaadiBharni)}</td>
279
+ </tr>
280
+ <tr>
281
+ <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Other Expenses</td>
282
+ <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(otherExpenses)}</td>
283
+ </tr>
284
+ <tr style={{ backgroundColor: '#f3f4f6' }}>
285
+ <th style={{ padding: '8px 4px', textAlign: 'left', borderTop: '2px solid #000', fontSize: '14px' }}>Grand Total</th>
286
+ <th style={{ padding: '8px 4px', textAlign: 'right', borderTop: '2px solid #000', fontSize: '14px' }}>{formatINR(grandTotal)}</th>
287
+ </tr>
288
+ </tbody>
289
+ </table>
290
+ </div>
291
+ </div>
292
+
293
+ {/* Signatures */}
294
+ <div style={{ marginTop: '50px', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
295
+ <div style={{ textAlign: 'left', fontSize: '10px', color: '#666' }}>
296
+ <p>Thank you for your business.</p>
297
+ </div>
298
+ <div style={{ textAlign: 'center' }}>
299
+ <div style={{ marginBottom: '40px', fontSize: '10px' }}>For Authorised Signatory</div>
300
+ <div style={{ borderTop: '1px solid #000', width: '150px', margin: 'auto' }}></div>
301
+ <div style={{ fontSize: '10px', marginTop: '4px' }}>(Signature)</div>
302
+ </div>
303
+ </div>
304
+
305
+ </div>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ )}
311
+ </>
312
+ );
313
+ };
314
+
315
+ export default PrintInvoice;
context/PWAContext.tsx ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { createContext, useContext, useEffect, useState } from 'react';
2
+
3
+ interface PWAContextType {
4
+ deferredPrompt: any;
5
+ isInstallable: boolean;
6
+ isIOS: boolean;
7
+ isStandalone: boolean;
8
+ installApp: () => Promise<void>;
9
+ }
10
+
11
+ const PWAContext = createContext<PWAContextType | undefined>(undefined);
12
+
13
+ export const PWAProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
14
+ const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
15
+ const [isInstallable, setIsInstallable] = useState(false);
16
+ const [isIOS, setIsIOS] = useState(false);
17
+ const [isStandalone, setIsStandalone] = useState(false);
18
+
19
+ useEffect(() => {
20
+ // Check if already installed
21
+ const standalone = window.matchMedia('(display-mode: standalone)').matches ||
22
+ (window.navigator as any).standalone ||
23
+ document.referrer.includes('android-app://');
24
+
25
+ setIsStandalone(standalone);
26
+
27
+ // Check if iOS
28
+ const ios = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
29
+ setIsIOS(ios);
30
+
31
+ // Listen for beforeinstallprompt event
32
+ const handleBeforeInstallPrompt = (e: Event) => {
33
+ // Prevent the mini-infobar from appearing on mobile
34
+ e.preventDefault();
35
+ // Stash the event so it can be triggered later.
36
+ setDeferredPrompt(e);
37
+ setIsInstallable(true);
38
+ console.log('✅ PWA Install Prompt captured');
39
+ };
40
+
41
+ window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
42
+
43
+ return () => {
44
+ window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
45
+ };
46
+ }, []);
47
+
48
+ const installApp = async () => {
49
+ if (!deferredPrompt) {
50
+ return;
51
+ }
52
+
53
+ // Show the install prompt
54
+ deferredPrompt.prompt();
55
+
56
+ // Wait for the user to respond to the prompt
57
+ const { outcome } = await deferredPrompt.userChoice;
58
+
59
+ if (outcome === 'accepted') {
60
+ console.log('User accepted the install prompt');
61
+ } else {
62
+ console.log('User dismissed the install prompt');
63
+ }
64
+
65
+ // We've used the prompt, so clear it
66
+ setDeferredPrompt(null);
67
+ setIsInstallable(false);
68
+ };
69
+
70
+ return (
71
+ <PWAContext.Provider value={{ deferredPrompt, isInstallable, isIOS, isStandalone, installApp }}>
72
+ {children}
73
+ </PWAContext.Provider>
74
+ );
75
+ };
76
+
77
+ export const usePWA = () => {
78
+ const context = useContext(PWAContext);
79
+ if (context === undefined) {
80
+ throw new Error('usePWA must be used within a PWAProvider');
81
+ }
82
+ return context;
83
+ };
dist/404.html ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Page Not Found</title>
7
+
8
+ <style media="screen">
9
+ body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
10
+ #message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px 16px; border-radius: 3px; }
11
+ #message h3 { color: #888; font-weight: normal; font-size: 16px; margin: 16px 0 12px; }
12
+ #message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
13
+ #message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
14
+ #message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
15
+ #message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
16
+ #message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
17
+ #load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
18
+ @media (max-width: 600px) {
19
+ body, #message { margin-top: 0; background: white; box-shadow: none; }
20
+ body { border-top: 16px solid #ffa100; }
21
+ }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <div id="message">
26
+ <h2>404</h2>
27
+ <h1>Page Not Found</h1>
28
+ <p>The specified file was not found on this website. Please check the URL for mistakes and try again.</p>
29
+ <h3>Why am I seeing this?</h3>
30
+ <p>This page was generated by the Firebase Command-Line Interface. To modify it, edit the <code>404.html</code> file in your project's configured <code>public</code> directory.</p>
31
+ </div>
32
+ </body>
33
+ </html>
dist/assets/html2canvas.esm-QH1iLAAe.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/assets/index-DEfz7c9M.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/assets/index.es-qlPCKdii.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/assets/purify.es-B6FQ9oRL.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ /*! @license DOMPurify 3.3.0 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.0/LICENSE */const{entries:_t,setPrototypeOf:ct,isFrozen:Yt,getPrototypeOf:Xt,getOwnPropertyDescriptor:jt}=Object;let{freeze:S,seal:y,create:Pe}=Object,{apply:ve,construct:ke}=typeof Reflect<"u"&&Reflect;S||(S=function(o){return o});y||(y=function(o){return o});ve||(ve=function(o,l){for(var a=arguments.length,c=new Array(a>2?a-2:0),O=2;O<a;O++)c[O-2]=arguments[O];return o.apply(l,c)});ke||(ke=function(o){for(var l=arguments.length,a=new Array(l>1?l-1:0),c=1;c<l;c++)a[c-1]=arguments[c];return new o(...a)});const ce=R(Array.prototype.forEach),Vt=R(Array.prototype.lastIndexOf),ft=R(Array.prototype.pop),q=R(Array.prototype.push),$t=R(Array.prototype.splice),ue=R(String.prototype.toLowerCase),Ne=R(String.prototype.toString),Ie=R(String.prototype.match),K=R(String.prototype.replace),qt=R(String.prototype.indexOf),Kt=R(String.prototype.trim),b=R(Object.prototype.hasOwnProperty),A=R(RegExp.prototype.test),Z=Zt(TypeError);function R(s){return function(o){o instanceof RegExp&&(o.lastIndex=0);for(var l=arguments.length,a=new Array(l>1?l-1:0),c=1;c<l;c++)a[c-1]=arguments[c];return ve(s,o,a)}}function Zt(s){return function(){for(var o=arguments.length,l=new Array(o),a=0;a<o;a++)l[a]=arguments[a];return ke(s,l)}}function r(s,o){let l=arguments.length>2&&arguments[2]!==void 0?arguments[2]:ue;ct&&ct(s,null);let a=o.length;for(;a--;){let c=o[a];if(typeof c=="string"){const O=l(c);O!==c&&(Yt(o)||(o[a]=O),c=O)}s[c]=!0}return s}function Jt(s){for(let o=0;o<s.length;o++)b(s,o)||(s[o]=null);return s}function M(s){const o=Pe(null);for(const[l,a]of _t(s))b(s,l)&&(Array.isArray(a)?o[l]=Jt(a):a&&typeof a=="object"&&a.constructor===Object?o[l]=M(a):o[l]=a);return o}function J(s,o){for(;s!==null;){const a=jt(s,o);if(a){if(a.get)return R(a.get);if(typeof a.value=="function")return R(a.value)}s=Xt(s)}function l(){return null}return l}const ut=S(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),Ce=S(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),Me=S(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),Qt=S(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),we=S(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),en=S(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),mt=S(["#text"]),pt=S(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns","slot"]),xe=S(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),dt=S(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),fe=S(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),tn=y(/\{\{[\w\W]*|[\w\W]*\}\}/gm),nn=y(/<%[\w\W]*|[\w\W]*%>/gm),on=y(/\$\{[\w\W]*/gm),an=y(/^data-[\-\w.\u00B7-\uFFFF]+$/),rn=y(/^aria-[\-\w]+$/),gt=y(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),sn=y(/^(?:\w+script|data):/i),ln=y(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),ht=y(/^html$/i),cn=y(/^[a-z][.\w]*(-[.\w]+)+$/i);var Tt=Object.freeze({__proto__:null,ARIA_ATTR:rn,ATTR_WHITESPACE:ln,CUSTOM_ELEMENT:cn,DATA_ATTR:an,DOCTYPE_NAME:ht,ERB_EXPR:nn,IS_ALLOWED_URI:gt,IS_SCRIPT_OR_DATA:sn,MUSTACHE_EXPR:tn,TMPLIT_EXPR:on});const Q={element:1,text:3,progressingInstruction:7,comment:8,document:9},fn=function(){return typeof window>"u"?null:window},un=function(o,l){if(typeof o!="object"||typeof o.createPolicy!="function")return null;let a=null;const c="data-tt-policy-suffix";l&&l.hasAttribute(c)&&(a=l.getAttribute(c));const O="dompurify"+(a?"#"+a:"");try{return o.createPolicy(O,{createHTML(P){return P},createScriptURL(P){return P}})}catch{return console.warn("TrustedTypes policy "+O+" could not be created."),null}},Et=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function At(){let s=arguments.length>0&&arguments[0]!==void 0?arguments[0]:fn();const o=i=>At(i);if(o.version="3.3.0",o.removed=[],!s||!s.document||s.document.nodeType!==Q.document||!s.Element)return o.isSupported=!1,o;let{document:l}=s;const a=l,c=a.currentScript,{DocumentFragment:O,HTMLTemplateElement:P,Node:me,Element:Ue,NodeFilter:B,NamedNodeMap:St=s.NamedNodeMap||s.MozNamedAttrMap,HTMLFormElement:Rt,DOMParser:Ot,trustedTypes:ee}=s,Y=Ue.prototype,Lt=J(Y,"cloneNode"),yt=J(Y,"remove"),bt=J(Y,"nextSibling"),Dt=J(Y,"childNodes"),te=J(Y,"parentNode");if(typeof P=="function"){const i=l.createElement("template");i.content&&i.content.ownerDocument&&(l=i.content.ownerDocument)}let g,X="";const{implementation:pe,createNodeIterator:Nt,createDocumentFragment:It,getElementsByTagName:Ct}=l,{importNode:Mt}=a;let h=Et();o.isSupported=typeof _t=="function"&&typeof te=="function"&&pe&&pe.createHTMLDocument!==void 0;const{MUSTACHE_EXPR:de,ERB_EXPR:Te,TMPLIT_EXPR:Ee,DATA_ATTR:wt,ARIA_ATTR:xt,IS_SCRIPT_OR_DATA:Pt,ATTR_WHITESPACE:Fe,CUSTOM_ELEMENT:vt}=Tt;let{IS_ALLOWED_URI:He}=Tt,p=null;const ze=r({},[...ut,...Ce,...Me,...we,...mt]);let T=null;const Ge=r({},[...pt,...xe,...dt,...fe]);let u=Object.seal(Pe(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),j=null,_e=null;const v=Object.seal(Pe(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let We=!0,ge=!0,Be=!1,Ye=!0,k=!1,ne=!0,w=!1,he=!1,Ae=!1,U=!1,oe=!1,ie=!1,Xe=!0,je=!1;const kt="user-content-";let Se=!0,V=!1,F={},H=null;const Ve=r({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let $e=null;const qe=r({},["audio","video","img","source","image","track"]);let Re=null;const Ke=r({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),ae="http://www.w3.org/1998/Math/MathML",re="http://www.w3.org/2000/svg",N="http://www.w3.org/1999/xhtml";let z=N,Oe=!1,Le=null;const Ut=r({},[ae,re,N],Ne);let se=r({},["mi","mo","mn","ms","mtext"]),le=r({},["annotation-xml"]);const Ft=r({},["title","style","font","a","script"]);let $=null;const Ht=["application/xhtml+xml","text/html"],zt="text/html";let d=null,G=null;const Gt=l.createElement("form"),Ze=function(e){return e instanceof RegExp||e instanceof Function},ye=function(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(!(G&&G===e)){if((!e||typeof e!="object")&&(e={}),e=M(e),$=Ht.indexOf(e.PARSER_MEDIA_TYPE)===-1?zt:e.PARSER_MEDIA_TYPE,d=$==="application/xhtml+xml"?Ne:ue,p=b(e,"ALLOWED_TAGS")?r({},e.ALLOWED_TAGS,d):ze,T=b(e,"ALLOWED_ATTR")?r({},e.ALLOWED_ATTR,d):Ge,Le=b(e,"ALLOWED_NAMESPACES")?r({},e.ALLOWED_NAMESPACES,Ne):Ut,Re=b(e,"ADD_URI_SAFE_ATTR")?r(M(Ke),e.ADD_URI_SAFE_ATTR,d):Ke,$e=b(e,"ADD_DATA_URI_TAGS")?r(M(qe),e.ADD_DATA_URI_TAGS,d):qe,H=b(e,"FORBID_CONTENTS")?r({},e.FORBID_CONTENTS,d):Ve,j=b(e,"FORBID_TAGS")?r({},e.FORBID_TAGS,d):M({}),_e=b(e,"FORBID_ATTR")?r({},e.FORBID_ATTR,d):M({}),F=b(e,"USE_PROFILES")?e.USE_PROFILES:!1,We=e.ALLOW_ARIA_ATTR!==!1,ge=e.ALLOW_DATA_ATTR!==!1,Be=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Ye=e.ALLOW_SELF_CLOSE_IN_ATTR!==!1,k=e.SAFE_FOR_TEMPLATES||!1,ne=e.SAFE_FOR_XML!==!1,w=e.WHOLE_DOCUMENT||!1,U=e.RETURN_DOM||!1,oe=e.RETURN_DOM_FRAGMENT||!1,ie=e.RETURN_TRUSTED_TYPE||!1,Ae=e.FORCE_BODY||!1,Xe=e.SANITIZE_DOM!==!1,je=e.SANITIZE_NAMED_PROPS||!1,Se=e.KEEP_CONTENT!==!1,V=e.IN_PLACE||!1,He=e.ALLOWED_URI_REGEXP||gt,z=e.NAMESPACE||N,se=e.MATHML_TEXT_INTEGRATION_POINTS||se,le=e.HTML_INTEGRATION_POINTS||le,u=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&Ze(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(u.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&Ze(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(u.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements=="boolean"&&(u.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),k&&(ge=!1),oe&&(U=!0),F&&(p=r({},mt),T=[],F.html===!0&&(r(p,ut),r(T,pt)),F.svg===!0&&(r(p,Ce),r(T,xe),r(T,fe)),F.svgFilters===!0&&(r(p,Me),r(T,xe),r(T,fe)),F.mathMl===!0&&(r(p,we),r(T,dt),r(T,fe))),e.ADD_TAGS&&(typeof e.ADD_TAGS=="function"?v.tagCheck=e.ADD_TAGS:(p===ze&&(p=M(p)),r(p,e.ADD_TAGS,d))),e.ADD_ATTR&&(typeof e.ADD_ATTR=="function"?v.attributeCheck=e.ADD_ATTR:(T===Ge&&(T=M(T)),r(T,e.ADD_ATTR,d))),e.ADD_URI_SAFE_ATTR&&r(Re,e.ADD_URI_SAFE_ATTR,d),e.FORBID_CONTENTS&&(H===Ve&&(H=M(H)),r(H,e.FORBID_CONTENTS,d)),Se&&(p["#text"]=!0),w&&r(p,["html","head","body"]),p.table&&(r(p,["tbody"]),delete j.tbody),e.TRUSTED_TYPES_POLICY){if(typeof e.TRUSTED_TYPES_POLICY.createHTML!="function")throw Z('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof e.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw Z('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');g=e.TRUSTED_TYPES_POLICY,X=g.createHTML("")}else g===void 0&&(g=un(ee,c)),g!==null&&typeof X=="string"&&(X=g.createHTML(""));S&&S(e),G=e}},Je=r({},[...Ce,...Me,...Qt]),Qe=r({},[...we,...en]),Wt=function(e){let t=te(e);(!t||!t.tagName)&&(t={namespaceURI:z,tagName:"template"});const n=ue(e.tagName),f=ue(t.tagName);return Le[e.namespaceURI]?e.namespaceURI===re?t.namespaceURI===N?n==="svg":t.namespaceURI===ae?n==="svg"&&(f==="annotation-xml"||se[f]):!!Je[n]:e.namespaceURI===ae?t.namespaceURI===N?n==="math":t.namespaceURI===re?n==="math"&&le[f]:!!Qe[n]:e.namespaceURI===N?t.namespaceURI===re&&!le[f]||t.namespaceURI===ae&&!se[f]?!1:!Qe[n]&&(Ft[n]||!Je[n]):!!($==="application/xhtml+xml"&&Le[e.namespaceURI]):!1},D=function(e){q(o.removed,{element:e});try{te(e).removeChild(e)}catch{yt(e)}},x=function(e,t){try{q(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch{q(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),e==="is")if(U||oe)try{D(t)}catch{}else try{t.setAttribute(e,"")}catch{}},et=function(e){let t=null,n=null;if(Ae)e="<remove></remove>"+e;else{const m=Ie(e,/^[\r\n\t ]+/);n=m&&m[0]}$==="application/xhtml+xml"&&z===N&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");const f=g?g.createHTML(e):e;if(z===N)try{t=new Ot().parseFromString(f,$)}catch{}if(!t||!t.documentElement){t=pe.createDocument(z,"template",null);try{t.documentElement.innerHTML=Oe?X:f}catch{}}const _=t.body||t.documentElement;return e&&n&&_.insertBefore(l.createTextNode(n),_.childNodes[0]||null),z===N?Ct.call(t,w?"html":"body")[0]:w?t.documentElement:_},tt=function(e){return Nt.call(e.ownerDocument||e,e,B.SHOW_ELEMENT|B.SHOW_COMMENT|B.SHOW_TEXT|B.SHOW_PROCESSING_INSTRUCTION|B.SHOW_CDATA_SECTION,null)},be=function(e){return e instanceof Rt&&(typeof e.nodeName!="string"||typeof e.textContent!="string"||typeof e.removeChild!="function"||!(e.attributes instanceof St)||typeof e.removeAttribute!="function"||typeof e.setAttribute!="function"||typeof e.namespaceURI!="string"||typeof e.insertBefore!="function"||typeof e.hasChildNodes!="function")},nt=function(e){return typeof me=="function"&&e instanceof me};function I(i,e,t){ce(i,n=>{n.call(o,e,t,G)})}const ot=function(e){let t=null;if(I(h.beforeSanitizeElements,e,null),be(e))return D(e),!0;const n=d(e.nodeName);if(I(h.uponSanitizeElement,e,{tagName:n,allowedTags:p}),ne&&e.hasChildNodes()&&!nt(e.firstElementChild)&&A(/<[/\w!]/g,e.innerHTML)&&A(/<[/\w!]/g,e.textContent)||e.nodeType===Q.progressingInstruction||ne&&e.nodeType===Q.comment&&A(/<[/\w]/g,e.data))return D(e),!0;if(!(v.tagCheck instanceof Function&&v.tagCheck(n))&&(!p[n]||j[n])){if(!j[n]&&at(n)&&(u.tagNameCheck instanceof RegExp&&A(u.tagNameCheck,n)||u.tagNameCheck instanceof Function&&u.tagNameCheck(n)))return!1;if(Se&&!H[n]){const f=te(e)||e.parentNode,_=Dt(e)||e.childNodes;if(_&&f){const m=_.length;for(let L=m-1;L>=0;--L){const C=Lt(_[L],!0);C.__removalCount=(e.__removalCount||0)+1,f.insertBefore(C,bt(e))}}}return D(e),!0}return e instanceof Ue&&!Wt(e)||(n==="noscript"||n==="noembed"||n==="noframes")&&A(/<\/no(script|embed|frames)/i,e.innerHTML)?(D(e),!0):(k&&e.nodeType===Q.text&&(t=e.textContent,ce([de,Te,Ee],f=>{t=K(t,f," ")}),e.textContent!==t&&(q(o.removed,{element:e.cloneNode()}),e.textContent=t)),I(h.afterSanitizeElements,e,null),!1)},it=function(e,t,n){if(Xe&&(t==="id"||t==="name")&&(n in l||n in Gt))return!1;if(!(ge&&!_e[t]&&A(wt,t))){if(!(We&&A(xt,t))){if(!(v.attributeCheck instanceof Function&&v.attributeCheck(t,e))){if(!T[t]||_e[t]){if(!(at(e)&&(u.tagNameCheck instanceof RegExp&&A(u.tagNameCheck,e)||u.tagNameCheck instanceof Function&&u.tagNameCheck(e))&&(u.attributeNameCheck instanceof RegExp&&A(u.attributeNameCheck,t)||u.attributeNameCheck instanceof Function&&u.attributeNameCheck(t,e))||t==="is"&&u.allowCustomizedBuiltInElements&&(u.tagNameCheck instanceof RegExp&&A(u.tagNameCheck,n)||u.tagNameCheck instanceof Function&&u.tagNameCheck(n))))return!1}else if(!Re[t]){if(!A(He,K(n,Fe,""))){if(!((t==="src"||t==="xlink:href"||t==="href")&&e!=="script"&&qt(n,"data:")===0&&$e[e])){if(!(Be&&!A(Pt,K(n,Fe,"")))){if(n)return!1}}}}}}}return!0},at=function(e){return e!=="annotation-xml"&&Ie(e,vt)},rt=function(e){I(h.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||be(e))return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:T,forceKeepAttr:void 0};let f=t.length;for(;f--;){const _=t[f],{name:m,namespaceURI:L,value:C}=_,W=d(m),De=C;let E=m==="value"?De:Kt(De);if(n.attrName=W,n.attrValue=E,n.keepAttr=!0,n.forceKeepAttr=void 0,I(h.uponSanitizeAttribute,e,n),E=n.attrValue,je&&(W==="id"||W==="name")&&(x(m,e),E=kt+E),ne&&A(/((--!?|])>)|<\/(style|title|textarea)/i,E)){x(m,e);continue}if(W==="attributename"&&Ie(E,"href")){x(m,e);continue}if(n.forceKeepAttr)continue;if(!n.keepAttr){x(m,e);continue}if(!Ye&&A(/\/>/i,E)){x(m,e);continue}k&&ce([de,Te,Ee],lt=>{E=K(E,lt," ")});const st=d(e.nodeName);if(!it(st,W,E)){x(m,e);continue}if(g&&typeof ee=="object"&&typeof ee.getAttributeType=="function"&&!L)switch(ee.getAttributeType(st,W)){case"TrustedHTML":{E=g.createHTML(E);break}case"TrustedScriptURL":{E=g.createScriptURL(E);break}}if(E!==De)try{L?e.setAttributeNS(L,m,E):e.setAttribute(m,E),be(e)?D(e):ft(o.removed)}catch{x(m,e)}}I(h.afterSanitizeAttributes,e,null)},Bt=function i(e){let t=null;const n=tt(e);for(I(h.beforeSanitizeShadowDOM,e,null);t=n.nextNode();)I(h.uponSanitizeShadowNode,t,null),ot(t),rt(t),t.content instanceof O&&i(t.content);I(h.afterSanitizeShadowDOM,e,null)};return o.sanitize=function(i){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},t=null,n=null,f=null,_=null;if(Oe=!i,Oe&&(i="<!-->"),typeof i!="string"&&!nt(i))if(typeof i.toString=="function"){if(i=i.toString(),typeof i!="string")throw Z("dirty is not a string, aborting")}else throw Z("toString is not a function");if(!o.isSupported)return i;if(he||ye(e),o.removed=[],typeof i=="string"&&(V=!1),V){if(i.nodeName){const C=d(i.nodeName);if(!p[C]||j[C])throw Z("root node is forbidden and cannot be sanitized in-place")}}else if(i instanceof me)t=et("<!---->"),n=t.ownerDocument.importNode(i,!0),n.nodeType===Q.element&&n.nodeName==="BODY"||n.nodeName==="HTML"?t=n:t.appendChild(n);else{if(!U&&!k&&!w&&i.indexOf("<")===-1)return g&&ie?g.createHTML(i):i;if(t=et(i),!t)return U?null:ie?X:""}t&&Ae&&D(t.firstChild);const m=tt(V?i:t);for(;f=m.nextNode();)ot(f),rt(f),f.content instanceof O&&Bt(f.content);if(V)return i;if(U){if(oe)for(_=It.call(t.ownerDocument);t.firstChild;)_.appendChild(t.firstChild);else _=t;return(T.shadowroot||T.shadowrootmode)&&(_=Mt.call(a,_,!0)),_}let L=w?t.outerHTML:t.innerHTML;return w&&p["!doctype"]&&t.ownerDocument&&t.ownerDocument.doctype&&t.ownerDocument.doctype.name&&A(ht,t.ownerDocument.doctype.name)&&(L="<!DOCTYPE "+t.ownerDocument.doctype.name+`>
2
+ `+L),k&&ce([de,Te,Ee],C=>{L=K(L,C," ")}),g&&ie?g.createHTML(L):L},o.setConfig=function(){let i=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};ye(i),he=!0},o.clearConfig=function(){G=null,he=!1},o.isValidAttribute=function(i,e,t){G||ye({});const n=d(i),f=d(e);return it(n,f,t)},o.addHook=function(i,e){typeof e=="function"&&q(h[i],e)},o.removeHook=function(i,e){if(e!==void 0){const t=Vt(h[i],e);return t===-1?void 0:$t(h[i],t,1)[0]}return ft(h[i])},o.removeHooks=function(i){h[i]=[]},o.removeAllHooks=function(){h=Et()},o}var mn=At();export{mn as default};
dist/icon-192.png ADDED

Git LFS Details

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

Git LFS Details

  • SHA256: df87a32301726f75e84cf90101da2089eae3509a50f0a508f7782dd3b513c3c7
  • Pointer size: 131 Bytes
  • Size of remote file: 360 kB
dist/index.html ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
51
+ <link rel="stylesheet" href="/index.css">
52
+ <script type="importmap">
53
+ {
54
+ "imports": {
55
+ "react-router-dom": "https://aistudiocdn.com/react-router-dom@^7.9.6",
56
+ "react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
57
+ "react/": "https://aistudiocdn.com/react@^19.2.0/",
58
+ "react": "https://aistudiocdn.com/react@^19.2.0",
59
+ "lucide-react": "https://aistudiocdn.com/lucide-react@^0.554.0",
60
+ "recharts": "https://aistudiocdn.com/recharts@^3.4.1"
61
+ }
62
+ }
63
+ </script>
64
+ <script type="module" crossorigin src="/assets/index-DEfz7c9M.js"></script>
65
+ </head>
66
+
67
+ <body class="bg-gray-50 text-gray-900">
68
+ <div id="root"></div>
69
+
70
+ <!-- Service Worker Registration -->
71
+ <script>
72
+ if ('serviceWorker' in navigator) {
73
+ window.addEventListener('load', () => {
74
+ navigator.serviceWorker.register('/service-worker.js')
75
+ .then((registration) => {
76
+ console.log('[SW] Registered successfully:', registration.scope);
77
+
78
+ // Check for updates
79
+ registration.addEventListener('updatefound', () => {
80
+ const newWorker = registration.installing;
81
+ newWorker.addEventListener('statechange', () => {
82
+ if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
83
+ console.log('[SW] New version available! Refresh to update.');
84
+ }
85
+ });
86
+ });
87
+ })
88
+ .catch((error) => {
89
+ console.error('[SW] Registration failed:', error);
90
+ });
91
+ });
92
+ }
93
+ </script>
94
+ </body>
95
+
96
+ </html>
dist/manifest.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Pattanshetty Inventory",
3
+ "short_name": "Pattanshetty",
4
+ "description": "Inventory Management System for Mirchi Trading",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#f9fafb",
8
+ "theme_color": "#0d9488",
9
+ "orientation": "portrait-primary",
10
+ "icons": [
11
+ {
12
+ "src": "/icon-192.png",
13
+ "sizes": "192x192",
14
+ "type": "image/png",
15
+ "purpose": "any maskable"
16
+ },
17
+ {
18
+ "src": "/icon-512.png",
19
+ "sizes": "512x512",
20
+ "type": "image/png",
21
+ "purpose": "any maskable"
22
+ }
23
+ ],
24
+ "categories": [
25
+ "business",
26
+ "productivity"
27
+ ],
28
+ "screenshots": [],
29
+ "shortcuts": [
30
+ {
31
+ "name": "New Purchase Bill",
32
+ "short_name": "Purchase",
33
+ "description": "Create new purchase bill",
34
+ "url": "/awaak",
35
+ "icons": []
36
+ },
37
+ {
38
+ "name": "New Sales Bill",
39
+ "short_name": "Sales",
40
+ "description": "Create new sales bill",
41
+ "url": "/jawaak",
42
+ "icons": []
43
+ },
44
+ {
45
+ "name": "Party Ledger",
46
+ "short_name": "Ledger",
47
+ "description": "View party ledger",
48
+ "url": "/ledger",
49
+ "icons": []
50
+ }
51
+ ]
52
+ }
dist/service-worker.js ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const CACHE_NAME = 'pattanshetty-v2';
2
+ const urlsToCache = [
3
+ '/',
4
+ '/index.html',
5
+ ];
6
+
7
+ // Install event - cache resources
8
+ self.addEventListener('install', (event) => {
9
+ console.log('[ServiceWorker] Installing...');
10
+ event.waitUntil(
11
+ caches.open(CACHE_NAME)
12
+ .then((cache) => {
13
+ console.log('[ServiceWorker] Caching app shell');
14
+ return cache.addAll(urlsToCache).catch((err) => {
15
+ console.error('[ServiceWorker] Cache addAll failed:', err);
16
+ });
17
+ })
18
+ .catch((err) => {
19
+ console.error('[ServiceWorker] Cache open failed:', err);
20
+ })
21
+ );
22
+ self.skipWaiting();
23
+ });
24
+
25
+ // Activate event - clean up old caches
26
+ self.addEventListener('activate', (event) => {
27
+ console.log('[ServiceWorker] Activating...');
28
+ event.waitUntil(
29
+ caches.keys().then((cacheNames) => {
30
+ return Promise.all(
31
+ cacheNames.map((cacheName) => {
32
+ if (cacheName !== CACHE_NAME) {
33
+ console.log('[ServiceWorker] Deleting old cache:', cacheName);
34
+ return caches.delete(cacheName);
35
+ }
36
+ })
37
+ );
38
+ }).catch((err) => {
39
+ console.error('[ServiceWorker] Activation failed:', err);
40
+ })
41
+ );
42
+ self.clients.claim();
43
+ });
44
+
45
+ // Fetch event - Network first, fallback to cache
46
+ self.addEventListener('fetch', (event) => {
47
+ // Skip non-GET requests
48
+ if (event.request.method !== 'GET') {
49
+ return;
50
+ }
51
+
52
+ event.respondWith(
53
+ fetch(event.request)
54
+ .then((response) => {
55
+ // Check if valid response
56
+ if (!response || response.status !== 200 || response.type === 'error') {
57
+ return response;
58
+ }
59
+
60
+ // Clone the response
61
+ const responseToCache = response.clone();
62
+
63
+ caches.open(CACHE_NAME)
64
+ .then((cache) => {
65
+ cache.put(event.request, responseToCache);
66
+ })
67
+ .catch((err) => {
68
+ console.error('[ServiceWorker] Cache put failed:', err);
69
+ });
70
+
71
+ return response;
72
+ })
73
+ .catch((err) => {
74
+ console.log('[ServiceWorker] Fetch failed, trying cache:', err);
75
+ // Network failed, try cache
76
+ return caches.match(event.request)
77
+ .then((cachedResponse) => {
78
+ if (cachedResponse) {
79
+ return cachedResponse;
80
+ }
81
+ // Return offline page or error
82
+ return new Response('Offline - No cached version available', {
83
+ status: 503,
84
+ statusText: 'Service Unavailable',
85
+ headers: new Headers({
86
+ 'Content-Type': 'text/plain'
87
+ })
88
+ });
89
+ });
90
+ })
91
+ );
92
+ });
firebase.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "hosting": {
3
+ "public": "dist",
4
+ "ignore": [
5
+ "firebase.json",
6
+ "**/.*",
7
+ "**/node_modules/**"
8
+ ]
9
+ }
10
+ }
index.html ADDED
@@ -0,0 +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>
index.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+
5
+ const rootElement = document.getElementById('root');
6
+ if (!rootElement) {
7
+ throw new Error("Could not find root element to mount to");
8
+ }
9
+
10
+ const root = ReactDOM.createRoot(rootElement);
11
+ root.render(
12
+ <React.StrictMode>
13
+ <App />
14
+ </React.StrictMode>
15
+ );
metadata.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "name": "Mirchi Vyapar Manager",
3
+ "description": "A comprehensive Chili Trading Management System for managing Purchases (Jawaak), Sales (Awaak), Inventory (Stock), and Financial Ledgers with Marathi interface support.",
4
+ "requestFramePermissions": []
5
+ }
nginx.conf ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ server {
2
+ listen 7860;
3
+ server_name _;
4
+
5
+ root /usr/share/nginx/html;
6
+ index index.html;
7
+
8
+ # Enable gzip compression
9
+ gzip on;
10
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
11
+
12
+ # Handle client-side routing
13
+ location / {
14
+ try_files $uri $uri/ /index.html;
15
+ }
16
+
17
+ # Cache static assets
18
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
19
+ expires 1y;
20
+ add_header Cache-Control "public, immutable";
21
+ }
22
+
23
+ # Security headers
24
+ add_header X-Frame-Options "SAMEORIGIN" always;
25
+ add_header X-Content-Type-Options "nosniff" always;
26
+ add_header X-XSS-Protection "1; mode=block" always;
27
+ }
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ "xlsx-js-style": "^1.2.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.14.0",
25
+ "@vitejs/plugin-react": "^5.0.0",
26
+ "typescript": "~5.8.2",
27
+ "vite": "^6.2.0"
28
+ }
29
+ }
pages/Analysis.tsx ADDED
@@ -0,0 +1,836 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { getActiveLots, getTransactions } from '../services/db';
3
+ import { BillType, Lot, Transaction } from '../types';
4
+ import { TrendingUp, Search, Download, ArrowLeft } from 'lucide-react';
5
+ import { exportAnalysisReport, exportLotDetailAnalysis } from '../utils/exportToExcel';
6
+
7
+ interface LotAnalysisRow {
8
+ lotId: string;
9
+ lotNumber: string;
10
+ mirchiName: string;
11
+ purchaseQty: number;
12
+ purchaseValue: number;
13
+ avgPurchaseRate: number;
14
+ soldQty: number;
15
+ salesValue: number;
16
+ avgSaleRate: number;
17
+ realizedProfit: number;
18
+ marginPercent: number;
19
+ remainingQty: number;
20
+ remainingValueAtCost: number;
21
+ isActive: boolean;
22
+ }
23
+
24
+ interface LotDetailRow {
25
+ id: string;
26
+ date: string;
27
+ billNo: string;
28
+ typeLabel: string;
29
+ directionLabel: string;
30
+ qtyKg: number;
31
+ ratePerKg: number;
32
+ grossAmount: number;
33
+ expensesAllocated: number;
34
+ netAmount: number;
35
+ profitImpact: number;
36
+ }
37
+
38
+ const Analysis: React.FC = () => {
39
+ const [lots, setLots] = useState<Lot[]>([]);
40
+ const [transactions, setTransactions] = useState<Transaction[]>([]);
41
+ const [searchTerm, setSearchTerm] = useState('');
42
+ const [viewMode, setViewMode] = useState<'summary' | 'detail'>('summary');
43
+ const [selectedLotId, setSelectedLotId] = useState<string | null>(null);
44
+ const [selectedLotNumber, setSelectedLotNumber] = useState<string | null>(null);
45
+ const [selectedLotMirchiName, setSelectedLotMirchiName] = useState<string | null>(null);
46
+ const [lotFilter, setLotFilter] = useState<'all' | 'active' | 'closed'>('all');
47
+
48
+ useEffect(() => {
49
+ const fetchData = async () => {
50
+ const [lotsData, transactionsData] = await Promise.all([
51
+ getActiveLots(),
52
+ getTransactions(),
53
+ ]);
54
+ setLots(lotsData);
55
+ setTransactions(transactionsData);
56
+ };
57
+
58
+ fetchData();
59
+ }, []);
60
+
61
+ const analysisRowsAll: LotAnalysisRow[] = useMemo(() => {
62
+ if (!transactions.length) {
63
+ return [];
64
+ }
65
+
66
+ const lotMetaById = new Map<string, Lot>();
67
+ const lotMetaByNumber = new Map<string, Lot>();
68
+ lots.forEach((lot) => {
69
+ lotMetaById.set(lot.id, lot);
70
+ if (lot.lot_number) {
71
+ lotMetaByNumber.set(lot.lot_number.toLowerCase(), lot);
72
+ }
73
+ });
74
+
75
+ const byLot: Record<
76
+ string,
77
+ {
78
+ lotId: string;
79
+ lotNumber: string;
80
+ mirchiName: string;
81
+ purchaseQty: number;
82
+ purchaseValue: number;
83
+ soldQty: number;
84
+ salesValue: number;
85
+ }
86
+ > = {};
87
+
88
+ transactions.forEach((tx) => {
89
+ const txTotalWeight =
90
+ tx.items.reduce((sum, item) => sum + item.net_weight, 0) || 1;
91
+
92
+ tx.items.forEach((item) => {
93
+ const rawLotId = item.lot_id as string | undefined;
94
+ const rawLotNumber = (item as any).lot_number as string | undefined;
95
+
96
+ const metaById = rawLotId ? lotMetaById.get(rawLotId) : undefined;
97
+ const metaByNumber = rawLotNumber
98
+ ? lotMetaByNumber.get(rawLotNumber.toLowerCase())
99
+ : undefined;
100
+ const meta = metaById || metaByNumber;
101
+
102
+ const key = meta?.id || rawLotId || rawLotNumber;
103
+ if (!key) {
104
+ return;
105
+ }
106
+
107
+ let existing = byLot[key];
108
+
109
+ if (!existing) {
110
+ const fallbackName = item.mirchi_name || 'Unknown';
111
+
112
+ existing = {
113
+ lotId: meta?.id || rawLotId || key,
114
+ lotNumber: meta?.lot_number || rawLotNumber || key.slice(0, 8),
115
+ mirchiName: meta?.mirchi_name || fallbackName,
116
+ purchaseQty: 0,
117
+ purchaseValue: 0,
118
+ soldQty: 0,
119
+ salesValue: 0,
120
+ };
121
+ byLot[key] = existing;
122
+ }
123
+
124
+ const weightShare = item.net_weight / txTotalWeight;
125
+ const allocatedExpenses = tx.total_expenses * weightShare;
126
+
127
+ if (tx.bill_type === BillType.AWAAK) {
128
+ const sign = tx.is_return ? -1 : 1;
129
+ const cost = item.item_total + allocatedExpenses;
130
+ existing.purchaseQty += sign * item.net_weight;
131
+ existing.purchaseValue += sign * cost;
132
+ } else {
133
+ const sign = tx.is_return ? -1 : 1;
134
+ const revenue = item.item_total - allocatedExpenses;
135
+ existing.soldQty += sign * item.net_weight;
136
+ existing.salesValue += sign * revenue;
137
+ }
138
+ });
139
+ });
140
+
141
+ return Object.values(byLot).map((entry) => {
142
+ let { purchaseQty, purchaseValue, soldQty, salesValue } = entry;
143
+
144
+ if (purchaseQty < 0) {
145
+ purchaseQty = 0;
146
+ purchaseValue = 0;
147
+ }
148
+ if (soldQty < 0) {
149
+ soldQty = 0;
150
+ salesValue = 0;
151
+ }
152
+
153
+ const avgPurchaseRate = purchaseQty > 0 ? purchaseValue / purchaseQty : 0;
154
+ const avgSaleRate = soldQty > 0 ? salesValue / soldQty : 0;
155
+ const realizedProfit = salesValue - avgPurchaseRate * soldQty;
156
+ const marginPercent =
157
+ purchaseValue > 0 ? (realizedProfit / purchaseValue) * 100 : 0;
158
+
159
+ const metaFromId = lotMetaById.get(entry.lotId);
160
+ const metaFromNumber = lotMetaByNumber.get(entry.lotNumber.toLowerCase());
161
+ const meta = metaFromId || metaFromNumber;
162
+ const isActive = !!meta;
163
+ const remainingQty = isActive
164
+ ? Math.max(0, meta?.remaining_quantity ?? 0)
165
+ : 0;
166
+ const remainingValueAtCost = remainingQty * avgPurchaseRate;
167
+
168
+ return {
169
+ lotId: entry.lotId,
170
+ lotNumber: entry.lotNumber,
171
+ mirchiName: entry.mirchiName,
172
+ purchaseQty,
173
+ purchaseValue,
174
+ avgPurchaseRate,
175
+ soldQty,
176
+ salesValue,
177
+ avgSaleRate,
178
+ realizedProfit,
179
+ marginPercent,
180
+ remainingQty,
181
+ remainingValueAtCost,
182
+ isActive,
183
+ };
184
+ });
185
+ }, [lots, transactions]);
186
+
187
+ const analysisRows: LotAnalysisRow[] = useMemo(
188
+ () =>
189
+ analysisRowsAll.filter((row) => {
190
+ const term = searchTerm.toLowerCase();
191
+ const matchesSearch =
192
+ row.mirchiName.toLowerCase().includes(term) ||
193
+ row.lotNumber.toLowerCase().includes(term)
194
+ || !term;
195
+
196
+ if (!matchesSearch) return false;
197
+
198
+ if (lotFilter === 'active') {
199
+ return row.isActive;
200
+ }
201
+ if (lotFilter === 'closed') {
202
+ return !row.isActive;
203
+ }
204
+
205
+ return true;
206
+ }),
207
+ [analysisRowsAll, searchTerm, lotFilter]
208
+ );
209
+
210
+ const totals = useMemo(
211
+ () =>
212
+ analysisRows.reduce(
213
+ (acc, row) => {
214
+ acc.totalPurchaseValue += row.purchaseValue;
215
+ acc.totalSalesValue += row.salesValue;
216
+ acc.totalRealizedProfit += row.realizedProfit;
217
+ acc.totalRemainingValueAtCost += row.remainingValueAtCost;
218
+ return acc;
219
+ },
220
+ {
221
+ totalPurchaseValue: 0,
222
+ totalSalesValue: 0,
223
+ totalRealizedProfit: 0,
224
+ totalRemainingValueAtCost: 0,
225
+ }
226
+ ),
227
+ [analysisRows]
228
+ );
229
+
230
+ const lotsInProfit = useMemo(
231
+ () => analysisRows.filter((row) => row.realizedProfit > 0).length,
232
+ [analysisRows]
233
+ );
234
+ const lotsInLoss = useMemo(
235
+ () => analysisRows.filter((row) => row.realizedProfit < 0).length,
236
+ [analysisRows]
237
+ );
238
+
239
+ const selectedLotSummary = useMemo(
240
+ () =>
241
+ selectedLotId
242
+ ? analysisRows.find((row) => row.lotId === selectedLotId) || null
243
+ : null,
244
+ [analysisRows, selectedLotId]
245
+ );
246
+
247
+ const detailRows: LotDetailRow[] = useMemo(() => {
248
+ if (!selectedLotId) {
249
+ return [];
250
+ }
251
+
252
+ const avgPurchaseRate =
253
+ selectedLotSummary && selectedLotSummary.purchaseQty > 0
254
+ ? selectedLotSummary.purchaseValue / selectedLotSummary.purchaseQty
255
+ : 0;
256
+
257
+ const rows: LotDetailRow[] = [];
258
+
259
+ transactions.forEach((tx) => {
260
+ const lotItems = tx.items.filter((item) => item.lot_id === selectedLotId);
261
+ if (lotItems.length === 0) {
262
+ return;
263
+ }
264
+
265
+ const txTotalWeight =
266
+ tx.items.reduce((sum, item) => sum + item.net_weight, 0) || 1;
267
+
268
+ lotItems.forEach((item) => {
269
+ const weightShare = item.net_weight / txTotalWeight;
270
+ const allocatedExpenses = tx.total_expenses * weightShare;
271
+
272
+ let typeLabel = '';
273
+ let directionLabel = '';
274
+ let grossAmount = item.item_total;
275
+ let netAmount = grossAmount;
276
+ let profitImpact = 0;
277
+
278
+ if (tx.bill_type === BillType.AWAAK) {
279
+ if (tx.is_return) {
280
+ typeLabel = 'Purchase Return';
281
+ directionLabel = 'Export';
282
+ grossAmount = -item.item_total;
283
+ netAmount = grossAmount - allocatedExpenses;
284
+ } else {
285
+ typeLabel = 'Purchase';
286
+ directionLabel = 'Import';
287
+ netAmount = grossAmount + allocatedExpenses;
288
+ }
289
+ } else {
290
+ if (tx.is_return) {
291
+ typeLabel = 'Sales Return';
292
+ directionLabel = 'Import';
293
+ grossAmount = -item.item_total;
294
+ netAmount = grossAmount + allocatedExpenses;
295
+ } else {
296
+ typeLabel = 'Sale';
297
+ directionLabel = 'Export';
298
+ netAmount = grossAmount - allocatedExpenses;
299
+ }
300
+
301
+ const cost = item.net_weight * avgPurchaseRate;
302
+ profitImpact = netAmount - cost;
303
+ }
304
+
305
+ const displayDate = tx.bill_date
306
+ ? tx.bill_date.split('T')[0]
307
+ : tx.bill_date;
308
+
309
+ rows.push({
310
+ id: `${tx.id}-${item.id}`,
311
+ date: displayDate,
312
+ billNo: tx.bill_number,
313
+ typeLabel,
314
+ directionLabel,
315
+ qtyKg: item.net_weight,
316
+ ratePerKg: item.rate_per_kg,
317
+ grossAmount,
318
+ expensesAllocated: allocatedExpenses,
319
+ netAmount,
320
+ profitImpact,
321
+ });
322
+ });
323
+ });
324
+
325
+ rows.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
326
+ return rows;
327
+ }, [selectedLotId, selectedLotSummary, transactions]);
328
+
329
+ const handleExport = () => {
330
+ if (viewMode === 'detail' && selectedLotSummary) {
331
+ const lotSummaryForExport = {
332
+ lotNumber: selectedLotSummary.lotNumber,
333
+ mirchiName: selectedLotSummary.mirchiName,
334
+ purchaseQty: selectedLotSummary.purchaseQty,
335
+ purchaseValue: selectedLotSummary.purchaseValue,
336
+ avgPurchaseRate: selectedLotSummary.avgPurchaseRate,
337
+ soldQty: selectedLotSummary.soldQty,
338
+ salesValue: selectedLotSummary.salesValue,
339
+ avgSaleRate: selectedLotSummary.avgSaleRate,
340
+ realizedProfit: selectedLotSummary.realizedProfit,
341
+ marginPercent: selectedLotSummary.marginPercent,
342
+ remainingQty: selectedLotSummary.remainingQty,
343
+ remainingValueAtCost: selectedLotSummary.remainingValueAtCost,
344
+ };
345
+
346
+ const detailExportRows = detailRows.map((row) => ({
347
+ date: row.date,
348
+ billNo: row.billNo,
349
+ typeLabel: row.typeLabel,
350
+ directionLabel: row.directionLabel,
351
+ qtyKg: row.qtyKg,
352
+ ratePerKg: row.ratePerKg,
353
+ grossAmount: row.grossAmount,
354
+ expensesAllocated: row.expensesAllocated,
355
+ netAmount: row.netAmount,
356
+ profitImpact: row.profitImpact,
357
+ }));
358
+
359
+ exportLotDetailAnalysis(lotSummaryForExport, detailExportRows);
360
+ return;
361
+ }
362
+
363
+ if (analysisRows.length === 0) {
364
+ return;
365
+ }
366
+
367
+ const rowsToExport = analysisRows.map((row) => ({
368
+ lotNumber: row.lotNumber,
369
+ mirchiName: row.mirchiName,
370
+ purchaseQty: row.purchaseQty,
371
+ purchaseValue: row.purchaseValue,
372
+ avgPurchaseRate: row.avgPurchaseRate,
373
+ soldQty: row.soldQty,
374
+ salesValue: row.salesValue,
375
+ avgSaleRate: row.avgSaleRate,
376
+ realizedProfit: row.realizedProfit,
377
+ marginPercent: row.marginPercent,
378
+ remainingQty: row.remainingQty,
379
+ remainingValueAtCost: row.remainingValueAtCost,
380
+ }));
381
+
382
+ exportAnalysisReport(rowsToExport);
383
+ };
384
+
385
+ const profitColor =
386
+ totals.totalRealizedProfit > 0
387
+ ? 'text-green-600'
388
+ : totals.totalRealizedProfit < 0
389
+ ? 'text-red-600'
390
+ : 'text-gray-700';
391
+
392
+ return (
393
+ <div className="space-y-4">
394
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
395
+ <div className="sticky top-0 z-10 bg-white p-4 md:p-6 border-b flex flex-col md:flex-row md:items-center justify-between gap-4">
396
+ <div className="flex items-center gap-2">
397
+ {viewMode === 'detail' && (
398
+ <button
399
+ onClick={() => {
400
+ setViewMode('summary');
401
+ setSelectedLotId(null);
402
+ setSelectedLotNumber(null);
403
+ setSelectedLotMirchiName(null);
404
+ }}
405
+ className="mr-1 p-1 hover:bg-gray-100 rounded-full"
406
+ >
407
+ <ArrowLeft size={20} className="text-gray-600" />
408
+ </button>
409
+ )}
410
+ <TrendingUp className="text-teal-600" />
411
+ <h2 className="text-lg md:text-xl font-bold text-gray-800">
412
+ {viewMode === 'summary'
413
+ ? 'नफा-तोटा अ‍ॅनालिसीस (Profit / Loss Analysis)'
414
+ : `${selectedLotNumber ?? ''} - ${selectedLotMirchiName ?? ''} Analysis`}
415
+ </h2>
416
+ </div>
417
+ <div className="flex flex-col md:flex-row gap-3 w-full md:w-auto md:items-center justify-end">
418
+ <div className="relative w-full md:w-64">
419
+ <Search
420
+ className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
421
+ size={18}
422
+ />
423
+ <input
424
+ type="text"
425
+ placeholder="Search LOT / Mirchi Type..."
426
+ className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 outline-none w-full text-sm"
427
+ value={searchTerm}
428
+ onChange={(e) => setSearchTerm(e.target.value)}
429
+ />
430
+ </div>
431
+ <div className="flex gap-2 text-xs md:text-sm justify-start md:justify-end">
432
+ <button
433
+ type="button"
434
+ onClick={() => setLotFilter('all')}
435
+ className={`px-2 py-1 rounded-full border text-[11px] md:text-xs ${
436
+ lotFilter === 'all'
437
+ ? 'bg-teal-600 text-white border-teal-600'
438
+ : 'bg-white text-gray-600 border-gray-300'
439
+ }`}
440
+ >
441
+ All
442
+ </button>
443
+ <button
444
+ type="button"
445
+ onClick={() => setLotFilter('active')}
446
+ className={`px-2 py-1 rounded-full border text-[11px] md:text-xs ${
447
+ lotFilter === 'active'
448
+ ? 'bg-teal-600 text-white border-teal-600'
449
+ : 'bg-white text-gray-600 border-gray-300'
450
+ }`}
451
+ >
452
+ In Stock
453
+ </button>
454
+ <button
455
+ type="button"
456
+ onClick={() => setLotFilter('closed')}
457
+ className={`px-2 py-1 rounded-full border text-[11px] md:text-xs ${
458
+ lotFilter === 'closed'
459
+ ? 'bg-teal-600 text-white border-teal-600'
460
+ : 'bg-white text-gray-600 border-gray-300'
461
+ }`}
462
+ >
463
+ Stockout
464
+ </button>
465
+ </div>
466
+ <button
467
+ onClick={handleExport}
468
+ className="inline-flex items-center justify-center px-3 py-2 rounded-lg bg-teal-600 hover:bg-teal-700 text-white text-sm font-medium shadow-sm"
469
+ >
470
+ <Download size={16} className="mr-2" />
471
+ Export to Excel
472
+ </button>
473
+ </div>
474
+ </div>
475
+
476
+ {viewMode === 'detail' && selectedLotSummary && (
477
+ <div className="p-4 md:p-5 border-b bg-gray-50">
478
+ <div className="flex flex-col gap-3 text-xs md:text-sm">
479
+ <div className="flex items-center justify-between gap-3">
480
+ <div>
481
+ <div className="font-mono font-semibold text-teal-700">
482
+ {selectedLotSummary.lotNumber}
483
+ </div>
484
+ <div className="text-[11px] md:text-xs text-gray-500">
485
+ {selectedLotSummary.mirchiName}
486
+ </div>
487
+ </div>
488
+ <div className="text-right">
489
+ <div className="text-[11px] text-gray-500">Remaining</div>
490
+ <div className="font-semibold text-gray-800">
491
+ {selectedLotSummary.remainingQty.toFixed(2)} kg
492
+ </div>
493
+ </div>
494
+ </div>
495
+ <div className="grid grid-cols-2 gap-2">
496
+ <div className="bg-white rounded-lg border border-gray-100 p-2 flex flex-col">
497
+ <span className="text-[11px] text-gray-500">Purchase</span>
498
+ <span className="mt-1 text-xs md:text-sm font-medium text-gray-800">
499
+ {selectedLotSummary.purchaseQty.toFixed(2)} kg · ₹
500
+ {selectedLotSummary.purchaseValue.toFixed(0)}
501
+ </span>
502
+ </div>
503
+ <div className="bg-white rounded-lg border border-gray-100 p-2 flex flex-col">
504
+ <span className="text-[11px] text-gray-500">Sales</span>
505
+ <span className="mt-1 text-xs md:text-sm font-medium text-gray-800">
506
+ {selectedLotSummary.soldQty.toFixed(2)} kg · ₹
507
+ {selectedLotSummary.salesValue.toFixed(0)}
508
+ </span>
509
+ </div>
510
+ </div>
511
+ <div className="flex items-center justify-between gap-3">
512
+ <div className="text-[11px] md:text-xs text-gray-500">
513
+ Avg Buy ₹{selectedLotSummary.avgPurchaseRate.toFixed(0)} /kg
514
+ {selectedLotSummary.avgSaleRate > 0 && (
515
+ <>
516
+ {' '}
517
+ · Avg Sell ₹{selectedLotSummary.avgSaleRate.toFixed(0)} /kg
518
+ </>
519
+ )}
520
+ </div>
521
+ <div
522
+ className={`text-xs md:text-sm font-semibold ${
523
+ selectedLotSummary.realizedProfit > 0
524
+ ? 'text-green-600'
525
+ : selectedLotSummary.realizedProfit < 0
526
+ ? 'text-red-600'
527
+ : 'text-gray-700'
528
+ }`}
529
+ >
530
+ {selectedLotSummary.realizedProfit > 0
531
+ ? 'Profit'
532
+ : selectedLotSummary.realizedProfit < 0
533
+ ? 'Loss'
534
+ : 'Break-even'}{' '}
535
+ ₹{selectedLotSummary.realizedProfit.toFixed(0)}
536
+ </div>
537
+ </div>
538
+ </div>
539
+ </div>
540
+ )}
541
+
542
+ {viewMode === 'summary' ? (
543
+ <>
544
+ <div className="hidden md:block overflow-x-auto">
545
+ <table className="w-full text-sm text-left">
546
+ <thead className="bg-gray-50 text-gray-500 font-medium">
547
+ <tr>
548
+ <th className="px-6 py-4">LOT</th>
549
+ <th className="px-6 py-4">Mirchi Type</th>
550
+ <th className="px-6 py-4 text-right">Purchase Qty / Value</th>
551
+ <th className="px-6 py-4 text-right">Sales Qty / Value</th>
552
+ <th className="px-6 py-4 text-right">Remaining Qty</th>
553
+ <th className="px-6 py-4 text-right">Profit / Loss</th>
554
+ </tr>
555
+ </thead>
556
+ <tbody className="divide-y divide-gray-100">
557
+ {analysisRows.length === 0 ? (
558
+ <tr>
559
+ <td
560
+ colSpan={6}
561
+ className="px-6 py-8 text-center text-gray-500"
562
+ >
563
+ No analysis data available
564
+ </td>
565
+ </tr>
566
+ ) : (
567
+ analysisRows.map((row) => {
568
+ const profitClass =
569
+ row.realizedProfit > 0
570
+ ? 'text-green-600'
571
+ : row.realizedProfit < 0
572
+ ? 'text-red-600'
573
+ : 'text-gray-700';
574
+
575
+ return (
576
+ <tr
577
+ key={row.lotId}
578
+ className="hover:bg-gray-50 cursor-pointer"
579
+ onClick={() => {
580
+ setSelectedLotId(row.lotId);
581
+ setSelectedLotNumber(row.lotNumber);
582
+ setSelectedLotMirchiName(row.mirchiName);
583
+ setViewMode('detail');
584
+ }}
585
+ >
586
+ <td className="px-6 py-4 font-mono text-sm font-medium text-teal-700">
587
+ {row.lotNumber}
588
+ </td>
589
+ <td className="px-6 py-4">{row.mirchiName}</td>
590
+ <td className="px-6 py-4 text-right">
591
+ <div className="flex flex-col items-end">
592
+ <span className="text-gray-800 font-semibold">
593
+ {row.purchaseQty.toFixed(2)} kg
594
+ </span>
595
+ <span className="text-xs text-gray-500">
596
+ ₹{row.purchaseValue.toFixed(0)}
597
+ </span>
598
+ </div>
599
+ </td>
600
+ <td className="px-6 py-4 text-right">
601
+ <div className="flex flex-col items-end">
602
+ <span className="text-gray-800 font-semibold">
603
+ {row.soldQty.toFixed(2)} kg
604
+ </span>
605
+ <span className="text-xs text-gray-500">
606
+ ₹{row.salesValue.toFixed(0)}
607
+ </span>
608
+ </div>
609
+ </td>
610
+ <td className="px-6 py-4 text-right">
611
+ <span className="font-semibold text-gray-800">
612
+ {row.remainingQty.toFixed(2)} kg
613
+ </span>
614
+ </td>
615
+ <td className="px-6 py-4 text-right">
616
+ <span className={`font-semibold ${profitClass}`}>
617
+ {row.realizedProfit > 0
618
+ ? 'Profit'
619
+ : row.realizedProfit < 0
620
+ ? 'Loss'
621
+ : 'Break-even'}{' '}
622
+ ₹{row.realizedProfit.toFixed(0)}
623
+ </span>
624
+ </td>
625
+ </tr>
626
+ );
627
+ })
628
+ )}
629
+ </tbody>
630
+ </table>
631
+ </div>
632
+
633
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
634
+ {analysisRows.length === 0 ? (
635
+ <div className="p-6 text-center text-gray-500 text-sm">
636
+ No analysis data available
637
+ </div>
638
+ ) : (
639
+ analysisRows.map((row) => {
640
+ const profitClass =
641
+ row.realizedProfit > 0
642
+ ? 'text-green-600'
643
+ : row.realizedProfit < 0
644
+ ? 'text-red-600'
645
+ : 'text-gray-700';
646
+
647
+ return (
648
+ <button
649
+ key={row.lotId}
650
+ type="button"
651
+ className="text-left p-4 space-y-2 hover:bg-gray-50"
652
+ onClick={() => {
653
+ setSelectedLotId(row.lotId);
654
+ setSelectedLotNumber(row.lotNumber);
655
+ setSelectedLotMirchiName(row.mirchiName);
656
+ setViewMode('detail');
657
+ }}
658
+ >
659
+ <div className="flex items-center justify-between gap-2">
660
+ <div>
661
+ <div className="text-sm font-mono font-semibold text-teal-700">
662
+ {row.lotNumber}
663
+ </div>
664
+ <div className="text-xs text-gray-500">
665
+ {row.mirchiName}
666
+ </div>
667
+ <div className="text-[11px] text-gray-500 mt-0.5">
668
+ Remaining {row.remainingQty.toFixed(1)} kg
669
+ </div>
670
+ </div>
671
+ <div className="text-right">
672
+ <div className={`text-sm font-semibold ${profitClass}`}>
673
+ {row.realizedProfit > 0
674
+ ? 'Profit'
675
+ : row.realizedProfit < 0
676
+ ? 'Loss'
677
+ : 'Break-even'}{' '}
678
+ ₹{row.realizedProfit.toFixed(0)}
679
+ </div>
680
+ <div className="text-[11px] text-gray-500">
681
+ Margin {row.marginPercent.toFixed(1)}%
682
+ </div>
683
+ </div>
684
+ </div>
685
+ <div className="grid grid-cols-2 gap-2 text-xs pt-2 border-t border-gray-50">
686
+ <div className="bg-gray-50 p-2 rounded">
687
+ <div className="text-gray-500">Purchase</div>
688
+ <div className="font-medium">
689
+ {row.purchaseQty.toFixed(1)} kg · ₹
690
+ {row.purchaseValue.toFixed(0)}
691
+ </div>
692
+ </div>
693
+ <div className="bg-gray-50 p-2 rounded">
694
+ <div className="text-gray-500">Sales</div>
695
+ <div className="font-medium">
696
+ {row.soldQty.toFixed(1)} kg · ₹
697
+ {row.salesValue.toFixed(0)}
698
+ </div>
699
+ </div>
700
+ </div>
701
+ </button>
702
+ );
703
+ })
704
+ )}
705
+ </div>
706
+ </>
707
+ ) : (
708
+ <>
709
+ <div className="hidden md:block overflow-x-auto">
710
+ <table className="w-full text-sm text-left">
711
+ <thead className="bg-gray-50 text-gray-500 font-medium">
712
+ <tr>
713
+ <th className="px-6 py-4">Date</th>
714
+ <th className="px-6 py-4">Bill No</th>
715
+ <th className="px-6 py-4">Type</th>
716
+ <th className="px-6 py-4">Import / Export</th>
717
+ <th className="px-6 py-4 text-right">Qty (kg)</th>
718
+ <th className="px-6 py-4 text-right">Rate (₹/kg)</th>
719
+ <th className="px-6 py-4 text-right">Net Amount (₹)</th>
720
+ <th className="px-6 py-4 text-right">Profit Impact (₹)</th>
721
+ </tr>
722
+ </thead>
723
+ <tbody className="divide-y divide-gray-100">
724
+ {detailRows.length === 0 ? (
725
+ <tr>
726
+ <td
727
+ colSpan={8}
728
+ className="px-6 py-8 text-center text-gray-500"
729
+ >
730
+ No transactions found for this LOT
731
+ </td>
732
+ </tr>
733
+ ) : (
734
+ detailRows.map((row) => {
735
+ const profitClass =
736
+ row.profitImpact > 0
737
+ ? 'text-green-600'
738
+ : row.profitImpact < 0
739
+ ? 'text-red-600'
740
+ : 'text-gray-700';
741
+
742
+ return (
743
+ <tr key={row.id} className="hover:bg-gray-50">
744
+ <td className="px-6 py-4 text-gray-600">{row.date}</td>
745
+ <td className="px-6 py-4 font-mono text-xs text-gray-600">
746
+ {row.billNo}
747
+ </td>
748
+ <td className="px-6 py-4 text-gray-700">{row.typeLabel}</td>
749
+ <td className="px-6 py-4 text-gray-700">
750
+ {row.directionLabel}
751
+ </td>
752
+ <td className="px-6 py-4 text-right text-gray-800">
753
+ {row.qtyKg.toFixed(2)}
754
+ </td>
755
+ <td className="px-6 py-4 text-right text-gray-800">
756
+ ₹{row.ratePerKg.toFixed(2)}
757
+ </td>
758
+ <td className="px-6 py-4 text-right text-gray-800">
759
+ ₹{row.netAmount.toFixed(0)}
760
+ </td>
761
+ <td className="px-6 py-4 text-right">
762
+ <span className={`font-semibold ${profitClass}`}>
763
+ ₹{row.profitImpact.toFixed(0)}
764
+ </span>
765
+ </td>
766
+ </tr>
767
+ );
768
+ })
769
+ )}
770
+ </tbody>
771
+ </table>
772
+ </div>
773
+
774
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
775
+ {detailRows.length === 0 ? (
776
+ <div className="p-6 text-center text-gray-500 text-sm">
777
+ No transactions found for this LOT
778
+ </div>
779
+ ) : (
780
+ detailRows.map((row) => {
781
+ const profitClass =
782
+ row.profitImpact > 0
783
+ ? 'text-green-600'
784
+ : row.profitImpact < 0
785
+ ? 'text-red-600'
786
+ : 'text-gray-700';
787
+
788
+ return (
789
+ <div key={row.id} className="p-4 space-y-2">
790
+ <div className="flex items-center justify-between gap-2">
791
+ <div>
792
+ <div className="text-xs text-gray-500">{row.date}</div>
793
+ <div className="text-sm font-mono font-medium text-gray-800">
794
+ {row.billNo}
795
+ </div>
796
+ <div className="text-[11px] text-gray-500">
797
+ {row.typeLabel} · {row.directionLabel}
798
+ </div>
799
+ </div>
800
+ <div className="text-right">
801
+ <div className="text-xs text-gray-500">Net Amount</div>
802
+ <div className="text-sm font-semibold text-gray-800">
803
+ ₹{row.netAmount.toFixed(0)}
804
+ </div>
805
+ <div className={`text-[11px] font-semibold ${profitClass}`}>
806
+ Profit ₹{row.profitImpact.toFixed(0)}
807
+ </div>
808
+ </div>
809
+ </div>
810
+ <div className="grid grid-cols-2 gap-2 text-xs pt-2 border-t border-gray-50">
811
+ <div className="bg-gray-50 p-2 rounded">
812
+ <div className="text-gray-500">Qty (kg)</div>
813
+ <div className="font-medium">
814
+ {row.qtyKg.toFixed(2)} kg
815
+ </div>
816
+ </div>
817
+ <div className="bg-gray-50 p-2 rounded">
818
+ <div className="text-gray-500">Rate / kg</div>
819
+ <div className="font-medium">
820
+ ₹{row.ratePerKg.toFixed(2)}
821
+ </div>
822
+ </div>
823
+ </div>
824
+ </div>
825
+ );
826
+ })
827
+ )}
828
+ </div>
829
+ </>
830
+ )}
831
+ </div>
832
+ </div>
833
+ );
834
+ };
835
+
836
+ export default Analysis;
pages/AwaakBill.tsx ADDED
@@ -0,0 +1,897 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
303
+ const hasWeights = item.poti_weights && item.poti_weights.length > 0;
304
+ const hasCountAndNet = (item.poti_count || 0) > 0 && (item.net_weight || 0) > 0;
305
+
306
+ if (!hasWeights && !hasCountAndNet) {
307
+ alert(`Row ${i + 1}: Please either enter poti weights (e.g. 10, 20) or fill both Poti count and Net weight`);
308
+ return false;
309
+ }
310
+
311
+ if (!item.rate_per_kg || item.rate_per_kg <= 0) {
312
+ alert(`Row ${i + 1}: Rate must be greater than 0`);
313
+ return false;
314
+ }
315
+ }
316
+ if (isOverpaid) {
317
+ alert('Paid amount cannot be greater than Total Amount');
318
+ return false;
319
+ }
320
+ return true;
321
+ };
322
+
323
+ const handleSubmit = async () => {
324
+ if (isSubmitting) return;
325
+ if (!validateForm()) return;
326
+
327
+ setIsSubmitting(true);
328
+
329
+ // Populate mirchi_name for each item from mirchiTypes
330
+ const itemsWithNames = items.map(item => ({
331
+ ...item,
332
+ mirchi_name: mirchiTypes.find(m => m.id === item.mirchi_type_id)?.name || 'Unknown'
333
+ }));
334
+
335
+ const transaction: Transaction = {
336
+ id: Date.now().toString(),
337
+ bill_number: billNumber,
338
+ bill_date: billDate,
339
+ bill_type: BillType.AWAAK,
340
+ is_return: isReturnMode,
341
+ party_id: selectedParty,
342
+ party_name: parties.find(p => p.id === selectedParty)?.name,
343
+ items: itemsWithNames as TransactionItem[],
344
+ expenses: {
345
+ ...expenses,
346
+ cess_amount: cessAmt,
347
+ adat_amount: adatAmt,
348
+ poti_amount: potiAmt,
349
+ hamali_amount: hamaliAmt
350
+ },
351
+ payments: [
352
+ ...(finalCash > 0 ? [{ mode: PaymentMode.CASH, amount: finalCash }] : []),
353
+ ...(finalOnline > 0 ? [{ mode: PaymentMode.ONLINE, amount: finalOnline }] : []),
354
+ ...(balance > 0 ? [{ mode: PaymentMode.DUE, amount: balance }] : []) // Optional: Track due as a payment entry or just balance? Usually balance is enough. Keeping logic simple.
355
+ ],
356
+ gross_weight_total: items.reduce((a, i) => a + (i.gross_weight || 0), 0),
357
+ net_weight_total: items.reduce((a, i) => a + (i.net_weight || 0), 0),
358
+ subtotal: subtotal,
359
+ total_expenses: totalExp,
360
+ total_amount: grandTotal,
361
+ paid_amount: currentPaid,
362
+ balance_amount: balance
363
+ };
364
+
365
+ const result = await saveTransaction(transaction);
366
+ if (result.success) {
367
+ // Use the complete transaction object we created, not just the API response
368
+ // This ensures all items are included for printing
369
+ const completeTransaction = result.data ? {
370
+ ...transaction,
371
+ ...result.data,
372
+ items: transaction.items, // Ensure items are preserved
373
+ expenses: {
374
+ ...result.data.expenses,
375
+ other_expenses: transaction.expenses.other_expenses,
376
+ },
377
+ } : transaction;
378
+ setSavedTransaction(completeTransaction);
379
+ alert('Bill Saved Successfully! You can now print the invoice.');
380
+ } else {
381
+ alert(`Error: ${result.message}`);
382
+ }
383
+ setIsSubmitting(false);
384
+ };
385
+
386
+ const handleHybridCashChange = (val: number) => {
387
+ setCashAmount(val);
388
+ // Online amount is derived in render, no state update needed for it in hybrid
389
+ };
390
+
391
+ const SummaryContent = () => (
392
+ <div className="space-y-3 text-sm">
393
+ <div className="flex justify-between">
394
+ <span className="text-gray-600">एकुण रक्कम (Subtotal)</span>
395
+ <span className="font-semibold">₹{subtotal.toFixed(2)}</span>
396
+ </div>
397
+
398
+ <div className="pt-2 border-t border-dashed space-y-2">
399
+ {/* 1. Poti (Bags) Rate */}
400
+ <SummaryInput
401
+ label={`पोती (Bags ${totalPoti}) Rate`}
402
+ value={expenses.poti_rate}
403
+ onChange={val => setExpenses({ ...expenses, poti_rate: val })}
404
+ />
405
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
406
+ <span>Amount:</span>
407
+ <span>₹{potiAmt.toFixed(2)}</span>
408
+ </div>
409
+
410
+ {/* 2. Cess Tax (calculated on subtotal + poti) */}
411
+ <SummaryInput
412
+ label={`सेस (Cess ${expenses.cess_percent}%) on ₹${baseForCess.toFixed(2)}`}
413
+ value={expenses.cess_percent}
414
+ onChange={val => setExpenses({ ...expenses, cess_percent: val })}
415
+ />
416
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
417
+ <span>Amount:</span>
418
+ <span>₹{cessAmt.toFixed(2)}</span>
419
+ </div>
420
+
421
+ {/* 3. Adat / Market Yard Tax */}
422
+ <SummaryInput
423
+ label={`अडत (Adat ${expenses.adat_percent}%)`}
424
+ value={expenses.adat_percent}
425
+ onChange={val => setExpenses({ ...expenses, adat_percent: val })}
426
+ />
427
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
428
+ <span>Amount:</span>
429
+ <span>₹{adatAmt.toFixed(2)}</span>
430
+ </div>
431
+
432
+ {/* 4. Hamali */}
433
+ <SummaryInput
434
+ label={`हमाली (Hamali per poti)`}
435
+ value={expenses.hamali_per_poti}
436
+ onChange={val => setExpenses({ ...expenses, hamali_per_poti: val })}
437
+ />
438
+ <div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
439
+ <span>Amount ({expenses.hamali_per_poti} * {totalPoti}):</span>
440
+ <span>₹{hamaliAmt.toFixed(2)}</span>
441
+ </div>
442
+
443
+ {/* 5. Gaadi Bharni */}
444
+ <SummaryInput
445
+ label="गाडी भरणी"
446
+ value={expenses.gaadi_bharni}
447
+ onChange={val => setExpenses({ ...expenses, gaadi_bharni: val })}
448
+ />
449
+
450
+ {/* 6. Other Expenses */}
451
+ <SummaryInput
452
+ label="Other Expenses"
453
+ value={expenses.other_expenses}
454
+ onChange={val => setExpenses({ ...expenses, other_expenses: val })}
455
+ />
456
+ </div>
457
+
458
+ {/* 7. Total Price */}
459
+ <div className="pt-2 border-t border-gray-200 flex justify-between items-center">
460
+ <span className="text-base font-bold text-gray-800">एकुण बिल (Total)</span>
461
+ <span className="text-xl font-bold text-red-600">₹{grandTotal.toFixed(2)}</span>
462
+ </div>
463
+
464
+ {/* Payment Section */}
465
+ <div className="bg-teal-50 p-3 rounded-lg mt-2 border border-teal-100">
466
+ <label className="block text-xs font-medium text-teal-800 mb-2">Payment Mode</label>
467
+ <div className="flex gap-2 mb-3">
468
+ {['cash', 'online', 'hybrid', 'due'].map(mode => (
469
+ <button
470
+ key={mode}
471
+ onClick={() => {
472
+ setPaymentMode(mode as any);
473
+ setCashAmount(0);
474
+ setOnlineAmount(0);
475
+ }}
476
+ className={`flex-1 py-1 text-xs font-medium rounded border ${paymentMode === mode
477
+ ? 'bg-teal-600 text-white border-teal-600'
478
+ : 'bg-white text-gray-600 border-gray-200'
479
+ } capitalize`}
480
+ >
481
+ {mode}
482
+ </button>
483
+ ))}
484
+ </div>
485
+
486
+ {paymentMode === 'cash' && (
487
+ <div className="text-center py-2 text-sm text-gray-600">
488
+ Full Payment in Cash: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
489
+ </div>
490
+ )}
491
+
492
+ {paymentMode === 'online' && (
493
+ <div className="text-center py-2 text-sm text-gray-600">
494
+ Full Payment Online: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
495
+ </div>
496
+ )}
497
+
498
+ {paymentMode === 'hybrid' && (
499
+ <div className="space-y-2">
500
+ <div>
501
+ <label className="text-xs text-gray-600">Cash Amount</label>
502
+ <input
503
+ type="number"
504
+ min="0"
505
+ max={grandTotal}
506
+ step="0.01"
507
+ className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
508
+ value={cashAmount === 0 ? '' : cashAmount}
509
+ onChange={e => {
510
+ const val = parseFloat(e.target.value) || 0;
511
+ if (val >= 0 && val <= grandTotal) {
512
+ handleHybridCashChange(val);
513
+ }
514
+ }}
515
+ onKeyDown={e => {
516
+ // Prevent special characters: -, +, e, E
517
+ if (['-', '+', 'e', 'E'].includes(e.key)) {
518
+ e.preventDefault();
519
+ }
520
+ }}
521
+ placeholder="Cash"
522
+ />
523
+ </div>
524
+ <div>
525
+ <label className="text-xs text-gray-600">Online Amount (Auto)</label>
526
+ <input
527
+ type="text"
528
+ readOnly
529
+ className="w-full bg-gray-100 border border-gray-200 rounded p-2 text-right font-medium text-gray-600"
530
+ value={(grandTotal - cashAmount).toFixed(2)}
531
+ />
532
+ </div>
533
+ </div>
534
+ )}
535
+
536
+ {paymentMode === 'due' && (
537
+ <div className="space-y-2">
538
+ <div>
539
+ <label className="text-xs text-gray-600">Online Amount (Partial Payment)</label>
540
+ <input
541
+ type="number"
542
+ min="0"
543
+ max={grandTotal}
544
+ step="0.01"
545
+ className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
546
+ value={onlineAmount === 0 ? '' : onlineAmount}
547
+ onChange={e => {
548
+ const val = parseFloat(e.target.value) || 0;
549
+ if (val >= 0 && val <= grandTotal) {
550
+ setOnlineAmount(val);
551
+ }
552
+ }}
553
+ onKeyDown={e => {
554
+ // Prevent special characters: -, +, e, E
555
+ if (['-', '+', 'e', 'E'].includes(e.key)) {
556
+ e.preventDefault();
557
+ }
558
+ }}
559
+ placeholder="Enter amount paid online (0 for full due)"
560
+ />
561
+ </div>
562
+ <div>
563
+ <label className="text-xs text-gray-600">Due Amount (Auto)</label>
564
+ <input
565
+ type="text"
566
+ readOnly
567
+ className="w-full bg-red-50 border border-red-200 rounded p-2 text-right font-bold text-red-600"
568
+ value={(grandTotal - onlineAmount).toFixed(2)}
569
+ />
570
+ </div>
571
+ </div>
572
+ )}
573
+
574
+ {isOverpaid && (
575
+ <div className="text-red-500 text-xs mt-2 font-medium text-center">
576
+ Error: Amount exceeds Total!
577
+ </div>
578
+ )}
579
+ </div>
580
+
581
+ <div className="flex justify-between items-center pt-2">
582
+ <span className="text-gray-600 font-medium">बाकी (Balance)</span>
583
+ <span className={`font-bold ${balance > 0 ? 'text-red-500' : 'text-green-600'}`}>₹{balance.toFixed(2)}</span>
584
+ </div>
585
+ </div>
586
+ );
587
+
588
+ return (
589
+ <div className="flex flex-col lg:flex-row gap-4 h-full p-4 bg-gray-50">
590
+ {/* Loading State */}
591
+ {isLoading && (
592
+ <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-gray-100">
593
+ <div className="text-center">
594
+ <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mb-4"></div>
595
+ <p className="text-gray-600">Loading data...</p>
596
+ </div>
597
+ </div>
598
+ )}
599
+
600
+ {/* Error State */}
601
+ {error && !isLoading && (
602
+ <div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-red-200">
603
+ <div className="text-center p-8">
604
+ <div className="text-red-500 text-5xl mb-4">⚠️</div>
605
+ <h3 className="text-lg font-semibold text-gray-800 mb-2">Error Loading Data</h3>
606
+ <p className="text-gray-600 mb-4">{error}</p>
607
+ <button
608
+ onClick={() => window.location.reload()}
609
+ className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"
610
+ >
611
+ Retry
612
+ </button>
613
+ </div>
614
+ </div>
615
+ )}
616
+
617
+ {/* Main Content - Only show when not loading and no error */}
618
+ {!isLoading && !error && (
619
+ <>
620
+ {/* Left: Form */}
621
+ <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'}`}>
622
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
623
+ <h2 className={`text-lg font-bold flex items-center gap-2 ${isReturnMode ? 'text-red-600' : 'text-gray-800'}`}>
624
+ {isReturnMode ? <RotateCcw className="text-red-500" /> : <div className="text-teal-600 font-bold">IN</div>}
625
+ {isReturnMode ? 'आवक परतावा (Purchase Return)' : 'आवक बिल (Purchase)'}
626
+ </h2>
627
+
628
+ <div className="flex items-center gap-3">
629
+ <div className="flex bg-gray-100 rounded-lg p-1">
630
+ <button
631
+ onClick={() => setIsReturnMode(false)}
632
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${!isReturnMode ? 'bg-white text-teal-700 shadow-sm' : 'text-gray-500'}`}
633
+ >
634
+ Regular
635
+ </button>
636
+ <button
637
+ onClick={() => setIsReturnMode(true)}
638
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${isReturnMode ? 'bg-red-500 text-white shadow-sm' : 'text-gray-500'}`}
639
+ >
640
+ Return
641
+ </button>
642
+ </div>
643
+
644
+ <input
645
+ type="date"
646
+ 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"
647
+ value={billDate}
648
+ onChange={(e) => setBillDate(e.target.value)}
649
+ max={new Date().toISOString().split('T')[0]}
650
+ />
651
+ </div>
652
+ </div>
653
+
654
+ {/* Header Fields */}
655
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
656
+
657
+ <div>
658
+ <label className="block text-sm font-medium text-gray-700 mb-1">पार्टी नाव (Party)</label>
659
+ <select
660
+ className="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-teal-500 outline-none"
661
+ value={selectedParty}
662
+ onChange={e => setSelectedParty(e.target.value)}
663
+ >
664
+ <option value="">Select Party</option>
665
+ {parties.map(p => (
666
+ <option key={p.id} value={p.id}>{p.name} - {p.city}</option>
667
+ ))}
668
+ </select>
669
+ </div>
670
+ </div>
671
+
672
+ {/* Items Table */}
673
+ <div className="mb-6">
674
+ <div className="flex justify-between items-center mb-2">
675
+ <h3 className="font-semibold text-gray-700">माल तपशील (Items)</h3>
676
+ </div>
677
+
678
+ <div className="space-y-4">
679
+ {items.map((item, index) => (
680
+ <div key={item.id} className="p-4 border rounded-lg bg-gray-50 relative shadow-sm">
681
+ <button
682
+ onClick={() => removeItem(item.id!)}
683
+ className="absolute top-2 right-2 text-red-400 hover:text-red-600 p-1"
684
+ >
685
+ <Trash2 size={18} />
686
+ </button>
687
+
688
+ <div className="grid grid-cols-2 md:grid-cols-6 gap-3">
689
+ <div className="col-span-2">
690
+ <label className="block text-xs font-medium text-gray-500 mb-1">मिरची जात (Type)</label>
691
+ <select
692
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
693
+ value={item.mirchi_type_id || ''}
694
+ onChange={e => handleMirchiChange(item.id!, e.target.value)}
695
+ >
696
+ <option value="">Select Type</option>
697
+ {mirchiTypes.map(m => (
698
+ <option key={m.id} value={m.id}>{m.name}</option>
699
+ ))}
700
+ </select>
701
+ </div>
702
+
703
+ {/* LOT Number Section - Dynamic based on mode */}
704
+ {item.mirchi_type_id && (
705
+ <div className="col-span-2">
706
+ <label className="block text-xs font-medium text-gray-500 mb-1">
707
+ LOT Number {isReturnMode && <span className="text-red-500">(Select to Return)</span>}
708
+ </label>
709
+ {isReturnMode ? (
710
+ // RETURN MODE: Select existing lot
711
+ <select
712
+ className="w-full border border-gray-300 rounded p-2 text-sm font-mono bg-white"
713
+ value={item.lot_id || ''}
714
+ onChange={e => {
715
+ const selectedLot = availableLots[item.id!]?.find(l => l.id === e.target.value);
716
+ if (selectedLot) {
717
+ handleItemChange(item.id!, 'lot_id', selectedLot.id);
718
+ handleItemChange(item.id!, 'lot_number', selectedLot.lot_number);
719
+ }
720
+ }}
721
+ >
722
+ <option value="">Select LOT to return</option>
723
+ {(availableLots[item.id!] || []).map(lot => (
724
+ <option key={lot.id} value={lot.id}>
725
+ {lot.lot_number} ({lot.remaining_quantity}kg available)
726
+ </option>
727
+ ))}
728
+ </select>
729
+ ) : (
730
+ // NORMAL MODE: New lot input
731
+ <input
732
+ type="text"
733
+ className="w-full border border-gray-300 rounded p-2 text-sm font-mono"
734
+ placeholder="Enter new LOT number"
735
+ value={lotInputs[item.id!] || ''}
736
+ onChange={e => {
737
+ setLotInputs({ ...lotInputs, [item.id!]: e.target.value });
738
+ handleItemChange(item.id!, 'lot_number', e.target.value);
739
+ handleItemChange(item.id!, 'is_existing_lot', false);
740
+ }}
741
+ />
742
+ )}
743
+ </div>
744
+ )}
745
+ <div className="col-span-2 md:col-span-4">
746
+ <label className="block text-xs font-medium text-gray-500 mb-1">पोटी वजन (Eg: 10,20,30)</label>
747
+ <input
748
+ type="text"
749
+ placeholder="10, 20, 30"
750
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
751
+ value={potiInputs[item.id!] || ''}
752
+ onChange={e => handlePotiInputChange(item.id!, e.target.value)}
753
+ />
754
+ </div>
755
+
756
+ <div>
757
+ <label className="block text-xs font-medium text-gray-500 mb-1">Gross</label>
758
+ <input
759
+ type="number"
760
+ readOnly
761
+ className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm"
762
+ value={item.gross_weight}
763
+ />
764
+ </div>
765
+ <div>
766
+ <label className="block text-xs font-medium text-gray-500 mb-1">Poti (count)</label>
767
+ <input
768
+ type="number"
769
+ min="0"
770
+ className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm text-red-500 appearance-none"
771
+ value={item.poti_count === 0 ? '' : item.poti_count}
772
+ onChange={e => {
773
+ const count = Math.max(0, parseFloat(e.target.value) || 0);
774
+ const gross = item.gross_weight || 0;
775
+ const potya = count; // 1kg deduction per bag
776
+
777
+ // Keep poti_count in sync for totals (hamali, etc.)
778
+ handleItemChange(item.id!, 'poti_count', count);
779
+ handleItemChange(item.id!, 'total_potya', potya);
780
+
781
+ // If gross is known (from detailed weights), adjust net accordingly
782
+ if (gross > 0) {
783
+ const net = Math.max(0, gross - potya);
784
+ handleItemChange(item.id!, 'net_weight', net);
785
+ }
786
+ }}
787
+ placeholder="0"
788
+ />
789
+ </div>
790
+ <div>
791
+ <label className="block text-xs font-medium text-gray-500 mb-1">Net (Editable)</label>
792
+ <input
793
+ type="number"
794
+ min="0"
795
+ className="w-full bg-blue-50 border border-blue-200 rounded p-2 text-sm font-bold text-blue-800 appearance-none"
796
+ value={item.net_weight === 0 ? '' : item.net_weight}
797
+ onChange={e => handleItemChange(item.id!, 'net_weight', Math.max(0, parseFloat(e.target.value) || 0))}
798
+ placeholder="Auto-calculated"
799
+ />
800
+ </div>
801
+
802
+ <div>
803
+ <label className="block text-xs font-medium text-gray-500 mb-1">Rate (₹)</label>
804
+ <input
805
+ type="number"
806
+ min="0"
807
+ className="w-full border border-gray-300 rounded p-2 text-sm bg-white appearance-none"
808
+ onWheel={e => e.currentTarget.blur()}
809
+ value={item.rate_per_kg === 0 ? '' : item.rate_per_kg}
810
+ onChange={e => handleItemChange(item.id!, 'rate_per_kg', Math.max(0, parseFloat(e.target.value) || 0))}
811
+ placeholder="0"
812
+ />
813
+ </div>
814
+ <div className="col-span-2 md:col-span-2">
815
+ <label className="block text-xs font-medium text-gray-500 mb-1">Total (₹)</label>
816
+ <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)} />
817
+ </div>
818
+ </div>
819
+ </div>
820
+ ))}
821
+ </div>
822
+ <button
823
+ onClick={addItem}
824
+ 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"
825
+ >
826
+ <Plus size={18} /> Add New Item (नवीन माल)
827
+ </button>
828
+ </div>
829
+ </div>
830
+
831
+ {/* Desktop: Right Summary Sidebar */}
832
+ <div className="hidden lg:flex w-80 flex-col gap-4">
833
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-0">
834
+ <h3 className="font-bold text-gray-800 mb-4 border-b pb-2">बिल सारांश (Summary)</h3>
835
+ {SummaryContent()}
836
+ <div className="flex gap-2 mt-6">
837
+ {savedTransaction && (
838
+ <>
839
+ <PrintInvoice transaction={savedTransaction} />
840
+ <PdfInvoice transaction={savedTransaction} />
841
+ </>
842
+ )}
843
+ <button
844
+ onClick={handleSubmit}
845
+ disabled={isSubmitting || items.length === 0}
846
+ 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' : ''}`}
847
+ >
848
+ <Save size={20} />
849
+ {isSubmitting ? 'Saving...' : 'Save Bill'}
850
+ </button>
851
+ </div>
852
+ </div>
853
+ </div>
854
+
855
+ {/* Mobile: Sticky Bottom Action Bar */}
856
+ <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">
857
+ <div className="flex items-center justify-between p-4 bg-white z-20 relative">
858
+ <div
859
+ onClick={() => setShowMobileSummary(!showMobileSummary)}
860
+ className="flex flex-col cursor-pointer"
861
+ >
862
+ <div className="flex items-center gap-1 text-gray-500 text-xs font-medium uppercase tracking-wide">
863
+ Summary {showMobileSummary ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
864
+ </div>
865
+ <div className="text-xl font-bold text-red-600">
866
+ ₹{grandTotal.toFixed(2)}
867
+ </div>
868
+ </div>
869
+ <button
870
+ onClick={handleSubmit}
871
+ disabled={isSubmitting}
872
+ 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"
873
+ >
874
+ <Save size={18} /> {isSubmitting ? '...' : 'Save'}
875
+ </button>
876
+ </div>
877
+
878
+ {showMobileSummary && (
879
+ <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">
880
+ <div className="mt-4">
881
+ {SummaryContent()}
882
+ </div>
883
+ {savedTransaction && (
884
+ <div className="mt-4 flex justify-center">
885
+ <PrintInvoice transaction={savedTransaction} />
886
+ </div>
887
+ )}
888
+ </div>
889
+ )}
890
+ </div>
891
+ </>
892
+ )}
893
+ </div>
894
+ );
895
+ };
896
+
897
+ export default AwaakBill;
pages/Dashboard.tsx ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from 'react';
2
+ import { getTransactions, getActiveLots, getParties } from '../services/db';
3
+ import { Transaction, Lot, Party, BillType } from '../types';
4
+
5
+ import {
6
+ TrendingUp,
7
+ Package,
8
+ AlertCircle,
9
+ Bell,
10
+ IndianRupee,
11
+ ArrowUpRight,
12
+ ArrowDownLeft
13
+ } from 'lucide-react';
14
+ import {
15
+ BarChart,
16
+ Bar,
17
+ XAxis,
18
+ Tooltip,
19
+ ResponsiveContainer
20
+ } from 'recharts';
21
+ import { Link } from 'react-router-dom';
22
+
23
+ const Dashboard = () => {
24
+ const [recentTx, setRecentTx] = useState<Transaction[]>([]);
25
+ const [totalStock, setTotalStock] = useState(0);
26
+ const [stockValue, setStockValue] = useState(0);
27
+ const [activeLots, setActiveLots] = useState<Lot[]>([]);
28
+ const [dueParties, setDueParties] = useState<Party[]>([]);
29
+ const [notifications, setNotifications] = useState<string[]>([]);
30
+ const [showNotif, setShowNotif] = useState(false);
31
+ const [lowStockBagsThreshold, setLowStockBagsThreshold] = useState<number>(10);
32
+
33
+ useEffect(() => {
34
+ try {
35
+ const stored = localStorage.getItem('pp_alert_config');
36
+ if (!stored) return;
37
+ const parsed = JSON.parse(stored);
38
+ if (
39
+ parsed &&
40
+ typeof parsed.lowStockBagsThreshold === 'number' &&
41
+ !Number.isNaN(parsed.lowStockBagsThreshold)
42
+ ) {
43
+ setLowStockBagsThreshold(parsed.lowStockBagsThreshold);
44
+ }
45
+ } catch (error) {
46
+ console.error('Error loading alert config', error);
47
+ }
48
+ }, []);
49
+
50
+ useEffect(() => {
51
+ const loadData = async () => {
52
+ const txs = await getTransactions();
53
+ const lots = await getActiveLots();
54
+ const parties = await getParties();
55
+
56
+ setRecentTx(txs.slice(0, 5));
57
+ setActiveLots(lots);
58
+
59
+ const stockQty = lots.reduce((acc, lot) => acc + lot.remaining_quantity, 0);
60
+ const stockVal = lots.reduce((acc, lot) => acc + (lot.remaining_quantity * lot.avg_rate), 0);
61
+
62
+ setTotalStock(stockQty);
63
+ setStockValue(stockVal);
64
+
65
+ const dues = parties.filter(p => p.current_balance !== 0);
66
+ setDueParties(dues);
67
+
68
+ const bagCounts: Record<string, number> = {};
69
+ txs.forEach((tx) => {
70
+ tx.items.forEach((item) => {
71
+ if (!item.lot_id) return;
72
+ const lotId = item.lot_id;
73
+ if (!bagCounts[lotId]) bagCounts[lotId] = 0;
74
+ if (tx.bill_type === BillType.AWAAK) {
75
+ bagCounts[lotId] += tx.is_return ? -item.poti_count : item.poti_count;
76
+ } else {
77
+ bagCounts[lotId] += tx.is_return ? item.poti_count : -item.poti_count;
78
+ }
79
+ });
80
+ });
81
+
82
+ const alerts: string[] = [];
83
+
84
+ const lowStockLots = lots.filter((lot) => {
85
+ const bags = Math.max(0, bagCounts[lot.id] || 0);
86
+ return bags > 0 && bags <= lowStockBagsThreshold;
87
+ });
88
+
89
+ if (lowStockLots.length > 0) {
90
+ alerts.push(`${lowStockLots.length} lot(s) are running low on stock (≤ ${lowStockBagsThreshold} bags).`);
91
+ lowStockLots.slice(0, 3).forEach((lot) => {
92
+ const bags = Math.max(0, bagCounts[lot.id] || 0);
93
+ alerts.push(`LOT ${lot.lot_number} has only ${bags} bags left.`);
94
+ });
95
+ }
96
+
97
+ const paymentPending = dues.length;
98
+ if (paymentPending > 0) alerts.push(`${paymentPending} parties have pending payments.`);
99
+ if (txs.length === 0) alerts.push("Welcome! Create your first bill to get started.");
100
+
101
+ setNotifications(alerts);
102
+ };
103
+ loadData();
104
+ }, [lowStockBagsThreshold]);
105
+
106
+ const chartData = activeLots.map(lot => ({
107
+ name: lot.mirchi_name,
108
+ qty: lot.remaining_quantity
109
+ }));
110
+
111
+ const aggregatedChartData = Object.values(chartData.reduce((acc: any, curr) => {
112
+ if (!acc[curr.name]) acc[curr.name] = { name: curr.name, qty: 0 };
113
+ acc[curr.name].qty += curr.qty;
114
+ return acc;
115
+ }, {}));
116
+
117
+ return (
118
+ <div className="space-y-4 md:space-y-8 pb-6 md:pb-10">
119
+ {/* Header Section with Separated Notification Bell */}
120
+ <div className="flex flex-col md:flex-row justify-between items-stretch gap-3 md:gap-6">
121
+ {/* Welcome Card */}
122
+ <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">
123
+ <div className="z-10 relative">
124
+ <h1 className="text-xl md:text-2xl font-bold">नमस्कार, Admin! 👋</h1>
125
+ <p className="text-teal-100 mt-2 opacity-90 max-w-lg text-sm md:text-base">
126
+ तुमच्या व्यवसायाचा आजचा आढावा येथे आहे. (Here is your business overview for today.)
127
+ </p>
128
+ </div>
129
+ {/* Decorative Background Elements */}
130
+ <div className="absolute right-0 top-0 h-full w-1/3 bg-white/10 skew-x-12 z-0 pointer-events-none"></div>
131
+ <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>
132
+ </div>
133
+
134
+ {/* Actions / Notifications */}
135
+ <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">
136
+ <div className="flex flex-col text-left text-xs md:text-sm">
137
+ <span className="text-xs text-gray-500 font-medium uppercase">Current Date</span>
138
+ <span className="font-bold text-gray-800">{new Date().toLocaleDateString('en-IN', { day: 'numeric', month: 'short' })}</span>
139
+ </div>
140
+ <div className="hidden md:block h-10 w-px" />
141
+ <div className="relative flex-shrink-0 ml-auto">
142
+ <button
143
+ onClick={() => setShowNotif(!showNotif)}
144
+ 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"
145
+ aria-label="Notifications"
146
+ >
147
+ <Bell size={24} />
148
+ {notifications.length > 0 && (
149
+ <span className="absolute top-0 right-0 w-3 h-3 bg-red-500 rounded-full border-2 border-white animate-pulse"></span>
150
+ )}
151
+ </button>
152
+
153
+ {/* Notification Dropdown - aligned to bell */}
154
+ {showNotif && (
155
+ <div
156
+ 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"
157
+ >
158
+ <div className="p-4 border-b bg-gray-50 rounded-t-xl font-semibold flex justify-between items-center">
159
+ <span>सूचना (Notifications)</span>
160
+ <span className="text-xs bg-teal-100 text-teal-800 px-2 py-1 rounded-full font-bold">{notifications.length}</span>
161
+ </div>
162
+ <div className="max-h-72 overflow-y-auto custom-scrollbar">
163
+ {notifications.length === 0 ? (
164
+ <div className="p-6 text-center text-gray-400 text-sm">No new notifications</div>
165
+ ) : (
166
+ <div className="divide-y divide-gray-100">
167
+ {notifications.map((note, i) => (
168
+ <div key={i} className="p-4 hover:bg-gray-50 text-sm flex items-start gap-3 transition-colors">
169
+ <div className="w-2 h-2 mt-1.5 rounded-full bg-orange-500 shrink-0"></div>
170
+ <p className="text-gray-600 leading-relaxed">{note}</p>
171
+ </div>
172
+ ))}
173
+ </div>
174
+ )}
175
+ </div>
176
+ <div className="p-2 border-t text-center">
177
+ <button onClick={() => setShowNotif(false)} className="text-xs text-teal-600 font-medium hover:underline">Close</button>
178
+ </div>
179
+ </div>
180
+ )}
181
+ </div>
182
+ </div>
183
+ </div>
184
+
185
+ {/* KPI Cards - mobile swipeable row, desktop grid */}
186
+ {/* Mobile: horizontal scroll */}
187
+ <div className="md:hidden -mx-4 px-4 mt-1">
188
+ <div className="flex gap-3 overflow-x-auto pb-1 no-scrollbar snap-x snap-mandatory">
189
+ <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">
190
+ <div className="flex items-center justify-between">
191
+ <div>
192
+ <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Total Stock</p>
193
+ <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>
194
+ </div>
195
+ <div className="p-3 bg-blue-50 text-blue-600 rounded-2xl group-hover:bg-blue-100 transition-colors">
196
+ <Package size={20} />
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <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">
202
+ <div className="flex items-center justify-between">
203
+ <div>
204
+ <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Est. Value</p>
205
+ <h3 className="text-xl font-bold text-gray-800 mt-1 group-hover:text-green-600 transition-colors">₹ {(stockValue / 100000).toFixed(2)} L</h3>
206
+ </div>
207
+ <div className="p-3 bg-green-50 text-green-600 rounded-2xl group-hover:bg-green-100 transition-colors">
208
+ <IndianRupee size={20} />
209
+ </div>
210
+ </div>
211
+ </div>
212
+
213
+ <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">
214
+ <div className="flex items-center justify-between">
215
+ <div>
216
+ <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Receivables (येणे)</p>
217
+ <h3 className="text-xl font-bold text-teal-600 mt-1">
218
+ ₹ {dueParties.filter(p => p.current_balance > 0).reduce((a,b) => a + b.current_balance, 0).toLocaleString()}
219
+ </h3>
220
+ </div>
221
+ <div className="p-3 bg-teal-50 text-teal-600 rounded-2xl group-hover:bg-teal-100 transition-colors">
222
+ <ArrowDownLeft size={20} />
223
+ </div>
224
+ </div>
225
+ </div>
226
+
227
+ <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">
228
+ <div className="flex items-center justify-between">
229
+ <div>
230
+ <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Payables (देणे)</p>
231
+ <h3 className="text-xl font-bold text-red-500 mt-1">
232
+ ₹ {Math.abs(dueParties.filter(p => p.current_balance < 0).reduce((a,b) => a + b.current_balance, 0)).toLocaleString()}
233
+ </h3>
234
+ </div>
235
+ <div className="p-3 bg-red-50 text-red-600 rounded-2xl group-hover:bg-red-100 transition-colors">
236
+ <ArrowUpRight size={20} />
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ </div>
242
+
243
+ {/* Desktop / tablet: original grid */}
244
+ <div className="hidden md:grid grid-cols-2 lg:grid-cols-4 gap-6">
245
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
246
+ <div className="flex items-center justify-between">
247
+ <div>
248
+ <p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Total Stock</p>
249
+ <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>
250
+ </div>
251
+ <div className="p-4 bg-blue-50 text-blue-600 rounded-2xl group-hover:bg-blue-100 transition-colors">
252
+ <Package size={24} />
253
+ </div>
254
+ </div>
255
+ </div>
256
+
257
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
258
+ <div className="flex items-center justify-between">
259
+ <div>
260
+ <p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Est. Value</p>
261
+ <h3 className="text-2xl font-bold text-gray-800 mt-2 group-hover:text-green-600 transition-colors">₹ {(stockValue / 100000).toFixed(2)} L</h3>
262
+ </div>
263
+ <div className="p-4 bg-green-50 text-green-600 rounded-2xl group-hover:bg-green-100 transition-colors">
264
+ <IndianRupee size={24} />
265
+ </div>
266
+ </div>
267
+ </div>
268
+
269
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
270
+ <div className="flex items-center justify-between">
271
+ <div>
272
+ <p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Receivables (येणे)</p>
273
+ <h3 className="text-2xl font-bold text-teal-600 mt-2">
274
+ ₹ {dueParties.filter(p => p.current_balance > 0).reduce((a,b) => a + b.current_balance, 0).toLocaleString()}
275
+ </h3>
276
+ </div>
277
+ <div className="p-4 bg-teal-50 text-teal-600 rounded-2xl group-hover:bg-teal-100 transition-colors">
278
+ <ArrowDownLeft size={24} />
279
+ </div>
280
+ </div>
281
+ </div>
282
+
283
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
284
+ <div className="flex items-center justify-between">
285
+ <div>
286
+ <p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Payables (देणे)</p>
287
+ <h3 className="text-2xl font-bold text-red-500 mt-2">
288
+ ₹ {Math.abs(dueParties.filter(p => p.current_balance < 0).reduce((a,b) => a + b.current_balance, 0)).toLocaleString()}
289
+ </h3>
290
+ </div>
291
+ <div className="p-4 bg-red-50 text-red-600 rounded-2xl group-hover:bg-red-100 transition-colors">
292
+ <ArrowUpRight size={24} />
293
+ </div>
294
+ </div>
295
+ </div>
296
+ </div>
297
+
298
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 md:gap-8">
299
+ {/* Recent Transactions */}
300
+ <div className="lg:col-span-2 flex flex-col gap-6">
301
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 flex flex-col h-full">
302
+ <div className="p-3 md:p-4 border-b border-gray-100 flex items-center justify-between">
303
+ <h3 className="font-bold text-gray-800 flex items-center gap-2">
304
+ <TrendingUp size={20} className="text-gray-400" />
305
+ अलीकडील व्यवहार (Recent)
306
+ </h3>
307
+ <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>
308
+ </div>
309
+ {/* Desktop table */}
310
+ <div className="hidden md:block overflow-x-auto flex-1">
311
+ <table className="w-full text-sm text-left">
312
+ <thead className="bg-gray-50 text-gray-500 uppercase text-xs tracking-wider">
313
+ <tr>
314
+ <th className="px-6 py-4">Bill No</th>
315
+ <th className="px-6 py-4">Party</th>
316
+ <th className="px-6 py-4">Type</th>
317
+ <th className="px-6 py-4 text-right">Amount</th>
318
+ </tr>
319
+ </thead>
320
+ <tbody className="divide-y divide-gray-100">
321
+ {recentTx.length === 0 ? (
322
+ <tr><td colSpan={4} className="text-center py-12 text-gray-400">No transactions recorded yet.</td></tr>
323
+ ) : (
324
+ recentTx.map(tx => (
325
+ <tr key={tx.id} className="hover:bg-gray-50 transition-colors">
326
+ <td className="px-6 py-4">
327
+ <div className="font-bold text-gray-700">{tx.bill_number}</div>
328
+ <div className="text-xs text-gray-400 mt-0.5">{tx.bill_date}</div>
329
+ </td>
330
+ <td className="px-6 py-4 font-medium text-gray-900">{tx.party_name}</td>
331
+ <td className="px-6 py-4">
332
+ <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold capitalize ${
333
+ tx.is_return
334
+ ? 'bg-orange-100 text-orange-800'
335
+ : tx.bill_type === 'jawaak'
336
+ ? 'bg-blue-100 text-blue-800'
337
+ : 'bg-green-100 text-green-800'
338
+ }`}>
339
+ {tx.is_return ? 'Return' : (tx.bill_type === 'jawaak' ? 'Purchase' : 'Sale')}
340
+ </span>
341
+ </td>
342
+ <td className="px-6 py-4 text-right font-bold text-gray-700">₹{tx.total_amount.toLocaleString()}</td>
343
+ </tr>
344
+ ))
345
+ )}
346
+ </tbody>
347
+ </table>
348
+ </div>
349
+
350
+ {/* Mobile cards */}
351
+ <div className="md:hidden flex-1 p-3 space-y-2.5">
352
+ {recentTx.length === 0 ? (
353
+ <div className="text-center py-8 text-gray-400 text-sm">No transactions recorded yet.</div>
354
+ ) : (
355
+ recentTx.map(tx => (
356
+ <div
357
+ key={tx.id}
358
+ className="border border-gray-100 rounded-lg p-3 hover:bg-gray-50 transition-colors flex flex-col gap-2"
359
+ >
360
+ <div className="flex justify-between items-start gap-2">
361
+ <div>
362
+ <div className="text-sm font-semibold text-gray-800">{tx.bill_number}</div>
363
+ <div className="text-[11px] text-gray-400 mt-0.5">{tx.bill_date}</div>
364
+ </div>
365
+ <span
366
+ className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold capitalize ${
367
+ tx.is_return
368
+ ? 'bg-orange-100 text-orange-800'
369
+ : tx.bill_type === 'jawaak'
370
+ ? 'bg-blue-100 text-blue-800'
371
+ : 'bg-green-100 text-green-800'
372
+ }`}
373
+ >
374
+ {tx.is_return ? 'Return' : tx.bill_type === 'jawaak' ? 'Purchase' : 'Sale'}
375
+ </span>
376
+ </div>
377
+ <div className="flex justify-between items-center text-xs mt-1">
378
+ <div className="text-gray-600 font-medium truncate max-w-[60%]">
379
+ {tx.party_name || 'Unknown Party'}
380
+ </div>
381
+ <div className="text-gray-900 font-bold text-sm">
382
+ ₹{tx.total_amount.toLocaleString()}
383
+ </div>
384
+ </div>
385
+ </div>
386
+ ))
387
+ )}
388
+ </div>
389
+ </div>
390
+ </div>
391
+
392
+ {/* Right Column */}
393
+ <div className="flex flex-col gap-6">
394
+ {/* Pending Payments List */}
395
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 flex flex-col max-h-[360px] md:max-h-[400px]">
396
+ <div className="p-3 md:p-4 border-b border-gray-100">
397
+ <h3 className="font-bold text-gray-800 flex items-center gap-2">
398
+ <AlertCircle size={20} className="text-orange-500" />
399
+ पेमेंट बाकी (Pending)
400
+ </h3>
401
+ </div>
402
+ <div className="overflow-y-auto custom-scrollbar p-2 space-y-2">
403
+ {dueParties.length === 0 ? (
404
+ <div className="text-center py-6 md:py-8 text-gray-400 text-sm">
405
+ <p>All payments settled! 🎉</p>
406
+ </div>
407
+ ) : (
408
+ dueParties.map(p => (
409
+ <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">
410
+ <div>
411
+ <p className="font-bold text-sm text-gray-800">{p.name}</p>
412
+ <p className="text-xs text-gray-500 mt-0.5">{p.city}</p>
413
+ </div>
414
+ <div className={`text-sm font-bold ${p.current_balance > 0 ? 'text-teal-600' : 'text-red-500'}`}>
415
+ {p.current_balance > 0 ? '+' : ''}{p.current_balance.toLocaleString()}
416
+ </div>
417
+ </div>
418
+ ))
419
+ )}
420
+ </div>
421
+ </div>
422
+
423
+ {/* Stock Chart */}
424
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 md:p-5">
425
+ <h3 className="font-bold text-gray-800 mb-4 md:mb-5 text-sm md:text-base">स्टॉक (Quantity)</h3>
426
+ <div className="h-40 md:h-48 w-full">
427
+ <ResponsiveContainer width="100%" height="100%">
428
+ <BarChart data={aggregatedChartData}>
429
+ <XAxis dataKey="name" axisLine={false} tickLine={false} tick={{fontSize: 12, fill: '#6b7280'}} />
430
+ <Tooltip
431
+ contentStyle={{borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'}}
432
+ cursor={{fill: '#f3f4f6'}}
433
+ />
434
+ <Bar dataKey="qty" fill="#0d9488" radius={[4, 4, 0, 0]} barSize={40} />
435
+ </BarChart>
436
+ </ResponsiveContainer>
437
+ </div>
438
+ </div>
439
+ </div>
440
+ </div>
441
+ </div>
442
+ );
443
+ };
444
+
445
+ export default Dashboard;
pages/JawaakBill.tsx ADDED
@@ -0,0 +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
+ 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 ADDED
@@ -0,0 +1,864 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, Calendar, ArrowUpDown, FileText } from 'lucide-react';
5
+ import PrintInvoice from '../components/PrintInvoice.tsx';
6
+ import PdfInvoice from '../components/PdfInvoice.tsx';
7
+ import { exportPartyLedger } from '../utils/exportToExcel';
8
+ import { generateLedgerPDF } from '../utils/LedgerPdfGenerator'; // Import the new utility
9
+
10
+ const PartyLedger = () => {
11
+ const [parties, setParties] = useState<Party[]>([]);
12
+ const [transactions, setTransactions] = useState<Transaction[]>([]);
13
+ const [activeTab, setActiveTab] = useState<'awaak' | 'jawaak'>('awaak');
14
+ const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
15
+ const [selectedParty, setSelectedParty] = useState<Party | null>(null);
16
+ const [searchQuery, setSearchQuery] = useState('');
17
+
18
+ // Update Payment State
19
+ const [editingTxId, setEditingTxId] = useState<string | null>(null);
20
+ const [paymentAmount, setPaymentAmount] = useState<string>('');
21
+
22
+ // Multi-select for combined invoice
23
+ const [selectedTransactions, setSelectedTransactions] = useState<string[]>([]);
24
+
25
+ // NEW: Detail view filter state
26
+ const [detailSearch, setDetailSearch] = useState('');
27
+ const [fromDate, setFromDate] = useState('');
28
+ const [toDate, setToDate] = useState('');
29
+ const [sortOrder, setSortOrder] = useState<'newest' | 'oldest'>('newest');
30
+
31
+ const loadData = async () => {
32
+ setParties(await getParties());
33
+ setTransactions(await getTransactions());
34
+ };
35
+
36
+ useEffect(() => {
37
+ loadData();
38
+ }, []);
39
+
40
+ // Filter Parties based on Tab and Search
41
+ const filteredParties = parties.filter(p => {
42
+ const matchesTab = activeTab === 'awaak'
43
+ ? (p.party_type === PartyType.AWAAK || p.party_type === PartyType.BOTH)
44
+ : (p.party_type === PartyType.JAWAAK || p.party_type === PartyType.BOTH);
45
+
46
+ const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
47
+ p.phone.includes(searchQuery) ||
48
+ p.city.toLowerCase().includes(searchQuery.toLowerCase());
49
+
50
+ return matchesTab && matchesSearch;
51
+ });
52
+
53
+ // Get Transactions for Selected Party (base list)
54
+ const partyTransactions = selectedParty
55
+ ? transactions.filter(t => t.party_id === selectedParty.id)
56
+ : [];
57
+
58
+ // NEW: Memoized filtered and sorted party transactions
59
+ const filteredPartyTransactions = useMemo(() => {
60
+ let filtered = [...partyTransactions];
61
+
62
+ // Apply search filter (dynamic - searches everything)
63
+ if (detailSearch.trim()) {
64
+ const query = detailSearch.toLowerCase().trim();
65
+ filtered = filtered.filter(tx => {
66
+ // Search in bill number
67
+ if (tx.bill_number?.toLowerCase().includes(query)) return true;
68
+
69
+ // Search in date
70
+ const txDate = tx.bill_date?.split('T')[0] || tx.bill_date || '';
71
+ if (txDate.includes(query)) return true;
72
+
73
+ // Search in items (mirchi name, lot number)
74
+ const itemsMatch = tx.items?.some(item =>
75
+ item.mirchi_name?.toLowerCase().includes(query) ||
76
+ item.lot_number?.toLowerCase().includes(query)
77
+ );
78
+ if (itemsMatch) return true;
79
+
80
+ // Search in amounts (as string)
81
+ if (tx.total_amount?.toString().includes(query)) return true;
82
+ if (tx.paid_amount?.toString().includes(query)) return true;
83
+ if (tx.balance_amount?.toString().includes(query)) return true;
84
+
85
+ // Search for "return" or "returned"
86
+ if (tx.is_return && ('returned'.includes(query) || 'return'.includes(query))) return true;
87
+
88
+ // Search for "paid" status
89
+ if (tx.balance_amount === 0 && 'paid'.includes(query)) return true;
90
+
91
+ // Search for "due" or "pending"
92
+ if (tx.balance_amount > 0 && ('due'.includes(query) || 'pending'.includes(query))) return true;
93
+
94
+ return false;
95
+ });
96
+ }
97
+
98
+ // Apply date range filter
99
+ if (fromDate) {
100
+ filtered = filtered.filter(tx => {
101
+ const txDate = tx.bill_date?.split('T')[0] || tx.bill_date || '';
102
+ return txDate >= fromDate;
103
+ });
104
+ }
105
+
106
+ if (toDate) {
107
+ filtered = filtered.filter(tx => {
108
+ const txDate = tx.bill_date?.split('T')[0] || tx.bill_date || '';
109
+ return txDate <= toDate;
110
+ });
111
+ }
112
+
113
+ // Apply sort
114
+ filtered.sort((a, b) => {
115
+ const dateA = new Date(a.bill_date || 0).getTime();
116
+ const dateB = new Date(b.bill_date || 0).getTime();
117
+ return sortOrder === 'newest' ? dateB - dateA : dateA - dateB;
118
+ });
119
+
120
+ return filtered;
121
+ }, [partyTransactions, detailSearch, fromDate, toDate, sortOrder]);
122
+
123
+ // Memoize combined transaction for PDF generation
124
+ const combinedTransaction = useMemo(() => {
125
+ if (!selectedParty || selectedTransactions.length === 0) return null;
126
+
127
+ const selected = filteredPartyTransactions.filter(t => selectedTransactions.includes(t.id));
128
+ if (selected.length === 0) return null;
129
+
130
+ return {
131
+ ...selected[0], // Use first transaction as base
132
+ id: `combined-${selectedParty.id}-${Date.now()}`, // Unique ID for combined
133
+ bill_number: `COMBINED-${selected.length}-BILLS`,
134
+ items: selected.flatMap(t => t.items),
135
+ subtotal: selected.reduce((sum, t) => sum + (t.subtotal || 0), 0),
136
+ total_expenses: selected.reduce((sum, t) => sum + (t.total_expenses || 0), 0),
137
+ total_amount: selected.reduce((sum, t) => sum + t.total_amount, 0),
138
+ paid_amount: selected.reduce((sum, t) => sum + t.paid_amount, 0),
139
+ balance_amount: selected.reduce((sum, t) => sum + t.balance_amount, 0),
140
+ gross_weight_total: selected.reduce((sum, t) => sum + (t.gross_weight_total || 0), 0),
141
+ net_weight_total: selected.reduce((sum, t) => sum + (t.net_weight_total || 0), 0),
142
+ };
143
+ }, [selectedTransactions, filteredPartyTransactions, selectedParty]);
144
+
145
+ // Calculate Totals for List View
146
+ const getPartyStats = (partyId: string) => {
147
+ const txs = transactions.filter(t => t.party_id === partyId);
148
+
149
+ // Calculate totals - returns should subtract from totals
150
+ const totalBill = txs.reduce((sum, t) => {
151
+ if (t.is_return) {
152
+ return sum - t.total_amount; // Subtract returns
153
+ }
154
+ return sum + t.total_amount; // Add normal transactions
155
+ }, 0);
156
+
157
+ const totalPaid = txs.reduce((sum, t) => {
158
+ if (t.is_return) {
159
+ return sum - t.paid_amount; // Subtract returns
160
+ }
161
+ return sum + t.paid_amount; // Add normal transactions
162
+ }, 0);
163
+
164
+ // Balance is directly from party object for accuracy
165
+ const party = parties.find(p => p.id === partyId);
166
+ return {
167
+ totalBill,
168
+ totalPaid,
169
+ balance: party?.current_balance || 0
170
+ };
171
+ };
172
+
173
+ const handleUpdatePayment = async (tx: Transaction) => {
174
+ const amount = parseFloat(paymentAmount);
175
+ if (isNaN(amount) || amount <= 0) {
176
+ alert('Please enter a valid amount');
177
+ return;
178
+ }
179
+ if (amount > tx.balance_amount) {
180
+ alert('Amount cannot exceed due balance');
181
+ return;
182
+ }
183
+
184
+ const res = await updateTransactionPayment(tx.id, amount);
185
+ if (res.success) {
186
+ // Update local state for demo/sample data
187
+ setTransactions((prev) =>
188
+ prev.map((t) =>
189
+ t.id === tx.id
190
+ ? {
191
+ ...t,
192
+ paid_amount: t.paid_amount + amount,
193
+ balance_amount: t.balance_amount - amount,
194
+ }
195
+ : t,
196
+ ),
197
+ );
198
+ setEditingTxId(null);
199
+ setPaymentAmount('');
200
+ // Reload data to refresh party balances
201
+ loadData();
202
+ } else {
203
+ alert(res.message);
204
+ }
205
+ };
206
+
207
+ // NEW: Reset detail filters when going back to list
208
+ const handleBackToList = () => {
209
+ setViewMode('list');
210
+ setSelectedParty(null);
211
+ setSelectedTransactions([]);
212
+ setDetailSearch('');
213
+ setFromDate('');
214
+ setToDate('');
215
+ setSortOrder('newest');
216
+ };
217
+
218
+ // NEW: Clear all detail filters
219
+ const clearDetailFilters = () => {
220
+ setDetailSearch('');
221
+ setFromDate('');
222
+ setToDate('');
223
+ setSortOrder('newest');
224
+ };
225
+
226
+ // NEW: Check if any filter is active
227
+ const hasActiveFilters = detailSearch || fromDate || toDate || sortOrder !== 'newest';
228
+
229
+
230
+ return (
231
+ <div className="space-y-6">
232
+ {/* Header & Controls */}
233
+ <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">
234
+ {viewMode === 'detail' && selectedParty ? (
235
+ <>
236
+ {/* Header with Party Info */}
237
+ <div className="flex items-center justify-between w-full">
238
+ <div className="flex items-center gap-4">
239
+ <button
240
+ onClick={handleBackToList}
241
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
242
+ >
243
+ <ArrowLeft size={20} className="text-gray-600" />
244
+ </button>
245
+ <div>
246
+ <h2 className="text-2xl font-bold text-gray-800">{selectedParty.name}</h2>
247
+ <p className="text-sm text-gray-500">{selectedParty.city} • {selectedParty.phone}</p>
248
+ </div>
249
+ </div>
250
+
251
+ <div className="flex items-center gap-2">
252
+ {selectedTransactions.length > 0 && combinedTransaction && (
253
+ <PrintInvoice
254
+ transaction={combinedTransaction}
255
+ 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"
256
+ >
257
+ <Printer size={18} className="shrink-0" />
258
+ <span className="hidden sm:inline">Print {selectedTransactions.length} Selected</span>
259
+ </PrintInvoice>
260
+ )}
261
+
262
+ {/* --- NEW RED PDF BUTTON --- */}
263
+ <button
264
+ onClick={() => generateLedgerPDF(selectedParty, filteredPartyTransactions)}
265
+ 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 shadow-sm"
266
+ title="Download PDF Ledger"
267
+ >
268
+ <FileText size={18} className="shrink-0" />
269
+ <span className="hidden sm:inline">PDF Ledger</span>
270
+ </button>
271
+ {/* ------------------------- */}
272
+
273
+ <button
274
+ onClick={() => exportPartyLedger(selectedParty, filteredPartyTransactions)}
275
+ 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"
276
+ title="Export to Excel"
277
+ >
278
+ <Download size={18} className="shrink-0" />
279
+ <span className="hidden sm:inline">Export to Excel</span>
280
+ </button>
281
+ </div>
282
+ </div>
283
+ </>
284
+ ) : (
285
+ <div className="flex items-center gap-2">
286
+ <Users className="text-teal-600" />
287
+ <h2 className="text-lg font-bold">
288
+ पार्टी लेजर (Party Ledger)
289
+ </h2>
290
+ </div>
291
+ )}
292
+
293
+ {viewMode === 'list' && (
294
+ <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
295
+ {/* Tabs */}
296
+ <div className="flex bg-gray-100 p-1 rounded-lg">
297
+ <button
298
+ onClick={() => setActiveTab('awaak')}
299
+ 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'}`}
300
+ >
301
+ Awaak (Purchase)
302
+ </button>
303
+ <button
304
+ onClick={() => setActiveTab('jawaak')}
305
+ 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'}`}
306
+ >
307
+ Jawaak (Sales)
308
+ </button>
309
+ </div>
310
+
311
+ {/* Search */}
312
+ <div className="relative w-full md:w-auto mt-2 md:mt-0">
313
+ <Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
314
+ <input
315
+ type="text"
316
+ placeholder="Search Party..."
317
+ 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"
318
+ value={searchQuery}
319
+ onChange={e => setSearchQuery(e.target.value)}
320
+ />
321
+ </div>
322
+ </div>
323
+ )}
324
+ </div>
325
+
326
+ {/* NEW: Detail View Filters */}
327
+ {viewMode === 'detail' && selectedParty && (
328
+ <div className="bg-white p-4 rounded-xl border border-gray-100 shadow-sm">
329
+ <div className="flex flex-col lg:flex-row gap-4 items-start lg:items-center justify-between">
330
+ {/* Search & Filters Row */}
331
+ <div className="flex flex-col sm:flex-row gap-3 w-full lg:w-auto">
332
+ {/* Dynamic Search */}
333
+ <div className="relative flex-1 sm:flex-none">
334
+ <Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
335
+ <input
336
+ type="text"
337
+ placeholder="Search bill, date, mirchi, amount..."
338
+ 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 sm:w-72"
339
+ value={detailSearch}
340
+ onChange={e => setDetailSearch(e.target.value)}
341
+ />
342
+ </div>
343
+
344
+ {/* Date Range */}
345
+ <div className="flex items-center gap-2">
346
+ <div className="relative">
347
+ <Calendar size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
348
+ <input
349
+ type="date"
350
+ className="pl-9 pr-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500 outline-none"
351
+ value={fromDate}
352
+ onChange={e => setFromDate(e.target.value)}
353
+ title="From Date"
354
+ />
355
+ </div>
356
+ <span className="text-gray-400 text-sm">to</span>
357
+ <div className="relative">
358
+ <Calendar size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
359
+ <input
360
+ type="date"
361
+ className="pl-9 pr-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500 outline-none"
362
+ value={toDate}
363
+ onChange={e => setToDate(e.target.value)}
364
+ title="To Date"
365
+ />
366
+ </div>
367
+ </div>
368
+
369
+ {/* Sort Order */}
370
+ <div className="relative">
371
+ <ArrowUpDown size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
372
+ <select
373
+ className="pl-9 pr-8 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500 outline-none appearance-none bg-white cursor-pointer"
374
+ value={sortOrder}
375
+ onChange={e => setSortOrder(e.target.value as 'newest' | 'oldest')}
376
+ >
377
+ <option value="newest">Newest First</option>
378
+ <option value="oldest">Oldest First</option>
379
+ </select>
380
+ </div>
381
+ </div>
382
+
383
+ {/* Results Count & Clear */}
384
+ <div className="flex items-center gap-3">
385
+ <span className="text-sm text-gray-500">
386
+ {filteredPartyTransactions.length} of {partyTransactions.length} transactions
387
+ </span>
388
+ {hasActiveFilters && (
389
+ <button
390
+ onClick={clearDetailFilters}
391
+ className="px-3 py-1.5 text-sm text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-1"
392
+ >
393
+ <X size={14} />
394
+ Clear Filters
395
+ </button>
396
+ )}
397
+ </div>
398
+ </div>
399
+ </div>
400
+ )}
401
+
402
+ {/* Content Area */}
403
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
404
+ {viewMode === 'list' ? (
405
+ <>
406
+ {/* Desktop Table View */}
407
+ <div className="hidden md:block overflow-x-auto">
408
+ <table className="w-full text-sm text-left">
409
+ <thead className="bg-gray-50 text-gray-500 border-b border-gray-100">
410
+ <tr>
411
+ <th className="px-6 py-4 font-medium">Party Name</th>
412
+ <th className="px-6 py-4 font-medium text-right">Total Bill Amount</th>
413
+ <th className="px-6 py-4 font-medium text-right">Jama (Paid)</th>
414
+ <th className="px-6 py-4 font-medium text-right">Baki (Balance)</th>
415
+ <th className="px-6 py-4 font-medium text-center">Action</th>
416
+ </tr>
417
+ </thead>
418
+ <tbody className="divide-y divide-gray-100">
419
+ {filteredParties.length > 0 ? (
420
+ filteredParties.map(party => {
421
+ const stats = getPartyStats(party.id);
422
+ return (
423
+ <tr key={party.id} className="hover:bg-gray-50 transition-colors">
424
+ <td className="px-6 py-4 font-medium text-gray-900">{party.name}</td>
425
+ <td className="px-6 py-4 text-right font-medium">₹{stats.totalBill.toLocaleString()}</td>
426
+ <td className="px-6 py-4 text-right text-green-600 font-medium">₹{stats.totalPaid.toLocaleString()}</td>
427
+ <td className={`px-6 py-4 text-right font-bold ${stats.balance !== 0 ? 'text-red-500' : 'text-gray-400'}`}>
428
+ ₹{Math.abs(stats.balance).toLocaleString()} {stats.balance > 0 ? '(Dr)' : stats.balance < 0 ? '(Cr)' : ''}
429
+ </td>
430
+ <td className="px-6 py-4 text-center">
431
+ <button
432
+ onClick={() => {
433
+ setSelectedParty(party);
434
+ setViewMode('detail');
435
+ // If this party has no transactions, add some sample data for demo
436
+ const hasTx = transactions.some(t => t.party_id === party.id);
437
+ if (!hasTx) {
438
+ const sampleTxs = [
439
+ {
440
+ id: `${party.id}-tx1`,
441
+ party_id: party.id,
442
+ bill_date: new Date().toISOString().split('T')[0],
443
+ bill_number: 'SAMPLE001',
444
+ items: [{ mirchi_name: 'Red Chili' }],
445
+ is_return: false,
446
+ total_amount: 5000,
447
+ paid_amount: 2000,
448
+ balance_amount: 3000,
449
+ bill_type: PartyType.AWAAK,
450
+ },
451
+ {
452
+ id: `${party.id}-tx2`,
453
+ party_id: party.id,
454
+ bill_date: new Date().toISOString().split('T')[0],
455
+ bill_number: 'SAMPLE002',
456
+ items: [{ mirchi_name: 'Green Chili' }],
457
+ is_return: false,
458
+ total_amount: 3000,
459
+ paid_amount: 3000,
460
+ balance_amount: 0,
461
+ bill_type: PartyType.AWAAK,
462
+ },
463
+ ];
464
+ setTransactions(prev => [...prev, ...sampleTxs]);
465
+ }
466
+ }}
467
+ className="p-2 hover:bg-teal-50 text-teal-600 rounded-full transition-colors"
468
+ title="View Ledger"
469
+ >
470
+ <Eye size={18} />
471
+ </button>
472
+ </td>
473
+ </tr>
474
+ );
475
+ })
476
+ ) : (
477
+ <tr>
478
+ <td colSpan={5} className="px-6 py-8 text-center text-gray-500">
479
+ No parties found.
480
+ </td>
481
+ </tr>
482
+ )}
483
+ </tbody>
484
+ </table>
485
+ </div>
486
+
487
+ {/* Mobile Card View */}
488
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
489
+ {filteredParties.length > 0 ? (
490
+ filteredParties.map(party => {
491
+ const stats = getPartyStats(party.id);
492
+ return (
493
+ <div key={party.id} className="p-4 space-y-3">
494
+ <div className="flex justify-between items-start">
495
+ <div>
496
+ <h3 className="font-bold text-gray-900">{party.name}</h3>
497
+ </div>
498
+ <button
499
+ onClick={() => {
500
+ setSelectedParty(party);
501
+ setViewMode('detail');
502
+ // If this party has no transactions, add sample data for demo
503
+ const hasTx = transactions.some(t => t.party_id === party.id);
504
+ if (!hasTx) {
505
+ const sampleTxs = [
506
+ {
507
+ id: `${party.id}-tx1`,
508
+ party_id: party.id,
509
+ bill_date: new Date().toISOString().split('T')[0],
510
+ bill_number: 'SAMPLE001',
511
+ items: [{ mirchi_name: 'Red Chili' }],
512
+ is_return: false,
513
+ total_amount: 5000,
514
+ paid_amount: 2000,
515
+ balance_amount: 3000,
516
+ bill_type: PartyType.AWAAK,
517
+ },
518
+ {
519
+ id: `${party.id}-tx2`,
520
+ party_id: party.id,
521
+ bill_date: new Date().toISOString().split('T')[0],
522
+ bill_number: 'SAMPLE002',
523
+ items: [{ mirchi_name: 'Green Chili' }],
524
+ is_return: false,
525
+ total_amount: 3000,
526
+ paid_amount: 3000,
527
+ balance_amount: 0,
528
+ bill_type: PartyType.AWAAK,
529
+ },
530
+ ];
531
+ setTransactions(prev => [...prev, ...sampleTxs]);
532
+ }
533
+ }}
534
+ className="p-2 bg-teal-50 text-teal-600 rounded-lg"
535
+ >
536
+ <Eye size={18} />
537
+ </button>
538
+ </div>
539
+ <div className="grid grid-cols-3 gap-2 text-xs">
540
+ <div className="bg-gray-50 p-2 rounded">
541
+ <div className="text-gray-500 mb-1">Total Bill</div>
542
+ <div className="font-medium">₹{stats.totalBill.toLocaleString()}</div>
543
+ </div>
544
+ <div className="bg-green-50 p-2 rounded">
545
+ <div className="text-green-600 mb-1">Paid</div>
546
+ <div className="font-medium text-green-700">₹{stats.totalPaid.toLocaleString()}</div>
547
+ </div>
548
+ <div className="bg-red-50 p-2 rounded">
549
+ <div className="text-red-500 mb-1">Balance</div>
550
+ <div className="font-bold text-red-600">
551
+ ₹{Math.abs(stats.balance).toLocaleString()} {stats.balance > 0 ? 'Dr' : stats.balance < 0 ? 'Cr' : ''}
552
+ </div>
553
+ </div>
554
+ </div>
555
+ </div>
556
+ );
557
+ })
558
+ ) : (
559
+ <div className="p-8 text-center text-gray-500">
560
+ No parties found.
561
+ </div>
562
+ )}
563
+ </div>
564
+ </>
565
+ ) : (
566
+ // Detail View
567
+ <>
568
+ {/* Desktop Table View */}
569
+ <div className="hidden md:block overflow-x-auto">
570
+ <table className="w-full text-sm text-left">
571
+ <thead className="bg-gray-50 text-gray-500 border-b border-gray-100">
572
+ <tr>
573
+ <th className="px-6 py-4 font-medium text-center">
574
+ <input
575
+ type="checkbox"
576
+ checked={selectedTransactions.length === filteredPartyTransactions.length && filteredPartyTransactions.length > 0}
577
+ onChange={(e) => {
578
+ if (e.target.checked) {
579
+ setSelectedTransactions(filteredPartyTransactions.map(t => t.id));
580
+ } else {
581
+ setSelectedTransactions([]);
582
+ }
583
+ }}
584
+ className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500"
585
+ />
586
+ </th>
587
+ <th className="px-6 py-4 font-medium">Date</th>
588
+ <th className="px-6 py-4 font-medium">Bill No</th>
589
+ <th className="px-6 py-4 font-medium">Mirchi Type</th>
590
+ <th className="px-6 py-4 font-medium">Remark</th>
591
+ <th className="px-6 py-4 font-medium text-right">Bill Amount</th>
592
+ <th className="px-6 py-4 font-medium text-right">Paid</th>
593
+ <th className="px-6 py-4 font-medium text-right">Due Balance</th>
594
+ <th className="px-6 py-4 font-medium text-center">Action</th>
595
+ <th className="px-6 py-4 font-medium text-center">Print</th>
596
+ </tr>
597
+ </thead>
598
+ <tbody className="divide-y divide-gray-100">
599
+ {filteredPartyTransactions.length > 0 ? (
600
+ filteredPartyTransactions.map(tx => (
601
+ <React.Fragment key={tx.id}>
602
+ <tr className="hover:bg-gray-50">
603
+ <td className="px-6 py-4 text-center">
604
+ <input
605
+ type="checkbox"
606
+ checked={selectedTransactions.includes(tx.id)}
607
+ onChange={(e) => {
608
+ if (e.target.checked) {
609
+ setSelectedTransactions([...selectedTransactions, tx.id]);
610
+ } else {
611
+ setSelectedTransactions(selectedTransactions.filter(id => id !== tx.id));
612
+ }
613
+ }}
614
+ className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500"
615
+ />
616
+ </td>
617
+ <td className="px-6 py-4 text-gray-600">{tx.bill_date?.split('T')[0] || tx.bill_date}</td>
618
+ <td className="px-6 py-4 font-mono text-gray-500">{tx.bill_number}</td>
619
+ <td className="px-6 py-4">
620
+ {tx.items.map((i, idx) => (
621
+ <div key={idx} className="text-sm">
622
+ {i.mirchi_name}
623
+ {i.lot_number && (
624
+ <span className="ml-2 text-xs font-mono text-gray-500">({i.lot_number})</span>
625
+ )}
626
+ </div>
627
+ ))}
628
+ </td>
629
+ <td className="px-6 py-4">
630
+ {tx.is_return ? (
631
+ <span className="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs font-semibold">Returned</span>
632
+ ) : (
633
+ <span className="text-gray-400">-</span>
634
+ )}
635
+ </td>
636
+ <td className="px-6 py-4 text-right font-medium">₹{tx.total_amount.toLocaleString()}</td>
637
+ <td className="px-6 py-4 text-right text-green-600">₹{tx.paid_amount.toLocaleString()}</td>
638
+ <td className="px-6 py-4 text-right font-bold text-red-500">₹{tx.balance_amount.toLocaleString()}</td>
639
+ <td className="px-6 py-4 text-center">
640
+ {editingTxId === tx.id ? (
641
+ <div className="flex items-center gap-2 justify-end">
642
+ <input
643
+ type="number"
644
+ className="w-24 border border-teal-300 rounded px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-teal-500"
645
+ placeholder="Amount"
646
+ value={paymentAmount}
647
+ onChange={e => setPaymentAmount(e.target.value)}
648
+ autoFocus
649
+ />
650
+ <button
651
+ onClick={() => handleUpdatePayment(tx)}
652
+ className="p-2 bg-teal-600 text-white rounded hover:bg-teal-700 disabled:opacity-50"
653
+ title="Save"
654
+ disabled={Number(paymentAmount) > tx.balance_amount || Number(paymentAmount) <= 0}
655
+ >
656
+ <Save size={16} />
657
+ </button>
658
+ <button
659
+ onClick={() => { setEditingTxId(null); setPaymentAmount(''); }}
660
+ className="p-1 bg-gray-200 text-gray-600 rounded hover:bg-gray-300"
661
+ title="Cancel"
662
+ >
663
+ <X size={14} />
664
+ </button>
665
+ </div>
666
+ ) : (
667
+ <button
668
+ onClick={() => { setEditingTxId(tx.id); setPaymentAmount(''); }}
669
+ className="px-3 py-1 border border-teal-600 text-teal-600 rounded-md text-xs font-medium hover:bg-teal-50 transition-colors"
670
+ >
671
+ Update Due
672
+ </button>
673
+ )}
674
+ </td>
675
+ <td className="px-6 py-4 text-center">
676
+ <div className="flex gap-2 justify-center items-center">
677
+ <PrintInvoice
678
+ transaction={tx}
679
+ 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"
680
+ />
681
+ <PdfInvoice
682
+ transaction={tx}
683
+ 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"
684
+ >
685
+ <Download size={16} />
686
+ </PdfInvoice>
687
+ </div>
688
+ </td>
689
+ </tr>
690
+ {tx.payments && tx.payments.length > 0 && (
691
+ <tr className="bg-gray-50">
692
+ <td colSpan={10} className="px-12 py-2 text-xs text-gray-600">
693
+ <div className="flex items-start justify-between gap-4">
694
+ <span className="font-semibold text-gray-700 mt-0.5">Payment History:</span>
695
+ <div className="flex flex-wrap gap-3">
696
+ {[...tx.payments.filter(p => p.mode === 'due'), ...tx.payments.filter(p => p.mode !== 'due')].map((p, idx) => (
697
+ <div key={idx} className="flex items-center gap-1 bg-white border border-gray-200 rounded-full px-2 py-0.5 shadow-sm">
698
+ <span className="px-2 py-0.5 rounded-full text-[11px] bg-teal-50 text-teal-700 border border-teal-100 capitalize">
699
+ {p.mode === 'cash' ? 'Cash' : p.mode === 'online' ? 'Online' : p.mode === 'due' ? 'Due' : p.mode}
700
+ </span>
701
+ <span className="text-gray-800 font-medium text-xs">₹{p.amount.toLocaleString()}</span>
702
+ {p.reference && (
703
+ <span className="text-gray-400 text-[11px] ml-1">{p.reference}</span>
704
+ )}
705
+ </div>
706
+ ))}
707
+ </div>
708
+ </div>
709
+ </td>
710
+ </tr>
711
+ )}
712
+ </React.Fragment>
713
+ ))
714
+ ) : (
715
+ <tr>
716
+ <td colSpan={10} className="px-6 py-8 text-center text-gray-500">
717
+ {partyTransactions.length > 0
718
+ ? 'No transactions match your filters.'
719
+ : 'No transactions found for this party.'}
720
+ </td>
721
+ </tr>
722
+ )}
723
+ </tbody>
724
+ </table>
725
+ </div>
726
+
727
+ {/* Mobile Card View for Detail */}
728
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
729
+ {filteredPartyTransactions.length > 0 ? (
730
+ filteredPartyTransactions.map(tx => (
731
+ <div key={tx.id} className="p-4 space-y-3">
732
+ <div className="flex justify-between items-start">
733
+ <div className="flex items-start gap-3">
734
+ <input
735
+ type="checkbox"
736
+ checked={selectedTransactions.includes(tx.id)}
737
+ onChange={(e) => {
738
+ if (e.target.checked) {
739
+ setSelectedTransactions([...selectedTransactions, tx.id]);
740
+ } else {
741
+ setSelectedTransactions(selectedTransactions.filter(id => id !== tx.id));
742
+ }
743
+ }}
744
+ className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500 mt-1"
745
+ />
746
+ <div>
747
+ <div className="text-xs text-gray-500">{tx.bill_date?.split('T')[0] || tx.bill_date}</div>
748
+ <div className="font-mono text-sm font-medium text-gray-800">{tx.bill_number}</div>
749
+ </div>
750
+ </div>
751
+ {tx.is_return && (
752
+ <span className="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs font-semibold">Returned</span>
753
+ )}
754
+ </div>
755
+
756
+ <div className="text-sm text-gray-600">
757
+ <span className="font-medium text-gray-500 text-xs block mb-1">Items:</span>
758
+ {tx.items.map((i, idx) => (
759
+ <div key={idx}>
760
+ {i.mirchi_name}
761
+ {i.lot_number && (
762
+ <span className="ml-2 text-xs font-mono text-gray-500">({i.lot_number})</span>
763
+ )}
764
+ </div>
765
+ ))}
766
+ </div>
767
+
768
+ <div className="grid grid-cols-3 gap-2 text-xs pt-2 border-t border-gray-50">
769
+ <div>
770
+ <div className="text-gray-500">Bill Amount</div>
771
+ <div className="font-medium">₹{tx.total_amount.toLocaleString()}</div>
772
+ </div>
773
+ <div>
774
+ <div className="text-gray-500">Paid</div>
775
+ <div className="font-medium text-green-600">₹{tx.paid_amount.toLocaleString()}</div>
776
+ </div>
777
+ <div>
778
+ <div className="text-gray-500">Due</div>
779
+ <div className="font-bold text-red-500">₹{tx.balance_amount.toLocaleString()}</div>
780
+ </div>
781
+ </div>
782
+
783
+ {tx.payments && tx.payments.length > 0 && (
784
+ <div className="pt-2 border-t border-gray-100 mt-2 text-xs text-gray-600 space-y-1">
785
+ <div className="font-semibold text-gray-700">Payment History</div>
786
+ {[...tx.payments.filter(p => p.mode === 'due'), ...tx.payments.filter(p => p.mode !== 'due')].map((p, idx) => (
787
+ <div key={idx} className="flex justify-between">
788
+ <span>
789
+ {p.mode === 'cash' ? 'Cash' : p.mode === 'online' ? 'Online' : p.mode === 'due' ? 'Due' : p.mode}
790
+ {p.reference && (
791
+ <span className="ml-1 text-gray-400">({p.reference})</span>
792
+ )}
793
+ </span>
794
+ <span className="font-medium">₹{p.amount.toLocaleString()}</span>
795
+ </div>
796
+ ))}
797
+ </div>
798
+ )}
799
+
800
+ <div className="pt-2">
801
+ {editingTxId === tx.id ? (
802
+ <div className="flex items-center gap-2">
803
+ <input
804
+ type="number"
805
+ className="flex-1 border border-teal-300 rounded px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-teal-500"
806
+ placeholder="Enter Amount"
807
+ value={paymentAmount}
808
+ onChange={e => setPaymentAmount(e.target.value)}
809
+ autoFocus
810
+ />
811
+ <button
812
+ onClick={() => handleUpdatePayment(tx)}
813
+ className="p-2 bg-teal-600 text-white rounded hover:bg-teal-700"
814
+ >
815
+ <Save size={16} />
816
+ </button>
817
+ <button
818
+ onClick={() => { setEditingTxId(null); setPaymentAmount(''); }}
819
+ className="p-2 bg-gray-200 text-gray-600 rounded hover:bg-gray-300"
820
+ >
821
+ <X size={16} />
822
+ </button>
823
+ </div>
824
+ ) : (
825
+ <button
826
+ onClick={() => { setEditingTxId(tx.id); setPaymentAmount(''); }}
827
+ className="w-full py-2 border border-teal-600 text-teal-600 rounded-lg text-sm font-medium hover:bg-teal-50 transition-colors"
828
+ >
829
+ Update Due
830
+ </button>
831
+ )}
832
+ </div>
833
+
834
+ {/* Print Button for Mobile */}
835
+ <div className="pt-2 border-t border-gray-100 mt-2 flex gap-3 items-center">
836
+ <PrintInvoice
837
+ transaction={tx}
838
+ 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"
839
+ />
840
+ <PdfInvoice
841
+ transaction={tx}
842
+ 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"
843
+ >
844
+ <Download size={16} />
845
+ </PdfInvoice>
846
+ </div>
847
+ </div>
848
+ ))
849
+ ) : (
850
+ <div className="p-8 text-center text-gray-500">
851
+ {partyTransactions.length > 0
852
+ ? 'No transactions match your filters.'
853
+ : 'No transactions found.'}
854
+ </div>
855
+ )}
856
+ </div>
857
+ </>
858
+ )}
859
+ </div>
860
+ </div >
861
+ );
862
+ };
863
+
864
+ export default PartyLedger;
pages/Settings.tsx ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ lowStockBagsThreshold: 10,
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
+ useEffect(() => {
47
+ try {
48
+ const stored = localStorage.getItem('pp_alert_config');
49
+ if (!stored) return;
50
+ const parsed = JSON.parse(stored);
51
+ setConfig({
52
+ lowStockBagsThreshold:
53
+ typeof parsed.lowStockBagsThreshold === 'number' &&
54
+ !Number.isNaN(parsed.lowStockBagsThreshold)
55
+ ? parsed.lowStockBagsThreshold
56
+ : 10,
57
+ enableNotifications:
58
+ typeof parsed.enableNotifications === 'boolean'
59
+ ? parsed.enableNotifications
60
+ : true,
61
+ });
62
+ } catch (error) {
63
+ console.error('Error loading alert config', error);
64
+ }
65
+ }, []);
66
+
67
+ const handleSaveParty = async () => {
68
+ if (!newParty.name) {
69
+ setPartyMessage({ type: 'error', text: 'Party Name is required.' });
70
+ return;
71
+ }
72
+
73
+ const party: Party = {
74
+ id: newParty.id || `p-${Date.now()}`,
75
+ name: newParty.name,
76
+ city: newParty.city || '',
77
+ phone: newParty.phone || '',
78
+ party_type: newParty.party_type as PartyType,
79
+ current_balance: parseFloat(String(newParty.current_balance || 0))
80
+ };
81
+
82
+ await saveParty(party);
83
+ setNewParty({ name: '', city: '', phone: '', party_type: PartyType.BOTH, current_balance: 0 });
84
+ loadData();
85
+ setPartyMessage({ type: 'success', text: 'Party saved successfully.' });
86
+ };
87
+
88
+ const handleSaveMirchi = async () => {
89
+ if (!newMirchi.name) {
90
+ setMirchiMessage({ type: 'error', text: 'Mirchi type name is required.' });
91
+ return;
92
+ }
93
+
94
+ const type: MirchiType = {
95
+ id: newMirchi.id || `m-${Date.now()}`,
96
+ name: newMirchi.name,
97
+ current_rate: 0
98
+ };
99
+
100
+ const result = await apiSaveMirchiType(type);
101
+ if (!result.success) {
102
+ setMirchiMessage({ type: 'error', text: result.message || 'Error saving mirchi type.' });
103
+ return;
104
+ }
105
+ setNewMirchi({ name: '' });
106
+ loadData();
107
+ setMirchiMessage({ type: 'success', text: 'Mirchi type saved successfully.' });
108
+ };
109
+
110
+ const loadData = async () => {
111
+ setParties(await getParties());
112
+ setMirchiTypes(await apiGetMirchiTypes());
113
+ };
114
+
115
+ const handleSaveAlertConfig = () => {
116
+ try {
117
+ localStorage.setItem('pp_alert_config', JSON.stringify(config));
118
+ } catch (error) {
119
+ console.error('Error saving alert config', error);
120
+ }
121
+ };
122
+
123
+ const handleInstallClick = async () => {
124
+ await installApp();
125
+ };
126
+
127
+ return (
128
+ <div className="flex flex-col md:flex-row h-full gap-6">
129
+ {/* Sidebar / Tabs */}
130
+ <div className="w-full md:w-64 bg-white rounded-xl shadow-sm border border-gray-100 p-4 h-fit">
131
+ <h2 className="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
132
+ <SettingsIcon className="text-teal-600" size={20} /> सेटिंग्स
133
+ </h2>
134
+ <div className="space-y-2">
135
+ <button
136
+ onClick={() => setActiveTab('parties')}
137
+ 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'}`}
138
+ >
139
+ <Users size={18} /> पार्टी व्यवस्थापन
140
+ </button>
141
+ <button
142
+ onClick={() => setActiveTab('mirchi')}
143
+ 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'}`}
144
+ >
145
+ <Sprout size={18} /> मिरची दर & प्रकार
146
+ </button>
147
+ <button
148
+ onClick={() => setActiveTab('general')}
149
+ 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'}`}
150
+ >
151
+ <Bell size={18} /> Alerts & Rules
152
+ </button>
153
+ </div>
154
+ </div>
155
+
156
+ {/* Content Area */}
157
+ <div className="flex-1 bg-white rounded-xl shadow-sm border border-gray-100 p-6 overflow-y-auto no-scrollbar">
158
+
159
+ {/* PARTIES TAB */}
160
+ {activeTab === 'parties' && (
161
+ <div className="space-y-6">
162
+ <div className="border-b pb-4">
163
+ <h3 className="text-lg font-bold text-gray-800">Party व्यवस्थापन (Party Management)</h3>
164
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
165
+ <input
166
+ placeholder="Party Name"
167
+ className="border rounded p-2"
168
+ value={newParty.name}
169
+ onChange={e => setNewParty({ ...newParty, name: e.target.value })}
170
+ />
171
+ <input
172
+ placeholder="Phone"
173
+ className="border rounded p-2"
174
+ value={newParty.phone}
175
+ onChange={e => setNewParty({ ...newParty, phone: e.target.value })}
176
+ />
177
+ <select
178
+ className="border rounded p-2"
179
+ value={newParty.party_type}
180
+ onChange={e => setNewParty({ ...newParty, party_type: e.target.value as PartyType })}
181
+ >
182
+ <option value={PartyType.BOTH}>Both (Purchase & Sales)</option>
183
+ <option value={PartyType.AWAAK}>Only Awaak (Purchase)</option>
184
+ <option value={PartyType.JAWAAK}>Only Jawaak (Sales)</option>
185
+ </select>
186
+ <button
187
+ onClick={handleSaveParty}
188
+ className="bg-teal-600 text-white rounded p-2 flex items-center justify-center gap-2 font-medium hover:bg-teal-700"
189
+ >
190
+ <Plus size={18} /> Save Party
191
+ </button>
192
+ </div>
193
+ {partyMessage && (
194
+ <div
195
+ className={`mt-3 text-sm rounded px-3 py-2 ${partyMessage.type === 'error'
196
+ ? 'bg-red-50 text-red-700 border border-red-100'
197
+ : 'bg-green-50 text-green-700 border border-green-100'
198
+ }`}
199
+ >
200
+ {partyMessage.text}
201
+ </div>
202
+ )}
203
+ </div>
204
+
205
+ <div>
206
+ <h3 className="font-semibold text-gray-700 mb-3">All Parties</h3>
207
+ <div className="overflow-x-auto">
208
+ <table className="w-full text-sm text-left">
209
+ <thead className="bg-gray-50 text-gray-500">
210
+ <tr>
211
+ <th className="p-2">Name</th>
212
+ <th className="p-2">Phone</th>
213
+ <th className="p-2">Type</th>
214
+ <th className="p-2 text-center">Action</th>
215
+ </tr>
216
+ </thead>
217
+ <tbody className="divide-y">
218
+ {parties.map(p => (
219
+ <tr key={p.id}>
220
+ <td className="p-2">{p.name}</td>
221
+ <td className="p-2">{p.phone}</td>
222
+ <td className="p-2">{p.party_type}</td>
223
+ <td className="p-2 text-center">
224
+ <button
225
+ onClick={() => setNewParty(p)}
226
+ className="text-blue-600 hover:underline text-xs"
227
+ >
228
+ Edit
229
+ </button>
230
+ </td>
231
+ </tr>
232
+ ))}
233
+ </tbody>
234
+ </table>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ )}
239
+
240
+ {/* MIRCHI TAB */}
241
+ {activeTab === 'mirchi' && (
242
+ <div className="space-y-6">
243
+ <div className="border-b pb-4">
244
+ <h3 className="text-lg font-bold text-gray-800">मिरची प्रकार (Mirchi Types)</h3>
245
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
246
+ <input
247
+ placeholder="Variety Name (e.g. Teja)"
248
+ className="border rounded p-2"
249
+ value={newMirchi.name}
250
+ onChange={e => setNewMirchi({ ...newMirchi, name: e.target.value })}
251
+ />
252
+ <button
253
+ onClick={handleSaveMirchi}
254
+ className="bg-teal-600 text-white rounded p-2 flex items-center justify-center gap-2 font-medium hover:bg-teal-700"
255
+ >
256
+ <Plus size={18} /> Save Type
257
+ </button>
258
+ </div>
259
+ {mirchiMessage && (
260
+ <div
261
+ className={`mt-3 text-sm rounded px-3 py-2 ${mirchiMessage.type === 'error'
262
+ ? 'bg-red-50 text-red-700 border border-red-100'
263
+ : 'bg-green-50 text-green-700 border border-green-100'
264
+ }`}
265
+ >
266
+ {mirchiMessage.text}
267
+ </div>
268
+ )}
269
+ </div>
270
+
271
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
272
+ {mirchiTypes.map(m => (
273
+ <div key={m.id} className="p-4 border rounded-lg hover:shadow-md transition bg-gray-50">
274
+ <div className="flex justify-between items-start mb-2">
275
+ <h4 className="font-bold text-gray-800">{m.name}</h4>
276
+ <button
277
+ onClick={() => setNewMirchi(m)}
278
+ className="text-teal-600 text-xs font-medium"
279
+ >
280
+ Edit
281
+ </button>
282
+ </div>
283
+ <div className="text-xs text-gray-500 mt-1">Mirchi Type</div>
284
+ </div>
285
+ ))}
286
+ </div>
287
+ </div>
288
+ )}
289
+
290
+ {/* General / Alerts Tab */}
291
+ {activeTab === 'general' && (
292
+ <div className="space-y-6">
293
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
294
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">Install as App</h3>
295
+ <p className="text-sm text-gray-600 mb-4">
296
+ Install this application on your device for quick access and offline support.
297
+ </p>
298
+ {isInstallable ? (
299
+ <button
300
+ onClick={handleInstallClick}
301
+ 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"
302
+ >
303
+ <Download size={20} />
304
+ Install App
305
+ </button>
306
+ ) : (
307
+ <div className="text-sm text-gray-500 bg-gray-50 p-4 rounded-lg border border-gray-200">
308
+ App is already installed or not available for installation on this device.
309
+ </div>
310
+ )}
311
+ </div>
312
+
313
+ <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
314
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">Alert Configuration</h3>
315
+ <div className="space-y-4">
316
+ <div>
317
+ <label className="block text-sm font-medium text-gray-700 mb-2">Low Stock Threshold (Bags / Poti)</label>
318
+ <input
319
+ type="number"
320
+ value={config.lowStockBagsThreshold}
321
+ onChange={(e) =>
322
+ setConfig({
323
+ ...config,
324
+ lowStockBagsThreshold: Number(e.target.value) || 0,
325
+ })
326
+ }
327
+ className="w-full border border-gray-300 rounded-lg p-2"
328
+ />
329
+ </div>
330
+
331
+ <div className="flex items-center gap-3">
332
+ <input
333
+ type="checkbox"
334
+ checked={config.enableNotifications}
335
+ onChange={(e) => setConfig({ ...config, enableNotifications: e.target.checked })}
336
+ className="w-4 h-4 text-teal-600 rounded"
337
+ />
338
+ <label className="text-sm text-gray-700">Enable Notifications</label>
339
+ </div>
340
+ <button
341
+ onClick={handleSaveAlertConfig}
342
+ className="bg-gray-800 text-white px-4 py-2 rounded text-sm hover:bg-gray-900"
343
+ >
344
+ Save Config
345
+ </button>
346
+ </div>
347
+ </div>
348
+ </div>
349
+ )}
350
+ </div>
351
+ </div>
352
+ );
353
+ };
354
+
355
+ export default Settings;
pages/StockReport.tsx ADDED
@@ -0,0 +1,471 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+
3
+ import { getActiveLots, getParties, getTransactions } from '../services/db';
4
+ import { BillType, Lot, Party, Transaction } from '../types';
5
+ import { Package, Search, ArrowLeft } from 'lucide-react';
6
+
7
+ const StockReport = () => {
8
+ const [lots, setLots] = useState<Lot[]>([]);
9
+ const [parties, setParties] = useState<Party[]>([]);
10
+ const [transactions, setTransactions] = useState<Transaction[]>([]);
11
+ const [searchTerm, setSearchTerm] = useState('');
12
+ const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
13
+ const [selectedMirchiId, setSelectedMirchiId] = useState<string | null>(null);
14
+ const [selectedMirchiName, setSelectedMirchiName] = useState<string | null>(null);
15
+ const [lowStockBagsThreshold, setLowStockBagsThreshold] = useState<number>(10);
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
+ useEffect(() => {
27
+ try {
28
+ const stored = localStorage.getItem('pp_alert_config');
29
+ if (!stored) return;
30
+ const parsed = JSON.parse(stored);
31
+ if (
32
+ parsed &&
33
+ typeof parsed.lowStockBagsThreshold === 'number' &&
34
+ !Number.isNaN(parsed.lowStockBagsThreshold)
35
+ ) {
36
+ setLowStockBagsThreshold(parsed.lowStockBagsThreshold);
37
+ }
38
+ } catch (error) {
39
+ console.error('Error loading alert config', error);
40
+ }
41
+ }, []);
42
+
43
+ const filteredLots = useMemo(
44
+ () =>
45
+ lots.filter((l) =>
46
+ l.mirchi_name.toLowerCase().includes(searchTerm.toLowerCase())
47
+ ),
48
+ [lots, searchTerm]
49
+ );
50
+
51
+ const aggregatedByLot = useMemo(() => {
52
+ // Calculate bag count for each lot from transactions
53
+ return filteredLots.map(lot => {
54
+ let totalBags = 0;
55
+
56
+ // Calculate bags from all transactions for this lot
57
+ transactions.forEach(tx => {
58
+ tx.items.forEach(item => {
59
+ if (item.lot_id === lot.id) {
60
+ if (tx.bill_type === BillType.AWAAK) {
61
+ // Purchase: Add bags
62
+ totalBags += tx.is_return ? -item.poti_count : item.poti_count;
63
+ } else {
64
+ // Sale: Subtract bags
65
+ totalBags += tx.is_return ? item.poti_count : -item.poti_count;
66
+ }
67
+ }
68
+ });
69
+ });
70
+
71
+ return {
72
+ lotId: lot.id,
73
+ lotNumber: lot.lot_number,
74
+ mirchiName: lot.mirchi_name,
75
+ totalQuantity: lot.total_quantity,
76
+ remainingQty: lot.remaining_quantity,
77
+ totalBags: Math.max(0, totalBags),
78
+ status: lot.status,
79
+ purchaseDate: lot.purchase_date,
80
+ avgRate: lot.avg_rate
81
+ };
82
+ });
83
+ }, [filteredLots, transactions]);
84
+
85
+ const detailMovements = useMemo(() => {
86
+ if (!selectedMirchiId) return [];
87
+
88
+ const rows: {
89
+ id: string;
90
+ date: string;
91
+ billNo: string;
92
+ partyName: string;
93
+ inQty: number;
94
+ outQty: number;
95
+ inBags: number;
96
+ outBags: number;
97
+ typeLabel: string;
98
+ isReturn: boolean;
99
+ }[] = [];
100
+
101
+ transactions.forEach((tx) => {
102
+ tx.items
103
+ .filter((item) => item.lot_id === selectedMirchiId) // Filter by lot_id instead of mirchi_type_id
104
+ .forEach((item) => {
105
+ const party = parties.find((p) => p.id === tx.party_id);
106
+
107
+ let inQty = 0;
108
+ let outQty = 0;
109
+ let inBags = 0;
110
+ let outBags = 0;
111
+ let typeLabel = '';
112
+
113
+ if (tx.bill_type === BillType.AWAAK) {
114
+ // Awaak = Purchase / Stock IN
115
+ if (tx.is_return) {
116
+ // Purchase Return: Stock OUT
117
+ outQty = item.net_weight;
118
+ outBags = item.poti_count;
119
+ typeLabel = 'Purchase Return';
120
+ } else {
121
+ inQty = item.net_weight;
122
+ inBags = item.poti_count;
123
+ typeLabel = 'Purchase';
124
+ }
125
+ } else {
126
+ // Jawaak = Sales / Stock OUT
127
+ if (tx.is_return) {
128
+ // Sales Return: Stock IN
129
+ inQty = item.net_weight;
130
+ inBags = item.poti_count;
131
+ typeLabel = 'Sales Return';
132
+ } else {
133
+ outQty = item.net_weight;
134
+ outBags = item.poti_count;
135
+ typeLabel = 'Sale';
136
+ }
137
+ }
138
+
139
+ const displayDate = tx.bill_date ? tx.bill_date.split('T')[0] : tx.bill_date;
140
+
141
+ rows.push({
142
+ id: `${tx.id}-${item.id}`,
143
+ date: displayDate,
144
+ billNo: tx.bill_number,
145
+ partyName: party?.name || tx.party_name || 'Unknown Party',
146
+ inQty,
147
+ outQty,
148
+ inBags,
149
+ outBags,
150
+ typeLabel,
151
+ isReturn: tx.is_return,
152
+ });
153
+ });
154
+ });
155
+
156
+ // Sort latest first
157
+ rows.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
158
+ return rows;
159
+ }, [selectedMirchiId, transactions, parties]);
160
+
161
+ return (
162
+ <div className="space-y-4">
163
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
164
+ <div className="p-4 md:p-6 border-b flex flex-col md:flex-row md:items-center justify-between gap-4">
165
+ <div className="flex items-center gap-2">
166
+ {viewMode === 'detail' && (
167
+ <button
168
+ onClick={() => {
169
+ setViewMode('list');
170
+ setSelectedMirchiId(null);
171
+ setSelectedMirchiName(null);
172
+ }}
173
+ className="mr-1 p-1 hover:bg-gray-100 rounded-full"
174
+ >
175
+ <ArrowLeft size={20} className="text-gray-600" />
176
+ </button>
177
+ )}
178
+ <Package className="text-teal-600" />
179
+ <h2 className="text-lg md:text-xl font-bold text-gray-800">
180
+ {viewMode === 'list'
181
+ ? 'स्टॉक रिपोर्ट (Stock Inventory)'
182
+ : `${selectedMirchiName ?? ''} - Stock Detail`}
183
+ </h2>
184
+ </div>
185
+
186
+ {viewMode === 'list' && (
187
+ <div className="relative w-full md:w-auto">
188
+ <Search
189
+ className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
190
+ size={18}
191
+ />
192
+ <input
193
+ type="text"
194
+ placeholder="Search Mirchi Jaat / Type..."
195
+ 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"
196
+ value={searchTerm}
197
+ onChange={(e) => setSearchTerm(e.target.value)}
198
+ />
199
+ </div>
200
+ )}
201
+ </div>
202
+
203
+ {viewMode === 'list' ? (
204
+ <>
205
+ {/* Desktop table */}
206
+ <div className="hidden md:block overflow-x-auto">
207
+ <table className="w-full text-sm text-left">
208
+ <thead className="bg-gray-50 text-gray-500 font-medium">
209
+ <tr>
210
+ <th className="px-6 py-4">LOT Number</th>
211
+ <th className="px-6 py-4">मिरची जात (Mirchi Type)</th>
212
+ <th className="px-6 py-4 text-right">बॅग/पोती (Bags)</th>
213
+ <th className="px-6 py-4 text-right">शिल्लक वजन (Remaining Qty)</th>
214
+ <th className="px-6 py-4 text-center">स्थिती (Status)</th>
215
+ </tr>
216
+ </thead>
217
+ <tbody className="divide-y divide-gray-100">
218
+ {aggregatedByLot.length === 0 ? (
219
+ <tr>
220
+ <td
221
+ colSpan={5}
222
+ className="text-center py-8 text-gray-500"
223
+ >
224
+ No active stock found
225
+ </td>
226
+ </tr>
227
+ ) : (
228
+ aggregatedByLot.map((row) => {
229
+ const isLow = row.totalBags > 0 && row.totalBags <= lowStockBagsThreshold;
230
+ const statusLabel =
231
+ row.remainingQty === 0
232
+ ? 'Stockout'
233
+ : isLow
234
+ ? 'Low Stock'
235
+ : 'Available';
236
+ const statusClasses =
237
+ row.remainingQty === 0
238
+ ? 'bg-red-100 text-red-700'
239
+ : isLow
240
+ ? 'bg-orange-100 text-orange-700'
241
+ : 'bg-green-100 text-green-700';
242
+
243
+ return (
244
+ <tr
245
+ key={row.lotId}
246
+ className="hover:bg-gray-50 transition-colors cursor-pointer"
247
+ onClick={() => {
248
+ setSelectedMirchiId(row.lotId);
249
+ setSelectedMirchiName(row.lotNumber);
250
+ setViewMode('detail');
251
+ }}
252
+ >
253
+ <td className="px-6 py-4 font-mono text-sm font-medium text-teal-700">
254
+ {row.lotNumber}
255
+ </td>
256
+ <td className="px-6 py-4">
257
+ {row.mirchiName}
258
+ </td>
259
+ <td className="px-6 py-4 text-right">
260
+ <span className="font-medium text-gray-700">
261
+ {row.totalBags} bags
262
+ </span>
263
+ </td>
264
+ <td className="px-6 py-4 text-right">
265
+ <span
266
+ className={`font-bold ${isLow ? 'text-red-500' : 'text-gray-800'
267
+ }`}
268
+ >
269
+ {row.remainingQty} kg
270
+ </span>
271
+ </td>
272
+ <td className="px-6 py-4 text-center">
273
+ <span
274
+ className={`inline-block px-2 py-1 rounded-full text-xs font-semibold ${statusClasses}`}
275
+ >
276
+ {statusLabel}
277
+ </span>
278
+ </td>
279
+ </tr>
280
+ );
281
+ })
282
+ )}
283
+ </tbody>
284
+ </table>
285
+ </div>
286
+
287
+ {/* Mobile cards */}
288
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
289
+ {aggregatedByLot.length === 0 ? (
290
+ <div className="p-6 text-center text-gray-500 text-sm">
291
+ No active stock found
292
+ </div>
293
+ ) : (
294
+ aggregatedByLot.map((row) => {
295
+ const isLow = row.totalBags > 0 && row.totalBags <= lowStockBagsThreshold;
296
+ const statusLabel =
297
+ row.remainingQty === 0
298
+ ? 'Stockout'
299
+ : isLow
300
+ ? 'Low Stock'
301
+ : 'Available';
302
+ const statusClasses =
303
+ row.remainingQty === 0
304
+ ? 'bg-red-100 text-red-700'
305
+ : isLow
306
+ ? 'bg-orange-100 text-orange-700'
307
+ : 'bg-green-100 text-green-700';
308
+
309
+ return (
310
+ <button
311
+ key={row.lotId}
312
+ className="text-left p-4 space-y-2 hover:bg-gray-50 transition-colors"
313
+ onClick={() => {
314
+ setSelectedMirchiId(row.lotId);
315
+ setSelectedMirchiName(row.lotNumber);
316
+ setViewMode('detail');
317
+ }}
318
+ >
319
+ <div className="flex items-center justify-between gap-2">
320
+ <div>
321
+ <div className="text-sm font-mono font-semibold text-teal-700">
322
+ {row.lotNumber}
323
+ </div>
324
+ <div className="text-xs text-gray-500">
325
+ {row.mirchiName}
326
+ </div>
327
+ </div>
328
+ <span
329
+ className={`px-2 py-1 rounded-full text-[10px] font-semibold ${statusClasses}`}
330
+ >
331
+ {statusLabel}
332
+ </span>
333
+ </div>
334
+ <div className="grid grid-cols-2 gap-2 text-xs">
335
+ <div className="bg-gray-50 p-2 rounded">
336
+ <div className="text-gray-500">Bags</div>
337
+ <div className="font-medium">{row.totalBags} bags</div>
338
+ </div>
339
+ <div className="bg-gray-50 p-2 rounded">
340
+ <div className="text-gray-500">Remaining Qty</div>
341
+ <div
342
+ className={`font-bold ${isLow ? 'text-red-500' : 'text-gray-800'
343
+ }`}
344
+ >
345
+ {row.remainingQty} kg
346
+ </div>
347
+ </div>
348
+ </div>
349
+ </button>
350
+ );
351
+ })
352
+ )}
353
+ </div>
354
+ </>
355
+ ) : (
356
+ <>
357
+ {/* Desktop detail table */}
358
+ <div className="hidden md:block overflow-x-auto">
359
+ <table className="w-full text-sm text-left">
360
+ <thead className="bg-gray-50 text-gray-500 font-medium">
361
+ <tr>
362
+ <th className="px-6 py-4">तारीख (Date)</th>
363
+ <th className="px-6 py-4">बिल नंबर (Bill No)</th>
364
+ <th className="px-6 py-4">पार्टी (Party)</th>
365
+ <th className="px-6 py-4 text-right">बॅग इन (In Bags)</th>
366
+ <th className="px-6 py-4 text-right">बॅग आउट (Out Bags)</th>
367
+ <th className="px-6 py-4 text-right">माल इन (In Qty)</th>
368
+ <th className="px-6 py-4 text-right">माल आउट (Out Qty)</th>
369
+ <th className="px-6 py-4 text-center">टाइप (Type)</th>
370
+ </tr>
371
+ </thead>
372
+ <tbody className="divide-y divide-gray-100">
373
+ {detailMovements.length === 0 ? (
374
+ <tr>
375
+ <td colSpan={8} className="px-6 py-8 text-center text-gray-500">
376
+ No movements found for this Mirchi type.
377
+ </td>
378
+ </tr>
379
+ ) : (
380
+ detailMovements.map((row) => (
381
+ <tr key={row.id} className="hover:bg-gray-50">
382
+ <td className="px-6 py-4 text-gray-600">{row.date}</td>
383
+ <td className="px-6 py-4 font-mono text-gray-500">{row.billNo}</td>
384
+ <td className="px-6 py-4 text-gray-700">{row.partyName}</td>
385
+ <td className="px-6 py-4 text-right text-green-600 font-medium">
386
+ {row.inBags > 0 ? `${row.inBags} bags` : '-'}
387
+ </td>
388
+ <td className="px-6 py-4 text-right text-red-600 font-medium">
389
+ {row.outBags > 0 ? `${row.outBags} bags` : '-'}
390
+ </td>
391
+ <td className="px-6 py-4 text-right text-green-600 font-medium">
392
+ {row.inQty > 0 ? `${row.inQty} kg` : '-'}
393
+ </td>
394
+ <td className="px-6 py-4 text-right text-red-600 font-medium">
395
+ {row.outQty > 0 ? `${row.outQty} kg` : '-'}
396
+ </td>
397
+ <td className="px-6 py-4 text-center">
398
+ <span
399
+ className={`inline-block px-2 py-1 rounded-full text-xs font-semibold ${row.typeLabel.includes('Return')
400
+ ? 'bg-orange-100 text-orange-700'
401
+ : row.typeLabel === 'Purchase'
402
+ ? 'bg-teal-100 text-teal-700'
403
+ : 'bg-blue-100 text-blue-700'
404
+ }`}
405
+ >
406
+ {row.typeLabel}
407
+ </span>
408
+ </td>
409
+ </tr>
410
+ ))
411
+ )}
412
+ </tbody>
413
+ </table>
414
+ </div>
415
+
416
+ {/* Mobile detail cards */}
417
+ <div className="md:hidden flex flex-col divide-y divide-gray-100">
418
+ {detailMovements.length === 0 ? (
419
+ <div className="p-6 text-center text-gray-500 text-sm">
420
+ No movements found for this Mirchi type.
421
+ </div>
422
+ ) : (
423
+ detailMovements.map((row) => (
424
+ <div key={row.id} className="p-4 space-y-2">
425
+ <div className="flex justify-between items-start gap-2">
426
+ <div>
427
+ <div className="text-xs text-gray-500">{row.date}</div>
428
+ <div className="font-mono text-sm font-medium text-gray-800">
429
+ {row.billNo}
430
+ </div>
431
+ <div className="text-xs text-gray-600 mt-1">
432
+ {row.partyName}
433
+ </div>
434
+ </div>
435
+ <span
436
+ className={`px-2 py-1 rounded-full text-[10px] font-semibold ${row.typeLabel.includes('Return')
437
+ ? 'bg-orange-100 text-orange-700'
438
+ : row.typeLabel === 'Purchase'
439
+ ? 'bg-teal-100 text-teal-700'
440
+ : 'bg-blue-100 text-blue-700'
441
+ }`}
442
+ >
443
+ {row.typeLabel}
444
+ </span>
445
+ </div>
446
+ <div className="grid grid-cols-2 gap-2 text-xs pt-2 border-t border-gray-50">
447
+ <div className="bg-gray-50 p-2 rounded">
448
+ <div className="text-gray-500">माल इन (In)</div>
449
+ <div className="font-medium text-green-700">
450
+ {row.inQty > 0 ? `${row.inQty} kg` : '-'}
451
+ </div>
452
+ </div>
453
+ <div className="bg-gray-50 p-2 rounded">
454
+ <div className="text-gray-500">माल आउट (Out)</div>
455
+ <div className="font-medium text-red-600">
456
+ {row.outQty > 0 ? `${row.outQty} kg` : '-'}
457
+ </div>
458
+ </div>
459
+ </div>
460
+ </div>
461
+ ))
462
+ )}
463
+ </div>
464
+ </>
465
+ )}
466
+ </div>
467
+ </div>
468
+ );
469
+ };
470
+
471
+ export default StockReport;
public/icon-192.png ADDED

Git LFS Details

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

Git LFS Details

  • SHA256: df87a32301726f75e84cf90101da2089eae3509a50f0a508f7782dd3b513c3c7
  • Pointer size: 131 Bytes
  • Size of remote file: 360 kB
public/manifest.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Pattanshetty Inventory",
3
+ "short_name": "Pattanshetty",
4
+ "description": "Inventory Management System for Mirchi Trading",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#f9fafb",
8
+ "theme_color": "#0d9488",
9
+ "orientation": "portrait-primary",
10
+ "icons": [
11
+ {
12
+ "src": "/icon-192.png",
13
+ "sizes": "192x192",
14
+ "type": "image/png",
15
+ "purpose": "any maskable"
16
+ },
17
+ {
18
+ "src": "/icon-512.png",
19
+ "sizes": "512x512",
20
+ "type": "image/png",
21
+ "purpose": "any maskable"
22
+ }
23
+ ],
24
+ "categories": [
25
+ "business",
26
+ "productivity"
27
+ ],
28
+ "screenshots": [],
29
+ "shortcuts": [
30
+ {
31
+ "name": "New Purchase Bill",
32
+ "short_name": "Purchase",
33
+ "description": "Create new purchase bill",
34
+ "url": "/awaak",
35
+ "icons": []
36
+ },
37
+ {
38
+ "name": "New Sales Bill",
39
+ "short_name": "Sales",
40
+ "description": "Create new sales bill",
41
+ "url": "/jawaak",
42
+ "icons": []
43
+ },
44
+ {
45
+ "name": "Party Ledger",
46
+ "short_name": "Ledger",
47
+ "description": "View party ledger",
48
+ "url": "/ledger",
49
+ "icons": []
50
+ }
51
+ ]
52
+ }
public/service-worker.js ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const CACHE_NAME = 'pattanshetty-v2';
2
+ const urlsToCache = [
3
+ '/',
4
+ '/index.html',
5
+ ];
6
+
7
+ // Install event - cache resources
8
+ self.addEventListener('install', (event) => {
9
+ console.log('[ServiceWorker] Installing...');
10
+ event.waitUntil(
11
+ caches.open(CACHE_NAME)
12
+ .then((cache) => {
13
+ console.log('[ServiceWorker] Caching app shell');
14
+ return cache.addAll(urlsToCache).catch((err) => {
15
+ console.error('[ServiceWorker] Cache addAll failed:', err);
16
+ });
17
+ })
18
+ .catch((err) => {
19
+ console.error('[ServiceWorker] Cache open failed:', err);
20
+ })
21
+ );
22
+ self.skipWaiting();
23
+ });
24
+
25
+ // Activate event - clean up old caches
26
+ self.addEventListener('activate', (event) => {
27
+ console.log('[ServiceWorker] Activating...');
28
+ event.waitUntil(
29
+ caches.keys().then((cacheNames) => {
30
+ return Promise.all(
31
+ cacheNames.map((cacheName) => {
32
+ if (cacheName !== CACHE_NAME) {
33
+ console.log('[ServiceWorker] Deleting old cache:', cacheName);
34
+ return caches.delete(cacheName);
35
+ }
36
+ })
37
+ );
38
+ }).catch((err) => {
39
+ console.error('[ServiceWorker] Activation failed:', err);
40
+ })
41
+ );
42
+ self.clients.claim();
43
+ });
44
+
45
+ // Fetch event - Network first, fallback to cache
46
+ self.addEventListener('fetch', (event) => {
47
+ // Skip non-GET requests
48
+ if (event.request.method !== 'GET') {
49
+ return;
50
+ }
51
+
52
+ event.respondWith(
53
+ fetch(event.request)
54
+ .then((response) => {
55
+ // Check if valid response
56
+ if (!response || response.status !== 200 || response.type === 'error') {
57
+ return response;
58
+ }
59
+
60
+ // Clone the response
61
+ const responseToCache = response.clone();
62
+
63
+ caches.open(CACHE_NAME)
64
+ .then((cache) => {
65
+ cache.put(event.request, responseToCache);
66
+ })
67
+ .catch((err) => {
68
+ console.error('[ServiceWorker] Cache put failed:', err);
69
+ });
70
+
71
+ return response;
72
+ })
73
+ .catch((err) => {
74
+ console.log('[ServiceWorker] Fetch failed, trying cache:', err);
75
+ // Network failed, try cache
76
+ return caches.match(event.request)
77
+ .then((cachedResponse) => {
78
+ if (cachedResponse) {
79
+ return cachedResponse;
80
+ }
81
+ // Return offline page or error
82
+ return new Response('Offline - No cached version available', {
83
+ status: 503,
84
+ statusText: 'Service Unavailable',
85
+ headers: new Headers({
86
+ 'Content-Type': 'text/plain'
87
+ })
88
+ });
89
+ });
90
+ })
91
+ );
92
+ });
services/db.ts ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Party, MirchiType, Lot, Transaction, PartyType,
3
+ LotStatus, BillType, ApiResponse, PaymentMode
4
+ } from '../types';
5
+
6
+ const API_BASE = 'http://localhost:4000/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 || []).map((p: any) => ({
126
+ ...p,
127
+ amount: parseNumeric(p.amount),
128
+ })),
129
+ };
130
+ };
131
+
132
+ // Parties API
133
+ export const getParties = async (): Promise<Party[]> => {
134
+ try {
135
+ const res = await fetch(`${API_BASE}/parties`);
136
+ if (!res.ok) throw new Error('Failed to load parties');
137
+ const data = await res.json();
138
+ return data.map((p: any) => ({
139
+ ...p,
140
+ current_balance: parseNumeric(p.current_balance)
141
+ }));
142
+ } catch (error) {
143
+ console.error('Error fetching parties:', error);
144
+ return [];
145
+ }
146
+ };
147
+
148
+ export const saveParty = async (party: Party): Promise<ApiResponse<Party>> => {
149
+ try {
150
+ const res = await fetch(`${API_BASE}/parties`, {
151
+ method: 'POST',
152
+ headers: { 'Content-Type': 'application/json' },
153
+ body: JSON.stringify(party),
154
+ });
155
+
156
+ const data = await res.json();
157
+ if (!res.ok || !data.success) {
158
+ return { success: false, message: data?.message || 'Error saving party' };
159
+ }
160
+
161
+ return {
162
+ success: true,
163
+ data: {
164
+ ...data.data,
165
+ current_balance: parseNumeric(data.data.current_balance)
166
+ },
167
+ message: data.message || 'Party saved successfully'
168
+ };
169
+ } catch (e: any) {
170
+ return { success: false, message: e.message || 'Database error' };
171
+ }
172
+ };
173
+
174
+ // Mirchi Types API
175
+ export const apiGetMirchiTypes = async (): Promise<MirchiType[]> => {
176
+ try {
177
+ const res = await fetch(`${API_BASE}/mirchi-types`);
178
+ if (!res.ok) throw new Error('Failed to load mirchi types');
179
+ const data = await res.json();
180
+ return data.map((m: any) => ({
181
+ ...m,
182
+ current_rate: parseNumeric(m.current_rate)
183
+ }));
184
+ } catch (error) {
185
+ console.error('Error fetching mirchi types:', error);
186
+ return [];
187
+ }
188
+ };
189
+
190
+ export const apiSaveMirchiType = async (type: MirchiType): Promise<ApiResponse<MirchiType>> => {
191
+ try {
192
+ const res = await fetch(`${API_BASE}/mirchi-types`, {
193
+ method: 'POST',
194
+ headers: { 'Content-Type': 'application/json' },
195
+ body: JSON.stringify(type),
196
+ });
197
+ const data = await res.json();
198
+ if (!res.ok || !data.success) {
199
+ return { success: false, message: data?.message || 'Error saving type' };
200
+ }
201
+ return {
202
+ success: true,
203
+ data: {
204
+ ...data.data,
205
+ current_rate: parseNumeric(data.data.current_rate)
206
+ },
207
+ message: data.message
208
+ };
209
+ } catch (e: any) {
210
+ return { success: false, message: e.message || 'Error saving type' };
211
+ }
212
+ };
213
+
214
+ // Alias for compatibility
215
+ export const getMirchiTypes = apiGetMirchiTypes;
216
+ export const saveMirchiType = apiSaveMirchiType;
217
+
218
+ // Lots API
219
+ export const getActiveLots = async (): Promise<Lot[]> => {
220
+ try {
221
+ const res = await fetch(`${API_BASE}/lots/active`);
222
+ if (!res.ok) throw new Error('Failed to load active lots');
223
+ const data = await res.json();
224
+ return data.map((l: any) => ({
225
+ ...l,
226
+ total_quantity: parseNumeric(l.total_quantity),
227
+ remaining_quantity: parseNumeric(l.remaining_quantity),
228
+ avg_rate: parseNumeric(l.avg_rate)
229
+ }));
230
+ } catch (error) {
231
+ console.error('Error fetching active lots:', error);
232
+ return [];
233
+ }
234
+ };
235
+
236
+ // Check if lot number is unique
237
+ export const checkLotUnique = async (lotNumber: string): Promise<boolean> => {
238
+ try {
239
+ const res = await fetch(`${API_BASE}/lots/check-unique`, {
240
+ method: 'POST',
241
+ headers: { 'Content-Type': 'application/json' },
242
+ body: JSON.stringify({ lot_number: lotNumber }),
243
+ });
244
+ if (!res.ok) throw new Error('Failed to check lot uniqueness');
245
+ const data = await res.json();
246
+ return !data.exists; // Return true if unique (not exists)
247
+ } catch (error) {
248
+ console.error('Error checking lot uniqueness:', error);
249
+ return false;
250
+ }
251
+ };
252
+
253
+ // Get available lots for a specific mirchi type
254
+ export const getAvailableLotsByMirchi = async (mirchiTypeId: string): Promise<Lot[]> => {
255
+ try {
256
+ const res = await fetch(`${API_BASE}/lots/available/${mirchiTypeId}`);
257
+ if (!res.ok) throw new Error('Failed to load available lots');
258
+ const data = await res.json();
259
+ return data.map((l: any) => ({
260
+ ...l,
261
+ total_quantity: parseNumeric(l.total_quantity),
262
+ remaining_quantity: parseNumeric(l.remaining_quantity),
263
+ avg_rate: parseNumeric(l.avg_rate)
264
+ }));
265
+ } catch (error) {
266
+ console.error('Error fetching available lots:', error);
267
+ return [];
268
+ }
269
+ };
270
+
271
+ // Transactions API
272
+ export const getTransactions = async (): Promise<Transaction[]> => {
273
+ try {
274
+ const res = await fetch(`${API_BASE}/transactions`);
275
+ if (!res.ok) throw new Error('Failed to load transactions');
276
+ const data = await res.json();
277
+ return data.map(normalizeTransaction);
278
+ } catch (error) {
279
+ console.error('Error fetching transactions:', error);
280
+ return [];
281
+ }
282
+ };
283
+
284
+ export const generateBillNumber = (type: BillType, isReturn: boolean = false): string => {
285
+ const prefix = type === BillType.JAWAAK ? (isReturn ? 'JAWAAK-RET' : 'JAWAAK') : (isReturn ? 'AWAAK-RET' : 'AWAAK');
286
+ const timestamp = Date.now();
287
+ return `${prefix}-${new Date().getFullYear()}-${String(timestamp).slice(-4)}`;
288
+ };
289
+
290
+ export const saveTransaction = async (transaction: Transaction): Promise<ApiResponse<Transaction>> => {
291
+ try {
292
+ // Validate Payload
293
+ if (!transaction.party_id) return { success: false, message: "Party is required" };
294
+ if (!transaction.items || transaction.items.length === 0) return { success: false, message: "At least one item is required" };
295
+
296
+ console.log('=== SAVING TRANSACTION ===');
297
+ console.log('Transaction ID:', transaction.id);
298
+ console.log('Items count:', transaction.items?.length);
299
+ console.log('Items data:', JSON.stringify(transaction.items, null, 2));
300
+
301
+ const res = await fetch(`${API_BASE}/transactions`, {
302
+ method: 'POST',
303
+ headers: { 'Content-Type': 'application/json' },
304
+ body: JSON.stringify(transaction)
305
+ });
306
+
307
+ if (!res.ok) {
308
+ const error = await res.json();
309
+ console.error('Save transaction error:', error);
310
+ throw new Error(error.message || 'Failed to save transaction');
311
+ }
312
+
313
+ const result = await res.json();
314
+ console.log('=== SAVE RESPONSE ===');
315
+ console.log('Response data:', result.data);
316
+ console.log('Response items count:', result.data?.items?.length);
317
+ console.log('Response items:', JSON.stringify(result.data?.items, null, 2));
318
+
319
+ return {
320
+ success: true,
321
+ data: normalizeTransaction(result.data),
322
+ message: result.message
323
+ };
324
+ } catch (error: any) {
325
+ console.error('Error saving transaction:', error);
326
+ return {
327
+ success: false,
328
+ message: error.message || 'Failed to save transaction'
329
+ };
330
+ }
331
+ };
332
+
333
+ export const updateTransactionPayment = async (transactionId: string, amount: number): Promise<ApiResponse<Transaction>> => {
334
+ try {
335
+ // Validate amount
336
+ if (amount <= 0) return { success: false, message: "Amount must be greater than 0" };
337
+
338
+ const res = await fetch(`${API_BASE}/transactions/${transactionId}/payment`, {
339
+ method: 'PATCH',
340
+ headers: { 'Content-Type': 'application/json' },
341
+ body: JSON.stringify({ amount }),
342
+ });
343
+
344
+ const data = await res.json();
345
+ if (!res.ok || !data.success) {
346
+ return { success: false, message: data?.message || 'Error updating payment' };
347
+ }
348
+
349
+ return {
350
+ success: true,
351
+ message: data.message || 'Payment updated successfully'
352
+ };
353
+ } catch (e: any) {
354
+ console.error('Error updating payment:', e);
355
+ return { success: false, message: e.message || 'Error updating payment' };
356
+ }
357
+ };
tsconfig.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "experimentalDecorators": true,
5
+ "useDefineForClassFields": false,
6
+ "module": "ESNext",
7
+ "lib": [
8
+ "ES2022",
9
+ "DOM",
10
+ "DOM.Iterable"
11
+ ],
12
+ "skipLibCheck": true,
13
+ "types": [
14
+ "node"
15
+ ],
16
+ "moduleResolution": "bundler",
17
+ "isolatedModules": true,
18
+ "moduleDetection": "force",
19
+ "allowJs": true,
20
+ "jsx": "react-jsx",
21
+ "paths": {
22
+ "@/*": [
23
+ "./*"
24
+ ]
25
+ },
26
+ "allowImportingTsExtensions": true,
27
+ "noEmit": true
28
+ }
29
+ }
types.ts ADDED
@@ -0,0 +1,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
+ 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
+ }
utils/LedgerPdfGenerator.ts ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import jsPDF from 'jspdf';
2
+ import autoTable from 'jspdf-autotable';
3
+ import { Party, Transaction } from '../types';
4
+
5
+ // --- CONFIGURATION ---
6
+ const BG_COLOR: [number, number, number] = [240, 234, 214]; // Beige
7
+ const BORDER_COLOR: [number, number, number] = [0, 0, 0]; // Black
8
+
9
+ // Helper: Currency Format
10
+ const FORMAT_CURRENCY = (amount: number) => {
11
+ return new Intl.NumberFormat('en-IN', {
12
+ minimumFractionDigits: 2,
13
+ maximumFractionDigits: 2
14
+ }).format(amount);
15
+ };
16
+
17
+ // Helper: Date Format
18
+ const FORMAT_DATE = (dateStr: string) => {
19
+ if (!dateStr) return "";
20
+ const dateObj = new Date(dateStr);
21
+ return `${dateObj.getDate().toString().padStart(2, '0')}/${(dateObj.getMonth() + 1).toString().padStart(2, '0')}/${dateObj.getFullYear().toString().slice(-2)}`;
22
+ };
23
+
24
+ export const generateLedgerPDF = (party: Party, transactions: Transaction[]) => {
25
+ const doc = new jsPDF();
26
+
27
+ // --- 1. PAGE CONFIGURATION ---
28
+ const PAGE_WIDTH = 210;
29
+ const PAGE_HEIGHT = 297;
30
+ const MARGIN = 14;
31
+ const CONTENT_WIDTH = PAGE_WIDTH - (MARGIN * 2);
32
+
33
+ // --- 2. SETUP & BACKGROUND ---
34
+ doc.setFillColor(...BG_COLOR);
35
+ doc.rect(0, 0, PAGE_WIDTH, PAGE_HEIGHT, 'F');
36
+
37
+ // --- 3. DATA PREPARATION ---
38
+ const sortedTxs = [...transactions].sort((a, b) =>
39
+ new Date(a.bill_date).getTime() - new Date(b.bill_date).getTime()
40
+ );
41
+
42
+ let runningBalance = 0;
43
+
44
+ const tableRows = sortedTxs.flatMap(tx => {
45
+ const rows = [];
46
+ const dateStr = FORMAT_DATE(tx.bill_date);
47
+
48
+ // --- DETERMINE TYPE (AWAAK vs JAWAAK) ---
49
+ // AWAAK = Purchase (We Owe - Credit)
50
+ // JAWAAK = Sales (They Owe - Debit)
51
+ const typeStr = String(tx.bill_type).toUpperCase();
52
+ const numStr = tx.bill_number ? tx.bill_number.toUpperCase() : "";
53
+
54
+ const isAwaak = typeStr === 'AWAAK' || numStr.includes('AWAAK');
55
+
56
+ // --- A. BILL ROW ---
57
+ // 1. Particulars Construction
58
+ let itemDetails = "";
59
+ if (tx.items && tx.items.length > 0) {
60
+ tx.items.forEach(item => {
61
+ let wStr = "";
62
+ if (Array.isArray(item.poti_weights)) wStr = item.poti_weights.join(",");
63
+ else if (item.poti_weights) wStr = String(item.poti_weights);
64
+
65
+ const prefix = itemDetails ? "\n" : "";
66
+ itemDetails += `${prefix}${item.mirchi_name}`;
67
+ if (wStr) itemDetails += ` (${wStr})`;
68
+ });
69
+ }
70
+
71
+ let billParticulars = `Bill No: ${tx.bill_number}`;
72
+ if (tx.is_return) billParticulars += " (RETURN)";
73
+ if (itemDetails) billParticulars += `\n${itemDetails}`;
74
+
75
+ let totalPoti = 0;
76
+ tx.items?.forEach(i => totalPoti += Number(i.poti_count) || 0);
77
+
78
+ // 2. FINANCIAL LOGIC (THE FIX)
79
+ let billDebit = 0;
80
+ let billCredit = 0;
81
+
82
+ if (isAwaak) {
83
+ // AWAAK (Purchase):
84
+ // - Normal Bill: CREDIT (Liability increases)
85
+ // - Return Bill: DEBIT (Liability decreases)
86
+ if (tx.is_return) billDebit = tx.total_amount;
87
+ else billCredit = tx.total_amount;
88
+ } else {
89
+ // JAWAAK (Sales):
90
+ // - Normal Bill: DEBIT (Asset increases - They owe us)
91
+ // - Return Bill: CREDIT (Asset decreases)
92
+ if (tx.is_return) billCredit = tx.total_amount;
93
+ else billDebit = tx.total_amount;
94
+ }
95
+
96
+ // Running Balance = Old + Debit - Credit
97
+ runningBalance = runningBalance + billDebit - billCredit;
98
+
99
+ // Push Bill Row
100
+ rows.push({
101
+ date: dateStr,
102
+ particulars: billParticulars,
103
+ poti: totalPoti > 0 ? totalPoti.toString() : "-",
104
+ credit: billCredit > 0 ? FORMAT_CURRENCY(billCredit) : "-",
105
+ debit: billDebit > 0 ? FORMAT_CURRENCY(billDebit) : "-",
106
+ balance: `${FORMAT_CURRENCY(Math.abs(runningBalance))} ${runningBalance >= 0 ? 'Dr' : 'Cr'}`,
107
+ isMainRow: true
108
+ });
109
+
110
+ // --- B. PAYMENT ROWS ---
111
+ if (tx.payments && tx.payments.length > 0) {
112
+ const validPayments = tx.payments.filter(p => p.mode.toLowerCase() !== 'due');
113
+
114
+ validPayments.forEach(p => {
115
+ let payDebit = 0;
116
+ let payCredit = 0;
117
+
118
+ if (isAwaak) {
119
+ // AWAAK (Purchase) Payment:
120
+ // - We Pay Money: DEBIT (Liability decreases)
121
+ // - Refund (Return): CREDIT
122
+ if (tx.is_return) payCredit = p.amount;
123
+ else payDebit = p.amount;
124
+ } else {
125
+ // JAWAAK (Sales) Payment:
126
+ // - They Pay Money: CREDIT (Asset decreases - Debt paid off)
127
+ // - Refund (Return): DEBIT
128
+ if (tx.is_return) payDebit = p.amount;
129
+ else payCredit = p.amount;
130
+ }
131
+
132
+ runningBalance = runningBalance + payDebit - payCredit;
133
+
134
+ // Description
135
+ const modeStr = p.mode.charAt(0).toUpperCase() + p.mode.slice(1);
136
+ let payDesc = ` [${modeStr}]`;
137
+ if (p.reference) payDesc += ` ${p.reference}`;
138
+
139
+ rows.push({
140
+ date: dateStr,
141
+ particulars: payDesc,
142
+ poti: "-",
143
+ credit: payCredit > 0 ? FORMAT_CURRENCY(payCredit) : "-",
144
+ debit: payDebit > 0 ? FORMAT_CURRENCY(payDebit) : "-",
145
+ balance: `${FORMAT_CURRENCY(Math.abs(runningBalance))} ${runningBalance >= 0 ? 'Dr' : 'Cr'}`,
146
+ isMainRow: false
147
+ });
148
+ });
149
+ }
150
+ return rows;
151
+ });
152
+
153
+ // --- 4. GENERATE TABLE ---
154
+ autoTable(doc, {
155
+ startY: 35,
156
+ tableWidth: CONTENT_WIDTH,
157
+ margin: { left: MARGIN, right: MARGIN },
158
+
159
+ head: [[
160
+ 'Date',
161
+ 'Particulars',
162
+ 'Poti',
163
+ 'Credit\n(Jama)',
164
+ 'Debit\n(Nave)',
165
+ 'Balance'
166
+ ]],
167
+
168
+ body: tableRows.map(r => [r.date, r.particulars, r.poti, r.credit, r.debit, r.balance]),
169
+
170
+ theme: 'grid',
171
+
172
+ styles: {
173
+ fillColor: false,
174
+ textColor: 0,
175
+ lineColor: 0,
176
+ lineWidth: 0.1,
177
+ font: 'helvetica',
178
+ fontSize: 9,
179
+ valign: 'top',
180
+ cellPadding: 3,
181
+ overflow: 'linebreak'
182
+ },
183
+
184
+ headStyles: {
185
+ fillColor: false,
186
+ textColor: 0,
187
+ fontStyle: 'bold',
188
+ halign: 'center',
189
+ valign: 'middle',
190
+ lineWidth: 0.1,
191
+ },
192
+
193
+ columnStyles: {
194
+ 0: { cellWidth: 20, halign: 'center' }, // Date
195
+ 1: { cellWidth: 72, halign: 'left' }, // Particulars
196
+ 2: { cellWidth: 12, halign: 'center' }, // Poti
197
+ 3: { cellWidth: 26, halign: 'right' }, // Credit
198
+ 4: { cellWidth: 26, halign: 'right' }, // Debit
199
+ 5: { cellWidth: 26, halign: 'right', fontStyle: 'bold' } // Balance
200
+ },
201
+
202
+ didParseCell: (data) => {
203
+ const rowIdx = data.row.index;
204
+ const rowData = tableRows[rowIdx];
205
+ if (data.section === 'body' && data.column.index === 1 && rowData?.isMainRow) {
206
+ data.cell.styles.fontStyle = 'bold';
207
+ }
208
+ },
209
+
210
+ didDrawPage: (data) => {
211
+ // Border
212
+ doc.setDrawColor(...BORDER_COLOR);
213
+ doc.setLineWidth(0.4);
214
+ doc.rect(MARGIN, MARGIN, CONTENT_WIDTH, PAGE_HEIGHT - (MARGIN * 2));
215
+
216
+ // Header
217
+ if (data.pageNumber === 1) {
218
+ const startX = MARGIN + 4;
219
+ const startY = 25;
220
+
221
+ doc.setFontSize(14);
222
+ doc.setFont("helvetica", "bold");
223
+ doc.text("Name of A/c :", startX, startY);
224
+
225
+ const labelWidth = doc.getTextWidth("Name of A/c : ");
226
+ doc.text(party.name, startX + labelWidth, startY);
227
+
228
+ if (party.city || party.phone) {
229
+ doc.setFontSize(9);
230
+ doc.setFont("helvetica", "normal");
231
+ const subText = [party.city, party.phone].filter(Boolean).join(" - ");
232
+ doc.text(subText, startX, startY + 6);
233
+ }
234
+ }
235
+ }
236
+ });
237
+
238
+ const cleanName = party.name.replace(/[^a-zA-Z0-9]/g, '_');
239
+ doc.save(`${cleanName}_Ledger.pdf`);
240
+ };
utils/dateFormatter.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Date formatting utility for consistent date display across the app
2
+
3
+ /**
4
+ * Formats a date string to DD/MM/YYYY format
5
+ * @param dateStr - Date string in any format
6
+ * @returns Formatted date string (DD/MM/YYYY)
7
+ */
8
+ export const formatDate = (dateStr: string | Date): string => {
9
+ const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr;
10
+
11
+ if (isNaN(date.getTime())) {
12
+ return '-';
13
+ }
14
+
15
+ return date.toLocaleDateString('en-IN', {
16
+ day: '2-digit',
17
+ month: '2-digit',
18
+ year: 'numeric'
19
+ });
20
+ };
21
+
22
+ /**
23
+ * Formats a date string to YYYY-MM-DD format (for input fields)
24
+ * @param dateStr - Date string in any format
25
+ * @returns Formatted date string (YYYY-MM-DD)
26
+ */
27
+ export const formatDateForInput = (dateStr: string | Date): string => {
28
+ const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr;
29
+
30
+ if (isNaN(date.getTime())) {
31
+ return '';
32
+ }
33
+
34
+ const year = date.getFullYear();
35
+ const month = String(date.getMonth() + 1).padStart(2, '0');
36
+ const day = String(date.getDate()).padStart(2, '0');
37
+
38
+ return `${year}-${month}-${day}`;
39
+ };
40
+
41
+ /**
42
+ * Gets today's date in YYYY-MM-DD format
43
+ * @returns Today's date string
44
+ */
45
+ export const getTodayDate = (): string => {
46
+ return formatDateForInput(new Date());
47
+ };
48
+
49
+ /**
50
+ * Formats a date to display only date (no time)
51
+ * @param dateStr - Date string
52
+ * @returns Formatted date string
53
+ */
54
+ export const formatDateOnly = (dateStr: string | Date): string => {
55
+ return formatDate(dateStr);
56
+ };
utils/exportToExcel.ts ADDED
@@ -0,0 +1,430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import XLSX from 'xlsx-js-style';
2
+ import { Transaction, Party, BillType } from '../types';
3
+
4
+ export interface AnalysisExportRow {
5
+ lotNumber: string;
6
+ mirchiName: string;
7
+ purchaseQty: number;
8
+ purchaseValue: number;
9
+ avgPurchaseRate: number;
10
+ soldQty: number;
11
+ salesValue: number;
12
+ avgSaleRate: number;
13
+ realizedProfit: number;
14
+ marginPercent: number;
15
+ remainingQty: number;
16
+ remainingValueAtCost: number;
17
+ }
18
+
19
+ export interface LotOverviewExportRow {
20
+ lotNumber: string;
21
+ mirchiName: string;
22
+ purchaseQty: number;
23
+ purchaseValue: number;
24
+ avgPurchaseRate: number;
25
+ soldQty: number;
26
+ salesValue: number;
27
+ avgSaleRate: number;
28
+ realizedProfit: number;
29
+ marginPercent: number;
30
+ remainingQty: number;
31
+ remainingValueAtCost: number;
32
+ }
33
+
34
+ export interface LotDetailExportRow {
35
+ date: string;
36
+ billNo: string;
37
+ typeLabel: string;
38
+ directionLabel: string;
39
+ qtyKg: number;
40
+ ratePerKg: number;
41
+ grossAmount: number;
42
+ expensesAllocated: number;
43
+ netAmount: number;
44
+ profitImpact: number;
45
+ }
46
+
47
+ // ==========================================
48
+ // 2. STYLING CONFIGURATION
49
+ // ==========================================
50
+
51
+ // Common Colors
52
+ const CLR_BORDER = { rgb: "D1D5DB" }; // Gray-300
53
+ const CLR_TEXT = { rgb: "374151" }; // Gray-700
54
+ const CLR_HEAD_BG = { rgb: "F3F4F6" }; // Gray-100
55
+ const CLR_GREEN = { rgb: "059669" };
56
+ const CLR_RED = { rgb: "DC2626" };
57
+ const CLR_TEAL = { rgb: "0D9488" };
58
+ const CLR_WHITE = { rgb: "FFFFFF" };
59
+
60
+ // Border Definitions
61
+ const BORDER_ALL = {
62
+ top: { style: 'thin', color: CLR_BORDER },
63
+ bottom: { style: 'thin', color: CLR_BORDER },
64
+ left: { style: 'thin', color: CLR_BORDER },
65
+ right: { style: 'thin', color: CLR_BORDER }
66
+ };
67
+
68
+ const BORDER_TOP_SIDE = {
69
+ top: { style: 'thin', color: { rgb: "9CA3AF" } }, // Darker Gray for Card
70
+ left: { style: 'thin', color: { rgb: "9CA3AF" } },
71
+ right: { style: 'thin', color: { rgb: "9CA3AF" } }
72
+ };
73
+
74
+ const BORDER_BOT_SIDE = {
75
+ bottom: { style: 'thin', color: { rgb: "9CA3AF" } },
76
+ left: { style: 'thin', color: { rgb: "9CA3AF" } },
77
+ right: { style: 'thin', color: { rgb: "9CA3AF" } }
78
+ };
79
+
80
+ // Style Objects
81
+ const STYLES = {
82
+ TITLE: {
83
+ font: { bold: true, sz: 14, color: { rgb: "111827" } },
84
+ alignment: { horizontal: 'left', vertical: 'center' }
85
+ },
86
+
87
+ // Card Styles
88
+ CARD_HEADER: {
89
+ border: BORDER_TOP_SIDE,
90
+ fill: { fgColor: CLR_WHITE },
91
+ font: { sz: 10, color: { rgb: "6B7280" } },
92
+ alignment: { vertical: 'bottom', horizontal: 'left', indent: 1 }
93
+ },
94
+ CARD_VALUE: {
95
+ border: BORDER_BOT_SIDE,
96
+ fill: { fgColor: CLR_WHITE },
97
+ font: { bold: true, sz: 11, color: { rgb: "1F2937" } },
98
+ alignment: { vertical: 'top', horizontal: 'left', indent: 1 }
99
+ },
100
+
101
+ // Table Styles
102
+ TABLE_HEAD: {
103
+ fill: { fgColor: CLR_HEAD_BG },
104
+ font: { bold: true, color: CLR_TEXT },
105
+ border: BORDER_ALL,
106
+ alignment: { horizontal: 'center', vertical: 'center' }
107
+ },
108
+ CELL_TEXT: {
109
+ border: BORDER_ALL,
110
+ alignment: { vertical: 'center', horizontal: 'left' },
111
+ font: { color: CLR_TEXT }
112
+ },
113
+ CELL_NUM: {
114
+ border: BORDER_ALL,
115
+ alignment: { vertical: 'center', horizontal: 'right' },
116
+ font: { color: { rgb: "111827" } }
117
+ },
118
+ CELL_MONO: {
119
+ border: BORDER_ALL,
120
+ alignment: { vertical: 'center', horizontal: 'left' },
121
+ font: { name: 'Courier New', sz: 10, color: CLR_TEXT }
122
+ },
123
+
124
+ // Text Colors
125
+ TXT_TEAL: { font: { bold: true, sz: 12, color: CLR_TEAL } },
126
+ TXT_GRAY: { font: { color: { rgb: "6B7280" } } },
127
+ TXT_GREEN: { font: { bold: true, color: CLR_GREEN } },
128
+ TXT_RED: { font: { bold: true, color: CLR_RED } }
129
+ };
130
+
131
+ // Formats
132
+ const FMT_CURRENCY = "₹#,##0.00"; // 2 decimal places for clean alignment
133
+ const FMT_QTY = "0.00 \"kg\"";
134
+
135
+ // ==========================================
136
+ // 3. HELPER FUNCTIONS
137
+ // ==========================================
138
+
139
+ // Helper to create cell.
140
+ // Uses empty string "" if value is null to avoid "null" text in Excel.
141
+ const c = (val: any, s: any = {}, fmt?: string) => {
142
+ return {
143
+ v: val === null || val === undefined ? "" : val,
144
+ s: { ...s, numFmt: fmt }, // Inject format into style for xlsx-js-style
145
+ z: fmt // Inject format into standard key for compatibility
146
+ };
147
+ };
148
+
149
+ const formatIndCurrency = (val: number) => {
150
+ return val.toLocaleString('en-IN', {
151
+ maximumFractionDigits: 0, // Keep summary cards clean (no decimals)
152
+ style: 'currency',
153
+ currency: 'INR'
154
+ }).replace('₹', '₹'); // Ensure symbol consistency
155
+ };
156
+
157
+ // ==========================================
158
+ // 4. EXPORT FUNCTIONS
159
+ // ==========================================
160
+
161
+ export const exportLotDetailAnalysis = (
162
+ lot: LotOverviewExportRow,
163
+ rows: LotDetailExportRow[],
164
+ ) => {
165
+ const data: any[][] = [];
166
+
167
+ // --- ROW 1: Main Title ---
168
+ data.push([
169
+ c(`${lot.lotNumber} - ${lot.mirchiName} Analysis`, STYLES.TITLE),
170
+ c(""), c(""), c(""), c(""), c(""), c(""), c("")
171
+ ]);
172
+
173
+ // --- ROW 2: Spacer ---
174
+ data.push([c("")]);
175
+
176
+ // --- ROW 3: Lot Info (Left) & Remaining (Right) ---
177
+ // Note: We used c("") for spacers to ensure no "null" text
178
+ data.push([
179
+ c(lot.lotNumber, { ...STYLES.TXT_TEAL, alignment: { vertical: 'bottom' } }),
180
+ c(""), c(""), c(""), c(""), c(""),
181
+ c("Remaining", { ...STYLES.TXT_GRAY, alignment: { horizontal: 'right', vertical: 'bottom' } }),
182
+ c(Number(lot.remainingQty.toFixed(2)), { font: { bold: true }, alignment: { horizontal: 'right', vertical: 'bottom' } }, FMT_QTY)
183
+ ]);
184
+
185
+ // --- ROW 4: Mirchi Name ---
186
+ data.push([
187
+ c(lot.mirchiName, STYLES.TXT_GRAY),
188
+ c(""), c(""), c(""), c(""), c(""), c(""), c("")
189
+ ]);
190
+
191
+ // --- ROW 5: Spacer ---
192
+ data.push([c("")]);
193
+
194
+ // --- ROW 6: CARD HEADERS ---
195
+ data.push([
196
+ c("Purchase", STYLES.CARD_HEADER), c(""), c(""), c(""),
197
+ c("Sales", STYLES.CARD_HEADER), c(""), c(""), c("")
198
+ ]);
199
+
200
+ // --- ROW 7: CARD VALUES ---
201
+ // Format text manually for the card to ensure it looks perfect
202
+ const buyTxt = `${lot.purchaseQty.toFixed(2)} kg • ${formatIndCurrency(lot.purchaseValue)}`;
203
+ const sellTxt = `${lot.soldQty.toFixed(2)} kg • ${formatIndCurrency(lot.salesValue)}`;
204
+
205
+ data.push([
206
+ c(buyTxt, STYLES.CARD_VALUE), c(""), c(""), c(""),
207
+ c(sellTxt, STYLES.CARD_VALUE), c(""), c(""), c("")
208
+ ]);
209
+
210
+ // --- ROW 8: Spacer ---
211
+ data.push([c("")]);
212
+
213
+ // --- ROW 9: Avg & Profit Summary ---
214
+ const avgText = `Avg Buy ₹${lot.avgPurchaseRate.toFixed(0)} /kg • Avg Sell ₹${lot.avgSaleRate.toFixed(0)} /kg`;
215
+
216
+ // Profit Logic
217
+ const pVal = Number(lot.realizedProfit.toFixed(2)); // Round to avoid .774
218
+ const pLabel = pVal > 0 ? "Profit" : pVal < 0 ? "Loss" : "Break-even";
219
+ const pStyle = pVal > 0 ? STYLES.TXT_GREEN : pVal < 0 ? STYLES.TXT_RED : STYLES.TXT_GRAY;
220
+
221
+ data.push([
222
+ c(avgText, { font: { color: { rgb: "4B5563" }, italic: true }, alignment: { vertical: 'center' } }),
223
+ c(""), c(""), c(""), c(""), c(""),
224
+ c(pLabel, { ...STYLES.TXT_GRAY, alignment: { horizontal: 'right', vertical: 'center' } }),
225
+ c(pVal, { ...pStyle, alignment: { horizontal: 'right', vertical: 'center' } }, FMT_CURRENCY)
226
+ ]);
227
+
228
+ // --- ROW 10: Spacer ---
229
+ data.push([c("")]);
230
+
231
+ // --- ROW 11: TABLE HEADERS ---
232
+ data.push([
233
+ c('Date', STYLES.TABLE_HEAD),
234
+ c('Bill No', STYLES.TABLE_HEAD),
235
+ c('Type', STYLES.TABLE_HEAD),
236
+ c('Import / Export', STYLES.TABLE_HEAD),
237
+ c('Qty (kg)', STYLES.TABLE_HEAD),
238
+ c('Rate', STYLES.TABLE_HEAD),
239
+ c('Net Amount', STYLES.TABLE_HEAD),
240
+ c('Profit Impact', STYLES.TABLE_HEAD)
241
+ ]);
242
+
243
+ // --- ROW 12+: DATA ---
244
+ if (rows.length === 0) {
245
+ data.push([c('No transactions found', STYLES.CELL_TEXT)]);
246
+ } else {
247
+ rows.forEach(row => {
248
+ // Round numbers to 2 decimals to prevent floating point garbage
249
+ const safeProfit = Number(row.profitImpact.toFixed(2));
250
+ const safeNet = Number(row.netAmount.toFixed(2));
251
+ const safeQty = Number(row.qtyKg.toFixed(2));
252
+ const safeRate = Number(row.ratePerKg.toFixed(2));
253
+
254
+ const impactStyle = safeProfit > 0
255
+ ? { ...STYLES.CELL_NUM, font: { color: CLR_GREEN } }
256
+ : safeProfit < 0
257
+ ? { ...STYLES.CELL_NUM, font: { color: CLR_RED } }
258
+ : STYLES.CELL_NUM;
259
+
260
+ data.push([
261
+ c(row.date, STYLES.CELL_TEXT),
262
+ c(row.billNo, STYLES.CELL_MONO),
263
+ c(row.typeLabel, STYLES.CELL_TEXT),
264
+ c(row.directionLabel, STYLES.CELL_TEXT),
265
+ c(safeQty, STYLES.CELL_NUM, "0.00"),
266
+ c(safeRate, STYLES.CELL_NUM, "0.00"),
267
+ c(safeNet, STYLES.CELL_NUM, FMT_CURRENCY),
268
+ c(safeProfit, impactStyle, FMT_CURRENCY)
269
+ ]);
270
+ });
271
+ }
272
+
273
+ // --- GENERATE ---
274
+ const ws = XLSX.utils.aoa_to_sheet(data);
275
+
276
+ // --- MERGES (0-based indices) ---
277
+ ws['!merges'] = [
278
+ { s: { r: 0, c: 0 }, e: { r: 0, c: 7 } }, // Title
279
+ { s: { r: 2, c: 0 }, e: { r: 2, c: 5 } }, // Lot Number
280
+ { s: { r: 3, c: 0 }, e: { r: 3, c: 5 } }, // Mirchi Name
281
+ // Purchase Card
282
+ { s: { r: 5, c: 0 }, e: { r: 5, c: 3 } },
283
+ { s: { r: 6, c: 0 }, e: { r: 6, c: 3 } },
284
+ // Sales Card
285
+ { s: { r: 5, c: 4 }, e: { r: 5, c: 7 } },
286
+ { s: { r: 6, c: 4 }, e: { r: 6, c: 7 } },
287
+ // Avg Line
288
+ { s: { r: 8, c: 0 }, e: { r: 8, c: 5 } },
289
+ ];
290
+
291
+ // --- COL WIDTHS ---
292
+ ws['!cols'] = [
293
+ { wch: 14 }, // Date
294
+ { wch: 22 }, // Bill No
295
+ { wch: 12 }, // Type
296
+ { wch: 14 }, // Imp/Exp
297
+ { wch: 12 }, // Qty
298
+ { wch: 10 }, // Rate
299
+ { wch: 16 }, // Net Amt
300
+ { wch: 16 }, // Profit
301
+ ];
302
+
303
+ const wb = XLSX.utils.book_new();
304
+ const safeName = lot.lotNumber.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 30);
305
+ XLSX.utils.book_append_sheet(wb, ws, safeName);
306
+ XLSX.writeFile(wb, `Lot_${safeName}_Analysis.xlsx`);
307
+ };
308
+
309
+
310
+ // ==========================================
311
+ // 5. OTHER EXPORT FUNCTIONS (Parties/Ledger)
312
+ // ==========================================
313
+
314
+ export const exportPartyLedger = (party: Party, transactions: Transaction[], dateRange?: { from: string; to: string }) => {
315
+ // Filter & Sort
316
+ const filtered = (dateRange ? transactions.filter(t => {
317
+ const d = new Date(t.bill_date);
318
+ return d >= new Date(dateRange.from) && d <= new Date(dateRange.to);
319
+ }) : [...transactions]).sort((a, b) => new Date(a.bill_date).getTime() - new Date(b.bill_date).getTime());
320
+
321
+ const data: any[][] = [];
322
+ data.push([c(`Ledger: ${party.name}`, STYLES.TITLE)]);
323
+ data.push([c("")]);
324
+ data.push(['Date', 'Particulars', 'Credit', 'Debit', 'Balance', 'Dr/Cr'].map(h => c(h, STYLES.TABLE_HEAD)));
325
+
326
+ let running = 0, tCr = 0, tDr = 0;
327
+ filtered.forEach(tx => {
328
+ let cr = 0, dr = 0;
329
+ if (tx.bill_type === BillType.AWAAK) tx.is_return ? (cr = tx.total_amount) : (dr = tx.total_amount);
330
+ else tx.is_return ? (dr = tx.total_amount) : (cr = tx.total_amount);
331
+
332
+ tCr += cr; tDr += dr; running += (dr - cr);
333
+ const desc = `${tx.bill_number} (${tx.bill_type})` + (tx.items.length ? ` - ${tx.items[0].mirchi_name}` : "");
334
+
335
+ data.push([
336
+ c(new Date(tx.bill_date).toLocaleDateString('en-IN'), STYLES.CELL_TEXT),
337
+ c(desc, STYLES.CELL_TEXT),
338
+ c(cr || "", STYLES.CELL_NUM, FMT_CURRENCY),
339
+ c(dr || "", STYLES.CELL_NUM, FMT_CURRENCY),
340
+ c(Math.abs(running), STYLES.CELL_NUM, FMT_CURRENCY),
341
+ c(running > 0 ? 'Dr' : 'Cr', STYLES.CELL_TEXT)
342
+ ]);
343
+ });
344
+
345
+ const totStyle = { ...STYLES.CELL_NUM, font: { bold: true } };
346
+ data.push([
347
+ c('TOTAL', { ...STYLES.CELL_TEXT, font: { bold: true } }), c(""),
348
+ c(tCr, totStyle, FMT_CURRENCY), c(tDr, totStyle, FMT_CURRENCY),
349
+ c(Math.abs(running), totStyle, FMT_CURRENCY),
350
+ c(running > 0 ? 'Dr' : 'Cr', { ...STYLES.CELL_TEXT, font: { bold: true } })
351
+ ]);
352
+
353
+ const ws = XLSX.utils.aoa_to_sheet(data);
354
+ ws['!merges'] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 5 } }];
355
+ ws['!cols'] = [{ wch: 12 }, { wch: 40 }, { wch: 15 }, { wch: 15 }, { wch: 15 }, { wch: 8 }];
356
+ const wb = XLSX.utils.book_new();
357
+ XLSX.utils.book_append_sheet(wb, ws, 'Ledger');
358
+ XLSX.writeFile(wb, `${party.name.replace(/\s/g,'_')}_Ledger.xlsx`);
359
+ };
360
+
361
+ export const exportAllParties = (parties: Party[]) => {
362
+ const data: any[][] = [];
363
+ data.push(['Party Name', 'Phone', 'City', 'Type', 'Balance', 'Status'].map(h => c(h, STYLES.TABLE_HEAD)));
364
+ let tRec = 0, tPay = 0;
365
+
366
+ parties.forEach(p => {
367
+ if(p.current_balance > 0) tRec += p.current_balance;
368
+ if(p.current_balance < 0) tPay += Math.abs(p.current_balance);
369
+ const color = p.current_balance > 0 ? CLR_GREEN : p.current_balance < 0 ? CLR_RED : undefined;
370
+
371
+ data.push([
372
+ c(p.name, STYLES.CELL_TEXT), c(p.phone||'-', STYLES.CELL_TEXT), c(p.city||'-', STYLES.CELL_TEXT),
373
+ c(p.party_type, STYLES.CELL_TEXT),
374
+ c(p.current_balance, { ...STYLES.CELL_NUM, font: { color } }, FMT_CURRENCY),
375
+ c(p.current_balance > 0 ? 'Rec' : p.current_balance < 0 ? 'Pay' : '-', STYLES.CELL_TEXT)
376
+ ]);
377
+ });
378
+
379
+ data.push([c("")]); data.push([c('SUMMARY', { font: { bold: true } })]);
380
+ data.push([c('Total Receivable'), c(""), c(""), c(""), c(tRec, STYLES.CELL_NUM, FMT_CURRENCY)]);
381
+ data.push([c('Total Payable'), c(""), c(""), c(""), c(tPay, STYLES.CELL_NUM, FMT_CURRENCY)]);
382
+
383
+ const ws = XLSX.utils.aoa_to_sheet(data);
384
+ ws['!cols'] = [{ wch: 30 }, { wch: 15 }, { wch: 20 }, { wch: 10 }, { wch: 18 }, { wch: 10 }];
385
+ const wb = XLSX.utils.book_new();
386
+ XLSX.utils.book_append_sheet(wb, ws, 'Parties');
387
+ XLSX.writeFile(wb, `All_Parties.xlsx`);
388
+ };
389
+
390
+ export const exportAllTransactions = (transactions: Transaction[]) => {
391
+ const data: any[][] = [];
392
+ data.push(['Date', 'Bill No', 'Party', 'Type', 'Amount', 'Paid', 'Balance'].map(h => c(h, STYLES.TABLE_HEAD)));
393
+ transactions.forEach(tx => {
394
+ data.push([
395
+ c(new Date(tx.bill_date).toLocaleDateString('en-IN'), STYLES.CELL_TEXT),
396
+ c(tx.bill_number, STYLES.CELL_MONO), c(tx.party_name, STYLES.CELL_TEXT), c(tx.bill_type, STYLES.CELL_TEXT),
397
+ c(tx.total_amount, STYLES.CELL_NUM, FMT_CURRENCY),
398
+ c(tx.paid_amount, STYLES.CELL_NUM, FMT_CURRENCY),
399
+ c(tx.balance_amount, STYLES.CELL_NUM, FMT_CURRENCY)
400
+ ]);
401
+ });
402
+ const ws = XLSX.utils.aoa_to_sheet(data);
403
+ ws['!cols'] = [{ wch: 12 }, { wch: 20 }, { wch: 25 }, { wch: 10 }, { wch: 15 }, { wch: 15 }, { wch: 15 }];
404
+ const wb = XLSX.utils.book_new();
405
+ XLSX.utils.book_append_sheet(wb, ws, 'Transactions');
406
+ XLSX.writeFile(wb, `All_Transactions.xlsx`);
407
+ };
408
+
409
+ export const exportAnalysisReport = (rows: AnalysisExportRow[]) => {
410
+ if (!rows.length) return;
411
+ const data: any[][] = [];
412
+ const headers = ['LOT','Mirchi','Buy Qty','Buy Val','Avg Buy','Sell Qty','Sell Val','Avg Sell','Rem Qty','Rem Val','Profit','Margin %'];
413
+ data.push(headers.map(h => c(h, STYLES.TABLE_HEAD)));
414
+
415
+ rows.forEach(r => {
416
+ const pStyle = r.realizedProfit > 0 ? { ...STYLES.CELL_NUM, font: { color: CLR_GREEN } } : r.realizedProfit < 0 ? { ...STYLES.CELL_NUM, font: { color: CLR_RED } } : STYLES.CELL_NUM;
417
+ data.push([
418
+ c(r.lotNumber, STYLES.CELL_TEXT), c(r.mirchiName, STYLES.CELL_TEXT),
419
+ c(r.purchaseQty, STYLES.CELL_NUM,"0.00"), c(r.purchaseValue, STYLES.CELL_NUM,FMT_CURRENCY), c(r.avgPurchaseRate, STYLES.CELL_NUM,FMT_CURRENCY),
420
+ c(r.soldQty, STYLES.CELL_NUM,"0.00"), c(r.salesValue, STYLES.CELL_NUM,FMT_CURRENCY), c(r.avgSaleRate, STYLES.CELL_NUM,FMT_CURRENCY),
421
+ c(r.remainingQty, STYLES.CELL_NUM,"0.00"), c(r.remainingValueAtCost, STYLES.CELL_NUM,FMT_CURRENCY),
422
+ c(r.realizedProfit, pStyle,FMT_CURRENCY), c(r.marginPercent/100, STYLES.CELL_NUM,"0.00%")
423
+ ]);
424
+ });
425
+ const ws = XLSX.utils.aoa_to_sheet(data);
426
+ ws['!cols'] = headers.map(() => ({ wch: 14 }));
427
+ const wb = XLSX.utils.book_new();
428
+ XLSX.utils.book_append_sheet(wb, ws, 'Summary');
429
+ XLSX.writeFile(wb, `Analysis_Summary.xlsx`);
430
+ };
vite.config.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ host: true, // Listen on all addresses
8
+ port: 3000
9
+ },
10
+ preview: {
11
+ host: true, // Listen on all addresses
12
+ port: 7860,
13
+ strictPort: true,
14
+ allowedHosts: ['antaram-pratikpattanshetty.hf.space'] // Add this line
15
+ }
16
+ })