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