Spaces:
Sleeping
Sleeping
Upload 49 files
Browse files- .dockerignore +31 -0
- .env +4 -0
- .env.local +1 -0
- .firebaserc +5 -0
- .gitattributes +39 -35
- .gitignore +24 -0
- App.tsx +51 -0
- DEPLOYMENT.md +38 -0
- Dockerfile +21 -0
- components/Layout.tsx +124 -0
- components/PWAInstallPrompt.tsx +98 -0
- components/PdfInvoice.tsx +219 -0
- components/PrintInvoice.tsx +315 -0
- context/PWAContext.tsx +83 -0
- dist/404.html +33 -0
- dist/assets/html2canvas.esm-QH1iLAAe.js +0 -0
- dist/assets/index-DEfz7c9M.js +0 -0
- dist/assets/index.es-qlPCKdii.js +0 -0
- dist/assets/purify.es-B6FQ9oRL.js +2 -0
- dist/icon-192.png +3 -0
- dist/icon-512.png +3 -0
- dist/index.html +96 -0
- dist/manifest.json +52 -0
- dist/service-worker.js +92 -0
- firebase.json +10 -0
- index.html +95 -0
- index.tsx +15 -0
- metadata.json +5 -0
- nginx.conf +27 -0
- package-lock.json +0 -0
- package.json +29 -0
- pages/Analysis.tsx +836 -0
- pages/AwaakBill.tsx +897 -0
- pages/Dashboard.tsx +445 -0
- pages/JawaakBill.tsx +807 -0
- pages/PartyLedger.tsx +864 -0
- pages/Settings.tsx +355 -0
- pages/StockReport.tsx +471 -0
- public/icon-192.png +3 -0
- public/icon-512.png +3 -0
- public/manifest.json +52 -0
- public/service-worker.js +92 -0
- services/db.ts +357 -0
- tsconfig.json +29 -0
- types.ts +123 -0
- utils/LedgerPdfGenerator.ts +240 -0
- utils/dateFormatter.ts +56 -0
- utils/exportToExcel.ts +430 -0
- vite.config.ts +16 -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
|
.firebaserc
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"projects": {
|
| 3 |
+
"default": "pattanshetti-03-12"
|
| 4 |
+
}
|
| 5 |
+
}
|
.gitattributes
CHANGED
|
@@ -1,35 +1,39 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
public/icon-192.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
public/icon-512.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
dist/icon-192.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
dist/icon-512.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
App.tsx
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect } from 'react';
|
| 2 |
+
import { HashRouter, Routes, Route, Navigate } from 'react-router-dom';
|
| 3 |
+
import Layout from './components/Layout';
|
| 4 |
+
import Dashboard from './pages/Dashboard';
|
| 5 |
+
import JawaakBill from './pages/JawaakBill';
|
| 6 |
+
import AwaakBill from './pages/AwaakBill';
|
| 7 |
+
import StockReport from './pages/StockReport';
|
| 8 |
+
import PartyLedger from './pages/PartyLedger';
|
| 9 |
+
import Settings from './pages/Settings';
|
| 10 |
+
import Analysis from './pages/Analysis';
|
| 11 |
+
import PWAInstallPrompt from './components/PWAInstallPrompt';
|
| 12 |
+
import { PWAProvider } from './context/PWAContext';
|
| 13 |
+
|
| 14 |
+
const App = () => {
|
| 15 |
+
// Register Service Worker for PWA
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
if ('serviceWorker' in navigator) {
|
| 18 |
+
window.addEventListener('load', () => {
|
| 19 |
+
navigator.serviceWorker.register('/service-worker.js')
|
| 20 |
+
.then(registration => {
|
| 21 |
+
console.log('✅ PWA Service Worker registered:', registration);
|
| 22 |
+
})
|
| 23 |
+
.catch(error => {
|
| 24 |
+
console.log('❌ SW registration failed:', error);
|
| 25 |
+
});
|
| 26 |
+
});
|
| 27 |
+
}
|
| 28 |
+
}, []);
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<PWAProvider>
|
| 32 |
+
<HashRouter>
|
| 33 |
+
<Layout>
|
| 34 |
+
<Routes>
|
| 35 |
+
<Route path="/" element={<Dashboard />} />
|
| 36 |
+
<Route path="/jawaak" element={<JawaakBill />} />
|
| 37 |
+
<Route path="/awaak" element={<AwaakBill />} />
|
| 38 |
+
<Route path="/stock" element={<StockReport />} />
|
| 39 |
+
<Route path="/ledger" element={<PartyLedger />} />
|
| 40 |
+
<Route path="/analysis" element={<Analysis />} />
|
| 41 |
+
<Route path="/settings" element={<Settings />} />
|
| 42 |
+
<Route path="*" element={<Navigate to="/" replace />} />
|
| 43 |
+
</Routes>
|
| 44 |
+
</Layout>
|
| 45 |
+
<PWAInstallPrompt />
|
| 46 |
+
</HashRouter>
|
| 47 |
+
</PWAProvider>
|
| 48 |
+
);
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
export default App;
|
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces Deployment
|
| 2 |
+
|
| 3 |
+
## Frontend Dockerfile
|
| 4 |
+
|
| 5 |
+
Simple Dockerfile for deploying the React frontend to Hugging Face Spaces.
|
| 6 |
+
|
| 7 |
+
### Build & Run Locally
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
# Build the Docker image
|
| 11 |
+
docker build -t pattanshetty-frontend .
|
| 12 |
+
|
| 13 |
+
# Run the container
|
| 14 |
+
docker run -p 7860:7860 pattanshetty-frontend
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
### Deploy to Hugging Face Spaces
|
| 18 |
+
|
| 19 |
+
1. Create a new Space on Hugging Face
|
| 20 |
+
2. Choose "Docker" as the SDK
|
| 21 |
+
3. Upload the entire `frontend` folder
|
| 22 |
+
4. The Dockerfile will automatically build and deploy
|
| 23 |
+
|
| 24 |
+
### Port Configuration
|
| 25 |
+
|
| 26 |
+
- **Port 7860** - Hugging Face Spaces default port
|
| 27 |
+
- The app will be accessible at your Space URL
|
| 28 |
+
|
| 29 |
+
### Environment Variables
|
| 30 |
+
|
| 31 |
+
If you need to set environment variables (like API URL), create a `.env` file or set them in Hugging Face Spaces settings.
|
| 32 |
+
|
| 33 |
+
### Notes
|
| 34 |
+
|
| 35 |
+
- Uses `npm run preview` which serves the production build
|
| 36 |
+
- No nginx needed - Vite's preview server handles everything
|
| 37 |
+
- Lightweight Alpine Linux base image
|
| 38 |
+
- Production-ready build
|
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:18-alpine
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Copy package files
|
| 6 |
+
COPY package*.json ./
|
| 7 |
+
|
| 8 |
+
# Install dependencies
|
| 9 |
+
RUN npm install
|
| 10 |
+
|
| 11 |
+
# Copy all files
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
# Build the app
|
| 15 |
+
RUN npm run build
|
| 16 |
+
|
| 17 |
+
# Expose port 7860 (Hugging Face Spaces default)
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
# Start the preview server
|
| 21 |
+
CMD ["npm", "run", "preview", "--", "--port", "7860", "--host", "0.0.0.0"]
|
components/Layout.tsx
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { Link, useLocation } from 'react-router-dom';
|
| 3 |
+
import {
|
| 4 |
+
Home,
|
| 5 |
+
FileInput,
|
| 6 |
+
FileOutput,
|
| 7 |
+
Users,
|
| 8 |
+
Package,
|
| 9 |
+
Settings,
|
| 10 |
+
Menu,
|
| 11 |
+
X,
|
| 12 |
+
TrendingUp
|
| 13 |
+
} from 'lucide-react';
|
| 14 |
+
|
| 15 |
+
interface LayoutProps {
|
| 16 |
+
children: React.ReactNode;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
| 20 |
+
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
| 21 |
+
const location = useLocation();
|
| 22 |
+
|
| 23 |
+
const navItems = [
|
| 24 |
+
{ path: '/', label: 'डॅशबोर्ड (Home)', icon: Home },
|
| 25 |
+
{ path: '/jawaak', label: 'जावक बिल (Sales)', icon: FileOutput },
|
| 26 |
+
{ path: '/awaak', label: 'आवक बिल (Purchase)', icon: FileInput },
|
| 27 |
+
{ path: '/ledger', label: 'पार्टी लेजर (Ledger)', icon: Users },
|
| 28 |
+
{ path: '/stock', label: 'स्टॉक रिपोर्ट (Stock)', icon: Package },
|
| 29 |
+
{ path: '/analysis', label: 'अॅनालिसीस (Analysis)', icon: TrendingUp },
|
| 30 |
+
{ path: '/settings', label: 'सेटिंग्स (Settings)', icon: Settings },
|
| 31 |
+
];
|
| 32 |
+
|
| 33 |
+
const isActive = (path: string) => location.pathname === path;
|
| 34 |
+
|
| 35 |
+
return (
|
| 36 |
+
<div className="flex h-screen bg-gray-50 overflow-hidden">
|
| 37 |
+
{/* Mobile Sidebar Overlay */}
|
| 38 |
+
{isSidebarOpen && (
|
| 39 |
+
<div
|
| 40 |
+
className="fixed inset-0 bg-black bg-opacity-50 z-20 lg:hidden"
|
| 41 |
+
onClick={() => setSidebarOpen(false)}
|
| 42 |
+
/>
|
| 43 |
+
)}
|
| 44 |
+
|
| 45 |
+
{/* Sidebar */}
|
| 46 |
+
<aside
|
| 47 |
+
className={`fixed lg:static inset-y-0 left-0 w-64 bg-teal-800 text-white transform transition-transform duration-200 ease-in-out z-30 ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
|
| 48 |
+
}`}
|
| 49 |
+
>
|
| 50 |
+
<div className="flex items-center justify-between p-4 border-b border-teal-700 h-16">
|
| 51 |
+
<div className="flex items-center gap-2">
|
| 52 |
+
<TrendingUp className="w-6 h-6 text-teal-300" />
|
| 53 |
+
<span className="font-bold text-xl">Mirchi Vyapar</span>
|
| 54 |
+
</div>
|
| 55 |
+
<button
|
| 56 |
+
className="lg:hidden p-1 hover:bg-teal-700 rounded"
|
| 57 |
+
onClick={() => setSidebarOpen(false)}
|
| 58 |
+
>
|
| 59 |
+
<X size={24} />
|
| 60 |
+
</button>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<nav className="p-4 space-y-1">
|
| 64 |
+
{navItems.map((item) => (
|
| 65 |
+
<Link
|
| 66 |
+
key={item.path}
|
| 67 |
+
to={item.path}
|
| 68 |
+
onClick={() => setSidebarOpen(false)}
|
| 69 |
+
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive(item.path)
|
| 70 |
+
? 'bg-teal-900 text-teal-100 shadow-sm'
|
| 71 |
+
: 'text-teal-100 hover:bg-teal-700'
|
| 72 |
+
}`}
|
| 73 |
+
>
|
| 74 |
+
<item.icon size={20} />
|
| 75 |
+
<span className="font-medium">{item.label}</span>
|
| 76 |
+
</Link>
|
| 77 |
+
))}
|
| 78 |
+
</nav>
|
| 79 |
+
|
| 80 |
+
<div className="absolute bottom-0 w-full p-4 border-t border-teal-700 bg-teal-800">
|
| 81 |
+
<div className="flex items-center gap-3">
|
| 82 |
+
<div className="w-8 h-8 rounded-full bg-teal-600 flex items-center justify-center font-bold">
|
| 83 |
+
A
|
| 84 |
+
</div>
|
| 85 |
+
<div>
|
| 86 |
+
<p className="text-sm font-medium">Admin User</p>
|
| 87 |
+
<p className="text-xs text-teal-300">Mirchi Mandi</p>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
</aside>
|
| 92 |
+
|
| 93 |
+
{/* Main Content */}
|
| 94 |
+
<main className="flex-1 flex flex-col overflow-hidden">
|
| 95 |
+
{/* Top Header */}
|
| 96 |
+
<header className="h-16 bg-white border-b flex items-center justify-between px-4 lg:px-8">
|
| 97 |
+
<button
|
| 98 |
+
className="lg:hidden p-2 -ml-2 text-gray-600 hover:bg-gray-100 rounded-lg"
|
| 99 |
+
onClick={() => setSidebarOpen(true)}
|
| 100 |
+
>
|
| 101 |
+
<Menu size={24} />
|
| 102 |
+
</button>
|
| 103 |
+
|
| 104 |
+
<h1 className="text-xl font-semibold text-gray-800 ml-2 lg:ml-0">
|
| 105 |
+
{navItems.find(i => isActive(i.path))?.label.split(' (')[0] || 'Mirchi Vyapar'}
|
| 106 |
+
</h1>
|
| 107 |
+
|
| 108 |
+
<div className="flex items-center gap-4">
|
| 109 |
+
<span className="text-sm text-gray-500 hidden md:block">
|
| 110 |
+
{new Date().toLocaleDateString('mr-IN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
|
| 111 |
+
</span>
|
| 112 |
+
</div>
|
| 113 |
+
</header>
|
| 114 |
+
|
| 115 |
+
{/* Page Content */}
|
| 116 |
+
<div className="flex-1 overflow-auto p-4 lg:p-6 pb-20 lg:pb-6">
|
| 117 |
+
{children}
|
| 118 |
+
</div>
|
| 119 |
+
</main>
|
| 120 |
+
</div>
|
| 121 |
+
);
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
export default Layout;
|
components/PWAInstallPrompt.tsx
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState } from 'react';
|
| 2 |
+
import { Download, X } from 'lucide-react';
|
| 3 |
+
import { usePWA } from '../context/PWAContext';
|
| 4 |
+
|
| 5 |
+
const PWAInstallPrompt: React.FC = () => {
|
| 6 |
+
const { deferredPrompt, isInstallable, isIOS, isStandalone, installApp } = usePWA();
|
| 7 |
+
const [showInstallPrompt, setShowInstallPrompt] = useState(false);
|
| 8 |
+
|
| 9 |
+
useEffect(() => {
|
| 10 |
+
// Show install prompt if installable and not already installed
|
| 11 |
+
if (isInstallable && !isStandalone) {
|
| 12 |
+
setShowInstallPrompt(true);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
// For iOS, show prompt if not installed
|
| 16 |
+
if (isIOS && !isStandalone) {
|
| 17 |
+
setShowInstallPrompt(true);
|
| 18 |
+
}
|
| 19 |
+
}, [isInstallable, isStandalone, isIOS]);
|
| 20 |
+
|
| 21 |
+
const handleInstallClick = async () => {
|
| 22 |
+
if (isIOS) {
|
| 23 |
+
// iOS doesn't support programmatic install, show instructions
|
| 24 |
+
return;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
await installApp();
|
| 28 |
+
setShowInstallPrompt(false);
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
const handleDismiss = () => {
|
| 32 |
+
setShowInstallPrompt(false);
|
| 33 |
+
// Remember dismissal for 7 days
|
| 34 |
+
localStorage.setItem('pwa-install-dismissed', Date.now().toString());
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
// Don't show if already installed
|
| 38 |
+
if (isStandalone) {
|
| 39 |
+
return null;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Check if dismissed recently (within 7 days)
|
| 43 |
+
const dismissedTime = localStorage.getItem('pwa-install-dismissed');
|
| 44 |
+
if (dismissedTime) {
|
| 45 |
+
const daysSinceDismissed = (Date.now() - parseInt(dismissedTime)) / (1000 * 60 * 60 * 24);
|
| 46 |
+
if (daysSinceDismissed < 7) {
|
| 47 |
+
return null;
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
if (!showInstallPrompt) {
|
| 52 |
+
return null;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
return (
|
| 56 |
+
<div className="fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-96 bg-white rounded-lg shadow-2xl border-2 border-teal-600 p-4 z-50 animate-slide-up">
|
| 57 |
+
<button
|
| 58 |
+
onClick={handleDismiss}
|
| 59 |
+
className="absolute top-2 right-2 text-gray-400 hover:text-gray-600"
|
| 60 |
+
>
|
| 61 |
+
<X size={20} />
|
| 62 |
+
</button>
|
| 63 |
+
|
| 64 |
+
<div className="flex items-start gap-3">
|
| 65 |
+
<div className="p-2 bg-teal-100 rounded-lg">
|
| 66 |
+
<Download className="text-teal-600" size={24} />
|
| 67 |
+
</div>
|
| 68 |
+
<div className="flex-1">
|
| 69 |
+
<h3 className="font-bold text-gray-900 mb-1">Install App</h3>
|
| 70 |
+
{isIOS ? (
|
| 71 |
+
<div className="text-sm text-gray-600">
|
| 72 |
+
<p className="mb-2">Install this app on your iPhone:</p>
|
| 73 |
+
<ol className="list-decimal list-inside space-y-1 text-xs">
|
| 74 |
+
<li>Tap the Share button <span className="inline-block">⎋</span></li>
|
| 75 |
+
<li>Scroll and tap "Add to Home Screen"</li>
|
| 76 |
+
<li>Tap "Add" in the top right</li>
|
| 77 |
+
</ol>
|
| 78 |
+
</div>
|
| 79 |
+
) : (
|
| 80 |
+
<>
|
| 81 |
+
<p className="text-sm text-gray-600 mb-3">
|
| 82 |
+
Install our app for a better experience and offline access!
|
| 83 |
+
</p>
|
| 84 |
+
<button
|
| 85 |
+
onClick={handleInstallClick}
|
| 86 |
+
className="w-full bg-teal-600 text-white py-2 px-4 rounded-lg font-medium hover:bg-teal-700 transition-colors"
|
| 87 |
+
>
|
| 88 |
+
Install Now
|
| 89 |
+
</button>
|
| 90 |
+
</>
|
| 91 |
+
)}
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
);
|
| 96 |
+
};
|
| 97 |
+
|
| 98 |
+
export default PWAInstallPrompt;
|
components/PdfInvoice.tsx
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { Transaction } from '../types';
|
| 3 |
+
import { Download, X, Eye } from 'lucide-react';
|
| 4 |
+
import jsPDF from 'jspdf';
|
| 5 |
+
import 'jspdf-autotable';
|
| 6 |
+
|
| 7 |
+
interface PdfInvoiceProps {
|
| 8 |
+
transaction: Transaction;
|
| 9 |
+
className?: string;
|
| 10 |
+
children?: React.ReactNode;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const PdfInvoice: React.FC<PdfInvoiceProps> = ({ transaction, className, children }) => {
|
| 14 |
+
const [showPreview, setShowPreview] = useState(false);
|
| 15 |
+
const [pdfUrl, setPdfUrl] = useState<string>('');
|
| 16 |
+
|
| 17 |
+
const formatINR = (v: number) => `₹${v.toFixed(2)}`;
|
| 18 |
+
const formatDate = (dateStr: string) => {
|
| 19 |
+
const date = new Date(dateStr);
|
| 20 |
+
return date.toLocaleDateString('en-IN', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
const generatePDF = (download: boolean = false) => {
|
| 24 |
+
const doc = new jsPDF();
|
| 25 |
+
|
| 26 |
+
const subtotal = transaction.subtotal || 0;
|
| 27 |
+
const packing = transaction.expenses?.poti_amount || 0;
|
| 28 |
+
const cess = transaction.expenses?.cess_amount || 0;
|
| 29 |
+
const adat = transaction.expenses?.adat_amount || 0;
|
| 30 |
+
const hamali = (transaction.expenses?.hamali_amount || 0) + (transaction.expenses?.packaging_hamali_amount || 0);
|
| 31 |
+
const grandTotal = transaction.total_amount || 0;
|
| 32 |
+
const paidAmount = transaction.paid_amount || 0;
|
| 33 |
+
const balanceAmount = transaction.balance_amount || 0;
|
| 34 |
+
|
| 35 |
+
// Header - Invoice Date on left, Bill No on right
|
| 36 |
+
doc.setFontSize(13);
|
| 37 |
+
doc.setFont('helvetica', 'bold');
|
| 38 |
+
doc.text(`Invoice Date: ${formatDate(transaction.bill_date)}`, 14, 15);
|
| 39 |
+
doc.text(`Bill No: ${transaction.bill_number}`, 196, 15, { align: 'right' });
|
| 40 |
+
|
| 41 |
+
// Party Details Section - LEFT ALIGNED
|
| 42 |
+
doc.setFontSize(11);
|
| 43 |
+
doc.setFont('helvetica', 'bold');
|
| 44 |
+
doc.text('Party Details', 14, 25);
|
| 45 |
+
doc.setLineWidth(0.5);
|
| 46 |
+
doc.line(14, 26, 55, 26);
|
| 47 |
+
|
| 48 |
+
// Party info - explicitly left aligned at x=14
|
| 49 |
+
doc.setFont('helvetica', 'normal');
|
| 50 |
+
doc.setFontSize(9);
|
| 51 |
+
|
| 52 |
+
const partyName = transaction.party_name || '-';
|
| 53 |
+
const partyCity = transaction.party_city || '-';
|
| 54 |
+
const partyPhone = transaction.party_phone || '-';
|
| 55 |
+
|
| 56 |
+
doc.text(`Name: ${partyName}`, 14, 31, { align: 'left' });
|
| 57 |
+
doc.text(`Place: ${partyCity}`, 14, 36, { align: 'left' });
|
| 58 |
+
doc.text(`Mobile: ${partyPhone}`, 14, 41, { align: 'left' });
|
| 59 |
+
|
| 60 |
+
// Items Table
|
| 61 |
+
const tableData = transaction.items.map((item, idx) => {
|
| 62 |
+
const potiWeights = Array.isArray(item.poti_weights) ? item.poti_weights.join(', ') : (typeof item.poti_weights === 'string' ? item.poti_weights : '-');
|
| 63 |
+
const amount = item.net_weight * item.rate_per_kg;
|
| 64 |
+
return [idx + 1, `${item.mirchi_name}\n${potiWeights}`, item.poti_count, item.net_weight, item.rate_per_kg, formatINR(amount)];
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
(doc as any).autoTable({
|
| 68 |
+
startY: 47,
|
| 69 |
+
head: [['#', 'Product Type', 'Bags', 'Net (kg)', 'Rate (₹)', 'Amount (₹)']],
|
| 70 |
+
body: tableData,
|
| 71 |
+
theme: 'grid',
|
| 72 |
+
styles: { fontSize: 8, cellPadding: 2, lineWidth: 0.5, lineColor: [0, 0, 0] },
|
| 73 |
+
headStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontStyle: 'bold' },
|
| 74 |
+
bodyStyles: { textColor: [0, 0, 0] },
|
| 75 |
+
columnStyles: {
|
| 76 |
+
0: { halign: 'center', cellWidth: 10 },
|
| 77 |
+
1: { halign: 'left', cellWidth: 60 },
|
| 78 |
+
2: { halign: 'center', cellWidth: 20 },
|
| 79 |
+
3: { halign: 'center', cellWidth: 25 },
|
| 80 |
+
4: { halign: 'center', cellWidth: 25 },
|
| 81 |
+
5: { halign: 'right', cellWidth: 35 }
|
| 82 |
+
}
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
// Summary Table - Right side
|
| 86 |
+
const finalY = (doc as any).lastAutoTable.finalY + 5;
|
| 87 |
+
const summaryData = [
|
| 88 |
+
['Sub Total', formatINR(subtotal)],
|
| 89 |
+
['Packing', formatINR(packing)],
|
| 90 |
+
['CESS', formatINR(cess)],
|
| 91 |
+
['Adat', formatINR(adat)],
|
| 92 |
+
['Hamali', formatINR(hamali)],
|
| 93 |
+
['Total Amount', formatINR(grandTotal)]
|
| 94 |
+
];
|
| 95 |
+
|
| 96 |
+
(doc as any).autoTable({
|
| 97 |
+
startY: finalY,
|
| 98 |
+
body: summaryData,
|
| 99 |
+
theme: 'grid',
|
| 100 |
+
styles: { fontSize: 8, cellPadding: 2, lineWidth: 0.5, lineColor: [0, 0, 0] },
|
| 101 |
+
bodyStyles: { textColor: [0, 0, 0] },
|
| 102 |
+
columnStyles: {
|
| 103 |
+
0: { halign: 'left', cellWidth: 50 },
|
| 104 |
+
1: { halign: 'right', cellWidth: 35 }
|
| 105 |
+
},
|
| 106 |
+
margin: { left: 111 }
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
// Payment Status Section - LEFT ALIGNED
|
| 110 |
+
const paymentY = (doc as any).lastAutoTable.finalY + 10;
|
| 111 |
+
doc.setFontSize(11);
|
| 112 |
+
doc.setFont('helvetica', 'bold');
|
| 113 |
+
doc.text('Payment Status', 14, paymentY, { align: 'left' });
|
| 114 |
+
doc.setLineWidth(0.5);
|
| 115 |
+
doc.line(14, paymentY + 1, 60, paymentY + 1);
|
| 116 |
+
|
| 117 |
+
doc.setFont('helvetica', 'normal');
|
| 118 |
+
doc.setFontSize(9);
|
| 119 |
+
|
| 120 |
+
let currentY = paymentY + 6;
|
| 121 |
+
|
| 122 |
+
if (transaction.payments && transaction.payments.length > 0) {
|
| 123 |
+
transaction.payments.forEach((payment) => {
|
| 124 |
+
const mode = payment.mode === 'cash' ? 'Cash' : payment.mode === 'online' ? 'Online' : payment.mode;
|
| 125 |
+
doc.text(`${mode}: ${formatINR(payment.amount)}`, 14, currentY, { align: 'left' });
|
| 126 |
+
currentY += 5;
|
| 127 |
+
});
|
| 128 |
+
doc.setFont('helvetica', 'bold');
|
| 129 |
+
doc.text(`Total Paid: ${formatINR(paidAmount)}`, 14, currentY, { align: 'left' });
|
| 130 |
+
currentY += 5;
|
| 131 |
+
} else {
|
| 132 |
+
doc.text(`Paid Amount: ${formatINR(paidAmount)}`, 14, currentY, { align: 'left' });
|
| 133 |
+
currentY += 5;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
if (balanceAmount > 0) {
|
| 137 |
+
doc.setFont('helvetica', 'bold');
|
| 138 |
+
doc.setTextColor(211, 47, 47);
|
| 139 |
+
doc.text(`Due Amount: ${formatINR(balanceAmount)}`, 14, currentY, { align: 'left' });
|
| 140 |
+
doc.setTextColor(0, 0, 0);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// Signature - Right side
|
| 144 |
+
doc.setFont('helvetica', 'normal');
|
| 145 |
+
doc.setFontSize(9);
|
| 146 |
+
doc.text('(Authorised Signatory)', 196, paymentY + 20, { align: 'right' });
|
| 147 |
+
|
| 148 |
+
if (download) {
|
| 149 |
+
doc.save(`Invoice_${transaction.bill_number}.pdf`);
|
| 150 |
+
} else {
|
| 151 |
+
const pdfBlob = doc.output('blob');
|
| 152 |
+
const url = URL.createObjectURL(pdfBlob);
|
| 153 |
+
setPdfUrl(url);
|
| 154 |
+
setShowPreview(true);
|
| 155 |
+
}
|
| 156 |
+
};
|
| 157 |
+
|
| 158 |
+
const handleDownload = () => {
|
| 159 |
+
generatePDF(true);
|
| 160 |
+
setShowPreview(false);
|
| 161 |
+
if (pdfUrl) {
|
| 162 |
+
URL.revokeObjectURL(pdfUrl);
|
| 163 |
+
}
|
| 164 |
+
};
|
| 165 |
+
|
| 166 |
+
const handleClose = () => {
|
| 167 |
+
setShowPreview(false);
|
| 168 |
+
if (pdfUrl) {
|
| 169 |
+
URL.revokeObjectURL(pdfUrl);
|
| 170 |
+
}
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
return (
|
| 174 |
+
<>
|
| 175 |
+
<button
|
| 176 |
+
onClick={() => generatePDF(false)}
|
| 177 |
+
className={className || "p-2 bg-white text-red-600 border border-red-600 rounded-md hover:bg-red-50 transition-colors flex items-center justify-center shadow-sm"}
|
| 178 |
+
title="Preview & Download PDF"
|
| 179 |
+
>
|
| 180 |
+
{children || <Eye size={16} />}
|
| 181 |
+
</button>
|
| 182 |
+
|
| 183 |
+
{showPreview && (
|
| 184 |
+
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
| 185 |
+
<div className="bg-white rounded-lg shadow-2xl max-w-6xl w-full max-h-[90vh] flex flex-col">
|
| 186 |
+
<div className="border-b border-gray-200 px-6 py-4 flex justify-between items-center">
|
| 187 |
+
<h2 className="text-xl font-bold text-gray-800">PDF Preview</h2>
|
| 188 |
+
<div className="flex gap-2">
|
| 189 |
+
<button
|
| 190 |
+
onClick={handleDownload}
|
| 191 |
+
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center gap-2"
|
| 192 |
+
>
|
| 193 |
+
<Download size={18} />
|
| 194 |
+
Download PDF
|
| 195 |
+
</button>
|
| 196 |
+
<button
|
| 197 |
+
onClick={handleClose}
|
| 198 |
+
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
| 199 |
+
>
|
| 200 |
+
<X size={24} className="text-gray-600" />
|
| 201 |
+
</button>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<div className="flex-1 overflow-auto p-4 bg-gray-100">
|
| 206 |
+
<iframe
|
| 207 |
+
src={pdfUrl}
|
| 208 |
+
className="w-full h-full min-h-[600px] bg-white rounded shadow-lg"
|
| 209 |
+
title="PDF Preview"
|
| 210 |
+
/>
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
)}
|
| 215 |
+
</>
|
| 216 |
+
);
|
| 217 |
+
};
|
| 218 |
+
|
| 219 |
+
export default PdfInvoice;
|
components/PrintInvoice.tsx
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { Transaction } from '../types';
|
| 3 |
+
import { Printer, X } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
interface PrintInvoiceProps {
|
| 6 |
+
transaction: Transaction;
|
| 7 |
+
className?: string;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const PrintInvoice: React.FC<PrintInvoiceProps> = ({ transaction, className }) => {
|
| 11 |
+
const [showPreview, setShowPreview] = useState(false);
|
| 12 |
+
|
| 13 |
+
const formatINR = (v: number) => `₹${v.toFixed(2)}`;
|
| 14 |
+
const formatDate = (dateStr: string) => {
|
| 15 |
+
const date = new Date(dateStr);
|
| 16 |
+
return date.toLocaleDateString('en-IN', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
// Calculate totals
|
| 20 |
+
const subtotal = transaction.subtotal || 0;
|
| 21 |
+
const packing = transaction.expenses?.poti_amount || 0;
|
| 22 |
+
const cess = transaction.expenses?.cess_amount || 0;
|
| 23 |
+
const adat = transaction.expenses?.adat_amount || 0;
|
| 24 |
+
const hamali = (transaction.expenses?.hamali_amount || 0) + (transaction.expenses?.packaging_hamali_amount || 0);
|
| 25 |
+
const gaadiBharni = transaction.expenses?.gaadi_bharni || 0;
|
| 26 |
+
const otherExpenses = transaction.expenses?.other_expenses || 0;
|
| 27 |
+
const grandTotal = transaction.total_amount || (subtotal + packing + cess + adat + hamali + gaadiBharni + otherExpenses);
|
| 28 |
+
|
| 29 |
+
const paidAmount = transaction.paid_amount || 0;
|
| 30 |
+
const balanceAmount = transaction.balance_amount || 0;
|
| 31 |
+
|
| 32 |
+
const handlePrint = () => {
|
| 33 |
+
const printContent = document.getElementById('invoice-content');
|
| 34 |
+
if (!printContent) return;
|
| 35 |
+
|
| 36 |
+
const iframe = document.createElement('iframe');
|
| 37 |
+
iframe.style.position = 'absolute';
|
| 38 |
+
iframe.style.width = '0';
|
| 39 |
+
iframe.style.height = '0';
|
| 40 |
+
iframe.style.border = 'none';
|
| 41 |
+
document.body.appendChild(iframe);
|
| 42 |
+
|
| 43 |
+
const doc = iframe.contentWindow?.document;
|
| 44 |
+
if (doc) {
|
| 45 |
+
doc.open();
|
| 46 |
+
doc.write(`
|
| 47 |
+
<html>
|
| 48 |
+
<head>
|
| 49 |
+
<title>Invoice-${transaction.bill_number}</title>
|
| 50 |
+
<style>
|
| 51 |
+
/* Reset margins to absolute zero */
|
| 52 |
+
@page { size: A4; margin: 0; }
|
| 53 |
+
body { margin: 0; padding: 0; background-color: white; font-family: Helvetica, Arial, sans-serif; }
|
| 54 |
+
|
| 55 |
+
/* Overwrite the container style for printing */
|
| 56 |
+
#invoice-container-inner {
|
| 57 |
+
width: 210mm !important;
|
| 58 |
+
/* Reducing slightly from 297mm to 295mm prevents the 'spillover' blank page */
|
| 59 |
+
min-height: 295mm !important;
|
| 60 |
+
padding: 15mm !important;
|
| 61 |
+
margin: 0 auto !important;
|
| 62 |
+
box-sizing: border-box !important;
|
| 63 |
+
overflow: hidden !important; /* Cut off any rogue pixels */
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/* Helper to hide non-print elements if any sneak in */
|
| 67 |
+
.print-hidden { display: none !important; }
|
| 68 |
+
</style>
|
| 69 |
+
</head>
|
| 70 |
+
<body>
|
| 71 |
+
${printContent.innerHTML}
|
| 72 |
+
</body>
|
| 73 |
+
</html>
|
| 74 |
+
`);
|
| 75 |
+
doc.close();
|
| 76 |
+
|
| 77 |
+
iframe.contentWindow?.focus();
|
| 78 |
+
setTimeout(() => {
|
| 79 |
+
iframe.contentWindow?.print();
|
| 80 |
+
setTimeout(() => {
|
| 81 |
+
document.body.removeChild(iframe);
|
| 82 |
+
}, 1000);
|
| 83 |
+
}, 500);
|
| 84 |
+
}
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
return (
|
| 88 |
+
<>
|
| 89 |
+
<button
|
| 90 |
+
onClick={() => setShowPreview(true)}
|
| 91 |
+
className={className || "p-2 bg-white text-teal-600 border border-teal-600 rounded-md hover:bg-teal-50 transition-colors flex items-center justify-center shadow-sm"}
|
| 92 |
+
title="Print Invoice"
|
| 93 |
+
>
|
| 94 |
+
<Printer size={16} />
|
| 95 |
+
</button>
|
| 96 |
+
|
| 97 |
+
{showPreview && (
|
| 98 |
+
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-2 sm:p-4">
|
| 99 |
+
<div className="bg-white rounded-lg shadow-2xl max-w-4xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden flex flex-col">
|
| 100 |
+
|
| 101 |
+
{/* Header */}
|
| 102 |
+
<div className="sticky top-0 bg-white border-b border-gray-200 px-3 py-3 sm:px-6 sm:py-4 flex justify-between items-center z-10 shrink-0">
|
| 103 |
+
<h2 className="text-lg sm:text-xl font-bold text-gray-800 truncate mr-2">Invoice Preview</h2>
|
| 104 |
+
<div className="flex gap-2 items-center">
|
| 105 |
+
<button
|
| 106 |
+
onClick={handlePrint}
|
| 107 |
+
className="px-3 py-1.5 sm:px-4 sm:py-2 text-sm sm:text-base bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-1 sm:gap-2"
|
| 108 |
+
>
|
| 109 |
+
<Printer size={16} className="sm:w-[18px] sm:h-[18px]" />
|
| 110 |
+
<span>Print</span>
|
| 111 |
+
</button>
|
| 112 |
+
<button
|
| 113 |
+
onClick={() => setShowPreview(false)}
|
| 114 |
+
className="p-1.5 sm:p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
| 115 |
+
>
|
| 116 |
+
<X size={20} className="text-gray-600 sm:w-6 sm:h-6" />
|
| 117 |
+
</button>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
{/* Scrollable Area */}
|
| 122 |
+
<div className="overflow-auto flex-1 p-2 sm:p-8 bg-gray-100">
|
| 123 |
+
<div className="mb-4 text-xs text-gray-500 text-center sm:hidden">
|
| 124 |
+
Scroll horizontally to view full invoice
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
{/* Wrapper for ID targeting */}
|
| 128 |
+
<div id="invoice-content">
|
| 129 |
+
<div
|
| 130 |
+
id="invoice-container-inner"
|
| 131 |
+
className="bg-white mx-auto shadow-sm"
|
| 132 |
+
style={{
|
| 133 |
+
width: '210mm',
|
| 134 |
+
minHeight: '297mm', /* This stays 297mm for screen preview, but Print overrides it to 295mm */
|
| 135 |
+
padding: '15mm',
|
| 136 |
+
margin: 'auto',
|
| 137 |
+
fontFamily: 'Helvetica, Arial, sans-serif',
|
| 138 |
+
fontSize: '12px',
|
| 139 |
+
color: '#000',
|
| 140 |
+
backgroundColor: '#fff',
|
| 141 |
+
boxSizing: 'border-box',
|
| 142 |
+
position: 'relative'
|
| 143 |
+
}}
|
| 144 |
+
>
|
| 145 |
+
|
| 146 |
+
{/* Invoice Header */}
|
| 147 |
+
<div style={{ borderBottom: '2px solid #333', paddingBottom: '10px', marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
|
| 148 |
+
<div>
|
| 149 |
+
<h1 style={{ margin: 0, fontSize: '24px', fontWeight: 'bold', letterSpacing: '1px' }}>INVOICE</h1>
|
| 150 |
+
</div>
|
| 151 |
+
<div style={{ textAlign: 'right' }}>
|
| 152 |
+
{/* <div style={{ fontSize: '14px', fontWeight: 'bold' }}>Bill No: {transaction.bill_number}</div> */}
|
| 153 |
+
<div style={{ fontSize: '12px', marginTop: '4px' }}>Date: {formatDate(transaction.bill_date)}</div>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
{/* Info Section */}
|
| 158 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
|
| 159 |
+
<div style={{ width: '55%', textAlign: 'left' }}>
|
| 160 |
+
<div style={{
|
| 161 |
+
textTransform: 'uppercase',
|
| 162 |
+
fontSize: '10px',
|
| 163 |
+
color: '#555',
|
| 164 |
+
fontWeight: 'bold',
|
| 165 |
+
marginBottom: '4px',
|
| 166 |
+
borderBottom: '1px solid #eee',
|
| 167 |
+
paddingBottom: '2px',
|
| 168 |
+
width: '100%'
|
| 169 |
+
}}>Billed To</div>
|
| 170 |
+
|
| 171 |
+
<div style={{ fontSize: '14px', fontWeight: 'bold', marginTop: '5px' }}>{transaction.party_name}</div>
|
| 172 |
+
<div style={{ marginTop: '2px' }}>Phone: {transaction.party_phone || '-'}</div>
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
{/* Table */}
|
| 177 |
+
<table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: '20px' }}>
|
| 178 |
+
<thead>
|
| 179 |
+
<tr style={{ backgroundColor: '#f3f4f6' }}>
|
| 180 |
+
<th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'center', width: '5%' }}>#</th>
|
| 181 |
+
<th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'left', width: '40%' }}>Item Description</th>
|
| 182 |
+
<th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'center', width: '10%' }}>Bags</th>
|
| 183 |
+
<th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'center', width: '15%' }}>Net Weight</th>
|
| 184 |
+
<th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'right', width: '15%' }}>Rate</th>
|
| 185 |
+
<th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'right', width: '15%' }}>Amount</th>
|
| 186 |
+
</tr>
|
| 187 |
+
</thead>
|
| 188 |
+
<tbody>
|
| 189 |
+
{transaction.items.map((item, idx) => {
|
| 190 |
+
const potiWeights = Array.isArray(item.poti_weights) ? item.poti_weights.join(', ') : (typeof item.poti_weights === 'string' ? item.poti_weights : '-');
|
| 191 |
+
const amount = item.net_weight * item.rate_per_kg;
|
| 192 |
+
return (
|
| 193 |
+
<tr key={idx}>
|
| 194 |
+
<td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'center' }}>{idx + 1}</td>
|
| 195 |
+
<td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'left' }}>
|
| 196 |
+
<div style={{ fontWeight: 'bold' }}>{item.mirchi_name}</div>
|
| 197 |
+
<div style={{ fontSize: '9px', color: '#666', marginTop: '2px', lineHeight: '1.2' }}>
|
| 198 |
+
Weights: {potiWeights}
|
| 199 |
+
</div>
|
| 200 |
+
</td>
|
| 201 |
+
<td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'center' }}>{item.poti_count}</td>
|
| 202 |
+
<td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'center' }}>{item.net_weight} kg</td>
|
| 203 |
+
<td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'right' }}>{formatINR(item.rate_per_kg)}</td>
|
| 204 |
+
<td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'right' }}>{formatINR(amount)}</td>
|
| 205 |
+
</tr>
|
| 206 |
+
);
|
| 207 |
+
})}
|
| 208 |
+
</tbody>
|
| 209 |
+
</table>
|
| 210 |
+
|
| 211 |
+
{/* Totals Section */}
|
| 212 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
| 213 |
+
<div style={{ width: '50%', paddingRight: '20px', textAlign: 'left' }}>
|
| 214 |
+
<div style={{ border: '1px solid #ccc', padding: '10px', borderRadius: '4px' }}>
|
| 215 |
+
<div style={{ fontWeight: 'bold', borderBottom: '1px solid #eee', marginBottom: '8px', paddingBottom: '4px' }}>Payment Status</div>
|
| 216 |
+
{transaction.payments && transaction.payments.length > 0 ? (
|
| 217 |
+
<>
|
| 218 |
+
{transaction.payments.map((payment, idx) => (
|
| 219 |
+
<div key={idx} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '11px', marginBottom: '2px' }}>
|
| 220 |
+
<span>{payment.mode === 'cash' ? 'Cash' : payment.mode === 'online' ? 'Online' : payment.mode}:</span>
|
| 221 |
+
<span>{formatINR(payment.amount)}</span>
|
| 222 |
+
</div>
|
| 223 |
+
))}
|
| 224 |
+
<div style={{ borderTop: '1px dashed #ccc', marginTop: '6px', paddingTop: '6px', display: 'flex', justifyContent: 'space-between', fontWeight: 'bold' }}>
|
| 225 |
+
<span>Total Paid:</span>
|
| 226 |
+
<span>{formatINR(paidAmount)}</span>
|
| 227 |
+
</div>
|
| 228 |
+
</>
|
| 229 |
+
) : (
|
| 230 |
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
| 231 |
+
<span>Total Paid:</span>
|
| 232 |
+
<span>{formatINR(paidAmount)}</span>
|
| 233 |
+
</div>
|
| 234 |
+
)}
|
| 235 |
+
|
| 236 |
+
{balanceAmount > 0 && (
|
| 237 |
+
<div style={{ marginTop: '8px', color: '#c62828', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', fontSize: '13px' }}>
|
| 238 |
+
<span>Due Amount:</span>
|
| 239 |
+
<span>{formatINR(balanceAmount)}</span>
|
| 240 |
+
</div>
|
| 241 |
+
)}
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
|
| 245 |
+
<div style={{ width: '45%' }}>
|
| 246 |
+
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
| 247 |
+
<tbody>
|
| 248 |
+
{/* Renamed Sub Total to Total */}
|
| 249 |
+
{/* <tr>
|
| 250 |
+
<td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee' }}>Total</td>
|
| 251 |
+
<td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee', fontWeight: 'bold' }}>{formatINR(subtotal)}</td>
|
| 252 |
+
</tr> */}
|
| 253 |
+
<tr>
|
| 254 |
+
<td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Packing Expenses</td>
|
| 255 |
+
<td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(packing)}</td>
|
| 256 |
+
</tr>
|
| 257 |
+
|
| 258 |
+
{/* New Row: Total + Packing Calculation */}
|
| 259 |
+
<tr>
|
| 260 |
+
<td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', fontWeight: 'bold' }}>Sub Total</td>
|
| 261 |
+
<td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee', fontWeight: 'bold' }}>{formatINR(subtotal + packing)}</td>
|
| 262 |
+
</tr>
|
| 263 |
+
|
| 264 |
+
<tr>
|
| 265 |
+
<td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>CESS</td>
|
| 266 |
+
<td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(cess)}</td>
|
| 267 |
+
</tr>
|
| 268 |
+
<tr>
|
| 269 |
+
<td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Adat</td>
|
| 270 |
+
<td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(adat)}</td>
|
| 271 |
+
</tr>
|
| 272 |
+
<tr>
|
| 273 |
+
<td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Hamali</td>
|
| 274 |
+
<td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(hamali)}</td>
|
| 275 |
+
</tr>
|
| 276 |
+
<tr>
|
| 277 |
+
<td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Gaadi Bharni</td>
|
| 278 |
+
<td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(gaadiBharni)}</td>
|
| 279 |
+
</tr>
|
| 280 |
+
<tr>
|
| 281 |
+
<td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Other Expenses</td>
|
| 282 |
+
<td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(otherExpenses)}</td>
|
| 283 |
+
</tr>
|
| 284 |
+
<tr style={{ backgroundColor: '#f3f4f6' }}>
|
| 285 |
+
<th style={{ padding: '8px 4px', textAlign: 'left', borderTop: '2px solid #000', fontSize: '14px' }}>Grand Total</th>
|
| 286 |
+
<th style={{ padding: '8px 4px', textAlign: 'right', borderTop: '2px solid #000', fontSize: '14px' }}>{formatINR(grandTotal)}</th>
|
| 287 |
+
</tr>
|
| 288 |
+
</tbody>
|
| 289 |
+
</table>
|
| 290 |
+
</div>
|
| 291 |
+
</div>
|
| 292 |
+
|
| 293 |
+
{/* Signatures */}
|
| 294 |
+
<div style={{ marginTop: '50px', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
|
| 295 |
+
<div style={{ textAlign: 'left', fontSize: '10px', color: '#666' }}>
|
| 296 |
+
<p>Thank you for your business.</p>
|
| 297 |
+
</div>
|
| 298 |
+
<div style={{ textAlign: 'center' }}>
|
| 299 |
+
<div style={{ marginBottom: '40px', fontSize: '10px' }}>For Authorised Signatory</div>
|
| 300 |
+
<div style={{ borderTop: '1px solid #000', width: '150px', margin: 'auto' }}></div>
|
| 301 |
+
<div style={{ fontSize: '10px', marginTop: '4px' }}>(Signature)</div>
|
| 302 |
+
</div>
|
| 303 |
+
</div>
|
| 304 |
+
|
| 305 |
+
</div>
|
| 306 |
+
</div>
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
</div>
|
| 310 |
+
)}
|
| 311 |
+
</>
|
| 312 |
+
);
|
| 313 |
+
};
|
| 314 |
+
|
| 315 |
+
export default PrintInvoice;
|
context/PWAContext.tsx
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { createContext, useContext, useEffect, useState } from 'react';
|
| 2 |
+
|
| 3 |
+
interface PWAContextType {
|
| 4 |
+
deferredPrompt: any;
|
| 5 |
+
isInstallable: boolean;
|
| 6 |
+
isIOS: boolean;
|
| 7 |
+
isStandalone: boolean;
|
| 8 |
+
installApp: () => Promise<void>;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const PWAContext = createContext<PWAContextType | undefined>(undefined);
|
| 12 |
+
|
| 13 |
+
export const PWAProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
| 14 |
+
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
|
| 15 |
+
const [isInstallable, setIsInstallable] = useState(false);
|
| 16 |
+
const [isIOS, setIsIOS] = useState(false);
|
| 17 |
+
const [isStandalone, setIsStandalone] = useState(false);
|
| 18 |
+
|
| 19 |
+
useEffect(() => {
|
| 20 |
+
// Check if already installed
|
| 21 |
+
const standalone = window.matchMedia('(display-mode: standalone)').matches ||
|
| 22 |
+
(window.navigator as any).standalone ||
|
| 23 |
+
document.referrer.includes('android-app://');
|
| 24 |
+
|
| 25 |
+
setIsStandalone(standalone);
|
| 26 |
+
|
| 27 |
+
// Check if iOS
|
| 28 |
+
const ios = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
|
| 29 |
+
setIsIOS(ios);
|
| 30 |
+
|
| 31 |
+
// Listen for beforeinstallprompt event
|
| 32 |
+
const handleBeforeInstallPrompt = (e: Event) => {
|
| 33 |
+
// Prevent the mini-infobar from appearing on mobile
|
| 34 |
+
e.preventDefault();
|
| 35 |
+
// Stash the event so it can be triggered later.
|
| 36 |
+
setDeferredPrompt(e);
|
| 37 |
+
setIsInstallable(true);
|
| 38 |
+
console.log('✅ PWA Install Prompt captured');
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
| 42 |
+
|
| 43 |
+
return () => {
|
| 44 |
+
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
| 45 |
+
};
|
| 46 |
+
}, []);
|
| 47 |
+
|
| 48 |
+
const installApp = async () => {
|
| 49 |
+
if (!deferredPrompt) {
|
| 50 |
+
return;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// Show the install prompt
|
| 54 |
+
deferredPrompt.prompt();
|
| 55 |
+
|
| 56 |
+
// Wait for the user to respond to the prompt
|
| 57 |
+
const { outcome } = await deferredPrompt.userChoice;
|
| 58 |
+
|
| 59 |
+
if (outcome === 'accepted') {
|
| 60 |
+
console.log('User accepted the install prompt');
|
| 61 |
+
} else {
|
| 62 |
+
console.log('User dismissed the install prompt');
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// We've used the prompt, so clear it
|
| 66 |
+
setDeferredPrompt(null);
|
| 67 |
+
setIsInstallable(false);
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
return (
|
| 71 |
+
<PWAContext.Provider value={{ deferredPrompt, isInstallable, isIOS, isStandalone, installApp }}>
|
| 72 |
+
{children}
|
| 73 |
+
</PWAContext.Provider>
|
| 74 |
+
);
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
+
export const usePWA = () => {
|
| 78 |
+
const context = useContext(PWAContext);
|
| 79 |
+
if (context === undefined) {
|
| 80 |
+
throw new Error('usePWA must be used within a PWAProvider');
|
| 81 |
+
}
|
| 82 |
+
return context;
|
| 83 |
+
};
|
dist/404.html
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Page Not Found</title>
|
| 7 |
+
|
| 8 |
+
<style media="screen">
|
| 9 |
+
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
|
| 10 |
+
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px 16px; border-radius: 3px; }
|
| 11 |
+
#message h3 { color: #888; font-weight: normal; font-size: 16px; margin: 16px 0 12px; }
|
| 12 |
+
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
|
| 13 |
+
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
|
| 14 |
+
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
|
| 15 |
+
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
|
| 16 |
+
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
|
| 17 |
+
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
|
| 18 |
+
@media (max-width: 600px) {
|
| 19 |
+
body, #message { margin-top: 0; background: white; box-shadow: none; }
|
| 20 |
+
body { border-top: 16px solid #ffa100; }
|
| 21 |
+
}
|
| 22 |
+
</style>
|
| 23 |
+
</head>
|
| 24 |
+
<body>
|
| 25 |
+
<div id="message">
|
| 26 |
+
<h2>404</h2>
|
| 27 |
+
<h1>Page Not Found</h1>
|
| 28 |
+
<p>The specified file was not found on this website. Please check the URL for mistakes and try again.</p>
|
| 29 |
+
<h3>Why am I seeing this?</h3>
|
| 30 |
+
<p>This page was generated by the Firebase Command-Line Interface. To modify it, edit the <code>404.html</code> file in your project's configured <code>public</code> directory.</p>
|
| 31 |
+
</div>
|
| 32 |
+
</body>
|
| 33 |
+
</html>
|
dist/assets/html2canvas.esm-QH1iLAAe.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dist/assets/index-DEfz7c9M.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dist/assets/index.es-qlPCKdii.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dist/assets/purify.es-B6FQ9oRL.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*! @license DOMPurify 3.3.0 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.0/LICENSE */const{entries:_t,setPrototypeOf:ct,isFrozen:Yt,getPrototypeOf:Xt,getOwnPropertyDescriptor:jt}=Object;let{freeze:S,seal:y,create:Pe}=Object,{apply:ve,construct:ke}=typeof Reflect<"u"&&Reflect;S||(S=function(o){return o});y||(y=function(o){return o});ve||(ve=function(o,l){for(var a=arguments.length,c=new Array(a>2?a-2:0),O=2;O<a;O++)c[O-2]=arguments[O];return o.apply(l,c)});ke||(ke=function(o){for(var l=arguments.length,a=new Array(l>1?l-1:0),c=1;c<l;c++)a[c-1]=arguments[c];return new o(...a)});const ce=R(Array.prototype.forEach),Vt=R(Array.prototype.lastIndexOf),ft=R(Array.prototype.pop),q=R(Array.prototype.push),$t=R(Array.prototype.splice),ue=R(String.prototype.toLowerCase),Ne=R(String.prototype.toString),Ie=R(String.prototype.match),K=R(String.prototype.replace),qt=R(String.prototype.indexOf),Kt=R(String.prototype.trim),b=R(Object.prototype.hasOwnProperty),A=R(RegExp.prototype.test),Z=Zt(TypeError);function R(s){return function(o){o instanceof RegExp&&(o.lastIndex=0);for(var l=arguments.length,a=new Array(l>1?l-1:0),c=1;c<l;c++)a[c-1]=arguments[c];return ve(s,o,a)}}function Zt(s){return function(){for(var o=arguments.length,l=new Array(o),a=0;a<o;a++)l[a]=arguments[a];return ke(s,l)}}function r(s,o){let l=arguments.length>2&&arguments[2]!==void 0?arguments[2]:ue;ct&&ct(s,null);let a=o.length;for(;a--;){let c=o[a];if(typeof c=="string"){const O=l(c);O!==c&&(Yt(o)||(o[a]=O),c=O)}s[c]=!0}return s}function Jt(s){for(let o=0;o<s.length;o++)b(s,o)||(s[o]=null);return s}function M(s){const o=Pe(null);for(const[l,a]of _t(s))b(s,l)&&(Array.isArray(a)?o[l]=Jt(a):a&&typeof a=="object"&&a.constructor===Object?o[l]=M(a):o[l]=a);return o}function J(s,o){for(;s!==null;){const a=jt(s,o);if(a){if(a.get)return R(a.get);if(typeof a.value=="function")return R(a.value)}s=Xt(s)}function l(){return null}return l}const ut=S(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),Ce=S(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),Me=S(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),Qt=S(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),we=S(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),en=S(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),mt=S(["#text"]),pt=S(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns","slot"]),xe=S(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),dt=S(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),fe=S(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),tn=y(/\{\{[\w\W]*|[\w\W]*\}\}/gm),nn=y(/<%[\w\W]*|[\w\W]*%>/gm),on=y(/\$\{[\w\W]*/gm),an=y(/^data-[\-\w.\u00B7-\uFFFF]+$/),rn=y(/^aria-[\-\w]+$/),gt=y(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),sn=y(/^(?:\w+script|data):/i),ln=y(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),ht=y(/^html$/i),cn=y(/^[a-z][.\w]*(-[.\w]+)+$/i);var Tt=Object.freeze({__proto__:null,ARIA_ATTR:rn,ATTR_WHITESPACE:ln,CUSTOM_ELEMENT:cn,DATA_ATTR:an,DOCTYPE_NAME:ht,ERB_EXPR:nn,IS_ALLOWED_URI:gt,IS_SCRIPT_OR_DATA:sn,MUSTACHE_EXPR:tn,TMPLIT_EXPR:on});const Q={element:1,text:3,progressingInstruction:7,comment:8,document:9},fn=function(){return typeof window>"u"?null:window},un=function(o,l){if(typeof o!="object"||typeof o.createPolicy!="function")return null;let a=null;const c="data-tt-policy-suffix";l&&l.hasAttribute(c)&&(a=l.getAttribute(c));const O="dompurify"+(a?"#"+a:"");try{return o.createPolicy(O,{createHTML(P){return P},createScriptURL(P){return P}})}catch{return console.warn("TrustedTypes policy "+O+" could not be created."),null}},Et=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function At(){let s=arguments.length>0&&arguments[0]!==void 0?arguments[0]:fn();const o=i=>At(i);if(o.version="3.3.0",o.removed=[],!s||!s.document||s.document.nodeType!==Q.document||!s.Element)return o.isSupported=!1,o;let{document:l}=s;const a=l,c=a.currentScript,{DocumentFragment:O,HTMLTemplateElement:P,Node:me,Element:Ue,NodeFilter:B,NamedNodeMap:St=s.NamedNodeMap||s.MozNamedAttrMap,HTMLFormElement:Rt,DOMParser:Ot,trustedTypes:ee}=s,Y=Ue.prototype,Lt=J(Y,"cloneNode"),yt=J(Y,"remove"),bt=J(Y,"nextSibling"),Dt=J(Y,"childNodes"),te=J(Y,"parentNode");if(typeof P=="function"){const i=l.createElement("template");i.content&&i.content.ownerDocument&&(l=i.content.ownerDocument)}let g,X="";const{implementation:pe,createNodeIterator:Nt,createDocumentFragment:It,getElementsByTagName:Ct}=l,{importNode:Mt}=a;let h=Et();o.isSupported=typeof _t=="function"&&typeof te=="function"&&pe&&pe.createHTMLDocument!==void 0;const{MUSTACHE_EXPR:de,ERB_EXPR:Te,TMPLIT_EXPR:Ee,DATA_ATTR:wt,ARIA_ATTR:xt,IS_SCRIPT_OR_DATA:Pt,ATTR_WHITESPACE:Fe,CUSTOM_ELEMENT:vt}=Tt;let{IS_ALLOWED_URI:He}=Tt,p=null;const ze=r({},[...ut,...Ce,...Me,...we,...mt]);let T=null;const Ge=r({},[...pt,...xe,...dt,...fe]);let u=Object.seal(Pe(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),j=null,_e=null;const v=Object.seal(Pe(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let We=!0,ge=!0,Be=!1,Ye=!0,k=!1,ne=!0,w=!1,he=!1,Ae=!1,U=!1,oe=!1,ie=!1,Xe=!0,je=!1;const kt="user-content-";let Se=!0,V=!1,F={},H=null;const Ve=r({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let $e=null;const qe=r({},["audio","video","img","source","image","track"]);let Re=null;const Ke=r({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),ae="http://www.w3.org/1998/Math/MathML",re="http://www.w3.org/2000/svg",N="http://www.w3.org/1999/xhtml";let z=N,Oe=!1,Le=null;const Ut=r({},[ae,re,N],Ne);let se=r({},["mi","mo","mn","ms","mtext"]),le=r({},["annotation-xml"]);const Ft=r({},["title","style","font","a","script"]);let $=null;const Ht=["application/xhtml+xml","text/html"],zt="text/html";let d=null,G=null;const Gt=l.createElement("form"),Ze=function(e){return e instanceof RegExp||e instanceof Function},ye=function(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(!(G&&G===e)){if((!e||typeof e!="object")&&(e={}),e=M(e),$=Ht.indexOf(e.PARSER_MEDIA_TYPE)===-1?zt:e.PARSER_MEDIA_TYPE,d=$==="application/xhtml+xml"?Ne:ue,p=b(e,"ALLOWED_TAGS")?r({},e.ALLOWED_TAGS,d):ze,T=b(e,"ALLOWED_ATTR")?r({},e.ALLOWED_ATTR,d):Ge,Le=b(e,"ALLOWED_NAMESPACES")?r({},e.ALLOWED_NAMESPACES,Ne):Ut,Re=b(e,"ADD_URI_SAFE_ATTR")?r(M(Ke),e.ADD_URI_SAFE_ATTR,d):Ke,$e=b(e,"ADD_DATA_URI_TAGS")?r(M(qe),e.ADD_DATA_URI_TAGS,d):qe,H=b(e,"FORBID_CONTENTS")?r({},e.FORBID_CONTENTS,d):Ve,j=b(e,"FORBID_TAGS")?r({},e.FORBID_TAGS,d):M({}),_e=b(e,"FORBID_ATTR")?r({},e.FORBID_ATTR,d):M({}),F=b(e,"USE_PROFILES")?e.USE_PROFILES:!1,We=e.ALLOW_ARIA_ATTR!==!1,ge=e.ALLOW_DATA_ATTR!==!1,Be=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Ye=e.ALLOW_SELF_CLOSE_IN_ATTR!==!1,k=e.SAFE_FOR_TEMPLATES||!1,ne=e.SAFE_FOR_XML!==!1,w=e.WHOLE_DOCUMENT||!1,U=e.RETURN_DOM||!1,oe=e.RETURN_DOM_FRAGMENT||!1,ie=e.RETURN_TRUSTED_TYPE||!1,Ae=e.FORCE_BODY||!1,Xe=e.SANITIZE_DOM!==!1,je=e.SANITIZE_NAMED_PROPS||!1,Se=e.KEEP_CONTENT!==!1,V=e.IN_PLACE||!1,He=e.ALLOWED_URI_REGEXP||gt,z=e.NAMESPACE||N,se=e.MATHML_TEXT_INTEGRATION_POINTS||se,le=e.HTML_INTEGRATION_POINTS||le,u=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&Ze(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(u.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&Ze(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(u.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements=="boolean"&&(u.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),k&&(ge=!1),oe&&(U=!0),F&&(p=r({},mt),T=[],F.html===!0&&(r(p,ut),r(T,pt)),F.svg===!0&&(r(p,Ce),r(T,xe),r(T,fe)),F.svgFilters===!0&&(r(p,Me),r(T,xe),r(T,fe)),F.mathMl===!0&&(r(p,we),r(T,dt),r(T,fe))),e.ADD_TAGS&&(typeof e.ADD_TAGS=="function"?v.tagCheck=e.ADD_TAGS:(p===ze&&(p=M(p)),r(p,e.ADD_TAGS,d))),e.ADD_ATTR&&(typeof e.ADD_ATTR=="function"?v.attributeCheck=e.ADD_ATTR:(T===Ge&&(T=M(T)),r(T,e.ADD_ATTR,d))),e.ADD_URI_SAFE_ATTR&&r(Re,e.ADD_URI_SAFE_ATTR,d),e.FORBID_CONTENTS&&(H===Ve&&(H=M(H)),r(H,e.FORBID_CONTENTS,d)),Se&&(p["#text"]=!0),w&&r(p,["html","head","body"]),p.table&&(r(p,["tbody"]),delete j.tbody),e.TRUSTED_TYPES_POLICY){if(typeof e.TRUSTED_TYPES_POLICY.createHTML!="function")throw Z('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof e.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw Z('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');g=e.TRUSTED_TYPES_POLICY,X=g.createHTML("")}else g===void 0&&(g=un(ee,c)),g!==null&&typeof X=="string"&&(X=g.createHTML(""));S&&S(e),G=e}},Je=r({},[...Ce,...Me,...Qt]),Qe=r({},[...we,...en]),Wt=function(e){let t=te(e);(!t||!t.tagName)&&(t={namespaceURI:z,tagName:"template"});const n=ue(e.tagName),f=ue(t.tagName);return Le[e.namespaceURI]?e.namespaceURI===re?t.namespaceURI===N?n==="svg":t.namespaceURI===ae?n==="svg"&&(f==="annotation-xml"||se[f]):!!Je[n]:e.namespaceURI===ae?t.namespaceURI===N?n==="math":t.namespaceURI===re?n==="math"&&le[f]:!!Qe[n]:e.namespaceURI===N?t.namespaceURI===re&&!le[f]||t.namespaceURI===ae&&!se[f]?!1:!Qe[n]&&(Ft[n]||!Je[n]):!!($==="application/xhtml+xml"&&Le[e.namespaceURI]):!1},D=function(e){q(o.removed,{element:e});try{te(e).removeChild(e)}catch{yt(e)}},x=function(e,t){try{q(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch{q(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),e==="is")if(U||oe)try{D(t)}catch{}else try{t.setAttribute(e,"")}catch{}},et=function(e){let t=null,n=null;if(Ae)e="<remove></remove>"+e;else{const m=Ie(e,/^[\r\n\t ]+/);n=m&&m[0]}$==="application/xhtml+xml"&&z===N&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");const f=g?g.createHTML(e):e;if(z===N)try{t=new Ot().parseFromString(f,$)}catch{}if(!t||!t.documentElement){t=pe.createDocument(z,"template",null);try{t.documentElement.innerHTML=Oe?X:f}catch{}}const _=t.body||t.documentElement;return e&&n&&_.insertBefore(l.createTextNode(n),_.childNodes[0]||null),z===N?Ct.call(t,w?"html":"body")[0]:w?t.documentElement:_},tt=function(e){return Nt.call(e.ownerDocument||e,e,B.SHOW_ELEMENT|B.SHOW_COMMENT|B.SHOW_TEXT|B.SHOW_PROCESSING_INSTRUCTION|B.SHOW_CDATA_SECTION,null)},be=function(e){return e instanceof Rt&&(typeof e.nodeName!="string"||typeof e.textContent!="string"||typeof e.removeChild!="function"||!(e.attributes instanceof St)||typeof e.removeAttribute!="function"||typeof e.setAttribute!="function"||typeof e.namespaceURI!="string"||typeof e.insertBefore!="function"||typeof e.hasChildNodes!="function")},nt=function(e){return typeof me=="function"&&e instanceof me};function I(i,e,t){ce(i,n=>{n.call(o,e,t,G)})}const ot=function(e){let t=null;if(I(h.beforeSanitizeElements,e,null),be(e))return D(e),!0;const n=d(e.nodeName);if(I(h.uponSanitizeElement,e,{tagName:n,allowedTags:p}),ne&&e.hasChildNodes()&&!nt(e.firstElementChild)&&A(/<[/\w!]/g,e.innerHTML)&&A(/<[/\w!]/g,e.textContent)||e.nodeType===Q.progressingInstruction||ne&&e.nodeType===Q.comment&&A(/<[/\w]/g,e.data))return D(e),!0;if(!(v.tagCheck instanceof Function&&v.tagCheck(n))&&(!p[n]||j[n])){if(!j[n]&&at(n)&&(u.tagNameCheck instanceof RegExp&&A(u.tagNameCheck,n)||u.tagNameCheck instanceof Function&&u.tagNameCheck(n)))return!1;if(Se&&!H[n]){const f=te(e)||e.parentNode,_=Dt(e)||e.childNodes;if(_&&f){const m=_.length;for(let L=m-1;L>=0;--L){const C=Lt(_[L],!0);C.__removalCount=(e.__removalCount||0)+1,f.insertBefore(C,bt(e))}}}return D(e),!0}return e instanceof Ue&&!Wt(e)||(n==="noscript"||n==="noembed"||n==="noframes")&&A(/<\/no(script|embed|frames)/i,e.innerHTML)?(D(e),!0):(k&&e.nodeType===Q.text&&(t=e.textContent,ce([de,Te,Ee],f=>{t=K(t,f," ")}),e.textContent!==t&&(q(o.removed,{element:e.cloneNode()}),e.textContent=t)),I(h.afterSanitizeElements,e,null),!1)},it=function(e,t,n){if(Xe&&(t==="id"||t==="name")&&(n in l||n in Gt))return!1;if(!(ge&&!_e[t]&&A(wt,t))){if(!(We&&A(xt,t))){if(!(v.attributeCheck instanceof Function&&v.attributeCheck(t,e))){if(!T[t]||_e[t]){if(!(at(e)&&(u.tagNameCheck instanceof RegExp&&A(u.tagNameCheck,e)||u.tagNameCheck instanceof Function&&u.tagNameCheck(e))&&(u.attributeNameCheck instanceof RegExp&&A(u.attributeNameCheck,t)||u.attributeNameCheck instanceof Function&&u.attributeNameCheck(t,e))||t==="is"&&u.allowCustomizedBuiltInElements&&(u.tagNameCheck instanceof RegExp&&A(u.tagNameCheck,n)||u.tagNameCheck instanceof Function&&u.tagNameCheck(n))))return!1}else if(!Re[t]){if(!A(He,K(n,Fe,""))){if(!((t==="src"||t==="xlink:href"||t==="href")&&e!=="script"&&qt(n,"data:")===0&&$e[e])){if(!(Be&&!A(Pt,K(n,Fe,"")))){if(n)return!1}}}}}}}return!0},at=function(e){return e!=="annotation-xml"&&Ie(e,vt)},rt=function(e){I(h.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||be(e))return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:T,forceKeepAttr:void 0};let f=t.length;for(;f--;){const _=t[f],{name:m,namespaceURI:L,value:C}=_,W=d(m),De=C;let E=m==="value"?De:Kt(De);if(n.attrName=W,n.attrValue=E,n.keepAttr=!0,n.forceKeepAttr=void 0,I(h.uponSanitizeAttribute,e,n),E=n.attrValue,je&&(W==="id"||W==="name")&&(x(m,e),E=kt+E),ne&&A(/((--!?|])>)|<\/(style|title|textarea)/i,E)){x(m,e);continue}if(W==="attributename"&&Ie(E,"href")){x(m,e);continue}if(n.forceKeepAttr)continue;if(!n.keepAttr){x(m,e);continue}if(!Ye&&A(/\/>/i,E)){x(m,e);continue}k&&ce([de,Te,Ee],lt=>{E=K(E,lt," ")});const st=d(e.nodeName);if(!it(st,W,E)){x(m,e);continue}if(g&&typeof ee=="object"&&typeof ee.getAttributeType=="function"&&!L)switch(ee.getAttributeType(st,W)){case"TrustedHTML":{E=g.createHTML(E);break}case"TrustedScriptURL":{E=g.createScriptURL(E);break}}if(E!==De)try{L?e.setAttributeNS(L,m,E):e.setAttribute(m,E),be(e)?D(e):ft(o.removed)}catch{x(m,e)}}I(h.afterSanitizeAttributes,e,null)},Bt=function i(e){let t=null;const n=tt(e);for(I(h.beforeSanitizeShadowDOM,e,null);t=n.nextNode();)I(h.uponSanitizeShadowNode,t,null),ot(t),rt(t),t.content instanceof O&&i(t.content);I(h.afterSanitizeShadowDOM,e,null)};return o.sanitize=function(i){let e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},t=null,n=null,f=null,_=null;if(Oe=!i,Oe&&(i="<!-->"),typeof i!="string"&&!nt(i))if(typeof i.toString=="function"){if(i=i.toString(),typeof i!="string")throw Z("dirty is not a string, aborting")}else throw Z("toString is not a function");if(!o.isSupported)return i;if(he||ye(e),o.removed=[],typeof i=="string"&&(V=!1),V){if(i.nodeName){const C=d(i.nodeName);if(!p[C]||j[C])throw Z("root node is forbidden and cannot be sanitized in-place")}}else if(i instanceof me)t=et("<!---->"),n=t.ownerDocument.importNode(i,!0),n.nodeType===Q.element&&n.nodeName==="BODY"||n.nodeName==="HTML"?t=n:t.appendChild(n);else{if(!U&&!k&&!w&&i.indexOf("<")===-1)return g&&ie?g.createHTML(i):i;if(t=et(i),!t)return U?null:ie?X:""}t&&Ae&&D(t.firstChild);const m=tt(V?i:t);for(;f=m.nextNode();)ot(f),rt(f),f.content instanceof O&&Bt(f.content);if(V)return i;if(U){if(oe)for(_=It.call(t.ownerDocument);t.firstChild;)_.appendChild(t.firstChild);else _=t;return(T.shadowroot||T.shadowrootmode)&&(_=Mt.call(a,_,!0)),_}let L=w?t.outerHTML:t.innerHTML;return w&&p["!doctype"]&&t.ownerDocument&&t.ownerDocument.doctype&&t.ownerDocument.doctype.name&&A(ht,t.ownerDocument.doctype.name)&&(L="<!DOCTYPE "+t.ownerDocument.doctype.name+`>
|
| 2 |
+
`+L),k&&ce([de,Te,Ee],C=>{L=K(L,C," ")}),g&&ie?g.createHTML(L):L},o.setConfig=function(){let i=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};ye(i),he=!0},o.clearConfig=function(){G=null,he=!1},o.isValidAttribute=function(i,e,t){G||ye({});const n=d(i),f=d(e);return it(n,f,t)},o.addHook=function(i,e){typeof e=="function"&&q(h[i],e)},o.removeHook=function(i,e){if(e!==void 0){const t=Vt(h[i],e);return t===-1?void 0:$t(h[i],t,1)[0]}return ft(h[i])},o.removeHooks=function(i){h[i]=[]},o.removeAllHooks=function(){h=Et()},o}var mn=At();export{mn as default};
|
dist/icon-192.png
ADDED
|
|
Git LFS Details
|
dist/icon-512.png
ADDED
|
|
Git LFS Details
|
dist/index.html
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="mr">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8" />
|
| 6 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 7 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 8 |
+
<title>Pattanshetty Inventory</title>
|
| 9 |
+
|
| 10 |
+
<!-- PWA Manifest -->
|
| 11 |
+
<link rel="manifest" href="/manifest.json" />
|
| 12 |
+
<meta name="theme-color" content="#0d9488" />
|
| 13 |
+
<meta name="mobile-web-app-capable" content="yes" />
|
| 14 |
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
| 15 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
| 16 |
+
<meta name="apple-mobile-web-app-title" content="Pattanshetty" />
|
| 17 |
+
<link rel="apple-touch-icon" href="/icon-192.png" />
|
| 18 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 19 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 20 |
+
<style>
|
| 21 |
+
body {
|
| 22 |
+
font-family: 'Inter', sans-serif;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/* Hide scrollbar for Chrome, Safari and Opera */
|
| 26 |
+
.no-scrollbar::-webkit-scrollbar {
|
| 27 |
+
display: none;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* Hide scrollbar for IE, Edge and Firefox */
|
| 31 |
+
.no-scrollbar {
|
| 32 |
+
-ms-overflow-style: none;
|
| 33 |
+
/* IE and Edge */
|
| 34 |
+
scrollbar-width: none;
|
| 35 |
+
/* Firefox */
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/* Chrome, Safari, Edge, Opera - Hide number input arrows */
|
| 39 |
+
input::-webkit-outer-spin-button,
|
| 40 |
+
input::-webkit-inner-spin-button {
|
| 41 |
+
-webkit-appearance: none;
|
| 42 |
+
margin: 0;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/* Firefox - Hide number input arrows */
|
| 46 |
+
input[type=number] {
|
| 47 |
+
-moz-appearance: textfield;
|
| 48 |
+
}
|
| 49 |
+
</style>
|
| 50 |
+
|
| 51 |
+
<link rel="stylesheet" href="/index.css">
|
| 52 |
+
<script type="importmap">
|
| 53 |
+
{
|
| 54 |
+
"imports": {
|
| 55 |
+
"react-router-dom": "https://aistudiocdn.com/react-router-dom@^7.9.6",
|
| 56 |
+
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
|
| 57 |
+
"react/": "https://aistudiocdn.com/react@^19.2.0/",
|
| 58 |
+
"react": "https://aistudiocdn.com/react@^19.2.0",
|
| 59 |
+
"lucide-react": "https://aistudiocdn.com/lucide-react@^0.554.0",
|
| 60 |
+
"recharts": "https://aistudiocdn.com/recharts@^3.4.1"
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
</script>
|
| 64 |
+
<script type="module" crossorigin src="/assets/index-DEfz7c9M.js"></script>
|
| 65 |
+
</head>
|
| 66 |
+
|
| 67 |
+
<body class="bg-gray-50 text-gray-900">
|
| 68 |
+
<div id="root"></div>
|
| 69 |
+
|
| 70 |
+
<!-- Service Worker Registration -->
|
| 71 |
+
<script>
|
| 72 |
+
if ('serviceWorker' in navigator) {
|
| 73 |
+
window.addEventListener('load', () => {
|
| 74 |
+
navigator.serviceWorker.register('/service-worker.js')
|
| 75 |
+
.then((registration) => {
|
| 76 |
+
console.log('[SW] Registered successfully:', registration.scope);
|
| 77 |
+
|
| 78 |
+
// Check for updates
|
| 79 |
+
registration.addEventListener('updatefound', () => {
|
| 80 |
+
const newWorker = registration.installing;
|
| 81 |
+
newWorker.addEventListener('statechange', () => {
|
| 82 |
+
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
| 83 |
+
console.log('[SW] New version available! Refresh to update.');
|
| 84 |
+
}
|
| 85 |
+
});
|
| 86 |
+
});
|
| 87 |
+
})
|
| 88 |
+
.catch((error) => {
|
| 89 |
+
console.error('[SW] Registration failed:', error);
|
| 90 |
+
});
|
| 91 |
+
});
|
| 92 |
+
}
|
| 93 |
+
</script>
|
| 94 |
+
</body>
|
| 95 |
+
|
| 96 |
+
</html>
|
dist/manifest.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Pattanshetty Inventory",
|
| 3 |
+
"short_name": "Pattanshetty",
|
| 4 |
+
"description": "Inventory Management System for Mirchi Trading",
|
| 5 |
+
"start_url": "/",
|
| 6 |
+
"display": "standalone",
|
| 7 |
+
"background_color": "#f9fafb",
|
| 8 |
+
"theme_color": "#0d9488",
|
| 9 |
+
"orientation": "portrait-primary",
|
| 10 |
+
"icons": [
|
| 11 |
+
{
|
| 12 |
+
"src": "/icon-192.png",
|
| 13 |
+
"sizes": "192x192",
|
| 14 |
+
"type": "image/png",
|
| 15 |
+
"purpose": "any maskable"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"src": "/icon-512.png",
|
| 19 |
+
"sizes": "512x512",
|
| 20 |
+
"type": "image/png",
|
| 21 |
+
"purpose": "any maskable"
|
| 22 |
+
}
|
| 23 |
+
],
|
| 24 |
+
"categories": [
|
| 25 |
+
"business",
|
| 26 |
+
"productivity"
|
| 27 |
+
],
|
| 28 |
+
"screenshots": [],
|
| 29 |
+
"shortcuts": [
|
| 30 |
+
{
|
| 31 |
+
"name": "New Purchase Bill",
|
| 32 |
+
"short_name": "Purchase",
|
| 33 |
+
"description": "Create new purchase bill",
|
| 34 |
+
"url": "/awaak",
|
| 35 |
+
"icons": []
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"name": "New Sales Bill",
|
| 39 |
+
"short_name": "Sales",
|
| 40 |
+
"description": "Create new sales bill",
|
| 41 |
+
"url": "/jawaak",
|
| 42 |
+
"icons": []
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"name": "Party Ledger",
|
| 46 |
+
"short_name": "Ledger",
|
| 47 |
+
"description": "View party ledger",
|
| 48 |
+
"url": "/ledger",
|
| 49 |
+
"icons": []
|
| 50 |
+
}
|
| 51 |
+
]
|
| 52 |
+
}
|
dist/service-worker.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const CACHE_NAME = 'pattanshetty-v2';
|
| 2 |
+
const urlsToCache = [
|
| 3 |
+
'/',
|
| 4 |
+
'/index.html',
|
| 5 |
+
];
|
| 6 |
+
|
| 7 |
+
// Install event - cache resources
|
| 8 |
+
self.addEventListener('install', (event) => {
|
| 9 |
+
console.log('[ServiceWorker] Installing...');
|
| 10 |
+
event.waitUntil(
|
| 11 |
+
caches.open(CACHE_NAME)
|
| 12 |
+
.then((cache) => {
|
| 13 |
+
console.log('[ServiceWorker] Caching app shell');
|
| 14 |
+
return cache.addAll(urlsToCache).catch((err) => {
|
| 15 |
+
console.error('[ServiceWorker] Cache addAll failed:', err);
|
| 16 |
+
});
|
| 17 |
+
})
|
| 18 |
+
.catch((err) => {
|
| 19 |
+
console.error('[ServiceWorker] Cache open failed:', err);
|
| 20 |
+
})
|
| 21 |
+
);
|
| 22 |
+
self.skipWaiting();
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
// Activate event - clean up old caches
|
| 26 |
+
self.addEventListener('activate', (event) => {
|
| 27 |
+
console.log('[ServiceWorker] Activating...');
|
| 28 |
+
event.waitUntil(
|
| 29 |
+
caches.keys().then((cacheNames) => {
|
| 30 |
+
return Promise.all(
|
| 31 |
+
cacheNames.map((cacheName) => {
|
| 32 |
+
if (cacheName !== CACHE_NAME) {
|
| 33 |
+
console.log('[ServiceWorker] Deleting old cache:', cacheName);
|
| 34 |
+
return caches.delete(cacheName);
|
| 35 |
+
}
|
| 36 |
+
})
|
| 37 |
+
);
|
| 38 |
+
}).catch((err) => {
|
| 39 |
+
console.error('[ServiceWorker] Activation failed:', err);
|
| 40 |
+
})
|
| 41 |
+
);
|
| 42 |
+
self.clients.claim();
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
// Fetch event - Network first, fallback to cache
|
| 46 |
+
self.addEventListener('fetch', (event) => {
|
| 47 |
+
// Skip non-GET requests
|
| 48 |
+
if (event.request.method !== 'GET') {
|
| 49 |
+
return;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
event.respondWith(
|
| 53 |
+
fetch(event.request)
|
| 54 |
+
.then((response) => {
|
| 55 |
+
// Check if valid response
|
| 56 |
+
if (!response || response.status !== 200 || response.type === 'error') {
|
| 57 |
+
return response;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Clone the response
|
| 61 |
+
const responseToCache = response.clone();
|
| 62 |
+
|
| 63 |
+
caches.open(CACHE_NAME)
|
| 64 |
+
.then((cache) => {
|
| 65 |
+
cache.put(event.request, responseToCache);
|
| 66 |
+
})
|
| 67 |
+
.catch((err) => {
|
| 68 |
+
console.error('[ServiceWorker] Cache put failed:', err);
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
return response;
|
| 72 |
+
})
|
| 73 |
+
.catch((err) => {
|
| 74 |
+
console.log('[ServiceWorker] Fetch failed, trying cache:', err);
|
| 75 |
+
// Network failed, try cache
|
| 76 |
+
return caches.match(event.request)
|
| 77 |
+
.then((cachedResponse) => {
|
| 78 |
+
if (cachedResponse) {
|
| 79 |
+
return cachedResponse;
|
| 80 |
+
}
|
| 81 |
+
// Return offline page or error
|
| 82 |
+
return new Response('Offline - No cached version available', {
|
| 83 |
+
status: 503,
|
| 84 |
+
statusText: 'Service Unavailable',
|
| 85 |
+
headers: new Headers({
|
| 86 |
+
'Content-Type': 'text/plain'
|
| 87 |
+
})
|
| 88 |
+
});
|
| 89 |
+
});
|
| 90 |
+
})
|
| 91 |
+
);
|
| 92 |
+
});
|
firebase.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"hosting": {
|
| 3 |
+
"public": "dist",
|
| 4 |
+
"ignore": [
|
| 5 |
+
"firebase.json",
|
| 6 |
+
"**/.*",
|
| 7 |
+
"**/node_modules/**"
|
| 8 |
+
]
|
| 9 |
+
}
|
| 10 |
+
}
|
index.html
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="mr">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8" />
|
| 6 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 7 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 8 |
+
<title>Pattanshetty Inventory</title>
|
| 9 |
+
|
| 10 |
+
<!-- PWA Manifest -->
|
| 11 |
+
<link rel="manifest" href="/manifest.json" />
|
| 12 |
+
<meta name="theme-color" content="#0d9488" />
|
| 13 |
+
<meta name="mobile-web-app-capable" content="yes" />
|
| 14 |
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
| 15 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
| 16 |
+
<meta name="apple-mobile-web-app-title" content="Pattanshetty" />
|
| 17 |
+
<link rel="apple-touch-icon" href="/icon-192.png" />
|
| 18 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 19 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 20 |
+
<style>
|
| 21 |
+
body {
|
| 22 |
+
font-family: 'Inter', sans-serif;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/* Hide scrollbar for Chrome, Safari and Opera */
|
| 26 |
+
.no-scrollbar::-webkit-scrollbar {
|
| 27 |
+
display: none;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* Hide scrollbar for IE, Edge and Firefox */
|
| 31 |
+
.no-scrollbar {
|
| 32 |
+
-ms-overflow-style: none;
|
| 33 |
+
/* IE and Edge */
|
| 34 |
+
scrollbar-width: none;
|
| 35 |
+
/* Firefox */
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/* Chrome, Safari, Edge, Opera - Hide number input arrows */
|
| 39 |
+
input::-webkit-outer-spin-button,
|
| 40 |
+
input::-webkit-inner-spin-button {
|
| 41 |
+
-webkit-appearance: none;
|
| 42 |
+
margin: 0;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/* Firefox - Hide number input arrows */
|
| 46 |
+
input[type=number] {
|
| 47 |
+
-moz-appearance: textfield;
|
| 48 |
+
}
|
| 49 |
+
</style>
|
| 50 |
+
<script type="importmap">
|
| 51 |
+
{
|
| 52 |
+
"imports": {
|
| 53 |
+
"react-router-dom": "https://aistudiocdn.com/react-router-dom@^7.9.6",
|
| 54 |
+
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
|
| 55 |
+
"react/": "https://aistudiocdn.com/react@^19.2.0/",
|
| 56 |
+
"react": "https://aistudiocdn.com/react@^19.2.0",
|
| 57 |
+
"lucide-react": "https://aistudiocdn.com/lucide-react@^0.554.0",
|
| 58 |
+
"recharts": "https://aistudiocdn.com/recharts@^3.4.1"
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
</script>
|
| 62 |
+
<link rel="stylesheet" href="/index.css">
|
| 63 |
+
</head>
|
| 64 |
+
|
| 65 |
+
<body class="bg-gray-50 text-gray-900">
|
| 66 |
+
<div id="root"></div>
|
| 67 |
+
<script type="module" src="/index.tsx"></script>
|
| 68 |
+
|
| 69 |
+
<!-- Service Worker Registration -->
|
| 70 |
+
<script>
|
| 71 |
+
if ('serviceWorker' in navigator) {
|
| 72 |
+
window.addEventListener('load', () => {
|
| 73 |
+
navigator.serviceWorker.register('/service-worker.js')
|
| 74 |
+
.then((registration) => {
|
| 75 |
+
console.log('[SW] Registered successfully:', registration.scope);
|
| 76 |
+
|
| 77 |
+
// Check for updates
|
| 78 |
+
registration.addEventListener('updatefound', () => {
|
| 79 |
+
const newWorker = registration.installing;
|
| 80 |
+
newWorker.addEventListener('statechange', () => {
|
| 81 |
+
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
| 82 |
+
console.log('[SW] New version available! Refresh to update.');
|
| 83 |
+
}
|
| 84 |
+
});
|
| 85 |
+
});
|
| 86 |
+
})
|
| 87 |
+
.catch((error) => {
|
| 88 |
+
console.error('[SW] Registration failed:', error);
|
| 89 |
+
});
|
| 90 |
+
});
|
| 91 |
+
}
|
| 92 |
+
</script>
|
| 93 |
+
</body>
|
| 94 |
+
|
| 95 |
+
</html>
|
index.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import ReactDOM from 'react-dom/client';
|
| 3 |
+
import App from './App';
|
| 4 |
+
|
| 5 |
+
const rootElement = document.getElementById('root');
|
| 6 |
+
if (!rootElement) {
|
| 7 |
+
throw new Error("Could not find root element to mount to");
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const root = ReactDOM.createRoot(rootElement);
|
| 11 |
+
root.render(
|
| 12 |
+
<React.StrictMode>
|
| 13 |
+
<App />
|
| 14 |
+
</React.StrictMode>
|
| 15 |
+
);
|
metadata.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Mirchi Vyapar Manager",
|
| 3 |
+
"description": "A comprehensive Chili Trading Management System for managing Purchases (Jawaak), Sales (Awaak), Inventory (Stock), and Financial Ledgers with Marathi interface support.",
|
| 4 |
+
"requestFramePermissions": []
|
| 5 |
+
}
|
nginx.conf
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
server {
|
| 2 |
+
listen 7860;
|
| 3 |
+
server_name _;
|
| 4 |
+
|
| 5 |
+
root /usr/share/nginx/html;
|
| 6 |
+
index index.html;
|
| 7 |
+
|
| 8 |
+
# Enable gzip compression
|
| 9 |
+
gzip on;
|
| 10 |
+
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
| 11 |
+
|
| 12 |
+
# Handle client-side routing
|
| 13 |
+
location / {
|
| 14 |
+
try_files $uri $uri/ /index.html;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
# Cache static assets
|
| 18 |
+
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
| 19 |
+
expires 1y;
|
| 20 |
+
add_header Cache-Control "public, immutable";
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
# Security headers
|
| 24 |
+
add_header X-Frame-Options "SAMEORIGIN" always;
|
| 25 |
+
add_header X-Content-Type-Options "nosniff" always;
|
| 26 |
+
add_header X-XSS-Protection "1; mode=block" always;
|
| 27 |
+
}
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "mirchi-vyapar-manager",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"html2canvas": "^1.4.1",
|
| 13 |
+
"jspdf": "^3.0.4",
|
| 14 |
+
"jspdf-autotable": "^5.0.2",
|
| 15 |
+
"lucide-react": "^0.554.0",
|
| 16 |
+
"react": "^19.2.0",
|
| 17 |
+
"react-dom": "^19.2.0",
|
| 18 |
+
"react-router-dom": "^7.9.6",
|
| 19 |
+
"recharts": "^3.4.1",
|
| 20 |
+
"xlsx": "^0.18.5",
|
| 21 |
+
"xlsx-js-style": "^1.2.0"
|
| 22 |
+
},
|
| 23 |
+
"devDependencies": {
|
| 24 |
+
"@types/node": "^22.14.0",
|
| 25 |
+
"@vitejs/plugin-react": "^5.0.0",
|
| 26 |
+
"typescript": "~5.8.2",
|
| 27 |
+
"vite": "^6.2.0"
|
| 28 |
+
}
|
| 29 |
+
}
|
pages/Analysis.tsx
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useMemo, useState } from 'react';
|
| 2 |
+
import { getActiveLots, getTransactions } from '../services/db';
|
| 3 |
+
import { BillType, Lot, Transaction } from '../types';
|
| 4 |
+
import { TrendingUp, Search, Download, ArrowLeft } from 'lucide-react';
|
| 5 |
+
import { exportAnalysisReport, exportLotDetailAnalysis } from '../utils/exportToExcel';
|
| 6 |
+
|
| 7 |
+
interface LotAnalysisRow {
|
| 8 |
+
lotId: string;
|
| 9 |
+
lotNumber: string;
|
| 10 |
+
mirchiName: string;
|
| 11 |
+
purchaseQty: number;
|
| 12 |
+
purchaseValue: number;
|
| 13 |
+
avgPurchaseRate: number;
|
| 14 |
+
soldQty: number;
|
| 15 |
+
salesValue: number;
|
| 16 |
+
avgSaleRate: number;
|
| 17 |
+
realizedProfit: number;
|
| 18 |
+
marginPercent: number;
|
| 19 |
+
remainingQty: number;
|
| 20 |
+
remainingValueAtCost: number;
|
| 21 |
+
isActive: boolean;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
interface LotDetailRow {
|
| 25 |
+
id: string;
|
| 26 |
+
date: string;
|
| 27 |
+
billNo: string;
|
| 28 |
+
typeLabel: string;
|
| 29 |
+
directionLabel: string;
|
| 30 |
+
qtyKg: number;
|
| 31 |
+
ratePerKg: number;
|
| 32 |
+
grossAmount: number;
|
| 33 |
+
expensesAllocated: number;
|
| 34 |
+
netAmount: number;
|
| 35 |
+
profitImpact: number;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
const Analysis: React.FC = () => {
|
| 39 |
+
const [lots, setLots] = useState<Lot[]>([]);
|
| 40 |
+
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
| 41 |
+
const [searchTerm, setSearchTerm] = useState('');
|
| 42 |
+
const [viewMode, setViewMode] = useState<'summary' | 'detail'>('summary');
|
| 43 |
+
const [selectedLotId, setSelectedLotId] = useState<string | null>(null);
|
| 44 |
+
const [selectedLotNumber, setSelectedLotNumber] = useState<string | null>(null);
|
| 45 |
+
const [selectedLotMirchiName, setSelectedLotMirchiName] = useState<string | null>(null);
|
| 46 |
+
const [lotFilter, setLotFilter] = useState<'all' | 'active' | 'closed'>('all');
|
| 47 |
+
|
| 48 |
+
useEffect(() => {
|
| 49 |
+
const fetchData = async () => {
|
| 50 |
+
const [lotsData, transactionsData] = await Promise.all([
|
| 51 |
+
getActiveLots(),
|
| 52 |
+
getTransactions(),
|
| 53 |
+
]);
|
| 54 |
+
setLots(lotsData);
|
| 55 |
+
setTransactions(transactionsData);
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
fetchData();
|
| 59 |
+
}, []);
|
| 60 |
+
|
| 61 |
+
const analysisRowsAll: LotAnalysisRow[] = useMemo(() => {
|
| 62 |
+
if (!transactions.length) {
|
| 63 |
+
return [];
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
const lotMetaById = new Map<string, Lot>();
|
| 67 |
+
const lotMetaByNumber = new Map<string, Lot>();
|
| 68 |
+
lots.forEach((lot) => {
|
| 69 |
+
lotMetaById.set(lot.id, lot);
|
| 70 |
+
if (lot.lot_number) {
|
| 71 |
+
lotMetaByNumber.set(lot.lot_number.toLowerCase(), lot);
|
| 72 |
+
}
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
const byLot: Record<
|
| 76 |
+
string,
|
| 77 |
+
{
|
| 78 |
+
lotId: string;
|
| 79 |
+
lotNumber: string;
|
| 80 |
+
mirchiName: string;
|
| 81 |
+
purchaseQty: number;
|
| 82 |
+
purchaseValue: number;
|
| 83 |
+
soldQty: number;
|
| 84 |
+
salesValue: number;
|
| 85 |
+
}
|
| 86 |
+
> = {};
|
| 87 |
+
|
| 88 |
+
transactions.forEach((tx) => {
|
| 89 |
+
const txTotalWeight =
|
| 90 |
+
tx.items.reduce((sum, item) => sum + item.net_weight, 0) || 1;
|
| 91 |
+
|
| 92 |
+
tx.items.forEach((item) => {
|
| 93 |
+
const rawLotId = item.lot_id as string | undefined;
|
| 94 |
+
const rawLotNumber = (item as any).lot_number as string | undefined;
|
| 95 |
+
|
| 96 |
+
const metaById = rawLotId ? lotMetaById.get(rawLotId) : undefined;
|
| 97 |
+
const metaByNumber = rawLotNumber
|
| 98 |
+
? lotMetaByNumber.get(rawLotNumber.toLowerCase())
|
| 99 |
+
: undefined;
|
| 100 |
+
const meta = metaById || metaByNumber;
|
| 101 |
+
|
| 102 |
+
const key = meta?.id || rawLotId || rawLotNumber;
|
| 103 |
+
if (!key) {
|
| 104 |
+
return;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
let existing = byLot[key];
|
| 108 |
+
|
| 109 |
+
if (!existing) {
|
| 110 |
+
const fallbackName = item.mirchi_name || 'Unknown';
|
| 111 |
+
|
| 112 |
+
existing = {
|
| 113 |
+
lotId: meta?.id || rawLotId || key,
|
| 114 |
+
lotNumber: meta?.lot_number || rawLotNumber || key.slice(0, 8),
|
| 115 |
+
mirchiName: meta?.mirchi_name || fallbackName,
|
| 116 |
+
purchaseQty: 0,
|
| 117 |
+
purchaseValue: 0,
|
| 118 |
+
soldQty: 0,
|
| 119 |
+
salesValue: 0,
|
| 120 |
+
};
|
| 121 |
+
byLot[key] = existing;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
const weightShare = item.net_weight / txTotalWeight;
|
| 125 |
+
const allocatedExpenses = tx.total_expenses * weightShare;
|
| 126 |
+
|
| 127 |
+
if (tx.bill_type === BillType.AWAAK) {
|
| 128 |
+
const sign = tx.is_return ? -1 : 1;
|
| 129 |
+
const cost = item.item_total + allocatedExpenses;
|
| 130 |
+
existing.purchaseQty += sign * item.net_weight;
|
| 131 |
+
existing.purchaseValue += sign * cost;
|
| 132 |
+
} else {
|
| 133 |
+
const sign = tx.is_return ? -1 : 1;
|
| 134 |
+
const revenue = item.item_total - allocatedExpenses;
|
| 135 |
+
existing.soldQty += sign * item.net_weight;
|
| 136 |
+
existing.salesValue += sign * revenue;
|
| 137 |
+
}
|
| 138 |
+
});
|
| 139 |
+
});
|
| 140 |
+
|
| 141 |
+
return Object.values(byLot).map((entry) => {
|
| 142 |
+
let { purchaseQty, purchaseValue, soldQty, salesValue } = entry;
|
| 143 |
+
|
| 144 |
+
if (purchaseQty < 0) {
|
| 145 |
+
purchaseQty = 0;
|
| 146 |
+
purchaseValue = 0;
|
| 147 |
+
}
|
| 148 |
+
if (soldQty < 0) {
|
| 149 |
+
soldQty = 0;
|
| 150 |
+
salesValue = 0;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
const avgPurchaseRate = purchaseQty > 0 ? purchaseValue / purchaseQty : 0;
|
| 154 |
+
const avgSaleRate = soldQty > 0 ? salesValue / soldQty : 0;
|
| 155 |
+
const realizedProfit = salesValue - avgPurchaseRate * soldQty;
|
| 156 |
+
const marginPercent =
|
| 157 |
+
purchaseValue > 0 ? (realizedProfit / purchaseValue) * 100 : 0;
|
| 158 |
+
|
| 159 |
+
const metaFromId = lotMetaById.get(entry.lotId);
|
| 160 |
+
const metaFromNumber = lotMetaByNumber.get(entry.lotNumber.toLowerCase());
|
| 161 |
+
const meta = metaFromId || metaFromNumber;
|
| 162 |
+
const isActive = !!meta;
|
| 163 |
+
const remainingQty = isActive
|
| 164 |
+
? Math.max(0, meta?.remaining_quantity ?? 0)
|
| 165 |
+
: 0;
|
| 166 |
+
const remainingValueAtCost = remainingQty * avgPurchaseRate;
|
| 167 |
+
|
| 168 |
+
return {
|
| 169 |
+
lotId: entry.lotId,
|
| 170 |
+
lotNumber: entry.lotNumber,
|
| 171 |
+
mirchiName: entry.mirchiName,
|
| 172 |
+
purchaseQty,
|
| 173 |
+
purchaseValue,
|
| 174 |
+
avgPurchaseRate,
|
| 175 |
+
soldQty,
|
| 176 |
+
salesValue,
|
| 177 |
+
avgSaleRate,
|
| 178 |
+
realizedProfit,
|
| 179 |
+
marginPercent,
|
| 180 |
+
remainingQty,
|
| 181 |
+
remainingValueAtCost,
|
| 182 |
+
isActive,
|
| 183 |
+
};
|
| 184 |
+
});
|
| 185 |
+
}, [lots, transactions]);
|
| 186 |
+
|
| 187 |
+
const analysisRows: LotAnalysisRow[] = useMemo(
|
| 188 |
+
() =>
|
| 189 |
+
analysisRowsAll.filter((row) => {
|
| 190 |
+
const term = searchTerm.toLowerCase();
|
| 191 |
+
const matchesSearch =
|
| 192 |
+
row.mirchiName.toLowerCase().includes(term) ||
|
| 193 |
+
row.lotNumber.toLowerCase().includes(term)
|
| 194 |
+
|| !term;
|
| 195 |
+
|
| 196 |
+
if (!matchesSearch) return false;
|
| 197 |
+
|
| 198 |
+
if (lotFilter === 'active') {
|
| 199 |
+
return row.isActive;
|
| 200 |
+
}
|
| 201 |
+
if (lotFilter === 'closed') {
|
| 202 |
+
return !row.isActive;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
return true;
|
| 206 |
+
}),
|
| 207 |
+
[analysisRowsAll, searchTerm, lotFilter]
|
| 208 |
+
);
|
| 209 |
+
|
| 210 |
+
const totals = useMemo(
|
| 211 |
+
() =>
|
| 212 |
+
analysisRows.reduce(
|
| 213 |
+
(acc, row) => {
|
| 214 |
+
acc.totalPurchaseValue += row.purchaseValue;
|
| 215 |
+
acc.totalSalesValue += row.salesValue;
|
| 216 |
+
acc.totalRealizedProfit += row.realizedProfit;
|
| 217 |
+
acc.totalRemainingValueAtCost += row.remainingValueAtCost;
|
| 218 |
+
return acc;
|
| 219 |
+
},
|
| 220 |
+
{
|
| 221 |
+
totalPurchaseValue: 0,
|
| 222 |
+
totalSalesValue: 0,
|
| 223 |
+
totalRealizedProfit: 0,
|
| 224 |
+
totalRemainingValueAtCost: 0,
|
| 225 |
+
}
|
| 226 |
+
),
|
| 227 |
+
[analysisRows]
|
| 228 |
+
);
|
| 229 |
+
|
| 230 |
+
const lotsInProfit = useMemo(
|
| 231 |
+
() => analysisRows.filter((row) => row.realizedProfit > 0).length,
|
| 232 |
+
[analysisRows]
|
| 233 |
+
);
|
| 234 |
+
const lotsInLoss = useMemo(
|
| 235 |
+
() => analysisRows.filter((row) => row.realizedProfit < 0).length,
|
| 236 |
+
[analysisRows]
|
| 237 |
+
);
|
| 238 |
+
|
| 239 |
+
const selectedLotSummary = useMemo(
|
| 240 |
+
() =>
|
| 241 |
+
selectedLotId
|
| 242 |
+
? analysisRows.find((row) => row.lotId === selectedLotId) || null
|
| 243 |
+
: null,
|
| 244 |
+
[analysisRows, selectedLotId]
|
| 245 |
+
);
|
| 246 |
+
|
| 247 |
+
const detailRows: LotDetailRow[] = useMemo(() => {
|
| 248 |
+
if (!selectedLotId) {
|
| 249 |
+
return [];
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
const avgPurchaseRate =
|
| 253 |
+
selectedLotSummary && selectedLotSummary.purchaseQty > 0
|
| 254 |
+
? selectedLotSummary.purchaseValue / selectedLotSummary.purchaseQty
|
| 255 |
+
: 0;
|
| 256 |
+
|
| 257 |
+
const rows: LotDetailRow[] = [];
|
| 258 |
+
|
| 259 |
+
transactions.forEach((tx) => {
|
| 260 |
+
const lotItems = tx.items.filter((item) => item.lot_id === selectedLotId);
|
| 261 |
+
if (lotItems.length === 0) {
|
| 262 |
+
return;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
const txTotalWeight =
|
| 266 |
+
tx.items.reduce((sum, item) => sum + item.net_weight, 0) || 1;
|
| 267 |
+
|
| 268 |
+
lotItems.forEach((item) => {
|
| 269 |
+
const weightShare = item.net_weight / txTotalWeight;
|
| 270 |
+
const allocatedExpenses = tx.total_expenses * weightShare;
|
| 271 |
+
|
| 272 |
+
let typeLabel = '';
|
| 273 |
+
let directionLabel = '';
|
| 274 |
+
let grossAmount = item.item_total;
|
| 275 |
+
let netAmount = grossAmount;
|
| 276 |
+
let profitImpact = 0;
|
| 277 |
+
|
| 278 |
+
if (tx.bill_type === BillType.AWAAK) {
|
| 279 |
+
if (tx.is_return) {
|
| 280 |
+
typeLabel = 'Purchase Return';
|
| 281 |
+
directionLabel = 'Export';
|
| 282 |
+
grossAmount = -item.item_total;
|
| 283 |
+
netAmount = grossAmount - allocatedExpenses;
|
| 284 |
+
} else {
|
| 285 |
+
typeLabel = 'Purchase';
|
| 286 |
+
directionLabel = 'Import';
|
| 287 |
+
netAmount = grossAmount + allocatedExpenses;
|
| 288 |
+
}
|
| 289 |
+
} else {
|
| 290 |
+
if (tx.is_return) {
|
| 291 |
+
typeLabel = 'Sales Return';
|
| 292 |
+
directionLabel = 'Import';
|
| 293 |
+
grossAmount = -item.item_total;
|
| 294 |
+
netAmount = grossAmount + allocatedExpenses;
|
| 295 |
+
} else {
|
| 296 |
+
typeLabel = 'Sale';
|
| 297 |
+
directionLabel = 'Export';
|
| 298 |
+
netAmount = grossAmount - allocatedExpenses;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
const cost = item.net_weight * avgPurchaseRate;
|
| 302 |
+
profitImpact = netAmount - cost;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
const displayDate = tx.bill_date
|
| 306 |
+
? tx.bill_date.split('T')[0]
|
| 307 |
+
: tx.bill_date;
|
| 308 |
+
|
| 309 |
+
rows.push({
|
| 310 |
+
id: `${tx.id}-${item.id}`,
|
| 311 |
+
date: displayDate,
|
| 312 |
+
billNo: tx.bill_number,
|
| 313 |
+
typeLabel,
|
| 314 |
+
directionLabel,
|
| 315 |
+
qtyKg: item.net_weight,
|
| 316 |
+
ratePerKg: item.rate_per_kg,
|
| 317 |
+
grossAmount,
|
| 318 |
+
expensesAllocated: allocatedExpenses,
|
| 319 |
+
netAmount,
|
| 320 |
+
profitImpact,
|
| 321 |
+
});
|
| 322 |
+
});
|
| 323 |
+
});
|
| 324 |
+
|
| 325 |
+
rows.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
| 326 |
+
return rows;
|
| 327 |
+
}, [selectedLotId, selectedLotSummary, transactions]);
|
| 328 |
+
|
| 329 |
+
const handleExport = () => {
|
| 330 |
+
if (viewMode === 'detail' && selectedLotSummary) {
|
| 331 |
+
const lotSummaryForExport = {
|
| 332 |
+
lotNumber: selectedLotSummary.lotNumber,
|
| 333 |
+
mirchiName: selectedLotSummary.mirchiName,
|
| 334 |
+
purchaseQty: selectedLotSummary.purchaseQty,
|
| 335 |
+
purchaseValue: selectedLotSummary.purchaseValue,
|
| 336 |
+
avgPurchaseRate: selectedLotSummary.avgPurchaseRate,
|
| 337 |
+
soldQty: selectedLotSummary.soldQty,
|
| 338 |
+
salesValue: selectedLotSummary.salesValue,
|
| 339 |
+
avgSaleRate: selectedLotSummary.avgSaleRate,
|
| 340 |
+
realizedProfit: selectedLotSummary.realizedProfit,
|
| 341 |
+
marginPercent: selectedLotSummary.marginPercent,
|
| 342 |
+
remainingQty: selectedLotSummary.remainingQty,
|
| 343 |
+
remainingValueAtCost: selectedLotSummary.remainingValueAtCost,
|
| 344 |
+
};
|
| 345 |
+
|
| 346 |
+
const detailExportRows = detailRows.map((row) => ({
|
| 347 |
+
date: row.date,
|
| 348 |
+
billNo: row.billNo,
|
| 349 |
+
typeLabel: row.typeLabel,
|
| 350 |
+
directionLabel: row.directionLabel,
|
| 351 |
+
qtyKg: row.qtyKg,
|
| 352 |
+
ratePerKg: row.ratePerKg,
|
| 353 |
+
grossAmount: row.grossAmount,
|
| 354 |
+
expensesAllocated: row.expensesAllocated,
|
| 355 |
+
netAmount: row.netAmount,
|
| 356 |
+
profitImpact: row.profitImpact,
|
| 357 |
+
}));
|
| 358 |
+
|
| 359 |
+
exportLotDetailAnalysis(lotSummaryForExport, detailExportRows);
|
| 360 |
+
return;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
if (analysisRows.length === 0) {
|
| 364 |
+
return;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
const rowsToExport = analysisRows.map((row) => ({
|
| 368 |
+
lotNumber: row.lotNumber,
|
| 369 |
+
mirchiName: row.mirchiName,
|
| 370 |
+
purchaseQty: row.purchaseQty,
|
| 371 |
+
purchaseValue: row.purchaseValue,
|
| 372 |
+
avgPurchaseRate: row.avgPurchaseRate,
|
| 373 |
+
soldQty: row.soldQty,
|
| 374 |
+
salesValue: row.salesValue,
|
| 375 |
+
avgSaleRate: row.avgSaleRate,
|
| 376 |
+
realizedProfit: row.realizedProfit,
|
| 377 |
+
marginPercent: row.marginPercent,
|
| 378 |
+
remainingQty: row.remainingQty,
|
| 379 |
+
remainingValueAtCost: row.remainingValueAtCost,
|
| 380 |
+
}));
|
| 381 |
+
|
| 382 |
+
exportAnalysisReport(rowsToExport);
|
| 383 |
+
};
|
| 384 |
+
|
| 385 |
+
const profitColor =
|
| 386 |
+
totals.totalRealizedProfit > 0
|
| 387 |
+
? 'text-green-600'
|
| 388 |
+
: totals.totalRealizedProfit < 0
|
| 389 |
+
? 'text-red-600'
|
| 390 |
+
: 'text-gray-700';
|
| 391 |
+
|
| 392 |
+
return (
|
| 393 |
+
<div className="space-y-4">
|
| 394 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
| 395 |
+
<div className="sticky top-0 z-10 bg-white p-4 md:p-6 border-b flex flex-col md:flex-row md:items-center justify-between gap-4">
|
| 396 |
+
<div className="flex items-center gap-2">
|
| 397 |
+
{viewMode === 'detail' && (
|
| 398 |
+
<button
|
| 399 |
+
onClick={() => {
|
| 400 |
+
setViewMode('summary');
|
| 401 |
+
setSelectedLotId(null);
|
| 402 |
+
setSelectedLotNumber(null);
|
| 403 |
+
setSelectedLotMirchiName(null);
|
| 404 |
+
}}
|
| 405 |
+
className="mr-1 p-1 hover:bg-gray-100 rounded-full"
|
| 406 |
+
>
|
| 407 |
+
<ArrowLeft size={20} className="text-gray-600" />
|
| 408 |
+
</button>
|
| 409 |
+
)}
|
| 410 |
+
<TrendingUp className="text-teal-600" />
|
| 411 |
+
<h2 className="text-lg md:text-xl font-bold text-gray-800">
|
| 412 |
+
{viewMode === 'summary'
|
| 413 |
+
? 'नफा-तोटा अॅनालिसीस (Profit / Loss Analysis)'
|
| 414 |
+
: `${selectedLotNumber ?? ''} - ${selectedLotMirchiName ?? ''} Analysis`}
|
| 415 |
+
</h2>
|
| 416 |
+
</div>
|
| 417 |
+
<div className="flex flex-col md:flex-row gap-3 w-full md:w-auto md:items-center justify-end">
|
| 418 |
+
<div className="relative w-full md:w-64">
|
| 419 |
+
<Search
|
| 420 |
+
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
|
| 421 |
+
size={18}
|
| 422 |
+
/>
|
| 423 |
+
<input
|
| 424 |
+
type="text"
|
| 425 |
+
placeholder="Search LOT / Mirchi Type..."
|
| 426 |
+
className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 outline-none w-full text-sm"
|
| 427 |
+
value={searchTerm}
|
| 428 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 429 |
+
/>
|
| 430 |
+
</div>
|
| 431 |
+
<div className="flex gap-2 text-xs md:text-sm justify-start md:justify-end">
|
| 432 |
+
<button
|
| 433 |
+
type="button"
|
| 434 |
+
onClick={() => setLotFilter('all')}
|
| 435 |
+
className={`px-2 py-1 rounded-full border text-[11px] md:text-xs ${
|
| 436 |
+
lotFilter === 'all'
|
| 437 |
+
? 'bg-teal-600 text-white border-teal-600'
|
| 438 |
+
: 'bg-white text-gray-600 border-gray-300'
|
| 439 |
+
}`}
|
| 440 |
+
>
|
| 441 |
+
All
|
| 442 |
+
</button>
|
| 443 |
+
<button
|
| 444 |
+
type="button"
|
| 445 |
+
onClick={() => setLotFilter('active')}
|
| 446 |
+
className={`px-2 py-1 rounded-full border text-[11px] md:text-xs ${
|
| 447 |
+
lotFilter === 'active'
|
| 448 |
+
? 'bg-teal-600 text-white border-teal-600'
|
| 449 |
+
: 'bg-white text-gray-600 border-gray-300'
|
| 450 |
+
}`}
|
| 451 |
+
>
|
| 452 |
+
In Stock
|
| 453 |
+
</button>
|
| 454 |
+
<button
|
| 455 |
+
type="button"
|
| 456 |
+
onClick={() => setLotFilter('closed')}
|
| 457 |
+
className={`px-2 py-1 rounded-full border text-[11px] md:text-xs ${
|
| 458 |
+
lotFilter === 'closed'
|
| 459 |
+
? 'bg-teal-600 text-white border-teal-600'
|
| 460 |
+
: 'bg-white text-gray-600 border-gray-300'
|
| 461 |
+
}`}
|
| 462 |
+
>
|
| 463 |
+
Stockout
|
| 464 |
+
</button>
|
| 465 |
+
</div>
|
| 466 |
+
<button
|
| 467 |
+
onClick={handleExport}
|
| 468 |
+
className="inline-flex items-center justify-center px-3 py-2 rounded-lg bg-teal-600 hover:bg-teal-700 text-white text-sm font-medium shadow-sm"
|
| 469 |
+
>
|
| 470 |
+
<Download size={16} className="mr-2" />
|
| 471 |
+
Export to Excel
|
| 472 |
+
</button>
|
| 473 |
+
</div>
|
| 474 |
+
</div>
|
| 475 |
+
|
| 476 |
+
{viewMode === 'detail' && selectedLotSummary && (
|
| 477 |
+
<div className="p-4 md:p-5 border-b bg-gray-50">
|
| 478 |
+
<div className="flex flex-col gap-3 text-xs md:text-sm">
|
| 479 |
+
<div className="flex items-center justify-between gap-3">
|
| 480 |
+
<div>
|
| 481 |
+
<div className="font-mono font-semibold text-teal-700">
|
| 482 |
+
{selectedLotSummary.lotNumber}
|
| 483 |
+
</div>
|
| 484 |
+
<div className="text-[11px] md:text-xs text-gray-500">
|
| 485 |
+
{selectedLotSummary.mirchiName}
|
| 486 |
+
</div>
|
| 487 |
+
</div>
|
| 488 |
+
<div className="text-right">
|
| 489 |
+
<div className="text-[11px] text-gray-500">Remaining</div>
|
| 490 |
+
<div className="font-semibold text-gray-800">
|
| 491 |
+
{selectedLotSummary.remainingQty.toFixed(2)} kg
|
| 492 |
+
</div>
|
| 493 |
+
</div>
|
| 494 |
+
</div>
|
| 495 |
+
<div className="grid grid-cols-2 gap-2">
|
| 496 |
+
<div className="bg-white rounded-lg border border-gray-100 p-2 flex flex-col">
|
| 497 |
+
<span className="text-[11px] text-gray-500">Purchase</span>
|
| 498 |
+
<span className="mt-1 text-xs md:text-sm font-medium text-gray-800">
|
| 499 |
+
{selectedLotSummary.purchaseQty.toFixed(2)} kg · ₹
|
| 500 |
+
{selectedLotSummary.purchaseValue.toFixed(0)}
|
| 501 |
+
</span>
|
| 502 |
+
</div>
|
| 503 |
+
<div className="bg-white rounded-lg border border-gray-100 p-2 flex flex-col">
|
| 504 |
+
<span className="text-[11px] text-gray-500">Sales</span>
|
| 505 |
+
<span className="mt-1 text-xs md:text-sm font-medium text-gray-800">
|
| 506 |
+
{selectedLotSummary.soldQty.toFixed(2)} kg · ₹
|
| 507 |
+
{selectedLotSummary.salesValue.toFixed(0)}
|
| 508 |
+
</span>
|
| 509 |
+
</div>
|
| 510 |
+
</div>
|
| 511 |
+
<div className="flex items-center justify-between gap-3">
|
| 512 |
+
<div className="text-[11px] md:text-xs text-gray-500">
|
| 513 |
+
Avg Buy ₹{selectedLotSummary.avgPurchaseRate.toFixed(0)} /kg
|
| 514 |
+
{selectedLotSummary.avgSaleRate > 0 && (
|
| 515 |
+
<>
|
| 516 |
+
{' '}
|
| 517 |
+
· Avg Sell ₹{selectedLotSummary.avgSaleRate.toFixed(0)} /kg
|
| 518 |
+
</>
|
| 519 |
+
)}
|
| 520 |
+
</div>
|
| 521 |
+
<div
|
| 522 |
+
className={`text-xs md:text-sm font-semibold ${
|
| 523 |
+
selectedLotSummary.realizedProfit > 0
|
| 524 |
+
? 'text-green-600'
|
| 525 |
+
: selectedLotSummary.realizedProfit < 0
|
| 526 |
+
? 'text-red-600'
|
| 527 |
+
: 'text-gray-700'
|
| 528 |
+
}`}
|
| 529 |
+
>
|
| 530 |
+
{selectedLotSummary.realizedProfit > 0
|
| 531 |
+
? 'Profit'
|
| 532 |
+
: selectedLotSummary.realizedProfit < 0
|
| 533 |
+
? 'Loss'
|
| 534 |
+
: 'Break-even'}{' '}
|
| 535 |
+
₹{selectedLotSummary.realizedProfit.toFixed(0)}
|
| 536 |
+
</div>
|
| 537 |
+
</div>
|
| 538 |
+
</div>
|
| 539 |
+
</div>
|
| 540 |
+
)}
|
| 541 |
+
|
| 542 |
+
{viewMode === 'summary' ? (
|
| 543 |
+
<>
|
| 544 |
+
<div className="hidden md:block overflow-x-auto">
|
| 545 |
+
<table className="w-full text-sm text-left">
|
| 546 |
+
<thead className="bg-gray-50 text-gray-500 font-medium">
|
| 547 |
+
<tr>
|
| 548 |
+
<th className="px-6 py-4">LOT</th>
|
| 549 |
+
<th className="px-6 py-4">Mirchi Type</th>
|
| 550 |
+
<th className="px-6 py-4 text-right">Purchase Qty / Value</th>
|
| 551 |
+
<th className="px-6 py-4 text-right">Sales Qty / Value</th>
|
| 552 |
+
<th className="px-6 py-4 text-right">Remaining Qty</th>
|
| 553 |
+
<th className="px-6 py-4 text-right">Profit / Loss</th>
|
| 554 |
+
</tr>
|
| 555 |
+
</thead>
|
| 556 |
+
<tbody className="divide-y divide-gray-100">
|
| 557 |
+
{analysisRows.length === 0 ? (
|
| 558 |
+
<tr>
|
| 559 |
+
<td
|
| 560 |
+
colSpan={6}
|
| 561 |
+
className="px-6 py-8 text-center text-gray-500"
|
| 562 |
+
>
|
| 563 |
+
No analysis data available
|
| 564 |
+
</td>
|
| 565 |
+
</tr>
|
| 566 |
+
) : (
|
| 567 |
+
analysisRows.map((row) => {
|
| 568 |
+
const profitClass =
|
| 569 |
+
row.realizedProfit > 0
|
| 570 |
+
? 'text-green-600'
|
| 571 |
+
: row.realizedProfit < 0
|
| 572 |
+
? 'text-red-600'
|
| 573 |
+
: 'text-gray-700';
|
| 574 |
+
|
| 575 |
+
return (
|
| 576 |
+
<tr
|
| 577 |
+
key={row.lotId}
|
| 578 |
+
className="hover:bg-gray-50 cursor-pointer"
|
| 579 |
+
onClick={() => {
|
| 580 |
+
setSelectedLotId(row.lotId);
|
| 581 |
+
setSelectedLotNumber(row.lotNumber);
|
| 582 |
+
setSelectedLotMirchiName(row.mirchiName);
|
| 583 |
+
setViewMode('detail');
|
| 584 |
+
}}
|
| 585 |
+
>
|
| 586 |
+
<td className="px-6 py-4 font-mono text-sm font-medium text-teal-700">
|
| 587 |
+
{row.lotNumber}
|
| 588 |
+
</td>
|
| 589 |
+
<td className="px-6 py-4">{row.mirchiName}</td>
|
| 590 |
+
<td className="px-6 py-4 text-right">
|
| 591 |
+
<div className="flex flex-col items-end">
|
| 592 |
+
<span className="text-gray-800 font-semibold">
|
| 593 |
+
{row.purchaseQty.toFixed(2)} kg
|
| 594 |
+
</span>
|
| 595 |
+
<span className="text-xs text-gray-500">
|
| 596 |
+
₹{row.purchaseValue.toFixed(0)}
|
| 597 |
+
</span>
|
| 598 |
+
</div>
|
| 599 |
+
</td>
|
| 600 |
+
<td className="px-6 py-4 text-right">
|
| 601 |
+
<div className="flex flex-col items-end">
|
| 602 |
+
<span className="text-gray-800 font-semibold">
|
| 603 |
+
{row.soldQty.toFixed(2)} kg
|
| 604 |
+
</span>
|
| 605 |
+
<span className="text-xs text-gray-500">
|
| 606 |
+
₹{row.salesValue.toFixed(0)}
|
| 607 |
+
</span>
|
| 608 |
+
</div>
|
| 609 |
+
</td>
|
| 610 |
+
<td className="px-6 py-4 text-right">
|
| 611 |
+
<span className="font-semibold text-gray-800">
|
| 612 |
+
{row.remainingQty.toFixed(2)} kg
|
| 613 |
+
</span>
|
| 614 |
+
</td>
|
| 615 |
+
<td className="px-6 py-4 text-right">
|
| 616 |
+
<span className={`font-semibold ${profitClass}`}>
|
| 617 |
+
{row.realizedProfit > 0
|
| 618 |
+
? 'Profit'
|
| 619 |
+
: row.realizedProfit < 0
|
| 620 |
+
? 'Loss'
|
| 621 |
+
: 'Break-even'}{' '}
|
| 622 |
+
₹{row.realizedProfit.toFixed(0)}
|
| 623 |
+
</span>
|
| 624 |
+
</td>
|
| 625 |
+
</tr>
|
| 626 |
+
);
|
| 627 |
+
})
|
| 628 |
+
)}
|
| 629 |
+
</tbody>
|
| 630 |
+
</table>
|
| 631 |
+
</div>
|
| 632 |
+
|
| 633 |
+
<div className="md:hidden flex flex-col divide-y divide-gray-100">
|
| 634 |
+
{analysisRows.length === 0 ? (
|
| 635 |
+
<div className="p-6 text-center text-gray-500 text-sm">
|
| 636 |
+
No analysis data available
|
| 637 |
+
</div>
|
| 638 |
+
) : (
|
| 639 |
+
analysisRows.map((row) => {
|
| 640 |
+
const profitClass =
|
| 641 |
+
row.realizedProfit > 0
|
| 642 |
+
? 'text-green-600'
|
| 643 |
+
: row.realizedProfit < 0
|
| 644 |
+
? 'text-red-600'
|
| 645 |
+
: 'text-gray-700';
|
| 646 |
+
|
| 647 |
+
return (
|
| 648 |
+
<button
|
| 649 |
+
key={row.lotId}
|
| 650 |
+
type="button"
|
| 651 |
+
className="text-left p-4 space-y-2 hover:bg-gray-50"
|
| 652 |
+
onClick={() => {
|
| 653 |
+
setSelectedLotId(row.lotId);
|
| 654 |
+
setSelectedLotNumber(row.lotNumber);
|
| 655 |
+
setSelectedLotMirchiName(row.mirchiName);
|
| 656 |
+
setViewMode('detail');
|
| 657 |
+
}}
|
| 658 |
+
>
|
| 659 |
+
<div className="flex items-center justify-between gap-2">
|
| 660 |
+
<div>
|
| 661 |
+
<div className="text-sm font-mono font-semibold text-teal-700">
|
| 662 |
+
{row.lotNumber}
|
| 663 |
+
</div>
|
| 664 |
+
<div className="text-xs text-gray-500">
|
| 665 |
+
{row.mirchiName}
|
| 666 |
+
</div>
|
| 667 |
+
<div className="text-[11px] text-gray-500 mt-0.5">
|
| 668 |
+
Remaining {row.remainingQty.toFixed(1)} kg
|
| 669 |
+
</div>
|
| 670 |
+
</div>
|
| 671 |
+
<div className="text-right">
|
| 672 |
+
<div className={`text-sm font-semibold ${profitClass}`}>
|
| 673 |
+
{row.realizedProfit > 0
|
| 674 |
+
? 'Profit'
|
| 675 |
+
: row.realizedProfit < 0
|
| 676 |
+
? 'Loss'
|
| 677 |
+
: 'Break-even'}{' '}
|
| 678 |
+
₹{row.realizedProfit.toFixed(0)}
|
| 679 |
+
</div>
|
| 680 |
+
<div className="text-[11px] text-gray-500">
|
| 681 |
+
Margin {row.marginPercent.toFixed(1)}%
|
| 682 |
+
</div>
|
| 683 |
+
</div>
|
| 684 |
+
</div>
|
| 685 |
+
<div className="grid grid-cols-2 gap-2 text-xs pt-2 border-t border-gray-50">
|
| 686 |
+
<div className="bg-gray-50 p-2 rounded">
|
| 687 |
+
<div className="text-gray-500">Purchase</div>
|
| 688 |
+
<div className="font-medium">
|
| 689 |
+
{row.purchaseQty.toFixed(1)} kg · ₹
|
| 690 |
+
{row.purchaseValue.toFixed(0)}
|
| 691 |
+
</div>
|
| 692 |
+
</div>
|
| 693 |
+
<div className="bg-gray-50 p-2 rounded">
|
| 694 |
+
<div className="text-gray-500">Sales</div>
|
| 695 |
+
<div className="font-medium">
|
| 696 |
+
{row.soldQty.toFixed(1)} kg · ₹
|
| 697 |
+
{row.salesValue.toFixed(0)}
|
| 698 |
+
</div>
|
| 699 |
+
</div>
|
| 700 |
+
</div>
|
| 701 |
+
</button>
|
| 702 |
+
);
|
| 703 |
+
})
|
| 704 |
+
)}
|
| 705 |
+
</div>
|
| 706 |
+
</>
|
| 707 |
+
) : (
|
| 708 |
+
<>
|
| 709 |
+
<div className="hidden md:block overflow-x-auto">
|
| 710 |
+
<table className="w-full text-sm text-left">
|
| 711 |
+
<thead className="bg-gray-50 text-gray-500 font-medium">
|
| 712 |
+
<tr>
|
| 713 |
+
<th className="px-6 py-4">Date</th>
|
| 714 |
+
<th className="px-6 py-4">Bill No</th>
|
| 715 |
+
<th className="px-6 py-4">Type</th>
|
| 716 |
+
<th className="px-6 py-4">Import / Export</th>
|
| 717 |
+
<th className="px-6 py-4 text-right">Qty (kg)</th>
|
| 718 |
+
<th className="px-6 py-4 text-right">Rate (₹/kg)</th>
|
| 719 |
+
<th className="px-6 py-4 text-right">Net Amount (₹)</th>
|
| 720 |
+
<th className="px-6 py-4 text-right">Profit Impact (₹)</th>
|
| 721 |
+
</tr>
|
| 722 |
+
</thead>
|
| 723 |
+
<tbody className="divide-y divide-gray-100">
|
| 724 |
+
{detailRows.length === 0 ? (
|
| 725 |
+
<tr>
|
| 726 |
+
<td
|
| 727 |
+
colSpan={8}
|
| 728 |
+
className="px-6 py-8 text-center text-gray-500"
|
| 729 |
+
>
|
| 730 |
+
No transactions found for this LOT
|
| 731 |
+
</td>
|
| 732 |
+
</tr>
|
| 733 |
+
) : (
|
| 734 |
+
detailRows.map((row) => {
|
| 735 |
+
const profitClass =
|
| 736 |
+
row.profitImpact > 0
|
| 737 |
+
? 'text-green-600'
|
| 738 |
+
: row.profitImpact < 0
|
| 739 |
+
? 'text-red-600'
|
| 740 |
+
: 'text-gray-700';
|
| 741 |
+
|
| 742 |
+
return (
|
| 743 |
+
<tr key={row.id} className="hover:bg-gray-50">
|
| 744 |
+
<td className="px-6 py-4 text-gray-600">{row.date}</td>
|
| 745 |
+
<td className="px-6 py-4 font-mono text-xs text-gray-600">
|
| 746 |
+
{row.billNo}
|
| 747 |
+
</td>
|
| 748 |
+
<td className="px-6 py-4 text-gray-700">{row.typeLabel}</td>
|
| 749 |
+
<td className="px-6 py-4 text-gray-700">
|
| 750 |
+
{row.directionLabel}
|
| 751 |
+
</td>
|
| 752 |
+
<td className="px-6 py-4 text-right text-gray-800">
|
| 753 |
+
{row.qtyKg.toFixed(2)}
|
| 754 |
+
</td>
|
| 755 |
+
<td className="px-6 py-4 text-right text-gray-800">
|
| 756 |
+
₹{row.ratePerKg.toFixed(2)}
|
| 757 |
+
</td>
|
| 758 |
+
<td className="px-6 py-4 text-right text-gray-800">
|
| 759 |
+
₹{row.netAmount.toFixed(0)}
|
| 760 |
+
</td>
|
| 761 |
+
<td className="px-6 py-4 text-right">
|
| 762 |
+
<span className={`font-semibold ${profitClass}`}>
|
| 763 |
+
₹{row.profitImpact.toFixed(0)}
|
| 764 |
+
</span>
|
| 765 |
+
</td>
|
| 766 |
+
</tr>
|
| 767 |
+
);
|
| 768 |
+
})
|
| 769 |
+
)}
|
| 770 |
+
</tbody>
|
| 771 |
+
</table>
|
| 772 |
+
</div>
|
| 773 |
+
|
| 774 |
+
<div className="md:hidden flex flex-col divide-y divide-gray-100">
|
| 775 |
+
{detailRows.length === 0 ? (
|
| 776 |
+
<div className="p-6 text-center text-gray-500 text-sm">
|
| 777 |
+
No transactions found for this LOT
|
| 778 |
+
</div>
|
| 779 |
+
) : (
|
| 780 |
+
detailRows.map((row) => {
|
| 781 |
+
const profitClass =
|
| 782 |
+
row.profitImpact > 0
|
| 783 |
+
? 'text-green-600'
|
| 784 |
+
: row.profitImpact < 0
|
| 785 |
+
? 'text-red-600'
|
| 786 |
+
: 'text-gray-700';
|
| 787 |
+
|
| 788 |
+
return (
|
| 789 |
+
<div key={row.id} className="p-4 space-y-2">
|
| 790 |
+
<div className="flex items-center justify-between gap-2">
|
| 791 |
+
<div>
|
| 792 |
+
<div className="text-xs text-gray-500">{row.date}</div>
|
| 793 |
+
<div className="text-sm font-mono font-medium text-gray-800">
|
| 794 |
+
{row.billNo}
|
| 795 |
+
</div>
|
| 796 |
+
<div className="text-[11px] text-gray-500">
|
| 797 |
+
{row.typeLabel} · {row.directionLabel}
|
| 798 |
+
</div>
|
| 799 |
+
</div>
|
| 800 |
+
<div className="text-right">
|
| 801 |
+
<div className="text-xs text-gray-500">Net Amount</div>
|
| 802 |
+
<div className="text-sm font-semibold text-gray-800">
|
| 803 |
+
₹{row.netAmount.toFixed(0)}
|
| 804 |
+
</div>
|
| 805 |
+
<div className={`text-[11px] font-semibold ${profitClass}`}>
|
| 806 |
+
Profit ₹{row.profitImpact.toFixed(0)}
|
| 807 |
+
</div>
|
| 808 |
+
</div>
|
| 809 |
+
</div>
|
| 810 |
+
<div className="grid grid-cols-2 gap-2 text-xs pt-2 border-t border-gray-50">
|
| 811 |
+
<div className="bg-gray-50 p-2 rounded">
|
| 812 |
+
<div className="text-gray-500">Qty (kg)</div>
|
| 813 |
+
<div className="font-medium">
|
| 814 |
+
{row.qtyKg.toFixed(2)} kg
|
| 815 |
+
</div>
|
| 816 |
+
</div>
|
| 817 |
+
<div className="bg-gray-50 p-2 rounded">
|
| 818 |
+
<div className="text-gray-500">Rate / kg</div>
|
| 819 |
+
<div className="font-medium">
|
| 820 |
+
₹{row.ratePerKg.toFixed(2)}
|
| 821 |
+
</div>
|
| 822 |
+
</div>
|
| 823 |
+
</div>
|
| 824 |
+
</div>
|
| 825 |
+
);
|
| 826 |
+
})
|
| 827 |
+
)}
|
| 828 |
+
</div>
|
| 829 |
+
</>
|
| 830 |
+
)}
|
| 831 |
+
</div>
|
| 832 |
+
</div>
|
| 833 |
+
);
|
| 834 |
+
};
|
| 835 |
+
|
| 836 |
+
export default Analysis;
|
pages/AwaakBill.tsx
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import {
|
| 4 |
+
getParties, getMirchiTypes, generateBillNumber, saveTransaction,
|
| 5 |
+
checkLotUnique, getAvailableLotsByMirchi
|
| 6 |
+
} from '../services/db';
|
| 7 |
+
import {
|
| 8 |
+
Party, MirchiType, Lot, Transaction, TransactionItem, BillType, PaymentMode, PartyType
|
| 9 |
+
} from '../types';
|
| 10 |
+
import { Save, Trash2, Plus, RotateCcw, Printer, Download, ChevronUp, ChevronDown } from 'lucide-react';
|
| 11 |
+
import PrintInvoice from '../components/PrintInvoice.tsx';
|
| 12 |
+
import PdfInvoice from '../components/PdfInvoice.tsx';
|
| 13 |
+
|
| 14 |
+
// Defined outside to prevent re-render focus loss
|
| 15 |
+
const SummaryInput = ({ label, value, onChange }: { label: string, value: number, onChange: (val: number) => void }) => (
|
| 16 |
+
<div className="flex justify-between items-center mb-1 bg-gray-50 p-1.5 rounded">
|
| 17 |
+
<span className="text-gray-600 text-xs">{label}</span>
|
| 18 |
+
<input
|
| 19 |
+
type="number"
|
| 20 |
+
step="0.01"
|
| 21 |
+
min="0"
|
| 22 |
+
className="w-24 border rounded text-right p-1 text-sm bg-white focus:ring-1 focus:ring-teal-500 outline-none appearance-none"
|
| 23 |
+
value={value === 0 ? '' : value}
|
| 24 |
+
onChange={e => {
|
| 25 |
+
const parsed = parseFloat(e.target.value);
|
| 26 |
+
onChange(isNaN(parsed) ? 0 : Math.max(0, parsed));
|
| 27 |
+
}}
|
| 28 |
+
placeholder="0"
|
| 29 |
+
/>
|
| 30 |
+
</div>
|
| 31 |
+
);
|
| 32 |
+
|
| 33 |
+
const AwaakBill = () => {
|
| 34 |
+
const navigate = useNavigate();
|
| 35 |
+
|
| 36 |
+
// Master Data
|
| 37 |
+
const [parties, setParties] = useState<Party[]>([]);
|
| 38 |
+
const [mirchiTypes, setMirchiTypes] = useState<MirchiType[]>([]);
|
| 39 |
+
|
| 40 |
+
// Form State
|
| 41 |
+
const [isReturnMode, setIsReturnMode] = useState(false);
|
| 42 |
+
const [billDate, setBillDate] = useState(new Date().toISOString().split('T')[0]);
|
| 43 |
+
const [billNumber, setBillNumber] = useState('');
|
| 44 |
+
const [selectedParty, setSelectedParty] = useState('');
|
| 45 |
+
|
| 46 |
+
// Mobile UI State
|
| 47 |
+
const [showMobileSummary, setShowMobileSummary] = useState(false);
|
| 48 |
+
|
| 49 |
+
const [items, setItems] = useState<Partial<TransactionItem>[]>([{
|
| 50 |
+
id: Date.now().toString(),
|
| 51 |
+
poti_weights: [],
|
| 52 |
+
gross_weight: 0,
|
| 53 |
+
poti_count: 0,
|
| 54 |
+
total_potya: 0,
|
| 55 |
+
net_weight: 0,
|
| 56 |
+
rate_per_kg: 0,
|
| 57 |
+
item_total: 0
|
| 58 |
+
}]);
|
| 59 |
+
|
| 60 |
+
// Temp inputs for items row
|
| 61 |
+
const [potiInputs, setPotiInputs] = useState<{ [key: string]: string }>({});
|
| 62 |
+
|
| 63 |
+
// LOT number state
|
| 64 |
+
const [lotInputs, setLotInputs] = useState<{ [key: string]: string }>({});
|
| 65 |
+
const [availableLots, setAvailableLots] = useState<{ [key: string]: Lot[] }>({});
|
| 66 |
+
|
| 67 |
+
const [expenses, setExpenses] = useState({
|
| 68 |
+
cess_percent: 1.0,
|
| 69 |
+
cess_amount: 0,
|
| 70 |
+
adat_percent: 3.0,
|
| 71 |
+
adat_amount: 0,
|
| 72 |
+
poti_rate: 0,
|
| 73 |
+
poti_amount: 0,
|
| 74 |
+
hamali_per_poti: 6,
|
| 75 |
+
hamali_amount: 0,
|
| 76 |
+
gaadi_bharni: 0,
|
| 77 |
+
other_expenses: 0,
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
// Payment State
|
| 81 |
+
const [paymentMode, setPaymentMode] = useState<'cash' | 'online' | 'hybrid' | 'due'>('cash');
|
| 82 |
+
const [paidAmount, setPaidAmount] = useState(0); // Not used directly in new logic, derived
|
| 83 |
+
const [cashAmount, setCashAmount] = useState(0); // For hybrid
|
| 84 |
+
const [onlineAmount, setOnlineAmount] = useState(0); // For hybrid & due
|
| 85 |
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
| 86 |
+
const [savedTransaction, setSavedTransaction] = useState<Transaction | null>(null);
|
| 87 |
+
const [isLoading, setIsLoading] = useState(true);
|
| 88 |
+
const [error, setError] = useState<string | null>(null);
|
| 89 |
+
|
| 90 |
+
// Initial Load & Bill Number Generation
|
| 91 |
+
useEffect(() => {
|
| 92 |
+
const loadData = async () => {
|
| 93 |
+
try {
|
| 94 |
+
setIsLoading(true);
|
| 95 |
+
setError(null);
|
| 96 |
+
const [partiesData, mirchiData] = await Promise.all([
|
| 97 |
+
getParties(),
|
| 98 |
+
getMirchiTypes()
|
| 99 |
+
]);
|
| 100 |
+
|
| 101 |
+
if (!partiesData || partiesData.length === 0) {
|
| 102 |
+
setError('No parties found. Please add parties in Settings first.');
|
| 103 |
+
}
|
| 104 |
+
if (!mirchiData || mirchiData.length === 0) {
|
| 105 |
+
setError('No mirchi types found. Please add mirchi types in Settings first.');
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// Filter parties for Awaak bills
|
| 109 |
+
const filteredParties = partiesData.filter(p =>
|
| 110 |
+
p.party_type === PartyType.AWAAK || p.party_type === PartyType.BOTH
|
| 111 |
+
);
|
| 112 |
+
|
| 113 |
+
setParties(filteredParties || []);
|
| 114 |
+
setMirchiTypes(mirchiData || []);
|
| 115 |
+
setBillNumber(generateBillNumber(BillType.AWAAK, isReturnMode));
|
| 116 |
+
} catch (err: any) {
|
| 117 |
+
console.error('Error loading data:', err);
|
| 118 |
+
setError('Failed to load data. Please check your connection and try again.');
|
| 119 |
+
} finally {
|
| 120 |
+
setIsLoading(false);
|
| 121 |
+
}
|
| 122 |
+
};
|
| 123 |
+
loadData();
|
| 124 |
+
}, [isReturnMode]);
|
| 125 |
+
|
| 126 |
+
// Calculation Logic
|
| 127 |
+
const calculateRow = (item: Partial<TransactionItem>, potiString: string) => {
|
| 128 |
+
const weights = potiString.split(/[\s,+]+/).map(n => parseFloat(n)).filter(n => !isNaN(n) && n > 0);
|
| 129 |
+
|
| 130 |
+
const gross = weights.reduce((a, b) => a + b, 0);
|
| 131 |
+
const count = weights.length;
|
| 132 |
+
const potya = count * 1; // 1kg deduction per bag
|
| 133 |
+
const net = Math.max(0, gross - potya);
|
| 134 |
+
const total = net * (item.rate_per_kg || 0);
|
| 135 |
+
|
| 136 |
+
return {
|
| 137 |
+
...item,
|
| 138 |
+
poti_weights: weights,
|
| 139 |
+
gross_weight: gross,
|
| 140 |
+
poti_count: count,
|
| 141 |
+
total_potya: potya,
|
| 142 |
+
net_weight: net,
|
| 143 |
+
item_total: total
|
| 144 |
+
};
|
| 145 |
+
};
|
| 146 |
+
|
| 147 |
+
const handleItemChange = (id: string, field: string, value: any) => {
|
| 148 |
+
setItems(prev => prev.map(item => {
|
| 149 |
+
if (item.id !== id) return item;
|
| 150 |
+
|
| 151 |
+
let updatedItem = { ...item, [field]: value };
|
| 152 |
+
|
| 153 |
+
if (field === 'rate_per_kg') {
|
| 154 |
+
updatedItem.item_total = (updatedItem.net_weight || 0) * Math.max(0, parseFloat(value || 0));
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
if (field === 'net_weight') {
|
| 158 |
+
updatedItem.item_total = Math.max(0, parseFloat(value || 0)) * (updatedItem.rate_per_kg || 0);
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
return updatedItem;
|
| 162 |
+
}));
|
| 163 |
+
};
|
| 164 |
+
|
| 165 |
+
const handlePotiInputChange = (id: string, value: string) => {
|
| 166 |
+
setPotiInputs(prev => ({ ...prev, [id]: value }));
|
| 167 |
+
setItems(prev => prev.map(item => {
|
| 168 |
+
if (item.id !== id) return item;
|
| 169 |
+
return calculateRow(item, value);
|
| 170 |
+
}));
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
// Handle mirchi type change - load available lots
|
| 174 |
+
const handleMirchiChange = async (id: string, mirchiTypeId: string) => {
|
| 175 |
+
handleItemChange(id, 'mirchi_type_id', mirchiTypeId);
|
| 176 |
+
|
| 177 |
+
// Load available lots for this mirchi type
|
| 178 |
+
if (mirchiTypeId) {
|
| 179 |
+
const lots = await getAvailableLotsByMirchi(mirchiTypeId);
|
| 180 |
+
setAvailableLots(prev => ({ ...prev, [id]: lots }));
|
| 181 |
+
|
| 182 |
+
// Generate suggested lot number
|
| 183 |
+
const mirchi = mirchiTypes.find(m => m.id === mirchiTypeId);
|
| 184 |
+
if (mirchi) {
|
| 185 |
+
const today = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
| 186 |
+
const mirchiCode = mirchi.name.substring(0, 4).toUpperCase();
|
| 187 |
+
const suggestedLot = `LOT-${mirchiCode}-${today}-`;
|
| 188 |
+
setLotInputs(prev => ({ ...prev, [id]: suggestedLot }));
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
};
|
| 192 |
+
|
| 193 |
+
// Handle LOT number input change
|
| 194 |
+
const handleLotNumberChange = (id: string, value: string) => {
|
| 195 |
+
setLotInputs(prev => ({ ...prev, [id]: value }));
|
| 196 |
+
handleItemChange(id, 'lot_number', value);
|
| 197 |
+
};
|
| 198 |
+
|
| 199 |
+
// Handle selecting existing lot
|
| 200 |
+
const handleLotSelection = (id: string, lotNumber: string) => {
|
| 201 |
+
if (lotNumber) {
|
| 202 |
+
setLotInputs(prev => ({ ...prev, [id]: lotNumber }));
|
| 203 |
+
handleItemChange(id, 'lot_number', lotNumber);
|
| 204 |
+
handleItemChange(id, 'is_existing_lot', true);
|
| 205 |
+
} else {
|
| 206 |
+
handleItemChange(id, 'is_existing_lot', false);
|
| 207 |
+
}
|
| 208 |
+
};
|
| 209 |
+
|
| 210 |
+
const addItem = () => {
|
| 211 |
+
setItems(prev => [...prev, {
|
| 212 |
+
id: Date.now().toString(),
|
| 213 |
+
poti_weights: [],
|
| 214 |
+
gross_weight: 0,
|
| 215 |
+
poti_count: 0,
|
| 216 |
+
total_potya: 0,
|
| 217 |
+
net_weight: 0,
|
| 218 |
+
rate_per_kg: 0,
|
| 219 |
+
item_total: 0
|
| 220 |
+
}]);
|
| 221 |
+
};
|
| 222 |
+
|
| 223 |
+
const removeItem = (id: string) => {
|
| 224 |
+
if (items.length > 1) {
|
| 225 |
+
setItems(prev => prev.filter(i => i.id !== id));
|
| 226 |
+
const newInputs = { ...potiInputs };
|
| 227 |
+
delete newInputs[id];
|
| 228 |
+
setPotiInputs(newInputs);
|
| 229 |
+
}
|
| 230 |
+
};
|
| 231 |
+
|
| 232 |
+
// Totals Calculation
|
| 233 |
+
const subtotal = items.reduce((acc, item) => acc + (item.item_total || 0), 0);
|
| 234 |
+
const totalPoti = items.reduce((acc, item) => acc + (item.poti_count || 0), 0);
|
| 235 |
+
|
| 236 |
+
// Derived Expenses - New sequence
|
| 237 |
+
const potiAmt = totalPoti * expenses.poti_rate;
|
| 238 |
+
const baseForCess = subtotal + potiAmt; // Cess calculated on subtotal + poti
|
| 239 |
+
const cessAmt = (baseForCess * expenses.cess_percent) / 100;
|
| 240 |
+
const adatAmt = (subtotal * expenses.adat_percent) / 100;
|
| 241 |
+
const hamaliAmt = totalPoti * expenses.hamali_per_poti;
|
| 242 |
+
const totalExp = potiAmt + cessAmt + adatAmt + hamaliAmt + (expenses.gaadi_bharni || 0) + (expenses.other_expenses || 0);
|
| 243 |
+
const grandTotal = subtotal + totalExp;
|
| 244 |
+
|
| 245 |
+
// Calculate Payment Details based on Mode
|
| 246 |
+
let finalCash = 0;
|
| 247 |
+
let finalOnline = 0;
|
| 248 |
+
let currentPaid = 0;
|
| 249 |
+
|
| 250 |
+
if (paymentMode === 'cash') {
|
| 251 |
+
finalCash = grandTotal;
|
| 252 |
+
finalOnline = 0;
|
| 253 |
+
currentPaid = grandTotal;
|
| 254 |
+
} else if (paymentMode === 'online') {
|
| 255 |
+
finalCash = 0;
|
| 256 |
+
finalOnline = grandTotal;
|
| 257 |
+
currentPaid = grandTotal;
|
| 258 |
+
} else if (paymentMode === 'hybrid') {
|
| 259 |
+
finalCash = cashAmount;
|
| 260 |
+
finalOnline = Math.max(0, grandTotal - cashAmount);
|
| 261 |
+
currentPaid = grandTotal; // Hybrid assumes full payment
|
| 262 |
+
} else if (paymentMode === 'due') {
|
| 263 |
+
finalCash = 0;
|
| 264 |
+
finalOnline = onlineAmount; // User defined
|
| 265 |
+
currentPaid = onlineAmount;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
const balance = grandTotal - currentPaid;
|
| 269 |
+
|
| 270 |
+
// Validation Error
|
| 271 |
+
// Hybrid: Cash cannot exceed Total
|
| 272 |
+
// Due: Online cannot exceed Total
|
| 273 |
+
const isOverpaid = (paymentMode === 'hybrid' && cashAmount > grandTotal) ||
|
| 274 |
+
(paymentMode === 'due' && onlineAmount > grandTotal);
|
| 275 |
+
|
| 276 |
+
const validateForm = () => {
|
| 277 |
+
if (!selectedParty) {
|
| 278 |
+
alert('Please select a Party (पार्टी निवडा)');
|
| 279 |
+
|
| 280 |
+
return false;
|
| 281 |
+
}
|
| 282 |
+
for (let i = 0; i < items.length; i++) {
|
| 283 |
+
const item = items[i];
|
| 284 |
+
if (!item.mirchi_type_id) {
|
| 285 |
+
alert(`Row ${i + 1}: Please select Mirchi Type`);
|
| 286 |
+
return false;
|
| 287 |
+
}
|
| 288 |
+
// LOT validation - different for return vs normal mode
|
| 289 |
+
if (isReturnMode) {
|
| 290 |
+
// Return mode: must select existing lot
|
| 291 |
+
if (!item.lot_id || !item.lot_number) {
|
| 292 |
+
alert(`Row ${i + 1}: Please select a LOT to return`);
|
| 293 |
+
return false;
|
| 294 |
+
}
|
| 295 |
+
} else {
|
| 296 |
+
// Normal mode: must enter new lot number
|
| 297 |
+
if (!item.lot_number || item.lot_number.trim() === '') {
|
| 298 |
+
alert(`Row ${i + 1}: Please enter LOT number`);
|
| 299 |
+
return false;
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
const hasWeights = item.poti_weights && item.poti_weights.length > 0;
|
| 304 |
+
const hasCountAndNet = (item.poti_count || 0) > 0 && (item.net_weight || 0) > 0;
|
| 305 |
+
|
| 306 |
+
if (!hasWeights && !hasCountAndNet) {
|
| 307 |
+
alert(`Row ${i + 1}: Please either enter poti weights (e.g. 10, 20) or fill both Poti count and Net weight`);
|
| 308 |
+
return false;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
if (!item.rate_per_kg || item.rate_per_kg <= 0) {
|
| 312 |
+
alert(`Row ${i + 1}: Rate must be greater than 0`);
|
| 313 |
+
return false;
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
if (isOverpaid) {
|
| 317 |
+
alert('Paid amount cannot be greater than Total Amount');
|
| 318 |
+
return false;
|
| 319 |
+
}
|
| 320 |
+
return true;
|
| 321 |
+
};
|
| 322 |
+
|
| 323 |
+
const handleSubmit = async () => {
|
| 324 |
+
if (isSubmitting) return;
|
| 325 |
+
if (!validateForm()) return;
|
| 326 |
+
|
| 327 |
+
setIsSubmitting(true);
|
| 328 |
+
|
| 329 |
+
// Populate mirchi_name for each item from mirchiTypes
|
| 330 |
+
const itemsWithNames = items.map(item => ({
|
| 331 |
+
...item,
|
| 332 |
+
mirchi_name: mirchiTypes.find(m => m.id === item.mirchi_type_id)?.name || 'Unknown'
|
| 333 |
+
}));
|
| 334 |
+
|
| 335 |
+
const transaction: Transaction = {
|
| 336 |
+
id: Date.now().toString(),
|
| 337 |
+
bill_number: billNumber,
|
| 338 |
+
bill_date: billDate,
|
| 339 |
+
bill_type: BillType.AWAAK,
|
| 340 |
+
is_return: isReturnMode,
|
| 341 |
+
party_id: selectedParty,
|
| 342 |
+
party_name: parties.find(p => p.id === selectedParty)?.name,
|
| 343 |
+
items: itemsWithNames as TransactionItem[],
|
| 344 |
+
expenses: {
|
| 345 |
+
...expenses,
|
| 346 |
+
cess_amount: cessAmt,
|
| 347 |
+
adat_amount: adatAmt,
|
| 348 |
+
poti_amount: potiAmt,
|
| 349 |
+
hamali_amount: hamaliAmt
|
| 350 |
+
},
|
| 351 |
+
payments: [
|
| 352 |
+
...(finalCash > 0 ? [{ mode: PaymentMode.CASH, amount: finalCash }] : []),
|
| 353 |
+
...(finalOnline > 0 ? [{ mode: PaymentMode.ONLINE, amount: finalOnline }] : []),
|
| 354 |
+
...(balance > 0 ? [{ mode: PaymentMode.DUE, amount: balance }] : []) // Optional: Track due as a payment entry or just balance? Usually balance is enough. Keeping logic simple.
|
| 355 |
+
],
|
| 356 |
+
gross_weight_total: items.reduce((a, i) => a + (i.gross_weight || 0), 0),
|
| 357 |
+
net_weight_total: items.reduce((a, i) => a + (i.net_weight || 0), 0),
|
| 358 |
+
subtotal: subtotal,
|
| 359 |
+
total_expenses: totalExp,
|
| 360 |
+
total_amount: grandTotal,
|
| 361 |
+
paid_amount: currentPaid,
|
| 362 |
+
balance_amount: balance
|
| 363 |
+
};
|
| 364 |
+
|
| 365 |
+
const result = await saveTransaction(transaction);
|
| 366 |
+
if (result.success) {
|
| 367 |
+
// Use the complete transaction object we created, not just the API response
|
| 368 |
+
// This ensures all items are included for printing
|
| 369 |
+
const completeTransaction = result.data ? {
|
| 370 |
+
...transaction,
|
| 371 |
+
...result.data,
|
| 372 |
+
items: transaction.items, // Ensure items are preserved
|
| 373 |
+
expenses: {
|
| 374 |
+
...result.data.expenses,
|
| 375 |
+
other_expenses: transaction.expenses.other_expenses,
|
| 376 |
+
},
|
| 377 |
+
} : transaction;
|
| 378 |
+
setSavedTransaction(completeTransaction);
|
| 379 |
+
alert('Bill Saved Successfully! You can now print the invoice.');
|
| 380 |
+
} else {
|
| 381 |
+
alert(`Error: ${result.message}`);
|
| 382 |
+
}
|
| 383 |
+
setIsSubmitting(false);
|
| 384 |
+
};
|
| 385 |
+
|
| 386 |
+
const handleHybridCashChange = (val: number) => {
|
| 387 |
+
setCashAmount(val);
|
| 388 |
+
// Online amount is derived in render, no state update needed for it in hybrid
|
| 389 |
+
};
|
| 390 |
+
|
| 391 |
+
const SummaryContent = () => (
|
| 392 |
+
<div className="space-y-3 text-sm">
|
| 393 |
+
<div className="flex justify-between">
|
| 394 |
+
<span className="text-gray-600">एकुण रक्कम (Subtotal)</span>
|
| 395 |
+
<span className="font-semibold">₹{subtotal.toFixed(2)}</span>
|
| 396 |
+
</div>
|
| 397 |
+
|
| 398 |
+
<div className="pt-2 border-t border-dashed space-y-2">
|
| 399 |
+
{/* 1. Poti (Bags) Rate */}
|
| 400 |
+
<SummaryInput
|
| 401 |
+
label={`पोती (Bags ${totalPoti}) Rate`}
|
| 402 |
+
value={expenses.poti_rate}
|
| 403 |
+
onChange={val => setExpenses({ ...expenses, poti_rate: val })}
|
| 404 |
+
/>
|
| 405 |
+
<div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
|
| 406 |
+
<span>Amount:</span>
|
| 407 |
+
<span>₹{potiAmt.toFixed(2)}</span>
|
| 408 |
+
</div>
|
| 409 |
+
|
| 410 |
+
{/* 2. Cess Tax (calculated on subtotal + poti) */}
|
| 411 |
+
<SummaryInput
|
| 412 |
+
label={`सेस (Cess ${expenses.cess_percent}%) on ₹${baseForCess.toFixed(2)}`}
|
| 413 |
+
value={expenses.cess_percent}
|
| 414 |
+
onChange={val => setExpenses({ ...expenses, cess_percent: val })}
|
| 415 |
+
/>
|
| 416 |
+
<div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
|
| 417 |
+
<span>Amount:</span>
|
| 418 |
+
<span>₹{cessAmt.toFixed(2)}</span>
|
| 419 |
+
</div>
|
| 420 |
+
|
| 421 |
+
{/* 3. Adat / Market Yard Tax */}
|
| 422 |
+
<SummaryInput
|
| 423 |
+
label={`अडत (Adat ${expenses.adat_percent}%)`}
|
| 424 |
+
value={expenses.adat_percent}
|
| 425 |
+
onChange={val => setExpenses({ ...expenses, adat_percent: val })}
|
| 426 |
+
/>
|
| 427 |
+
<div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
|
| 428 |
+
<span>Amount:</span>
|
| 429 |
+
<span>₹{adatAmt.toFixed(2)}</span>
|
| 430 |
+
</div>
|
| 431 |
+
|
| 432 |
+
{/* 4. Hamali */}
|
| 433 |
+
<SummaryInput
|
| 434 |
+
label={`हमाली (Hamali per poti)`}
|
| 435 |
+
value={expenses.hamali_per_poti}
|
| 436 |
+
onChange={val => setExpenses({ ...expenses, hamali_per_poti: val })}
|
| 437 |
+
/>
|
| 438 |
+
<div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
|
| 439 |
+
<span>Amount ({expenses.hamali_per_poti} * {totalPoti}):</span>
|
| 440 |
+
<span>₹{hamaliAmt.toFixed(2)}</span>
|
| 441 |
+
</div>
|
| 442 |
+
|
| 443 |
+
{/* 5. Gaadi Bharni */}
|
| 444 |
+
<SummaryInput
|
| 445 |
+
label="गाडी भरणी"
|
| 446 |
+
value={expenses.gaadi_bharni}
|
| 447 |
+
onChange={val => setExpenses({ ...expenses, gaadi_bharni: val })}
|
| 448 |
+
/>
|
| 449 |
+
|
| 450 |
+
{/* 6. Other Expenses */}
|
| 451 |
+
<SummaryInput
|
| 452 |
+
label="Other Expenses"
|
| 453 |
+
value={expenses.other_expenses}
|
| 454 |
+
onChange={val => setExpenses({ ...expenses, other_expenses: val })}
|
| 455 |
+
/>
|
| 456 |
+
</div>
|
| 457 |
+
|
| 458 |
+
{/* 7. Total Price */}
|
| 459 |
+
<div className="pt-2 border-t border-gray-200 flex justify-between items-center">
|
| 460 |
+
<span className="text-base font-bold text-gray-800">एकुण बिल (Total)</span>
|
| 461 |
+
<span className="text-xl font-bold text-red-600">₹{grandTotal.toFixed(2)}</span>
|
| 462 |
+
</div>
|
| 463 |
+
|
| 464 |
+
{/* Payment Section */}
|
| 465 |
+
<div className="bg-teal-50 p-3 rounded-lg mt-2 border border-teal-100">
|
| 466 |
+
<label className="block text-xs font-medium text-teal-800 mb-2">Payment Mode</label>
|
| 467 |
+
<div className="flex gap-2 mb-3">
|
| 468 |
+
{['cash', 'online', 'hybrid', 'due'].map(mode => (
|
| 469 |
+
<button
|
| 470 |
+
key={mode}
|
| 471 |
+
onClick={() => {
|
| 472 |
+
setPaymentMode(mode as any);
|
| 473 |
+
setCashAmount(0);
|
| 474 |
+
setOnlineAmount(0);
|
| 475 |
+
}}
|
| 476 |
+
className={`flex-1 py-1 text-xs font-medium rounded border ${paymentMode === mode
|
| 477 |
+
? 'bg-teal-600 text-white border-teal-600'
|
| 478 |
+
: 'bg-white text-gray-600 border-gray-200'
|
| 479 |
+
} capitalize`}
|
| 480 |
+
>
|
| 481 |
+
{mode}
|
| 482 |
+
</button>
|
| 483 |
+
))}
|
| 484 |
+
</div>
|
| 485 |
+
|
| 486 |
+
{paymentMode === 'cash' && (
|
| 487 |
+
<div className="text-center py-2 text-sm text-gray-600">
|
| 488 |
+
Full Payment in Cash: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
|
| 489 |
+
</div>
|
| 490 |
+
)}
|
| 491 |
+
|
| 492 |
+
{paymentMode === 'online' && (
|
| 493 |
+
<div className="text-center py-2 text-sm text-gray-600">
|
| 494 |
+
Full Payment Online: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
|
| 495 |
+
</div>
|
| 496 |
+
)}
|
| 497 |
+
|
| 498 |
+
{paymentMode === 'hybrid' && (
|
| 499 |
+
<div className="space-y-2">
|
| 500 |
+
<div>
|
| 501 |
+
<label className="text-xs text-gray-600">Cash Amount</label>
|
| 502 |
+
<input
|
| 503 |
+
type="number"
|
| 504 |
+
min="0"
|
| 505 |
+
max={grandTotal}
|
| 506 |
+
step="0.01"
|
| 507 |
+
className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
|
| 508 |
+
value={cashAmount === 0 ? '' : cashAmount}
|
| 509 |
+
onChange={e => {
|
| 510 |
+
const val = parseFloat(e.target.value) || 0;
|
| 511 |
+
if (val >= 0 && val <= grandTotal) {
|
| 512 |
+
handleHybridCashChange(val);
|
| 513 |
+
}
|
| 514 |
+
}}
|
| 515 |
+
onKeyDown={e => {
|
| 516 |
+
// Prevent special characters: -, +, e, E
|
| 517 |
+
if (['-', '+', 'e', 'E'].includes(e.key)) {
|
| 518 |
+
e.preventDefault();
|
| 519 |
+
}
|
| 520 |
+
}}
|
| 521 |
+
placeholder="Cash"
|
| 522 |
+
/>
|
| 523 |
+
</div>
|
| 524 |
+
<div>
|
| 525 |
+
<label className="text-xs text-gray-600">Online Amount (Auto)</label>
|
| 526 |
+
<input
|
| 527 |
+
type="text"
|
| 528 |
+
readOnly
|
| 529 |
+
className="w-full bg-gray-100 border border-gray-200 rounded p-2 text-right font-medium text-gray-600"
|
| 530 |
+
value={(grandTotal - cashAmount).toFixed(2)}
|
| 531 |
+
/>
|
| 532 |
+
</div>
|
| 533 |
+
</div>
|
| 534 |
+
)}
|
| 535 |
+
|
| 536 |
+
{paymentMode === 'due' && (
|
| 537 |
+
<div className="space-y-2">
|
| 538 |
+
<div>
|
| 539 |
+
<label className="text-xs text-gray-600">Online Amount (Partial Payment)</label>
|
| 540 |
+
<input
|
| 541 |
+
type="number"
|
| 542 |
+
min="0"
|
| 543 |
+
max={grandTotal}
|
| 544 |
+
step="0.01"
|
| 545 |
+
className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
|
| 546 |
+
value={onlineAmount === 0 ? '' : onlineAmount}
|
| 547 |
+
onChange={e => {
|
| 548 |
+
const val = parseFloat(e.target.value) || 0;
|
| 549 |
+
if (val >= 0 && val <= grandTotal) {
|
| 550 |
+
setOnlineAmount(val);
|
| 551 |
+
}
|
| 552 |
+
}}
|
| 553 |
+
onKeyDown={e => {
|
| 554 |
+
// Prevent special characters: -, +, e, E
|
| 555 |
+
if (['-', '+', 'e', 'E'].includes(e.key)) {
|
| 556 |
+
e.preventDefault();
|
| 557 |
+
}
|
| 558 |
+
}}
|
| 559 |
+
placeholder="Enter amount paid online (0 for full due)"
|
| 560 |
+
/>
|
| 561 |
+
</div>
|
| 562 |
+
<div>
|
| 563 |
+
<label className="text-xs text-gray-600">Due Amount (Auto)</label>
|
| 564 |
+
<input
|
| 565 |
+
type="text"
|
| 566 |
+
readOnly
|
| 567 |
+
className="w-full bg-red-50 border border-red-200 rounded p-2 text-right font-bold text-red-600"
|
| 568 |
+
value={(grandTotal - onlineAmount).toFixed(2)}
|
| 569 |
+
/>
|
| 570 |
+
</div>
|
| 571 |
+
</div>
|
| 572 |
+
)}
|
| 573 |
+
|
| 574 |
+
{isOverpaid && (
|
| 575 |
+
<div className="text-red-500 text-xs mt-2 font-medium text-center">
|
| 576 |
+
Error: Amount exceeds Total!
|
| 577 |
+
</div>
|
| 578 |
+
)}
|
| 579 |
+
</div>
|
| 580 |
+
|
| 581 |
+
<div className="flex justify-between items-center pt-2">
|
| 582 |
+
<span className="text-gray-600 font-medium">बाकी (Balance)</span>
|
| 583 |
+
<span className={`font-bold ${balance > 0 ? 'text-red-500' : 'text-green-600'}`}>₹{balance.toFixed(2)}</span>
|
| 584 |
+
</div>
|
| 585 |
+
</div>
|
| 586 |
+
);
|
| 587 |
+
|
| 588 |
+
return (
|
| 589 |
+
<div className="flex flex-col lg:flex-row gap-4 h-full p-4 bg-gray-50">
|
| 590 |
+
{/* Loading State */}
|
| 591 |
+
{isLoading && (
|
| 592 |
+
<div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-gray-100">
|
| 593 |
+
<div className="text-center">
|
| 594 |
+
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mb-4"></div>
|
| 595 |
+
<p className="text-gray-600">Loading data...</p>
|
| 596 |
+
</div>
|
| 597 |
+
</div>
|
| 598 |
+
)}
|
| 599 |
+
|
| 600 |
+
{/* Error State */}
|
| 601 |
+
{error && !isLoading && (
|
| 602 |
+
<div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-red-200">
|
| 603 |
+
<div className="text-center p-8">
|
| 604 |
+
<div className="text-red-500 text-5xl mb-4">⚠️</div>
|
| 605 |
+
<h3 className="text-lg font-semibold text-gray-800 mb-2">Error Loading Data</h3>
|
| 606 |
+
<p className="text-gray-600 mb-4">{error}</p>
|
| 607 |
+
<button
|
| 608 |
+
onClick={() => window.location.reload()}
|
| 609 |
+
className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"
|
| 610 |
+
>
|
| 611 |
+
Retry
|
| 612 |
+
</button>
|
| 613 |
+
</div>
|
| 614 |
+
</div>
|
| 615 |
+
)}
|
| 616 |
+
|
| 617 |
+
{/* Main Content - Only show when not loading and no error */}
|
| 618 |
+
{!isLoading && !error && (
|
| 619 |
+
<>
|
| 620 |
+
{/* Left: Form */}
|
| 621 |
+
<div className={`flex-1 bg-white rounded-xl shadow-sm border p-4 lg:p-6 lg:overflow-y-auto lg:h-full no-scrollbar pb-40 lg:pb-6 ${isReturnMode ? 'border-red-200' : 'border-gray-100'}`}>
|
| 622 |
+
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
|
| 623 |
+
<h2 className={`text-lg font-bold flex items-center gap-2 ${isReturnMode ? 'text-red-600' : 'text-gray-800'}`}>
|
| 624 |
+
{isReturnMode ? <RotateCcw className="text-red-500" /> : <div className="text-teal-600 font-bold">IN</div>}
|
| 625 |
+
{isReturnMode ? 'आवक परतावा (Purchase Return)' : 'आवक बिल (Purchase)'}
|
| 626 |
+
</h2>
|
| 627 |
+
|
| 628 |
+
<div className="flex items-center gap-3">
|
| 629 |
+
<div className="flex bg-gray-100 rounded-lg p-1">
|
| 630 |
+
<button
|
| 631 |
+
onClick={() => setIsReturnMode(false)}
|
| 632 |
+
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${!isReturnMode ? 'bg-white text-teal-700 shadow-sm' : 'text-gray-500'}`}
|
| 633 |
+
>
|
| 634 |
+
Regular
|
| 635 |
+
</button>
|
| 636 |
+
<button
|
| 637 |
+
onClick={() => setIsReturnMode(true)}
|
| 638 |
+
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${isReturnMode ? 'bg-red-500 text-white shadow-sm' : 'text-gray-500'}`}
|
| 639 |
+
>
|
| 640 |
+
Return
|
| 641 |
+
</button>
|
| 642 |
+
</div>
|
| 643 |
+
|
| 644 |
+
<input
|
| 645 |
+
type="date"
|
| 646 |
+
className="bg-gray-100 px-3 py-1 rounded text-sm font-mono text-gray-600 border border-gray-200 hover:border-teal-400 focus:border-teal-500 focus:ring-1 focus:ring-teal-500 outline-none cursor-pointer transition-colors"
|
| 647 |
+
value={billDate}
|
| 648 |
+
onChange={(e) => setBillDate(e.target.value)}
|
| 649 |
+
max={new Date().toISOString().split('T')[0]}
|
| 650 |
+
/>
|
| 651 |
+
</div>
|
| 652 |
+
</div>
|
| 653 |
+
|
| 654 |
+
{/* Header Fields */}
|
| 655 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
| 656 |
+
|
| 657 |
+
<div>
|
| 658 |
+
<label className="block text-sm font-medium text-gray-700 mb-1">पार्टी नाव (Party)</label>
|
| 659 |
+
<select
|
| 660 |
+
className="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-teal-500 outline-none"
|
| 661 |
+
value={selectedParty}
|
| 662 |
+
onChange={e => setSelectedParty(e.target.value)}
|
| 663 |
+
>
|
| 664 |
+
<option value="">Select Party</option>
|
| 665 |
+
{parties.map(p => (
|
| 666 |
+
<option key={p.id} value={p.id}>{p.name} - {p.city}</option>
|
| 667 |
+
))}
|
| 668 |
+
</select>
|
| 669 |
+
</div>
|
| 670 |
+
</div>
|
| 671 |
+
|
| 672 |
+
{/* Items Table */}
|
| 673 |
+
<div className="mb-6">
|
| 674 |
+
<div className="flex justify-between items-center mb-2">
|
| 675 |
+
<h3 className="font-semibold text-gray-700">माल तपशील (Items)</h3>
|
| 676 |
+
</div>
|
| 677 |
+
|
| 678 |
+
<div className="space-y-4">
|
| 679 |
+
{items.map((item, index) => (
|
| 680 |
+
<div key={item.id} className="p-4 border rounded-lg bg-gray-50 relative shadow-sm">
|
| 681 |
+
<button
|
| 682 |
+
onClick={() => removeItem(item.id!)}
|
| 683 |
+
className="absolute top-2 right-2 text-red-400 hover:text-red-600 p-1"
|
| 684 |
+
>
|
| 685 |
+
<Trash2 size={18} />
|
| 686 |
+
</button>
|
| 687 |
+
|
| 688 |
+
<div className="grid grid-cols-2 md:grid-cols-6 gap-3">
|
| 689 |
+
<div className="col-span-2">
|
| 690 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">मिरची जात (Type)</label>
|
| 691 |
+
<select
|
| 692 |
+
className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
|
| 693 |
+
value={item.mirchi_type_id || ''}
|
| 694 |
+
onChange={e => handleMirchiChange(item.id!, e.target.value)}
|
| 695 |
+
>
|
| 696 |
+
<option value="">Select Type</option>
|
| 697 |
+
{mirchiTypes.map(m => (
|
| 698 |
+
<option key={m.id} value={m.id}>{m.name}</option>
|
| 699 |
+
))}
|
| 700 |
+
</select>
|
| 701 |
+
</div>
|
| 702 |
+
|
| 703 |
+
{/* LOT Number Section - Dynamic based on mode */}
|
| 704 |
+
{item.mirchi_type_id && (
|
| 705 |
+
<div className="col-span-2">
|
| 706 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">
|
| 707 |
+
LOT Number {isReturnMode && <span className="text-red-500">(Select to Return)</span>}
|
| 708 |
+
</label>
|
| 709 |
+
{isReturnMode ? (
|
| 710 |
+
// RETURN MODE: Select existing lot
|
| 711 |
+
<select
|
| 712 |
+
className="w-full border border-gray-300 rounded p-2 text-sm font-mono bg-white"
|
| 713 |
+
value={item.lot_id || ''}
|
| 714 |
+
onChange={e => {
|
| 715 |
+
const selectedLot = availableLots[item.id!]?.find(l => l.id === e.target.value);
|
| 716 |
+
if (selectedLot) {
|
| 717 |
+
handleItemChange(item.id!, 'lot_id', selectedLot.id);
|
| 718 |
+
handleItemChange(item.id!, 'lot_number', selectedLot.lot_number);
|
| 719 |
+
}
|
| 720 |
+
}}
|
| 721 |
+
>
|
| 722 |
+
<option value="">Select LOT to return</option>
|
| 723 |
+
{(availableLots[item.id!] || []).map(lot => (
|
| 724 |
+
<option key={lot.id} value={lot.id}>
|
| 725 |
+
{lot.lot_number} ({lot.remaining_quantity}kg available)
|
| 726 |
+
</option>
|
| 727 |
+
))}
|
| 728 |
+
</select>
|
| 729 |
+
) : (
|
| 730 |
+
// NORMAL MODE: New lot input
|
| 731 |
+
<input
|
| 732 |
+
type="text"
|
| 733 |
+
className="w-full border border-gray-300 rounded p-2 text-sm font-mono"
|
| 734 |
+
placeholder="Enter new LOT number"
|
| 735 |
+
value={lotInputs[item.id!] || ''}
|
| 736 |
+
onChange={e => {
|
| 737 |
+
setLotInputs({ ...lotInputs, [item.id!]: e.target.value });
|
| 738 |
+
handleItemChange(item.id!, 'lot_number', e.target.value);
|
| 739 |
+
handleItemChange(item.id!, 'is_existing_lot', false);
|
| 740 |
+
}}
|
| 741 |
+
/>
|
| 742 |
+
)}
|
| 743 |
+
</div>
|
| 744 |
+
)}
|
| 745 |
+
<div className="col-span-2 md:col-span-4">
|
| 746 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">पोटी वजन (Eg: 10,20,30)</label>
|
| 747 |
+
<input
|
| 748 |
+
type="text"
|
| 749 |
+
placeholder="10, 20, 30"
|
| 750 |
+
className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
|
| 751 |
+
value={potiInputs[item.id!] || ''}
|
| 752 |
+
onChange={e => handlePotiInputChange(item.id!, e.target.value)}
|
| 753 |
+
/>
|
| 754 |
+
</div>
|
| 755 |
+
|
| 756 |
+
<div>
|
| 757 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">Gross</label>
|
| 758 |
+
<input
|
| 759 |
+
type="number"
|
| 760 |
+
readOnly
|
| 761 |
+
className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm"
|
| 762 |
+
value={item.gross_weight}
|
| 763 |
+
/>
|
| 764 |
+
</div>
|
| 765 |
+
<div>
|
| 766 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">Poti (count)</label>
|
| 767 |
+
<input
|
| 768 |
+
type="number"
|
| 769 |
+
min="0"
|
| 770 |
+
className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm text-red-500 appearance-none"
|
| 771 |
+
value={item.poti_count === 0 ? '' : item.poti_count}
|
| 772 |
+
onChange={e => {
|
| 773 |
+
const count = Math.max(0, parseFloat(e.target.value) || 0);
|
| 774 |
+
const gross = item.gross_weight || 0;
|
| 775 |
+
const potya = count; // 1kg deduction per bag
|
| 776 |
+
|
| 777 |
+
// Keep poti_count in sync for totals (hamali, etc.)
|
| 778 |
+
handleItemChange(item.id!, 'poti_count', count);
|
| 779 |
+
handleItemChange(item.id!, 'total_potya', potya);
|
| 780 |
+
|
| 781 |
+
// If gross is known (from detailed weights), adjust net accordingly
|
| 782 |
+
if (gross > 0) {
|
| 783 |
+
const net = Math.max(0, gross - potya);
|
| 784 |
+
handleItemChange(item.id!, 'net_weight', net);
|
| 785 |
+
}
|
| 786 |
+
}}
|
| 787 |
+
placeholder="0"
|
| 788 |
+
/>
|
| 789 |
+
</div>
|
| 790 |
+
<div>
|
| 791 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">Net (Editable)</label>
|
| 792 |
+
<input
|
| 793 |
+
type="number"
|
| 794 |
+
min="0"
|
| 795 |
+
className="w-full bg-blue-50 border border-blue-200 rounded p-2 text-sm font-bold text-blue-800 appearance-none"
|
| 796 |
+
value={item.net_weight === 0 ? '' : item.net_weight}
|
| 797 |
+
onChange={e => handleItemChange(item.id!, 'net_weight', Math.max(0, parseFloat(e.target.value) || 0))}
|
| 798 |
+
placeholder="Auto-calculated"
|
| 799 |
+
/>
|
| 800 |
+
</div>
|
| 801 |
+
|
| 802 |
+
<div>
|
| 803 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">Rate (₹)</label>
|
| 804 |
+
<input
|
| 805 |
+
type="number"
|
| 806 |
+
min="0"
|
| 807 |
+
className="w-full border border-gray-300 rounded p-2 text-sm bg-white appearance-none"
|
| 808 |
+
onWheel={e => e.currentTarget.blur()}
|
| 809 |
+
value={item.rate_per_kg === 0 ? '' : item.rate_per_kg}
|
| 810 |
+
onChange={e => handleItemChange(item.id!, 'rate_per_kg', Math.max(0, parseFloat(e.target.value) || 0))}
|
| 811 |
+
placeholder="0"
|
| 812 |
+
/>
|
| 813 |
+
</div>
|
| 814 |
+
<div className="col-span-2 md:col-span-2">
|
| 815 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">Total (₹)</label>
|
| 816 |
+
<input type="number" readOnly className="w-full bg-green-50 border border-green-200 rounded p-2 text-sm font-bold text-right text-green-800" value={item.item_total?.toFixed(2)} />
|
| 817 |
+
</div>
|
| 818 |
+
</div>
|
| 819 |
+
</div>
|
| 820 |
+
))}
|
| 821 |
+
</div>
|
| 822 |
+
<button
|
| 823 |
+
onClick={addItem}
|
| 824 |
+
className="w-full mt-4 flex justify-center items-center gap-2 text-teal-600 font-medium bg-teal-50 hover:bg-teal-100 py-3 rounded-lg border border-teal-100 transition"
|
| 825 |
+
>
|
| 826 |
+
<Plus size={18} /> Add New Item (नवीन माल)
|
| 827 |
+
</button>
|
| 828 |
+
</div>
|
| 829 |
+
</div>
|
| 830 |
+
|
| 831 |
+
{/* Desktop: Right Summary Sidebar */}
|
| 832 |
+
<div className="hidden lg:flex w-80 flex-col gap-4">
|
| 833 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-0">
|
| 834 |
+
<h3 className="font-bold text-gray-800 mb-4 border-b pb-2">बिल सारांश (Summary)</h3>
|
| 835 |
+
{SummaryContent()}
|
| 836 |
+
<div className="flex gap-2 mt-6">
|
| 837 |
+
{savedTransaction && (
|
| 838 |
+
<>
|
| 839 |
+
<PrintInvoice transaction={savedTransaction} />
|
| 840 |
+
<PdfInvoice transaction={savedTransaction} />
|
| 841 |
+
</>
|
| 842 |
+
)}
|
| 843 |
+
<button
|
| 844 |
+
onClick={handleSubmit}
|
| 845 |
+
disabled={isSubmitting || items.length === 0}
|
| 846 |
+
className={`flex-1 bg-teal-600 text-white py-3 rounded-lg font-bold shadow-lg hover:bg-teal-700 transition-all flex items-center justify-center gap-2 ${isSubmitting || items.length === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
|
| 847 |
+
>
|
| 848 |
+
<Save size={20} />
|
| 849 |
+
{isSubmitting ? 'Saving...' : 'Save Bill'}
|
| 850 |
+
</button>
|
| 851 |
+
</div>
|
| 852 |
+
</div>
|
| 853 |
+
</div>
|
| 854 |
+
|
| 855 |
+
{/* Mobile: Sticky Bottom Action Bar */}
|
| 856 |
+
<div className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-[0_-4px_10px_rgba(0,0,0,0.1)] z-20">
|
| 857 |
+
<div className="flex items-center justify-between p-4 bg-white z-20 relative">
|
| 858 |
+
<div
|
| 859 |
+
onClick={() => setShowMobileSummary(!showMobileSummary)}
|
| 860 |
+
className="flex flex-col cursor-pointer"
|
| 861 |
+
>
|
| 862 |
+
<div className="flex items-center gap-1 text-gray-500 text-xs font-medium uppercase tracking-wide">
|
| 863 |
+
Summary {showMobileSummary ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
|
| 864 |
+
</div>
|
| 865 |
+
<div className="text-xl font-bold text-red-600">
|
| 866 |
+
₹{grandTotal.toFixed(2)}
|
| 867 |
+
</div>
|
| 868 |
+
</div>
|
| 869 |
+
<button
|
| 870 |
+
onClick={handleSubmit}
|
| 871 |
+
disabled={isSubmitting}
|
| 872 |
+
className="bg-teal-600 text-white px-6 py-2.5 rounded-lg font-semibold flex items-center gap-2 shadow-sm active:bg-teal-700 disabled:opacity-50"
|
| 873 |
+
>
|
| 874 |
+
<Save size={18} /> {isSubmitting ? '...' : 'Save'}
|
| 875 |
+
</button>
|
| 876 |
+
</div>
|
| 877 |
+
|
| 878 |
+
{showMobileSummary && (
|
| 879 |
+
<div className="px-6 pb-6 pt-0 bg-white border-t border-dashed border-gray-200 max-h-[60vh] overflow-y-auto animate-in slide-in-from-bottom-2">
|
| 880 |
+
<div className="mt-4">
|
| 881 |
+
{SummaryContent()}
|
| 882 |
+
</div>
|
| 883 |
+
{savedTransaction && (
|
| 884 |
+
<div className="mt-4 flex justify-center">
|
| 885 |
+
<PrintInvoice transaction={savedTransaction} />
|
| 886 |
+
</div>
|
| 887 |
+
)}
|
| 888 |
+
</div>
|
| 889 |
+
)}
|
| 890 |
+
</div>
|
| 891 |
+
</>
|
| 892 |
+
)}
|
| 893 |
+
</div>
|
| 894 |
+
);
|
| 895 |
+
};
|
| 896 |
+
|
| 897 |
+
export default AwaakBill;
|
pages/Dashboard.tsx
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState } from 'react';
|
| 2 |
+
import { getTransactions, getActiveLots, getParties } from '../services/db';
|
| 3 |
+
import { Transaction, Lot, Party, BillType } from '../types';
|
| 4 |
+
|
| 5 |
+
import {
|
| 6 |
+
TrendingUp,
|
| 7 |
+
Package,
|
| 8 |
+
AlertCircle,
|
| 9 |
+
Bell,
|
| 10 |
+
IndianRupee,
|
| 11 |
+
ArrowUpRight,
|
| 12 |
+
ArrowDownLeft
|
| 13 |
+
} from 'lucide-react';
|
| 14 |
+
import {
|
| 15 |
+
BarChart,
|
| 16 |
+
Bar,
|
| 17 |
+
XAxis,
|
| 18 |
+
Tooltip,
|
| 19 |
+
ResponsiveContainer
|
| 20 |
+
} from 'recharts';
|
| 21 |
+
import { Link } from 'react-router-dom';
|
| 22 |
+
|
| 23 |
+
const Dashboard = () => {
|
| 24 |
+
const [recentTx, setRecentTx] = useState<Transaction[]>([]);
|
| 25 |
+
const [totalStock, setTotalStock] = useState(0);
|
| 26 |
+
const [stockValue, setStockValue] = useState(0);
|
| 27 |
+
const [activeLots, setActiveLots] = useState<Lot[]>([]);
|
| 28 |
+
const [dueParties, setDueParties] = useState<Party[]>([]);
|
| 29 |
+
const [notifications, setNotifications] = useState<string[]>([]);
|
| 30 |
+
const [showNotif, setShowNotif] = useState(false);
|
| 31 |
+
const [lowStockBagsThreshold, setLowStockBagsThreshold] = useState<number>(10);
|
| 32 |
+
|
| 33 |
+
useEffect(() => {
|
| 34 |
+
try {
|
| 35 |
+
const stored = localStorage.getItem('pp_alert_config');
|
| 36 |
+
if (!stored) return;
|
| 37 |
+
const parsed = JSON.parse(stored);
|
| 38 |
+
if (
|
| 39 |
+
parsed &&
|
| 40 |
+
typeof parsed.lowStockBagsThreshold === 'number' &&
|
| 41 |
+
!Number.isNaN(parsed.lowStockBagsThreshold)
|
| 42 |
+
) {
|
| 43 |
+
setLowStockBagsThreshold(parsed.lowStockBagsThreshold);
|
| 44 |
+
}
|
| 45 |
+
} catch (error) {
|
| 46 |
+
console.error('Error loading alert config', error);
|
| 47 |
+
}
|
| 48 |
+
}, []);
|
| 49 |
+
|
| 50 |
+
useEffect(() => {
|
| 51 |
+
const loadData = async () => {
|
| 52 |
+
const txs = await getTransactions();
|
| 53 |
+
const lots = await getActiveLots();
|
| 54 |
+
const parties = await getParties();
|
| 55 |
+
|
| 56 |
+
setRecentTx(txs.slice(0, 5));
|
| 57 |
+
setActiveLots(lots);
|
| 58 |
+
|
| 59 |
+
const stockQty = lots.reduce((acc, lot) => acc + lot.remaining_quantity, 0);
|
| 60 |
+
const stockVal = lots.reduce((acc, lot) => acc + (lot.remaining_quantity * lot.avg_rate), 0);
|
| 61 |
+
|
| 62 |
+
setTotalStock(stockQty);
|
| 63 |
+
setStockValue(stockVal);
|
| 64 |
+
|
| 65 |
+
const dues = parties.filter(p => p.current_balance !== 0);
|
| 66 |
+
setDueParties(dues);
|
| 67 |
+
|
| 68 |
+
const bagCounts: Record<string, number> = {};
|
| 69 |
+
txs.forEach((tx) => {
|
| 70 |
+
tx.items.forEach((item) => {
|
| 71 |
+
if (!item.lot_id) return;
|
| 72 |
+
const lotId = item.lot_id;
|
| 73 |
+
if (!bagCounts[lotId]) bagCounts[lotId] = 0;
|
| 74 |
+
if (tx.bill_type === BillType.AWAAK) {
|
| 75 |
+
bagCounts[lotId] += tx.is_return ? -item.poti_count : item.poti_count;
|
| 76 |
+
} else {
|
| 77 |
+
bagCounts[lotId] += tx.is_return ? item.poti_count : -item.poti_count;
|
| 78 |
+
}
|
| 79 |
+
});
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
const alerts: string[] = [];
|
| 83 |
+
|
| 84 |
+
const lowStockLots = lots.filter((lot) => {
|
| 85 |
+
const bags = Math.max(0, bagCounts[lot.id] || 0);
|
| 86 |
+
return bags > 0 && bags <= lowStockBagsThreshold;
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
if (lowStockLots.length > 0) {
|
| 90 |
+
alerts.push(`${lowStockLots.length} lot(s) are running low on stock (≤ ${lowStockBagsThreshold} bags).`);
|
| 91 |
+
lowStockLots.slice(0, 3).forEach((lot) => {
|
| 92 |
+
const bags = Math.max(0, bagCounts[lot.id] || 0);
|
| 93 |
+
alerts.push(`LOT ${lot.lot_number} has only ${bags} bags left.`);
|
| 94 |
+
});
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
const paymentPending = dues.length;
|
| 98 |
+
if (paymentPending > 0) alerts.push(`${paymentPending} parties have pending payments.`);
|
| 99 |
+
if (txs.length === 0) alerts.push("Welcome! Create your first bill to get started.");
|
| 100 |
+
|
| 101 |
+
setNotifications(alerts);
|
| 102 |
+
};
|
| 103 |
+
loadData();
|
| 104 |
+
}, [lowStockBagsThreshold]);
|
| 105 |
+
|
| 106 |
+
const chartData = activeLots.map(lot => ({
|
| 107 |
+
name: lot.mirchi_name,
|
| 108 |
+
qty: lot.remaining_quantity
|
| 109 |
+
}));
|
| 110 |
+
|
| 111 |
+
const aggregatedChartData = Object.values(chartData.reduce((acc: any, curr) => {
|
| 112 |
+
if (!acc[curr.name]) acc[curr.name] = { name: curr.name, qty: 0 };
|
| 113 |
+
acc[curr.name].qty += curr.qty;
|
| 114 |
+
return acc;
|
| 115 |
+
}, {}));
|
| 116 |
+
|
| 117 |
+
return (
|
| 118 |
+
<div className="space-y-4 md:space-y-8 pb-6 md:pb-10">
|
| 119 |
+
{/* Header Section with Separated Notification Bell */}
|
| 120 |
+
<div className="flex flex-col md:flex-row justify-between items-stretch gap-3 md:gap-6">
|
| 121 |
+
{/* Welcome Card */}
|
| 122 |
+
<div className="flex-1 bg-gradient-to-r from-teal-800 to-teal-700 rounded-xl p-4 md:p-6 text-white shadow-md relative overflow-hidden min-h-[110px] md:min-h-[120px] flex flex-col justify-center">
|
| 123 |
+
<div className="z-10 relative">
|
| 124 |
+
<h1 className="text-xl md:text-2xl font-bold">नमस्कार, Admin! 👋</h1>
|
| 125 |
+
<p className="text-teal-100 mt-2 opacity-90 max-w-lg text-sm md:text-base">
|
| 126 |
+
तुमच्या व्यवसायाचा आजचा आढावा येथे आहे. (Here is your business overview for today.)
|
| 127 |
+
</p>
|
| 128 |
+
</div>
|
| 129 |
+
{/* Decorative Background Elements */}
|
| 130 |
+
<div className="absolute right-0 top-0 h-full w-1/3 bg-white/10 skew-x-12 z-0 pointer-events-none"></div>
|
| 131 |
+
<div className="absolute -bottom-8 -right-8 w-32 h-32 bg-teal-500/20 rounded-full blur-2xl z-0 pointer-events-none"></div>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
{/* Actions / Notifications */}
|
| 135 |
+
<div className="flex items-center justify-between md:justify-center gap-2 md:gap-4 bg-white px-3 py-2 md:p-4 rounded-xl shadow-sm border border-gray-100 h-auto self-stretch">
|
| 136 |
+
<div className="flex flex-col text-left text-xs md:text-sm">
|
| 137 |
+
<span className="text-xs text-gray-500 font-medium uppercase">Current Date</span>
|
| 138 |
+
<span className="font-bold text-gray-800">{new Date().toLocaleDateString('en-IN', { day: 'numeric', month: 'short' })}</span>
|
| 139 |
+
</div>
|
| 140 |
+
<div className="hidden md:block h-10 w-px" />
|
| 141 |
+
<div className="relative flex-shrink-0 ml-auto">
|
| 142 |
+
<button
|
| 143 |
+
onClick={() => setShowNotif(!showNotif)}
|
| 144 |
+
className="p-3 bg-gray-50 hover:bg-teal-50 text-gray-600 hover:text-teal-700 rounded-full transition-colors relative border border-gray-200"
|
| 145 |
+
aria-label="Notifications"
|
| 146 |
+
>
|
| 147 |
+
<Bell size={24} />
|
| 148 |
+
{notifications.length > 0 && (
|
| 149 |
+
<span className="absolute top-0 right-0 w-3 h-3 bg-red-500 rounded-full border-2 border-white animate-pulse"></span>
|
| 150 |
+
)}
|
| 151 |
+
</button>
|
| 152 |
+
|
| 153 |
+
{/* Notification Dropdown - aligned to bell */}
|
| 154 |
+
{showNotif && (
|
| 155 |
+
<div
|
| 156 |
+
className="absolute top-full mt-2 w-72 sm:w-80 bg-white rounded-xl shadow-2xl border border-gray-100 text-gray-800 z-50 animate-in slide-in-from-top-2 origin-top right-0"
|
| 157 |
+
>
|
| 158 |
+
<div className="p-4 border-b bg-gray-50 rounded-t-xl font-semibold flex justify-between items-center">
|
| 159 |
+
<span>सूचना (Notifications)</span>
|
| 160 |
+
<span className="text-xs bg-teal-100 text-teal-800 px-2 py-1 rounded-full font-bold">{notifications.length}</span>
|
| 161 |
+
</div>
|
| 162 |
+
<div className="max-h-72 overflow-y-auto custom-scrollbar">
|
| 163 |
+
{notifications.length === 0 ? (
|
| 164 |
+
<div className="p-6 text-center text-gray-400 text-sm">No new notifications</div>
|
| 165 |
+
) : (
|
| 166 |
+
<div className="divide-y divide-gray-100">
|
| 167 |
+
{notifications.map((note, i) => (
|
| 168 |
+
<div key={i} className="p-4 hover:bg-gray-50 text-sm flex items-start gap-3 transition-colors">
|
| 169 |
+
<div className="w-2 h-2 mt-1.5 rounded-full bg-orange-500 shrink-0"></div>
|
| 170 |
+
<p className="text-gray-600 leading-relaxed">{note}</p>
|
| 171 |
+
</div>
|
| 172 |
+
))}
|
| 173 |
+
</div>
|
| 174 |
+
)}
|
| 175 |
+
</div>
|
| 176 |
+
<div className="p-2 border-t text-center">
|
| 177 |
+
<button onClick={() => setShowNotif(false)} className="text-xs text-teal-600 font-medium hover:underline">Close</button>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
)}
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
{/* KPI Cards - mobile swipeable row, desktop grid */}
|
| 186 |
+
{/* Mobile: horizontal scroll */}
|
| 187 |
+
<div className="md:hidden -mx-4 px-4 mt-1">
|
| 188 |
+
<div className="flex gap-3 overflow-x-auto pb-1 no-scrollbar snap-x snap-mandatory">
|
| 189 |
+
<div className="min-w-[75%] snap-start bg-white p-4 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
|
| 190 |
+
<div className="flex items-center justify-between">
|
| 191 |
+
<div>
|
| 192 |
+
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Total Stock</p>
|
| 193 |
+
<h3 className="text-xl font-bold text-gray-800 mt-1 group-hover:text-teal-600 transition-colors">{totalStock.toLocaleString()} <span className="text-xs font-normal text-gray-400">kg</span></h3>
|
| 194 |
+
</div>
|
| 195 |
+
<div className="p-3 bg-blue-50 text-blue-600 rounded-2xl group-hover:bg-blue-100 transition-colors">
|
| 196 |
+
<Package size={20} />
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
|
| 201 |
+
<div className="min-w-[75%] snap-start bg-white p-4 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
|
| 202 |
+
<div className="flex items-center justify-between">
|
| 203 |
+
<div>
|
| 204 |
+
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Est. Value</p>
|
| 205 |
+
<h3 className="text-xl font-bold text-gray-800 mt-1 group-hover:text-green-600 transition-colors">₹ {(stockValue / 100000).toFixed(2)} L</h3>
|
| 206 |
+
</div>
|
| 207 |
+
<div className="p-3 bg-green-50 text-green-600 rounded-2xl group-hover:bg-green-100 transition-colors">
|
| 208 |
+
<IndianRupee size={20} />
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
|
| 213 |
+
<div className="min-w-[75%] snap-start bg-white p-4 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
|
| 214 |
+
<div className="flex items-center justify-between">
|
| 215 |
+
<div>
|
| 216 |
+
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Receivables (येणे)</p>
|
| 217 |
+
<h3 className="text-xl font-bold text-teal-600 mt-1">
|
| 218 |
+
₹ {dueParties.filter(p => p.current_balance > 0).reduce((a,b) => a + b.current_balance, 0).toLocaleString()}
|
| 219 |
+
</h3>
|
| 220 |
+
</div>
|
| 221 |
+
<div className="p-3 bg-teal-50 text-teal-600 rounded-2xl group-hover:bg-teal-100 transition-colors">
|
| 222 |
+
<ArrowDownLeft size={20} />
|
| 223 |
+
</div>
|
| 224 |
+
</div>
|
| 225 |
+
</div>
|
| 226 |
+
|
| 227 |
+
<div className="min-w-[75%] snap-start bg-white p-4 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
|
| 228 |
+
<div className="flex items-center justify-between">
|
| 229 |
+
<div>
|
| 230 |
+
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Payables (देणे)</p>
|
| 231 |
+
<h3 className="text-xl font-bold text-red-500 mt-1">
|
| 232 |
+
₹ {Math.abs(dueParties.filter(p => p.current_balance < 0).reduce((a,b) => a + b.current_balance, 0)).toLocaleString()}
|
| 233 |
+
</h3>
|
| 234 |
+
</div>
|
| 235 |
+
<div className="p-3 bg-red-50 text-red-600 rounded-2xl group-hover:bg-red-100 transition-colors">
|
| 236 |
+
<ArrowUpRight size={20} />
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
|
| 243 |
+
{/* Desktop / tablet: original grid */}
|
| 244 |
+
<div className="hidden md:grid grid-cols-2 lg:grid-cols-4 gap-6">
|
| 245 |
+
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
|
| 246 |
+
<div className="flex items-center justify-between">
|
| 247 |
+
<div>
|
| 248 |
+
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Total Stock</p>
|
| 249 |
+
<h3 className="text-2xl font-bold text-gray-800 mt-2 group-hover:text-teal-600 transition-colors">{totalStock.toLocaleString()} <span className="text-sm font-normal text-gray-400">kg</span></h3>
|
| 250 |
+
</div>
|
| 251 |
+
<div className="p-4 bg-blue-50 text-blue-600 rounded-2xl group-hover:bg-blue-100 transition-colors">
|
| 252 |
+
<Package size={24} />
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
</div>
|
| 256 |
+
|
| 257 |
+
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
|
| 258 |
+
<div className="flex items-center justify-between">
|
| 259 |
+
<div>
|
| 260 |
+
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Est. Value</p>
|
| 261 |
+
<h3 className="text-2xl font-bold text-gray-800 mt-2 group-hover:text-green-600 transition-colors">₹ {(stockValue / 100000).toFixed(2)} L</h3>
|
| 262 |
+
</div>
|
| 263 |
+
<div className="p-4 bg-green-50 text-green-600 rounded-2xl group-hover:bg-green-100 transition-colors">
|
| 264 |
+
<IndianRupee size={24} />
|
| 265 |
+
</div>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
|
| 269 |
+
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
|
| 270 |
+
<div className="flex items-center justify-between">
|
| 271 |
+
<div>
|
| 272 |
+
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Receivables (येणे)</p>
|
| 273 |
+
<h3 className="text-2xl font-bold text-teal-600 mt-2">
|
| 274 |
+
₹ {dueParties.filter(p => p.current_balance > 0).reduce((a,b) => a + b.current_balance, 0).toLocaleString()}
|
| 275 |
+
</h3>
|
| 276 |
+
</div>
|
| 277 |
+
<div className="p-4 bg-teal-50 text-teal-600 rounded-2xl group-hover:bg-teal-100 transition-colors">
|
| 278 |
+
<ArrowDownLeft size={24} />
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
|
| 283 |
+
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow group">
|
| 284 |
+
<div className="flex items-center justify-between">
|
| 285 |
+
<div>
|
| 286 |
+
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Payables (देणे)</p>
|
| 287 |
+
<h3 className="text-2xl font-bold text-red-500 mt-2">
|
| 288 |
+
₹ {Math.abs(dueParties.filter(p => p.current_balance < 0).reduce((a,b) => a + b.current_balance, 0)).toLocaleString()}
|
| 289 |
+
</h3>
|
| 290 |
+
</div>
|
| 291 |
+
<div className="p-4 bg-red-50 text-red-600 rounded-2xl group-hover:bg-red-100 transition-colors">
|
| 292 |
+
<ArrowUpRight size={24} />
|
| 293 |
+
</div>
|
| 294 |
+
</div>
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
+
|
| 298 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 md:gap-8">
|
| 299 |
+
{/* Recent Transactions */}
|
| 300 |
+
<div className="lg:col-span-2 flex flex-col gap-6">
|
| 301 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-100 flex flex-col h-full">
|
| 302 |
+
<div className="p-3 md:p-4 border-b border-gray-100 flex items-center justify-between">
|
| 303 |
+
<h3 className="font-bold text-gray-800 flex items-center gap-2">
|
| 304 |
+
<TrendingUp size={20} className="text-gray-400" />
|
| 305 |
+
अलीकडील व्यवहार (Recent)
|
| 306 |
+
</h3>
|
| 307 |
+
<Link to="/ledger" className="text-xs md:text-sm text-teal-600 font-bold hover:bg-teal-50 px-3 py-1.5 rounded-lg transition">View All</Link>
|
| 308 |
+
</div>
|
| 309 |
+
{/* Desktop table */}
|
| 310 |
+
<div className="hidden md:block overflow-x-auto flex-1">
|
| 311 |
+
<table className="w-full text-sm text-left">
|
| 312 |
+
<thead className="bg-gray-50 text-gray-500 uppercase text-xs tracking-wider">
|
| 313 |
+
<tr>
|
| 314 |
+
<th className="px-6 py-4">Bill No</th>
|
| 315 |
+
<th className="px-6 py-4">Party</th>
|
| 316 |
+
<th className="px-6 py-4">Type</th>
|
| 317 |
+
<th className="px-6 py-4 text-right">Amount</th>
|
| 318 |
+
</tr>
|
| 319 |
+
</thead>
|
| 320 |
+
<tbody className="divide-y divide-gray-100">
|
| 321 |
+
{recentTx.length === 0 ? (
|
| 322 |
+
<tr><td colSpan={4} className="text-center py-12 text-gray-400">No transactions recorded yet.</td></tr>
|
| 323 |
+
) : (
|
| 324 |
+
recentTx.map(tx => (
|
| 325 |
+
<tr key={tx.id} className="hover:bg-gray-50 transition-colors">
|
| 326 |
+
<td className="px-6 py-4">
|
| 327 |
+
<div className="font-bold text-gray-700">{tx.bill_number}</div>
|
| 328 |
+
<div className="text-xs text-gray-400 mt-0.5">{tx.bill_date}</div>
|
| 329 |
+
</td>
|
| 330 |
+
<td className="px-6 py-4 font-medium text-gray-900">{tx.party_name}</td>
|
| 331 |
+
<td className="px-6 py-4">
|
| 332 |
+
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold capitalize ${
|
| 333 |
+
tx.is_return
|
| 334 |
+
? 'bg-orange-100 text-orange-800'
|
| 335 |
+
: tx.bill_type === 'jawaak'
|
| 336 |
+
? 'bg-blue-100 text-blue-800'
|
| 337 |
+
: 'bg-green-100 text-green-800'
|
| 338 |
+
}`}>
|
| 339 |
+
{tx.is_return ? 'Return' : (tx.bill_type === 'jawaak' ? 'Purchase' : 'Sale')}
|
| 340 |
+
</span>
|
| 341 |
+
</td>
|
| 342 |
+
<td className="px-6 py-4 text-right font-bold text-gray-700">₹{tx.total_amount.toLocaleString()}</td>
|
| 343 |
+
</tr>
|
| 344 |
+
))
|
| 345 |
+
)}
|
| 346 |
+
</tbody>
|
| 347 |
+
</table>
|
| 348 |
+
</div>
|
| 349 |
+
|
| 350 |
+
{/* Mobile cards */}
|
| 351 |
+
<div className="md:hidden flex-1 p-3 space-y-2.5">
|
| 352 |
+
{recentTx.length === 0 ? (
|
| 353 |
+
<div className="text-center py-8 text-gray-400 text-sm">No transactions recorded yet.</div>
|
| 354 |
+
) : (
|
| 355 |
+
recentTx.map(tx => (
|
| 356 |
+
<div
|
| 357 |
+
key={tx.id}
|
| 358 |
+
className="border border-gray-100 rounded-lg p-3 hover:bg-gray-50 transition-colors flex flex-col gap-2"
|
| 359 |
+
>
|
| 360 |
+
<div className="flex justify-between items-start gap-2">
|
| 361 |
+
<div>
|
| 362 |
+
<div className="text-sm font-semibold text-gray-800">{tx.bill_number}</div>
|
| 363 |
+
<div className="text-[11px] text-gray-400 mt-0.5">{tx.bill_date}</div>
|
| 364 |
+
</div>
|
| 365 |
+
<span
|
| 366 |
+
className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold capitalize ${
|
| 367 |
+
tx.is_return
|
| 368 |
+
? 'bg-orange-100 text-orange-800'
|
| 369 |
+
: tx.bill_type === 'jawaak'
|
| 370 |
+
? 'bg-blue-100 text-blue-800'
|
| 371 |
+
: 'bg-green-100 text-green-800'
|
| 372 |
+
}`}
|
| 373 |
+
>
|
| 374 |
+
{tx.is_return ? 'Return' : tx.bill_type === 'jawaak' ? 'Purchase' : 'Sale'}
|
| 375 |
+
</span>
|
| 376 |
+
</div>
|
| 377 |
+
<div className="flex justify-between items-center text-xs mt-1">
|
| 378 |
+
<div className="text-gray-600 font-medium truncate max-w-[60%]">
|
| 379 |
+
{tx.party_name || 'Unknown Party'}
|
| 380 |
+
</div>
|
| 381 |
+
<div className="text-gray-900 font-bold text-sm">
|
| 382 |
+
₹{tx.total_amount.toLocaleString()}
|
| 383 |
+
</div>
|
| 384 |
+
</div>
|
| 385 |
+
</div>
|
| 386 |
+
))
|
| 387 |
+
)}
|
| 388 |
+
</div>
|
| 389 |
+
</div>
|
| 390 |
+
</div>
|
| 391 |
+
|
| 392 |
+
{/* Right Column */}
|
| 393 |
+
<div className="flex flex-col gap-6">
|
| 394 |
+
{/* Pending Payments List */}
|
| 395 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-100 flex flex-col max-h-[360px] md:max-h-[400px]">
|
| 396 |
+
<div className="p-3 md:p-4 border-b border-gray-100">
|
| 397 |
+
<h3 className="font-bold text-gray-800 flex items-center gap-2">
|
| 398 |
+
<AlertCircle size={20} className="text-orange-500" />
|
| 399 |
+
पेमेंट बाकी (Pending)
|
| 400 |
+
</h3>
|
| 401 |
+
</div>
|
| 402 |
+
<div className="overflow-y-auto custom-scrollbar p-2 space-y-2">
|
| 403 |
+
{dueParties.length === 0 ? (
|
| 404 |
+
<div className="text-center py-6 md:py-8 text-gray-400 text-sm">
|
| 405 |
+
<p>All payments settled! 🎉</p>
|
| 406 |
+
</div>
|
| 407 |
+
) : (
|
| 408 |
+
dueParties.map(p => (
|
| 409 |
+
<div key={p.id} className="flex justify-between items-center p-4 hover:bg-gray-50 rounded-lg border border-transparent hover:border-gray-100 transition-all">
|
| 410 |
+
<div>
|
| 411 |
+
<p className="font-bold text-sm text-gray-800">{p.name}</p>
|
| 412 |
+
<p className="text-xs text-gray-500 mt-0.5">{p.city}</p>
|
| 413 |
+
</div>
|
| 414 |
+
<div className={`text-sm font-bold ${p.current_balance > 0 ? 'text-teal-600' : 'text-red-500'}`}>
|
| 415 |
+
{p.current_balance > 0 ? '+' : ''}{p.current_balance.toLocaleString()}
|
| 416 |
+
</div>
|
| 417 |
+
</div>
|
| 418 |
+
))
|
| 419 |
+
)}
|
| 420 |
+
</div>
|
| 421 |
+
</div>
|
| 422 |
+
|
| 423 |
+
{/* Stock Chart */}
|
| 424 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 md:p-5">
|
| 425 |
+
<h3 className="font-bold text-gray-800 mb-4 md:mb-5 text-sm md:text-base">स्टॉक (Quantity)</h3>
|
| 426 |
+
<div className="h-40 md:h-48 w-full">
|
| 427 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 428 |
+
<BarChart data={aggregatedChartData}>
|
| 429 |
+
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{fontSize: 12, fill: '#6b7280'}} />
|
| 430 |
+
<Tooltip
|
| 431 |
+
contentStyle={{borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'}}
|
| 432 |
+
cursor={{fill: '#f3f4f6'}}
|
| 433 |
+
/>
|
| 434 |
+
<Bar dataKey="qty" fill="#0d9488" radius={[4, 4, 0, 0]} barSize={40} />
|
| 435 |
+
</BarChart>
|
| 436 |
+
</ResponsiveContainer>
|
| 437 |
+
</div>
|
| 438 |
+
</div>
|
| 439 |
+
</div>
|
| 440 |
+
</div>
|
| 441 |
+
</div>
|
| 442 |
+
);
|
| 443 |
+
};
|
| 444 |
+
|
| 445 |
+
export default Dashboard;
|
pages/JawaakBill.tsx
ADDED
|
@@ -0,0 +1,807 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import {
|
| 4 |
+
getParties, getMirchiTypes, generateBillNumber, saveTransaction,
|
| 5 |
+
getAvailableLotsByMirchi
|
| 6 |
+
} from '../services/db';
|
| 7 |
+
import {
|
| 8 |
+
Party, MirchiType, Lot, Transaction, TransactionItem, BillType, PaymentMode, PartyType
|
| 9 |
+
} from '../types';
|
| 10 |
+
import { Save, Trash2, Plus, RotateCcw, Printer, Download, ChevronUp, ChevronDown } from 'lucide-react';
|
| 11 |
+
import PrintInvoice from '../components/PrintInvoice';
|
| 12 |
+
import PdfInvoice from '../components/PdfInvoice';
|
| 13 |
+
|
| 14 |
+
const SummaryInput = ({ label, value, onChange }: { label: string, value: number, onChange: (val: number) => void }) => (
|
| 15 |
+
<div className="flex justify-between items-center mb-1 bg-gray-50 p-1.5 rounded">
|
| 16 |
+
<span className="text-gray-600 text-xs">{label}</span>
|
| 17 |
+
<input
|
| 18 |
+
type="number"
|
| 19 |
+
step="0.01"
|
| 20 |
+
min="0"
|
| 21 |
+
className="w-24 border rounded text-right p-1 text-sm bg-white focus:ring-1 focus:ring-teal-500 outline-none appearance-none"
|
| 22 |
+
value={value === 0 ? '' : value}
|
| 23 |
+
onChange={e => {
|
| 24 |
+
const parsed = parseFloat(e.target.value);
|
| 25 |
+
onChange(isNaN(parsed) ? 0 : Math.max(0, parsed));
|
| 26 |
+
}}
|
| 27 |
+
placeholder="0"
|
| 28 |
+
/>
|
| 29 |
+
</div>
|
| 30 |
+
);
|
| 31 |
+
|
| 32 |
+
const JawaakBill = () => {
|
| 33 |
+
const navigate = useNavigate();
|
| 34 |
+
|
| 35 |
+
// Master Data
|
| 36 |
+
const [parties, setParties] = useState<Party[]>([]);
|
| 37 |
+
const [mirchiTypes, setMirchiTypes] = useState<MirchiType[]>([]);
|
| 38 |
+
|
| 39 |
+
// Form State
|
| 40 |
+
const [isReturnMode, setIsReturnMode] = useState(false);
|
| 41 |
+
const [billDate, setBillDate] = useState(new Date().toISOString().split('T')[0]);
|
| 42 |
+
const [billNumber, setBillNumber] = useState('');
|
| 43 |
+
const [selectedParty, setSelectedParty] = useState('');
|
| 44 |
+
|
| 45 |
+
// Mobile UI State
|
| 46 |
+
const [showMobileSummary, setShowMobileSummary] = useState(false);
|
| 47 |
+
|
| 48 |
+
const [items, setItems] = useState<Partial<TransactionItem>[]>([{
|
| 49 |
+
id: Date.now().toString(),
|
| 50 |
+
poti_weights: [],
|
| 51 |
+
gross_weight: 0,
|
| 52 |
+
poti_count: 0,
|
| 53 |
+
total_potya: 0,
|
| 54 |
+
net_weight: 0,
|
| 55 |
+
rate_per_kg: 0,
|
| 56 |
+
item_total: 0
|
| 57 |
+
}]);
|
| 58 |
+
|
| 59 |
+
// Temp inputs for items row
|
| 60 |
+
const [potiInputs, setPotiInputs] = useState<{ [key: string]: string }>({});
|
| 61 |
+
|
| 62 |
+
// LOT selection state
|
| 63 |
+
const [availableLots, setAvailableLots] = useState<{ [key: string]: Lot[] }>({});
|
| 64 |
+
|
| 65 |
+
const [expenses, setExpenses] = useState({
|
| 66 |
+
cess_percent: 1.0,
|
| 67 |
+
cess_amount: 0,
|
| 68 |
+
adat_percent: 3.0,
|
| 69 |
+
adat_amount: 0,
|
| 70 |
+
poti_rate: 18, // Default rate
|
| 71 |
+
poti_amount: 0,
|
| 72 |
+
hamali_per_poti: 6,
|
| 73 |
+
hamali_amount: 0,
|
| 74 |
+
packaging_hamali_per_poti: 0,
|
| 75 |
+
packaging_hamali_amount: 0,
|
| 76 |
+
gaadi_bharni: 0,
|
| 77 |
+
other_expenses: 0,
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
// Payment State
|
| 81 |
+
const [paymentMode, setPaymentMode] = useState<'cash' | 'online' | 'hybrid' | 'due'>('cash');
|
| 82 |
+
const [paidAmount, setPaidAmount] = useState(0); // Not used directly in new logic, derived
|
| 83 |
+
const [cashAmount, setCashAmount] = useState(0); // For hybrid
|
| 84 |
+
const [onlineAmount, setOnlineAmount] = useState(0); // For hybrid & due
|
| 85 |
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
| 86 |
+
const [savedTransaction, setSavedTransaction] = useState<Transaction | null>(null);
|
| 87 |
+
const [isLoading, setIsLoading] = useState(true);
|
| 88 |
+
const [error, setError] = useState<string | null>(null);
|
| 89 |
+
|
| 90 |
+
// Initial Load & Bill Number Generation
|
| 91 |
+
useEffect(() => {
|
| 92 |
+
const loadData = async () => {
|
| 93 |
+
try {
|
| 94 |
+
setIsLoading(true);
|
| 95 |
+
setError(null);
|
| 96 |
+
const [partiesData, mirchiData] = await Promise.all([
|
| 97 |
+
getParties(),
|
| 98 |
+
getMirchiTypes()
|
| 99 |
+
]);
|
| 100 |
+
|
| 101 |
+
if (!partiesData || partiesData.length === 0) {
|
| 102 |
+
setError('No parties found. Please add parties in Settings first.');
|
| 103 |
+
}
|
| 104 |
+
if (!mirchiData || mirchiData.length === 0) {
|
| 105 |
+
setError('No mirchi types found. Please add mirchi types in Settings first.');
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// Filter parties for Jawaak bills
|
| 109 |
+
const filteredParties = partiesData.filter(p =>
|
| 110 |
+
p.party_type === PartyType.JAWAAK || p.party_type === PartyType.BOTH
|
| 111 |
+
);
|
| 112 |
+
|
| 113 |
+
setParties(filteredParties || []);
|
| 114 |
+
setMirchiTypes(mirchiData || []);
|
| 115 |
+
setBillNumber(generateBillNumber(BillType.JAWAAK, isReturnMode));
|
| 116 |
+
} catch (err: any) {
|
| 117 |
+
console.error('Error loading data:', err);
|
| 118 |
+
setError('Failed to load data. Please check your connection and try again.');
|
| 119 |
+
} finally {
|
| 120 |
+
setIsLoading(false);
|
| 121 |
+
}
|
| 122 |
+
};
|
| 123 |
+
loadData();
|
| 124 |
+
}, [isReturnMode]);
|
| 125 |
+
|
| 126 |
+
// Calculation Logic
|
| 127 |
+
const calculateRow = (item: Partial<TransactionItem>, potiString: string) => {
|
| 128 |
+
const weights = potiString.split(/[\s,+]+/).map(n => parseFloat(n)).filter(n => !isNaN(n) && n > 0);
|
| 129 |
+
|
| 130 |
+
const gross = weights.reduce((a, b) => a + b, 0);
|
| 131 |
+
const count = weights.length;
|
| 132 |
+
const potya = count * 1;
|
| 133 |
+
const net = Math.max(0, gross - potya);
|
| 134 |
+
const total = net * (item.rate_per_kg || 0);
|
| 135 |
+
|
| 136 |
+
return {
|
| 137 |
+
...item,
|
| 138 |
+
poti_weights: weights,
|
| 139 |
+
gross_weight: gross,
|
| 140 |
+
poti_count: count,
|
| 141 |
+
total_potya: potya,
|
| 142 |
+
net_weight: net,
|
| 143 |
+
item_total: total
|
| 144 |
+
};
|
| 145 |
+
};
|
| 146 |
+
|
| 147 |
+
const handleItemChange = (id: string, field: string, value: any) => {
|
| 148 |
+
setItems(prev => prev.map(item => {
|
| 149 |
+
if (item.id !== id) return item;
|
| 150 |
+
|
| 151 |
+
let updatedItem = { ...item, [field]: value };
|
| 152 |
+
|
| 153 |
+
if (field === 'rate_per_kg') {
|
| 154 |
+
updatedItem.item_total = (updatedItem.net_weight || 0) * Math.max(0, parseFloat(value || 0));
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
return updatedItem;
|
| 158 |
+
}));
|
| 159 |
+
};
|
| 160 |
+
|
| 161 |
+
const handlePotiInputChange = (id: string, value: string) => {
|
| 162 |
+
setPotiInputs(prev => ({ ...prev, [id]: value }));
|
| 163 |
+
setItems(prev => prev.map(item => {
|
| 164 |
+
if (item.id !== id) return item;
|
| 165 |
+
return calculateRow(item, value);
|
| 166 |
+
}));
|
| 167 |
+
};
|
| 168 |
+
|
| 169 |
+
// Handle mirchi type change - load available lots
|
| 170 |
+
const handleMirchiChange = async (id: string, mirchiTypeId: string) => {
|
| 171 |
+
handleItemChange(id, 'mirchi_type_id', mirchiTypeId);
|
| 172 |
+
|
| 173 |
+
// Load available lots for this mirchi type
|
| 174 |
+
if (mirchiTypeId) {
|
| 175 |
+
const lots = await getAvailableLotsByMirchi(mirchiTypeId);
|
| 176 |
+
setAvailableLots(prev => ({ ...prev, [id]: lots }));
|
| 177 |
+
}
|
| 178 |
+
};
|
| 179 |
+
|
| 180 |
+
// Handle LOT selection
|
| 181 |
+
const handleLotSelection = (id: string, lotId: string) => {
|
| 182 |
+
handleItemChange(id, 'lot_id', lotId);
|
| 183 |
+
|
| 184 |
+
// Find the selected lot to get its details
|
| 185 |
+
const lots = availableLots[id] || [];
|
| 186 |
+
const selectedLot = lots.find(lot => lot.id === lotId);
|
| 187 |
+
if (selectedLot) {
|
| 188 |
+
handleItemChange(id, 'lot_number', selectedLot.lot_number);
|
| 189 |
+
}
|
| 190 |
+
};
|
| 191 |
+
|
| 192 |
+
const addItem = () => {
|
| 193 |
+
setItems(prev => [...prev, {
|
| 194 |
+
id: Date.now().toString(),
|
| 195 |
+
poti_weights: [],
|
| 196 |
+
gross_weight: 0,
|
| 197 |
+
poti_count: 0,
|
| 198 |
+
total_potya: 0,
|
| 199 |
+
net_weight: 0,
|
| 200 |
+
rate_per_kg: 0,
|
| 201 |
+
item_total: 0
|
| 202 |
+
}]);
|
| 203 |
+
};
|
| 204 |
+
|
| 205 |
+
const removeItem = (id: string) => {
|
| 206 |
+
if (items.length > 1) {
|
| 207 |
+
setItems(prev => prev.filter(i => i.id !== id));
|
| 208 |
+
const newInputs = { ...potiInputs };
|
| 209 |
+
delete newInputs[id];
|
| 210 |
+
setPotiInputs(newInputs);
|
| 211 |
+
}
|
| 212 |
+
};
|
| 213 |
+
|
| 214 |
+
// Totals Calculation
|
| 215 |
+
const subtotal = items.reduce((acc, item) => acc + (item.item_total || 0), 0);
|
| 216 |
+
const totalPoti = items.reduce((acc, item) => acc + (item.poti_count || 0), 0);
|
| 217 |
+
|
| 218 |
+
// Derived Expenses - New sequence
|
| 219 |
+
const potiAmt = totalPoti * expenses.poti_rate;
|
| 220 |
+
const baseForCess = subtotal + potiAmt; // Cess calculated on subtotal + poti
|
| 221 |
+
const cessAmt = (baseForCess * expenses.cess_percent) / 100;
|
| 222 |
+
const adatAmt = (subtotal * expenses.adat_percent) / 100;
|
| 223 |
+
const hamaliAmt = totalPoti * expenses.hamali_per_poti;
|
| 224 |
+
const packagingHamaliAmt = totalPoti * expenses.packaging_hamali_per_poti;
|
| 225 |
+
const totalExp = potiAmt + cessAmt + adatAmt + hamaliAmt + packagingHamaliAmt + (expenses.gaadi_bharni || 0) + (expenses.other_expenses || 0);
|
| 226 |
+
const grandTotal = subtotal + totalExp;
|
| 227 |
+
|
| 228 |
+
// Calculate Payment Details based on Mode
|
| 229 |
+
let finalCash = 0;
|
| 230 |
+
let finalOnline = 0;
|
| 231 |
+
let currentPaid = 0;
|
| 232 |
+
|
| 233 |
+
if (paymentMode === 'cash') {
|
| 234 |
+
finalCash = grandTotal;
|
| 235 |
+
finalOnline = 0;
|
| 236 |
+
currentPaid = grandTotal;
|
| 237 |
+
} else if (paymentMode === 'online') {
|
| 238 |
+
finalCash = 0;
|
| 239 |
+
finalOnline = grandTotal;
|
| 240 |
+
currentPaid = grandTotal;
|
| 241 |
+
} else if (paymentMode === 'hybrid') {
|
| 242 |
+
finalCash = cashAmount;
|
| 243 |
+
finalOnline = Math.max(0, grandTotal - cashAmount);
|
| 244 |
+
currentPaid = grandTotal; // Hybrid assumes full payment
|
| 245 |
+
} else if (paymentMode === 'due') {
|
| 246 |
+
finalCash = 0;
|
| 247 |
+
finalOnline = onlineAmount; // User defined
|
| 248 |
+
currentPaid = onlineAmount;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
const balance = grandTotal - currentPaid;
|
| 252 |
+
|
| 253 |
+
// Validation Error
|
| 254 |
+
// Hybrid: Cash cannot exceed Total
|
| 255 |
+
// Due: Online cannot exceed Total
|
| 256 |
+
const isOverpaid = (paymentMode === 'hybrid' && cashAmount > grandTotal) ||
|
| 257 |
+
(paymentMode === 'due' && onlineAmount > grandTotal);
|
| 258 |
+
|
| 259 |
+
const validateForm = () => {
|
| 260 |
+
if (!selectedParty) {
|
| 261 |
+
alert('Please select a Party (पार्टी निवडा)');
|
| 262 |
+
return false;
|
| 263 |
+
}
|
| 264 |
+
for (let i = 0; i < items.length; i++) {
|
| 265 |
+
const item = items[i];
|
| 266 |
+
if (!item.mirchi_type_id) {
|
| 267 |
+
alert(`Row ${i + 1}: Please select Mirchi Type`);
|
| 268 |
+
return false;
|
| 269 |
+
}
|
| 270 |
+
if (!item.poti_weights || item.poti_weights.length === 0) {
|
| 271 |
+
alert(`Row ${i + 1}: Please enter weights (e.g. 10, 20)`);
|
| 272 |
+
return false;
|
| 273 |
+
}
|
| 274 |
+
if (!item.rate_per_kg || item.rate_per_kg <= 0) {
|
| 275 |
+
alert(`Row ${i + 1}: Rate must be greater than 0`);
|
| 276 |
+
return false;
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
if (isOverpaid) {
|
| 280 |
+
alert('Paid amount cannot be greater than Total Amount');
|
| 281 |
+
return false;
|
| 282 |
+
}
|
| 283 |
+
return true;
|
| 284 |
+
};
|
| 285 |
+
|
| 286 |
+
const handleSubmit = async () => {
|
| 287 |
+
if (isSubmitting) return;
|
| 288 |
+
if (!validateForm()) return;
|
| 289 |
+
|
| 290 |
+
setIsSubmitting(true);
|
| 291 |
+
|
| 292 |
+
// Populate mirchi_name for each item from mirchiTypes
|
| 293 |
+
const itemsWithNames = items.map(item => ({
|
| 294 |
+
...item,
|
| 295 |
+
mirchi_name: mirchiTypes.find(m => m.id === item.mirchi_type_id)?.name || 'Unknown'
|
| 296 |
+
}));
|
| 297 |
+
|
| 298 |
+
const transaction: Transaction = {
|
| 299 |
+
id: Date.now().toString(),
|
| 300 |
+
bill_number: billNumber,
|
| 301 |
+
bill_date: billDate,
|
| 302 |
+
bill_type: BillType.JAWAAK,
|
| 303 |
+
is_return: isReturnMode,
|
| 304 |
+
party_id: selectedParty,
|
| 305 |
+
party_name: parties.find(p => p.id === selectedParty)?.name,
|
| 306 |
+
items: itemsWithNames as TransactionItem[],
|
| 307 |
+
expenses: {
|
| 308 |
+
...expenses,
|
| 309 |
+
cess_amount: cessAmt,
|
| 310 |
+
adat_amount: adatAmt,
|
| 311 |
+
poti_amount: potiAmt,
|
| 312 |
+
hamali_amount: hamaliAmt,
|
| 313 |
+
packaging_hamali_amount: packagingHamaliAmt
|
| 314 |
+
},
|
| 315 |
+
payments: [
|
| 316 |
+
...(finalCash > 0 ? [{ mode: PaymentMode.CASH, amount: finalCash }] : []),
|
| 317 |
+
...(finalOnline > 0 ? [{ mode: PaymentMode.ONLINE, amount: finalOnline }] : []),
|
| 318 |
+
...(balance > 0 ? [{ mode: PaymentMode.DUE, amount: balance }] : []) // Optional: Track due as a payment entry or just balance? Usually balance is enough. Keeping logic simple.
|
| 319 |
+
],
|
| 320 |
+
gross_weight_total: items.reduce((a, i) => a + (i.gross_weight || 0), 0),
|
| 321 |
+
net_weight_total: items.reduce((a, i) => a + (i.net_weight || 0), 0),
|
| 322 |
+
subtotal: subtotal,
|
| 323 |
+
total_expenses: totalExp,
|
| 324 |
+
total_amount: grandTotal,
|
| 325 |
+
paid_amount: currentPaid,
|
| 326 |
+
balance_amount: balance
|
| 327 |
+
};
|
| 328 |
+
|
| 329 |
+
const result = await saveTransaction(transaction);
|
| 330 |
+
if (result.success) {
|
| 331 |
+
// Use the complete transaction object we created, not just the API response
|
| 332 |
+
// This ensures all items are included for printing
|
| 333 |
+
const completeTransaction = result.data ? {
|
| 334 |
+
...transaction,
|
| 335 |
+
...result.data,
|
| 336 |
+
items: transaction.items, // Ensure items are preserved
|
| 337 |
+
expenses: {
|
| 338 |
+
...result.data.expenses,
|
| 339 |
+
other_expenses: transaction.expenses.other_expenses,
|
| 340 |
+
},
|
| 341 |
+
} : transaction;
|
| 342 |
+
setSavedTransaction(completeTransaction);
|
| 343 |
+
alert('Bill Saved Successfully! You can now print the invoice.');
|
| 344 |
+
} else {
|
| 345 |
+
alert(`Error: ${result.message}`);
|
| 346 |
+
}
|
| 347 |
+
setIsSubmitting(false);
|
| 348 |
+
};
|
| 349 |
+
|
| 350 |
+
const handleHybridCashChange = (val: number) => {
|
| 351 |
+
setCashAmount(val);
|
| 352 |
+
// Online amount is derived in render, no state update needed for it in hybrid
|
| 353 |
+
};
|
| 354 |
+
|
| 355 |
+
const SummaryContent = () => (
|
| 356 |
+
<div className="space-y-3 text-sm">
|
| 357 |
+
<div className="flex justify-between">
|
| 358 |
+
<span className="text-gray-600">एकुण रक्कम (Subtotal)</span>
|
| 359 |
+
<span className="font-semibold">₹{subtotal.toFixed(2)}</span>
|
| 360 |
+
</div>
|
| 361 |
+
|
| 362 |
+
<div className="pt-2 border-t border-dashed space-y-2">
|
| 363 |
+
{/* 1. Poti (Bags) Rate */}
|
| 364 |
+
<SummaryInput
|
| 365 |
+
label={`पोती (Bags ${totalPoti}) Rate`}
|
| 366 |
+
value={expenses.poti_rate}
|
| 367 |
+
onChange={val => setExpenses({ ...expenses, poti_rate: val })}
|
| 368 |
+
/>
|
| 369 |
+
<div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
|
| 370 |
+
<span>Amount:</span>
|
| 371 |
+
<span>₹{potiAmt.toFixed(2)}</span>
|
| 372 |
+
</div>
|
| 373 |
+
|
| 374 |
+
{/* 2. Cess Tax (calculated on subtotal + poti) */}
|
| 375 |
+
<SummaryInput
|
| 376 |
+
label={`सेस (Cess ${expenses.cess_percent}%) on ₹${baseForCess.toFixed(2)}`}
|
| 377 |
+
value={expenses.cess_percent}
|
| 378 |
+
onChange={val => setExpenses({ ...expenses, cess_percent: val })}
|
| 379 |
+
/>
|
| 380 |
+
<div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
|
| 381 |
+
<span>Amount:</span>
|
| 382 |
+
<span>₹{cessAmt.toFixed(2)}</span>
|
| 383 |
+
</div>
|
| 384 |
+
|
| 385 |
+
{/* 3. Adat / Market Yard Tax */}
|
| 386 |
+
<SummaryInput
|
| 387 |
+
label={`अडत (Adat ${expenses.adat_percent}%)`}
|
| 388 |
+
value={expenses.adat_percent}
|
| 389 |
+
onChange={val => setExpenses({ ...expenses, adat_percent: val })}
|
| 390 |
+
/>
|
| 391 |
+
<div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
|
| 392 |
+
<span>Amount:</span>
|
| 393 |
+
<span>₹{adatAmt.toFixed(2)}</span>
|
| 394 |
+
</div>
|
| 395 |
+
|
| 396 |
+
{/* 4. Hamali */}
|
| 397 |
+
<SummaryInput
|
| 398 |
+
label={`हमाली (Hamali per poti)`}
|
| 399 |
+
value={expenses.hamali_per_poti}
|
| 400 |
+
onChange={val => setExpenses({ ...expenses, hamali_per_poti: val })}
|
| 401 |
+
/>
|
| 402 |
+
<div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
|
| 403 |
+
<span>Amount ({expenses.hamali_per_poti} * {totalPoti}):</span>
|
| 404 |
+
<span>₹{hamaliAmt.toFixed(2)}</span>
|
| 405 |
+
</div>
|
| 406 |
+
|
| 407 |
+
{/* 5. Packaging Hamali */}
|
| 408 |
+
<SummaryInput
|
| 409 |
+
label="पॅकेजिंग हमाली (Rate)"
|
| 410 |
+
value={expenses.packaging_hamali_per_poti}
|
| 411 |
+
onChange={val => setExpenses({ ...expenses, packaging_hamali_per_poti: val })}
|
| 412 |
+
/>
|
| 413 |
+
<div className="flex justify-between items-center text-xs text-gray-500 -mt-1 mb-2">
|
| 414 |
+
<span>Amount ({totalPoti} * {expenses.packaging_hamali_per_poti}):</span>
|
| 415 |
+
<span>₹{packagingHamaliAmt.toFixed(2)}</span>
|
| 416 |
+
</div>
|
| 417 |
+
|
| 418 |
+
{/* 6. Gaadi Bharni */}
|
| 419 |
+
<SummaryInput
|
| 420 |
+
label="गाडी भरणी"
|
| 421 |
+
value={expenses.gaadi_bharni}
|
| 422 |
+
onChange={val => setExpenses({ ...expenses, gaadi_bharni: val })}
|
| 423 |
+
/>
|
| 424 |
+
|
| 425 |
+
{/* 7. Other Expenses */}
|
| 426 |
+
<SummaryInput
|
| 427 |
+
label="Other Expenses"
|
| 428 |
+
value={expenses.other_expenses}
|
| 429 |
+
onChange={val => setExpenses({ ...expenses, other_expenses: val })}
|
| 430 |
+
/>
|
| 431 |
+
</div>
|
| 432 |
+
|
| 433 |
+
{/* 8. Total Price */}
|
| 434 |
+
<div className="pt-2 border-t border-gray-200 flex justify-between items-center">
|
| 435 |
+
<span className="text-base font-bold text-gray-800">एकुण बिल (Total)</span>
|
| 436 |
+
<span className="text-xl font-bold text-red-600">₹{grandTotal.toFixed(2)}</span>
|
| 437 |
+
</div>
|
| 438 |
+
|
| 439 |
+
{/* Payment Section */}
|
| 440 |
+
<div className="bg-teal-50 p-3 rounded-lg mt-2 border border-teal-100">
|
| 441 |
+
<label className="block text-xs font-medium text-teal-800 mb-2">Payment Mode</label>
|
| 442 |
+
<div className="flex gap-2 mb-3">
|
| 443 |
+
{['cash', 'online', 'hybrid', 'due'].map(mode => (
|
| 444 |
+
<button
|
| 445 |
+
key={mode}
|
| 446 |
+
onClick={() => {
|
| 447 |
+
setPaymentMode(mode as any);
|
| 448 |
+
setCashAmount(0);
|
| 449 |
+
setOnlineAmount(0);
|
| 450 |
+
}}
|
| 451 |
+
className={`flex-1 py-1 text-xs font-medium rounded border ${paymentMode === mode
|
| 452 |
+
? 'bg-teal-600 text-white border-teal-600'
|
| 453 |
+
: 'bg-white text-gray-600 border-gray-200'
|
| 454 |
+
} capitalize`}
|
| 455 |
+
>
|
| 456 |
+
{mode}
|
| 457 |
+
</button>
|
| 458 |
+
))}
|
| 459 |
+
</div>
|
| 460 |
+
|
| 461 |
+
{paymentMode === 'cash' && (
|
| 462 |
+
<div className="text-center py-2 text-sm text-gray-600">
|
| 463 |
+
Full Payment in Cash: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
|
| 464 |
+
</div>
|
| 465 |
+
)}
|
| 466 |
+
|
| 467 |
+
{paymentMode === 'online' && (
|
| 468 |
+
<div className="text-center py-2 text-sm text-gray-600">
|
| 469 |
+
Full Payment Online: <span className="font-bold text-gray-800">₹{grandTotal.toFixed(2)}</span>
|
| 470 |
+
</div>
|
| 471 |
+
)}
|
| 472 |
+
|
| 473 |
+
{paymentMode === 'hybrid' && (
|
| 474 |
+
<div className="space-y-2">
|
| 475 |
+
<div>
|
| 476 |
+
<label className="text-xs text-gray-600">Cash Amount</label>
|
| 477 |
+
<input
|
| 478 |
+
type="number"
|
| 479 |
+
min="0"
|
| 480 |
+
max={grandTotal}
|
| 481 |
+
step="0.01"
|
| 482 |
+
className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
|
| 483 |
+
value={cashAmount === 0 ? '' : cashAmount}
|
| 484 |
+
onChange={e => {
|
| 485 |
+
const val = parseFloat(e.target.value) || 0;
|
| 486 |
+
if (val >= 0 && val <= grandTotal) {
|
| 487 |
+
handleHybridCashChange(val);
|
| 488 |
+
}
|
| 489 |
+
}}
|
| 490 |
+
onKeyDown={e => {
|
| 491 |
+
// Prevent special characters: -, +, e, E
|
| 492 |
+
if (['-', '+', 'e', 'E'].includes(e.key)) {
|
| 493 |
+
e.preventDefault();
|
| 494 |
+
}
|
| 495 |
+
}}
|
| 496 |
+
placeholder="Cash"
|
| 497 |
+
/>
|
| 498 |
+
</div>
|
| 499 |
+
<div>
|
| 500 |
+
<label className="text-xs text-gray-600">Online Amount (Auto)</label>
|
| 501 |
+
<input
|
| 502 |
+
type="text"
|
| 503 |
+
readOnly
|
| 504 |
+
className="w-full bg-gray-100 border border-gray-200 rounded p-2 text-right font-medium text-gray-600"
|
| 505 |
+
value={(grandTotal - cashAmount).toFixed(2)}
|
| 506 |
+
/>
|
| 507 |
+
</div>
|
| 508 |
+
</div>
|
| 509 |
+
)}
|
| 510 |
+
|
| 511 |
+
{paymentMode === 'due' && (
|
| 512 |
+
<div className="space-y-2">
|
| 513 |
+
<div>
|
| 514 |
+
<label className="text-xs text-gray-600">Online Amount (Partial Payment)</label>
|
| 515 |
+
<input
|
| 516 |
+
type="number"
|
| 517 |
+
min="0"
|
| 518 |
+
max={grandTotal}
|
| 519 |
+
step="0.01"
|
| 520 |
+
className="w-full border border-teal-200 rounded p-2 text-right font-medium focus:ring-1 focus:ring-teal-500 outline-none"
|
| 521 |
+
value={onlineAmount === 0 ? '' : onlineAmount}
|
| 522 |
+
onChange={e => {
|
| 523 |
+
const val = parseFloat(e.target.value) || 0;
|
| 524 |
+
if (val >= 0 && val <= grandTotal) {
|
| 525 |
+
setOnlineAmount(val);
|
| 526 |
+
}
|
| 527 |
+
}}
|
| 528 |
+
onKeyDown={e => {
|
| 529 |
+
// Prevent special characters: -, +, e, E
|
| 530 |
+
if (['-', '+', 'e', 'E'].includes(e.key)) {
|
| 531 |
+
e.preventDefault();
|
| 532 |
+
}
|
| 533 |
+
}}
|
| 534 |
+
placeholder="Enter amount paid online (0 for full due)"
|
| 535 |
+
/>
|
| 536 |
+
</div>
|
| 537 |
+
<div>
|
| 538 |
+
<label className="text-xs text-gray-600">Due Amount (Auto)</label>
|
| 539 |
+
<input
|
| 540 |
+
type="text"
|
| 541 |
+
readOnly
|
| 542 |
+
className="w-full bg-red-50 border border-red-200 rounded p-2 text-right font-bold text-red-600"
|
| 543 |
+
value={(grandTotal - onlineAmount).toFixed(2)}
|
| 544 |
+
/>
|
| 545 |
+
</div>
|
| 546 |
+
</div>
|
| 547 |
+
)}
|
| 548 |
+
|
| 549 |
+
{isOverpaid && (
|
| 550 |
+
<div className="text-red-500 text-xs mt-2 font-medium text-center">
|
| 551 |
+
Error: Amount exceeds Total!
|
| 552 |
+
</div>
|
| 553 |
+
)}
|
| 554 |
+
</div>
|
| 555 |
+
|
| 556 |
+
<div className="flex justify-between items-center pt-2">
|
| 557 |
+
<span className="text-gray-600 font-medium">बाकी (Balance)</span>
|
| 558 |
+
<span className={`font-bold ${balance > 0 ? 'text-red-500' : 'text-green-600'}`}>₹{balance.toFixed(2)}</span>
|
| 559 |
+
</div>
|
| 560 |
+
</div>
|
| 561 |
+
);
|
| 562 |
+
|
| 563 |
+
return (
|
| 564 |
+
<div className="flex flex-col lg:flex-row gap-4 h-full p-4 bg-gray-50">
|
| 565 |
+
{isLoading && (
|
| 566 |
+
<div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-gray-100">
|
| 567 |
+
<div className="text-center">
|
| 568 |
+
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mb-4"></div>
|
| 569 |
+
<p className="text-gray-600">Loading data...</p>
|
| 570 |
+
</div>
|
| 571 |
+
</div>
|
| 572 |
+
)}
|
| 573 |
+
{error && !isLoading && (
|
| 574 |
+
<div className="flex-1 flex items-center justify-center bg-white rounded-xl shadow-sm border border-red-200">
|
| 575 |
+
<div className="text-center p-8">
|
| 576 |
+
<div className="text-red-500 text-5xl mb-4">⚠️</div>
|
| 577 |
+
<h3 className="text-lg font-semibold text-gray-800 mb-2">Error Loading Data</h3>
|
| 578 |
+
<p className="text-gray-600 mb-4">{error}</p>
|
| 579 |
+
<button onClick={() => window.location.reload()} className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700">
|
| 580 |
+
Retry
|
| 581 |
+
</button>
|
| 582 |
+
</div>
|
| 583 |
+
</div>
|
| 584 |
+
)}
|
| 585 |
+
{!isLoading && !error && (
|
| 586 |
+
<>
|
| 587 |
+
{/* Left: Form */}
|
| 588 |
+
<div className={`flex-1 bg-white rounded-xl shadow-sm border p-4 lg:p-6 lg:overflow-y-auto lg:h-full no-scrollbar pb-40 lg:pb-6 ${isReturnMode ? 'border-red-200' : 'border-gray-100'}`}>
|
| 589 |
+
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
|
| 590 |
+
<h2 className={`text-lg font-bold flex items-center gap-2 ${isReturnMode ? 'text-red-600' : 'text-gray-800'}`}>
|
| 591 |
+
{isReturnMode ? <RotateCcw className="text-red-500" /> : <div className="text-teal-600 font-bold">OUT</div>}
|
| 592 |
+
{isReturnMode ? 'जावक परतावा (Sales Return)' : 'जावक बिल (Sales)'}
|
| 593 |
+
</h2>
|
| 594 |
+
|
| 595 |
+
<div className="flex items-center gap-3">
|
| 596 |
+
<div className="flex bg-gray-100 rounded-lg p-1">
|
| 597 |
+
<button
|
| 598 |
+
onClick={() => setIsReturnMode(false)}
|
| 599 |
+
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${!isReturnMode ? 'bg-white text-teal-700 shadow-sm' : 'text-gray-500'}`}
|
| 600 |
+
>
|
| 601 |
+
Regular
|
| 602 |
+
</button>
|
| 603 |
+
<button
|
| 604 |
+
onClick={() => setIsReturnMode(true)}
|
| 605 |
+
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${isReturnMode ? 'bg-red-500 text-white shadow-sm' : 'text-gray-500'}`}
|
| 606 |
+
>
|
| 607 |
+
Return
|
| 608 |
+
</button>
|
| 609 |
+
</div>
|
| 610 |
+
|
| 611 |
+
<input
|
| 612 |
+
type="date"
|
| 613 |
+
className="bg-gray-100 px-3 py-1 rounded text-sm font-mono text-gray-600 border border-gray-200 hover:border-blue-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none cursor-pointer transition-colors"
|
| 614 |
+
value={billDate}
|
| 615 |
+
onChange={(e) => setBillDate(e.target.value)}
|
| 616 |
+
max={new Date().toISOString().split('T')[0]}
|
| 617 |
+
/>
|
| 618 |
+
</div>
|
| 619 |
+
</div>
|
| 620 |
+
|
| 621 |
+
{/* Header Fields */}
|
| 622 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
| 623 |
+
|
| 624 |
+
<div>
|
| 625 |
+
<label className="block text-sm font-medium text-gray-700 mb-1">पार्टी नाव (Party)</label>
|
| 626 |
+
<select
|
| 627 |
+
className="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-teal-500 outline-none"
|
| 628 |
+
value={selectedParty}
|
| 629 |
+
onChange={e => setSelectedParty(e.target.value)}
|
| 630 |
+
>
|
| 631 |
+
<option value="">Select Party</option>
|
| 632 |
+
{parties.map(p => (
|
| 633 |
+
<option key={p.id} value={p.id}>{p.name} - {p.city}</option>
|
| 634 |
+
))}
|
| 635 |
+
</select>
|
| 636 |
+
</div>
|
| 637 |
+
</div>
|
| 638 |
+
|
| 639 |
+
{/* Items Table */}
|
| 640 |
+
<div className="mb-6">
|
| 641 |
+
<div className="flex justify-between items-center mb-2">
|
| 642 |
+
<h3 className="font-semibold text-gray-700">माल तपशील (Items)</h3>
|
| 643 |
+
</div>
|
| 644 |
+
|
| 645 |
+
<div className="space-y-4">
|
| 646 |
+
{items.map((item, index) => (
|
| 647 |
+
<div key={item.id} className="p-4 border rounded-lg bg-gray-50 relative shadow-sm">
|
| 648 |
+
<button
|
| 649 |
+
onClick={() => removeItem(item.id!)}
|
| 650 |
+
className="absolute top-2 right-2 text-red-400 hover:text-red-600 p-1"
|
| 651 |
+
>
|
| 652 |
+
<Trash2 size={18} />
|
| 653 |
+
</button>
|
| 654 |
+
|
| 655 |
+
<div className="grid grid-cols-2 md:grid-cols-6 gap-3">
|
| 656 |
+
<div className="col-span-2">
|
| 657 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">मिरची जात (Type)</label>
|
| 658 |
+
<select
|
| 659 |
+
className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
|
| 660 |
+
value={item.mirchi_type_id || ''}
|
| 661 |
+
onChange={e => handleMirchiChange(item.id!, e.target.value)}
|
| 662 |
+
>
|
| 663 |
+
<option value="">Select Type</option>
|
| 664 |
+
{mirchiTypes.map(m => (
|
| 665 |
+
<option key={m.id} value={m.id}>{m.name}</option>
|
| 666 |
+
))}
|
| 667 |
+
</select>
|
| 668 |
+
</div>
|
| 669 |
+
|
| 670 |
+
{/* LOT Selection */}
|
| 671 |
+
{item.mirchi_type_id && (
|
| 672 |
+
<div className="col-span-2">
|
| 673 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">LOT Number</label>
|
| 674 |
+
<select
|
| 675 |
+
className="w-full border border-gray-300 rounded p-2 text-sm bg-white font-mono"
|
| 676 |
+
value={item.lot_id || ''}
|
| 677 |
+
onChange={e => handleLotSelection(item.id!, e.target.value)}
|
| 678 |
+
>
|
| 679 |
+
<option value="">Select LOT</option>
|
| 680 |
+
{(availableLots[item.id!] || []).map(lot => (
|
| 681 |
+
<option key={lot.id} value={lot.id}>
|
| 682 |
+
{lot.lot_number} ({lot.remaining_quantity}kg available)
|
| 683 |
+
</option>
|
| 684 |
+
))}
|
| 685 |
+
</select>
|
| 686 |
+
</div>
|
| 687 |
+
)}
|
| 688 |
+
|
| 689 |
+
<div className={item.mirchi_type_id ? "col-span-2" : "col-span-2 md:col-span-4"}>
|
| 690 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">पोटी वजन (Eg: 10,20,30)</label>
|
| 691 |
+
<input
|
| 692 |
+
type="text"
|
| 693 |
+
placeholder="10, 20, 30"
|
| 694 |
+
className="w-full border border-gray-300 rounded p-2 text-sm bg-white"
|
| 695 |
+
value={potiInputs[item.id!] || ''}
|
| 696 |
+
onChange={e => handlePotiInputChange(item.id!, e.target.value)}
|
| 697 |
+
/>
|
| 698 |
+
</div>
|
| 699 |
+
|
| 700 |
+
<div>
|
| 701 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">Gross</label>
|
| 702 |
+
<input type="number" readOnly className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm" value={item.gross_weight} />
|
| 703 |
+
</div>
|
| 704 |
+
<div>
|
| 705 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">Poti (count)</label>
|
| 706 |
+
<input type="number" readOnly className="w-full bg-gray-100 border border-gray-300 rounded p-2 text-sm text-red-500" value={item.total_potya} />
|
| 707 |
+
</div>
|
| 708 |
+
<div>
|
| 709 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">Net</label>
|
| 710 |
+
<input type="number" readOnly className="w-full bg-blue-50 border border-blue-200 rounded p-2 text-sm font-bold text-blue-800" value={item.net_weight} />
|
| 711 |
+
</div>
|
| 712 |
+
<div>
|
| 713 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">Rate (₹)</label>
|
| 714 |
+
<input
|
| 715 |
+
type="number"
|
| 716 |
+
min="0"
|
| 717 |
+
className="w-full border border-gray-300 rounded p-2 text-sm bg-white appearance-none"
|
| 718 |
+
onWheel={e => e.currentTarget.blur()}
|
| 719 |
+
value={item.rate_per_kg === 0 ? '' : item.rate_per_kg}
|
| 720 |
+
onChange={e => handleItemChange(item.id!, 'rate_per_kg', Math.max(0, parseFloat(e.target.value) || 0))}
|
| 721 |
+
placeholder="0"
|
| 722 |
+
/>
|
| 723 |
+
</div>
|
| 724 |
+
<div className="col-span-2 md:col-span-2">
|
| 725 |
+
<label className="block text-xs font-medium text-gray-500 mb-1">Total (₹)</label>
|
| 726 |
+
<input type="number" readOnly className="w-full bg-green-50 border border-green-200 rounded p-2 text-sm font-bold text-right text-green-800" value={item.item_total?.toFixed(2)} />
|
| 727 |
+
</div>
|
| 728 |
+
</div>
|
| 729 |
+
</div>
|
| 730 |
+
))}
|
| 731 |
+
</div>
|
| 732 |
+
<button
|
| 733 |
+
onClick={addItem}
|
| 734 |
+
className="w-full mt-4 flex justify-center items-center gap-2 text-teal-600 font-medium bg-teal-50 hover:bg-teal-100 py-3 rounded-lg border border-teal-100 transition"
|
| 735 |
+
>
|
| 736 |
+
<Plus size={18} /> Add New Item (नवीन माल)
|
| 737 |
+
</button>
|
| 738 |
+
</div>
|
| 739 |
+
</div>
|
| 740 |
+
|
| 741 |
+
{/* Desktop: Right Summary Sidebar */}
|
| 742 |
+
<div className="hidden lg:flex w-80 flex-col gap-4">
|
| 743 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-0">
|
| 744 |
+
<h3 className="font-bold text-gray-800 mb-4 border-b pb-2">बिल सारांश (Summary)</h3>
|
| 745 |
+
{SummaryContent()}
|
| 746 |
+
<div className="flex gap-2 mt-6">
|
| 747 |
+
{savedTransaction && (
|
| 748 |
+
<>
|
| 749 |
+
<PrintInvoice transaction={savedTransaction} />
|
| 750 |
+
<PdfInvoice transaction={savedTransaction} />
|
| 751 |
+
</>
|
| 752 |
+
)}
|
| 753 |
+
<button
|
| 754 |
+
onClick={handleSubmit}
|
| 755 |
+
disabled={isSubmitting || items.length === 0}
|
| 756 |
+
className={`flex-1 bg-teal-600 text-white py-3 rounded-lg font-bold shadow-lg hover:bg-teal-700 transition-all flex items-center justify-center gap-2 ${isSubmitting || items.length === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
|
| 757 |
+
>
|
| 758 |
+
<Save size={20} />
|
| 759 |
+
{isSubmitting ? 'Saving...' : 'Save Bill'}
|
| 760 |
+
</button>
|
| 761 |
+
</div>
|
| 762 |
+
</div>
|
| 763 |
+
</div>
|
| 764 |
+
|
| 765 |
+
{/* Mobile: Sticky Bottom Action Bar */}
|
| 766 |
+
<div className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-[0_-4px_10px_rgba(0,0,0,0.1)] z-20">
|
| 767 |
+
<div className="flex items-center justify-between p-4 bg-white z-20 relative">
|
| 768 |
+
<div
|
| 769 |
+
onClick={() => setShowMobileSummary(!showMobileSummary)}
|
| 770 |
+
className="flex flex-col cursor-pointer"
|
| 771 |
+
>
|
| 772 |
+
<div className="flex items-center gap-1 text-gray-500 text-xs font-medium uppercase tracking-wide">
|
| 773 |
+
Summary {showMobileSummary ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
|
| 774 |
+
</div>
|
| 775 |
+
<div className="text-xl font-bold text-red-600">
|
| 776 |
+
₹{grandTotal.toFixed(2)}
|
| 777 |
+
</div>
|
| 778 |
+
</div>
|
| 779 |
+
<button
|
| 780 |
+
onClick={handleSubmit}
|
| 781 |
+
disabled={isSubmitting}
|
| 782 |
+
className="bg-teal-600 text-white px-6 py-2.5 rounded-lg font-semibold flex items-center gap-2 shadow-sm active:bg-teal-700 disabled:opacity-50"
|
| 783 |
+
>
|
| 784 |
+
<Save size={18} /> {isSubmitting ? '...' : 'Save'}
|
| 785 |
+
</button>
|
| 786 |
+
</div>
|
| 787 |
+
|
| 788 |
+
{showMobileSummary && (
|
| 789 |
+
<div className="px-6 pb-6 pt-0 bg-white border-t border-dashed border-gray-200 max-h-[60vh] overflow-y-auto animate-in slide-in-from-bottom-2">
|
| 790 |
+
<div className="mt-4">
|
| 791 |
+
{SummaryContent()}
|
| 792 |
+
</div>
|
| 793 |
+
{savedTransaction && (
|
| 794 |
+
<div className="mt-4 flex justify-center">
|
| 795 |
+
<PrintInvoice transaction={savedTransaction} />
|
| 796 |
+
</div>
|
| 797 |
+
)}
|
| 798 |
+
</div>
|
| 799 |
+
)}
|
| 800 |
+
</div>
|
| 801 |
+
</>
|
| 802 |
+
)}
|
| 803 |
+
</div>
|
| 804 |
+
);
|
| 805 |
+
};
|
| 806 |
+
|
| 807 |
+
export default JawaakBill;
|
pages/PartyLedger.tsx
ADDED
|
@@ -0,0 +1,864 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState, useMemo } from 'react';
|
| 2 |
+
import { getParties, getTransactions, updateTransactionPayment } from '../services/db';
|
| 3 |
+
import { Party, Transaction, PartyType, BillType } from '../types';
|
| 4 |
+
import { Users, Filter, Search, ArrowLeft, Eye, Edit, Save, X, Printer, Download, Calendar, ArrowUpDown, FileText } from 'lucide-react';
|
| 5 |
+
import PrintInvoice from '../components/PrintInvoice.tsx';
|
| 6 |
+
import PdfInvoice from '../components/PdfInvoice.tsx';
|
| 7 |
+
import { exportPartyLedger } from '../utils/exportToExcel';
|
| 8 |
+
import { generateLedgerPDF } from '../utils/LedgerPdfGenerator'; // Import the new utility
|
| 9 |
+
|
| 10 |
+
const PartyLedger = () => {
|
| 11 |
+
const [parties, setParties] = useState<Party[]>([]);
|
| 12 |
+
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
| 13 |
+
const [activeTab, setActiveTab] = useState<'awaak' | 'jawaak'>('awaak');
|
| 14 |
+
const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
|
| 15 |
+
const [selectedParty, setSelectedParty] = useState<Party | null>(null);
|
| 16 |
+
const [searchQuery, setSearchQuery] = useState('');
|
| 17 |
+
|
| 18 |
+
// Update Payment State
|
| 19 |
+
const [editingTxId, setEditingTxId] = useState<string | null>(null);
|
| 20 |
+
const [paymentAmount, setPaymentAmount] = useState<string>('');
|
| 21 |
+
|
| 22 |
+
// Multi-select for combined invoice
|
| 23 |
+
const [selectedTransactions, setSelectedTransactions] = useState<string[]>([]);
|
| 24 |
+
|
| 25 |
+
// NEW: Detail view filter state
|
| 26 |
+
const [detailSearch, setDetailSearch] = useState('');
|
| 27 |
+
const [fromDate, setFromDate] = useState('');
|
| 28 |
+
const [toDate, setToDate] = useState('');
|
| 29 |
+
const [sortOrder, setSortOrder] = useState<'newest' | 'oldest'>('newest');
|
| 30 |
+
|
| 31 |
+
const loadData = async () => {
|
| 32 |
+
setParties(await getParties());
|
| 33 |
+
setTransactions(await getTransactions());
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
useEffect(() => {
|
| 37 |
+
loadData();
|
| 38 |
+
}, []);
|
| 39 |
+
|
| 40 |
+
// Filter Parties based on Tab and Search
|
| 41 |
+
const filteredParties = parties.filter(p => {
|
| 42 |
+
const matchesTab = activeTab === 'awaak'
|
| 43 |
+
? (p.party_type === PartyType.AWAAK || p.party_type === PartyType.BOTH)
|
| 44 |
+
: (p.party_type === PartyType.JAWAAK || p.party_type === PartyType.BOTH);
|
| 45 |
+
|
| 46 |
+
const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
| 47 |
+
p.phone.includes(searchQuery) ||
|
| 48 |
+
p.city.toLowerCase().includes(searchQuery.toLowerCase());
|
| 49 |
+
|
| 50 |
+
return matchesTab && matchesSearch;
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
// Get Transactions for Selected Party (base list)
|
| 54 |
+
const partyTransactions = selectedParty
|
| 55 |
+
? transactions.filter(t => t.party_id === selectedParty.id)
|
| 56 |
+
: [];
|
| 57 |
+
|
| 58 |
+
// NEW: Memoized filtered and sorted party transactions
|
| 59 |
+
const filteredPartyTransactions = useMemo(() => {
|
| 60 |
+
let filtered = [...partyTransactions];
|
| 61 |
+
|
| 62 |
+
// Apply search filter (dynamic - searches everything)
|
| 63 |
+
if (detailSearch.trim()) {
|
| 64 |
+
const query = detailSearch.toLowerCase().trim();
|
| 65 |
+
filtered = filtered.filter(tx => {
|
| 66 |
+
// Search in bill number
|
| 67 |
+
if (tx.bill_number?.toLowerCase().includes(query)) return true;
|
| 68 |
+
|
| 69 |
+
// Search in date
|
| 70 |
+
const txDate = tx.bill_date?.split('T')[0] || tx.bill_date || '';
|
| 71 |
+
if (txDate.includes(query)) return true;
|
| 72 |
+
|
| 73 |
+
// Search in items (mirchi name, lot number)
|
| 74 |
+
const itemsMatch = tx.items?.some(item =>
|
| 75 |
+
item.mirchi_name?.toLowerCase().includes(query) ||
|
| 76 |
+
item.lot_number?.toLowerCase().includes(query)
|
| 77 |
+
);
|
| 78 |
+
if (itemsMatch) return true;
|
| 79 |
+
|
| 80 |
+
// Search in amounts (as string)
|
| 81 |
+
if (tx.total_amount?.toString().includes(query)) return true;
|
| 82 |
+
if (tx.paid_amount?.toString().includes(query)) return true;
|
| 83 |
+
if (tx.balance_amount?.toString().includes(query)) return true;
|
| 84 |
+
|
| 85 |
+
// Search for "return" or "returned"
|
| 86 |
+
if (tx.is_return && ('returned'.includes(query) || 'return'.includes(query))) return true;
|
| 87 |
+
|
| 88 |
+
// Search for "paid" status
|
| 89 |
+
if (tx.balance_amount === 0 && 'paid'.includes(query)) return true;
|
| 90 |
+
|
| 91 |
+
// Search for "due" or "pending"
|
| 92 |
+
if (tx.balance_amount > 0 && ('due'.includes(query) || 'pending'.includes(query))) return true;
|
| 93 |
+
|
| 94 |
+
return false;
|
| 95 |
+
});
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// Apply date range filter
|
| 99 |
+
if (fromDate) {
|
| 100 |
+
filtered = filtered.filter(tx => {
|
| 101 |
+
const txDate = tx.bill_date?.split('T')[0] || tx.bill_date || '';
|
| 102 |
+
return txDate >= fromDate;
|
| 103 |
+
});
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
if (toDate) {
|
| 107 |
+
filtered = filtered.filter(tx => {
|
| 108 |
+
const txDate = tx.bill_date?.split('T')[0] || tx.bill_date || '';
|
| 109 |
+
return txDate <= toDate;
|
| 110 |
+
});
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// Apply sort
|
| 114 |
+
filtered.sort((a, b) => {
|
| 115 |
+
const dateA = new Date(a.bill_date || 0).getTime();
|
| 116 |
+
const dateB = new Date(b.bill_date || 0).getTime();
|
| 117 |
+
return sortOrder === 'newest' ? dateB - dateA : dateA - dateB;
|
| 118 |
+
});
|
| 119 |
+
|
| 120 |
+
return filtered;
|
| 121 |
+
}, [partyTransactions, detailSearch, fromDate, toDate, sortOrder]);
|
| 122 |
+
|
| 123 |
+
// Memoize combined transaction for PDF generation
|
| 124 |
+
const combinedTransaction = useMemo(() => {
|
| 125 |
+
if (!selectedParty || selectedTransactions.length === 0) return null;
|
| 126 |
+
|
| 127 |
+
const selected = filteredPartyTransactions.filter(t => selectedTransactions.includes(t.id));
|
| 128 |
+
if (selected.length === 0) return null;
|
| 129 |
+
|
| 130 |
+
return {
|
| 131 |
+
...selected[0], // Use first transaction as base
|
| 132 |
+
id: `combined-${selectedParty.id}-${Date.now()}`, // Unique ID for combined
|
| 133 |
+
bill_number: `COMBINED-${selected.length}-BILLS`,
|
| 134 |
+
items: selected.flatMap(t => t.items),
|
| 135 |
+
subtotal: selected.reduce((sum, t) => sum + (t.subtotal || 0), 0),
|
| 136 |
+
total_expenses: selected.reduce((sum, t) => sum + (t.total_expenses || 0), 0),
|
| 137 |
+
total_amount: selected.reduce((sum, t) => sum + t.total_amount, 0),
|
| 138 |
+
paid_amount: selected.reduce((sum, t) => sum + t.paid_amount, 0),
|
| 139 |
+
balance_amount: selected.reduce((sum, t) => sum + t.balance_amount, 0),
|
| 140 |
+
gross_weight_total: selected.reduce((sum, t) => sum + (t.gross_weight_total || 0), 0),
|
| 141 |
+
net_weight_total: selected.reduce((sum, t) => sum + (t.net_weight_total || 0), 0),
|
| 142 |
+
};
|
| 143 |
+
}, [selectedTransactions, filteredPartyTransactions, selectedParty]);
|
| 144 |
+
|
| 145 |
+
// Calculate Totals for List View
|
| 146 |
+
const getPartyStats = (partyId: string) => {
|
| 147 |
+
const txs = transactions.filter(t => t.party_id === partyId);
|
| 148 |
+
|
| 149 |
+
// Calculate totals - returns should subtract from totals
|
| 150 |
+
const totalBill = txs.reduce((sum, t) => {
|
| 151 |
+
if (t.is_return) {
|
| 152 |
+
return sum - t.total_amount; // Subtract returns
|
| 153 |
+
}
|
| 154 |
+
return sum + t.total_amount; // Add normal transactions
|
| 155 |
+
}, 0);
|
| 156 |
+
|
| 157 |
+
const totalPaid = txs.reduce((sum, t) => {
|
| 158 |
+
if (t.is_return) {
|
| 159 |
+
return sum - t.paid_amount; // Subtract returns
|
| 160 |
+
}
|
| 161 |
+
return sum + t.paid_amount; // Add normal transactions
|
| 162 |
+
}, 0);
|
| 163 |
+
|
| 164 |
+
// Balance is directly from party object for accuracy
|
| 165 |
+
const party = parties.find(p => p.id === partyId);
|
| 166 |
+
return {
|
| 167 |
+
totalBill,
|
| 168 |
+
totalPaid,
|
| 169 |
+
balance: party?.current_balance || 0
|
| 170 |
+
};
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
const handleUpdatePayment = async (tx: Transaction) => {
|
| 174 |
+
const amount = parseFloat(paymentAmount);
|
| 175 |
+
if (isNaN(amount) || amount <= 0) {
|
| 176 |
+
alert('Please enter a valid amount');
|
| 177 |
+
return;
|
| 178 |
+
}
|
| 179 |
+
if (amount > tx.balance_amount) {
|
| 180 |
+
alert('Amount cannot exceed due balance');
|
| 181 |
+
return;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
const res = await updateTransactionPayment(tx.id, amount);
|
| 185 |
+
if (res.success) {
|
| 186 |
+
// Update local state for demo/sample data
|
| 187 |
+
setTransactions((prev) =>
|
| 188 |
+
prev.map((t) =>
|
| 189 |
+
t.id === tx.id
|
| 190 |
+
? {
|
| 191 |
+
...t,
|
| 192 |
+
paid_amount: t.paid_amount + amount,
|
| 193 |
+
balance_amount: t.balance_amount - amount,
|
| 194 |
+
}
|
| 195 |
+
: t,
|
| 196 |
+
),
|
| 197 |
+
);
|
| 198 |
+
setEditingTxId(null);
|
| 199 |
+
setPaymentAmount('');
|
| 200 |
+
// Reload data to refresh party balances
|
| 201 |
+
loadData();
|
| 202 |
+
} else {
|
| 203 |
+
alert(res.message);
|
| 204 |
+
}
|
| 205 |
+
};
|
| 206 |
+
|
| 207 |
+
// NEW: Reset detail filters when going back to list
|
| 208 |
+
const handleBackToList = () => {
|
| 209 |
+
setViewMode('list');
|
| 210 |
+
setSelectedParty(null);
|
| 211 |
+
setSelectedTransactions([]);
|
| 212 |
+
setDetailSearch('');
|
| 213 |
+
setFromDate('');
|
| 214 |
+
setToDate('');
|
| 215 |
+
setSortOrder('newest');
|
| 216 |
+
};
|
| 217 |
+
|
| 218 |
+
// NEW: Clear all detail filters
|
| 219 |
+
const clearDetailFilters = () => {
|
| 220 |
+
setDetailSearch('');
|
| 221 |
+
setFromDate('');
|
| 222 |
+
setToDate('');
|
| 223 |
+
setSortOrder('newest');
|
| 224 |
+
};
|
| 225 |
+
|
| 226 |
+
// NEW: Check if any filter is active
|
| 227 |
+
const hasActiveFilters = detailSearch || fromDate || toDate || sortOrder !== 'newest';
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
return (
|
| 231 |
+
<div className="space-y-6">
|
| 232 |
+
{/* Header & Controls */}
|
| 233 |
+
<div className="flex flex-col md:flex-row justify-between items-center bg-white p-4 rounded-xl border border-gray-100 shadow-sm gap-4">
|
| 234 |
+
{viewMode === 'detail' && selectedParty ? (
|
| 235 |
+
<>
|
| 236 |
+
{/* Header with Party Info */}
|
| 237 |
+
<div className="flex items-center justify-between w-full">
|
| 238 |
+
<div className="flex items-center gap-4">
|
| 239 |
+
<button
|
| 240 |
+
onClick={handleBackToList}
|
| 241 |
+
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
| 242 |
+
>
|
| 243 |
+
<ArrowLeft size={20} className="text-gray-600" />
|
| 244 |
+
</button>
|
| 245 |
+
<div>
|
| 246 |
+
<h2 className="text-2xl font-bold text-gray-800">{selectedParty.name}</h2>
|
| 247 |
+
<p className="text-sm text-gray-500">{selectedParty.city} • {selectedParty.phone}</p>
|
| 248 |
+
</div>
|
| 249 |
+
</div>
|
| 250 |
+
|
| 251 |
+
<div className="flex items-center gap-2">
|
| 252 |
+
{selectedTransactions.length > 0 && combinedTransaction && (
|
| 253 |
+
<PrintInvoice
|
| 254 |
+
transaction={combinedTransaction}
|
| 255 |
+
className="px-2 sm:px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-1 sm:gap-2"
|
| 256 |
+
>
|
| 257 |
+
<Printer size={18} className="shrink-0" />
|
| 258 |
+
<span className="hidden sm:inline">Print {selectedTransactions.length} Selected</span>
|
| 259 |
+
</PrintInvoice>
|
| 260 |
+
)}
|
| 261 |
+
|
| 262 |
+
{/* --- NEW RED PDF BUTTON --- */}
|
| 263 |
+
<button
|
| 264 |
+
onClick={() => generateLedgerPDF(selectedParty, filteredPartyTransactions)}
|
| 265 |
+
className="px-2 sm:px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center gap-1 sm:gap-2 shadow-sm"
|
| 266 |
+
title="Download PDF Ledger"
|
| 267 |
+
>
|
| 268 |
+
<FileText size={18} className="shrink-0" />
|
| 269 |
+
<span className="hidden sm:inline">PDF Ledger</span>
|
| 270 |
+
</button>
|
| 271 |
+
{/* ------------------------- */}
|
| 272 |
+
|
| 273 |
+
<button
|
| 274 |
+
onClick={() => exportPartyLedger(selectedParty, filteredPartyTransactions)}
|
| 275 |
+
className="px-2 sm:px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-1 sm:gap-2"
|
| 276 |
+
title="Export to Excel"
|
| 277 |
+
>
|
| 278 |
+
<Download size={18} className="shrink-0" />
|
| 279 |
+
<span className="hidden sm:inline">Export to Excel</span>
|
| 280 |
+
</button>
|
| 281 |
+
</div>
|
| 282 |
+
</div>
|
| 283 |
+
</>
|
| 284 |
+
) : (
|
| 285 |
+
<div className="flex items-center gap-2">
|
| 286 |
+
<Users className="text-teal-600" />
|
| 287 |
+
<h2 className="text-lg font-bold">
|
| 288 |
+
पार्टी लेजर (Party Ledger)
|
| 289 |
+
</h2>
|
| 290 |
+
</div>
|
| 291 |
+
)}
|
| 292 |
+
|
| 293 |
+
{viewMode === 'list' && (
|
| 294 |
+
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
|
| 295 |
+
{/* Tabs */}
|
| 296 |
+
<div className="flex bg-gray-100 p-1 rounded-lg">
|
| 297 |
+
<button
|
| 298 |
+
onClick={() => setActiveTab('awaak')}
|
| 299 |
+
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${activeTab === 'awaak' ? 'bg-white text-teal-700 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
|
| 300 |
+
>
|
| 301 |
+
Awaak (Purchase)
|
| 302 |
+
</button>
|
| 303 |
+
<button
|
| 304 |
+
onClick={() => setActiveTab('jawaak')}
|
| 305 |
+
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${activeTab === 'jawaak' ? 'bg-white text-blue-700 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
|
| 306 |
+
>
|
| 307 |
+
Jawaak (Sales)
|
| 308 |
+
</button>
|
| 309 |
+
</div>
|
| 310 |
+
|
| 311 |
+
{/* Search */}
|
| 312 |
+
<div className="relative w-full md:w-auto mt-2 md:mt-0">
|
| 313 |
+
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
| 314 |
+
<input
|
| 315 |
+
type="text"
|
| 316 |
+
placeholder="Search Party..."
|
| 317 |
+
className="pl-10 pr-4 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500 outline-none w-full md:w-64"
|
| 318 |
+
value={searchQuery}
|
| 319 |
+
onChange={e => setSearchQuery(e.target.value)}
|
| 320 |
+
/>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
)}
|
| 324 |
+
</div>
|
| 325 |
+
|
| 326 |
+
{/* NEW: Detail View Filters */}
|
| 327 |
+
{viewMode === 'detail' && selectedParty && (
|
| 328 |
+
<div className="bg-white p-4 rounded-xl border border-gray-100 shadow-sm">
|
| 329 |
+
<div className="flex flex-col lg:flex-row gap-4 items-start lg:items-center justify-between">
|
| 330 |
+
{/* Search & Filters Row */}
|
| 331 |
+
<div className="flex flex-col sm:flex-row gap-3 w-full lg:w-auto">
|
| 332 |
+
{/* Dynamic Search */}
|
| 333 |
+
<div className="relative flex-1 sm:flex-none">
|
| 334 |
+
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
| 335 |
+
<input
|
| 336 |
+
type="text"
|
| 337 |
+
placeholder="Search bill, date, mirchi, amount..."
|
| 338 |
+
className="pl-10 pr-4 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500 outline-none w-full sm:w-72"
|
| 339 |
+
value={detailSearch}
|
| 340 |
+
onChange={e => setDetailSearch(e.target.value)}
|
| 341 |
+
/>
|
| 342 |
+
</div>
|
| 343 |
+
|
| 344 |
+
{/* Date Range */}
|
| 345 |
+
<div className="flex items-center gap-2">
|
| 346 |
+
<div className="relative">
|
| 347 |
+
<Calendar size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
| 348 |
+
<input
|
| 349 |
+
type="date"
|
| 350 |
+
className="pl-9 pr-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500 outline-none"
|
| 351 |
+
value={fromDate}
|
| 352 |
+
onChange={e => setFromDate(e.target.value)}
|
| 353 |
+
title="From Date"
|
| 354 |
+
/>
|
| 355 |
+
</div>
|
| 356 |
+
<span className="text-gray-400 text-sm">to</span>
|
| 357 |
+
<div className="relative">
|
| 358 |
+
<Calendar size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
| 359 |
+
<input
|
| 360 |
+
type="date"
|
| 361 |
+
className="pl-9 pr-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500 outline-none"
|
| 362 |
+
value={toDate}
|
| 363 |
+
onChange={e => setToDate(e.target.value)}
|
| 364 |
+
title="To Date"
|
| 365 |
+
/>
|
| 366 |
+
</div>
|
| 367 |
+
</div>
|
| 368 |
+
|
| 369 |
+
{/* Sort Order */}
|
| 370 |
+
<div className="relative">
|
| 371 |
+
<ArrowUpDown size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
| 372 |
+
<select
|
| 373 |
+
className="pl-9 pr-8 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500 outline-none appearance-none bg-white cursor-pointer"
|
| 374 |
+
value={sortOrder}
|
| 375 |
+
onChange={e => setSortOrder(e.target.value as 'newest' | 'oldest')}
|
| 376 |
+
>
|
| 377 |
+
<option value="newest">Newest First</option>
|
| 378 |
+
<option value="oldest">Oldest First</option>
|
| 379 |
+
</select>
|
| 380 |
+
</div>
|
| 381 |
+
</div>
|
| 382 |
+
|
| 383 |
+
{/* Results Count & Clear */}
|
| 384 |
+
<div className="flex items-center gap-3">
|
| 385 |
+
<span className="text-sm text-gray-500">
|
| 386 |
+
{filteredPartyTransactions.length} of {partyTransactions.length} transactions
|
| 387 |
+
</span>
|
| 388 |
+
{hasActiveFilters && (
|
| 389 |
+
<button
|
| 390 |
+
onClick={clearDetailFilters}
|
| 391 |
+
className="px-3 py-1.5 text-sm text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-1"
|
| 392 |
+
>
|
| 393 |
+
<X size={14} />
|
| 394 |
+
Clear Filters
|
| 395 |
+
</button>
|
| 396 |
+
)}
|
| 397 |
+
</div>
|
| 398 |
+
</div>
|
| 399 |
+
</div>
|
| 400 |
+
)}
|
| 401 |
+
|
| 402 |
+
{/* Content Area */}
|
| 403 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
| 404 |
+
{viewMode === 'list' ? (
|
| 405 |
+
<>
|
| 406 |
+
{/* Desktop Table View */}
|
| 407 |
+
<div className="hidden md:block overflow-x-auto">
|
| 408 |
+
<table className="w-full text-sm text-left">
|
| 409 |
+
<thead className="bg-gray-50 text-gray-500 border-b border-gray-100">
|
| 410 |
+
<tr>
|
| 411 |
+
<th className="px-6 py-4 font-medium">Party Name</th>
|
| 412 |
+
<th className="px-6 py-4 font-medium text-right">Total Bill Amount</th>
|
| 413 |
+
<th className="px-6 py-4 font-medium text-right">Jama (Paid)</th>
|
| 414 |
+
<th className="px-6 py-4 font-medium text-right">Baki (Balance)</th>
|
| 415 |
+
<th className="px-6 py-4 font-medium text-center">Action</th>
|
| 416 |
+
</tr>
|
| 417 |
+
</thead>
|
| 418 |
+
<tbody className="divide-y divide-gray-100">
|
| 419 |
+
{filteredParties.length > 0 ? (
|
| 420 |
+
filteredParties.map(party => {
|
| 421 |
+
const stats = getPartyStats(party.id);
|
| 422 |
+
return (
|
| 423 |
+
<tr key={party.id} className="hover:bg-gray-50 transition-colors">
|
| 424 |
+
<td className="px-6 py-4 font-medium text-gray-900">{party.name}</td>
|
| 425 |
+
<td className="px-6 py-4 text-right font-medium">₹{stats.totalBill.toLocaleString()}</td>
|
| 426 |
+
<td className="px-6 py-4 text-right text-green-600 font-medium">₹{stats.totalPaid.toLocaleString()}</td>
|
| 427 |
+
<td className={`px-6 py-4 text-right font-bold ${stats.balance !== 0 ? 'text-red-500' : 'text-gray-400'}`}>
|
| 428 |
+
₹{Math.abs(stats.balance).toLocaleString()} {stats.balance > 0 ? '(Dr)' : stats.balance < 0 ? '(Cr)' : ''}
|
| 429 |
+
</td>
|
| 430 |
+
<td className="px-6 py-4 text-center">
|
| 431 |
+
<button
|
| 432 |
+
onClick={() => {
|
| 433 |
+
setSelectedParty(party);
|
| 434 |
+
setViewMode('detail');
|
| 435 |
+
// If this party has no transactions, add some sample data for demo
|
| 436 |
+
const hasTx = transactions.some(t => t.party_id === party.id);
|
| 437 |
+
if (!hasTx) {
|
| 438 |
+
const sampleTxs = [
|
| 439 |
+
{
|
| 440 |
+
id: `${party.id}-tx1`,
|
| 441 |
+
party_id: party.id,
|
| 442 |
+
bill_date: new Date().toISOString().split('T')[0],
|
| 443 |
+
bill_number: 'SAMPLE001',
|
| 444 |
+
items: [{ mirchi_name: 'Red Chili' }],
|
| 445 |
+
is_return: false,
|
| 446 |
+
total_amount: 5000,
|
| 447 |
+
paid_amount: 2000,
|
| 448 |
+
balance_amount: 3000,
|
| 449 |
+
bill_type: PartyType.AWAAK,
|
| 450 |
+
},
|
| 451 |
+
{
|
| 452 |
+
id: `${party.id}-tx2`,
|
| 453 |
+
party_id: party.id,
|
| 454 |
+
bill_date: new Date().toISOString().split('T')[0],
|
| 455 |
+
bill_number: 'SAMPLE002',
|
| 456 |
+
items: [{ mirchi_name: 'Green Chili' }],
|
| 457 |
+
is_return: false,
|
| 458 |
+
total_amount: 3000,
|
| 459 |
+
paid_amount: 3000,
|
| 460 |
+
balance_amount: 0,
|
| 461 |
+
bill_type: PartyType.AWAAK,
|
| 462 |
+
},
|
| 463 |
+
];
|
| 464 |
+
setTransactions(prev => [...prev, ...sampleTxs]);
|
| 465 |
+
}
|
| 466 |
+
}}
|
| 467 |
+
className="p-2 hover:bg-teal-50 text-teal-600 rounded-full transition-colors"
|
| 468 |
+
title="View Ledger"
|
| 469 |
+
>
|
| 470 |
+
<Eye size={18} />
|
| 471 |
+
</button>
|
| 472 |
+
</td>
|
| 473 |
+
</tr>
|
| 474 |
+
);
|
| 475 |
+
})
|
| 476 |
+
) : (
|
| 477 |
+
<tr>
|
| 478 |
+
<td colSpan={5} className="px-6 py-8 text-center text-gray-500">
|
| 479 |
+
No parties found.
|
| 480 |
+
</td>
|
| 481 |
+
</tr>
|
| 482 |
+
)}
|
| 483 |
+
</tbody>
|
| 484 |
+
</table>
|
| 485 |
+
</div>
|
| 486 |
+
|
| 487 |
+
{/* Mobile Card View */}
|
| 488 |
+
<div className="md:hidden flex flex-col divide-y divide-gray-100">
|
| 489 |
+
{filteredParties.length > 0 ? (
|
| 490 |
+
filteredParties.map(party => {
|
| 491 |
+
const stats = getPartyStats(party.id);
|
| 492 |
+
return (
|
| 493 |
+
<div key={party.id} className="p-4 space-y-3">
|
| 494 |
+
<div className="flex justify-between items-start">
|
| 495 |
+
<div>
|
| 496 |
+
<h3 className="font-bold text-gray-900">{party.name}</h3>
|
| 497 |
+
</div>
|
| 498 |
+
<button
|
| 499 |
+
onClick={() => {
|
| 500 |
+
setSelectedParty(party);
|
| 501 |
+
setViewMode('detail');
|
| 502 |
+
// If this party has no transactions, add sample data for demo
|
| 503 |
+
const hasTx = transactions.some(t => t.party_id === party.id);
|
| 504 |
+
if (!hasTx) {
|
| 505 |
+
const sampleTxs = [
|
| 506 |
+
{
|
| 507 |
+
id: `${party.id}-tx1`,
|
| 508 |
+
party_id: party.id,
|
| 509 |
+
bill_date: new Date().toISOString().split('T')[0],
|
| 510 |
+
bill_number: 'SAMPLE001',
|
| 511 |
+
items: [{ mirchi_name: 'Red Chili' }],
|
| 512 |
+
is_return: false,
|
| 513 |
+
total_amount: 5000,
|
| 514 |
+
paid_amount: 2000,
|
| 515 |
+
balance_amount: 3000,
|
| 516 |
+
bill_type: PartyType.AWAAK,
|
| 517 |
+
},
|
| 518 |
+
{
|
| 519 |
+
id: `${party.id}-tx2`,
|
| 520 |
+
party_id: party.id,
|
| 521 |
+
bill_date: new Date().toISOString().split('T')[0],
|
| 522 |
+
bill_number: 'SAMPLE002',
|
| 523 |
+
items: [{ mirchi_name: 'Green Chili' }],
|
| 524 |
+
is_return: false,
|
| 525 |
+
total_amount: 3000,
|
| 526 |
+
paid_amount: 3000,
|
| 527 |
+
balance_amount: 0,
|
| 528 |
+
bill_type: PartyType.AWAAK,
|
| 529 |
+
},
|
| 530 |
+
];
|
| 531 |
+
setTransactions(prev => [...prev, ...sampleTxs]);
|
| 532 |
+
}
|
| 533 |
+
}}
|
| 534 |
+
className="p-2 bg-teal-50 text-teal-600 rounded-lg"
|
| 535 |
+
>
|
| 536 |
+
<Eye size={18} />
|
| 537 |
+
</button>
|
| 538 |
+
</div>
|
| 539 |
+
<div className="grid grid-cols-3 gap-2 text-xs">
|
| 540 |
+
<div className="bg-gray-50 p-2 rounded">
|
| 541 |
+
<div className="text-gray-500 mb-1">Total Bill</div>
|
| 542 |
+
<div className="font-medium">₹{stats.totalBill.toLocaleString()}</div>
|
| 543 |
+
</div>
|
| 544 |
+
<div className="bg-green-50 p-2 rounded">
|
| 545 |
+
<div className="text-green-600 mb-1">Paid</div>
|
| 546 |
+
<div className="font-medium text-green-700">₹{stats.totalPaid.toLocaleString()}</div>
|
| 547 |
+
</div>
|
| 548 |
+
<div className="bg-red-50 p-2 rounded">
|
| 549 |
+
<div className="text-red-500 mb-1">Balance</div>
|
| 550 |
+
<div className="font-bold text-red-600">
|
| 551 |
+
₹{Math.abs(stats.balance).toLocaleString()} {stats.balance > 0 ? 'Dr' : stats.balance < 0 ? 'Cr' : ''}
|
| 552 |
+
</div>
|
| 553 |
+
</div>
|
| 554 |
+
</div>
|
| 555 |
+
</div>
|
| 556 |
+
);
|
| 557 |
+
})
|
| 558 |
+
) : (
|
| 559 |
+
<div className="p-8 text-center text-gray-500">
|
| 560 |
+
No parties found.
|
| 561 |
+
</div>
|
| 562 |
+
)}
|
| 563 |
+
</div>
|
| 564 |
+
</>
|
| 565 |
+
) : (
|
| 566 |
+
// Detail View
|
| 567 |
+
<>
|
| 568 |
+
{/* Desktop Table View */}
|
| 569 |
+
<div className="hidden md:block overflow-x-auto">
|
| 570 |
+
<table className="w-full text-sm text-left">
|
| 571 |
+
<thead className="bg-gray-50 text-gray-500 border-b border-gray-100">
|
| 572 |
+
<tr>
|
| 573 |
+
<th className="px-6 py-4 font-medium text-center">
|
| 574 |
+
<input
|
| 575 |
+
type="checkbox"
|
| 576 |
+
checked={selectedTransactions.length === filteredPartyTransactions.length && filteredPartyTransactions.length > 0}
|
| 577 |
+
onChange={(e) => {
|
| 578 |
+
if (e.target.checked) {
|
| 579 |
+
setSelectedTransactions(filteredPartyTransactions.map(t => t.id));
|
| 580 |
+
} else {
|
| 581 |
+
setSelectedTransactions([]);
|
| 582 |
+
}
|
| 583 |
+
}}
|
| 584 |
+
className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500"
|
| 585 |
+
/>
|
| 586 |
+
</th>
|
| 587 |
+
<th className="px-6 py-4 font-medium">Date</th>
|
| 588 |
+
<th className="px-6 py-4 font-medium">Bill No</th>
|
| 589 |
+
<th className="px-6 py-4 font-medium">Mirchi Type</th>
|
| 590 |
+
<th className="px-6 py-4 font-medium">Remark</th>
|
| 591 |
+
<th className="px-6 py-4 font-medium text-right">Bill Amount</th>
|
| 592 |
+
<th className="px-6 py-4 font-medium text-right">Paid</th>
|
| 593 |
+
<th className="px-6 py-4 font-medium text-right">Due Balance</th>
|
| 594 |
+
<th className="px-6 py-4 font-medium text-center">Action</th>
|
| 595 |
+
<th className="px-6 py-4 font-medium text-center">Print</th>
|
| 596 |
+
</tr>
|
| 597 |
+
</thead>
|
| 598 |
+
<tbody className="divide-y divide-gray-100">
|
| 599 |
+
{filteredPartyTransactions.length > 0 ? (
|
| 600 |
+
filteredPartyTransactions.map(tx => (
|
| 601 |
+
<React.Fragment key={tx.id}>
|
| 602 |
+
<tr className="hover:bg-gray-50">
|
| 603 |
+
<td className="px-6 py-4 text-center">
|
| 604 |
+
<input
|
| 605 |
+
type="checkbox"
|
| 606 |
+
checked={selectedTransactions.includes(tx.id)}
|
| 607 |
+
onChange={(e) => {
|
| 608 |
+
if (e.target.checked) {
|
| 609 |
+
setSelectedTransactions([...selectedTransactions, tx.id]);
|
| 610 |
+
} else {
|
| 611 |
+
setSelectedTransactions(selectedTransactions.filter(id => id !== tx.id));
|
| 612 |
+
}
|
| 613 |
+
}}
|
| 614 |
+
className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500"
|
| 615 |
+
/>
|
| 616 |
+
</td>
|
| 617 |
+
<td className="px-6 py-4 text-gray-600">{tx.bill_date?.split('T')[0] || tx.bill_date}</td>
|
| 618 |
+
<td className="px-6 py-4 font-mono text-gray-500">{tx.bill_number}</td>
|
| 619 |
+
<td className="px-6 py-4">
|
| 620 |
+
{tx.items.map((i, idx) => (
|
| 621 |
+
<div key={idx} className="text-sm">
|
| 622 |
+
{i.mirchi_name}
|
| 623 |
+
{i.lot_number && (
|
| 624 |
+
<span className="ml-2 text-xs font-mono text-gray-500">({i.lot_number})</span>
|
| 625 |
+
)}
|
| 626 |
+
</div>
|
| 627 |
+
))}
|
| 628 |
+
</td>
|
| 629 |
+
<td className="px-6 py-4">
|
| 630 |
+
{tx.is_return ? (
|
| 631 |
+
<span className="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs font-semibold">Returned</span>
|
| 632 |
+
) : (
|
| 633 |
+
<span className="text-gray-400">-</span>
|
| 634 |
+
)}
|
| 635 |
+
</td>
|
| 636 |
+
<td className="px-6 py-4 text-right font-medium">₹{tx.total_amount.toLocaleString()}</td>
|
| 637 |
+
<td className="px-6 py-4 text-right text-green-600">₹{tx.paid_amount.toLocaleString()}</td>
|
| 638 |
+
<td className="px-6 py-4 text-right font-bold text-red-500">₹{tx.balance_amount.toLocaleString()}</td>
|
| 639 |
+
<td className="px-6 py-4 text-center">
|
| 640 |
+
{editingTxId === tx.id ? (
|
| 641 |
+
<div className="flex items-center gap-2 justify-end">
|
| 642 |
+
<input
|
| 643 |
+
type="number"
|
| 644 |
+
className="w-24 border border-teal-300 rounded px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-teal-500"
|
| 645 |
+
placeholder="Amount"
|
| 646 |
+
value={paymentAmount}
|
| 647 |
+
onChange={e => setPaymentAmount(e.target.value)}
|
| 648 |
+
autoFocus
|
| 649 |
+
/>
|
| 650 |
+
<button
|
| 651 |
+
onClick={() => handleUpdatePayment(tx)}
|
| 652 |
+
className="p-2 bg-teal-600 text-white rounded hover:bg-teal-700 disabled:opacity-50"
|
| 653 |
+
title="Save"
|
| 654 |
+
disabled={Number(paymentAmount) > tx.balance_amount || Number(paymentAmount) <= 0}
|
| 655 |
+
>
|
| 656 |
+
<Save size={16} />
|
| 657 |
+
</button>
|
| 658 |
+
<button
|
| 659 |
+
onClick={() => { setEditingTxId(null); setPaymentAmount(''); }}
|
| 660 |
+
className="p-1 bg-gray-200 text-gray-600 rounded hover:bg-gray-300"
|
| 661 |
+
title="Cancel"
|
| 662 |
+
>
|
| 663 |
+
<X size={14} />
|
| 664 |
+
</button>
|
| 665 |
+
</div>
|
| 666 |
+
) : (
|
| 667 |
+
<button
|
| 668 |
+
onClick={() => { setEditingTxId(tx.id); setPaymentAmount(''); }}
|
| 669 |
+
className="px-3 py-1 border border-teal-600 text-teal-600 rounded-md text-xs font-medium hover:bg-teal-50 transition-colors"
|
| 670 |
+
>
|
| 671 |
+
Update Due
|
| 672 |
+
</button>
|
| 673 |
+
)}
|
| 674 |
+
</td>
|
| 675 |
+
<td className="px-6 py-4 text-center">
|
| 676 |
+
<div className="flex gap-2 justify-center items-center">
|
| 677 |
+
<PrintInvoice
|
| 678 |
+
transaction={tx}
|
| 679 |
+
className="p-2 bg-white text-teal-600 border border-teal-600 rounded-md hover:bg-teal-50 transition-colors flex items-center justify-center shadow-sm"
|
| 680 |
+
/>
|
| 681 |
+
<PdfInvoice
|
| 682 |
+
transaction={tx}
|
| 683 |
+
className="p-2 bg-white text-red-600 border border-red-600 rounded-md hover:bg-red-50 transition-colors flex items-center justify-center shadow-sm"
|
| 684 |
+
>
|
| 685 |
+
<Download size={16} />
|
| 686 |
+
</PdfInvoice>
|
| 687 |
+
</div>
|
| 688 |
+
</td>
|
| 689 |
+
</tr>
|
| 690 |
+
{tx.payments && tx.payments.length > 0 && (
|
| 691 |
+
<tr className="bg-gray-50">
|
| 692 |
+
<td colSpan={10} className="px-12 py-2 text-xs text-gray-600">
|
| 693 |
+
<div className="flex items-start justify-between gap-4">
|
| 694 |
+
<span className="font-semibold text-gray-700 mt-0.5">Payment History:</span>
|
| 695 |
+
<div className="flex flex-wrap gap-3">
|
| 696 |
+
{[...tx.payments.filter(p => p.mode === 'due'), ...tx.payments.filter(p => p.mode !== 'due')].map((p, idx) => (
|
| 697 |
+
<div key={idx} className="flex items-center gap-1 bg-white border border-gray-200 rounded-full px-2 py-0.5 shadow-sm">
|
| 698 |
+
<span className="px-2 py-0.5 rounded-full text-[11px] bg-teal-50 text-teal-700 border border-teal-100 capitalize">
|
| 699 |
+
{p.mode === 'cash' ? 'Cash' : p.mode === 'online' ? 'Online' : p.mode === 'due' ? 'Due' : p.mode}
|
| 700 |
+
</span>
|
| 701 |
+
<span className="text-gray-800 font-medium text-xs">₹{p.amount.toLocaleString()}</span>
|
| 702 |
+
{p.reference && (
|
| 703 |
+
<span className="text-gray-400 text-[11px] ml-1">{p.reference}</span>
|
| 704 |
+
)}
|
| 705 |
+
</div>
|
| 706 |
+
))}
|
| 707 |
+
</div>
|
| 708 |
+
</div>
|
| 709 |
+
</td>
|
| 710 |
+
</tr>
|
| 711 |
+
)}
|
| 712 |
+
</React.Fragment>
|
| 713 |
+
))
|
| 714 |
+
) : (
|
| 715 |
+
<tr>
|
| 716 |
+
<td colSpan={10} className="px-6 py-8 text-center text-gray-500">
|
| 717 |
+
{partyTransactions.length > 0
|
| 718 |
+
? 'No transactions match your filters.'
|
| 719 |
+
: 'No transactions found for this party.'}
|
| 720 |
+
</td>
|
| 721 |
+
</tr>
|
| 722 |
+
)}
|
| 723 |
+
</tbody>
|
| 724 |
+
</table>
|
| 725 |
+
</div>
|
| 726 |
+
|
| 727 |
+
{/* Mobile Card View for Detail */}
|
| 728 |
+
<div className="md:hidden flex flex-col divide-y divide-gray-100">
|
| 729 |
+
{filteredPartyTransactions.length > 0 ? (
|
| 730 |
+
filteredPartyTransactions.map(tx => (
|
| 731 |
+
<div key={tx.id} className="p-4 space-y-3">
|
| 732 |
+
<div className="flex justify-between items-start">
|
| 733 |
+
<div className="flex items-start gap-3">
|
| 734 |
+
<input
|
| 735 |
+
type="checkbox"
|
| 736 |
+
checked={selectedTransactions.includes(tx.id)}
|
| 737 |
+
onChange={(e) => {
|
| 738 |
+
if (e.target.checked) {
|
| 739 |
+
setSelectedTransactions([...selectedTransactions, tx.id]);
|
| 740 |
+
} else {
|
| 741 |
+
setSelectedTransactions(selectedTransactions.filter(id => id !== tx.id));
|
| 742 |
+
}
|
| 743 |
+
}}
|
| 744 |
+
className="w-4 h-4 text-teal-600 rounded focus:ring-teal-500 mt-1"
|
| 745 |
+
/>
|
| 746 |
+
<div>
|
| 747 |
+
<div className="text-xs text-gray-500">{tx.bill_date?.split('T')[0] || tx.bill_date}</div>
|
| 748 |
+
<div className="font-mono text-sm font-medium text-gray-800">{tx.bill_number}</div>
|
| 749 |
+
</div>
|
| 750 |
+
</div>
|
| 751 |
+
{tx.is_return && (
|
| 752 |
+
<span className="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs font-semibold">Returned</span>
|
| 753 |
+
)}
|
| 754 |
+
</div>
|
| 755 |
+
|
| 756 |
+
<div className="text-sm text-gray-600">
|
| 757 |
+
<span className="font-medium text-gray-500 text-xs block mb-1">Items:</span>
|
| 758 |
+
{tx.items.map((i, idx) => (
|
| 759 |
+
<div key={idx}>
|
| 760 |
+
{i.mirchi_name}
|
| 761 |
+
{i.lot_number && (
|
| 762 |
+
<span className="ml-2 text-xs font-mono text-gray-500">({i.lot_number})</span>
|
| 763 |
+
)}
|
| 764 |
+
</div>
|
| 765 |
+
))}
|
| 766 |
+
</div>
|
| 767 |
+
|
| 768 |
+
<div className="grid grid-cols-3 gap-2 text-xs pt-2 border-t border-gray-50">
|
| 769 |
+
<div>
|
| 770 |
+
<div className="text-gray-500">Bill Amount</div>
|
| 771 |
+
<div className="font-medium">₹{tx.total_amount.toLocaleString()}</div>
|
| 772 |
+
</div>
|
| 773 |
+
<div>
|
| 774 |
+
<div className="text-gray-500">Paid</div>
|
| 775 |
+
<div className="font-medium text-green-600">₹{tx.paid_amount.toLocaleString()}</div>
|
| 776 |
+
</div>
|
| 777 |
+
<div>
|
| 778 |
+
<div className="text-gray-500">Due</div>
|
| 779 |
+
<div className="font-bold text-red-500">₹{tx.balance_amount.toLocaleString()}</div>
|
| 780 |
+
</div>
|
| 781 |
+
</div>
|
| 782 |
+
|
| 783 |
+
{tx.payments && tx.payments.length > 0 && (
|
| 784 |
+
<div className="pt-2 border-t border-gray-100 mt-2 text-xs text-gray-600 space-y-1">
|
| 785 |
+
<div className="font-semibold text-gray-700">Payment History</div>
|
| 786 |
+
{[...tx.payments.filter(p => p.mode === 'due'), ...tx.payments.filter(p => p.mode !== 'due')].map((p, idx) => (
|
| 787 |
+
<div key={idx} className="flex justify-between">
|
| 788 |
+
<span>
|
| 789 |
+
{p.mode === 'cash' ? 'Cash' : p.mode === 'online' ? 'Online' : p.mode === 'due' ? 'Due' : p.mode}
|
| 790 |
+
{p.reference && (
|
| 791 |
+
<span className="ml-1 text-gray-400">({p.reference})</span>
|
| 792 |
+
)}
|
| 793 |
+
</span>
|
| 794 |
+
<span className="font-medium">₹{p.amount.toLocaleString()}</span>
|
| 795 |
+
</div>
|
| 796 |
+
))}
|
| 797 |
+
</div>
|
| 798 |
+
)}
|
| 799 |
+
|
| 800 |
+
<div className="pt-2">
|
| 801 |
+
{editingTxId === tx.id ? (
|
| 802 |
+
<div className="flex items-center gap-2">
|
| 803 |
+
<input
|
| 804 |
+
type="number"
|
| 805 |
+
className="flex-1 border border-teal-300 rounded px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-teal-500"
|
| 806 |
+
placeholder="Enter Amount"
|
| 807 |
+
value={paymentAmount}
|
| 808 |
+
onChange={e => setPaymentAmount(e.target.value)}
|
| 809 |
+
autoFocus
|
| 810 |
+
/>
|
| 811 |
+
<button
|
| 812 |
+
onClick={() => handleUpdatePayment(tx)}
|
| 813 |
+
className="p-2 bg-teal-600 text-white rounded hover:bg-teal-700"
|
| 814 |
+
>
|
| 815 |
+
<Save size={16} />
|
| 816 |
+
</button>
|
| 817 |
+
<button
|
| 818 |
+
onClick={() => { setEditingTxId(null); setPaymentAmount(''); }}
|
| 819 |
+
className="p-2 bg-gray-200 text-gray-600 rounded hover:bg-gray-300"
|
| 820 |
+
>
|
| 821 |
+
<X size={16} />
|
| 822 |
+
</button>
|
| 823 |
+
</div>
|
| 824 |
+
) : (
|
| 825 |
+
<button
|
| 826 |
+
onClick={() => { setEditingTxId(tx.id); setPaymentAmount(''); }}
|
| 827 |
+
className="w-full py-2 border border-teal-600 text-teal-600 rounded-lg text-sm font-medium hover:bg-teal-50 transition-colors"
|
| 828 |
+
>
|
| 829 |
+
Update Due
|
| 830 |
+
</button>
|
| 831 |
+
)}
|
| 832 |
+
</div>
|
| 833 |
+
|
| 834 |
+
{/* Print Button for Mobile */}
|
| 835 |
+
<div className="pt-2 border-t border-gray-100 mt-2 flex gap-3 items-center">
|
| 836 |
+
<PrintInvoice
|
| 837 |
+
transaction={tx}
|
| 838 |
+
className="p-2 bg-white text-teal-600 border border-teal-600 rounded-md hover:bg-teal-50 transition-colors flex items-center justify-center shadow-sm"
|
| 839 |
+
/>
|
| 840 |
+
<PdfInvoice
|
| 841 |
+
transaction={tx}
|
| 842 |
+
className="p-2 bg-white text-red-600 border border-red-600 rounded-md hover:bg-red-50 transition-colors flex items-center justify-center shadow-sm"
|
| 843 |
+
>
|
| 844 |
+
<Download size={16} />
|
| 845 |
+
</PdfInvoice>
|
| 846 |
+
</div>
|
| 847 |
+
</div>
|
| 848 |
+
))
|
| 849 |
+
) : (
|
| 850 |
+
<div className="p-8 text-center text-gray-500">
|
| 851 |
+
{partyTransactions.length > 0
|
| 852 |
+
? 'No transactions match your filters.'
|
| 853 |
+
: 'No transactions found.'}
|
| 854 |
+
</div>
|
| 855 |
+
)}
|
| 856 |
+
</div>
|
| 857 |
+
</>
|
| 858 |
+
)}
|
| 859 |
+
</div>
|
| 860 |
+
</div >
|
| 861 |
+
);
|
| 862 |
+
};
|
| 863 |
+
|
| 864 |
+
export default PartyLedger;
|
pages/Settings.tsx
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import {
|
| 3 |
+
getParties,
|
| 4 |
+
saveParty,
|
| 5 |
+
apiGetMirchiTypes,
|
| 6 |
+
apiSaveMirchiType,
|
| 7 |
+
} from '../services/db';
|
| 8 |
+
import { Party, MirchiType, PartyType } from '../types';
|
| 9 |
+
import { Save, Plus, Settings as SettingsIcon, Users, Sprout, Bell, Download } from 'lucide-react';
|
| 10 |
+
import { usePWA } from '../context/PWAContext';
|
| 11 |
+
|
| 12 |
+
const Settings = () => {
|
| 13 |
+
const [activeTab, setActiveTab] = useState<'parties' | 'mirchi' | 'general'>('parties');
|
| 14 |
+
const [parties, setParties] = useState<Party[]>([]);
|
| 15 |
+
const [mirchiTypes, setMirchiTypes] = useState<MirchiType[]>([]);
|
| 16 |
+
|
| 17 |
+
// PWA Install
|
| 18 |
+
const { isInstallable, installApp } = usePWA();
|
| 19 |
+
|
| 20 |
+
// Form States
|
| 21 |
+
const [newParty, setNewParty] = useState<Partial<Party>>({
|
| 22 |
+
name: '', city: '', phone: '', party_type: PartyType.BOTH, current_balance: 0
|
| 23 |
+
});
|
| 24 |
+
const [newMirchi, setNewMirchi] = useState<Partial<MirchiType>>({
|
| 25 |
+
name: ''
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
// Inline feedback
|
| 29 |
+
const [partyMessage, setPartyMessage] = useState<{ type: 'error' | 'success'; text: string } | null>(null);
|
| 30 |
+
const [mirchiMessage, setMirchiMessage] = useState<{ type: 'error' | 'success'; text: string } | null>(null);
|
| 31 |
+
|
| 32 |
+
// Alert Config (Mock)
|
| 33 |
+
const [config, setConfig] = useState({
|
| 34 |
+
lowStockBagsThreshold: 10,
|
| 35 |
+
enableNotifications: true,
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
useEffect(() => {
|
| 39 |
+
const loadData = async () => {
|
| 40 |
+
setParties(await getParties());
|
| 41 |
+
setMirchiTypes(await apiGetMirchiTypes());
|
| 42 |
+
};
|
| 43 |
+
loadData();
|
| 44 |
+
}, []);
|
| 45 |
+
|
| 46 |
+
useEffect(() => {
|
| 47 |
+
try {
|
| 48 |
+
const stored = localStorage.getItem('pp_alert_config');
|
| 49 |
+
if (!stored) return;
|
| 50 |
+
const parsed = JSON.parse(stored);
|
| 51 |
+
setConfig({
|
| 52 |
+
lowStockBagsThreshold:
|
| 53 |
+
typeof parsed.lowStockBagsThreshold === 'number' &&
|
| 54 |
+
!Number.isNaN(parsed.lowStockBagsThreshold)
|
| 55 |
+
? parsed.lowStockBagsThreshold
|
| 56 |
+
: 10,
|
| 57 |
+
enableNotifications:
|
| 58 |
+
typeof parsed.enableNotifications === 'boolean'
|
| 59 |
+
? parsed.enableNotifications
|
| 60 |
+
: true,
|
| 61 |
+
});
|
| 62 |
+
} catch (error) {
|
| 63 |
+
console.error('Error loading alert config', error);
|
| 64 |
+
}
|
| 65 |
+
}, []);
|
| 66 |
+
|
| 67 |
+
const handleSaveParty = async () => {
|
| 68 |
+
if (!newParty.name) {
|
| 69 |
+
setPartyMessage({ type: 'error', text: 'Party Name is required.' });
|
| 70 |
+
return;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
const party: Party = {
|
| 74 |
+
id: newParty.id || `p-${Date.now()}`,
|
| 75 |
+
name: newParty.name,
|
| 76 |
+
city: newParty.city || '',
|
| 77 |
+
phone: newParty.phone || '',
|
| 78 |
+
party_type: newParty.party_type as PartyType,
|
| 79 |
+
current_balance: parseFloat(String(newParty.current_balance || 0))
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
await saveParty(party);
|
| 83 |
+
setNewParty({ name: '', city: '', phone: '', party_type: PartyType.BOTH, current_balance: 0 });
|
| 84 |
+
loadData();
|
| 85 |
+
setPartyMessage({ type: 'success', text: 'Party saved successfully.' });
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
const handleSaveMirchi = async () => {
|
| 89 |
+
if (!newMirchi.name) {
|
| 90 |
+
setMirchiMessage({ type: 'error', text: 'Mirchi type name is required.' });
|
| 91 |
+
return;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
const type: MirchiType = {
|
| 95 |
+
id: newMirchi.id || `m-${Date.now()}`,
|
| 96 |
+
name: newMirchi.name,
|
| 97 |
+
current_rate: 0
|
| 98 |
+
};
|
| 99 |
+
|
| 100 |
+
const result = await apiSaveMirchiType(type);
|
| 101 |
+
if (!result.success) {
|
| 102 |
+
setMirchiMessage({ type: 'error', text: result.message || 'Error saving mirchi type.' });
|
| 103 |
+
return;
|
| 104 |
+
}
|
| 105 |
+
setNewMirchi({ name: '' });
|
| 106 |
+
loadData();
|
| 107 |
+
setMirchiMessage({ type: 'success', text: 'Mirchi type saved successfully.' });
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
const loadData = async () => {
|
| 111 |
+
setParties(await getParties());
|
| 112 |
+
setMirchiTypes(await apiGetMirchiTypes());
|
| 113 |
+
};
|
| 114 |
+
|
| 115 |
+
const handleSaveAlertConfig = () => {
|
| 116 |
+
try {
|
| 117 |
+
localStorage.setItem('pp_alert_config', JSON.stringify(config));
|
| 118 |
+
} catch (error) {
|
| 119 |
+
console.error('Error saving alert config', error);
|
| 120 |
+
}
|
| 121 |
+
};
|
| 122 |
+
|
| 123 |
+
const handleInstallClick = async () => {
|
| 124 |
+
await installApp();
|
| 125 |
+
};
|
| 126 |
+
|
| 127 |
+
return (
|
| 128 |
+
<div className="flex flex-col md:flex-row h-full gap-6">
|
| 129 |
+
{/* Sidebar / Tabs */}
|
| 130 |
+
<div className="w-full md:w-64 bg-white rounded-xl shadow-sm border border-gray-100 p-4 h-fit">
|
| 131 |
+
<h2 className="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
|
| 132 |
+
<SettingsIcon className="text-teal-600" size={20} /> सेटिंग्स
|
| 133 |
+
</h2>
|
| 134 |
+
<div className="space-y-2">
|
| 135 |
+
<button
|
| 136 |
+
onClick={() => setActiveTab('parties')}
|
| 137 |
+
className={`w-full text-left px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-3 transition ${activeTab === 'parties' ? 'bg-teal-50 text-teal-700' : 'text-gray-600 hover:bg-gray-50'}`}
|
| 138 |
+
>
|
| 139 |
+
<Users size={18} /> पार्टी व्यवस्थापन
|
| 140 |
+
</button>
|
| 141 |
+
<button
|
| 142 |
+
onClick={() => setActiveTab('mirchi')}
|
| 143 |
+
className={`w-full text-left px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-3 transition ${activeTab === 'mirchi' ? 'bg-teal-50 text-teal-700' : 'text-gray-600 hover:bg-gray-50'}`}
|
| 144 |
+
>
|
| 145 |
+
<Sprout size={18} /> मिरची दर & प्रकार
|
| 146 |
+
</button>
|
| 147 |
+
<button
|
| 148 |
+
onClick={() => setActiveTab('general')}
|
| 149 |
+
className={`w-full text-left px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-3 transition ${activeTab === 'general' ? 'bg-teal-50 text-teal-700' : 'text-gray-600 hover:bg-gray-50'}`}
|
| 150 |
+
>
|
| 151 |
+
<Bell size={18} /> Alerts & Rules
|
| 152 |
+
</button>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
|
| 156 |
+
{/* Content Area */}
|
| 157 |
+
<div className="flex-1 bg-white rounded-xl shadow-sm border border-gray-100 p-6 overflow-y-auto no-scrollbar">
|
| 158 |
+
|
| 159 |
+
{/* PARTIES TAB */}
|
| 160 |
+
{activeTab === 'parties' && (
|
| 161 |
+
<div className="space-y-6">
|
| 162 |
+
<div className="border-b pb-4">
|
| 163 |
+
<h3 className="text-lg font-bold text-gray-800">Party व्यवस्थापन (Party Management)</h3>
|
| 164 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
| 165 |
+
<input
|
| 166 |
+
placeholder="Party Name"
|
| 167 |
+
className="border rounded p-2"
|
| 168 |
+
value={newParty.name}
|
| 169 |
+
onChange={e => setNewParty({ ...newParty, name: e.target.value })}
|
| 170 |
+
/>
|
| 171 |
+
<input
|
| 172 |
+
placeholder="Phone"
|
| 173 |
+
className="border rounded p-2"
|
| 174 |
+
value={newParty.phone}
|
| 175 |
+
onChange={e => setNewParty({ ...newParty, phone: e.target.value })}
|
| 176 |
+
/>
|
| 177 |
+
<select
|
| 178 |
+
className="border rounded p-2"
|
| 179 |
+
value={newParty.party_type}
|
| 180 |
+
onChange={e => setNewParty({ ...newParty, party_type: e.target.value as PartyType })}
|
| 181 |
+
>
|
| 182 |
+
<option value={PartyType.BOTH}>Both (Purchase & Sales)</option>
|
| 183 |
+
<option value={PartyType.AWAAK}>Only Awaak (Purchase)</option>
|
| 184 |
+
<option value={PartyType.JAWAAK}>Only Jawaak (Sales)</option>
|
| 185 |
+
</select>
|
| 186 |
+
<button
|
| 187 |
+
onClick={handleSaveParty}
|
| 188 |
+
className="bg-teal-600 text-white rounded p-2 flex items-center justify-center gap-2 font-medium hover:bg-teal-700"
|
| 189 |
+
>
|
| 190 |
+
<Plus size={18} /> Save Party
|
| 191 |
+
</button>
|
| 192 |
+
</div>
|
| 193 |
+
{partyMessage && (
|
| 194 |
+
<div
|
| 195 |
+
className={`mt-3 text-sm rounded px-3 py-2 ${partyMessage.type === 'error'
|
| 196 |
+
? 'bg-red-50 text-red-700 border border-red-100'
|
| 197 |
+
: 'bg-green-50 text-green-700 border border-green-100'
|
| 198 |
+
}`}
|
| 199 |
+
>
|
| 200 |
+
{partyMessage.text}
|
| 201 |
+
</div>
|
| 202 |
+
)}
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<div>
|
| 206 |
+
<h3 className="font-semibold text-gray-700 mb-3">All Parties</h3>
|
| 207 |
+
<div className="overflow-x-auto">
|
| 208 |
+
<table className="w-full text-sm text-left">
|
| 209 |
+
<thead className="bg-gray-50 text-gray-500">
|
| 210 |
+
<tr>
|
| 211 |
+
<th className="p-2">Name</th>
|
| 212 |
+
<th className="p-2">Phone</th>
|
| 213 |
+
<th className="p-2">Type</th>
|
| 214 |
+
<th className="p-2 text-center">Action</th>
|
| 215 |
+
</tr>
|
| 216 |
+
</thead>
|
| 217 |
+
<tbody className="divide-y">
|
| 218 |
+
{parties.map(p => (
|
| 219 |
+
<tr key={p.id}>
|
| 220 |
+
<td className="p-2">{p.name}</td>
|
| 221 |
+
<td className="p-2">{p.phone}</td>
|
| 222 |
+
<td className="p-2">{p.party_type}</td>
|
| 223 |
+
<td className="p-2 text-center">
|
| 224 |
+
<button
|
| 225 |
+
onClick={() => setNewParty(p)}
|
| 226 |
+
className="text-blue-600 hover:underline text-xs"
|
| 227 |
+
>
|
| 228 |
+
Edit
|
| 229 |
+
</button>
|
| 230 |
+
</td>
|
| 231 |
+
</tr>
|
| 232 |
+
))}
|
| 233 |
+
</tbody>
|
| 234 |
+
</table>
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
)}
|
| 239 |
+
|
| 240 |
+
{/* MIRCHI TAB */}
|
| 241 |
+
{activeTab === 'mirchi' && (
|
| 242 |
+
<div className="space-y-6">
|
| 243 |
+
<div className="border-b pb-4">
|
| 244 |
+
<h3 className="text-lg font-bold text-gray-800">मिरची प्रकार (Mirchi Types)</h3>
|
| 245 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
|
| 246 |
+
<input
|
| 247 |
+
placeholder="Variety Name (e.g. Teja)"
|
| 248 |
+
className="border rounded p-2"
|
| 249 |
+
value={newMirchi.name}
|
| 250 |
+
onChange={e => setNewMirchi({ ...newMirchi, name: e.target.value })}
|
| 251 |
+
/>
|
| 252 |
+
<button
|
| 253 |
+
onClick={handleSaveMirchi}
|
| 254 |
+
className="bg-teal-600 text-white rounded p-2 flex items-center justify-center gap-2 font-medium hover:bg-teal-700"
|
| 255 |
+
>
|
| 256 |
+
<Plus size={18} /> Save Type
|
| 257 |
+
</button>
|
| 258 |
+
</div>
|
| 259 |
+
{mirchiMessage && (
|
| 260 |
+
<div
|
| 261 |
+
className={`mt-3 text-sm rounded px-3 py-2 ${mirchiMessage.type === 'error'
|
| 262 |
+
? 'bg-red-50 text-red-700 border border-red-100'
|
| 263 |
+
: 'bg-green-50 text-green-700 border border-green-100'
|
| 264 |
+
}`}
|
| 265 |
+
>
|
| 266 |
+
{mirchiMessage.text}
|
| 267 |
+
</div>
|
| 268 |
+
)}
|
| 269 |
+
</div>
|
| 270 |
+
|
| 271 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 272 |
+
{mirchiTypes.map(m => (
|
| 273 |
+
<div key={m.id} className="p-4 border rounded-lg hover:shadow-md transition bg-gray-50">
|
| 274 |
+
<div className="flex justify-between items-start mb-2">
|
| 275 |
+
<h4 className="font-bold text-gray-800">{m.name}</h4>
|
| 276 |
+
<button
|
| 277 |
+
onClick={() => setNewMirchi(m)}
|
| 278 |
+
className="text-teal-600 text-xs font-medium"
|
| 279 |
+
>
|
| 280 |
+
Edit
|
| 281 |
+
</button>
|
| 282 |
+
</div>
|
| 283 |
+
<div className="text-xs text-gray-500 mt-1">Mirchi Type</div>
|
| 284 |
+
</div>
|
| 285 |
+
))}
|
| 286 |
+
</div>
|
| 287 |
+
</div>
|
| 288 |
+
)}
|
| 289 |
+
|
| 290 |
+
{/* General / Alerts Tab */}
|
| 291 |
+
{activeTab === 'general' && (
|
| 292 |
+
<div className="space-y-6">
|
| 293 |
+
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
|
| 294 |
+
<h3 className="text-lg font-semibold text-gray-800 mb-4">Install as App</h3>
|
| 295 |
+
<p className="text-sm text-gray-600 mb-4">
|
| 296 |
+
Install this application on your device for quick access and offline support.
|
| 297 |
+
</p>
|
| 298 |
+
{isInstallable ? (
|
| 299 |
+
<button
|
| 300 |
+
onClick={handleInstallClick}
|
| 301 |
+
className="px-6 py-3 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-2 font-medium"
|
| 302 |
+
>
|
| 303 |
+
<Download size={20} />
|
| 304 |
+
Install App
|
| 305 |
+
</button>
|
| 306 |
+
) : (
|
| 307 |
+
<div className="text-sm text-gray-500 bg-gray-50 p-4 rounded-lg border border-gray-200">
|
| 308 |
+
App is already installed or not available for installation on this device.
|
| 309 |
+
</div>
|
| 310 |
+
)}
|
| 311 |
+
</div>
|
| 312 |
+
|
| 313 |
+
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
|
| 314 |
+
<h3 className="text-lg font-semibold text-gray-800 mb-4">Alert Configuration</h3>
|
| 315 |
+
<div className="space-y-4">
|
| 316 |
+
<div>
|
| 317 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">Low Stock Threshold (Bags / Poti)</label>
|
| 318 |
+
<input
|
| 319 |
+
type="number"
|
| 320 |
+
value={config.lowStockBagsThreshold}
|
| 321 |
+
onChange={(e) =>
|
| 322 |
+
setConfig({
|
| 323 |
+
...config,
|
| 324 |
+
lowStockBagsThreshold: Number(e.target.value) || 0,
|
| 325 |
+
})
|
| 326 |
+
}
|
| 327 |
+
className="w-full border border-gray-300 rounded-lg p-2"
|
| 328 |
+
/>
|
| 329 |
+
</div>
|
| 330 |
+
|
| 331 |
+
<div className="flex items-center gap-3">
|
| 332 |
+
<input
|
| 333 |
+
type="checkbox"
|
| 334 |
+
checked={config.enableNotifications}
|
| 335 |
+
onChange={(e) => setConfig({ ...config, enableNotifications: e.target.checked })}
|
| 336 |
+
className="w-4 h-4 text-teal-600 rounded"
|
| 337 |
+
/>
|
| 338 |
+
<label className="text-sm text-gray-700">Enable Notifications</label>
|
| 339 |
+
</div>
|
| 340 |
+
<button
|
| 341 |
+
onClick={handleSaveAlertConfig}
|
| 342 |
+
className="bg-gray-800 text-white px-4 py-2 rounded text-sm hover:bg-gray-900"
|
| 343 |
+
>
|
| 344 |
+
Save Config
|
| 345 |
+
</button>
|
| 346 |
+
</div>
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
)}
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
);
|
| 353 |
+
};
|
| 354 |
+
|
| 355 |
+
export default Settings;
|
pages/StockReport.tsx
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useMemo, useState } from 'react';
|
| 2 |
+
|
| 3 |
+
import { getActiveLots, getParties, getTransactions } from '../services/db';
|
| 4 |
+
import { BillType, Lot, Party, Transaction } from '../types';
|
| 5 |
+
import { Package, Search, ArrowLeft } from 'lucide-react';
|
| 6 |
+
|
| 7 |
+
const StockReport = () => {
|
| 8 |
+
const [lots, setLots] = useState<Lot[]>([]);
|
| 9 |
+
const [parties, setParties] = useState<Party[]>([]);
|
| 10 |
+
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
| 11 |
+
const [searchTerm, setSearchTerm] = useState('');
|
| 12 |
+
const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
|
| 13 |
+
const [selectedMirchiId, setSelectedMirchiId] = useState<string | null>(null);
|
| 14 |
+
const [selectedMirchiName, setSelectedMirchiName] = useState<string | null>(null);
|
| 15 |
+
const [lowStockBagsThreshold, setLowStockBagsThreshold] = useState<number>(10);
|
| 16 |
+
|
| 17 |
+
useEffect(() => {
|
| 18 |
+
const fetch = async () => {
|
| 19 |
+
setLots(await getActiveLots());
|
| 20 |
+
setParties(await getParties());
|
| 21 |
+
setTransactions(await getTransactions());
|
| 22 |
+
};
|
| 23 |
+
fetch();
|
| 24 |
+
}, []);
|
| 25 |
+
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
try {
|
| 28 |
+
const stored = localStorage.getItem('pp_alert_config');
|
| 29 |
+
if (!stored) return;
|
| 30 |
+
const parsed = JSON.parse(stored);
|
| 31 |
+
if (
|
| 32 |
+
parsed &&
|
| 33 |
+
typeof parsed.lowStockBagsThreshold === 'number' &&
|
| 34 |
+
!Number.isNaN(parsed.lowStockBagsThreshold)
|
| 35 |
+
) {
|
| 36 |
+
setLowStockBagsThreshold(parsed.lowStockBagsThreshold);
|
| 37 |
+
}
|
| 38 |
+
} catch (error) {
|
| 39 |
+
console.error('Error loading alert config', error);
|
| 40 |
+
}
|
| 41 |
+
}, []);
|
| 42 |
+
|
| 43 |
+
const filteredLots = useMemo(
|
| 44 |
+
() =>
|
| 45 |
+
lots.filter((l) =>
|
| 46 |
+
l.mirchi_name.toLowerCase().includes(searchTerm.toLowerCase())
|
| 47 |
+
),
|
| 48 |
+
[lots, searchTerm]
|
| 49 |
+
);
|
| 50 |
+
|
| 51 |
+
const aggregatedByLot = useMemo(() => {
|
| 52 |
+
// Calculate bag count for each lot from transactions
|
| 53 |
+
return filteredLots.map(lot => {
|
| 54 |
+
let totalBags = 0;
|
| 55 |
+
|
| 56 |
+
// Calculate bags from all transactions for this lot
|
| 57 |
+
transactions.forEach(tx => {
|
| 58 |
+
tx.items.forEach(item => {
|
| 59 |
+
if (item.lot_id === lot.id) {
|
| 60 |
+
if (tx.bill_type === BillType.AWAAK) {
|
| 61 |
+
// Purchase: Add bags
|
| 62 |
+
totalBags += tx.is_return ? -item.poti_count : item.poti_count;
|
| 63 |
+
} else {
|
| 64 |
+
// Sale: Subtract bags
|
| 65 |
+
totalBags += tx.is_return ? item.poti_count : -item.poti_count;
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
});
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
return {
|
| 72 |
+
lotId: lot.id,
|
| 73 |
+
lotNumber: lot.lot_number,
|
| 74 |
+
mirchiName: lot.mirchi_name,
|
| 75 |
+
totalQuantity: lot.total_quantity,
|
| 76 |
+
remainingQty: lot.remaining_quantity,
|
| 77 |
+
totalBags: Math.max(0, totalBags),
|
| 78 |
+
status: lot.status,
|
| 79 |
+
purchaseDate: lot.purchase_date,
|
| 80 |
+
avgRate: lot.avg_rate
|
| 81 |
+
};
|
| 82 |
+
});
|
| 83 |
+
}, [filteredLots, transactions]);
|
| 84 |
+
|
| 85 |
+
const detailMovements = useMemo(() => {
|
| 86 |
+
if (!selectedMirchiId) return [];
|
| 87 |
+
|
| 88 |
+
const rows: {
|
| 89 |
+
id: string;
|
| 90 |
+
date: string;
|
| 91 |
+
billNo: string;
|
| 92 |
+
partyName: string;
|
| 93 |
+
inQty: number;
|
| 94 |
+
outQty: number;
|
| 95 |
+
inBags: number;
|
| 96 |
+
outBags: number;
|
| 97 |
+
typeLabel: string;
|
| 98 |
+
isReturn: boolean;
|
| 99 |
+
}[] = [];
|
| 100 |
+
|
| 101 |
+
transactions.forEach((tx) => {
|
| 102 |
+
tx.items
|
| 103 |
+
.filter((item) => item.lot_id === selectedMirchiId) // Filter by lot_id instead of mirchi_type_id
|
| 104 |
+
.forEach((item) => {
|
| 105 |
+
const party = parties.find((p) => p.id === tx.party_id);
|
| 106 |
+
|
| 107 |
+
let inQty = 0;
|
| 108 |
+
let outQty = 0;
|
| 109 |
+
let inBags = 0;
|
| 110 |
+
let outBags = 0;
|
| 111 |
+
let typeLabel = '';
|
| 112 |
+
|
| 113 |
+
if (tx.bill_type === BillType.AWAAK) {
|
| 114 |
+
// Awaak = Purchase / Stock IN
|
| 115 |
+
if (tx.is_return) {
|
| 116 |
+
// Purchase Return: Stock OUT
|
| 117 |
+
outQty = item.net_weight;
|
| 118 |
+
outBags = item.poti_count;
|
| 119 |
+
typeLabel = 'Purchase Return';
|
| 120 |
+
} else {
|
| 121 |
+
inQty = item.net_weight;
|
| 122 |
+
inBags = item.poti_count;
|
| 123 |
+
typeLabel = 'Purchase';
|
| 124 |
+
}
|
| 125 |
+
} else {
|
| 126 |
+
// Jawaak = Sales / Stock OUT
|
| 127 |
+
if (tx.is_return) {
|
| 128 |
+
// Sales Return: Stock IN
|
| 129 |
+
inQty = item.net_weight;
|
| 130 |
+
inBags = item.poti_count;
|
| 131 |
+
typeLabel = 'Sales Return';
|
| 132 |
+
} else {
|
| 133 |
+
outQty = item.net_weight;
|
| 134 |
+
outBags = item.poti_count;
|
| 135 |
+
typeLabel = 'Sale';
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
const displayDate = tx.bill_date ? tx.bill_date.split('T')[0] : tx.bill_date;
|
| 140 |
+
|
| 141 |
+
rows.push({
|
| 142 |
+
id: `${tx.id}-${item.id}`,
|
| 143 |
+
date: displayDate,
|
| 144 |
+
billNo: tx.bill_number,
|
| 145 |
+
partyName: party?.name || tx.party_name || 'Unknown Party',
|
| 146 |
+
inQty,
|
| 147 |
+
outQty,
|
| 148 |
+
inBags,
|
| 149 |
+
outBags,
|
| 150 |
+
typeLabel,
|
| 151 |
+
isReturn: tx.is_return,
|
| 152 |
+
});
|
| 153 |
+
});
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
// Sort latest first
|
| 157 |
+
rows.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
| 158 |
+
return rows;
|
| 159 |
+
}, [selectedMirchiId, transactions, parties]);
|
| 160 |
+
|
| 161 |
+
return (
|
| 162 |
+
<div className="space-y-4">
|
| 163 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
| 164 |
+
<div className="p-4 md:p-6 border-b flex flex-col md:flex-row md:items-center justify-between gap-4">
|
| 165 |
+
<div className="flex items-center gap-2">
|
| 166 |
+
{viewMode === 'detail' && (
|
| 167 |
+
<button
|
| 168 |
+
onClick={() => {
|
| 169 |
+
setViewMode('list');
|
| 170 |
+
setSelectedMirchiId(null);
|
| 171 |
+
setSelectedMirchiName(null);
|
| 172 |
+
}}
|
| 173 |
+
className="mr-1 p-1 hover:bg-gray-100 rounded-full"
|
| 174 |
+
>
|
| 175 |
+
<ArrowLeft size={20} className="text-gray-600" />
|
| 176 |
+
</button>
|
| 177 |
+
)}
|
| 178 |
+
<Package className="text-teal-600" />
|
| 179 |
+
<h2 className="text-lg md:text-xl font-bold text-gray-800">
|
| 180 |
+
{viewMode === 'list'
|
| 181 |
+
? 'स्टॉक रिपोर्ट (Stock Inventory)'
|
| 182 |
+
: `${selectedMirchiName ?? ''} - Stock Detail`}
|
| 183 |
+
</h2>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
{viewMode === 'list' && (
|
| 187 |
+
<div className="relative w-full md:w-auto">
|
| 188 |
+
<Search
|
| 189 |
+
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
|
| 190 |
+
size={18}
|
| 191 |
+
/>
|
| 192 |
+
<input
|
| 193 |
+
type="text"
|
| 194 |
+
placeholder="Search Mirchi Jaat / Type..."
|
| 195 |
+
className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 outline-none w-full md:w-64 text-sm"
|
| 196 |
+
value={searchTerm}
|
| 197 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 198 |
+
/>
|
| 199 |
+
</div>
|
| 200 |
+
)}
|
| 201 |
+
</div>
|
| 202 |
+
|
| 203 |
+
{viewMode === 'list' ? (
|
| 204 |
+
<>
|
| 205 |
+
{/* Desktop table */}
|
| 206 |
+
<div className="hidden md:block overflow-x-auto">
|
| 207 |
+
<table className="w-full text-sm text-left">
|
| 208 |
+
<thead className="bg-gray-50 text-gray-500 font-medium">
|
| 209 |
+
<tr>
|
| 210 |
+
<th className="px-6 py-4">LOT Number</th>
|
| 211 |
+
<th className="px-6 py-4">मिरची जात (Mirchi Type)</th>
|
| 212 |
+
<th className="px-6 py-4 text-right">बॅग/पोती (Bags)</th>
|
| 213 |
+
<th className="px-6 py-4 text-right">शिल्लक वजन (Remaining Qty)</th>
|
| 214 |
+
<th className="px-6 py-4 text-center">स्थिती (Status)</th>
|
| 215 |
+
</tr>
|
| 216 |
+
</thead>
|
| 217 |
+
<tbody className="divide-y divide-gray-100">
|
| 218 |
+
{aggregatedByLot.length === 0 ? (
|
| 219 |
+
<tr>
|
| 220 |
+
<td
|
| 221 |
+
colSpan={5}
|
| 222 |
+
className="text-center py-8 text-gray-500"
|
| 223 |
+
>
|
| 224 |
+
No active stock found
|
| 225 |
+
</td>
|
| 226 |
+
</tr>
|
| 227 |
+
) : (
|
| 228 |
+
aggregatedByLot.map((row) => {
|
| 229 |
+
const isLow = row.totalBags > 0 && row.totalBags <= lowStockBagsThreshold;
|
| 230 |
+
const statusLabel =
|
| 231 |
+
row.remainingQty === 0
|
| 232 |
+
? 'Stockout'
|
| 233 |
+
: isLow
|
| 234 |
+
? 'Low Stock'
|
| 235 |
+
: 'Available';
|
| 236 |
+
const statusClasses =
|
| 237 |
+
row.remainingQty === 0
|
| 238 |
+
? 'bg-red-100 text-red-700'
|
| 239 |
+
: isLow
|
| 240 |
+
? 'bg-orange-100 text-orange-700'
|
| 241 |
+
: 'bg-green-100 text-green-700';
|
| 242 |
+
|
| 243 |
+
return (
|
| 244 |
+
<tr
|
| 245 |
+
key={row.lotId}
|
| 246 |
+
className="hover:bg-gray-50 transition-colors cursor-pointer"
|
| 247 |
+
onClick={() => {
|
| 248 |
+
setSelectedMirchiId(row.lotId);
|
| 249 |
+
setSelectedMirchiName(row.lotNumber);
|
| 250 |
+
setViewMode('detail');
|
| 251 |
+
}}
|
| 252 |
+
>
|
| 253 |
+
<td className="px-6 py-4 font-mono text-sm font-medium text-teal-700">
|
| 254 |
+
{row.lotNumber}
|
| 255 |
+
</td>
|
| 256 |
+
<td className="px-6 py-4">
|
| 257 |
+
{row.mirchiName}
|
| 258 |
+
</td>
|
| 259 |
+
<td className="px-6 py-4 text-right">
|
| 260 |
+
<span className="font-medium text-gray-700">
|
| 261 |
+
{row.totalBags} bags
|
| 262 |
+
</span>
|
| 263 |
+
</td>
|
| 264 |
+
<td className="px-6 py-4 text-right">
|
| 265 |
+
<span
|
| 266 |
+
className={`font-bold ${isLow ? 'text-red-500' : 'text-gray-800'
|
| 267 |
+
}`}
|
| 268 |
+
>
|
| 269 |
+
{row.remainingQty} kg
|
| 270 |
+
</span>
|
| 271 |
+
</td>
|
| 272 |
+
<td className="px-6 py-4 text-center">
|
| 273 |
+
<span
|
| 274 |
+
className={`inline-block px-2 py-1 rounded-full text-xs font-semibold ${statusClasses}`}
|
| 275 |
+
>
|
| 276 |
+
{statusLabel}
|
| 277 |
+
</span>
|
| 278 |
+
</td>
|
| 279 |
+
</tr>
|
| 280 |
+
);
|
| 281 |
+
})
|
| 282 |
+
)}
|
| 283 |
+
</tbody>
|
| 284 |
+
</table>
|
| 285 |
+
</div>
|
| 286 |
+
|
| 287 |
+
{/* Mobile cards */}
|
| 288 |
+
<div className="md:hidden flex flex-col divide-y divide-gray-100">
|
| 289 |
+
{aggregatedByLot.length === 0 ? (
|
| 290 |
+
<div className="p-6 text-center text-gray-500 text-sm">
|
| 291 |
+
No active stock found
|
| 292 |
+
</div>
|
| 293 |
+
) : (
|
| 294 |
+
aggregatedByLot.map((row) => {
|
| 295 |
+
const isLow = row.totalBags > 0 && row.totalBags <= lowStockBagsThreshold;
|
| 296 |
+
const statusLabel =
|
| 297 |
+
row.remainingQty === 0
|
| 298 |
+
? 'Stockout'
|
| 299 |
+
: isLow
|
| 300 |
+
? 'Low Stock'
|
| 301 |
+
: 'Available';
|
| 302 |
+
const statusClasses =
|
| 303 |
+
row.remainingQty === 0
|
| 304 |
+
? 'bg-red-100 text-red-700'
|
| 305 |
+
: isLow
|
| 306 |
+
? 'bg-orange-100 text-orange-700'
|
| 307 |
+
: 'bg-green-100 text-green-700';
|
| 308 |
+
|
| 309 |
+
return (
|
| 310 |
+
<button
|
| 311 |
+
key={row.lotId}
|
| 312 |
+
className="text-left p-4 space-y-2 hover:bg-gray-50 transition-colors"
|
| 313 |
+
onClick={() => {
|
| 314 |
+
setSelectedMirchiId(row.lotId);
|
| 315 |
+
setSelectedMirchiName(row.lotNumber);
|
| 316 |
+
setViewMode('detail');
|
| 317 |
+
}}
|
| 318 |
+
>
|
| 319 |
+
<div className="flex items-center justify-between gap-2">
|
| 320 |
+
<div>
|
| 321 |
+
<div className="text-sm font-mono font-semibold text-teal-700">
|
| 322 |
+
{row.lotNumber}
|
| 323 |
+
</div>
|
| 324 |
+
<div className="text-xs text-gray-500">
|
| 325 |
+
{row.mirchiName}
|
| 326 |
+
</div>
|
| 327 |
+
</div>
|
| 328 |
+
<span
|
| 329 |
+
className={`px-2 py-1 rounded-full text-[10px] font-semibold ${statusClasses}`}
|
| 330 |
+
>
|
| 331 |
+
{statusLabel}
|
| 332 |
+
</span>
|
| 333 |
+
</div>
|
| 334 |
+
<div className="grid grid-cols-2 gap-2 text-xs">
|
| 335 |
+
<div className="bg-gray-50 p-2 rounded">
|
| 336 |
+
<div className="text-gray-500">Bags</div>
|
| 337 |
+
<div className="font-medium">{row.totalBags} bags</div>
|
| 338 |
+
</div>
|
| 339 |
+
<div className="bg-gray-50 p-2 rounded">
|
| 340 |
+
<div className="text-gray-500">Remaining Qty</div>
|
| 341 |
+
<div
|
| 342 |
+
className={`font-bold ${isLow ? 'text-red-500' : 'text-gray-800'
|
| 343 |
+
}`}
|
| 344 |
+
>
|
| 345 |
+
{row.remainingQty} kg
|
| 346 |
+
</div>
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
</button>
|
| 350 |
+
);
|
| 351 |
+
})
|
| 352 |
+
)}
|
| 353 |
+
</div>
|
| 354 |
+
</>
|
| 355 |
+
) : (
|
| 356 |
+
<>
|
| 357 |
+
{/* Desktop detail table */}
|
| 358 |
+
<div className="hidden md:block overflow-x-auto">
|
| 359 |
+
<table className="w-full text-sm text-left">
|
| 360 |
+
<thead className="bg-gray-50 text-gray-500 font-medium">
|
| 361 |
+
<tr>
|
| 362 |
+
<th className="px-6 py-4">तारीख (Date)</th>
|
| 363 |
+
<th className="px-6 py-4">बिल नंबर (Bill No)</th>
|
| 364 |
+
<th className="px-6 py-4">पार्टी (Party)</th>
|
| 365 |
+
<th className="px-6 py-4 text-right">बॅग इन (In Bags)</th>
|
| 366 |
+
<th className="px-6 py-4 text-right">बॅग आउट (Out Bags)</th>
|
| 367 |
+
<th className="px-6 py-4 text-right">माल इन (In Qty)</th>
|
| 368 |
+
<th className="px-6 py-4 text-right">माल आउट (Out Qty)</th>
|
| 369 |
+
<th className="px-6 py-4 text-center">टाइप (Type)</th>
|
| 370 |
+
</tr>
|
| 371 |
+
</thead>
|
| 372 |
+
<tbody className="divide-y divide-gray-100">
|
| 373 |
+
{detailMovements.length === 0 ? (
|
| 374 |
+
<tr>
|
| 375 |
+
<td colSpan={8} className="px-6 py-8 text-center text-gray-500">
|
| 376 |
+
No movements found for this Mirchi type.
|
| 377 |
+
</td>
|
| 378 |
+
</tr>
|
| 379 |
+
) : (
|
| 380 |
+
detailMovements.map((row) => (
|
| 381 |
+
<tr key={row.id} className="hover:bg-gray-50">
|
| 382 |
+
<td className="px-6 py-4 text-gray-600">{row.date}</td>
|
| 383 |
+
<td className="px-6 py-4 font-mono text-gray-500">{row.billNo}</td>
|
| 384 |
+
<td className="px-6 py-4 text-gray-700">{row.partyName}</td>
|
| 385 |
+
<td className="px-6 py-4 text-right text-green-600 font-medium">
|
| 386 |
+
{row.inBags > 0 ? `${row.inBags} bags` : '-'}
|
| 387 |
+
</td>
|
| 388 |
+
<td className="px-6 py-4 text-right text-red-600 font-medium">
|
| 389 |
+
{row.outBags > 0 ? `${row.outBags} bags` : '-'}
|
| 390 |
+
</td>
|
| 391 |
+
<td className="px-6 py-4 text-right text-green-600 font-medium">
|
| 392 |
+
{row.inQty > 0 ? `${row.inQty} kg` : '-'}
|
| 393 |
+
</td>
|
| 394 |
+
<td className="px-6 py-4 text-right text-red-600 font-medium">
|
| 395 |
+
{row.outQty > 0 ? `${row.outQty} kg` : '-'}
|
| 396 |
+
</td>
|
| 397 |
+
<td className="px-6 py-4 text-center">
|
| 398 |
+
<span
|
| 399 |
+
className={`inline-block px-2 py-1 rounded-full text-xs font-semibold ${row.typeLabel.includes('Return')
|
| 400 |
+
? 'bg-orange-100 text-orange-700'
|
| 401 |
+
: row.typeLabel === 'Purchase'
|
| 402 |
+
? 'bg-teal-100 text-teal-700'
|
| 403 |
+
: 'bg-blue-100 text-blue-700'
|
| 404 |
+
}`}
|
| 405 |
+
>
|
| 406 |
+
{row.typeLabel}
|
| 407 |
+
</span>
|
| 408 |
+
</td>
|
| 409 |
+
</tr>
|
| 410 |
+
))
|
| 411 |
+
)}
|
| 412 |
+
</tbody>
|
| 413 |
+
</table>
|
| 414 |
+
</div>
|
| 415 |
+
|
| 416 |
+
{/* Mobile detail cards */}
|
| 417 |
+
<div className="md:hidden flex flex-col divide-y divide-gray-100">
|
| 418 |
+
{detailMovements.length === 0 ? (
|
| 419 |
+
<div className="p-6 text-center text-gray-500 text-sm">
|
| 420 |
+
No movements found for this Mirchi type.
|
| 421 |
+
</div>
|
| 422 |
+
) : (
|
| 423 |
+
detailMovements.map((row) => (
|
| 424 |
+
<div key={row.id} className="p-4 space-y-2">
|
| 425 |
+
<div className="flex justify-between items-start gap-2">
|
| 426 |
+
<div>
|
| 427 |
+
<div className="text-xs text-gray-500">{row.date}</div>
|
| 428 |
+
<div className="font-mono text-sm font-medium text-gray-800">
|
| 429 |
+
{row.billNo}
|
| 430 |
+
</div>
|
| 431 |
+
<div className="text-xs text-gray-600 mt-1">
|
| 432 |
+
{row.partyName}
|
| 433 |
+
</div>
|
| 434 |
+
</div>
|
| 435 |
+
<span
|
| 436 |
+
className={`px-2 py-1 rounded-full text-[10px] font-semibold ${row.typeLabel.includes('Return')
|
| 437 |
+
? 'bg-orange-100 text-orange-700'
|
| 438 |
+
: row.typeLabel === 'Purchase'
|
| 439 |
+
? 'bg-teal-100 text-teal-700'
|
| 440 |
+
: 'bg-blue-100 text-blue-700'
|
| 441 |
+
}`}
|
| 442 |
+
>
|
| 443 |
+
{row.typeLabel}
|
| 444 |
+
</span>
|
| 445 |
+
</div>
|
| 446 |
+
<div className="grid grid-cols-2 gap-2 text-xs pt-2 border-t border-gray-50">
|
| 447 |
+
<div className="bg-gray-50 p-2 rounded">
|
| 448 |
+
<div className="text-gray-500">माल इन (In)</div>
|
| 449 |
+
<div className="font-medium text-green-700">
|
| 450 |
+
{row.inQty > 0 ? `${row.inQty} kg` : '-'}
|
| 451 |
+
</div>
|
| 452 |
+
</div>
|
| 453 |
+
<div className="bg-gray-50 p-2 rounded">
|
| 454 |
+
<div className="text-gray-500">माल आउट (Out)</div>
|
| 455 |
+
<div className="font-medium text-red-600">
|
| 456 |
+
{row.outQty > 0 ? `${row.outQty} kg` : '-'}
|
| 457 |
+
</div>
|
| 458 |
+
</div>
|
| 459 |
+
</div>
|
| 460 |
+
</div>
|
| 461 |
+
))
|
| 462 |
+
)}
|
| 463 |
+
</div>
|
| 464 |
+
</>
|
| 465 |
+
)}
|
| 466 |
+
</div>
|
| 467 |
+
</div>
|
| 468 |
+
);
|
| 469 |
+
};
|
| 470 |
+
|
| 471 |
+
export default StockReport;
|
public/icon-192.png
ADDED
|
|
Git LFS Details
|
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,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const CACHE_NAME = 'pattanshetty-v2';
|
| 2 |
+
const urlsToCache = [
|
| 3 |
+
'/',
|
| 4 |
+
'/index.html',
|
| 5 |
+
];
|
| 6 |
+
|
| 7 |
+
// Install event - cache resources
|
| 8 |
+
self.addEventListener('install', (event) => {
|
| 9 |
+
console.log('[ServiceWorker] Installing...');
|
| 10 |
+
event.waitUntil(
|
| 11 |
+
caches.open(CACHE_NAME)
|
| 12 |
+
.then((cache) => {
|
| 13 |
+
console.log('[ServiceWorker] Caching app shell');
|
| 14 |
+
return cache.addAll(urlsToCache).catch((err) => {
|
| 15 |
+
console.error('[ServiceWorker] Cache addAll failed:', err);
|
| 16 |
+
});
|
| 17 |
+
})
|
| 18 |
+
.catch((err) => {
|
| 19 |
+
console.error('[ServiceWorker] Cache open failed:', err);
|
| 20 |
+
})
|
| 21 |
+
);
|
| 22 |
+
self.skipWaiting();
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
// Activate event - clean up old caches
|
| 26 |
+
self.addEventListener('activate', (event) => {
|
| 27 |
+
console.log('[ServiceWorker] Activating...');
|
| 28 |
+
event.waitUntil(
|
| 29 |
+
caches.keys().then((cacheNames) => {
|
| 30 |
+
return Promise.all(
|
| 31 |
+
cacheNames.map((cacheName) => {
|
| 32 |
+
if (cacheName !== CACHE_NAME) {
|
| 33 |
+
console.log('[ServiceWorker] Deleting old cache:', cacheName);
|
| 34 |
+
return caches.delete(cacheName);
|
| 35 |
+
}
|
| 36 |
+
})
|
| 37 |
+
);
|
| 38 |
+
}).catch((err) => {
|
| 39 |
+
console.error('[ServiceWorker] Activation failed:', err);
|
| 40 |
+
})
|
| 41 |
+
);
|
| 42 |
+
self.clients.claim();
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
// Fetch event - Network first, fallback to cache
|
| 46 |
+
self.addEventListener('fetch', (event) => {
|
| 47 |
+
// Skip non-GET requests
|
| 48 |
+
if (event.request.method !== 'GET') {
|
| 49 |
+
return;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
event.respondWith(
|
| 53 |
+
fetch(event.request)
|
| 54 |
+
.then((response) => {
|
| 55 |
+
// Check if valid response
|
| 56 |
+
if (!response || response.status !== 200 || response.type === 'error') {
|
| 57 |
+
return response;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Clone the response
|
| 61 |
+
const responseToCache = response.clone();
|
| 62 |
+
|
| 63 |
+
caches.open(CACHE_NAME)
|
| 64 |
+
.then((cache) => {
|
| 65 |
+
cache.put(event.request, responseToCache);
|
| 66 |
+
})
|
| 67 |
+
.catch((err) => {
|
| 68 |
+
console.error('[ServiceWorker] Cache put failed:', err);
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
return response;
|
| 72 |
+
})
|
| 73 |
+
.catch((err) => {
|
| 74 |
+
console.log('[ServiceWorker] Fetch failed, trying cache:', err);
|
| 75 |
+
// Network failed, try cache
|
| 76 |
+
return caches.match(event.request)
|
| 77 |
+
.then((cachedResponse) => {
|
| 78 |
+
if (cachedResponse) {
|
| 79 |
+
return cachedResponse;
|
| 80 |
+
}
|
| 81 |
+
// Return offline page or error
|
| 82 |
+
return new Response('Offline - No cached version available', {
|
| 83 |
+
status: 503,
|
| 84 |
+
statusText: 'Service Unavailable',
|
| 85 |
+
headers: new Headers({
|
| 86 |
+
'Content-Type': 'text/plain'
|
| 87 |
+
})
|
| 88 |
+
});
|
| 89 |
+
});
|
| 90 |
+
})
|
| 91 |
+
);
|
| 92 |
+
});
|
services/db.ts
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
Party, MirchiType, Lot, Transaction, PartyType,
|
| 3 |
+
LotStatus, BillType, ApiResponse, PaymentMode
|
| 4 |
+
} from '../types';
|
| 5 |
+
|
| 6 |
+
const API_BASE = 'http://localhost:4000/api';
|
| 7 |
+
|
| 8 |
+
// Helper function to parse numeric values from API
|
| 9 |
+
const parseNumeric = (value: any): number => {
|
| 10 |
+
if (typeof value === 'string') {
|
| 11 |
+
return parseFloat(value);
|
| 12 |
+
}
|
| 13 |
+
return value || 0;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
// Helper function to parse poti_weights from string to array
|
| 17 |
+
const parsePotiWeights = (weights: any): number[] => {
|
| 18 |
+
if (typeof weights === 'string') {
|
| 19 |
+
try {
|
| 20 |
+
return JSON.parse(weights);
|
| 21 |
+
} catch {
|
| 22 |
+
return [];
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
return weights || [];
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
// Helper function to normalize transaction data from API
|
| 29 |
+
const normalizeTransaction = (tx: any): Transaction => {
|
| 30 |
+
// Filter out null items that come from PostgreSQL json_agg when there are no items
|
| 31 |
+
const rawItems = tx.items || [];
|
| 32 |
+
const validItems = Array.isArray(rawItems)
|
| 33 |
+
? rawItems.filter((item: any) => item && item.id !== null)
|
| 34 |
+
: [];
|
| 35 |
+
|
| 36 |
+
const grossWeightTotal = parseNumeric(tx.gross_weight_total);
|
| 37 |
+
const netWeightTotal = parseNumeric(tx.net_weight_total);
|
| 38 |
+
const subtotal = parseNumeric(tx.subtotal);
|
| 39 |
+
const totalExpenses = parseNumeric(tx.total_expenses);
|
| 40 |
+
const totalAmount = parseNumeric(tx.total_amount);
|
| 41 |
+
const paidAmount = parseNumeric(tx.paid_amount);
|
| 42 |
+
const balanceAmount = parseNumeric(tx.balance_amount);
|
| 43 |
+
|
| 44 |
+
// Normalize expenses and reconstruct other_expenses if backend didn't send it
|
| 45 |
+
let normalizedExpenses;
|
| 46 |
+
if (tx.expenses) {
|
| 47 |
+
const hasOtherExpensesField = Object.prototype.hasOwnProperty.call(tx.expenses, 'other_expenses');
|
| 48 |
+
|
| 49 |
+
const baseExpenses = {
|
| 50 |
+
cess_percent: parseNumeric(tx.expenses.cess_percent),
|
| 51 |
+
cess_amount: parseNumeric(tx.expenses.cess_amount),
|
| 52 |
+
adat_percent: parseNumeric(tx.expenses.adat_percent),
|
| 53 |
+
adat_amount: parseNumeric(tx.expenses.adat_amount),
|
| 54 |
+
poti_rate: parseNumeric(tx.expenses.poti_rate),
|
| 55 |
+
poti_amount: parseNumeric(tx.expenses.poti_amount),
|
| 56 |
+
hamali_per_poti: parseNumeric(tx.expenses.hamali_per_poti),
|
| 57 |
+
hamali_amount: parseNumeric(tx.expenses.hamali_amount),
|
| 58 |
+
packaging_hamali_per_poti: parseNumeric(tx.expenses.packaging_hamali_per_poti),
|
| 59 |
+
packaging_hamali_amount: parseNumeric(tx.expenses.packaging_hamali_amount),
|
| 60 |
+
gaadi_bharni: parseNumeric(tx.expenses.gaadi_bharni),
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
let otherExpensesValue = hasOtherExpensesField
|
| 64 |
+
? parseNumeric(tx.expenses.other_expenses)
|
| 65 |
+
: 0;
|
| 66 |
+
|
| 67 |
+
// If backend doesn't provide other_expenses but total_expenses is known,
|
| 68 |
+
// infer other_expenses as the remaining part after subtracting known components.
|
| 69 |
+
if (!hasOtherExpensesField && totalExpenses) {
|
| 70 |
+
const knownComponents =
|
| 71 |
+
baseExpenses.poti_amount +
|
| 72 |
+
baseExpenses.cess_amount +
|
| 73 |
+
baseExpenses.adat_amount +
|
| 74 |
+
baseExpenses.hamali_amount +
|
| 75 |
+
baseExpenses.packaging_hamali_amount +
|
| 76 |
+
baseExpenses.gaadi_bharni;
|
| 77 |
+
|
| 78 |
+
const diff = totalExpenses - knownComponents;
|
| 79 |
+
if (diff > 0) {
|
| 80 |
+
otherExpensesValue = diff;
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
normalizedExpenses = {
|
| 85 |
+
...baseExpenses,
|
| 86 |
+
other_expenses: otherExpensesValue,
|
| 87 |
+
};
|
| 88 |
+
} else {
|
| 89 |
+
normalizedExpenses = {
|
| 90 |
+
cess_percent: 0,
|
| 91 |
+
cess_amount: 0,
|
| 92 |
+
adat_percent: 0,
|
| 93 |
+
adat_amount: 0,
|
| 94 |
+
poti_rate: 0,
|
| 95 |
+
poti_amount: 0,
|
| 96 |
+
hamali_per_poti: 0,
|
| 97 |
+
hamali_amount: 0,
|
| 98 |
+
packaging_hamali_per_poti: 0,
|
| 99 |
+
packaging_hamali_amount: 0,
|
| 100 |
+
gaadi_bharni: 0,
|
| 101 |
+
other_expenses: 0,
|
| 102 |
+
};
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
return {
|
| 106 |
+
...tx,
|
| 107 |
+
gross_weight_total: grossWeightTotal,
|
| 108 |
+
net_weight_total: netWeightTotal,
|
| 109 |
+
subtotal,
|
| 110 |
+
total_expenses: totalExpenses,
|
| 111 |
+
total_amount: totalAmount,
|
| 112 |
+
paid_amount: paidAmount,
|
| 113 |
+
balance_amount: balanceAmount,
|
| 114 |
+
items: validItems.map((item: any) => ({
|
| 115 |
+
...item,
|
| 116 |
+
poti_weights: parsePotiWeights(item.poti_weights),
|
| 117 |
+
gross_weight: parseNumeric(item.gross_weight),
|
| 118 |
+
poti_count: parseNumeric(item.poti_count),
|
| 119 |
+
total_potya: parseNumeric(item.total_potya),
|
| 120 |
+
net_weight: parseNumeric(item.net_weight),
|
| 121 |
+
rate_per_kg: parseNumeric(item.rate_per_kg),
|
| 122 |
+
item_total: parseNumeric(item.item_total),
|
| 123 |
+
})),
|
| 124 |
+
expenses: normalizedExpenses,
|
| 125 |
+
payments: (tx.payments || []).map((p: any) => ({
|
| 126 |
+
...p,
|
| 127 |
+
amount: parseNumeric(p.amount),
|
| 128 |
+
})),
|
| 129 |
+
};
|
| 130 |
+
};
|
| 131 |
+
|
| 132 |
+
// Parties API
|
| 133 |
+
export const getParties = async (): Promise<Party[]> => {
|
| 134 |
+
try {
|
| 135 |
+
const res = await fetch(`${API_BASE}/parties`);
|
| 136 |
+
if (!res.ok) throw new Error('Failed to load parties');
|
| 137 |
+
const data = await res.json();
|
| 138 |
+
return data.map((p: any) => ({
|
| 139 |
+
...p,
|
| 140 |
+
current_balance: parseNumeric(p.current_balance)
|
| 141 |
+
}));
|
| 142 |
+
} catch (error) {
|
| 143 |
+
console.error('Error fetching parties:', error);
|
| 144 |
+
return [];
|
| 145 |
+
}
|
| 146 |
+
};
|
| 147 |
+
|
| 148 |
+
export const saveParty = async (party: Party): Promise<ApiResponse<Party>> => {
|
| 149 |
+
try {
|
| 150 |
+
const res = await fetch(`${API_BASE}/parties`, {
|
| 151 |
+
method: 'POST',
|
| 152 |
+
headers: { 'Content-Type': 'application/json' },
|
| 153 |
+
body: JSON.stringify(party),
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
const data = await res.json();
|
| 157 |
+
if (!res.ok || !data.success) {
|
| 158 |
+
return { success: false, message: data?.message || 'Error saving party' };
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
return {
|
| 162 |
+
success: true,
|
| 163 |
+
data: {
|
| 164 |
+
...data.data,
|
| 165 |
+
current_balance: parseNumeric(data.data.current_balance)
|
| 166 |
+
},
|
| 167 |
+
message: data.message || 'Party saved successfully'
|
| 168 |
+
};
|
| 169 |
+
} catch (e: any) {
|
| 170 |
+
return { success: false, message: e.message || 'Database error' };
|
| 171 |
+
}
|
| 172 |
+
};
|
| 173 |
+
|
| 174 |
+
// Mirchi Types API
|
| 175 |
+
export const apiGetMirchiTypes = async (): Promise<MirchiType[]> => {
|
| 176 |
+
try {
|
| 177 |
+
const res = await fetch(`${API_BASE}/mirchi-types`);
|
| 178 |
+
if (!res.ok) throw new Error('Failed to load mirchi types');
|
| 179 |
+
const data = await res.json();
|
| 180 |
+
return data.map((m: any) => ({
|
| 181 |
+
...m,
|
| 182 |
+
current_rate: parseNumeric(m.current_rate)
|
| 183 |
+
}));
|
| 184 |
+
} catch (error) {
|
| 185 |
+
console.error('Error fetching mirchi types:', error);
|
| 186 |
+
return [];
|
| 187 |
+
}
|
| 188 |
+
};
|
| 189 |
+
|
| 190 |
+
export const apiSaveMirchiType = async (type: MirchiType): Promise<ApiResponse<MirchiType>> => {
|
| 191 |
+
try {
|
| 192 |
+
const res = await fetch(`${API_BASE}/mirchi-types`, {
|
| 193 |
+
method: 'POST',
|
| 194 |
+
headers: { 'Content-Type': 'application/json' },
|
| 195 |
+
body: JSON.stringify(type),
|
| 196 |
+
});
|
| 197 |
+
const data = await res.json();
|
| 198 |
+
if (!res.ok || !data.success) {
|
| 199 |
+
return { success: false, message: data?.message || 'Error saving type' };
|
| 200 |
+
}
|
| 201 |
+
return {
|
| 202 |
+
success: true,
|
| 203 |
+
data: {
|
| 204 |
+
...data.data,
|
| 205 |
+
current_rate: parseNumeric(data.data.current_rate)
|
| 206 |
+
},
|
| 207 |
+
message: data.message
|
| 208 |
+
};
|
| 209 |
+
} catch (e: any) {
|
| 210 |
+
return { success: false, message: e.message || 'Error saving type' };
|
| 211 |
+
}
|
| 212 |
+
};
|
| 213 |
+
|
| 214 |
+
// Alias for compatibility
|
| 215 |
+
export const getMirchiTypes = apiGetMirchiTypes;
|
| 216 |
+
export const saveMirchiType = apiSaveMirchiType;
|
| 217 |
+
|
| 218 |
+
// Lots API
|
| 219 |
+
export const getActiveLots = async (): Promise<Lot[]> => {
|
| 220 |
+
try {
|
| 221 |
+
const res = await fetch(`${API_BASE}/lots/active`);
|
| 222 |
+
if (!res.ok) throw new Error('Failed to load active lots');
|
| 223 |
+
const data = await res.json();
|
| 224 |
+
return data.map((l: any) => ({
|
| 225 |
+
...l,
|
| 226 |
+
total_quantity: parseNumeric(l.total_quantity),
|
| 227 |
+
remaining_quantity: parseNumeric(l.remaining_quantity),
|
| 228 |
+
avg_rate: parseNumeric(l.avg_rate)
|
| 229 |
+
}));
|
| 230 |
+
} catch (error) {
|
| 231 |
+
console.error('Error fetching active lots:', error);
|
| 232 |
+
return [];
|
| 233 |
+
}
|
| 234 |
+
};
|
| 235 |
+
|
| 236 |
+
// Check if lot number is unique
|
| 237 |
+
export const checkLotUnique = async (lotNumber: string): Promise<boolean> => {
|
| 238 |
+
try {
|
| 239 |
+
const res = await fetch(`${API_BASE}/lots/check-unique`, {
|
| 240 |
+
method: 'POST',
|
| 241 |
+
headers: { 'Content-Type': 'application/json' },
|
| 242 |
+
body: JSON.stringify({ lot_number: lotNumber }),
|
| 243 |
+
});
|
| 244 |
+
if (!res.ok) throw new Error('Failed to check lot uniqueness');
|
| 245 |
+
const data = await res.json();
|
| 246 |
+
return !data.exists; // Return true if unique (not exists)
|
| 247 |
+
} catch (error) {
|
| 248 |
+
console.error('Error checking lot uniqueness:', error);
|
| 249 |
+
return false;
|
| 250 |
+
}
|
| 251 |
+
};
|
| 252 |
+
|
| 253 |
+
// Get available lots for a specific mirchi type
|
| 254 |
+
export const getAvailableLotsByMirchi = async (mirchiTypeId: string): Promise<Lot[]> => {
|
| 255 |
+
try {
|
| 256 |
+
const res = await fetch(`${API_BASE}/lots/available/${mirchiTypeId}`);
|
| 257 |
+
if (!res.ok) throw new Error('Failed to load available lots');
|
| 258 |
+
const data = await res.json();
|
| 259 |
+
return data.map((l: any) => ({
|
| 260 |
+
...l,
|
| 261 |
+
total_quantity: parseNumeric(l.total_quantity),
|
| 262 |
+
remaining_quantity: parseNumeric(l.remaining_quantity),
|
| 263 |
+
avg_rate: parseNumeric(l.avg_rate)
|
| 264 |
+
}));
|
| 265 |
+
} catch (error) {
|
| 266 |
+
console.error('Error fetching available lots:', error);
|
| 267 |
+
return [];
|
| 268 |
+
}
|
| 269 |
+
};
|
| 270 |
+
|
| 271 |
+
// Transactions API
|
| 272 |
+
export const getTransactions = async (): Promise<Transaction[]> => {
|
| 273 |
+
try {
|
| 274 |
+
const res = await fetch(`${API_BASE}/transactions`);
|
| 275 |
+
if (!res.ok) throw new Error('Failed to load transactions');
|
| 276 |
+
const data = await res.json();
|
| 277 |
+
return data.map(normalizeTransaction);
|
| 278 |
+
} catch (error) {
|
| 279 |
+
console.error('Error fetching transactions:', error);
|
| 280 |
+
return [];
|
| 281 |
+
}
|
| 282 |
+
};
|
| 283 |
+
|
| 284 |
+
export const generateBillNumber = (type: BillType, isReturn: boolean = false): string => {
|
| 285 |
+
const prefix = type === BillType.JAWAAK ? (isReturn ? 'JAWAAK-RET' : 'JAWAAK') : (isReturn ? 'AWAAK-RET' : 'AWAAK');
|
| 286 |
+
const timestamp = Date.now();
|
| 287 |
+
return `${prefix}-${new Date().getFullYear()}-${String(timestamp).slice(-4)}`;
|
| 288 |
+
};
|
| 289 |
+
|
| 290 |
+
export const saveTransaction = async (transaction: Transaction): Promise<ApiResponse<Transaction>> => {
|
| 291 |
+
try {
|
| 292 |
+
// Validate Payload
|
| 293 |
+
if (!transaction.party_id) return { success: false, message: "Party is required" };
|
| 294 |
+
if (!transaction.items || transaction.items.length === 0) return { success: false, message: "At least one item is required" };
|
| 295 |
+
|
| 296 |
+
console.log('=== SAVING TRANSACTION ===');
|
| 297 |
+
console.log('Transaction ID:', transaction.id);
|
| 298 |
+
console.log('Items count:', transaction.items?.length);
|
| 299 |
+
console.log('Items data:', JSON.stringify(transaction.items, null, 2));
|
| 300 |
+
|
| 301 |
+
const res = await fetch(`${API_BASE}/transactions`, {
|
| 302 |
+
method: 'POST',
|
| 303 |
+
headers: { 'Content-Type': 'application/json' },
|
| 304 |
+
body: JSON.stringify(transaction)
|
| 305 |
+
});
|
| 306 |
+
|
| 307 |
+
if (!res.ok) {
|
| 308 |
+
const error = await res.json();
|
| 309 |
+
console.error('Save transaction error:', error);
|
| 310 |
+
throw new Error(error.message || 'Failed to save transaction');
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
const result = await res.json();
|
| 314 |
+
console.log('=== SAVE RESPONSE ===');
|
| 315 |
+
console.log('Response data:', result.data);
|
| 316 |
+
console.log('Response items count:', result.data?.items?.length);
|
| 317 |
+
console.log('Response items:', JSON.stringify(result.data?.items, null, 2));
|
| 318 |
+
|
| 319 |
+
return {
|
| 320 |
+
success: true,
|
| 321 |
+
data: normalizeTransaction(result.data),
|
| 322 |
+
message: result.message
|
| 323 |
+
};
|
| 324 |
+
} catch (error: any) {
|
| 325 |
+
console.error('Error saving transaction:', error);
|
| 326 |
+
return {
|
| 327 |
+
success: false,
|
| 328 |
+
message: error.message || 'Failed to save transaction'
|
| 329 |
+
};
|
| 330 |
+
}
|
| 331 |
+
};
|
| 332 |
+
|
| 333 |
+
export const updateTransactionPayment = async (transactionId: string, amount: number): Promise<ApiResponse<Transaction>> => {
|
| 334 |
+
try {
|
| 335 |
+
// Validate amount
|
| 336 |
+
if (amount <= 0) return { success: false, message: "Amount must be greater than 0" };
|
| 337 |
+
|
| 338 |
+
const res = await fetch(`${API_BASE}/transactions/${transactionId}/payment`, {
|
| 339 |
+
method: 'PATCH',
|
| 340 |
+
headers: { 'Content-Type': 'application/json' },
|
| 341 |
+
body: JSON.stringify({ amount }),
|
| 342 |
+
});
|
| 343 |
+
|
| 344 |
+
const data = await res.json();
|
| 345 |
+
if (!res.ok || !data.success) {
|
| 346 |
+
return { success: false, message: data?.message || 'Error updating payment' };
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
return {
|
| 350 |
+
success: true,
|
| 351 |
+
message: data.message || 'Payment updated successfully'
|
| 352 |
+
};
|
| 353 |
+
} catch (e: any) {
|
| 354 |
+
console.error('Error updating payment:', e);
|
| 355 |
+
return { success: false, message: e.message || 'Error updating payment' };
|
| 356 |
+
}
|
| 357 |
+
};
|
tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"experimentalDecorators": true,
|
| 5 |
+
"useDefineForClassFields": false,
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"lib": [
|
| 8 |
+
"ES2022",
|
| 9 |
+
"DOM",
|
| 10 |
+
"DOM.Iterable"
|
| 11 |
+
],
|
| 12 |
+
"skipLibCheck": true,
|
| 13 |
+
"types": [
|
| 14 |
+
"node"
|
| 15 |
+
],
|
| 16 |
+
"moduleResolution": "bundler",
|
| 17 |
+
"isolatedModules": true,
|
| 18 |
+
"moduleDetection": "force",
|
| 19 |
+
"allowJs": true,
|
| 20 |
+
"jsx": "react-jsx",
|
| 21 |
+
"paths": {
|
| 22 |
+
"@/*": [
|
| 23 |
+
"./*"
|
| 24 |
+
]
|
| 25 |
+
},
|
| 26 |
+
"allowImportingTsExtensions": true,
|
| 27 |
+
"noEmit": true
|
| 28 |
+
}
|
| 29 |
+
}
|
types.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Enums
|
| 2 |
+
// NOTE:
|
| 3 |
+
// - AWAAK = Purchase (Incoming stock)
|
| 4 |
+
// - JAWAAK = Sale (Outgoing stock)
|
| 5 |
+
export enum BillType {
|
| 6 |
+
JAWAAK = 'jawaak', // Sale / Outgoing
|
| 7 |
+
AWAAK = 'awaak' // Purchase / Incoming
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export enum PartyType {
|
| 11 |
+
JAWAAK = 'jawaak',
|
| 12 |
+
AWAAK = 'awaak',
|
| 13 |
+
BOTH = 'both'
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export enum PaymentMode {
|
| 17 |
+
CASH = 'cash',
|
| 18 |
+
ONLINE = 'online',
|
| 19 |
+
CHEQUE = 'cheque',
|
| 20 |
+
DUE = 'due'
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export enum LotStatus {
|
| 24 |
+
ACTIVE = 'active',
|
| 25 |
+
SOLD_OUT = 'sold_out'
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// API Response Wrapper (Backend Ready)
|
| 29 |
+
export interface ApiResponse<T> {
|
| 30 |
+
success: boolean;
|
| 31 |
+
data?: T;
|
| 32 |
+
message?: string;
|
| 33 |
+
errors?: string[];
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Interfaces
|
| 37 |
+
export interface Party {
|
| 38 |
+
id: string;
|
| 39 |
+
name: string;
|
| 40 |
+
phone: string;
|
| 41 |
+
city: string;
|
| 42 |
+
party_type: PartyType;
|
| 43 |
+
current_balance: number; // +ve = They owe us, -ve = We owe them
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export interface MirchiType {
|
| 47 |
+
id: string;
|
| 48 |
+
name: string;
|
| 49 |
+
current_rate: number;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
export interface Lot {
|
| 53 |
+
id: string;
|
| 54 |
+
lot_number: string;
|
| 55 |
+
mirchi_type_id: string;
|
| 56 |
+
mirchi_name: string; // Denormalized for display
|
| 57 |
+
total_quantity: number;
|
| 58 |
+
remaining_quantity: number;
|
| 59 |
+
purchase_date: string;
|
| 60 |
+
status: LotStatus;
|
| 61 |
+
avg_rate: number;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
export interface Payment {
|
| 65 |
+
mode: PaymentMode;
|
| 66 |
+
amount: number;
|
| 67 |
+
reference?: string;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
export interface Expenses {
|
| 71 |
+
cess_percent: number;
|
| 72 |
+
cess_amount: number;
|
| 73 |
+
adat_percent: number;
|
| 74 |
+
adat_amount: number;
|
| 75 |
+
poti_rate: number;
|
| 76 |
+
poti_amount: number;
|
| 77 |
+
hamali_per_poti: number;
|
| 78 |
+
hamali_amount: number;
|
| 79 |
+
packaging_hamali_per_poti: number;
|
| 80 |
+
packaging_hamali_amount: number;
|
| 81 |
+
gaadi_bharni: number;
|
| 82 |
+
other_expenses: number;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
export interface TransactionItem {
|
| 86 |
+
id: string;
|
| 87 |
+
mirchi_type_id: string;
|
| 88 |
+
mirchi_name?: string;
|
| 89 |
+
quality: string;
|
| 90 |
+
lot_id?: string; // Optional for Jawaak (created auto), Required for Awaak
|
| 91 |
+
poti_weights: number[]; // Array of weights [10, 20, 30]
|
| 92 |
+
gross_weight: number;
|
| 93 |
+
poti_count: number;
|
| 94 |
+
total_potya: number; // Deduction
|
| 95 |
+
net_weight: number;
|
| 96 |
+
rate_per_kg: number;
|
| 97 |
+
item_total: number;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
export interface Transaction {
|
| 101 |
+
id: string;
|
| 102 |
+
bill_number: string;
|
| 103 |
+
bill_date: string;
|
| 104 |
+
bill_type: BillType;
|
| 105 |
+
is_return: boolean; // True for Purchase Return or Sales Return
|
| 106 |
+
party_id: string;
|
| 107 |
+
party_name?: string;
|
| 108 |
+
items: TransactionItem[];
|
| 109 |
+
expenses: Expenses;
|
| 110 |
+
payments: Payment[];
|
| 111 |
+
|
| 112 |
+
// Totals
|
| 113 |
+
gross_weight_total: number;
|
| 114 |
+
net_weight_total: number;
|
| 115 |
+
subtotal: number;
|
| 116 |
+
total_expenses: number;
|
| 117 |
+
total_amount: number;
|
| 118 |
+
paid_amount: number;
|
| 119 |
+
balance_amount: number;
|
| 120 |
+
|
| 121 |
+
created_at?: string;
|
| 122 |
+
updated_at?: string;
|
| 123 |
+
}
|
utils/LedgerPdfGenerator.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import jsPDF from 'jspdf';
|
| 2 |
+
import autoTable from 'jspdf-autotable';
|
| 3 |
+
import { Party, Transaction } from '../types';
|
| 4 |
+
|
| 5 |
+
// --- CONFIGURATION ---
|
| 6 |
+
const BG_COLOR: [number, number, number] = [240, 234, 214]; // Beige
|
| 7 |
+
const BORDER_COLOR: [number, number, number] = [0, 0, 0]; // Black
|
| 8 |
+
|
| 9 |
+
// Helper: Currency Format
|
| 10 |
+
const FORMAT_CURRENCY = (amount: number) => {
|
| 11 |
+
return new Intl.NumberFormat('en-IN', {
|
| 12 |
+
minimumFractionDigits: 2,
|
| 13 |
+
maximumFractionDigits: 2
|
| 14 |
+
}).format(amount);
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
// Helper: Date Format
|
| 18 |
+
const FORMAT_DATE = (dateStr: string) => {
|
| 19 |
+
if (!dateStr) return "";
|
| 20 |
+
const dateObj = new Date(dateStr);
|
| 21 |
+
return `${dateObj.getDate().toString().padStart(2, '0')}/${(dateObj.getMonth() + 1).toString().padStart(2, '0')}/${dateObj.getFullYear().toString().slice(-2)}`;
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
export const generateLedgerPDF = (party: Party, transactions: Transaction[]) => {
|
| 25 |
+
const doc = new jsPDF();
|
| 26 |
+
|
| 27 |
+
// --- 1. PAGE CONFIGURATION ---
|
| 28 |
+
const PAGE_WIDTH = 210;
|
| 29 |
+
const PAGE_HEIGHT = 297;
|
| 30 |
+
const MARGIN = 14;
|
| 31 |
+
const CONTENT_WIDTH = PAGE_WIDTH - (MARGIN * 2);
|
| 32 |
+
|
| 33 |
+
// --- 2. SETUP & BACKGROUND ---
|
| 34 |
+
doc.setFillColor(...BG_COLOR);
|
| 35 |
+
doc.rect(0, 0, PAGE_WIDTH, PAGE_HEIGHT, 'F');
|
| 36 |
+
|
| 37 |
+
// --- 3. DATA PREPARATION ---
|
| 38 |
+
const sortedTxs = [...transactions].sort((a, b) =>
|
| 39 |
+
new Date(a.bill_date).getTime() - new Date(b.bill_date).getTime()
|
| 40 |
+
);
|
| 41 |
+
|
| 42 |
+
let runningBalance = 0;
|
| 43 |
+
|
| 44 |
+
const tableRows = sortedTxs.flatMap(tx => {
|
| 45 |
+
const rows = [];
|
| 46 |
+
const dateStr = FORMAT_DATE(tx.bill_date);
|
| 47 |
+
|
| 48 |
+
// --- DETERMINE TYPE (AWAAK vs JAWAAK) ---
|
| 49 |
+
// AWAAK = Purchase (We Owe - Credit)
|
| 50 |
+
// JAWAAK = Sales (They Owe - Debit)
|
| 51 |
+
const typeStr = String(tx.bill_type).toUpperCase();
|
| 52 |
+
const numStr = tx.bill_number ? tx.bill_number.toUpperCase() : "";
|
| 53 |
+
|
| 54 |
+
const isAwaak = typeStr === 'AWAAK' || numStr.includes('AWAAK');
|
| 55 |
+
|
| 56 |
+
// --- A. BILL ROW ---
|
| 57 |
+
// 1. Particulars Construction
|
| 58 |
+
let itemDetails = "";
|
| 59 |
+
if (tx.items && tx.items.length > 0) {
|
| 60 |
+
tx.items.forEach(item => {
|
| 61 |
+
let wStr = "";
|
| 62 |
+
if (Array.isArray(item.poti_weights)) wStr = item.poti_weights.join(",");
|
| 63 |
+
else if (item.poti_weights) wStr = String(item.poti_weights);
|
| 64 |
+
|
| 65 |
+
const prefix = itemDetails ? "\n" : "";
|
| 66 |
+
itemDetails += `${prefix}${item.mirchi_name}`;
|
| 67 |
+
if (wStr) itemDetails += ` (${wStr})`;
|
| 68 |
+
});
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
let billParticulars = `Bill No: ${tx.bill_number}`;
|
| 72 |
+
if (tx.is_return) billParticulars += " (RETURN)";
|
| 73 |
+
if (itemDetails) billParticulars += `\n${itemDetails}`;
|
| 74 |
+
|
| 75 |
+
let totalPoti = 0;
|
| 76 |
+
tx.items?.forEach(i => totalPoti += Number(i.poti_count) || 0);
|
| 77 |
+
|
| 78 |
+
// 2. FINANCIAL LOGIC (THE FIX)
|
| 79 |
+
let billDebit = 0;
|
| 80 |
+
let billCredit = 0;
|
| 81 |
+
|
| 82 |
+
if (isAwaak) {
|
| 83 |
+
// AWAAK (Purchase):
|
| 84 |
+
// - Normal Bill: CREDIT (Liability increases)
|
| 85 |
+
// - Return Bill: DEBIT (Liability decreases)
|
| 86 |
+
if (tx.is_return) billDebit = tx.total_amount;
|
| 87 |
+
else billCredit = tx.total_amount;
|
| 88 |
+
} else {
|
| 89 |
+
// JAWAAK (Sales):
|
| 90 |
+
// - Normal Bill: DEBIT (Asset increases - They owe us)
|
| 91 |
+
// - Return Bill: CREDIT (Asset decreases)
|
| 92 |
+
if (tx.is_return) billCredit = tx.total_amount;
|
| 93 |
+
else billDebit = tx.total_amount;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// Running Balance = Old + Debit - Credit
|
| 97 |
+
runningBalance = runningBalance + billDebit - billCredit;
|
| 98 |
+
|
| 99 |
+
// Push Bill Row
|
| 100 |
+
rows.push({
|
| 101 |
+
date: dateStr,
|
| 102 |
+
particulars: billParticulars,
|
| 103 |
+
poti: totalPoti > 0 ? totalPoti.toString() : "-",
|
| 104 |
+
credit: billCredit > 0 ? FORMAT_CURRENCY(billCredit) : "-",
|
| 105 |
+
debit: billDebit > 0 ? FORMAT_CURRENCY(billDebit) : "-",
|
| 106 |
+
balance: `${FORMAT_CURRENCY(Math.abs(runningBalance))} ${runningBalance >= 0 ? 'Dr' : 'Cr'}`,
|
| 107 |
+
isMainRow: true
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
// --- B. PAYMENT ROWS ---
|
| 111 |
+
if (tx.payments && tx.payments.length > 0) {
|
| 112 |
+
const validPayments = tx.payments.filter(p => p.mode.toLowerCase() !== 'due');
|
| 113 |
+
|
| 114 |
+
validPayments.forEach(p => {
|
| 115 |
+
let payDebit = 0;
|
| 116 |
+
let payCredit = 0;
|
| 117 |
+
|
| 118 |
+
if (isAwaak) {
|
| 119 |
+
// AWAAK (Purchase) Payment:
|
| 120 |
+
// - We Pay Money: DEBIT (Liability decreases)
|
| 121 |
+
// - Refund (Return): CREDIT
|
| 122 |
+
if (tx.is_return) payCredit = p.amount;
|
| 123 |
+
else payDebit = p.amount;
|
| 124 |
+
} else {
|
| 125 |
+
// JAWAAK (Sales) Payment:
|
| 126 |
+
// - They Pay Money: CREDIT (Asset decreases - Debt paid off)
|
| 127 |
+
// - Refund (Return): DEBIT
|
| 128 |
+
if (tx.is_return) payDebit = p.amount;
|
| 129 |
+
else payCredit = p.amount;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
runningBalance = runningBalance + payDebit - payCredit;
|
| 133 |
+
|
| 134 |
+
// Description
|
| 135 |
+
const modeStr = p.mode.charAt(0).toUpperCase() + p.mode.slice(1);
|
| 136 |
+
let payDesc = ` [${modeStr}]`;
|
| 137 |
+
if (p.reference) payDesc += ` ${p.reference}`;
|
| 138 |
+
|
| 139 |
+
rows.push({
|
| 140 |
+
date: dateStr,
|
| 141 |
+
particulars: payDesc,
|
| 142 |
+
poti: "-",
|
| 143 |
+
credit: payCredit > 0 ? FORMAT_CURRENCY(payCredit) : "-",
|
| 144 |
+
debit: payDebit > 0 ? FORMAT_CURRENCY(payDebit) : "-",
|
| 145 |
+
balance: `${FORMAT_CURRENCY(Math.abs(runningBalance))} ${runningBalance >= 0 ? 'Dr' : 'Cr'}`,
|
| 146 |
+
isMainRow: false
|
| 147 |
+
});
|
| 148 |
+
});
|
| 149 |
+
}
|
| 150 |
+
return rows;
|
| 151 |
+
});
|
| 152 |
+
|
| 153 |
+
// --- 4. GENERATE TABLE ---
|
| 154 |
+
autoTable(doc, {
|
| 155 |
+
startY: 35,
|
| 156 |
+
tableWidth: CONTENT_WIDTH,
|
| 157 |
+
margin: { left: MARGIN, right: MARGIN },
|
| 158 |
+
|
| 159 |
+
head: [[
|
| 160 |
+
'Date',
|
| 161 |
+
'Particulars',
|
| 162 |
+
'Poti',
|
| 163 |
+
'Credit\n(Jama)',
|
| 164 |
+
'Debit\n(Nave)',
|
| 165 |
+
'Balance'
|
| 166 |
+
]],
|
| 167 |
+
|
| 168 |
+
body: tableRows.map(r => [r.date, r.particulars, r.poti, r.credit, r.debit, r.balance]),
|
| 169 |
+
|
| 170 |
+
theme: 'grid',
|
| 171 |
+
|
| 172 |
+
styles: {
|
| 173 |
+
fillColor: false,
|
| 174 |
+
textColor: 0,
|
| 175 |
+
lineColor: 0,
|
| 176 |
+
lineWidth: 0.1,
|
| 177 |
+
font: 'helvetica',
|
| 178 |
+
fontSize: 9,
|
| 179 |
+
valign: 'top',
|
| 180 |
+
cellPadding: 3,
|
| 181 |
+
overflow: 'linebreak'
|
| 182 |
+
},
|
| 183 |
+
|
| 184 |
+
headStyles: {
|
| 185 |
+
fillColor: false,
|
| 186 |
+
textColor: 0,
|
| 187 |
+
fontStyle: 'bold',
|
| 188 |
+
halign: 'center',
|
| 189 |
+
valign: 'middle',
|
| 190 |
+
lineWidth: 0.1,
|
| 191 |
+
},
|
| 192 |
+
|
| 193 |
+
columnStyles: {
|
| 194 |
+
0: { cellWidth: 20, halign: 'center' }, // Date
|
| 195 |
+
1: { cellWidth: 72, halign: 'left' }, // Particulars
|
| 196 |
+
2: { cellWidth: 12, halign: 'center' }, // Poti
|
| 197 |
+
3: { cellWidth: 26, halign: 'right' }, // Credit
|
| 198 |
+
4: { cellWidth: 26, halign: 'right' }, // Debit
|
| 199 |
+
5: { cellWidth: 26, halign: 'right', fontStyle: 'bold' } // Balance
|
| 200 |
+
},
|
| 201 |
+
|
| 202 |
+
didParseCell: (data) => {
|
| 203 |
+
const rowIdx = data.row.index;
|
| 204 |
+
const rowData = tableRows[rowIdx];
|
| 205 |
+
if (data.section === 'body' && data.column.index === 1 && rowData?.isMainRow) {
|
| 206 |
+
data.cell.styles.fontStyle = 'bold';
|
| 207 |
+
}
|
| 208 |
+
},
|
| 209 |
+
|
| 210 |
+
didDrawPage: (data) => {
|
| 211 |
+
// Border
|
| 212 |
+
doc.setDrawColor(...BORDER_COLOR);
|
| 213 |
+
doc.setLineWidth(0.4);
|
| 214 |
+
doc.rect(MARGIN, MARGIN, CONTENT_WIDTH, PAGE_HEIGHT - (MARGIN * 2));
|
| 215 |
+
|
| 216 |
+
// Header
|
| 217 |
+
if (data.pageNumber === 1) {
|
| 218 |
+
const startX = MARGIN + 4;
|
| 219 |
+
const startY = 25;
|
| 220 |
+
|
| 221 |
+
doc.setFontSize(14);
|
| 222 |
+
doc.setFont("helvetica", "bold");
|
| 223 |
+
doc.text("Name of A/c :", startX, startY);
|
| 224 |
+
|
| 225 |
+
const labelWidth = doc.getTextWidth("Name of A/c : ");
|
| 226 |
+
doc.text(party.name, startX + labelWidth, startY);
|
| 227 |
+
|
| 228 |
+
if (party.city || party.phone) {
|
| 229 |
+
doc.setFontSize(9);
|
| 230 |
+
doc.setFont("helvetica", "normal");
|
| 231 |
+
const subText = [party.city, party.phone].filter(Boolean).join(" - ");
|
| 232 |
+
doc.text(subText, startX, startY + 6);
|
| 233 |
+
}
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
});
|
| 237 |
+
|
| 238 |
+
const cleanName = party.name.replace(/[^a-zA-Z0-9]/g, '_');
|
| 239 |
+
doc.save(`${cleanName}_Ledger.pdf`);
|
| 240 |
+
};
|
utils/dateFormatter.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Date formatting utility for consistent date display across the app
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Formats a date string to DD/MM/YYYY format
|
| 5 |
+
* @param dateStr - Date string in any format
|
| 6 |
+
* @returns Formatted date string (DD/MM/YYYY)
|
| 7 |
+
*/
|
| 8 |
+
export const formatDate = (dateStr: string | Date): string => {
|
| 9 |
+
const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr;
|
| 10 |
+
|
| 11 |
+
if (isNaN(date.getTime())) {
|
| 12 |
+
return '-';
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
return date.toLocaleDateString('en-IN', {
|
| 16 |
+
day: '2-digit',
|
| 17 |
+
month: '2-digit',
|
| 18 |
+
year: 'numeric'
|
| 19 |
+
});
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* Formats a date string to YYYY-MM-DD format (for input fields)
|
| 24 |
+
* @param dateStr - Date string in any format
|
| 25 |
+
* @returns Formatted date string (YYYY-MM-DD)
|
| 26 |
+
*/
|
| 27 |
+
export const formatDateForInput = (dateStr: string | Date): string => {
|
| 28 |
+
const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr;
|
| 29 |
+
|
| 30 |
+
if (isNaN(date.getTime())) {
|
| 31 |
+
return '';
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const year = date.getFullYear();
|
| 35 |
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
| 36 |
+
const day = String(date.getDate()).padStart(2, '0');
|
| 37 |
+
|
| 38 |
+
return `${year}-${month}-${day}`;
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* Gets today's date in YYYY-MM-DD format
|
| 43 |
+
* @returns Today's date string
|
| 44 |
+
*/
|
| 45 |
+
export const getTodayDate = (): string => {
|
| 46 |
+
return formatDateForInput(new Date());
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Formats a date to display only date (no time)
|
| 51 |
+
* @param dateStr - Date string
|
| 52 |
+
* @returns Formatted date string
|
| 53 |
+
*/
|
| 54 |
+
export const formatDateOnly = (dateStr: string | Date): string => {
|
| 55 |
+
return formatDate(dateStr);
|
| 56 |
+
};
|
utils/exportToExcel.ts
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import XLSX from 'xlsx-js-style';
|
| 2 |
+
import { Transaction, Party, BillType } from '../types';
|
| 3 |
+
|
| 4 |
+
export interface AnalysisExportRow {
|
| 5 |
+
lotNumber: string;
|
| 6 |
+
mirchiName: string;
|
| 7 |
+
purchaseQty: number;
|
| 8 |
+
purchaseValue: number;
|
| 9 |
+
avgPurchaseRate: number;
|
| 10 |
+
soldQty: number;
|
| 11 |
+
salesValue: number;
|
| 12 |
+
avgSaleRate: number;
|
| 13 |
+
realizedProfit: number;
|
| 14 |
+
marginPercent: number;
|
| 15 |
+
remainingQty: number;
|
| 16 |
+
remainingValueAtCost: number;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export interface LotOverviewExportRow {
|
| 20 |
+
lotNumber: string;
|
| 21 |
+
mirchiName: string;
|
| 22 |
+
purchaseQty: number;
|
| 23 |
+
purchaseValue: number;
|
| 24 |
+
avgPurchaseRate: number;
|
| 25 |
+
soldQty: number;
|
| 26 |
+
salesValue: number;
|
| 27 |
+
avgSaleRate: number;
|
| 28 |
+
realizedProfit: number;
|
| 29 |
+
marginPercent: number;
|
| 30 |
+
remainingQty: number;
|
| 31 |
+
remainingValueAtCost: number;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export interface LotDetailExportRow {
|
| 35 |
+
date: string;
|
| 36 |
+
billNo: string;
|
| 37 |
+
typeLabel: string;
|
| 38 |
+
directionLabel: string;
|
| 39 |
+
qtyKg: number;
|
| 40 |
+
ratePerKg: number;
|
| 41 |
+
grossAmount: number;
|
| 42 |
+
expensesAllocated: number;
|
| 43 |
+
netAmount: number;
|
| 44 |
+
profitImpact: number;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// ==========================================
|
| 48 |
+
// 2. STYLING CONFIGURATION
|
| 49 |
+
// ==========================================
|
| 50 |
+
|
| 51 |
+
// Common Colors
|
| 52 |
+
const CLR_BORDER = { rgb: "D1D5DB" }; // Gray-300
|
| 53 |
+
const CLR_TEXT = { rgb: "374151" }; // Gray-700
|
| 54 |
+
const CLR_HEAD_BG = { rgb: "F3F4F6" }; // Gray-100
|
| 55 |
+
const CLR_GREEN = { rgb: "059669" };
|
| 56 |
+
const CLR_RED = { rgb: "DC2626" };
|
| 57 |
+
const CLR_TEAL = { rgb: "0D9488" };
|
| 58 |
+
const CLR_WHITE = { rgb: "FFFFFF" };
|
| 59 |
+
|
| 60 |
+
// Border Definitions
|
| 61 |
+
const BORDER_ALL = {
|
| 62 |
+
top: { style: 'thin', color: CLR_BORDER },
|
| 63 |
+
bottom: { style: 'thin', color: CLR_BORDER },
|
| 64 |
+
left: { style: 'thin', color: CLR_BORDER },
|
| 65 |
+
right: { style: 'thin', color: CLR_BORDER }
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
const BORDER_TOP_SIDE = {
|
| 69 |
+
top: { style: 'thin', color: { rgb: "9CA3AF" } }, // Darker Gray for Card
|
| 70 |
+
left: { style: 'thin', color: { rgb: "9CA3AF" } },
|
| 71 |
+
right: { style: 'thin', color: { rgb: "9CA3AF" } }
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
const BORDER_BOT_SIDE = {
|
| 75 |
+
bottom: { style: 'thin', color: { rgb: "9CA3AF" } },
|
| 76 |
+
left: { style: 'thin', color: { rgb: "9CA3AF" } },
|
| 77 |
+
right: { style: 'thin', color: { rgb: "9CA3AF" } }
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
// Style Objects
|
| 81 |
+
const STYLES = {
|
| 82 |
+
TITLE: {
|
| 83 |
+
font: { bold: true, sz: 14, color: { rgb: "111827" } },
|
| 84 |
+
alignment: { horizontal: 'left', vertical: 'center' }
|
| 85 |
+
},
|
| 86 |
+
|
| 87 |
+
// Card Styles
|
| 88 |
+
CARD_HEADER: {
|
| 89 |
+
border: BORDER_TOP_SIDE,
|
| 90 |
+
fill: { fgColor: CLR_WHITE },
|
| 91 |
+
font: { sz: 10, color: { rgb: "6B7280" } },
|
| 92 |
+
alignment: { vertical: 'bottom', horizontal: 'left', indent: 1 }
|
| 93 |
+
},
|
| 94 |
+
CARD_VALUE: {
|
| 95 |
+
border: BORDER_BOT_SIDE,
|
| 96 |
+
fill: { fgColor: CLR_WHITE },
|
| 97 |
+
font: { bold: true, sz: 11, color: { rgb: "1F2937" } },
|
| 98 |
+
alignment: { vertical: 'top', horizontal: 'left', indent: 1 }
|
| 99 |
+
},
|
| 100 |
+
|
| 101 |
+
// Table Styles
|
| 102 |
+
TABLE_HEAD: {
|
| 103 |
+
fill: { fgColor: CLR_HEAD_BG },
|
| 104 |
+
font: { bold: true, color: CLR_TEXT },
|
| 105 |
+
border: BORDER_ALL,
|
| 106 |
+
alignment: { horizontal: 'center', vertical: 'center' }
|
| 107 |
+
},
|
| 108 |
+
CELL_TEXT: {
|
| 109 |
+
border: BORDER_ALL,
|
| 110 |
+
alignment: { vertical: 'center', horizontal: 'left' },
|
| 111 |
+
font: { color: CLR_TEXT }
|
| 112 |
+
},
|
| 113 |
+
CELL_NUM: {
|
| 114 |
+
border: BORDER_ALL,
|
| 115 |
+
alignment: { vertical: 'center', horizontal: 'right' },
|
| 116 |
+
font: { color: { rgb: "111827" } }
|
| 117 |
+
},
|
| 118 |
+
CELL_MONO: {
|
| 119 |
+
border: BORDER_ALL,
|
| 120 |
+
alignment: { vertical: 'center', horizontal: 'left' },
|
| 121 |
+
font: { name: 'Courier New', sz: 10, color: CLR_TEXT }
|
| 122 |
+
},
|
| 123 |
+
|
| 124 |
+
// Text Colors
|
| 125 |
+
TXT_TEAL: { font: { bold: true, sz: 12, color: CLR_TEAL } },
|
| 126 |
+
TXT_GRAY: { font: { color: { rgb: "6B7280" } } },
|
| 127 |
+
TXT_GREEN: { font: { bold: true, color: CLR_GREEN } },
|
| 128 |
+
TXT_RED: { font: { bold: true, color: CLR_RED } }
|
| 129 |
+
};
|
| 130 |
+
|
| 131 |
+
// Formats
|
| 132 |
+
const FMT_CURRENCY = "₹#,##0.00"; // 2 decimal places for clean alignment
|
| 133 |
+
const FMT_QTY = "0.00 \"kg\"";
|
| 134 |
+
|
| 135 |
+
// ==========================================
|
| 136 |
+
// 3. HELPER FUNCTIONS
|
| 137 |
+
// ==========================================
|
| 138 |
+
|
| 139 |
+
// Helper to create cell.
|
| 140 |
+
// Uses empty string "" if value is null to avoid "null" text in Excel.
|
| 141 |
+
const c = (val: any, s: any = {}, fmt?: string) => {
|
| 142 |
+
return {
|
| 143 |
+
v: val === null || val === undefined ? "" : val,
|
| 144 |
+
s: { ...s, numFmt: fmt }, // Inject format into style for xlsx-js-style
|
| 145 |
+
z: fmt // Inject format into standard key for compatibility
|
| 146 |
+
};
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
const formatIndCurrency = (val: number) => {
|
| 150 |
+
return val.toLocaleString('en-IN', {
|
| 151 |
+
maximumFractionDigits: 0, // Keep summary cards clean (no decimals)
|
| 152 |
+
style: 'currency',
|
| 153 |
+
currency: 'INR'
|
| 154 |
+
}).replace('₹', '₹'); // Ensure symbol consistency
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
// ==========================================
|
| 158 |
+
// 4. EXPORT FUNCTIONS
|
| 159 |
+
// ==========================================
|
| 160 |
+
|
| 161 |
+
export const exportLotDetailAnalysis = (
|
| 162 |
+
lot: LotOverviewExportRow,
|
| 163 |
+
rows: LotDetailExportRow[],
|
| 164 |
+
) => {
|
| 165 |
+
const data: any[][] = [];
|
| 166 |
+
|
| 167 |
+
// --- ROW 1: Main Title ---
|
| 168 |
+
data.push([
|
| 169 |
+
c(`${lot.lotNumber} - ${lot.mirchiName} Analysis`, STYLES.TITLE),
|
| 170 |
+
c(""), c(""), c(""), c(""), c(""), c(""), c("")
|
| 171 |
+
]);
|
| 172 |
+
|
| 173 |
+
// --- ROW 2: Spacer ---
|
| 174 |
+
data.push([c("")]);
|
| 175 |
+
|
| 176 |
+
// --- ROW 3: Lot Info (Left) & Remaining (Right) ---
|
| 177 |
+
// Note: We used c("") for spacers to ensure no "null" text
|
| 178 |
+
data.push([
|
| 179 |
+
c(lot.lotNumber, { ...STYLES.TXT_TEAL, alignment: { vertical: 'bottom' } }),
|
| 180 |
+
c(""), c(""), c(""), c(""), c(""),
|
| 181 |
+
c("Remaining", { ...STYLES.TXT_GRAY, alignment: { horizontal: 'right', vertical: 'bottom' } }),
|
| 182 |
+
c(Number(lot.remainingQty.toFixed(2)), { font: { bold: true }, alignment: { horizontal: 'right', vertical: 'bottom' } }, FMT_QTY)
|
| 183 |
+
]);
|
| 184 |
+
|
| 185 |
+
// --- ROW 4: Mirchi Name ---
|
| 186 |
+
data.push([
|
| 187 |
+
c(lot.mirchiName, STYLES.TXT_GRAY),
|
| 188 |
+
c(""), c(""), c(""), c(""), c(""), c(""), c("")
|
| 189 |
+
]);
|
| 190 |
+
|
| 191 |
+
// --- ROW 5: Spacer ---
|
| 192 |
+
data.push([c("")]);
|
| 193 |
+
|
| 194 |
+
// --- ROW 6: CARD HEADERS ---
|
| 195 |
+
data.push([
|
| 196 |
+
c("Purchase", STYLES.CARD_HEADER), c(""), c(""), c(""),
|
| 197 |
+
c("Sales", STYLES.CARD_HEADER), c(""), c(""), c("")
|
| 198 |
+
]);
|
| 199 |
+
|
| 200 |
+
// --- ROW 7: CARD VALUES ---
|
| 201 |
+
// Format text manually for the card to ensure it looks perfect
|
| 202 |
+
const buyTxt = `${lot.purchaseQty.toFixed(2)} kg • ${formatIndCurrency(lot.purchaseValue)}`;
|
| 203 |
+
const sellTxt = `${lot.soldQty.toFixed(2)} kg • ${formatIndCurrency(lot.salesValue)}`;
|
| 204 |
+
|
| 205 |
+
data.push([
|
| 206 |
+
c(buyTxt, STYLES.CARD_VALUE), c(""), c(""), c(""),
|
| 207 |
+
c(sellTxt, STYLES.CARD_VALUE), c(""), c(""), c("")
|
| 208 |
+
]);
|
| 209 |
+
|
| 210 |
+
// --- ROW 8: Spacer ---
|
| 211 |
+
data.push([c("")]);
|
| 212 |
+
|
| 213 |
+
// --- ROW 9: Avg & Profit Summary ---
|
| 214 |
+
const avgText = `Avg Buy ₹${lot.avgPurchaseRate.toFixed(0)} /kg • Avg Sell ₹${lot.avgSaleRate.toFixed(0)} /kg`;
|
| 215 |
+
|
| 216 |
+
// Profit Logic
|
| 217 |
+
const pVal = Number(lot.realizedProfit.toFixed(2)); // Round to avoid .774
|
| 218 |
+
const pLabel = pVal > 0 ? "Profit" : pVal < 0 ? "Loss" : "Break-even";
|
| 219 |
+
const pStyle = pVal > 0 ? STYLES.TXT_GREEN : pVal < 0 ? STYLES.TXT_RED : STYLES.TXT_GRAY;
|
| 220 |
+
|
| 221 |
+
data.push([
|
| 222 |
+
c(avgText, { font: { color: { rgb: "4B5563" }, italic: true }, alignment: { vertical: 'center' } }),
|
| 223 |
+
c(""), c(""), c(""), c(""), c(""),
|
| 224 |
+
c(pLabel, { ...STYLES.TXT_GRAY, alignment: { horizontal: 'right', vertical: 'center' } }),
|
| 225 |
+
c(pVal, { ...pStyle, alignment: { horizontal: 'right', vertical: 'center' } }, FMT_CURRENCY)
|
| 226 |
+
]);
|
| 227 |
+
|
| 228 |
+
// --- ROW 10: Spacer ---
|
| 229 |
+
data.push([c("")]);
|
| 230 |
+
|
| 231 |
+
// --- ROW 11: TABLE HEADERS ---
|
| 232 |
+
data.push([
|
| 233 |
+
c('Date', STYLES.TABLE_HEAD),
|
| 234 |
+
c('Bill No', STYLES.TABLE_HEAD),
|
| 235 |
+
c('Type', STYLES.TABLE_HEAD),
|
| 236 |
+
c('Import / Export', STYLES.TABLE_HEAD),
|
| 237 |
+
c('Qty (kg)', STYLES.TABLE_HEAD),
|
| 238 |
+
c('Rate', STYLES.TABLE_HEAD),
|
| 239 |
+
c('Net Amount', STYLES.TABLE_HEAD),
|
| 240 |
+
c('Profit Impact', STYLES.TABLE_HEAD)
|
| 241 |
+
]);
|
| 242 |
+
|
| 243 |
+
// --- ROW 12+: DATA ---
|
| 244 |
+
if (rows.length === 0) {
|
| 245 |
+
data.push([c('No transactions found', STYLES.CELL_TEXT)]);
|
| 246 |
+
} else {
|
| 247 |
+
rows.forEach(row => {
|
| 248 |
+
// Round numbers to 2 decimals to prevent floating point garbage
|
| 249 |
+
const safeProfit = Number(row.profitImpact.toFixed(2));
|
| 250 |
+
const safeNet = Number(row.netAmount.toFixed(2));
|
| 251 |
+
const safeQty = Number(row.qtyKg.toFixed(2));
|
| 252 |
+
const safeRate = Number(row.ratePerKg.toFixed(2));
|
| 253 |
+
|
| 254 |
+
const impactStyle = safeProfit > 0
|
| 255 |
+
? { ...STYLES.CELL_NUM, font: { color: CLR_GREEN } }
|
| 256 |
+
: safeProfit < 0
|
| 257 |
+
? { ...STYLES.CELL_NUM, font: { color: CLR_RED } }
|
| 258 |
+
: STYLES.CELL_NUM;
|
| 259 |
+
|
| 260 |
+
data.push([
|
| 261 |
+
c(row.date, STYLES.CELL_TEXT),
|
| 262 |
+
c(row.billNo, STYLES.CELL_MONO),
|
| 263 |
+
c(row.typeLabel, STYLES.CELL_TEXT),
|
| 264 |
+
c(row.directionLabel, STYLES.CELL_TEXT),
|
| 265 |
+
c(safeQty, STYLES.CELL_NUM, "0.00"),
|
| 266 |
+
c(safeRate, STYLES.CELL_NUM, "0.00"),
|
| 267 |
+
c(safeNet, STYLES.CELL_NUM, FMT_CURRENCY),
|
| 268 |
+
c(safeProfit, impactStyle, FMT_CURRENCY)
|
| 269 |
+
]);
|
| 270 |
+
});
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
// --- GENERATE ---
|
| 274 |
+
const ws = XLSX.utils.aoa_to_sheet(data);
|
| 275 |
+
|
| 276 |
+
// --- MERGES (0-based indices) ---
|
| 277 |
+
ws['!merges'] = [
|
| 278 |
+
{ s: { r: 0, c: 0 }, e: { r: 0, c: 7 } }, // Title
|
| 279 |
+
{ s: { r: 2, c: 0 }, e: { r: 2, c: 5 } }, // Lot Number
|
| 280 |
+
{ s: { r: 3, c: 0 }, e: { r: 3, c: 5 } }, // Mirchi Name
|
| 281 |
+
// Purchase Card
|
| 282 |
+
{ s: { r: 5, c: 0 }, e: { r: 5, c: 3 } },
|
| 283 |
+
{ s: { r: 6, c: 0 }, e: { r: 6, c: 3 } },
|
| 284 |
+
// Sales Card
|
| 285 |
+
{ s: { r: 5, c: 4 }, e: { r: 5, c: 7 } },
|
| 286 |
+
{ s: { r: 6, c: 4 }, e: { r: 6, c: 7 } },
|
| 287 |
+
// Avg Line
|
| 288 |
+
{ s: { r: 8, c: 0 }, e: { r: 8, c: 5 } },
|
| 289 |
+
];
|
| 290 |
+
|
| 291 |
+
// --- COL WIDTHS ---
|
| 292 |
+
ws['!cols'] = [
|
| 293 |
+
{ wch: 14 }, // Date
|
| 294 |
+
{ wch: 22 }, // Bill No
|
| 295 |
+
{ wch: 12 }, // Type
|
| 296 |
+
{ wch: 14 }, // Imp/Exp
|
| 297 |
+
{ wch: 12 }, // Qty
|
| 298 |
+
{ wch: 10 }, // Rate
|
| 299 |
+
{ wch: 16 }, // Net Amt
|
| 300 |
+
{ wch: 16 }, // Profit
|
| 301 |
+
];
|
| 302 |
+
|
| 303 |
+
const wb = XLSX.utils.book_new();
|
| 304 |
+
const safeName = lot.lotNumber.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 30);
|
| 305 |
+
XLSX.utils.book_append_sheet(wb, ws, safeName);
|
| 306 |
+
XLSX.writeFile(wb, `Lot_${safeName}_Analysis.xlsx`);
|
| 307 |
+
};
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
// ==========================================
|
| 311 |
+
// 5. OTHER EXPORT FUNCTIONS (Parties/Ledger)
|
| 312 |
+
// ==========================================
|
| 313 |
+
|
| 314 |
+
export const exportPartyLedger = (party: Party, transactions: Transaction[], dateRange?: { from: string; to: string }) => {
|
| 315 |
+
// Filter & Sort
|
| 316 |
+
const filtered = (dateRange ? transactions.filter(t => {
|
| 317 |
+
const d = new Date(t.bill_date);
|
| 318 |
+
return d >= new Date(dateRange.from) && d <= new Date(dateRange.to);
|
| 319 |
+
}) : [...transactions]).sort((a, b) => new Date(a.bill_date).getTime() - new Date(b.bill_date).getTime());
|
| 320 |
+
|
| 321 |
+
const data: any[][] = [];
|
| 322 |
+
data.push([c(`Ledger: ${party.name}`, STYLES.TITLE)]);
|
| 323 |
+
data.push([c("")]);
|
| 324 |
+
data.push(['Date', 'Particulars', 'Credit', 'Debit', 'Balance', 'Dr/Cr'].map(h => c(h, STYLES.TABLE_HEAD)));
|
| 325 |
+
|
| 326 |
+
let running = 0, tCr = 0, tDr = 0;
|
| 327 |
+
filtered.forEach(tx => {
|
| 328 |
+
let cr = 0, dr = 0;
|
| 329 |
+
if (tx.bill_type === BillType.AWAAK) tx.is_return ? (cr = tx.total_amount) : (dr = tx.total_amount);
|
| 330 |
+
else tx.is_return ? (dr = tx.total_amount) : (cr = tx.total_amount);
|
| 331 |
+
|
| 332 |
+
tCr += cr; tDr += dr; running += (dr - cr);
|
| 333 |
+
const desc = `${tx.bill_number} (${tx.bill_type})` + (tx.items.length ? ` - ${tx.items[0].mirchi_name}` : "");
|
| 334 |
+
|
| 335 |
+
data.push([
|
| 336 |
+
c(new Date(tx.bill_date).toLocaleDateString('en-IN'), STYLES.CELL_TEXT),
|
| 337 |
+
c(desc, STYLES.CELL_TEXT),
|
| 338 |
+
c(cr || "", STYLES.CELL_NUM, FMT_CURRENCY),
|
| 339 |
+
c(dr || "", STYLES.CELL_NUM, FMT_CURRENCY),
|
| 340 |
+
c(Math.abs(running), STYLES.CELL_NUM, FMT_CURRENCY),
|
| 341 |
+
c(running > 0 ? 'Dr' : 'Cr', STYLES.CELL_TEXT)
|
| 342 |
+
]);
|
| 343 |
+
});
|
| 344 |
+
|
| 345 |
+
const totStyle = { ...STYLES.CELL_NUM, font: { bold: true } };
|
| 346 |
+
data.push([
|
| 347 |
+
c('TOTAL', { ...STYLES.CELL_TEXT, font: { bold: true } }), c(""),
|
| 348 |
+
c(tCr, totStyle, FMT_CURRENCY), c(tDr, totStyle, FMT_CURRENCY),
|
| 349 |
+
c(Math.abs(running), totStyle, FMT_CURRENCY),
|
| 350 |
+
c(running > 0 ? 'Dr' : 'Cr', { ...STYLES.CELL_TEXT, font: { bold: true } })
|
| 351 |
+
]);
|
| 352 |
+
|
| 353 |
+
const ws = XLSX.utils.aoa_to_sheet(data);
|
| 354 |
+
ws['!merges'] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 5 } }];
|
| 355 |
+
ws['!cols'] = [{ wch: 12 }, { wch: 40 }, { wch: 15 }, { wch: 15 }, { wch: 15 }, { wch: 8 }];
|
| 356 |
+
const wb = XLSX.utils.book_new();
|
| 357 |
+
XLSX.utils.book_append_sheet(wb, ws, 'Ledger');
|
| 358 |
+
XLSX.writeFile(wb, `${party.name.replace(/\s/g,'_')}_Ledger.xlsx`);
|
| 359 |
+
};
|
| 360 |
+
|
| 361 |
+
export const exportAllParties = (parties: Party[]) => {
|
| 362 |
+
const data: any[][] = [];
|
| 363 |
+
data.push(['Party Name', 'Phone', 'City', 'Type', 'Balance', 'Status'].map(h => c(h, STYLES.TABLE_HEAD)));
|
| 364 |
+
let tRec = 0, tPay = 0;
|
| 365 |
+
|
| 366 |
+
parties.forEach(p => {
|
| 367 |
+
if(p.current_balance > 0) tRec += p.current_balance;
|
| 368 |
+
if(p.current_balance < 0) tPay += Math.abs(p.current_balance);
|
| 369 |
+
const color = p.current_balance > 0 ? CLR_GREEN : p.current_balance < 0 ? CLR_RED : undefined;
|
| 370 |
+
|
| 371 |
+
data.push([
|
| 372 |
+
c(p.name, STYLES.CELL_TEXT), c(p.phone||'-', STYLES.CELL_TEXT), c(p.city||'-', STYLES.CELL_TEXT),
|
| 373 |
+
c(p.party_type, STYLES.CELL_TEXT),
|
| 374 |
+
c(p.current_balance, { ...STYLES.CELL_NUM, font: { color } }, FMT_CURRENCY),
|
| 375 |
+
c(p.current_balance > 0 ? 'Rec' : p.current_balance < 0 ? 'Pay' : '-', STYLES.CELL_TEXT)
|
| 376 |
+
]);
|
| 377 |
+
});
|
| 378 |
+
|
| 379 |
+
data.push([c("")]); data.push([c('SUMMARY', { font: { bold: true } })]);
|
| 380 |
+
data.push([c('Total Receivable'), c(""), c(""), c(""), c(tRec, STYLES.CELL_NUM, FMT_CURRENCY)]);
|
| 381 |
+
data.push([c('Total Payable'), c(""), c(""), c(""), c(tPay, STYLES.CELL_NUM, FMT_CURRENCY)]);
|
| 382 |
+
|
| 383 |
+
const ws = XLSX.utils.aoa_to_sheet(data);
|
| 384 |
+
ws['!cols'] = [{ wch: 30 }, { wch: 15 }, { wch: 20 }, { wch: 10 }, { wch: 18 }, { wch: 10 }];
|
| 385 |
+
const wb = XLSX.utils.book_new();
|
| 386 |
+
XLSX.utils.book_append_sheet(wb, ws, 'Parties');
|
| 387 |
+
XLSX.writeFile(wb, `All_Parties.xlsx`);
|
| 388 |
+
};
|
| 389 |
+
|
| 390 |
+
export const exportAllTransactions = (transactions: Transaction[]) => {
|
| 391 |
+
const data: any[][] = [];
|
| 392 |
+
data.push(['Date', 'Bill No', 'Party', 'Type', 'Amount', 'Paid', 'Balance'].map(h => c(h, STYLES.TABLE_HEAD)));
|
| 393 |
+
transactions.forEach(tx => {
|
| 394 |
+
data.push([
|
| 395 |
+
c(new Date(tx.bill_date).toLocaleDateString('en-IN'), STYLES.CELL_TEXT),
|
| 396 |
+
c(tx.bill_number, STYLES.CELL_MONO), c(tx.party_name, STYLES.CELL_TEXT), c(tx.bill_type, STYLES.CELL_TEXT),
|
| 397 |
+
c(tx.total_amount, STYLES.CELL_NUM, FMT_CURRENCY),
|
| 398 |
+
c(tx.paid_amount, STYLES.CELL_NUM, FMT_CURRENCY),
|
| 399 |
+
c(tx.balance_amount, STYLES.CELL_NUM, FMT_CURRENCY)
|
| 400 |
+
]);
|
| 401 |
+
});
|
| 402 |
+
const ws = XLSX.utils.aoa_to_sheet(data);
|
| 403 |
+
ws['!cols'] = [{ wch: 12 }, { wch: 20 }, { wch: 25 }, { wch: 10 }, { wch: 15 }, { wch: 15 }, { wch: 15 }];
|
| 404 |
+
const wb = XLSX.utils.book_new();
|
| 405 |
+
XLSX.utils.book_append_sheet(wb, ws, 'Transactions');
|
| 406 |
+
XLSX.writeFile(wb, `All_Transactions.xlsx`);
|
| 407 |
+
};
|
| 408 |
+
|
| 409 |
+
export const exportAnalysisReport = (rows: AnalysisExportRow[]) => {
|
| 410 |
+
if (!rows.length) return;
|
| 411 |
+
const data: any[][] = [];
|
| 412 |
+
const headers = ['LOT','Mirchi','Buy Qty','Buy Val','Avg Buy','Sell Qty','Sell Val','Avg Sell','Rem Qty','Rem Val','Profit','Margin %'];
|
| 413 |
+
data.push(headers.map(h => c(h, STYLES.TABLE_HEAD)));
|
| 414 |
+
|
| 415 |
+
rows.forEach(r => {
|
| 416 |
+
const pStyle = r.realizedProfit > 0 ? { ...STYLES.CELL_NUM, font: { color: CLR_GREEN } } : r.realizedProfit < 0 ? { ...STYLES.CELL_NUM, font: { color: CLR_RED } } : STYLES.CELL_NUM;
|
| 417 |
+
data.push([
|
| 418 |
+
c(r.lotNumber, STYLES.CELL_TEXT), c(r.mirchiName, STYLES.CELL_TEXT),
|
| 419 |
+
c(r.purchaseQty, STYLES.CELL_NUM,"0.00"), c(r.purchaseValue, STYLES.CELL_NUM,FMT_CURRENCY), c(r.avgPurchaseRate, STYLES.CELL_NUM,FMT_CURRENCY),
|
| 420 |
+
c(r.soldQty, STYLES.CELL_NUM,"0.00"), c(r.salesValue, STYLES.CELL_NUM,FMT_CURRENCY), c(r.avgSaleRate, STYLES.CELL_NUM,FMT_CURRENCY),
|
| 421 |
+
c(r.remainingQty, STYLES.CELL_NUM,"0.00"), c(r.remainingValueAtCost, STYLES.CELL_NUM,FMT_CURRENCY),
|
| 422 |
+
c(r.realizedProfit, pStyle,FMT_CURRENCY), c(r.marginPercent/100, STYLES.CELL_NUM,"0.00%")
|
| 423 |
+
]);
|
| 424 |
+
});
|
| 425 |
+
const ws = XLSX.utils.aoa_to_sheet(data);
|
| 426 |
+
ws['!cols'] = headers.map(() => ({ wch: 14 }));
|
| 427 |
+
const wb = XLSX.utils.book_new();
|
| 428 |
+
XLSX.utils.book_append_sheet(wb, ws, 'Summary');
|
| 429 |
+
XLSX.writeFile(wb, `Analysis_Summary.xlsx`);
|
| 430 |
+
};
|
vite.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins: [react()],
|
| 6 |
+
server: {
|
| 7 |
+
host: true, // Listen on all addresses
|
| 8 |
+
port: 3000
|
| 9 |
+
},
|
| 10 |
+
preview: {
|
| 11 |
+
host: true, // Listen on all addresses
|
| 12 |
+
port: 7860,
|
| 13 |
+
strictPort: true,
|
| 14 |
+
allowedHosts: ['antaram-pratikpattanshetty.hf.space'] // Add this line
|
| 15 |
+
}
|
| 16 |
+
})
|