Upload folder using huggingface_hub
Browse files- .env.example +9 -0
- .gitignore +8 -0
- README.md +20 -0
- assets/.aistudio/.gitignore +1 -0
- index.html +13 -0
- metadata.json +6 -0
- package.json +35 -0
- src/App.tsx +545 -0
- src/components/Dashboard.tsx +507 -0
- src/components/InventoryManager.tsx +714 -0
- src/components/InvoiceGenerator.tsx +1277 -0
- src/components/Settings.tsx +490 -0
- src/components/SocialPromoter.tsx +408 -0
- src/components/StaffManager.tsx +347 -0
- src/index.css +1 -0
- src/initialData.ts +387 -0
- src/main.tsx +10 -0
- src/types.ts +91 -0
- src/utils/backup.ts +84 -0
- tsconfig.json +26 -0
- vite.config.ts +22 -0
.env.example
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
| 2 |
+
# AI Studio automatically injects this at runtime from user secrets.
|
| 3 |
+
# Users configure this via the Secrets panel in the AI Studio UI.
|
| 4 |
+
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
| 5 |
+
|
| 6 |
+
# APP_URL: The URL where this applet is hosted.
|
| 7 |
+
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
| 8 |
+
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
| 9 |
+
APP_URL="MY_APP_URL"
|
.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
build/
|
| 3 |
+
dist/
|
| 4 |
+
coverage/
|
| 5 |
+
.DS_Store
|
| 6 |
+
*.log
|
| 7 |
+
.env*
|
| 8 |
+
!.env.example
|
README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div align="center">
|
| 2 |
+
<img width="1200" height="475" alt="GHBanner" src="https://ai.google.dev/static/site-assets/images/share-ais-513315318.png" />
|
| 3 |
+
</div>
|
| 4 |
+
|
| 5 |
+
# Run and deploy your AI Studio app
|
| 6 |
+
|
| 7 |
+
This contains everything you need to run your app locally.
|
| 8 |
+
|
| 9 |
+
View your app in AI Studio: https://ai.studio/apps/e3c86eb8-3256-4b0c-aa17-a2893c04b228
|
| 10 |
+
|
| 11 |
+
## Run Locally
|
| 12 |
+
|
| 13 |
+
**Prerequisites:** Node.js
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
1. Install dependencies:
|
| 17 |
+
`npm install`
|
| 18 |
+
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
| 19 |
+
3. Run the app:
|
| 20 |
+
`npm run dev`
|
assets/.aistudio/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
*
|
index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>My Google AI Studio App</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<div id="root"></div>
|
| 10 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 11 |
+
</body>
|
| 12 |
+
</html>
|
| 13 |
+
|
metadata.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Haider Brother Traders",
|
| 3 |
+
"description": "Invoice generator and shop management software for Haider Brother Traders tire shop, containing real-time stock tracking, monthly reports, automated receipt layouts, multi-user permissions, and off-line modes.",
|
| 4 |
+
"requestFramePermissions": [],
|
| 5 |
+
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
| 6 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "react-example",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite --port=3000 --host=0.0.0.0",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview",
|
| 10 |
+
"clean": "rm -rf dist server.js",
|
| 11 |
+
"lint": "tsc --noEmit"
|
| 12 |
+
},
|
| 13 |
+
"dependencies": {
|
| 14 |
+
"@google/genai": "^2.4.0",
|
| 15 |
+
"@tailwindcss/vite": "^4.1.14",
|
| 16 |
+
"@vitejs/plugin-react": "^5.0.4",
|
| 17 |
+
"lucide-react": "^0.546.0",
|
| 18 |
+
"react": "^19.0.1",
|
| 19 |
+
"react-dom": "^19.0.1",
|
| 20 |
+
"vite": "^6.2.3",
|
| 21 |
+
"express": "^4.21.2",
|
| 22 |
+
"dotenv": "^17.2.3",
|
| 23 |
+
"motion": "^12.23.24"
|
| 24 |
+
},
|
| 25 |
+
"devDependencies": {
|
| 26 |
+
"@types/node": "^22.14.0",
|
| 27 |
+
"autoprefixer": "^10.4.21",
|
| 28 |
+
"esbuild": "^0.25.0",
|
| 29 |
+
"tailwindcss": "^4.1.14",
|
| 30 |
+
"tsx": "^4.21.0",
|
| 31 |
+
"typescript": "~5.8.2",
|
| 32 |
+
"vite": "^6.2.3",
|
| 33 |
+
"@types/express": "^4.17.21"
|
| 34 |
+
}
|
| 35 |
+
}
|
src/App.tsx
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @license
|
| 3 |
+
* SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import React, { useState, useEffect } from 'react';
|
| 7 |
+
import { motion, AnimatePresence } from 'motion/react';
|
| 8 |
+
import {
|
| 9 |
+
LayoutDashboard,
|
| 10 |
+
ShoppingCart,
|
| 11 |
+
Package,
|
| 12 |
+
Users,
|
| 13 |
+
Megaphone,
|
| 14 |
+
Settings,
|
| 15 |
+
Activity,
|
| 16 |
+
CloudLightning,
|
| 17 |
+
Wifi,
|
| 18 |
+
WifiOff,
|
| 19 |
+
Clock,
|
| 20 |
+
PhoneCall,
|
| 21 |
+
UserCircle
|
| 22 |
+
} from 'lucide-react';
|
| 23 |
+
|
| 24 |
+
import { TireProduct, StockHistoryItem, StaffUser, Invoice, SystemSettings, StaffRole } from './types';
|
| 25 |
+
import {
|
| 26 |
+
INITIAL_PRODUCTS,
|
| 27 |
+
INITIAL_HISTORY,
|
| 28 |
+
INITIAL_STAFF,
|
| 29 |
+
INITIAL_INVOICES,
|
| 30 |
+
INITIAL_SETTINGS
|
| 31 |
+
} from './initialData';
|
| 32 |
+
|
| 33 |
+
// Subcomponents
|
| 34 |
+
import Dashboard from './components/Dashboard';
|
| 35 |
+
import InvoiceGenerator from './components/InvoiceGenerator';
|
| 36 |
+
import InventoryManager from './components/InventoryManager';
|
| 37 |
+
import StaffManager from './components/StaffManager';
|
| 38 |
+
import SocialPromoter from './components/SocialPromoter';
|
| 39 |
+
import SettingsComponent from './components/Settings';
|
| 40 |
+
|
| 41 |
+
export default function App() {
|
| 42 |
+
const [activeTab, setActiveTab] = useState('dashboard');
|
| 43 |
+
const [isOnline, setIsOnline] = useState(true);
|
| 44 |
+
const [currentTime, setCurrentTime] = useState(new Date());
|
| 45 |
+
|
| 46 |
+
// Core database state hooks
|
| 47 |
+
const [settings, setSettings] = useState<SystemSettings>(INITIAL_SETTINGS);
|
| 48 |
+
const [products, setProducts] = useState<TireProduct[]>(INITIAL_PRODUCTS);
|
| 49 |
+
const [invoices, setInvoices] = useState<Invoice[]>(INITIAL_INVOICES);
|
| 50 |
+
const [history, setHistory] = useState<StockHistoryItem[]>(INITIAL_HISTORY);
|
| 51 |
+
const [staffList, setStaffList] = useState<StaffUser[]>(INITIAL_STAFF);
|
| 52 |
+
const [currentStaff, setCurrentStaff] = useState<StaffUser>(INITIAL_STAFF[1]); // Zin Brother as default cashier
|
| 53 |
+
|
| 54 |
+
// Invoice review overlay trigger state
|
| 55 |
+
const [selectedInvoiceForView, setSelectedInvoiceForView] = useState<Invoice | null>(null);
|
| 56 |
+
|
| 57 |
+
// Initialize and load from local storage if existing
|
| 58 |
+
useEffect(() => {
|
| 59 |
+
// Clock tick
|
| 60 |
+
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
| 61 |
+
|
| 62 |
+
const storedSettings = localStorage.getItem('HBT_SETTINGS');
|
| 63 |
+
const storedProducts = localStorage.getItem('HBT_PRODUCTS');
|
| 64 |
+
const storedInvoices = localStorage.getItem('HBT_INVOICES');
|
| 65 |
+
const storedHistory = localStorage.getItem('HBT_HISTORY');
|
| 66 |
+
const storedStaff = localStorage.getItem('HBT_STAFF');
|
| 67 |
+
const storedCurrent = localStorage.getItem('HBT_CURRENT_STAFF');
|
| 68 |
+
|
| 69 |
+
if (storedSettings) setSettings(JSON.parse(storedSettings));
|
| 70 |
+
if (storedProducts) setProducts(JSON.parse(storedProducts));
|
| 71 |
+
if (storedInvoices) setInvoices(JSON.parse(storedInvoices));
|
| 72 |
+
if (storedHistory) setHistory(JSON.parse(storedHistory));
|
| 73 |
+
if (storedStaff) setStaffList(JSON.parse(storedStaff));
|
| 74 |
+
if (storedCurrent) {
|
| 75 |
+
const parsedCurrent = JSON.parse(storedCurrent);
|
| 76 |
+
// Ensure currentStaff matches the parsed currentStaff safely
|
| 77 |
+
setCurrentStaff(parsedCurrent);
|
| 78 |
+
} else {
|
| 79 |
+
setCurrentStaff(INITIAL_STAFF[1]); // Zain Brother manager
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
return () => clearInterval(timer);
|
| 83 |
+
}, []);
|
| 84 |
+
|
| 85 |
+
// Sync utilities to persistence
|
| 86 |
+
const syncToLocalStorage = (key: string, data: any) => {
|
| 87 |
+
localStorage.setItem(key, JSON.stringify(data));
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
// State Adjustments multipliers
|
| 91 |
+
const handleUpdateSettings = (updated: SystemSettings) => {
|
| 92 |
+
setSettings(updated);
|
| 93 |
+
syncToLocalStorage('HBT_SETTINGS', updated);
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
// Triggered from Dashboard replenishment alarms
|
| 97 |
+
const handleQuickAddStock = (productId: string, amount: number) => {
|
| 98 |
+
const adjustedProducts = products.map(p => {
|
| 99 |
+
if (p.id === productId) {
|
| 100 |
+
const resultingStock = p.stock + amount;
|
| 101 |
+
|
| 102 |
+
// Append history log immediately
|
| 103 |
+
const logItem: StockHistoryItem = {
|
| 104 |
+
id: 'log_' + Math.random().toString(36).substr(2, 9),
|
| 105 |
+
productId: p.id,
|
| 106 |
+
productLabel: `${p.brand} ${p.model} (${p.size})`,
|
| 107 |
+
dateTime: new Date().toISOString(),
|
| 108 |
+
type: 'STOCK_IN',
|
| 109 |
+
quantity: amount,
|
| 110 |
+
resultingStock,
|
| 111 |
+
adjustedBy: `${currentStaff.name} (${currentStaff.role})`,
|
| 112 |
+
reason: `Quick Restock of Alarm: Restocked cargo pallet.`
|
| 113 |
+
};
|
| 114 |
+
|
| 115 |
+
const updatedHistory = [logItem, ...history];
|
| 116 |
+
setHistory(updatedHistory);
|
| 117 |
+
syncToLocalStorage('HBT_HISTORY', updatedHistory);
|
| 118 |
+
|
| 119 |
+
return { ...p, stock: resultingStock };
|
| 120 |
+
}
|
| 121 |
+
return p;
|
| 122 |
+
});
|
| 123 |
+
|
| 124 |
+
setProducts(adjustedProducts);
|
| 125 |
+
syncToLocalStorage('HBT_PRODUCTS', adjustedProducts);
|
| 126 |
+
};
|
| 127 |
+
|
| 128 |
+
// Triggered from InventoryManager manual configurations
|
| 129 |
+
const handleAdjustProductStock = (
|
| 130 |
+
productId: string,
|
| 131 |
+
quantityChange: number,
|
| 132 |
+
type: StockHistoryItem['type'],
|
| 133 |
+
reason: string
|
| 134 |
+
) => {
|
| 135 |
+
const adjustedProducts = products.map(p => {
|
| 136 |
+
if (p.id === productId) {
|
| 137 |
+
const resultingStock = p.stock + quantityChange;
|
| 138 |
+
|
| 139 |
+
// Create log item
|
| 140 |
+
const logItem: StockHistoryItem = {
|
| 141 |
+
id: 'log_' + Math.random().toString(36).substr(2, 9),
|
| 142 |
+
productId: p.id,
|
| 143 |
+
productLabel: `${p.brand} ${p.model} (${p.size})`,
|
| 144 |
+
dateTime: new Date().toISOString(),
|
| 145 |
+
type,
|
| 146 |
+
quantity: Math.abs(quantityChange),
|
| 147 |
+
resultingStock,
|
| 148 |
+
adjustedBy: `${currentStaff.name} (${currentStaff.role})`,
|
| 149 |
+
reason
|
| 150 |
+
};
|
| 151 |
+
|
| 152 |
+
const updatedHistory = [logItem, ...history];
|
| 153 |
+
setHistory(updatedHistory);
|
| 154 |
+
syncToLocalStorage('HBT_HISTORY', updatedHistory);
|
| 155 |
+
|
| 156 |
+
return { ...p, stock: resultingStock };
|
| 157 |
+
}
|
| 158 |
+
return p;
|
| 159 |
+
});
|
| 160 |
+
|
| 161 |
+
setProducts(adjustedProducts);
|
| 162 |
+
syncToLocalStorage('HBT_PRODUCTS', adjustedProducts);
|
| 163 |
+
};
|
| 164 |
+
|
| 165 |
+
const handleAddNewProduct = (prod: TireProduct) => {
|
| 166 |
+
const updatedProducts = [prod, ...products];
|
| 167 |
+
setProducts(updatedProducts);
|
| 168 |
+
syncToLocalStorage('HBT_PRODUCTS', updatedProducts);
|
| 169 |
+
|
| 170 |
+
// Write initial STOCK_IN history entry
|
| 171 |
+
const logItem: StockHistoryItem = {
|
| 172 |
+
id: 'log_' + Math.random().toString(36).substr(2, 9),
|
| 173 |
+
productId: prod.id,
|
| 174 |
+
productLabel: `${prod.brand} ${prod.model} (${prod.size})`,
|
| 175 |
+
dateTime: new Date().toISOString(),
|
| 176 |
+
type: 'STOCK_IN',
|
| 177 |
+
quantity: prod.stock,
|
| 178 |
+
resultingStock: prod.stock,
|
| 179 |
+
adjustedBy: `${currentStaff.name} (${currentStaff.role})`,
|
| 180 |
+
reason: `First inventory system initialization of tyre profile SKU.`
|
| 181 |
+
};
|
| 182 |
+
|
| 183 |
+
const updatedHistory = [logItem, ...history];
|
| 184 |
+
setHistory(updatedHistory);
|
| 185 |
+
syncToLocalStorage('HBT_HISTORY', updatedHistory);
|
| 186 |
+
};
|
| 187 |
+
|
| 188 |
+
const handleDeleteProduct = (productId: string) => {
|
| 189 |
+
const updatedProducts = products.filter(p => p.id !== productId);
|
| 190 |
+
setProducts(updatedProducts);
|
| 191 |
+
syncToLocalStorage('HBT_PRODUCTS', updatedProducts);
|
| 192 |
+
};
|
| 193 |
+
|
| 194 |
+
// Adding Client Invoice automatically reduces matching tire quantities and writes logs
|
| 195 |
+
const handleAddInvoice = (newInv: Invoice) => {
|
| 196 |
+
// Append invoice
|
| 197 |
+
const updatedInvoices = [...invoices, newInv];
|
| 198 |
+
setInvoices(updatedInvoices);
|
| 199 |
+
syncToLocalStorage('HBT_INVOICES', updatedInvoices);
|
| 200 |
+
|
| 201 |
+
// Iterate items and decrease physical stock counts
|
| 202 |
+
let revisedProducts = [...products];
|
| 203 |
+
let newLogs: StockHistoryItem[] = [];
|
| 204 |
+
|
| 205 |
+
newInv.items.forEach((item) => {
|
| 206 |
+
revisedProducts = revisedProducts.map(p => {
|
| 207 |
+
if (p.id === item.productId) {
|
| 208 |
+
const resultingStock = p.stock - item.quantity;
|
| 209 |
+
|
| 210 |
+
// Construct checkout history block
|
| 211 |
+
const logItem: StockHistoryItem = {
|
| 212 |
+
id: 'log_' + Math.random().toString(36).substr(2, 9),
|
| 213 |
+
productId: p.id,
|
| 214 |
+
productLabel: `${p.brand} ${p.model} (${p.size})`,
|
| 215 |
+
dateTime: new Date().toISOString(),
|
| 216 |
+
type: 'STOCK_OUT',
|
| 217 |
+
quantity: item.quantity,
|
| 218 |
+
resultingStock,
|
| 219 |
+
adjustedBy: `${currentStaff.name} (${currentStaff.role})`,
|
| 220 |
+
reason: `Retail Invoice checkout sales: Ref ${newInv.invoiceNumber}`
|
| 221 |
+
};
|
| 222 |
+
newLogs.push(logItem);
|
| 223 |
+
|
| 224 |
+
return { ...p, stock: resultingStock };
|
| 225 |
+
}
|
| 226 |
+
return p;
|
| 227 |
+
});
|
| 228 |
+
});
|
| 229 |
+
|
| 230 |
+
const revisedHistory = [...newLogs, ...history];
|
| 231 |
+
setHistory(revisedHistory);
|
| 232 |
+
setProducts(revisedProducts);
|
| 233 |
+
|
| 234 |
+
syncToLocalStorage('HBT_PRODUCTS', revisedProducts);
|
| 235 |
+
syncToLocalStorage('HBT_HISTORY', revisedHistory);
|
| 236 |
+
};
|
| 237 |
+
|
| 238 |
+
// Staff managers
|
| 239 |
+
const handleAddStaffMember = (newUser: StaffUser) => {
|
| 240 |
+
const updated = [...staffList, newUser];
|
| 241 |
+
setStaffList(updated);
|
| 242 |
+
syncToLocalStorage('HBT_STAFF', updated);
|
| 243 |
+
};
|
| 244 |
+
|
| 245 |
+
const handleToggleStaffStatus = (staffId: string) => {
|
| 246 |
+
const updated = staffList.map(s =>
|
| 247 |
+
s.id === staffId ? { ...s, active: !s.active } : s
|
| 248 |
+
);
|
| 249 |
+
setStaffList(updated);
|
| 250 |
+
syncToLocalStorage('HBT_STAFF', updated);
|
| 251 |
+
};
|
| 252 |
+
|
| 253 |
+
const handleUpdateStaffRole = (staffId: string, role: StaffRole) => {
|
| 254 |
+
const updated = staffList.map(s =>
|
| 255 |
+
s.id === staffId ? { ...s, role } : s
|
| 256 |
+
);
|
| 257 |
+
setStaffList(updated);
|
| 258 |
+
syncToLocalStorage('HBT_STAFF', updated);
|
| 259 |
+
};
|
| 260 |
+
|
| 261 |
+
const handleSetCurrentStaff = (staff: StaffUser) => {
|
| 262 |
+
setCurrentStaff(staff);
|
| 263 |
+
syncToLocalStorage('HBT_CURRENT_STAFF', staff);
|
| 264 |
+
};
|
| 265 |
+
|
| 266 |
+
// Decrypted Restore logic
|
| 267 |
+
const handleRestoreDatabase = (decoded: {
|
| 268 |
+
settings: SystemSettings;
|
| 269 |
+
products: any[];
|
| 270 |
+
invoices: any[];
|
| 271 |
+
history: any[];
|
| 272 |
+
staff: any[];
|
| 273 |
+
}) => {
|
| 274 |
+
setSettings(decoded.settings);
|
| 275 |
+
setProducts(decoded.products);
|
| 276 |
+
setInvoices(decoded.invoices);
|
| 277 |
+
setHistory(decoded.history);
|
| 278 |
+
setStaffList(decoded.staff);
|
| 279 |
+
|
| 280 |
+
// Save and persist all entries
|
| 281 |
+
syncToLocalStorage('HBT_SETTINGS', decoded.settings);
|
| 282 |
+
syncToLocalStorage('HBT_PRODUCTS', decoded.products);
|
| 283 |
+
syncToLocalStorage('HBT_INVOICES', decoded.invoices);
|
| 284 |
+
syncToLocalStorage('HBT_HISTORY', decoded.history);
|
| 285 |
+
syncToLocalStorage('HBT_STAFF', decoded.staff);
|
| 286 |
+
|
| 287 |
+
// Set Owner as current operator safely if there's any owner in the backup list
|
| 288 |
+
const firstOwner = decoded.staff.find(s => s.role === 'owner' && s.active);
|
| 289 |
+
if (firstOwner) {
|
| 290 |
+
setCurrentStaff(firstOwner);
|
| 291 |
+
syncToLocalStorage('HBT_CURRENT_STAFF', firstOwner);
|
| 292 |
+
}
|
| 293 |
+
};
|
| 294 |
+
|
| 295 |
+
// Factory defaults
|
| 296 |
+
const handleClearCacheToDefault = () => {
|
| 297 |
+
localStorage.clear();
|
| 298 |
+
};
|
| 299 |
+
|
| 300 |
+
return (
|
| 301 |
+
<div className="min-h-screen bg-[#F8FAFC] flex flex-col md:flex-row antialiased select-none font-sans" id="application-shell">
|
| 302 |
+
{/* 1. COLLAPSIBLE OR RESPONSIVE STATIC SIDEBAR */}
|
| 303 |
+
<aside className="w-full md:w-64 bg-white border-b md:border-r border-slate-200 text-slate-800 flex flex-col justify-between shrink-0 shadow-sm" id="sidebar-panel">
|
| 304 |
+
<div className="space-y-6">
|
| 305 |
+
{/* Top Shop Brand Logo */}
|
| 306 |
+
<div className="p-6 border-b border-slate-100 flex items-center justify-between">
|
| 307 |
+
<div className="flex items-center gap-3">
|
| 308 |
+
<div className="w-9 h-9 rounded-lg bg-slate-900 flex items-center justify-center font-bold text-white text-base shadow-sm">
|
| 309 |
+
HBT
|
| 310 |
+
</div>
|
| 311 |
+
<div className="space-y-0.5">
|
| 312 |
+
<h2 className="text-xs font-bold tracking-tight text-slate-900 uppercase">Haider Brothers</h2>
|
| 313 |
+
<span className="text-[10px] text-slate-400 font-medium block">Tire Shop Manager</span>
|
| 314 |
+
</div>
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
|
| 318 |
+
{/* Navigation Links list */}
|
| 319 |
+
<nav className="px-3 space-y-1">
|
| 320 |
+
<button
|
| 321 |
+
onClick={() => setActiveTab('dashboard')}
|
| 322 |
+
className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-lg text-xs font-medium font-sans transition cursor-pointer ${
|
| 323 |
+
activeTab === 'dashboard'
|
| 324 |
+
? 'bg-slate-900 text-white shadow-sm font-semibold'
|
| 325 |
+
: 'text-slate-500 hover:text-slate-900 hover:bg-slate-50'
|
| 326 |
+
}`}
|
| 327 |
+
>
|
| 328 |
+
<LayoutDashboard className="w-4 h-4 text-inherit" />
|
| 329 |
+
Reporting Cockpit
|
| 330 |
+
</button>
|
| 331 |
+
|
| 332 |
+
<button
|
| 333 |
+
onClick={() => {
|
| 334 |
+
setActiveTab('invoices');
|
| 335 |
+
setSelectedInvoiceForView(null);
|
| 336 |
+
}}
|
| 337 |
+
className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-lg text-xs font-medium font-sans transition cursor-pointer ${
|
| 338 |
+
activeTab === 'invoices'
|
| 339 |
+
? 'bg-slate-900 text-white shadow-sm font-semibold'
|
| 340 |
+
: 'text-slate-500 hover:text-slate-900 hover:bg-slate-50'
|
| 341 |
+
}`}
|
| 342 |
+
>
|
| 343 |
+
<ShoppingCart className="w-4 h-4 text-inherit" />
|
| 344 |
+
Invoice Generator
|
| 345 |
+
</button>
|
| 346 |
+
|
| 347 |
+
<button
|
| 348 |
+
onClick={() => setActiveTab('inventory')}
|
| 349 |
+
className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-lg text-xs font-medium font-sans transition cursor-pointer ${
|
| 350 |
+
activeTab === 'inventory'
|
| 351 |
+
? 'bg-slate-900 text-white shadow-sm font-semibold'
|
| 352 |
+
: 'text-slate-500 hover:text-slate-900 hover:bg-slate-50'
|
| 353 |
+
}`}
|
| 354 |
+
>
|
| 355 |
+
<Package className="w-4 h-4 text-inherit" />
|
| 356 |
+
Stock Inventory
|
| 357 |
+
</button>
|
| 358 |
+
|
| 359 |
+
<button
|
| 360 |
+
onClick={() => setActiveTab('staff')}
|
| 361 |
+
className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-lg text-xs font-medium font-sans transition cursor-pointer ${
|
| 362 |
+
activeTab === 'staff'
|
| 363 |
+
? 'bg-slate-900 text-white shadow-sm font-semibold'
|
| 364 |
+
: 'text-slate-500 hover:text-slate-900 hover:bg-slate-50'
|
| 365 |
+
}`}
|
| 366 |
+
>
|
| 367 |
+
<Users className="w-4 h-4 text-inherit" />
|
| 368 |
+
Cashiers & RBAC
|
| 369 |
+
</button>
|
| 370 |
+
|
| 371 |
+
<button
|
| 372 |
+
onClick={() => setActiveTab('social')}
|
| 373 |
+
className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-lg text-xs font-medium font-sans transition cursor-pointer ${
|
| 374 |
+
activeTab === 'social'
|
| 375 |
+
? 'bg-slate-900 text-white shadow-sm font-semibold'
|
| 376 |
+
: 'text-slate-500 hover:text-slate-900 hover:bg-slate-50'
|
| 377 |
+
}`}
|
| 378 |
+
>
|
| 379 |
+
<Megaphone className="w-4 h-4 text-inherit" />
|
| 380 |
+
Outreach Promos
|
| 381 |
+
</button>
|
| 382 |
+
|
| 383 |
+
<button
|
| 384 |
+
onClick={() => setActiveTab('settings')}
|
| 385 |
+
className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-lg text-xs font-medium font-sans transition cursor-pointer ${
|
| 386 |
+
activeTab === 'settings'
|
| 387 |
+
? 'bg-slate-900 text-white shadow-sm font-semibold'
|
| 388 |
+
: 'text-slate-500 hover:text-slate-900 hover:bg-slate-50'
|
| 389 |
+
}`}
|
| 390 |
+
>
|
| 391 |
+
<Settings className="w-4 h-4 text-inherit" />
|
| 392 |
+
Settings & Vault
|
| 393 |
+
</button>
|
| 394 |
+
</nav>
|
| 395 |
+
</div>
|
| 396 |
+
|
| 397 |
+
{/* Bottom Operator profile card in sidebar */}
|
| 398 |
+
<div className="p-4 border-t border-slate-100 bg-slate-50 space-y-2">
|
| 399 |
+
<div className="flex items-center gap-2.5 text-xs">
|
| 400 |
+
<UserCircle className="w-8 h-8 text-slate-400" />
|
| 401 |
+
<div className="flex-1 min-w-0">
|
| 402 |
+
<p className="font-semibold text-slate-950 truncate text-[11px]">{currentStaff.name}</p>
|
| 403 |
+
<p className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">{currentStaff.role}</p>
|
| 404 |
+
</div>
|
| 405 |
+
</div>
|
| 406 |
+
|
| 407 |
+
<div className="bg-white rounded-lg p-2 flex justify-between items-center text-[10px] border border-slate-200">
|
| 408 |
+
<span className="text-slate-400 font-medium">Node secure:</span>
|
| 409 |
+
<span className="font-mono text-emerald-600 font-bold flex items-center gap-1.5">
|
| 410 |
+
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full"></span>
|
| 411 |
+
ONLINE
|
| 412 |
+
</span>
|
| 413 |
+
</div>
|
| 414 |
+
</div>
|
| 415 |
+
</aside>
|
| 416 |
+
|
| 417 |
+
{/* 2. CORE WORKSPACE: HEADER + SCROLLABLE PAGE VIEWS */}
|
| 418 |
+
<main className="flex-1 flex flex-col min-w-0 overflow-y-auto h-screen" id="workspace-viewport">
|
| 419 |
+
{/* Top Navbar */}
|
| 420 |
+
<header className="bg-white border-b border-slate-200 px-6 md:px-8 py-4 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 shrink-0">
|
| 421 |
+
{/* Left info */}
|
| 422 |
+
<div className="flex items-center gap-2.5">
|
| 423 |
+
<span className="w-2 h-2 bg-slate-900 rounded-full animate-pulse"></span>
|
| 424 |
+
<span className="text-[11px] font-bold text-slate-800 font-sans tracking-wider uppercase">
|
| 425 |
+
SECURE CASHIER LEDGER
|
| 426 |
+
</span>
|
| 427 |
+
<span className="text-slate-200">|</span>
|
| 428 |
+
<span className="text-[11px] text-slate-500 flex items-center gap-1">
|
| 429 |
+
Store Support: {settings.shopPhone}
|
| 430 |
+
</span>
|
| 431 |
+
</div>
|
| 432 |
+
|
| 433 |
+
{/* Right systems specs */}
|
| 434 |
+
<div className="flex items-center gap-3.5 text-xs">
|
| 435 |
+
{/* Real-time Clock */}
|
| 436 |
+
<div className="text-slate-600 flex items-center gap-1.5 bg-[#F8FAFC] px-2.5 py-1.5 rounded-lg border border-slate-200/80">
|
| 437 |
+
<Clock className="w-3.5 h-3.5 text-slate-400" />
|
| 438 |
+
<span className="font-mono font-medium text-[11px] text-slate-700">{currentTime.toLocaleTimeString()}</span>
|
| 439 |
+
</div>
|
| 440 |
+
|
| 441 |
+
{/* Offline vs Online Beacons and alarms */}
|
| 442 |
+
{isOnline ? (
|
| 443 |
+
<span className="bg-emerald-50 text-emerald-800 border border-emerald-200/60 font-semibold font-sans px-2.5 py-1.5 rounded-lg flex items-center gap-1.5 text-[10px]">
|
| 444 |
+
<Wifi className="w-3.5 h-3.5 text-emerald-600" />
|
| 445 |
+
VAULT OK
|
| 446 |
+
</span>
|
| 447 |
+
) : (
|
| 448 |
+
<span className="bg-amber-50 text-amber-800 border border-amber-200/60 font-semibold font-sans px-2.5 py-1.5 rounded-lg flex items-center gap-1.5 text-[10px] animate-pulse">
|
| 449 |
+
<WifiOff className="w-3.5 h-3.5 text-amber-600" />
|
| 450 |
+
AIR-GAPPED LOCAL
|
| 451 |
+
</span>
|
| 452 |
+
)}
|
| 453 |
+
</div>
|
| 454 |
+
</header>
|
| 455 |
+
|
| 456 |
+
{/* Tab Pages dispatcher Router panel */}
|
| 457 |
+
<div className="flex-1 p-6 md:p-8 overflow-y-auto">
|
| 458 |
+
<AnimatePresence mode="wait">
|
| 459 |
+
<motion.div
|
| 460 |
+
key={activeTab}
|
| 461 |
+
initial={{ opacity: 0, y: 10 }}
|
| 462 |
+
animate={{ opacity: 1, y: 0 }}
|
| 463 |
+
exit={{ opacity: 0, y: -10 }}
|
| 464 |
+
transition={{ duration: 0.15 }}
|
| 465 |
+
className="h-full"
|
| 466 |
+
>
|
| 467 |
+
{activeTab === 'dashboard' && (
|
| 468 |
+
<Dashboard
|
| 469 |
+
products={products}
|
| 470 |
+
invoices={invoices}
|
| 471 |
+
currentStaff={currentStaff}
|
| 472 |
+
settings={settings}
|
| 473 |
+
setActiveTab={setActiveTab}
|
| 474 |
+
onQuickAddStock={handleQuickAddStock}
|
| 475 |
+
setSelectedInvoiceForView={(inv) => {
|
| 476 |
+
setSelectedInvoiceForView(inv);
|
| 477 |
+
setActiveTab('invoices');
|
| 478 |
+
}}
|
| 479 |
+
/>
|
| 480 |
+
)}
|
| 481 |
+
|
| 482 |
+
{activeTab === 'invoices' && (
|
| 483 |
+
<InvoiceGenerator
|
| 484 |
+
products={products}
|
| 485 |
+
invoices={invoices}
|
| 486 |
+
currentStaff={currentStaff}
|
| 487 |
+
settings={settings}
|
| 488 |
+
isOnline={isOnline}
|
| 489 |
+
onAddInvoice={handleAddInvoice}
|
| 490 |
+
selectedInvoiceForView={selectedInvoiceForView}
|
| 491 |
+
setSelectedInvoiceForView={setSelectedInvoiceForView}
|
| 492 |
+
/>
|
| 493 |
+
)}
|
| 494 |
+
|
| 495 |
+
{activeTab === 'inventory' && (
|
| 496 |
+
<InventoryManager
|
| 497 |
+
products={products}
|
| 498 |
+
history={history}
|
| 499 |
+
currentStaff={currentStaff}
|
| 500 |
+
settings={settings}
|
| 501 |
+
onAdjustProductStock={handleAdjustProductStock}
|
| 502 |
+
onAddNewProduct={handleAddNewProduct}
|
| 503 |
+
onDeleteProduct={handleDeleteProduct}
|
| 504 |
+
/>
|
| 505 |
+
)}
|
| 506 |
+
|
| 507 |
+
{activeTab === 'staff' && (
|
| 508 |
+
<StaffManager
|
| 509 |
+
staffList={staffList}
|
| 510 |
+
currentStaff={currentStaff}
|
| 511 |
+
onSetCurrentStaff={handleSetCurrentStaff}
|
| 512 |
+
onAddStaffMember={handleAddStaffMember}
|
| 513 |
+
onToggleStaffStatus={handleToggleStaffStatus}
|
| 514 |
+
onUpdateStaffRole={handleUpdateStaffRole}
|
| 515 |
+
/>
|
| 516 |
+
)}
|
| 517 |
+
|
| 518 |
+
{activeTab === 'social' && (
|
| 519 |
+
<SocialPromoter
|
| 520 |
+
products={products}
|
| 521 |
+
settings={settings}
|
| 522 |
+
/>
|
| 523 |
+
)}
|
| 524 |
+
|
| 525 |
+
{activeTab === 'settings' && (
|
| 526 |
+
<SettingsComponent
|
| 527 |
+
settings={settings}
|
| 528 |
+
isOnline={isOnline}
|
| 529 |
+
onUpdateSettings={handleUpdateSettings}
|
| 530 |
+
onToggleOnlineMode={() => setIsOnline(!isOnline)}
|
| 531 |
+
onRestoreDatabase={handleRestoreDatabase}
|
| 532 |
+
onClearCacheToDefault={handleClearCacheToDefault}
|
| 533 |
+
activeProducts={products}
|
| 534 |
+
activeInvoices={invoices}
|
| 535 |
+
activeHistory={history}
|
| 536 |
+
activeStaff={staffList}
|
| 537 |
+
/>
|
| 538 |
+
)}
|
| 539 |
+
</motion.div>
|
| 540 |
+
</AnimatePresence>
|
| 541 |
+
</div>
|
| 542 |
+
</main>
|
| 543 |
+
</div>
|
| 544 |
+
);
|
| 545 |
+
}
|
src/components/Dashboard.tsx
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @license
|
| 3 |
+
* SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import React, { useState } from 'react';
|
| 7 |
+
import { motion } from 'motion/react';
|
| 8 |
+
import {
|
| 9 |
+
TrendingUp,
|
| 10 |
+
Package,
|
| 11 |
+
AlertTriangle,
|
| 12 |
+
FileText,
|
| 13 |
+
UserCheck,
|
| 14 |
+
Currency,
|
| 15 |
+
ArrowRight,
|
| 16 |
+
Plus,
|
| 17 |
+
RefreshCw,
|
| 18 |
+
Search,
|
| 19 |
+
ShoppingCart
|
| 20 |
+
} from 'lucide-react';
|
| 21 |
+
import { TireProduct, Invoice, StaffUser, SystemSettings } from '../types';
|
| 22 |
+
|
| 23 |
+
interface DashboardProps {
|
| 24 |
+
products: TireProduct[];
|
| 25 |
+
invoices: Invoice[];
|
| 26 |
+
currentStaff: StaffUser;
|
| 27 |
+
settings: SystemSettings;
|
| 28 |
+
setActiveTab: (tab: string) => void;
|
| 29 |
+
onQuickAddStock: (productId: string, amount: number) => void;
|
| 30 |
+
setSelectedInvoiceForView: (invoice: Invoice | null) => void;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export default function Dashboard({
|
| 34 |
+
products,
|
| 35 |
+
invoices,
|
| 36 |
+
currentStaff,
|
| 37 |
+
settings,
|
| 38 |
+
setActiveTab,
|
| 39 |
+
onQuickAddStock,
|
| 40 |
+
setSelectedInvoiceForView
|
| 41 |
+
}: DashboardProps) {
|
| 42 |
+
const [replenishAmount, setReplenishAmount] = useState<{ [key: string]: number }>({});
|
| 43 |
+
const [searchTerm, setSearchTerm] = useState('');
|
| 44 |
+
|
| 45 |
+
// 1. Calculate general statistics
|
| 46 |
+
const totalInvoiced = invoices.reduce((sum, inv) => sum + (inv.paymentStatus === 'PAID' ? inv.total : 0), 0);
|
| 47 |
+
const totalActiveItems = products.reduce((sum, p) => sum + p.stock, 0);
|
| 48 |
+
const lowStockItems = products.filter(p => p.stock <= p.minStock);
|
| 49 |
+
const totalInvoicesCount = invoices.length;
|
| 50 |
+
|
| 51 |
+
const currentMonthName = "June 2026";
|
| 52 |
+
const juneInvoices = invoices.filter(inv => {
|
| 53 |
+
// Basic date checking
|
| 54 |
+
return inv.dateTime.includes('-06-') || inv.dateTime.includes('/06/') || inv.dateTime.startsWith('02-06-') || inv.dateTime.startsWith('03-06-');
|
| 55 |
+
});
|
| 56 |
+
const juneSalesTotal = juneInvoices.reduce((sum, inv) => sum + (inv.paymentStatus === 'PAID' ? inv.total : 0), 0);
|
| 57 |
+
|
| 58 |
+
// 2. Generate custom monthly line metrics for our 2026 sales chart
|
| 59 |
+
// Group invoices by month
|
| 60 |
+
const monthlyData = [
|
| 61 |
+
{ label: 'Jan', value: invoices.filter(i => i.dateTime.includes('-01-')).reduce((sum, i) => sum + i.total, 0) },
|
| 62 |
+
{ label: 'Feb', value: invoices.filter(i => i.dateTime.includes('-02-')).reduce((sum, i) => sum + i.total, 0) },
|
| 63 |
+
{ label: 'Mar', value: invoices.filter(i => i.dateTime.includes('-03-')).reduce((sum, i) => sum + i.total, 0) },
|
| 64 |
+
{ label: 'Apr', value: invoices.filter(i => i.dateTime.includes('-04-')).reduce((sum, i) => sum + i.total, 0) },
|
| 65 |
+
{ label: 'May', value: invoices.filter(i => i.dateTime.includes('-05-')).reduce((sum, i) => sum + i.total, 0) },
|
| 66 |
+
{ label: 'Jun', value: invoices.filter(i => i.dateTime.includes('-06-')).reduce((sum, i) => sum + i.total, 0) },
|
| 67 |
+
];
|
| 68 |
+
|
| 69 |
+
const maxMonthlyVal = Math.max(...monthlyData.map(d => d.value), 100000);
|
| 70 |
+
|
| 71 |
+
// 3. Category distribution
|
| 72 |
+
const categories = ['Passenger', 'SUV', 'Commercial', 'Light Truck', 'Truck/Bus'] as const;
|
| 73 |
+
const categoryStats = categories.map(cat => {
|
| 74 |
+
const items = products.filter(p => p.category === cat);
|
| 75 |
+
const count = items.reduce((sum, p) => sum + p.stock, 0);
|
| 76 |
+
const totalValue = items.reduce((sum, p) => sum + (p.stock * p.price), 0);
|
| 77 |
+
return { name: cat, count, value: totalValue };
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
const handleQuickAddClick = (productId: string) => {
|
| 81 |
+
const amt = replenishAmount[productId] || 10;
|
| 82 |
+
onQuickAddStock(productId, amt);
|
| 83 |
+
// Reset counter
|
| 84 |
+
setReplenishAmount(prev => ({ ...prev, [productId]: 10 }));
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
// Filter products for low stock alert segment
|
| 88 |
+
const filteredAlertProducts = lowStockItems.filter(p =>
|
| 89 |
+
p.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
| 90 |
+
p.model.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
| 91 |
+
p.size.toLowerCase().includes(searchTerm.toLowerCase())
|
| 92 |
+
);
|
| 93 |
+
|
| 94 |
+
return (
|
| 95 |
+
<div className="space-y-6" id="dashboard-container">
|
| 96 |
+
{/* Welcome Banner */}
|
| 97 |
+
<div className="bg-white border border-slate-200 rounded-xl p-6 md:p-8 text-slate-900 relative overflow-hidden flex flex-col md:flex-row justify-between items-start md:items-center gap-6 shadow-sm">
|
| 98 |
+
<div className="relative z-10 space-y-2.5">
|
| 99 |
+
<span className="bg-slate-100 text-slate-700 text-[10px] font-bold tracking-wider uppercase px-2.5 py-1 rounded-md border border-slate-200/60 inline-block font-sans">
|
| 100 |
+
★ Active Session — {currentStaff.role.toUpperCase()} MODE
|
| 101 |
+
</span>
|
| 102 |
+
<h1 className="text-2xl font-bold tracking-tight text-slate-900">
|
| 103 |
+
{settings.shopName}
|
| 104 |
+
</h1>
|
| 105 |
+
<p className="text-slate-500 text-xs max-w-xl leading-relaxed">
|
| 106 |
+
Direct invoicing, tax calculations, dynamic offline persistence, and real-time stock alerts. Welcomed operator: <span className="font-semibold text-slate-900">{currentStaff.name}</span>.
|
| 107 |
+
</p>
|
| 108 |
+
</div>
|
| 109 |
+
<div className="flex gap-2.5 relative z-10">
|
| 110 |
+
<button
|
| 111 |
+
onClick={() => setActiveTab('invoices')}
|
| 112 |
+
className="bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium px-4 py-2.5 rounded-lg transition duration-150 flex items-center gap-2 cursor-pointer shadow-sm"
|
| 113 |
+
>
|
| 114 |
+
<ShoppingCart className="w-3.5 h-3.5" />
|
| 115 |
+
New Invoice
|
| 116 |
+
</button>
|
| 117 |
+
<button
|
| 118 |
+
onClick={() => setActiveTab('inventory')}
|
| 119 |
+
className="bg-white hover:bg-slate-50 text-slate-700 text-xs font-medium px-4 py-2.5 rounded-lg border border-slate-200 transition duration-150 flex items-center gap-2 cursor-pointer"
|
| 120 |
+
>
|
| 121 |
+
<Package className="w-3.5 h-3.5 text-slate-400" />
|
| 122 |
+
Manage Tires
|
| 123 |
+
</button>
|
| 124 |
+
</div>
|
| 125 |
+
{/* Abstract tire background shadow */}
|
| 126 |
+
<div className="absolute right-[-40px] bottom-[-40px] opacity-5 pointer-events-none transform rotate-12">
|
| 127 |
+
<div className="w-64 h-64 rounded-full border-[24px] border-slate-900 flex items-center justify-center">
|
| 128 |
+
<div className="w-40 h-40 rounded-full border-[12px] border-slate-900 border-dashed"></div>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
{/* Main Core KPI Cards */}
|
| 134 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5" id="kpi-grid">
|
| 135 |
+
{/* Metric 1 */}
|
| 136 |
+
<div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex items-center justify-between">
|
| 137 |
+
<div className="space-y-1">
|
| 138 |
+
<p className="text-[10px] uppercase font-semibold text-slate-400 tracking-wider">Mtd Cash Flow ({currentMonthName})</p>
|
| 139 |
+
<p className="text-xl font-bold tracking-tight text-slate-900">
|
| 140 |
+
{settings.currencySymbol} {juneSalesTotal.toLocaleString()}
|
| 141 |
+
</p>
|
| 142 |
+
<p className="text-[11px] text-slate-500 flex items-center gap-1">
|
| 143 |
+
<span className="text-emerald-600 font-semibold flex items-center gap-0.5">
|
| 144 |
+
<TrendingUp className="w-3 h-3" />
|
| 145 |
+
+14.2%
|
| 146 |
+
</span>
|
| 147 |
+
<span>vs last month</span>
|
| 148 |
+
</p>
|
| 149 |
+
</div>
|
| 150 |
+
<div className="p-2.5 bg-slate-50 text-slate-800 border border-slate-150 rounded-lg">
|
| 151 |
+
<TrendingUp className="w-4 h-4 text-slate-500" />
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
{/* Metric 2 */}
|
| 156 |
+
<div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex items-center justify-between">
|
| 157 |
+
<div className="space-y-1">
|
| 158 |
+
<p className="text-[10px] uppercase font-semibold text-slate-400 tracking-wider">Total Warehouse Stock</p>
|
| 159 |
+
<p className="text-xl font-bold tracking-tight text-slate-900">
|
| 160 |
+
{totalActiveItems} pcs
|
| 161 |
+
</p>
|
| 162 |
+
<p className="text-[11px] text-slate-500">
|
| 163 |
+
Across {products.length} distinct models
|
| 164 |
+
</p>
|
| 165 |
+
</div>
|
| 166 |
+
<div className="p-2.5 bg-slate-50 text-slate-800 border border-slate-150 rounded-lg">
|
| 167 |
+
<Package className="w-4 h-4 text-slate-500" />
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
{/* Metric 3 */}
|
| 172 |
+
<div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex items-center justify-between">
|
| 173 |
+
<div className="space-y-1">
|
| 174 |
+
<p className="text-[10px] uppercase font-semibold text-slate-400 tracking-wider">Critical Alerts</p>
|
| 175 |
+
<p className="text-xl font-bold tracking-tight text-red-600">
|
| 176 |
+
{lowStockItems.length} items
|
| 177 |
+
</p>
|
| 178 |
+
<p className="text-[11px] text-red-500 flex items-center gap-1 font-medium">
|
| 179 |
+
<AlertTriangle className="w-3 h-3" />
|
| 180 |
+
Urgent restock alert
|
| 181 |
+
</p>
|
| 182 |
+
</div>
|
| 183 |
+
<div className="p-2.5 bg-red-50 text-red-600 border border-red-100 rounded-lg">
|
| 184 |
+
<AlertTriangle className="w-4 h-4 text-red-500" />
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
{/* Metric 4 */}
|
| 189 |
+
<div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex items-center justify-between">
|
| 190 |
+
<div className="space-y-1">
|
| 191 |
+
<p className="text-[10px] uppercase font-semibold text-slate-400 tracking-wider">Total Sales Invoices</p>
|
| 192 |
+
<p className="text-xl font-bold tracking-tight text-slate-900">
|
| 193 |
+
{totalInvoicesCount} sales
|
| 194 |
+
</p>
|
| 195 |
+
<p className="text-[11px] text-slate-500">
|
| 196 |
+
Invoiced: {settings.currencySymbol}{totalInvoiced.toLocaleString()}
|
| 197 |
+
</p>
|
| 198 |
+
</div>
|
| 199 |
+
<div className="p-2.5 bg-slate-50 text-slate-850 border border-slate-150 rounded-lg">
|
| 200 |
+
<FileText className="w-4 h-4 text-slate-500" />
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
{/* Analytics SVG Graph Row */}
|
| 206 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6" id="dashboard-analytics">
|
| 207 |
+
{/* Core SVG Revenue Chart */}
|
| 208 |
+
<div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm lg:col-span-2 flex flex-col justify-between">
|
| 209 |
+
<div className="flex justify-between items-center mb-6">
|
| 210 |
+
<div>
|
| 211 |
+
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Monthly Sales Reporting</h3>
|
| 212 |
+
<p className="text-xs text-slate-400">Telemetry of invoice turnover for year 2026</p>
|
| 213 |
+
</div>
|
| 214 |
+
<div className="bg-slate-50 text-slate-700 text-xs font-semibold px-2.5 py-1 rounded-md border border-slate-200">
|
| 215 |
+
Year {new Date().getFullYear()}
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
|
| 219 |
+
{/* SVG Animated Chart */}
|
| 220 |
+
<div className="relative h-64 w-full">
|
| 221 |
+
<svg viewBox="0 0 600 240" className="w-full h-full overflow-visible">
|
| 222 |
+
{/* Grid Lines */}
|
| 223 |
+
<line x1="40" y1="20" x2="580" y2="20" stroke="#f1f5f9" strokeWidth="1" />
|
| 224 |
+
<line x1="40" y1="70" x2="580" y2="70" stroke="#f1f5f9" strokeWidth="1" />
|
| 225 |
+
<line x1="40" y1="120" x2="580" y2="120" stroke="#f1f5f9" strokeWidth="1" />
|
| 226 |
+
<line x1="40" y1="170" x2="580" y2="170" stroke="#f1f5f9" strokeWidth="1" />
|
| 227 |
+
<line x1="40" y1="210" x2="580" y2="210" stroke="#e2e8f0" strokeWidth="1.5" />
|
| 228 |
+
|
| 229 |
+
{/* Chart line plotting */}
|
| 230 |
+
{(() => {
|
| 231 |
+
const getCoords = () => {
|
| 232 |
+
const xSpacing = 540 / 5;
|
| 233 |
+
return monthlyData.map((d, index) => {
|
| 234 |
+
const x = 40 + index * xSpacing;
|
| 235 |
+
// Scale value from 210 (bottom) to 20 (top)
|
| 236 |
+
const y = 210 - (d.value / maxMonthlyVal) * 180;
|
| 237 |
+
return { x, y, label: d.label, val: d.value };
|
| 238 |
+
});
|
| 239 |
+
};
|
| 240 |
+
|
| 241 |
+
const coords = getCoords();
|
| 242 |
+
const pathD = coords.reduce((acc, c, idx) => {
|
| 243 |
+
return acc + (idx === 0 ? `M ${c.x} ${c.y}` : ` L ${c.x} ${c.y}`);
|
| 244 |
+
}, "");
|
| 245 |
+
|
| 246 |
+
const areaD = coords.reduce((acc, c, idx) => {
|
| 247 |
+
if (idx === 0) return `M ${c.x} 210 L ${c.x} ${c.y}`;
|
| 248 |
+
let ret = acc + ` L ${c.x} ${c.y}`;
|
| 249 |
+
if (idx === coords.length - 1) ret += ` L ${c.x} 210 Z`;
|
| 250 |
+
return ret;
|
| 251 |
+
}, "");
|
| 252 |
+
|
| 253 |
+
return (
|
| 254 |
+
<>
|
| 255 |
+
{/* Shaded Area */}
|
| 256 |
+
<path d={areaD} fill="url(#blue-gradient)" opacity="0.1" />
|
| 257 |
+
|
| 258 |
+
{/* Connective Line */}
|
| 259 |
+
<path d={pathD} fill="none" stroke="#2563eb" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
| 260 |
+
|
| 261 |
+
{/* Dots and Labels */}
|
| 262 |
+
{coords.map((c, i) => (
|
| 263 |
+
<g key={i} className="group cursor-pointer">
|
| 264 |
+
{/* Hidden interactive target block */}
|
| 265 |
+
<circle cx={c.x} cy={c.y} r="14" fill="transparent" />
|
| 266 |
+
<circle cx={c.x} cy={c.y} r="6" fill="#2563eb" stroke="#ffffff" strokeWidth="2" className="transition-all group-hover:scale-150 duration-100" />
|
| 267 |
+
<text x={c.x} y="230" textAnchor="middle" className="text-[11px] font-medium fill-slate-500 font-sans">{c.label}</text>
|
| 268 |
+
{/* Turnover Value Overlay on Hover */}
|
| 269 |
+
<g className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 pointer-events-none">
|
| 270 |
+
<rect x={c.x - 55} y={c.y - 36} width="110" height="26" rx="6" fill="#0f172a" />
|
| 271 |
+
<text x={c.x} y={c.y - 19} textAnchor="middle" className="text-[10px] font-semibold fill-white font-sans">
|
| 272 |
+
{settings.currencySymbol}{Math.round(c.val).toLocaleString()}
|
| 273 |
+
</text>
|
| 274 |
+
</g>
|
| 275 |
+
</g>
|
| 276 |
+
))}
|
| 277 |
+
|
| 278 |
+
{/* Gradiant definitions */}
|
| 279 |
+
<defs>
|
| 280 |
+
<linearGradient id="blue-gradient" x1="0" y1="0" x2="0" y2="1">
|
| 281 |
+
<stop offset="0%" stopColor="#2563eb" />
|
| 282 |
+
<stop offset="100%" stopColor="#2563eb" stopOpacity="0" />
|
| 283 |
+
</linearGradient>
|
| 284 |
+
</defs>
|
| 285 |
+
</>
|
| 286 |
+
);
|
| 287 |
+
})()}
|
| 288 |
+
</svg>
|
| 289 |
+
</div>
|
| 290 |
+
|
| 291 |
+
<div className="border-t border-slate-50 pt-4 flex justify-between text-xs text-slate-400">
|
| 292 |
+
<span>Graph starts: Jan 2026</span>
|
| 293 |
+
<span>Real-time custom math calculation engine active</span>
|
| 294 |
+
<span>Graph terminal: June 2026</span>
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
+
|
| 298 |
+
{/* Category Breakdown list */}
|
| 299 |
+
<div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex flex-col justify-between">
|
| 300 |
+
<div>
|
| 301 |
+
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Tire Category Audit</h3>
|
| 302 |
+
<p className="text-xs text-slate-400">Warehouse composition and valuation</p>
|
| 303 |
+
</div>
|
| 304 |
+
|
| 305 |
+
<div className="space-y-4 my-6">
|
| 306 |
+
{categoryStats.map((stat, i) => {
|
| 307 |
+
const maxCount = Math.max(...categoryStats.map(s => s.count), 1);
|
| 308 |
+
const percentage = Math.round((stat.count / maxCount) * 100);
|
| 309 |
+
|
| 310 |
+
return (
|
| 311 |
+
<div key={i} className="space-y-1">
|
| 312 |
+
<div className="flex justify-between text-xs">
|
| 313 |
+
<span className="font-semibold text-slate-700">{stat.name}</span>
|
| 314 |
+
<span className="text-slate-500">{stat.count} pcs — <span className="font-medium text-slate-800">{settings.currencySymbol}{stat.value.toLocaleString()}</span></span>
|
| 315 |
+
</div>
|
| 316 |
+
<div className="w-full bg-slate-100 h-2 rounded-full overflow-hidden">
|
| 317 |
+
<div
|
| 318 |
+
className={`h-full rounded-full ${
|
| 319 |
+
stat.name === 'Passenger' ? 'bg-blue-600' :
|
| 320 |
+
stat.name === 'SUV' ? 'bg-indigo-500' :
|
| 321 |
+
stat.name === 'Commercial' ? 'bg-emerald-500' :
|
| 322 |
+
stat.name === 'Light Truck' ? 'bg-amber-500' : 'bg-red-500'
|
| 323 |
+
}`}
|
| 324 |
+
style={{ width: `${percentage}%` }}
|
| 325 |
+
/>
|
| 326 |
+
</div>
|
| 327 |
+
</div>
|
| 328 |
+
);
|
| 329 |
+
})}
|
| 330 |
+
</div>
|
| 331 |
+
|
| 332 |
+
<div className="bg-slate-50 rounded-xl p-3 text-center border border-slate-100">
|
| 333 |
+
<span className="text-xs font-semibold text-slate-600">Total Inventory Valuation:</span>
|
| 334 |
+
<p className="text-lg font-bold text-slate-900">
|
| 335 |
+
{settings.currencySymbol} {products.reduce((sum, p) => sum + (p.stock * p.price), 0).toLocaleString()}
|
| 336 |
+
</p>
|
| 337 |
+
</div>
|
| 338 |
+
</div>
|
| 339 |
+
</div>
|
| 340 |
+
|
| 341 |
+
{/* Stock critical alerts and Quick Add Section */}
|
| 342 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6" id="dashboard-tables">
|
| 343 |
+
{/* Real-time low stock warnings widget */}
|
| 344 |
+
<div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex flex-col justify-between">
|
| 345 |
+
<div className="space-y-2">
|
| 346 |
+
<div className="flex justify-between items-center">
|
| 347 |
+
<h3 className="text-sm font-bold text-slate-900 flex items-center gap-2 uppercase tracking-wide">
|
| 348 |
+
<AlertTriangle className="w-4 h-4 text-slate-800" />
|
| 349 |
+
Live Replenishment Alerts
|
| 350 |
+
</h3>
|
| 351 |
+
<span className="bg-red-50 text-red-800 text-[10px] font-bold px-2 py-0.5 rounded-md border border-red-100/60 font-sans">
|
| 352 |
+
{lowStockItems.length} Low Stock
|
| 353 |
+
</span>
|
| 354 |
+
</div>
|
| 355 |
+
<p className="text-xs text-slate-500">
|
| 356 |
+
The products listed below are currently equal to or below their alert limit. Top up immediately:
|
| 357 |
+
</p>
|
| 358 |
+
</div>
|
| 359 |
+
|
| 360 |
+
{/* Search alerts */}
|
| 361 |
+
<div className="my-4 relative">
|
| 362 |
+
<span className="absolute left-3.5 top-3 text-slate-400">
|
| 363 |
+
<Search className="w-4 h-4" />
|
| 364 |
+
</span>
|
| 365 |
+
<input
|
| 366 |
+
type="text"
|
| 367 |
+
placeholder="Search low-stock products..."
|
| 368 |
+
value={searchTerm}
|
| 369 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 370 |
+
className="w-full text-xs border border-slate-200 outline-none focus:border-blue-500 bg-slate-50 hover:bg-slate-100 focus:bg-white rounded-xl pl-10 pr-4 py-2.5 transition duration-150"
|
| 371 |
+
/>
|
| 372 |
+
</div>
|
| 373 |
+
|
| 374 |
+
<div className="space-y-3 max-h-64 overflow-y-auto pr-1 my-2">
|
| 375 |
+
{filteredAlertProducts.length === 0 ? (
|
| 376 |
+
<div className="text-center py-8 border border-dashed border-slate-200 rounded-xl">
|
| 377 |
+
<p className="text-sm text-slate-400">No matching low stock warnings!</p>
|
| 378 |
+
</div>
|
| 379 |
+
) : (
|
| 380 |
+
filteredAlertProducts.map((p) => {
|
| 381 |
+
const stockPercent = Math.max(Math.min((p.stock / p.minStock) * 100, 100), 5);
|
| 382 |
+
const currentReplValue = replenishAmount[p.id] || 10;
|
| 383 |
+
|
| 384 |
+
return (
|
| 385 |
+
<div key={p.id} className="border border-slate-100 hover:border-slate-200 rounded-xl p-3 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-slate-50/50">
|
| 386 |
+
<div className="space-y-1 col-span-2">
|
| 387 |
+
<div className="flex items-center gap-2">
|
| 388 |
+
<span className="text-xs font-bold text-slate-800">{p.brand} {p.model}</span>
|
| 389 |
+
<span className="text-[10px] bg-slate-200/70 text-slate-600 px-2 py-0.5 rounded font-mono">{p.size}</span>
|
| 390 |
+
</div>
|
| 391 |
+
<div className="flex items-center gap-3 text-xs">
|
| 392 |
+
<span className="text-slate-500">Min. stock threshold: <span className="font-semibold text-slate-700">{p.minStock}</span></span>
|
| 393 |
+
<span className="text-slate-300">|</span>
|
| 394 |
+
<span className="text-red-600 font-bold">Qty: {p.stock} remaining</span>
|
| 395 |
+
</div>
|
| 396 |
+
|
| 397 |
+
{/* Critical line representing the severe level */}
|
| 398 |
+
<div className="w-52 bg-slate-200 h-1 rounded-full overflow-hidden">
|
| 399 |
+
<div className="h-full bg-red-500" style={{ width: `${stockPercent}%` }} />
|
| 400 |
+
</div>
|
| 401 |
+
</div>
|
| 402 |
+
|
| 403 |
+
{/* Stock quick load inputs */}
|
| 404 |
+
{['owner', 'manager'].includes(currentStaff.role) ? (
|
| 405 |
+
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
|
| 406 |
+
<input
|
| 407 |
+
type="number"
|
| 408 |
+
min="1"
|
| 409 |
+
max="200"
|
| 410 |
+
value={currentReplValue}
|
| 411 |
+
onChange={(e) => setReplenishAmount(prev => ({ ...prev, [p.id]: parseInt(e.target.value) || 1 }))}
|
| 412 |
+
className="w-16 border border-slate-200 focus:border-blue-500 bg-white shadow-inner outline-none rounded-lg text-xs py-1 px-2 font-mono text-center"
|
| 413 |
+
/>
|
| 414 |
+
<button
|
| 415 |
+
onClick={() => handleQuickAddClick(p.id)}
|
| 416 |
+
className="bg-blue-600 hover:bg-blue-700 font-bold text-white p-1.5 rounded-lg text-xs flex items-center gap-1 cursor-pointer transition shadow"
|
| 417 |
+
title="Restock now"
|
| 418 |
+
>
|
| 419 |
+
<Plus className="w-3.5 h-3.5" />
|
| 420 |
+
Restock
|
| 421 |
+
</button>
|
| 422 |
+
</div>
|
| 423 |
+
) : (
|
| 424 |
+
<span className="text-[10px] text-slate-400 italic">Restock restricted to Managers</span>
|
| 425 |
+
)}
|
| 426 |
+
</div>
|
| 427 |
+
);
|
| 428 |
+
})
|
| 429 |
+
)}
|
| 430 |
+
</div>
|
| 431 |
+
<div className="pt-2 border-t border-slate-50 text-right">
|
| 432 |
+
<button
|
| 433 |
+
onClick={() => setActiveTab('inventory')}
|
| 434 |
+
className="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1 ml-auto font-semibold cursor-pointer"
|
| 435 |
+
>
|
| 436 |
+
Examine full inventory history
|
| 437 |
+
<ArrowRight className="w-3.5 h-3.5" />
|
| 438 |
+
</button>
|
| 439 |
+
</div>
|
| 440 |
+
</div>
|
| 441 |
+
|
| 442 |
+
{/* Recent Invoices Table list */}
|
| 443 |
+
<div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex flex-col justify-between">
|
| 444 |
+
<div className="space-y-1">
|
| 445 |
+
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Recent Transactions</h3>
|
| 446 |
+
<p className="text-xs text-slate-400">Review, query, and print historical customer receipts</p>
|
| 447 |
+
</div>
|
| 448 |
+
|
| 449 |
+
<div className="space-y-3 scrollbar-none overflow-y-auto max-h-80 pr-1 my-4">
|
| 450 |
+
{invoices.length === 0 ? (
|
| 451 |
+
<div className="text-center py-12 border border-dashed border-slate-200 rounded-xl">
|
| 452 |
+
<p className="text-sm text-slate-400">No invoices drafted yet!</p>
|
| 453 |
+
</div>
|
| 454 |
+
) : (
|
| 455 |
+
[...invoices].reverse().slice(0, 5).map((inv) => (
|
| 456 |
+
<div
|
| 457 |
+
key={inv.id}
|
| 458 |
+
onClick={() => setSelectedInvoiceForView(inv)}
|
| 459 |
+
className="group border border-slate-50 hover:border-blue-100 hover:bg-blue-50/20 rounded-xl p-3 flex justify-between items-center transition cursor-pointer"
|
| 460 |
+
>
|
| 461 |
+
<div className="space-y-1">
|
| 462 |
+
<div className="flex items-center gap-2">
|
| 463 |
+
<span className="font-mono text-xs font-semibold text-slate-900">{inv.invoiceNumber}</span>
|
| 464 |
+
{inv.isOfflineCreated && (
|
| 465 |
+
<span className="bg-orange-100 text-orange-800 text-[9px] font-bold px-1.5 py-0.5 rounded-full" title="Saved locally offline. Ready for vault sync.">
|
| 466 |
+
OFFLINE
|
| 467 |
+
</span>
|
| 468 |
+
)}
|
| 469 |
+
</div>
|
| 470 |
+
<p className="text-xs font-medium text-slate-700">{inv.customerName}</p>
|
| 471 |
+
<p className="text-[10px] text-slate-400">{inv.dateTime}</p>
|
| 472 |
+
</div>
|
| 473 |
+
|
| 474 |
+
<div className="text-right space-y-1">
|
| 475 |
+
<span className="text-xs font-bold text-slate-900 font-sans">
|
| 476 |
+
{inv.currencySymbol} {inv.total.toLocaleString()}
|
| 477 |
+
</span>
|
| 478 |
+
<div className="flex items-center gap-1 justify-end">
|
| 479 |
+
<span className={`text-[10px] font-bold px-2 py-0.5 rounded ${
|
| 480 |
+
inv.paymentStatus === 'PAID' ? 'bg-emerald-100 text-emerald-800' : 'bg-red-100 text-red-800'
|
| 481 |
+
}`}>
|
| 482 |
+
{inv.paymentStatus}
|
| 483 |
+
</span>
|
| 484 |
+
<span className="text-[9px] text-slate-400 bg-slate-100 px-1.5 rounded uppercase font-mono">
|
| 485 |
+
{inv.paymentMethod}
|
| 486 |
+
</span>
|
| 487 |
+
</div>
|
| 488 |
+
</div>
|
| 489 |
+
</div>
|
| 490 |
+
))
|
| 491 |
+
)}
|
| 492 |
+
</div>
|
| 493 |
+
|
| 494 |
+
<div className="pt-2 border-t border-slate-50">
|
| 495 |
+
<button
|
| 496 |
+
onClick={() => setActiveTab('invoices')}
|
| 497 |
+
className="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1 ml-auto font-semibold cursor-pointer"
|
| 498 |
+
>
|
| 499 |
+
Access complete Invoices terminal
|
| 500 |
+
<ArrowRight className="w-3.5 h-3.5" />
|
| 501 |
+
</button>
|
| 502 |
+
</div>
|
| 503 |
+
</div>
|
| 504 |
+
</div>
|
| 505 |
+
</div>
|
| 506 |
+
);
|
| 507 |
+
}
|
src/components/InventoryManager.tsx
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @license
|
| 3 |
+
* SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import React, { useState, useMemo } from 'react';
|
| 7 |
+
import { motion, AnimatePresence } from 'motion/react';
|
| 8 |
+
import {
|
| 9 |
+
Package,
|
| 10 |
+
Plus,
|
| 11 |
+
History,
|
| 12 |
+
Trash2,
|
| 13 |
+
Edit,
|
| 14 |
+
Folder,
|
| 15 |
+
AlertTriangle,
|
| 16 |
+
ChevronRight,
|
| 17 |
+
ArrowUpRight,
|
| 18 |
+
ArrowDownRight,
|
| 19 |
+
Search,
|
| 20 |
+
Filter,
|
| 21 |
+
Check,
|
| 22 |
+
PlusCircle,
|
| 23 |
+
TrendingUp,
|
| 24 |
+
Sliders,
|
| 25 |
+
DollarSign,
|
| 26 |
+
Tag
|
| 27 |
+
} from 'lucide-react';
|
| 28 |
+
import { TireProduct, StockHistoryItem, StaffUser, SystemSettings } from '../types';
|
| 29 |
+
|
| 30 |
+
interface InventoryManagerProps {
|
| 31 |
+
products: TireProduct[];
|
| 32 |
+
history: StockHistoryItem[];
|
| 33 |
+
currentStaff: StaffUser;
|
| 34 |
+
settings: SystemSettings;
|
| 35 |
+
onAdjustProductStock: (productId: string, quantityChange: number, type: StockHistoryItem['type'], reason: string) => void;
|
| 36 |
+
onAddNewProduct: (product: TireProduct) => void;
|
| 37 |
+
onDeleteProduct: (productId: string) => void;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export default function InventoryManager({
|
| 41 |
+
products,
|
| 42 |
+
history,
|
| 43 |
+
currentStaff,
|
| 44 |
+
settings,
|
| 45 |
+
onAdjustProductStock,
|
| 46 |
+
onAddNewProduct,
|
| 47 |
+
onDeleteProduct
|
| 48 |
+
}: InventoryManagerProps) {
|
| 49 |
+
const [selectedProduct, setSelectedProduct] = useState<TireProduct | null>(null);
|
| 50 |
+
const [inventorySearch, setInventorySearch] = useState('');
|
| 51 |
+
const [activeSegment, setActiveSegment] = useState<string>('All');
|
| 52 |
+
|
| 53 |
+
// Custom New Product Addition Modal toggle
|
| 54 |
+
const [showAddModal, setShowAddModal] = useState(false);
|
| 55 |
+
|
| 56 |
+
// Manual Adjustment flow inside details
|
| 57 |
+
const [adjustmentQty, setAdjustmentQty] = useState<number>(5);
|
| 58 |
+
const [adjustmentType, setAdjustmentType] = useState<'STOCK_IN' | 'STOCK_OUT' | 'MANUAL_ADJUSTMENT'>('STOCK_IN');
|
| 59 |
+
const [adjustmentReason, setAdjustmentReason] = useState('');
|
| 60 |
+
|
| 61 |
+
// New Product details state
|
| 62 |
+
const [newBrand, setNewBrand] = useState('');
|
| 63 |
+
const [newModel, setNewModel] = useState('');
|
| 64 |
+
const [newSize, setNewSize] = useState('');
|
| 65 |
+
const [newCategory, setNewCategory] = useState<TireProduct['category']>('Passenger');
|
| 66 |
+
const [newPrice, setNewPrice] = useState<number>(15000);
|
| 67 |
+
const [newPurchasePrice, setNewPurchasePrice] = useState<number>(11000);
|
| 68 |
+
const [newStock, setNewStock] = useState<number>(10);
|
| 69 |
+
const [newMinStock, setNewMinStock] = useState<number>(5);
|
| 70 |
+
const [newSku, setNewSku] = useState('');
|
| 71 |
+
const [newDesc, setNewDesc] = useState('');
|
| 72 |
+
|
| 73 |
+
// 1. Filtering products
|
| 74 |
+
const filteredProducts = useMemo(() => {
|
| 75 |
+
return products.filter(p => {
|
| 76 |
+
const matchSearch =
|
| 77 |
+
p.brand.toLowerCase().includes(inventorySearch.toLowerCase()) ||
|
| 78 |
+
p.model.toLowerCase().includes(inventorySearch.toLowerCase()) ||
|
| 79 |
+
p.size.toLowerCase().includes(inventorySearch.toLowerCase()) ||
|
| 80 |
+
p.sku.toLowerCase().includes(inventorySearch.toLowerCase());
|
| 81 |
+
|
| 82 |
+
const matchSegment = activeSegment === 'All' || p.category === activeSegment;
|
| 83 |
+
return matchSearch && matchSegment;
|
| 84 |
+
});
|
| 85 |
+
}, [products, inventorySearch, activeSegment]);
|
| 86 |
+
|
| 87 |
+
// If a product is selected, pull its dedicated history records
|
| 88 |
+
const selectedProductHistory = useMemo(() => {
|
| 89 |
+
if (!selectedProduct) return [];
|
| 90 |
+
return history
|
| 91 |
+
.filter(h => h.productId === selectedProduct.id)
|
| 92 |
+
.sort((a, b) => new Date(b.dateTime).getTime() - new Date(a.dateTime).getTime());
|
| 93 |
+
}, [history, selectedProduct]);
|
| 94 |
+
|
| 95 |
+
// Stock valuation and item counts
|
| 96 |
+
const totalSkuInSegment = filteredProducts.length;
|
| 97 |
+
const totalCountInSegment = filteredProducts.reduce((sum, p) => sum + p.stock, 0);
|
| 98 |
+
const totalValuationInSegment = filteredProducts.reduce((sum, p) => sum + (p.stock * p.price), 0);
|
| 99 |
+
const totalCostValuation = filteredProducts.reduce((sum, p) => sum + (p.stock * p.purchasePrice), 0);
|
| 100 |
+
const projectedMargin = totalValuationInSegment - totalCostValuation;
|
| 101 |
+
|
| 102 |
+
// Handle Adjustment submission
|
| 103 |
+
const handleAdjustmentSubmit = (e: React.FormEvent) => {
|
| 104 |
+
e.preventDefault();
|
| 105 |
+
if (!selectedProduct) return;
|
| 106 |
+
if (adjustmentQty <= 0) {
|
| 107 |
+
alert("Adjustment quantity must be greater than zero.");
|
| 108 |
+
return;
|
| 109 |
+
}
|
| 110 |
+
if (!adjustmentReason.trim()) {
|
| 111 |
+
alert("Please state concrete audit adjustment comments.");
|
| 112 |
+
return;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Determine quantity with correct algebraic sign: STOCK_IN (+), OUT/MAN ADJUSTMENT (-)
|
| 116 |
+
let finalQtyChange = adjustmentQty;
|
| 117 |
+
if (adjustmentType === 'STOCK_OUT') {
|
| 118 |
+
finalQtyChange = -adjustmentQty;
|
| 119 |
+
} else if (adjustmentType === 'MANUAL_ADJUSTMENT') {
|
| 120 |
+
// Manual adjustment could be adding or subtracting
|
| 121 |
+
// In our code logic, we'll let manager define positive adjustments as + adjustmentQty
|
| 122 |
+
// But we can let them input negative values or specify direct relative overrides!
|
| 123 |
+
// Here let's treat manual adjustment as relative addition. Let's ask them to input signed or we define.
|
| 124 |
+
// Let's assume standard manual addition. If they want to decrease, they select "Stock Out".
|
| 125 |
+
finalQtyChange = adjustmentQty;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// Guard stock limit
|
| 129 |
+
if (adjustmentType === 'STOCK_OUT' && selectedProduct.stock + finalQtyChange < 0) {
|
| 130 |
+
alert("Cannot complete operation! Net inventory stock cannot go below 0.");
|
| 131 |
+
return;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
onAdjustProductStock(
|
| 135 |
+
selectedProduct.id,
|
| 136 |
+
finalQtyChange,
|
| 137 |
+
adjustmentType,
|
| 138 |
+
adjustmentReason.trim()
|
| 139 |
+
);
|
| 140 |
+
|
| 141 |
+
// Sync selected item card visual indicators
|
| 142 |
+
const updatedProd = products.find(p => p.id === selectedProduct.id);
|
| 143 |
+
if (updatedProd) {
|
| 144 |
+
setSelectedProduct({
|
| 145 |
+
...updatedProd,
|
| 146 |
+
stock: updatedProd.stock + finalQtyChange
|
| 147 |
+
});
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
// Reset forms
|
| 151 |
+
setAdjustmentQty(5);
|
| 152 |
+
setAdjustmentReason('');
|
| 153 |
+
setAdjustmentType('STOCK_IN');
|
| 154 |
+
};
|
| 155 |
+
|
| 156 |
+
// Create Product Submit
|
| 157 |
+
const handleCreateProductSubmit = (e: React.FormEvent) => {
|
| 158 |
+
e.preventDefault();
|
| 159 |
+
if (!newBrand.trim() || !newModel.trim() || !newSize.trim() || !newSku.trim()) {
|
| 160 |
+
alert("Brand, Model, Size and SKU are mandatory properties.");
|
| 161 |
+
return;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// SKU uniqueness guard
|
| 165 |
+
if (products.some(p => p.sku.toUpperCase() === newSku.toUpperCase())) {
|
| 166 |
+
alert("SKU already active under another tire spec. Assign unique tracking barcode.");
|
| 167 |
+
return;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
const newProd: TireProduct = {
|
| 171 |
+
id: 'p_' + Math.random().toString(36).substr(2, 9),
|
| 172 |
+
sku: newSku.toUpperCase(),
|
| 173 |
+
brand: newBrand,
|
| 174 |
+
model: newModel,
|
| 175 |
+
size: newSize,
|
| 176 |
+
category: newCategory,
|
| 177 |
+
stock: newStock,
|
| 178 |
+
minStock: newMinStock,
|
| 179 |
+
price: newPrice,
|
| 180 |
+
purchasePrice: newPurchasePrice,
|
| 181 |
+
description: newDesc.trim() || undefined
|
| 182 |
+
};
|
| 183 |
+
|
| 184 |
+
onAddNewProduct(newProd);
|
| 185 |
+
|
| 186 |
+
// Reset states
|
| 187 |
+
setNewBrand('');
|
| 188 |
+
setNewModel('');
|
| 189 |
+
setNewSize('');
|
| 190 |
+
setNewSku('');
|
| 191 |
+
setNewDesc('');
|
| 192 |
+
setNewStock(10);
|
| 193 |
+
setNewMinStock(5);
|
| 194 |
+
setNewPrice(15000);
|
| 195 |
+
setNewPurchasePrice(11000);
|
| 196 |
+
setShowAddModal(false);
|
| 197 |
+
};
|
| 198 |
+
|
| 199 |
+
const handleProductDelete = (productId: string) => {
|
| 200 |
+
if (confirm("Are you absolutely sure you want to retire and remove this tire product spec? Historical invoice records will still show details, but stock adjustments will be closed.")) {
|
| 201 |
+
onDeleteProduct(productId);
|
| 202 |
+
if (selectedProduct?.id === productId) {
|
| 203 |
+
setSelectedProduct(null);
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
};
|
| 207 |
+
|
| 208 |
+
return (
|
| 209 |
+
<div className="space-y-6" id="warehouse-management-cockpit">
|
| 210 |
+
{/* Category Summaries & Valuation overview cards */}
|
| 211 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4" id="inventory-valuation">
|
| 212 |
+
<div className="bg-white border border-slate-200 p-5 rounded-xl shadow-sm space-y-1">
|
| 213 |
+
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Active Segment SKUs</span>
|
| 214 |
+
<h4 className="text-lg font-bold text-slate-900">{totalSkuInSegment} Profiles</h4>
|
| 215 |
+
<p className="text-xs text-slate-500">Currently filtered category</p>
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
<div className="bg-white border border-slate-200 p-5 rounded-xl shadow-sm space-y-1">
|
| 219 |
+
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Physical Tires Inventory</span>
|
| 220 |
+
<h4 className="text-lg font-bold text-slate-900">{totalCountInSegment} units</h4>
|
| 221 |
+
<p className="text-xs text-slate-500">Physical tire units stored</p>
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
<div className="bg-white border border-slate-200 p-5 rounded-xl shadow-sm space-y-1">
|
| 225 |
+
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Estimated Retail Value</span>
|
| 226 |
+
<h4 className="text-lg font-bold text-slate-900">{settings.currencySymbol} {totalValuationInSegment.toLocaleString()}</h4>
|
| 227 |
+
<p className="text-xs text-slate-500">Total potential turnover value</p>
|
| 228 |
+
</div>
|
| 229 |
+
|
| 230 |
+
<div className="bg-white border border-slate-200 p-5 rounded-xl shadow-sm space-y-1">
|
| 231 |
+
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">Projected Profit Margin</span>
|
| 232 |
+
<h4 className="text-lg font-bold text-emerald-600">{settings.currencySymbol} {projectedMargin.toLocaleString()}</h4>
|
| 233 |
+
<p className="text-xs text-emerald-500 font-medium font-sans">Est. average margin: {Math.round((projectedMargin / (totalValuationInSegment || 1)) * 100)}%</p>
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
|
| 237 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
| 238 |
+
{/* LEFT COLUMN: Core product list table */}
|
| 239 |
+
<div className={`${selectedProduct ? 'lg:col-span-7' : 'lg:col-span-12'} bg-white border border-slate-200 rounded-xl shadow-sm p-5 space-y-4 transition-all duration-300`}>
|
| 240 |
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-100 pb-4">
|
| 241 |
+
<div className="space-y-1">
|
| 242 |
+
<h3 className="text-sm font-bold text-slate-950 flex items-center gap-2 uppercase tracking-wide">
|
| 243 |
+
<Sliders className="w-4.5 h-4.5 text-slate-800" />
|
| 244 |
+
Haider Tire Warehouse Records
|
| 245 |
+
</h3>
|
| 246 |
+
<p className="text-xs text-slate-400">Configure stocks details, track low alerts, and view specific SKU histories</p>
|
| 247 |
+
</div>
|
| 248 |
+
|
| 249 |
+
{['owner', 'manager'].includes(currentStaff.role) && (
|
| 250 |
+
<button
|
| 251 |
+
type="button"
|
| 252 |
+
onClick={() => setShowAddModal(true)}
|
| 253 |
+
className="bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs px-3.5 py-2.5 rounded-lg cursor-pointer shadow-sm transition flex items-center gap-2"
|
| 254 |
+
>
|
| 255 |
+
<PlusCircle className="w-4 h-4" />
|
| 256 |
+
Add New SKU
|
| 257 |
+
</button>
|
| 258 |
+
)}
|
| 259 |
+
</div>
|
| 260 |
+
|
| 261 |
+
{/* Filtering row */}
|
| 262 |
+
<div className="flex flex-col sm:flex-row gap-3">
|
| 263 |
+
{/* Search */}
|
| 264 |
+
<div className="relative flex-1">
|
| 265 |
+
<span className="absolute left-3.5 top-3 text-slate-400">
|
| 266 |
+
<Search className="w-4 h-4" />
|
| 267 |
+
</span>
|
| 268 |
+
<input
|
| 269 |
+
type="text"
|
| 270 |
+
placeholder="Search by Brand, size parameter (e.g. R15), or SKU..."
|
| 271 |
+
value={inventorySearch}
|
| 272 |
+
onChange={(e) => setInventorySearch(e.target.value)}
|
| 273 |
+
className="w-full text-xs border border-slate-200 outline-none focus:border-slate-800 bg-white hover:bg-slate-50/50 focus:bg-white rounded-lg pl-10 pr-4 py-2.5 transition"
|
| 274 |
+
/>
|
| 275 |
+
</div>
|
| 276 |
+
|
| 277 |
+
{/* Filter segments tab */}
|
| 278 |
+
<div className="flex gap-1 overflow-x-auto pb-1 shrink-0 font-sans">
|
| 279 |
+
{['All', 'Passenger', 'SUV', 'Commercial', 'Light Truck', 'Truck/Bus'].map(seg => (
|
| 280 |
+
<button
|
| 281 |
+
key={seg}
|
| 282 |
+
onClick={() => setActiveSegment(seg)}
|
| 283 |
+
className={`text-xs px-3 py-1.5 font-semibold rounded-lg shrink-0 cursor-pointer transition ${
|
| 284 |
+
activeSegment === seg
|
| 285 |
+
? 'bg-slate-900 text-white shadow-sm'
|
| 286 |
+
: 'bg-slate-50 text-slate-600 hover:bg-slate-100 border border-slate-200/60'
|
| 287 |
+
}`}
|
| 288 |
+
>
|
| 289 |
+
{seg}
|
| 290 |
+
</button>
|
| 291 |
+
))}
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
{/* Core Table Grid layout */}
|
| 296 |
+
<div className="overflow-x-auto scrollbar-thin">
|
| 297 |
+
<table className="w-full text-left border-collapse font-sans text-xs">
|
| 298 |
+
<thead className="bg-slate-50 text-slate-500 uppercase text-[10px] tracking-wider leading-none">
|
| 299 |
+
<tr className="border-b border-slate-200">
|
| 300 |
+
<th className="py-3 px-4 font-bold">SKU / Tire Name</th>
|
| 301 |
+
<th className="py-3 px-4 font-bold">Category</th>
|
| 302 |
+
<th className="py-3 px-3 font-bold text-center">Remaining</th>
|
| 303 |
+
<th className="py-3 px-3 font-bold text-right">Cost Price</th>
|
| 304 |
+
<th className="py-3 px-4 font-bold text-right font-sans font-extrabold">Sell Price</th>
|
| 305 |
+
<th className="py-3 px-4 text-center">Audit Actions</th>
|
| 306 |
+
</tr>
|
| 307 |
+
</thead>
|
| 308 |
+
<tbody className="divide-y divide-slate-100 align-middle">
|
| 309 |
+
{filteredProducts.length === 0 ? (
|
| 310 |
+
<tr>
|
| 311 |
+
<td colSpan={6} className="text-center py-16 text-slate-400 bg-slate-50/20 rounded-xl">No products matched current filters or search query.</td>
|
| 312 |
+
</tr>
|
| 313 |
+
) : (
|
| 314 |
+
filteredProducts.map((p) => {
|
| 315 |
+
const isLow = p.stock <= p.minStock;
|
| 316 |
+
const isSelected = selectedProduct?.id === p.id;
|
| 317 |
+
|
| 318 |
+
return (
|
| 319 |
+
<tr
|
| 320 |
+
key={p.id}
|
| 321 |
+
className={`transition hover:bg-blue-50/10 cursor-pointer ${
|
| 322 |
+
isSelected ? 'bg-blue-50/30 font-semibold' : ''
|
| 323 |
+
}`}
|
| 324 |
+
onClick={() => setSelectedProduct(p)}
|
| 325 |
+
>
|
| 326 |
+
<td className="py-4 px-4 space-y-1">
|
| 327 |
+
<div className="flex items-center gap-1.5">
|
| 328 |
+
<span className="font-bold text-slate-950 text-sm leading-tight">{p.brand} {p.model}</span>
|
| 329 |
+
{isLow && (
|
| 330 |
+
<span className="bg-red-100 text-red-800 text-[9px] font-black px-1.5 py-0.5 rounded animate-pulse">
|
| 331 |
+
ALERT
|
| 332 |
+
</span>
|
| 333 |
+
)}
|
| 334 |
+
</div>
|
| 335 |
+
<div className="flex items-center gap-2 text-[10px] text-slate-500">
|
| 336 |
+
<span className="font-mono text-xs text-blue-600 bg-blue-50 font-bold px-1 rounded">{p.size}</span>
|
| 337 |
+
<span>•</span>
|
| 338 |
+
<span className="font-mono text-[9px] uppercase font-bold text-slate-400 bg-slate-100 px-1 rounded">SKU: {p.sku}</span>
|
| 339 |
+
</div>
|
| 340 |
+
</td>
|
| 341 |
+
|
| 342 |
+
<td className="py-4 px-4">
|
| 343 |
+
<span className="bg-slate-100 text-slate-700 font-bold text-[10px] px-2.5 py-1 rounded-full">{p.category}</span>
|
| 344 |
+
</td>
|
| 345 |
+
|
| 346 |
+
<td className="py-4 px-3 text-center">
|
| 347 |
+
<span className={`font-mono font-black text-sm px-2.5 py-1 rounded-lg ${
|
| 348 |
+
p.stock === 0 ? 'bg-red-100 text-red-800' :
|
| 349 |
+
isLow ? 'bg-amber-100 text-amber-800' : 'bg-slate-100 text-slate-900'
|
| 350 |
+
}`}>
|
| 351 |
+
{p.stock}
|
| 352 |
+
</span>
|
| 353 |
+
</td>
|
| 354 |
+
|
| 355 |
+
<td className="py-4 px-3 text-right text-slate-500 font-mono">
|
| 356 |
+
{settings.currencySymbol} {p.purchasePrice.toLocaleString()}
|
| 357 |
+
</td>
|
| 358 |
+
|
| 359 |
+
<td className="py-4 px-4 text-right font-extrabold text-slate-950 text-sm font-sans">
|
| 360 |
+
{settings.currencySymbol} {p.price.toLocaleString()}
|
| 361 |
+
</td>
|
| 362 |
+
|
| 363 |
+
<td className="py-4 px-4 text-center">
|
| 364 |
+
<div className="flex items-center justify-center gap-2" onClick={(e) => e.stopPropagation()}>
|
| 365 |
+
<button
|
| 366 |
+
type="button"
|
| 367 |
+
onClick={() => setSelectedProduct(p)}
|
| 368 |
+
className="text-blue-600 hover:text-blue-800 p-1.5 rounded-lg hover:bg-blue-50 transition"
|
| 369 |
+
title="Examine history logs"
|
| 370 |
+
>
|
| 371 |
+
<History className="w-4 h-4" />
|
| 372 |
+
</button>
|
| 373 |
+
{['owner', 'manager'].includes(currentStaff.role) && (
|
| 374 |
+
<button
|
| 375 |
+
type="button"
|
| 376 |
+
onClick={() => handleProductDelete(p.id)}
|
| 377 |
+
className="text-slate-400 hover:text-red-600 p-1.5 rounded-lg hover:bg-slate-100 transition"
|
| 378 |
+
title="Remove SKU reference"
|
| 379 |
+
>
|
| 380 |
+
<Trash2 className="w-4 h-4" />
|
| 381 |
+
</button>
|
| 382 |
+
)}
|
| 383 |
+
</div>
|
| 384 |
+
</td>
|
| 385 |
+
</tr>
|
| 386 |
+
);
|
| 387 |
+
})
|
| 388 |
+
)}
|
| 389 |
+
</tbody>
|
| 390 |
+
</table>
|
| 391 |
+
</div>
|
| 392 |
+
</div>
|
| 393 |
+
|
| 394 |
+
{/* RIGHT COLUMN: Selective detailed histories & manual stock operations drawer */}
|
| 395 |
+
{selectedProduct && (
|
| 396 |
+
<div className="lg:col-span-5 space-y-6">
|
| 397 |
+
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-5 space-y-5 relative overflow-visible">
|
| 398 |
+
<button
|
| 399 |
+
type="button"
|
| 400 |
+
onClick={() => setSelectedProduct(null)}
|
| 401 |
+
className="absolute top-4 right-4 bg-slate-50 hover:bg-slate-150 text-slate-400 hover:text-slate-800 font-bold px-2 py-1 rounded text-xs cursor-pointer"
|
| 402 |
+
>
|
| 403 |
+
✕
|
| 404 |
+
</button>
|
| 405 |
+
|
| 406 |
+
{/* Product Header Card */}
|
| 407 |
+
<div className="space-y-3.5">
|
| 408 |
+
<span className="bg-slate-100 text-slate-800 border border-slate-200/60 text-[10px] font-semibold px-2.5 py-0.5 rounded-md uppercase tracking-wider font-sans">
|
| 409 |
+
Detailed Tire Portfolio & Log
|
| 410 |
+
</span>
|
| 411 |
+
|
| 412 |
+
<div>
|
| 413 |
+
<h4 className="text-lg font-bold text-slate-950 uppercase">{selectedProduct.brand}</h4>
|
| 414 |
+
<p className="text-xs font-semibold text-slate-500 leading-tight">{selectedProduct.model}</p>
|
| 415 |
+
</div>
|
| 416 |
+
|
| 417 |
+
<div className="flex flex-wrap gap-2 text-xs">
|
| 418 |
+
<span className="font-sans text-[11px] font-semibold text-slate-800 bg-slate-100 border border-slate-200/50 px-2.5 py-0.5 rounded-md">
|
| 419 |
+
{selectedProduct.size}
|
| 420 |
+
</span>
|
| 421 |
+
<span className="bg-slate-150 text-slate-600 font-sans text-[10px] font-bold uppercase rounded-md px-2 py-0.5 self-center">
|
| 422 |
+
SKU: {selectedProduct.sku}
|
| 423 |
+
</span>
|
| 424 |
+
</div>
|
| 425 |
+
</div>
|
| 426 |
+
|
| 427 |
+
{/* MANUAL ADJUSTER STOCK PANEL FOR AUTHORIZED STAFF */}
|
| 428 |
+
{['owner', 'manager'].includes(currentStaff.role) ? (
|
| 429 |
+
<form onSubmit={handleAdjustmentSubmit} className="border-t border-b border-dashed border-slate-200 py-4 space-y-4">
|
| 430 |
+
<div className="flex items-center gap-1.5">
|
| 431 |
+
<Sliders className="w-4.5 h-4.5 text-blue-600" />
|
| 432 |
+
<span className="text-xs font-bold text-slate-950 uppercase">Manual Stock Adjuster Node</span>
|
| 433 |
+
</div>
|
| 434 |
+
|
| 435 |
+
<div className="grid grid-cols-2 gap-3">
|
| 436 |
+
<div className="space-y-1">
|
| 437 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Action Mode</label>
|
| 438 |
+
<select
|
| 439 |
+
value={adjustmentType}
|
| 440 |
+
onChange={(e) => setAdjustmentType(e.target.value as any)}
|
| 441 |
+
className="w-full text-xs border border-slate-200 bg-white shadow-sm p-2 rounded-xl outline-none"
|
| 442 |
+
>
|
| 443 |
+
<option value="STOCK_IN">Stock In (+ Incoming)</option>
|
| 444 |
+
<option value="STOCK_OUT">Stock Out (- Outgoing)</option>
|
| 445 |
+
<option value="MANUAL_ADJUSTMENT">Recount Adjustment (+/-)</option>
|
| 446 |
+
</select>
|
| 447 |
+
</div>
|
| 448 |
+
|
| 449 |
+
<div className="space-y-1">
|
| 450 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Tire count change</label>
|
| 451 |
+
<input
|
| 452 |
+
type="number"
|
| 453 |
+
min="1"
|
| 454 |
+
max="500"
|
| 455 |
+
value={adjustmentQty}
|
| 456 |
+
onChange={(e) => setAdjustmentQty(Math.max(1, parseInt(e.target.value) || 0))}
|
| 457 |
+
className="w-full text-xs font-mono border border-slate-200 bg-white p-2 rounded-xl outline-none focus:border-blue-500 shadow-inner"
|
| 458 |
+
/>
|
| 459 |
+
</div>
|
| 460 |
+
</div>
|
| 461 |
+
|
| 462 |
+
<div className="space-y-1">
|
| 463 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Audit Reason comments *</label>
|
| 464 |
+
<input
|
| 465 |
+
type="text"
|
| 466 |
+
placeholder="e.g. Visual count match: added cargo pallets..."
|
| 467 |
+
value={adjustmentReason}
|
| 468 |
+
onChange={(e) => setAdjustmentReason(e.target.value)}
|
| 469 |
+
className="w-full text-xs border border-slate-200 bg-white p-2 text-slate-700 outline-none focus:border-blue-500 rounded-xl"
|
| 470 |
+
/>
|
| 471 |
+
</div>
|
| 472 |
+
|
| 473 |
+
<button
|
| 474 |
+
type="submit"
|
| 475 |
+
className="w-full bg-slate-900 hover:bg-slate-800 text-white text-xs font-bold py-2.5 rounded-xl transition cursor-pointer flex items-center justify-center gap-1 shadow"
|
| 476 |
+
>
|
| 477 |
+
<Check className="w-4 h-4" />
|
| 478 |
+
Commit Stock Adjustment History
|
| 479 |
+
</button>
|
| 480 |
+
</form>
|
| 481 |
+
) : (
|
| 482 |
+
<div className="p-3 bg-slate-50 border border-slate-100 rounded-xl text-center text-xs text-slate-400 italic">
|
| 483 |
+
Manual adjustments restricted to Owner & Managers. Contact Zain Brother to restock.
|
| 484 |
+
</div>
|
| 485 |
+
)}
|
| 486 |
+
|
| 487 |
+
{/* CHRONOLOGICAL HISTORICAL LEDGER FOR THE ACTIVE TIRE */}
|
| 488 |
+
<div className="space-y-3">
|
| 489 |
+
<div className="flex items-center gap-1.5 justify-between">
|
| 490 |
+
<span className="text-xs font-bold text-slate-950 uppercase flex items-center gap-1">
|
| 491 |
+
<History className="w-4 h-4 text-slate-700" />
|
| 492 |
+
Tire Action History
|
| 493 |
+
</span>
|
| 494 |
+
<span className="bg-slate-100 text-slate-700 font-bold text-[10px] font-mono px-2 py-0.5 rounded">
|
| 495 |
+
{selectedProductHistory.length} ledger logs
|
| 496 |
+
</span>
|
| 497 |
+
</div>
|
| 498 |
+
|
| 499 |
+
<div className="space-y-2 max-h-72 overflow-y-auto pr-1">
|
| 500 |
+
{selectedProductHistory.length === 0 ? (
|
| 501 |
+
<div className="text-center py-10 text-slate-400 text-xs border border-dashed border-slate-150 rounded-xl">
|
| 502 |
+
No matching transaction logs for this tire size yet.
|
| 503 |
+
</div>
|
| 504 |
+
) : (
|
| 505 |
+
selectedProductHistory.map((item) => {
|
| 506 |
+
const isAddition = item.type === 'STOCK_IN';
|
| 507 |
+
const isReduction = item.type === 'STOCK_OUT';
|
| 508 |
+
|
| 509 |
+
return (
|
| 510 |
+
<div key={item.id} className="border border-slate-50 hover:bg-slate-50/50 p-2.5 rounded-xl space-y-1 text-xs">
|
| 511 |
+
<div className="flex justify-between items-center">
|
| 512 |
+
<span className="font-mono text-[9px] text-slate-400">
|
| 513 |
+
{new Date(item.dateTime).toLocaleString()}
|
| 514 |
+
</span>
|
| 515 |
+
|
| 516 |
+
<div className="flex items-center gap-1">
|
| 517 |
+
{isAddition ? (
|
| 518 |
+
<span className="bg-emerald-100 text-emerald-800 text-[9px] font-bold px-1.5 py-0.5 rounded font-mono flex items-center gap-0.5">
|
| 519 |
+
<ArrowUpRight className="w-3 h-3" />
|
| 520 |
+
+{item.quantity} In
|
| 521 |
+
</span>
|
| 522 |
+
) : isReduction ? (
|
| 523 |
+
<span className="bg-red-100 text-red-800 text-[9px] font-bold px-1.5 py-0.5 rounded font-mono flex items-center gap-0.5">
|
| 524 |
+
<ArrowDownRight className="w-3 h-3" />
|
| 525 |
+
{item.quantity} Sold
|
| 526 |
+
</span>
|
| 527 |
+
) : (
|
| 528 |
+
<span className="bg-blue-100 text-blue-800 text-[9px] font-bold px-1.5 py-0.5 rounded font-mono">
|
| 529 |
+
Recount: {item.quantity > 0 ? `+${item.quantity}` : item.quantity}
|
| 530 |
+
</span>
|
| 531 |
+
)}
|
| 532 |
+
</div>
|
| 533 |
+
</div>
|
| 534 |
+
|
| 535 |
+
<p className="font-semibold text-slate-800 leading-tight">
|
| 536 |
+
{item.reason}
|
| 537 |
+
</p>
|
| 538 |
+
|
| 539 |
+
<div className="text-[10px] text-slate-500 pt-1 flex justify-between border-t border-slate-100/50">
|
| 540 |
+
<span>Adjusted by: <span className="font-bold text-slate-700">{item.adjustedBy}</span></span>
|
| 541 |
+
<span>Resulting Stock: <strong className="font-mono">{item.resultingStock}</strong></span>
|
| 542 |
+
</div>
|
| 543 |
+
</div>
|
| 544 |
+
);
|
| 545 |
+
})
|
| 546 |
+
)}
|
| 547 |
+
</div>
|
| 548 |
+
</div>
|
| 549 |
+
</div>
|
| 550 |
+
</div>
|
| 551 |
+
)}
|
| 552 |
+
</div>
|
| 553 |
+
|
| 554 |
+
{/* CORE MODAL: NEW PRODUCT ADDITION FORM */}
|
| 555 |
+
{showAddModal && (
|
| 556 |
+
<div className="fixed inset-0 bg-slate-950/75 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
| 557 |
+
<div className="bg-white rounded-2xl border border-slate-200 max-w-lg w-full p-6 md:p-8 space-y-6 relative shadow-2xl">
|
| 558 |
+
<button
|
| 559 |
+
onClick={() => setShowAddModal(false)}
|
| 560 |
+
className="absolute top-4 right-4 bg-slate-100 hover:bg-slate-200 text-slate-500 hover:text-slate-900 font-bold px-3 py-1 rounded text-xs cursor-pointer"
|
| 561 |
+
>
|
| 562 |
+
✕
|
| 563 |
+
</button>
|
| 564 |
+
|
| 565 |
+
<div className="pb-3 border-b border-slate-100 text-center">
|
| 566 |
+
<h3 className="text-xl font-black text-slate-950">Add Brand-New Tire to Database</h3>
|
| 567 |
+
<p className="text-xs text-slate-500">Configure core sizing parameters, initial stock, and pricing thresholds</p>
|
| 568 |
+
</div>
|
| 569 |
+
|
| 570 |
+
<form onSubmit={handleCreateProductSubmit} className="space-y-4 font-sans text-xs">
|
| 571 |
+
<div className="grid grid-cols-2 gap-3">
|
| 572 |
+
<div className="space-y-1">
|
| 573 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Brand SKU Barcode *</label>
|
| 574 |
+
<input
|
| 575 |
+
type="text"
|
| 576 |
+
required
|
| 577 |
+
placeholder="e.g. DUN1856514"
|
| 578 |
+
value={newSku}
|
| 579 |
+
onChange={(e)=>setNewSku(e.target.value)}
|
| 580 |
+
className="w-full border border-slate-200 bg-white p-2 rounded-xl outline-none"
|
| 581 |
+
/>
|
| 582 |
+
</div>
|
| 583 |
+
|
| 584 |
+
<div className="space-y-1">
|
| 585 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Category Type</label>
|
| 586 |
+
<select
|
| 587 |
+
value={newCategory}
|
| 588 |
+
onChange={(e)=>setNewCategory(e.target.value as any)}
|
| 589 |
+
className="w-full border border-slate-200 bg-white p-2 rounded-xl outline-none"
|
| 590 |
+
>
|
| 591 |
+
<option value="Passenger">Passenger Radial</option>
|
| 592 |
+
<option value="SUV">SUV All-Terrain</option>
|
| 593 |
+
<option value="Commercial">Heavy Commercial Cargo</option>
|
| 594 |
+
<option value="Light Truck">Light Truck Commercial</option>
|
| 595 |
+
<option value="Truck/Bus">Truck/Bus Radial (TBR)</option>
|
| 596 |
+
</select>
|
| 597 |
+
</div>
|
| 598 |
+
</div>
|
| 599 |
+
|
| 600 |
+
<div className="grid grid-cols-3 gap-3">
|
| 601 |
+
<div className="space-y-1 col-span-1">
|
| 602 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Brand / Maker *</label>
|
| 603 |
+
<input
|
| 604 |
+
type="text"
|
| 605 |
+
required
|
| 606 |
+
placeholder="e.g. Dunlop"
|
| 607 |
+
value={newBrand}
|
| 608 |
+
onChange={(e)=>setNewBrand(e.target.value)}
|
| 609 |
+
className="w-full border border-slate-200 bg-white p-2 rounded-xl outline-none"
|
| 610 |
+
/>
|
| 611 |
+
</div>
|
| 612 |
+
|
| 613 |
+
<div className="space-y-1 col-span-1">
|
| 614 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Model Spec *</label>
|
| 615 |
+
<input
|
| 616 |
+
type="text"
|
| 617 |
+
required
|
| 618 |
+
placeholder="e.g. SP Touring"
|
| 619 |
+
value={newModel}
|
| 620 |
+
onChange={(e)=>setNewModel(e.target.value)}
|
| 621 |
+
className="w-full border border-slate-200 bg-white p-2 rounded-xl outline-none"
|
| 622 |
+
/>
|
| 623 |
+
</div>
|
| 624 |
+
|
| 625 |
+
<div className="space-y-1 col-span-1">
|
| 626 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Size Dimension *</label>
|
| 627 |
+
<input
|
| 628 |
+
type="text"
|
| 629 |
+
required
|
| 630 |
+
placeholder="e.g. 195/65 R15"
|
| 631 |
+
value={newSize}
|
| 632 |
+
onChange={(e)=>setNewSize(e.target.value)}
|
| 633 |
+
className="w-full border border-slate-200 bg-white p-2 rounded-xl outline-none"
|
| 634 |
+
/>
|
| 635 |
+
</div>
|
| 636 |
+
</div>
|
| 637 |
+
|
| 638 |
+
<div className="grid grid-cols-2 gap-3 pb-3 border-b border-slate-100">
|
| 639 |
+
<div className="space-y-1">
|
| 640 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Purchase Cost price ({settings.currencySymbol})</label>
|
| 641 |
+
<input
|
| 642 |
+
type="number"
|
| 643 |
+
min="100"
|
| 644 |
+
required
|
| 645 |
+
value={newPurchasePrice}
|
| 646 |
+
onChange={(e)=>setNewPurchasePrice(parseInt(e.target.value) || 0)}
|
| 647 |
+
className="w-full font-mono border border-slate-200 bg-white p-2 rounded-xl outline-none"
|
| 648 |
+
/>
|
| 649 |
+
</div>
|
| 650 |
+
|
| 651 |
+
<div className="space-y-1">
|
| 652 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">MSRP Sell Retail price ({settings.currencySymbol})</label>
|
| 653 |
+
<input
|
| 654 |
+
type="number"
|
| 655 |
+
min="100"
|
| 656 |
+
required
|
| 657 |
+
value={newPrice}
|
| 658 |
+
onChange={(e)=>setNewPrice(parseInt(e.target.value) || 0)}
|
| 659 |
+
className="w-full font-mono border border-slate-200 bg-white p-2 rounded-xl outline-none"
|
| 660 |
+
/>
|
| 661 |
+
</div>
|
| 662 |
+
</div>
|
| 663 |
+
|
| 664 |
+
<div className="grid grid-cols-2 gap-3 text-xs">
|
| 665 |
+
<div className="space-y-1">
|
| 666 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Initial Stock Count</label>
|
| 667 |
+
<input
|
| 668 |
+
type="number"
|
| 669 |
+
min="0"
|
| 670 |
+
required
|
| 671 |
+
value={newStock}
|
| 672 |
+
onChange={(e)=>setNewStock(parseInt(e.target.value) || 0)}
|
| 673 |
+
className="w-full border border-slate-200 bg-white p-2 rounded-xl outline-none"
|
| 674 |
+
/>
|
| 675 |
+
</div>
|
| 676 |
+
|
| 677 |
+
<div className="space-y-1">
|
| 678 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Low Alert Threshold limit</label>
|
| 679 |
+
<input
|
| 680 |
+
type="number"
|
| 681 |
+
min="1"
|
| 682 |
+
required
|
| 683 |
+
value={newMinStock}
|
| 684 |
+
onChange={(e)=>setNewMinStock(parseInt(e.target.value) || 0)}
|
| 685 |
+
className="w-full border border-slate-200 bg-white p-2 rounded-xl outline-none"
|
| 686 |
+
/>
|
| 687 |
+
</div>
|
| 688 |
+
</div>
|
| 689 |
+
|
| 690 |
+
<div className="space-y-1">
|
| 691 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Description / Performance parameters</label>
|
| 692 |
+
<textarea
|
| 693 |
+
rows={2}
|
| 694 |
+
placeholder="e.g. Robust local steel belted radial with comfortable rolling noise..."
|
| 695 |
+
value={newDesc}
|
| 696 |
+
onChange={(e)=>setNewDesc(e.target.value)}
|
| 697 |
+
className="w-full border border-slate-200 bg-white p-2 rounded-xl outline-none resize-none"
|
| 698 |
+
/>
|
| 699 |
+
</div>
|
| 700 |
+
|
| 701 |
+
<button
|
| 702 |
+
type="submit"
|
| 703 |
+
className="w-full bg-slate-900 hover:bg-slate-800 text-white font-medium py-2.5 rounded-lg transition cursor-pointer shadow-sm flex items-center justify-center gap-1.5"
|
| 704 |
+
>
|
| 705 |
+
<Check className="w-4 h-4" />
|
| 706 |
+
Initialize Tire Profile SKU
|
| 707 |
+
</button>
|
| 708 |
+
</form>
|
| 709 |
+
</div>
|
| 710 |
+
</div>
|
| 711 |
+
)}
|
| 712 |
+
</div>
|
| 713 |
+
);
|
| 714 |
+
}
|
src/components/InvoiceGenerator.tsx
ADDED
|
@@ -0,0 +1,1277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @license
|
| 3 |
+
* SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import React, { useState, useMemo } from 'react';
|
| 7 |
+
import { motion, AnimatePresence } from 'motion/react';
|
| 8 |
+
import {
|
| 9 |
+
FileText,
|
| 10 |
+
User,
|
| 11 |
+
Phone,
|
| 12 |
+
Car,
|
| 13 |
+
Search,
|
| 14 |
+
Plus,
|
| 15 |
+
Trash2,
|
| 16 |
+
Check,
|
| 17 |
+
CreditCard,
|
| 18 |
+
Printer,
|
| 19 |
+
CheckCircle,
|
| 20 |
+
CloudLightning,
|
| 21 |
+
Smartphone,
|
| 22 |
+
TrendingDown,
|
| 23 |
+
Briefcase,
|
| 24 |
+
Share2,
|
| 25 |
+
Download,
|
| 26 |
+
Receipt,
|
| 27 |
+
FileDown,
|
| 28 |
+
RefreshCw
|
| 29 |
+
} from 'lucide-react';
|
| 30 |
+
import { TireProduct, Invoice, StaffUser, SystemSettings, InvoiceLineItem } from '../types';
|
| 31 |
+
|
| 32 |
+
interface InvoiceGeneratorProps {
|
| 33 |
+
products: TireProduct[];
|
| 34 |
+
invoices: Invoice[];
|
| 35 |
+
currentStaff: StaffUser;
|
| 36 |
+
settings: SystemSettings;
|
| 37 |
+
isOnline: boolean;
|
| 38 |
+
onAddInvoice: (invoice: Invoice) => void;
|
| 39 |
+
selectedInvoiceForView: Invoice | null;
|
| 40 |
+
setSelectedInvoiceForView: (invoice: Invoice | null) => void;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
export default function InvoiceGenerator({
|
| 44 |
+
products,
|
| 45 |
+
invoices,
|
| 46 |
+
currentStaff,
|
| 47 |
+
settings,
|
| 48 |
+
isOnline,
|
| 49 |
+
onAddInvoice,
|
| 50 |
+
selectedInvoiceForView,
|
| 51 |
+
setSelectedInvoiceForView
|
| 52 |
+
}: InvoiceGeneratorProps) {
|
| 53 |
+
// Wizard steps: 'DRAFT' | 'GATEWAY' | 'COMPLETED'
|
| 54 |
+
const [generationStep, setGenerationStep] = useState<'DRAFT' | 'GATEWAY' | 'COMPLETED'>('DRAFT');
|
| 55 |
+
|
| 56 |
+
// Invoice Draft State
|
| 57 |
+
const [customerName, setCustomerName] = useState('');
|
| 58 |
+
const [customerPhone, setCustomerPhone] = useState('');
|
| 59 |
+
const [customerVehicle, setCustomerVehicle] = useState('');
|
| 60 |
+
const [notes, setNotes] = useState('');
|
| 61 |
+
const [cart, setCart] = useState<Omit<InvoiceLineItem, 'totalPrice'>[]>([]);
|
| 62 |
+
const [discount, setDiscount] = useState<number>(0);
|
| 63 |
+
const [paymentMethod, setPaymentMethod] = useState<'CASH' | 'CARD' | 'BANK_TRANSFER' | 'MOBILE_WALLET'>('CASH');
|
| 64 |
+
|
| 65 |
+
// Product search in generator
|
| 66 |
+
const [prodSearch, setProdSearch] = useState('');
|
| 67 |
+
const [selectedCategory, setSelectedCategory] = useState<string>('All');
|
| 68 |
+
|
| 69 |
+
// Checkout gateway simulation state
|
| 70 |
+
const [activeGateway, setActiveGateway] = useState<'Stripe' | 'PayPal' | 'EasyPaisa' | 'JazzCash' | 'Bank'>('Stripe');
|
| 71 |
+
const [gatewayProcessing, setGatewayProcessing] = useState(false);
|
| 72 |
+
const [gatewayAuthId, setGatewayAuthId] = useState('');
|
| 73 |
+
const [gatewayCompleted, setGatewayCompleted] = useState(false);
|
| 74 |
+
const [ccNumber, setCcNumber] = useState('4242 4242 4242 4242');
|
| 75 |
+
const [ccExpiry, setCcExpiry] = useState('12/28');
|
| 76 |
+
const [ccCvc, setCcCvc] = useState('321');
|
| 77 |
+
|
| 78 |
+
// Invoice final outcome
|
| 79 |
+
const [generatedInvoice, setGeneratedInvoice] = useState<Invoice | null>(null);
|
| 80 |
+
|
| 81 |
+
// Filter products for invoice item selection
|
| 82 |
+
const filteredProducts = useMemo(() => {
|
| 83 |
+
return products.filter(p => {
|
| 84 |
+
const matchSearch =
|
| 85 |
+
p.brand.toLowerCase().includes(prodSearch.toLowerCase()) ||
|
| 86 |
+
p.model.toLowerCase().includes(prodSearch.toLowerCase()) ||
|
| 87 |
+
p.size.toLowerCase().includes(prodSearch.toLowerCase()) ||
|
| 88 |
+
p.sku.toLowerCase().includes(prodSearch.toLowerCase());
|
| 89 |
+
const matchCategory = selectedCategory === 'All' || p.category === selectedCategory;
|
| 90 |
+
return matchSearch && matchCategory;
|
| 91 |
+
});
|
| 92 |
+
}, [products, prodSearch, selectedCategory]);
|
| 93 |
+
|
| 94 |
+
// Calculations
|
| 95 |
+
const subtotal = useMemo(() => {
|
| 96 |
+
return cart.reduce((sum, item) => sum + (item.quantity * item.unitPrice), 0);
|
| 97 |
+
}, [cart]);
|
| 98 |
+
|
| 99 |
+
const taxAmount = useMemo(() => {
|
| 100 |
+
return Math.round((subtotal * settings.taxRate) / 100);
|
| 101 |
+
}, [subtotal, settings.taxRate]);
|
| 102 |
+
|
| 103 |
+
const total = useMemo(() => {
|
| 104 |
+
const calculated = subtotal + taxAmount - discount;
|
| 105 |
+
return calculated < 0 ? 0 : calculated;
|
| 106 |
+
}, [subtotal, taxAmount, discount]);
|
| 107 |
+
|
| 108 |
+
// Cart operations
|
| 109 |
+
const addToCart = (product: TireProduct) => {
|
| 110 |
+
if (product.stock <= 0) {
|
| 111 |
+
alert(`Cannot add ${product.brand} ${product.model}. Insufficient stock in warehouse.`);
|
| 112 |
+
return;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
const existingItem = cart.find(item => item.productId === product.id);
|
| 116 |
+
if (existingItem) {
|
| 117 |
+
if (existingItem.quantity >= product.stock) {
|
| 118 |
+
alert(`Stock limit reached! Only ${product.stock} tires are currently available.`);
|
| 119 |
+
return;
|
| 120 |
+
}
|
| 121 |
+
setCart(cart.map(item =>
|
| 122 |
+
item.productId === product.id
|
| 123 |
+
? { ...item, quantity: item.quantity + 1 }
|
| 124 |
+
: item
|
| 125 |
+
));
|
| 126 |
+
} else {
|
| 127 |
+
setCart([...cart, {
|
| 128 |
+
id: 'li_' + Math.random().toString(36).substr(2, 9),
|
| 129 |
+
productId: product.id,
|
| 130 |
+
brand: product.brand,
|
| 131 |
+
model: product.model,
|
| 132 |
+
size: product.size,
|
| 133 |
+
quantity: 1,
|
| 134 |
+
unitPrice: product.price
|
| 135 |
+
}]);
|
| 136 |
+
}
|
| 137 |
+
};
|
| 138 |
+
|
| 139 |
+
const updateCartQty = (productId: string, qty: number, stockLimit: number) => {
|
| 140 |
+
if (qty <= 0) {
|
| 141 |
+
setCart(cart.filter(item => item.productId !== productId));
|
| 142 |
+
return;
|
| 143 |
+
}
|
| 144 |
+
if (qty > stockLimit) {
|
| 145 |
+
alert(`Insufficient stock! Only ${stockLimit} items available in warehouse.`);
|
| 146 |
+
return;
|
| 147 |
+
}
|
| 148 |
+
setCart(cart.map(item =>
|
| 149 |
+
item.productId === productId ? { ...item, quantity: qty } : item
|
| 150 |
+
));
|
| 151 |
+
};
|
| 152 |
+
|
| 153 |
+
const removeFromCart = (productId: string) => {
|
| 154 |
+
setCart(cart.filter(item => item.productId !== productId));
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
// Payment triggers
|
| 158 |
+
const handlePaymentSubmit = () => {
|
| 159 |
+
if (cart.length === 0) {
|
| 160 |
+
alert("Cart empty! Build custom tire elements first.");
|
| 161 |
+
return;
|
| 162 |
+
}
|
| 163 |
+
if (!customerName.trim()) {
|
| 164 |
+
alert("Customer Name is required to bind transaction record.");
|
| 165 |
+
return;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
// Cash checkout needs no gateway simulation
|
| 169 |
+
if (paymentMethod === 'CASH') {
|
| 170 |
+
finalizeInvoice(null);
|
| 171 |
+
} else {
|
| 172 |
+
// Map gateway based on method
|
| 173 |
+
if (paymentMethod === 'CARD') {
|
| 174 |
+
setActiveGateway('Stripe');
|
| 175 |
+
} else if (paymentMethod === 'MOBILE_WALLET') {
|
| 176 |
+
setActiveGateway('EasyPaisa');
|
| 177 |
+
} else {
|
| 178 |
+
setActiveGateway('Bank');
|
| 179 |
+
}
|
| 180 |
+
setGenerationStep('GATEWAY');
|
| 181 |
+
setGatewayCompleted(false);
|
| 182 |
+
setGatewayAuthId('TXN-' + Math.floor(100000 + Math.random() * 900000));
|
| 183 |
+
}
|
| 184 |
+
};
|
| 185 |
+
|
| 186 |
+
// Secure final commit
|
| 187 |
+
const finalizeInvoice = (gatewayData: Invoice['gatewayInfo'] | null) => {
|
| 188 |
+
const invNumber = `HBT-${new Date().getFullYear()}-${1000 + invoices.length + 1}`;
|
| 189 |
+
|
| 190 |
+
// Convert cart to full InvoiceLineItems
|
| 191 |
+
const finalItems: InvoiceLineItem[] = cart.map(item => ({
|
| 192 |
+
...item,
|
| 193 |
+
totalPrice: item.quantity * item.unitPrice
|
| 194 |
+
}));
|
| 195 |
+
|
| 196 |
+
// Local Format custom display date
|
| 197 |
+
const date = new Date();
|
| 198 |
+
const formattedDate = `${String(date.getDate()).padStart(2, '0')}-${String(date.getMonth() + 1).padStart(2, '0')}-${date.getFullYear()} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`;
|
| 199 |
+
|
| 200 |
+
const newInvoice: Invoice = {
|
| 201 |
+
id: 'inv_' + Math.random().toString(36).substr(2, 9),
|
| 202 |
+
invoiceNumber: invNumber,
|
| 203 |
+
dateTime: formattedDate,
|
| 204 |
+
customerName,
|
| 205 |
+
customerPhone,
|
| 206 |
+
customerVehicle,
|
| 207 |
+
items: finalItems,
|
| 208 |
+
subtotal,
|
| 209 |
+
taxRate: settings.taxRate,
|
| 210 |
+
taxAmount,
|
| 211 |
+
discount,
|
| 212 |
+
total,
|
| 213 |
+
currencySymbol: settings.currencySymbol,
|
| 214 |
+
currencyCode: settings.currencyCode,
|
| 215 |
+
paymentMethod,
|
| 216 |
+
paymentStatus: 'PAID',
|
| 217 |
+
amountPaid: total,
|
| 218 |
+
cashierName: currentStaff.name,
|
| 219 |
+
notes: notes.trim() ? notes : undefined,
|
| 220 |
+
gatewayInfo: gatewayData || undefined,
|
| 221 |
+
isOfflineCreated: !isOnline // Tracks offline generation tags
|
| 222 |
+
};
|
| 223 |
+
|
| 224 |
+
onAddInvoice(newInvoice);
|
| 225 |
+
setGeneratedInvoice(newInvoice);
|
| 226 |
+
setGenerationStep('COMPLETED');
|
| 227 |
+
|
| 228 |
+
// Wipe drafts
|
| 229 |
+
setCustomerName('');
|
| 230 |
+
setCustomerPhone('');
|
| 231 |
+
setCustomerVehicle('');
|
| 232 |
+
setNotes('');
|
| 233 |
+
setCart([]);
|
| 234 |
+
setDiscount(0);
|
| 235 |
+
};
|
| 236 |
+
|
| 237 |
+
// Execute Gateway authorization animation
|
| 238 |
+
const handleAuthorizeGateway = () => {
|
| 239 |
+
setGatewayProcessing(true);
|
| 240 |
+
setTimeout(() => {
|
| 241 |
+
setGatewayProcessing(false);
|
| 242 |
+
setGatewayCompleted(true);
|
| 243 |
+
setTimeout(() => {
|
| 244 |
+
// Complete checkout with authentic keys
|
| 245 |
+
finalizeInvoice({
|
| 246 |
+
gatewayName: activeGateway,
|
| 247 |
+
transactionId: gatewayAuthId,
|
| 248 |
+
authTime: new Date().toISOString()
|
| 249 |
+
});
|
| 250 |
+
}, 1000);
|
| 251 |
+
}, 1800);
|
| 252 |
+
};
|
| 253 |
+
|
| 254 |
+
// Format HTML share links
|
| 255 |
+
const copyShareText = (inv: Invoice) => {
|
| 256 |
+
const textDetails = `⚡ *Invoice ${inv.invoiceNumber} — Haider Brother Traders* ⚡\nVehicle: ${inv.customerVehicle || 'N/A'}\nCustomer: ${inv.customerName}\nTotal amount settled: *${inv.currencySymbol} ${inv.total.toLocaleString()}*\nPayment: ${inv.paymentMethod} (PAID)\n💼 Shop Phone: ${settings.shopPhone}\nThank you for choosing Haider Brother Traders!`;
|
| 257 |
+
navigator.clipboard.writeText(textDetails);
|
| 258 |
+
alert("Invoice share prompt successfully copied to clipboard (optimized for WhatsApp/Social)!");
|
| 259 |
+
};
|
| 260 |
+
|
| 261 |
+
// Export invoice structure to readable offline-friendly raw layout HTML file
|
| 262 |
+
const exportOfflineHTMLReceipt = (inv: Invoice) => {
|
| 263 |
+
const htmlSnippet = `
|
| 264 |
+
<!DOCTYPE html>
|
| 265 |
+
<html>
|
| 266 |
+
<head>
|
| 267 |
+
<title>Invoice ${inv.invoiceNumber}</title>
|
| 268 |
+
<style>
|
| 269 |
+
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #333; padding: 40px; line-height: 1.6; }
|
| 270 |
+
.header { border-bottom: 2px solid #0f172a; padding-bottom: 20px; margin-bottom: 30px; display: flex; justify-content: space-between; }
|
| 271 |
+
.brand { font-size: 24px; font-weight: bold; text-transform: uppercase; }
|
| 272 |
+
.meta { text-align: right; font-size: 14px; }
|
| 273 |
+
.customer { background: #f8fafc; padding: 15px; border-radius: 8px; margin-bottom: 30px; }
|
| 274 |
+
table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
|
| 275 |
+
th { background: #0f172a; color: #fff; text-align: left; padding: 12px; font-size: 14px; }
|
| 276 |
+
td { border-bottom: 1px solid #e2e8f0; padding: 12px; font-size: 14px; }
|
| 277 |
+
.summary { text-align: right; width: 40%; margin-left: auto; font-size: 14px; }
|
| 278 |
+
.summary-row { display: flex; justify-content: space-between; padding: 8px 0; }
|
| 279 |
+
.bold { font-weight: bold; font-size: 16px; border-top: 1px solid #333; padding-top: 10px; }
|
| 280 |
+
.center { text-align: center; margin-top: 50px; font-size: 12px; color: #777; }
|
| 281 |
+
</style>
|
| 282 |
+
</head>
|
| 283 |
+
<body>
|
| 284 |
+
<div class="header">
|
| 285 |
+
<div>
|
| 286 |
+
<div class="brand">${settings.shopName}</div>
|
| 287 |
+
<div style="font-size: 13px; color: #555;">${settings.shopAddress}</div>
|
| 288 |
+
<div style="font-size: 13px; color: #555;">Phone: ${settings.shopPhone}</div>
|
| 289 |
+
</div>
|
| 290 |
+
<div class="meta">
|
| 291 |
+
<h2>INVOICE</h2>
|
| 292 |
+
<div><strong>Inv No:</strong> ${inv.invoiceNumber}</div>
|
| 293 |
+
<div><strong>Date:</strong> ${inv.dateTime}</div>
|
| 294 |
+
<div><strong>Cashier:</strong> ${inv.cashierName}</div>
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
+
<div class="customer">
|
| 298 |
+
<strong>Customer Information:</strong>
|
| 299 |
+
<div>Name: ${inv.customerName}</div>
|
| 300 |
+
<div>Phone: ${inv.customerPhone || 'N/A'}</div>
|
| 301 |
+
<div>Vehicle: ${inv.customerVehicle || 'N/A'}</div>
|
| 302 |
+
</div>
|
| 303 |
+
<table>
|
| 304 |
+
<thead>
|
| 305 |
+
<tr>
|
| 306 |
+
<th>Item / Size</th>
|
| 307 |
+
<th>Qty</th>
|
| 308 |
+
<th>Unit Price</th>
|
| 309 |
+
<th>Total</th>
|
| 310 |
+
</tr>
|
| 311 |
+
</thead>
|
| 312 |
+
<tbody>
|
| 313 |
+
${inv.items.map(item => `
|
| 314 |
+
<tr>
|
| 315 |
+
<td>${item.brand} ${item.model} (${item.size})</td>
|
| 316 |
+
<td>${item.quantity}</td>
|
| 317 |
+
<td>${inv.currencySymbol}${item.unitPrice.toLocaleString()}</td>
|
| 318 |
+
<td>${inv.currencySymbol}${item.totalPrice.toLocaleString()}</td>
|
| 319 |
+
</tr>
|
| 320 |
+
`).join('')}
|
| 321 |
+
</tbody>
|
| 322 |
+
</table>
|
| 323 |
+
<div class="summary">
|
| 324 |
+
<div class="summary-row"><span>Subtotal:</span> <span>${inv.currencySymbol}${inv.subtotal.toLocaleString()}</span></div>
|
| 325 |
+
<div class="summary-row"><span>${settings.taxName} (${inv.taxRate}%):</span> <span>${inv.currencySymbol}${inv.taxAmount.toLocaleString()}</span></div>
|
| 326 |
+
<div class="summary-row"><span>Discounts:</span> <span>-${inv.currencySymbol}${inv.discount.toLocaleString()}</span></div>
|
| 327 |
+
<div class="summary-row class="bold" style="font-weight: bold; border-top: 2px solid #000; padding:10px 0;">
|
| 328 |
+
<span>Total Settled:</span> <span>${inv.currencySymbol}${inv.total.toLocaleString()}</span>
|
| 329 |
+
</div>
|
| 330 |
+
</div>
|
| 331 |
+
<p style="font-size: 12px; font-style: italic; margin-top: 40px;">* Invoice generated under security ledger vault. Offline tag: ${inv.isOfflineCreated ? 'True' : 'False'}</p>
|
| 332 |
+
<div class="center">
|
| 333 |
+
Thank you for choosing Haider Brother Traders!
|
| 334 |
+
</div>
|
| 335 |
+
</body>
|
| 336 |
+
</html>
|
| 337 |
+
`;
|
| 338 |
+
|
| 339 |
+
const blob = new Blob([htmlSnippet], { type: 'text/html' });
|
| 340 |
+
const link = document.createElement('a');
|
| 341 |
+
link.href = URL.createObjectURL(blob);
|
| 342 |
+
link.download = `Invoice_${inv.invoiceNumber}.html`;
|
| 343 |
+
link.click();
|
| 344 |
+
};
|
| 345 |
+
|
| 346 |
+
const handleManualPrint = () => {
|
| 347 |
+
window.print();
|
| 348 |
+
};
|
| 349 |
+
|
| 350 |
+
return (
|
| 351 |
+
<div className="space-y-8" id="invoice-builder-panel">
|
| 352 |
+
{/* Offline Status Warning Indicator */}
|
| 353 |
+
{!isOnline && (
|
| 354 |
+
<div className="bg-orange-50 border border-orange-200 text-orange-850 px-4 py-3 rounded-xl flex items-center justify-between gap-3 text-xs md:text-sm">
|
| 355 |
+
<div className="flex items-center gap-2">
|
| 356 |
+
<CloudLightning className="w-5 h-5 col-span-1 text-orange-600 animate-pulse" />
|
| 357 |
+
<span>
|
| 358 |
+
<strong>Offline Mode Activated</strong> — Internet connection offline. Drafted invoices are instantly committed and backed up locally to cache memory and will sync when system restarts or connects.
|
| 359 |
+
</span>
|
| 360 |
+
</div>
|
| 361 |
+
<span className="font-bold text-orange-900 border border-orange-300 rounded px-2 py-0.5 text-[10px] uppercase font-mono">
|
| 362 |
+
AIR-GAPPED COCKPIT
|
| 363 |
+
</span>
|
| 364 |
+
</div>
|
| 365 |
+
)}
|
| 366 |
+
|
| 367 |
+
{/* Main split wizard screen */}
|
| 368 |
+
{generationStep === 'DRAFT' && (
|
| 369 |
+
<div className="grid grid-cols-1 xl:grid-cols-12 gap-8">
|
| 370 |
+
{/* LEFT PANEL: Bill drafting & Customer Onboarding */}
|
| 371 |
+
<div className="xl:col-span-8 space-y-6">
|
| 372 |
+
<div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm space-y-6">
|
| 373 |
+
<div className="flex items-center gap-3 pb-4 border-b border-slate-100">
|
| 374 |
+
<div className="bg-slate-100 text-slate-800 p-2.5 rounded-lg">
|
| 375 |
+
<User className="w-5 h-5" />
|
| 376 |
+
</div>
|
| 377 |
+
<div>
|
| 378 |
+
<h3 className="text-base font-bold text-slate-900 uppercase tracking-wide">Customer & Vehicle Details</h3>
|
| 379 |
+
<p className="text-xs text-slate-400">Provide client and vehicle details for invoice audit accuracy</p>
|
| 380 |
+
</div>
|
| 381 |
+
</div>
|
| 382 |
+
|
| 383 |
+
{/* Patient Fields */}
|
| 384 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
| 385 |
+
<div className="space-y-1">
|
| 386 |
+
<label className="text-xs font-semibold text-slate-600 flex items-center gap-1.5">
|
| 387 |
+
Customer Name <span className="text-red-500">*</span>
|
| 388 |
+
</label>
|
| 389 |
+
<div className="relative">
|
| 390 |
+
<span className="absolute left-3.5 top-3 text-slate-400">
|
| 391 |
+
<User className="w-3.5 h-3.5" />
|
| 392 |
+
</span>
|
| 393 |
+
<input
|
| 394 |
+
type="text"
|
| 395 |
+
placeholder="e.g. Mehmood Khan"
|
| 396 |
+
value={customerName}
|
| 397 |
+
onChange={(e) => setCustomerName(e.target.value)}
|
| 398 |
+
className="w-full text-xs border border-slate-200 outline-none focus:border-slate-800 bg-white rounded-lg pl-9 pr-4 py-2 font-medium transition duration-150"
|
| 399 |
+
/>
|
| 400 |
+
</div>
|
| 401 |
+
</div>
|
| 402 |
+
|
| 403 |
+
<div className="space-y-1">
|
| 404 |
+
<label className="text-xs font-semibold text-slate-600">Client Phone Number</label>
|
| 405 |
+
<div className="relative">
|
| 406 |
+
<span className="absolute left-3.5 top-3 text-slate-400">
|
| 407 |
+
<Phone className="w-3.5 h-3.5" />
|
| 408 |
+
</span>
|
| 409 |
+
<input
|
| 410 |
+
type="text"
|
| 411 |
+
placeholder="e.g. 0333-1234567"
|
| 412 |
+
value={customerPhone}
|
| 413 |
+
onChange={(e) => setCustomerPhone(e.target.value)}
|
| 414 |
+
className="w-full text-xs border border-slate-200 outline-none focus:border-slate-800 bg-white rounded-lg pl-9 pr-4 py-2 font-medium transition duration-150"
|
| 415 |
+
/>
|
| 416 |
+
</div>
|
| 417 |
+
</div>
|
| 418 |
+
|
| 419 |
+
<div className="space-y-1">
|
| 420 |
+
<label className="text-xs font-semibold text-slate-600">Vehicle / Reg Mark</label>
|
| 421 |
+
<div className="relative">
|
| 422 |
+
<span className="absolute left-3.5 top-3 text-slate-400">
|
| 423 |
+
<Car className="w-3.5 h-3.5" />
|
| 424 |
+
</span>
|
| 425 |
+
<input
|
| 426 |
+
type="text"
|
| 427 |
+
placeholder="e.g. Toyota Prado (BE-777)"
|
| 428 |
+
value={customerVehicle}
|
| 429 |
+
onChange={(e) => setCustomerVehicle(e.target.value)}
|
| 430 |
+
className="w-full text-xs border border-slate-200 outline-none focus:border-slate-800 bg-white rounded-lg pl-9 pr-4 py-2 font-medium transition duration-150"
|
| 431 |
+
/>
|
| 432 |
+
</div>
|
| 433 |
+
</div>
|
| 434 |
+
</div>
|
| 435 |
+
</div>
|
| 436 |
+
|
| 437 |
+
{/* PRODUCT SEARCH AND SELECTION PANEL */}
|
| 438 |
+
<div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm space-y-5">
|
| 439 |
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-4 border-b border-slate-100">
|
| 440 |
+
<div className="flex items-center gap-3">
|
| 441 |
+
<div className="bg-slate-100 text-slate-800 p-2.5 rounded-lg">
|
| 442 |
+
<Search className="w-5 h-5" />
|
| 443 |
+
</div>
|
| 444 |
+
<div>
|
| 445 |
+
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Tire Stock Matcher</h3>
|
| 446 |
+
<p className="text-xs text-slate-400">Search and map tires directly onto live checkout ledger</p>
|
| 447 |
+
</div>
|
| 448 |
+
</div>
|
| 449 |
+
|
| 450 |
+
{/* Categories */}
|
| 451 |
+
<div className="flex gap-1.5 overflow-x-auto pb-1 sm:pb-0 font-sans">
|
| 452 |
+
{['All', 'Passenger', 'SUV', 'Commercial', 'Light Truck', 'Truck/Bus'].map(cat => (
|
| 453 |
+
<button
|
| 454 |
+
key={cat}
|
| 455 |
+
type="button"
|
| 456 |
+
onClick={() => setSelectedCategory(cat)}
|
| 457 |
+
className={`text-xs px-2.5 py-1.5 font-semibold rounded-md shrink-0 transition cursor-pointer ${
|
| 458 |
+
selectedCategory === cat
|
| 459 |
+
? 'bg-slate-900 text-white shadow-sm'
|
| 460 |
+
: 'bg-slate-50 text-slate-600 hover:bg-slate-100 border border-slate-200'
|
| 461 |
+
}`}
|
| 462 |
+
>
|
| 463 |
+
{cat}
|
| 464 |
+
</button>
|
| 465 |
+
))}
|
| 466 |
+
</div>
|
| 467 |
+
</div>
|
| 468 |
+
|
| 469 |
+
{/* Fast Search input */}
|
| 470 |
+
<div className="relative">
|
| 471 |
+
<span className="absolute left-3.5 top-3 text-slate-400">
|
| 472 |
+
<Search className="w-4 h-4" />
|
| 473 |
+
</span>
|
| 474 |
+
<input
|
| 475 |
+
type="text"
|
| 476 |
+
placeholder="Search by tire brand, profile size (e.g. 195/65), or model/SKU SKU..."
|
| 477 |
+
value={prodSearch}
|
| 478 |
+
onChange={(e) => setProdSearch(e.target.value)}
|
| 479 |
+
className="w-full text-xs border border-slate-200 outline-none focus:border-slate-800 bg-white rounded-lg pl-10 pr-4 py-2 font-sans transition duration-150"
|
| 480 |
+
/>
|
| 481 |
+
</div>
|
| 482 |
+
|
| 483 |
+
{/* Searched Items Grid */}
|
| 484 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-h-96 overflow-y-auto pr-1">
|
| 485 |
+
{filteredProducts.length === 0 ? (
|
| 486 |
+
<div className="col-span-2 text-center py-12 border border-dashed border-slate-200 rounded-lg bg-slate-50">
|
| 487 |
+
<p className="text-sm text-slate-400">No tires matching database entries.</p>
|
| 488 |
+
</div>
|
| 489 |
+
) : (
|
| 490 |
+
filteredProducts.map(p => {
|
| 491 |
+
const isLowStock = p.stock <= p.minStock;
|
| 492 |
+
const isOutOfStock = p.stock === 0;
|
| 493 |
+
|
| 494 |
+
return (
|
| 495 |
+
<div
|
| 496 |
+
key={p.id}
|
| 497 |
+
className={`border rounded-lg p-4 flex flex-col justify-between gap-4 transition duration-150 relative overflow-hidden ${
|
| 498 |
+
isOutOfStock ? 'bg-slate-50 border-slate-200 opacity-60' :
|
| 499 |
+
isLowStock ? 'bg-red-50/20 border-red-200 hover:border-red-300' :
|
| 500 |
+
'bg-white hover:border-slate-400 border-slate-200'
|
| 501 |
+
}`}
|
| 502 |
+
>
|
| 503 |
+
{isLowStock && (
|
| 504 |
+
<div className={`absolute top-0 right-0 px-2 py-0.5 text-[9px] font-bold uppercase rounded-bl-md ${isOutOfStock ? 'bg-slate-400 text-white' : 'bg-red-600 text-white shadow-sm'}`}>
|
| 505 |
+
{isOutOfStock ? 'OUT OF STOCK' : 'LOW STOCK'}
|
| 506 |
+
</div>
|
| 507 |
+
)}
|
| 508 |
+
<div className="space-y-2">
|
| 509 |
+
<div className="space-y-0.5">
|
| 510 |
+
<span className="text-[9px] text-slate-400 uppercase tracking-wider font-extrabold">{p.category}</span>
|
| 511 |
+
<h4 className="font-bold text-slate-900 text-sm">{p.brand} {p.model}</h4>
|
| 512 |
+
<p className="font-mono text-xs text-slate-800 font-semibold bg-slate-100 inline-block px-1.5 py-0.5 rounded-sm">
|
| 513 |
+
{p.size}
|
| 514 |
+
</p>
|
| 515 |
+
</div>
|
| 516 |
+
<p className="text-xs text-slate-500 line-clamp-2 leading-relaxed">
|
| 517 |
+
{p.description || "High structural performance tire."}
|
| 518 |
+
</p>
|
| 519 |
+
</div>
|
| 520 |
+
|
| 521 |
+
<div className="flex items-center justify-between border-t border-slate-100 pt-3">
|
| 522 |
+
<div className="space-y-0.5">
|
| 523 |
+
<span className="text-[10px] text-slate-400 font-semibold font-sans">MSRP Retail Price</span>
|
| 524 |
+
<p className="text-sm font-extrabold text-slate-950">
|
| 525 |
+
{settings.currencySymbol} {p.price.toLocaleString()}
|
| 526 |
+
</p>
|
| 527 |
+
<span className="text-[10px] text-slate-400">Qty in stock: <span className="font-bold text-slate-700">{p.stock}</span></span>
|
| 528 |
+
</div>
|
| 529 |
+
|
| 530 |
+
<button
|
| 531 |
+
type="button"
|
| 532 |
+
disabled={isOutOfStock}
|
| 533 |
+
onClick={() => addToCart(p)}
|
| 534 |
+
className={`p-2 rounded-md cursor-pointer transition flex items-center justify-center ${
|
| 535 |
+
isOutOfStock ? 'bg-slate-200 text-slate-400 cursor-not-allowed' :
|
| 536 |
+
'bg-slate-900 hover:bg-slate-800 text-white shadow-sm'
|
| 537 |
+
}`}
|
| 538 |
+
>
|
| 539 |
+
<Plus className="w-4 h-4" />
|
| 540 |
+
</button>
|
| 541 |
+
</div>
|
| 542 |
+
</div>
|
| 543 |
+
);
|
| 544 |
+
})
|
| 545 |
+
)}
|
| 546 |
+
</div>
|
| 547 |
+
</div>
|
| 548 |
+
</div>
|
| 549 |
+
|
| 550 |
+
{/* RIGHT PANEL: Live Cart Summary & Checkout triggers */}
|
| 551 |
+
<div className="xl:col-span-4 space-y-6">
|
| 552 |
+
<div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex flex-col justify-between min-h-[500px]">
|
| 553 |
+
<div className="space-y-4">
|
| 554 |
+
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
| 555 |
+
<h3 className="font-bold text-slate-900 uppercase tracking-wide text-xs flex items-center gap-2">
|
| 556 |
+
<Receipt className="w-4 h-4 text-slate-705" />
|
| 557 |
+
Ledger Checkout Cart
|
| 558 |
+
</h3>
|
| 559 |
+
<span className="bg-slate-950 text-white text-[10px] font-bold px-2 py-0.5 rounded-md font-mono">
|
| 560 |
+
{cart.reduce((sum, item) => sum + item.quantity, 0)} Pcs
|
| 561 |
+
</span>
|
| 562 |
+
</div>
|
| 563 |
+
|
| 564 |
+
{/* Items layout */}
|
| 565 |
+
<div className="space-y-3 overflow-y-auto max-h-[300px] pr-1">
|
| 566 |
+
{cart.length === 0 ? (
|
| 567 |
+
<div className="text-center py-16 text-slate-400 space-y-2">
|
| 568 |
+
<Printer className="w-10 h-10 mx-auto text-slate-300" />
|
| 569 |
+
<p className="text-[11px] leading-normal">Ledger is empty. Select products from matching list to queue invoice items.</p>
|
| 570 |
+
</div>
|
| 571 |
+
) : (
|
| 572 |
+
cart.map((item) => {
|
| 573 |
+
const matchedProd = products.find(p => p.id === item.productId);
|
| 574 |
+
const maxStock = matchedProd ? matchedProd.stock : 100;
|
| 575 |
+
|
| 576 |
+
return (
|
| 577 |
+
<div key={item.id} className="border border-slate-100 bg-slate-50 p-2.5 rounded-lg space-y-2">
|
| 578 |
+
<div className="flex justify-between items-start">
|
| 579 |
+
<div className="space-y-0.5">
|
| 580 |
+
<h5 className="text-xs font-bold text-slate-900 leading-tight">{item.brand} {item.model}</h5>
|
| 581 |
+
<p className="text-[9px] font-mono font-medium text-slate-500 bg-slate-200/50 inline-block px-1 rounded-sm">{item.size}</p>
|
| 582 |
+
</div>
|
| 583 |
+
<button
|
| 584 |
+
type="button"
|
| 585 |
+
onClick={() => removeFromCart(item.productId)}
|
| 586 |
+
className="text-slate-450 hover:text-red-600 p-1 rounded transition cursor-pointer"
|
| 587 |
+
>
|
| 588 |
+
<Trash2 className="w-3.5 h-3.5" />
|
| 589 |
+
</button>
|
| 590 |
+
</div>
|
| 591 |
+
|
| 592 |
+
<div className="flex items-center justify-between border-t border-slate-100 pt-2">
|
| 593 |
+
{/* Quantity buttons */}
|
| 594 |
+
<div className="flex items-center border border-slate-200 bg-white rounded-md">
|
| 595 |
+
<button
|
| 596 |
+
type="button"
|
| 597 |
+
onClick={() => updateCartQty(item.productId, item.quantity - 1, maxStock)}
|
| 598 |
+
className="px-2 py-0.5 text-xs text-slate-500 hover:bg-slate-100 rounded-l-md transition"
|
| 599 |
+
>
|
| 600 |
+
-
|
| 601 |
+
</button>
|
| 602 |
+
<span className="px-2 text-xs font-bold font-mono text-slate-800">{item.quantity}</span>
|
| 603 |
+
<button
|
| 604 |
+
type="button"
|
| 605 |
+
onClick={() => updateCartQty(item.productId, item.quantity + 1, maxStock)}
|
| 606 |
+
className="px-2 py-0.5 text-xs text-slate-500 hover:bg-slate-100 rounded-r-md transition"
|
| 607 |
+
>
|
| 608 |
+
+
|
| 609 |
+
</button>
|
| 610 |
+
</div>
|
| 611 |
+
|
| 612 |
+
<span className="text-xs font-bold text-slate-900 font-mono">
|
| 613 |
+
{settings.currencySymbol} {(item.quantity * item.unitPrice).toLocaleString()}
|
| 614 |
+
</span>
|
| 615 |
+
</div>
|
| 616 |
+
</div>
|
| 617 |
+
);
|
| 618 |
+
})
|
| 619 |
+
)}
|
| 620 |
+
</div>
|
| 621 |
+
</div>
|
| 622 |
+
|
| 623 |
+
{/* Totals Section */}
|
| 624 |
+
<div className="border-t border-slate-200 pt-4 space-y-3 mt-6">
|
| 625 |
+
<div className="flex justify-between text-xs">
|
| 626 |
+
<span className="text-slate-500 font-semibold">Subtotal</span>
|
| 627 |
+
<span className="font-semibold text-slate-800 font-mono">{settings.currencySymbol} {subtotal.toLocaleString()}</span>
|
| 628 |
+
</div>
|
| 629 |
+
|
| 630 |
+
<div className="flex justify-between text-xs">
|
| 631 |
+
<span className="text-slate-500 font-semibold">{settings.taxName} ({settings.taxRate}%)</span>
|
| 632 |
+
<span className="font-semibold text-slate-800 font-mono">{settings.currencySymbol} {taxAmount.toLocaleString()}</span>
|
| 633 |
+
</div>
|
| 634 |
+
|
| 635 |
+
{/* Discount field */}
|
| 636 |
+
<div className="flex justify-between items-center text-xs">
|
| 637 |
+
<span className="text-slate-500 font-semibold flex items-center gap-1">
|
| 638 |
+
<TrendingDown className="w-3.5 h-3.5 text-slate-500" />
|
| 639 |
+
Special Discount ({settings.currencySymbol})
|
| 640 |
+
</span>
|
| 641 |
+
<input
|
| 642 |
+
type="number"
|
| 643 |
+
min="0"
|
| 644 |
+
placeholder="0"
|
| 645 |
+
value={discount || ''}
|
| 646 |
+
onChange={(e) => setDiscount(Math.max(0, parseInt(e.target.value) || 0))}
|
| 647 |
+
className="w-20 border border-slate-200 focus:border-slate-800 text-right text-xs outline-none bg-white hover:bg-slate-50 focus:bg-white rounded-md p-1 font-mono font-bold"
|
| 648 |
+
/>
|
| 649 |
+
</div>
|
| 650 |
+
|
| 651 |
+
{/* Final calculated total size */}
|
| 652 |
+
<div className="border-t border-dashed border-slate-200 pt-3 flex justify-between items-center">
|
| 653 |
+
<span className="text-xs font-bold text-slate-900 uppercase">Total Settled Amount</span>
|
| 654 |
+
<span className="text-base font-black text-slate-950 font-mono">
|
| 655 |
+
{settings.currencySymbol} {total.toLocaleString()}
|
| 656 |
+
</span>
|
| 657 |
+
</div>
|
| 658 |
+
|
| 659 |
+
{/* Payment strategy */}
|
| 660 |
+
<div className="space-y-1.5 pt-2">
|
| 661 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Select Settlement Method</label>
|
| 662 |
+
<div className="grid grid-cols-2 gap-2 text-[10px] font-sans">
|
| 663 |
+
<button
|
| 664 |
+
type="button"
|
| 665 |
+
onClick={() => setPaymentMethod('CASH')}
|
| 666 |
+
className={`py-1.5 px-2 border rounded-lg flex flex-col items-center gap-1 cursor-pointer font-bold ${
|
| 667 |
+
paymentMethod === 'CASH' ? 'bg-slate-900 text-white border-transparent shadow-sm' : 'bg-slate-50 text-slate-700 hover:bg-slate-100 border-slate-200'
|
| 668 |
+
}`}
|
| 669 |
+
>
|
| 670 |
+
<Briefcase className="w-3.5 h-3.5" />
|
| 671 |
+
Cash
|
| 672 |
+
</button>
|
| 673 |
+
<button
|
| 674 |
+
type="button"
|
| 675 |
+
onClick={() => setPaymentMethod('CARD')}
|
| 676 |
+
className={`py-1.5 px-2 border rounded-lg flex flex-col items-center gap-1 cursor-pointer font-bold ${
|
| 677 |
+
paymentMethod === 'CARD' ? 'bg-slate-900 text-white border-transparent shadow-sm' : 'bg-slate-50 text-slate-700 hover:bg-slate-100 border-slate-200'
|
| 678 |
+
}`}
|
| 679 |
+
>
|
| 680 |
+
<CreditCard className="w-3.5 h-3.5" />
|
| 681 |
+
Card Swipe
|
| 682 |
+
</button>
|
| 683 |
+
<button
|
| 684 |
+
type="button"
|
| 685 |
+
onClick={() => setPaymentMethod('BANK_TRANSFER')}
|
| 686 |
+
className={`py-1.5 px-2 border rounded-lg flex flex-col items-center gap-1 cursor-pointer font-bold ${
|
| 687 |
+
paymentMethod === 'BANK_TRANSFER' ? 'bg-slate-900 text-white border-transparent shadow-sm' : 'bg-slate-50 text-slate-700 hover:bg-slate-100 border-slate-200'
|
| 688 |
+
}`}
|
| 689 |
+
>
|
| 690 |
+
<Printer className="w-3.5 h-3.5" />
|
| 691 |
+
Bank Wire
|
| 692 |
+
</button>
|
| 693 |
+
<button
|
| 694 |
+
type="button"
|
| 695 |
+
onClick={() => setPaymentMethod('MOBILE_WALLET')}
|
| 696 |
+
className={`py-1.5 px-2 border rounded-lg flex flex-col items-center gap-1 cursor-pointer font-bold ${
|
| 697 |
+
paymentMethod === 'MOBILE_WALLET' ? 'bg-slate-900 text-white border-transparent shadow-sm' : 'bg-slate-50 text-slate-700 hover:bg-slate-100 border-slate-200'
|
| 698 |
+
}`}
|
| 699 |
+
>
|
| 700 |
+
<Smartphone className="w-3.5 h-3.5" />
|
| 701 |
+
M-Wallet
|
| 702 |
+
</button>
|
| 703 |
+
</div>
|
| 704 |
+
</div>
|
| 705 |
+
|
| 706 |
+
{/* Comments block */}
|
| 707 |
+
<div className="space-y-1">
|
| 708 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Client / Warranty Notes</label>
|
| 709 |
+
<textarea
|
| 710 |
+
rows={2}
|
| 711 |
+
placeholder="e.g. 1 year structural carcass warranty, free alignment included..."
|
| 712 |
+
value={notes}
|
| 713 |
+
onChange={(e) => setNotes(e.target.value)}
|
| 714 |
+
className="w-full text-xs border border-slate-200 outline-none focus:border-slate-800 bg-white rounded-lg p-2 transition resize-none font-sans"
|
| 715 |
+
/>
|
| 716 |
+
</div>
|
| 717 |
+
|
| 718 |
+
<button
|
| 719 |
+
type="button"
|
| 720 |
+
onClick={handlePaymentSubmit}
|
| 721 |
+
disabled={cart.length === 0}
|
| 722 |
+
className={`w-full py-2.5 text-xs font-semibold rounded-lg flex items-center justify-center gap-2 cursor-pointer transition shadow-sm border ${
|
| 723 |
+
cart.length === 0
|
| 724 |
+
? 'bg-slate-100 text-slate-400 border-slate-200 cursor-not-allowed shadow-none'
|
| 725 |
+
: 'bg-slate-900 hover:bg-slate-800 border-transparent text-white'
|
| 726 |
+
}`}
|
| 727 |
+
>
|
| 728 |
+
<CheckCircle className="w-4 h-4" />
|
| 729 |
+
{paymentMethod === 'CASH' ? 'Commit & Cash Invoice' : 'Proceed to Checkout Gate'}
|
| 730 |
+
</button>
|
| 731 |
+
</div>
|
| 732 |
+
</div>
|
| 733 |
+
</div>
|
| 734 |
+
</div>
|
| 735 |
+
)}
|
| 736 |
+
|
| 737 |
+
{/* MID-GATEWAY SETTLEMENT MODAL */}
|
| 738 |
+
{generationStep === 'GATEWAY' && (
|
| 739 |
+
<div className="max-w-2xl mx-auto bg-white border border-slate-200 rounded-xl shadow-sm p-6 md:p-8 space-y-6">
|
| 740 |
+
<div className="text-center space-y-2 pb-4 border-b border-slate-100">
|
| 741 |
+
<h3 className="text-base font-bold text-slate-900 uppercase tracking-wider">Haider Brother Gateway Portal</h3>
|
| 742 |
+
<p className="text-xs text-slate-400">Securing payment gateway transaction pipeline for tire invoice processing</p>
|
| 743 |
+
</div>
|
| 744 |
+
|
| 745 |
+
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 bg-slate-50 p-4 rounded-lg border border-slate-200">
|
| 746 |
+
<div className="md:col-span-8 space-y-3">
|
| 747 |
+
<span className="bg-slate-200 text-slate-800 text-[9px] font-bold px-2 py-0.5 rounded font-mono uppercase">
|
| 748 |
+
Active Client Invoice Profile
|
| 749 |
+
</span>
|
| 750 |
+
<p className="text-xs font-bold text-slate-705">Client Account: <span className="text-slate-900 font-medium">{customerName}</span></p>
|
| 751 |
+
<p className="text-xs font-bold text-slate-705">Vehicle Mark: <span className="text-slate-900 font-medium">{customerVehicle || 'N/A'}</span></p>
|
| 752 |
+
<p className="text-base font-black text-slate-950 font-mono">
|
| 753 |
+
Amount Due: {settings.currencySymbol} {total.toLocaleString()}
|
| 754 |
+
</p>
|
| 755 |
+
</div>
|
| 756 |
+
|
| 757 |
+
<div className="md:col-span-4 flex flex-col justify-center items-center bg-white border border-slate-200 rounded-lg p-3 text-center space-y-1">
|
| 758 |
+
<span className="text-[9px] text-slate-400 uppercase tracking-widest font-black">Checkout Symbol</span>
|
| 759 |
+
<span className="text-xl font-bold font-mono text-slate-900">{paymentMethod}</span>
|
| 760 |
+
<span className="text-[9px] text-slate-500 font-semibold uppercase">Ledger Secured</span>
|
| 761 |
+
</div>
|
| 762 |
+
</div>
|
| 763 |
+
|
| 764 |
+
{/* Integration Gateway tabs based on payment method */}
|
| 765 |
+
<div className="space-y-4">
|
| 766 |
+
<div className="flex justify-center border-b border-slate-100 pb-2 gap-2 text-xs font-sans">
|
| 767 |
+
{paymentMethod === 'CARD' && (
|
| 768 |
+
<button
|
| 769 |
+
type="button"
|
| 770 |
+
onClick={() => setActiveGateway('Stripe')}
|
| 771 |
+
className={`px-3 py-1.5 text-xs font-semibold rounded-md ${activeGateway === 'Stripe' ? 'bg-slate-900 text-white' : 'bg-slate-50 text-slate-600 border border-slate-200'}`}
|
| 772 |
+
>
|
| 773 |
+
Stripe Terminal
|
| 774 |
+
</button>
|
| 775 |
+
)}
|
| 776 |
+
{paymentMethod === 'BANK_TRANSFER' && (
|
| 777 |
+
<button
|
| 778 |
+
type="button"
|
| 779 |
+
onClick={() => setActiveGateway('Bank')}
|
| 780 |
+
className="px-3 py-1.5 text-xs font-semibold rounded-md bg-slate-900 text-white"
|
| 781 |
+
>
|
| 782 |
+
Bank Direct Wire IBFT
|
| 783 |
+
</button>
|
| 784 |
+
)}
|
| 785 |
+
{paymentMethod === 'MOBILE_WALLET' && (
|
| 786 |
+
<div className="flex gap-2">
|
| 787 |
+
<button
|
| 788 |
+
type="button"
|
| 789 |
+
onClick={() => setActiveGateway('EasyPaisa')}
|
| 790 |
+
className={`px-3 py-1.5 text-xs font-semibold rounded-md ${activeGateway === 'EasyPaisa' ? 'bg-slate-900 text-white' : 'bg-slate-50 text-slate-600 border border-slate-200'}`}
|
| 791 |
+
>
|
| 792 |
+
EasyPaisa Mini QR
|
| 793 |
+
</button>
|
| 794 |
+
<button
|
| 795 |
+
type="button"
|
| 796 |
+
onClick={() => setActiveGateway('JazzCash')}
|
| 797 |
+
className={`px-3 py-1.5 text-xs font-semibold rounded-md ${activeGateway === 'JazzCash' ? 'bg-slate-900 text-white' : 'bg-slate-50 text-slate-600 border border-slate-200'}`}
|
| 798 |
+
>
|
| 799 |
+
JazzCash Wallet
|
| 800 |
+
</button>
|
| 801 |
+
</div>
|
| 802 |
+
)}
|
| 803 |
+
</div>
|
| 804 |
+
|
| 805 |
+
{/* Simulated interactive inputs */}
|
| 806 |
+
<div className="border border-slate-200 rounded-lg p-5 bg-slate-50 space-y-4 font-sans">
|
| 807 |
+
{activeGateway === 'Stripe' && (
|
| 808 |
+
<div className="space-y-3">
|
| 809 |
+
<div className="flex items-center gap-1">
|
| 810 |
+
<CreditCard className="w-4 h-4 text-slate-800" />
|
| 811 |
+
<span className="text-xs font-bold text-slate-700">Stripe Live Credit Card Fields</span>
|
| 812 |
+
</div>
|
| 813 |
+
|
| 814 |
+
<div className="space-y-2">
|
| 815 |
+
<div className="space-y-1">
|
| 816 |
+
<label className="text-[9px] text-slate-500 font-bold uppercase">Card Number</label>
|
| 817 |
+
<input
|
| 818 |
+
type="text"
|
| 819 |
+
value={ccNumber}
|
| 820 |
+
onChange={(e)=>setCcNumber(e.target.value)}
|
| 821 |
+
className="w-full text-xs font-mono border border-slate-200 bg-white p-2.5 rounded-md outline-none focus:border-slate-800"
|
| 822 |
+
/>
|
| 823 |
+
</div>
|
| 824 |
+
<div className="grid grid-cols-2 gap-3">
|
| 825 |
+
<div className="space-y-1">
|
| 826 |
+
<label className="text-[9px] text-slate-500 font-bold uppercase">Expiry (MM/YY)</label>
|
| 827 |
+
<input
|
| 828 |
+
type="text"
|
| 829 |
+
value={ccExpiry}
|
| 830 |
+
onChange={(e)=>setCcExpiry(e.target.value)}
|
| 831 |
+
className="w-full text-xs font-mono border border-slate-200 bg-white p-2.5 rounded-md outline-none focus:border-slate-800"
|
| 832 |
+
/>
|
| 833 |
+
</div>
|
| 834 |
+
<div className="space-y-1">
|
| 835 |
+
<label className="text-[9px] text-slate-500 font-bold uppercase">Security Code (CVC)</label>
|
| 836 |
+
<input
|
| 837 |
+
type="text"
|
| 838 |
+
value={ccCvc}
|
| 839 |
+
onChange={(e)=>setCcCvc(e.target.value)}
|
| 840 |
+
className="w-full text-xs font-mono border border-slate-200 bg-white p-2.5 rounded-md outline-none focus:border-slate-800"
|
| 841 |
+
/>
|
| 842 |
+
</div>
|
| 843 |
+
</div>
|
| 844 |
+
</div>
|
| 845 |
+
</div>
|
| 846 |
+
)}
|
| 847 |
+
|
| 848 |
+
{activeGateway === 'Bank' && (
|
| 849 |
+
<div className="space-y-3">
|
| 850 |
+
<div className="flex items-center gap-1.5">
|
| 851 |
+
<Printer className="w-4 h-4 text-slate-800" />
|
| 852 |
+
<span className="text-xs font-bold text-slate-700">Direct IBFT Clearing Account info</span>
|
| 853 |
+
</div>
|
| 854 |
+
<div className="p-3 bg-white border border-slate-200 rounded-md space-y-1.5 text-xs text-slate-600">
|
| 855 |
+
<div>Bank Name: <strong className="text-slate-800">Meezan Bank Limited</strong></div>
|
| 856 |
+
<div>Account Number: <strong className="text-slate-800">0123-0104882103</strong></div>
|
| 857 |
+
<div>Title of Account: <strong className="text-slate-800">Haider Brother Traders</strong></div>
|
| 858 |
+
<div>Branch Code: <strong className="text-slate-800">0210 Quetta Road</strong></div>
|
| 859 |
+
</div>
|
| 860 |
+
<p className="text-[9px] text-slate-400 leading-relaxed">
|
| 861 |
+
Verify physical receipt of funds within the Meezan bank app. Once cleared, cashier clicks authorize below to log the trace code.
|
| 862 |
+
</p>
|
| 863 |
+
</div>
|
| 864 |
+
)}
|
| 865 |
+
|
| 866 |
+
{(activeGateway === 'EasyPaisa' || activeGateway === 'JazzCash') && (
|
| 867 |
+
<div className="space-y-4 flex flex-col md:flex-row items-center gap-6">
|
| 868 |
+
{/* Digital QR generation element */}
|
| 869 |
+
<div className="w-28 h-28 bg-slate-900 border border-slate-800 rounded-lg flex flex-col items-center justify-center text-white p-2 shrink-0">
|
| 870 |
+
<div className="w-20 h-20 bg-white p-1 rounded-sm flex items-center justify-center">
|
| 871 |
+
{/* Interactive block representing mobile QR code */}
|
| 872 |
+
<div className="w-full h-full bg-slate-900 flex flex-wrap gap-0.5 p-0.5">
|
| 873 |
+
{Array.from({ length: 64 }).map((_, idx) => (
|
| 874 |
+
<div
|
| 875 |
+
key={idx}
|
| 876 |
+
className={`w-1.5 h-1.5 ${
|
| 877 |
+
(idx + 13) % 3 === 0 || (idx * 7) % 5 === 0 ? 'bg-white' : 'bg-slate-900'
|
| 878 |
+
}`}
|
| 879 |
+
/>
|
| 880 |
+
))}
|
| 881 |
+
</div>
|
| 882 |
+
</div>
|
| 883 |
+
<span className="text-[8px] font-bold tracking-wider mt-1.5 text-slate-450">SCAN MERCHANT QR</span>
|
| 884 |
+
</div>
|
| 885 |
+
|
| 886 |
+
<div className="space-y-2 text-xs flex-1">
|
| 887 |
+
<h5 className="font-bold text-slate-800">{activeGateway} Merchant Payment Node</h5>
|
| 888 |
+
<p className="text-slate-500 leading-relaxed text-[11px]">
|
| 889 |
+
QR code is synchronized with merchant checkout account details. User may scan with EasyPaisa/JazzCash app directly. Alternatively, complete instant prompt authentication.
|
| 890 |
+
</p>
|
| 891 |
+
<div className="space-y-1">
|
| 892 |
+
<label className="text-[9px] text-slate-500 font-bold uppercase">Customer Mobile Wallet Phone</label>
|
| 893 |
+
<input
|
| 894 |
+
type="text"
|
| 895 |
+
placeholder="e.g. 0345-1234567"
|
| 896 |
+
defaultValue={customerPhone || "0300-1122334"}
|
| 897 |
+
className="w-full text-xs font-mono border border-slate-200 bg-white p-2 rounded-md outline-none focus:border-slate-800"
|
| 898 |
+
/>
|
| 899 |
+
</div>
|
| 900 |
+
</div>
|
| 901 |
+
</div>
|
| 902 |
+
)}
|
| 903 |
+
|
| 904 |
+
{/* Security parameters */}
|
| 905 |
+
<div className="grid grid-cols-2 gap-3 border-t border-slate-200 pt-3">
|
| 906 |
+
<div className="space-y-1">
|
| 907 |
+
<label className="text-[9px] text-slate-500 font-bold uppercase">Assigned Transaction Ref Code</label>
|
| 908 |
+
<input
|
| 909 |
+
type="text"
|
| 910 |
+
value={gatewayAuthId}
|
| 911 |
+
onChange={(e)=>setGatewayAuthId(e.target.value)}
|
| 912 |
+
className="w-full text-xs font-mono border border-slate-200 bg-white p-1.5 rounded-md outline-none focus:border-slate-800"
|
| 913 |
+
/>
|
| 914 |
+
</div>
|
| 915 |
+
<div className="space-y-1">
|
| 916 |
+
<label className="text-[9px] text-slate-500 font-bold uppercase">Enforced Security Layer</label>
|
| 917 |
+
<div className="bg-slate-200 text-slate-700 p-2 rounded-lg font-mono text-[10px] font-bold text-center">
|
| 918 |
+
AES-SHA256 SECURED
|
| 919 |
+
</div>
|
| 920 |
+
</div>
|
| 921 |
+
</div>
|
| 922 |
+
</div>
|
| 923 |
+
</div>
|
| 924 |
+
|
| 925 |
+
{/* Checkout controls */}
|
| 926 |
+
<div className="flex gap-4">
|
| 927 |
+
<button
|
| 928 |
+
type="button"
|
| 929 |
+
disabled={gatewayProcessing}
|
| 930 |
+
onClick={() => setGenerationStep('DRAFT')}
|
| 931 |
+
className="flex-1 py-2 px-4 text-xs font-semibold bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg transition cursor-pointer"
|
| 932 |
+
>
|
| 933 |
+
Cancel Bill Draft
|
| 934 |
+
</button>
|
| 935 |
+
<button
|
| 936 |
+
type="button"
|
| 937 |
+
disabled={gatewayProcessing || gatewayCompleted}
|
| 938 |
+
onClick={handleAuthorizeGateway}
|
| 939 |
+
className={`flex-1 py-2 px-4 text-xs font-bold text-white rounded-lg col-span-2 transition cursor-pointer flex items-center justify-center gap-2 shadow-sm ${
|
| 940 |
+
gatewayCompleted ? 'bg-emerald-600' :
|
| 941 |
+
gatewayProcessing ? 'bg-slate-700 cursor-wait' : 'bg-slate-900 hover:bg-slate-850'
|
| 942 |
+
}`}
|
| 943 |
+
>
|
| 944 |
+
{gatewayProcessing ? (
|
| 945 |
+
<>
|
| 946 |
+
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
| 947 |
+
Authorizing Connection...
|
| 948 |
+
</>
|
| 949 |
+
) : gatewayCompleted ? (
|
| 950 |
+
<>
|
| 951 |
+
<Check className="w-3.5 h-3.5" />
|
| 952 |
+
Direct Clearance Secured!
|
| 953 |
+
</>
|
| 954 |
+
) : (
|
| 955 |
+
'Secure Gateway Clearance'
|
| 956 |
+
)}
|
| 957 |
+
</button>
|
| 958 |
+
</div>
|
| 959 |
+
</div>
|
| 960 |
+
)}
|
| 961 |
+
|
| 962 |
+
{/* FINAL STEP: COMPLETED INVOICE PRINTOUT CLIENT VIEW */}
|
| 963 |
+
{generationStep === 'COMPLETED' && generatedInvoice && (
|
| 964 |
+
<div className="max-w-4xl mx-auto space-y-6 animate-fade-in" id="printable-receipt-card">
|
| 965 |
+
<div className="bg-emerald-50 border border-emerald-200 text-emerald-800 p-4 rounded-xl flex items-center justify-between gap-3 text-xs font-sans">
|
| 966 |
+
<div className="flex items-center gap-2">
|
| 967 |
+
<CheckCircle className="w-5 h-5 text-emerald-600" />
|
| 968 |
+
<span>
|
| 969 |
+
<strong>Invoice generated successfully!</strong> Tire stock levels have been updated in the warehouse. Transaction logs saved in local secure buffer.
|
| 970 |
+
</span>
|
| 971 |
+
</div>
|
| 972 |
+
|
| 973 |
+
<button
|
| 974 |
+
type="button"
|
| 975 |
+
onClick={() => setGenerationStep('DRAFT')}
|
| 976 |
+
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold px-3 py-1 rounded-lg text-xs cursor-pointer transition shadow"
|
| 977 |
+
>
|
| 978 |
+
Start New Invoice
|
| 979 |
+
</button>
|
| 980 |
+
</div>
|
| 981 |
+
|
| 982 |
+
{/* Deep interactive A4 Printable receipt container */}
|
| 983 |
+
<div className="bg-white border border-slate-200 rounded-2xl shadow-lg p-6 md:p-10 text-slate-800" id="receipt-print-area">
|
| 984 |
+
{/* Header info */}
|
| 985 |
+
<div className="flex flex-col md:flex-row justify-between items-start border-b-2 border-slate-900 pb-6 gap-6">
|
| 986 |
+
<div className="space-y-1">
|
| 987 |
+
<span className="text-xs font-black bg-slate-900 text-white px-2.5 py-1 rounded font-sans leading-none uppercase">
|
| 988 |
+
Haider Brother Traders
|
| 989 |
+
</span>
|
| 990 |
+
<h2 className="text-2xl font-black text-slate-950 uppercase mt-2 tracking-tight">Tire Shop & Auto Vault</h2>
|
| 991 |
+
<div className="text-xs text-slate-500 max-w-sm space-y-0.5 font-sans">
|
| 992 |
+
<p>{settings.shopAddress}</p>
|
| 993 |
+
<p>Support phone: {settings.shopPhone}</p>
|
| 994 |
+
<p className="italic text-[10px] text-slate-400">Security registration code: NBT-VAT-99201</p>
|
| 995 |
+
</div>
|
| 996 |
+
</div>
|
| 997 |
+
|
| 998 |
+
<div className="text-start md:text-right space-y-1 font-mono text-xs text-slate-700">
|
| 999 |
+
<h4 className="text-lg font-black text-slate-900 font-sans tracking-tight">CLIENT INVOICE RECEIPT</h4>
|
| 1000 |
+
<div><strong>Receipt No:</strong> {generatedInvoice.invoiceNumber}</div>
|
| 1001 |
+
<div><strong>Draft Date:</strong> {generatedInvoice.dateTime}</div>
|
| 1002 |
+
<div><strong>Cashier:</strong> {generatedInvoice.cashierName}</div>
|
| 1003 |
+
<div><strong>Secure Back-Up:</strong> Enforced</div>
|
| 1004 |
+
</div>
|
| 1005 |
+
</div>
|
| 1006 |
+
|
| 1007 |
+
{/* Customer specs */}
|
| 1008 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 py-6 border-b border-slate-150 text-xs">
|
| 1009 |
+
<div className="space-y-1 font-sans">
|
| 1010 |
+
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-extrabold block">Billed To Client</span>
|
| 1011 |
+
<p className="text-sm font-bold text-slate-900">{generatedInvoice.customerName}</p>
|
| 1012 |
+
<p className="text-slate-500">Phone: {generatedInvoice.customerPhone || 'N/A'}</p>
|
| 1013 |
+
</div>
|
| 1014 |
+
|
| 1015 |
+
<div className="space-y-1 font-sans">
|
| 1016 |
+
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-extrabold block">Vehicle Registered Profile</span>
|
| 1017 |
+
<p className="text-sm font-bold text-slate-900">{generatedInvoice.customerVehicle || 'N/A'}</p>
|
| 1018 |
+
<p className="text-slate-500">Service: Alignment check-in done</p>
|
| 1019 |
+
</div>
|
| 1020 |
+
|
| 1021 |
+
<div className="space-y-1 font-sans">
|
| 1022 |
+
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-extrabold block">Payment Authentication</span>
|
| 1023 |
+
<p className="text-sm font-bold text-slate-900 flex items-center gap-1">
|
| 1024 |
+
<span className="bg-emerald-100 text-emerald-800 text-[10px] px-2 py-0.5 rounded uppercase">PAID</span>
|
| 1025 |
+
<span className="font-mono text-xs">{generatedInvoice.paymentMethod}</span>
|
| 1026 |
+
</p>
|
| 1027 |
+
{generatedInvoice.gatewayInfo && (
|
| 1028 |
+
<p className="text-[10px] text-slate-500 font-mono">
|
| 1029 |
+
ID: {generatedInvoice.gatewayInfo.transactionId} ({generatedInvoice.gatewayInfo.gatewayName})
|
| 1030 |
+
</p>
|
| 1031 |
+
)}
|
| 1032 |
+
</div>
|
| 1033 |
+
</div>
|
| 1034 |
+
|
| 1035 |
+
{/* Line items table */}
|
| 1036 |
+
<div className="py-6 overflow-x-auto">
|
| 1037 |
+
<table className="w-full text-left border-collapse font-sans text-xs">
|
| 1038 |
+
<thead>
|
| 1039 |
+
<tr className="border-b border-slate-300">
|
| 1040 |
+
<th className="py-3 font-semibold text-slate-600 block">Tire Specifications & Profile Model</th>
|
| 1041 |
+
<th className="py-3 font-semibold text-slate-600 text-center">Quantity</th>
|
| 1042 |
+
<th className="py-3 font-semibold text-slate-600 text-right">Unit Rate</th>
|
| 1043 |
+
<th className="py-3 font-semibold text-slate-600 text-right">Net Price</th>
|
| 1044 |
+
</tr>
|
| 1045 |
+
</thead>
|
| 1046 |
+
<tbody className="divide-y divide-slate-100">
|
| 1047 |
+
{generatedInvoice.items.map((item) => (
|
| 1048 |
+
<tr key={item.id} className="align-middle">
|
| 1049 |
+
<td className="py-4">
|
| 1050 |
+
<div className="font-bold text-slate-900">{item.brand} {item.model}</div>
|
| 1051 |
+
<div className="text-[10px] text-slate-400 font-mono">{item.size} — Premium radial rubber compound</div>
|
| 1052 |
+
</td>
|
| 1053 |
+
<td className="py-4 text-center font-bold font-mono">{item.quantity} pcs</td>
|
| 1054 |
+
<td className="py-4 text-right font-mono">{generatedInvoice.currencySymbol} {item.unitPrice.toLocaleString()}</td>
|
| 1055 |
+
<td className="py-4 text-right font-bold font-mono">{generatedInvoice.currencySymbol} {item.totalPrice.toLocaleString()}</td>
|
| 1056 |
+
</tr>
|
| 1057 |
+
))}
|
| 1058 |
+
</tbody>
|
| 1059 |
+
</table>
|
| 1060 |
+
</div>
|
| 1061 |
+
|
| 1062 |
+
{/* Summary Block */}
|
| 1063 |
+
<div className="flex flex-col md:flex-row justify-between items-start border-t border-slate-200 pt-6 gap-6 text-xs font-sans">
|
| 1064 |
+
<div className="max-w-sm space-y-1">
|
| 1065 |
+
<span className="font-bold text-slate-700">Client / Warranty Note:</span>
|
| 1066 |
+
<p className="text-slate-500 text-xs italic leading-relaxed">
|
| 1067 |
+
{generatedInvoice.notes || "All core tires sold include standard road hazard manufacturing coverage. Verify wear markers regularly. Balancing checked and calibrated."}
|
| 1068 |
+
</p>
|
| 1069 |
+
</div>
|
| 1070 |
+
|
| 1071 |
+
{/* Mathematics table */}
|
| 1072 |
+
<div className="w-full md:w-80 space-y-2 border-slate-150">
|
| 1073 |
+
<div className="flex justify-between text-slate-500">
|
| 1074 |
+
<span>Cart Subtotal:</span>
|
| 1075 |
+
<span className="font-mono">{generatedInvoice.currencySymbol} {generatedInvoice.subtotal.toLocaleString()}</span>
|
| 1076 |
+
</div>
|
| 1077 |
+
|
| 1078 |
+
<div className="flex justify-between text-slate-500">
|
| 1079 |
+
<span>{settings.taxName} ({generatedInvoice.taxRate}%):</span>
|
| 1080 |
+
<span className="font-mono">{generatedInvoice.currencySymbol} {generatedInvoice.taxAmount.toLocaleString()}</span>
|
| 1081 |
+
</div>
|
| 1082 |
+
|
| 1083 |
+
<div className="flex justify-between text-slate-500">
|
| 1084 |
+
<span>Authorized Discount:</span>
|
| 1085 |
+
<span className="font-mono text-red-600">-{generatedInvoice.currencySymbol} {generatedInvoice.discount.toLocaleString()}</span>
|
| 1086 |
+
</div>
|
| 1087 |
+
|
| 1088 |
+
<div className="flex justify-between items-center border-t-2 border-slate-900 pt-2 font-bold text-slate-900">
|
| 1089 |
+
<span className="text-sm">Paid In Full :</span>
|
| 1090 |
+
<span className="text-base font-black font-mono">
|
| 1091 |
+
{generatedInvoice.currencySymbol} {generatedInvoice.total.toLocaleString()}
|
| 1092 |
+
</span>
|
| 1093 |
+
</div>
|
| 1094 |
+
</div>
|
| 1095 |
+
</div>
|
| 1096 |
+
|
| 1097 |
+
{/* Print Friendly footer */}
|
| 1098 |
+
<div className="border-t border-dashed border-slate-200 mt-10 pt-6 text-center text-[10px] text-slate-400 font-sans space-y-1">
|
| 1099 |
+
<p>System authenticated and encrypted under primary warehouse inventory block.</p>
|
| 1100 |
+
<p>Thank you for shopping at Haider Brother Traders! Have a safe journey!</p>
|
| 1101 |
+
</div>
|
| 1102 |
+
</div>
|
| 1103 |
+
|
| 1104 |
+
{/* Action bars */}
|
| 1105 |
+
<div className="flex flex-col sm:flex-row justify-center items-center gap-3">
|
| 1106 |
+
<button
|
| 1107 |
+
onClick={handleManualPrint}
|
| 1108 |
+
className="w-full sm:w-auto bg-slate-900 hover:bg-slate-800 text-white font-bold px-6 py-3.5 rounded-xl flex items-center justify-center gap-2 cursor-pointer transition shadow"
|
| 1109 |
+
>
|
| 1110 |
+
<Printer className="w-4 h-4" />
|
| 1111 |
+
Direct Receipt Print / PDF Export
|
| 1112 |
+
</button>
|
| 1113 |
+
<button
|
| 1114 |
+
onClick={() => exportOfflineHTMLReceipt(generatedInvoice)}
|
| 1115 |
+
className="w-full sm:w-auto bg-blue-600 hover:bg-blue-500 text-white font-bold px-6 py-3.5 rounded-xl flex items-center justify-center gap-2 cursor-pointer transition shadow"
|
| 1116 |
+
>
|
| 1117 |
+
<FileDown className="w-4 h-4" />
|
| 1118 |
+
Download Offline-Ready HTML Bill
|
| 1119 |
+
</button>
|
| 1120 |
+
<button
|
| 1121 |
+
onClick={() => copyShareText(generatedInvoice)}
|
| 1122 |
+
className="w-full sm:w-auto bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold px-6 py-3.5 rounded-xl flex items-center justify-center gap-2 cursor-pointer transition border border-slate-200"
|
| 1123 |
+
>
|
| 1124 |
+
<Share2 className="w-4 h-4" />
|
| 1125 |
+
Copy Social Share Draft
|
| 1126 |
+
</button>
|
| 1127 |
+
</div>
|
| 1128 |
+
</div>
|
| 1129 |
+
)}
|
| 1130 |
+
|
| 1131 |
+
{/* HISTORICAL INVOICES BROWSER - VISIBLE IF USER REVIEWS PREVIOUS INVOICE */}
|
| 1132 |
+
{selectedInvoiceForView && (
|
| 1133 |
+
<div className="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 flex items-center justify-center p-4 overflow-y-auto">
|
| 1134 |
+
<div className="bg-white rounded-2xl max-w-4xl w-full p-6 md:p-8 space-y-6 max-h-[90vh] overflow-y-auto relative shadow-2xl">
|
| 1135 |
+
<button
|
| 1136 |
+
onClick={() => setSelectedInvoiceForView(null)}
|
| 1137 |
+
className="absolute top-4 right-4 bg-slate-100 hover:bg-slate-200 text-slate-600 hover:text-slate-900 font-bold px-3 py-1.5 rounded-lg text-xs cursor-pointer"
|
| 1138 |
+
>
|
| 1139 |
+
Close Ledger View
|
| 1140 |
+
</button>
|
| 1141 |
+
|
| 1142 |
+
{/* Content Mirror of completed code */}
|
| 1143 |
+
<div className="bg-white p-2">
|
| 1144 |
+
{/* Header info */}
|
| 1145 |
+
<div className="flex flex-col md:flex-row justify-between items-start border-b-2 border-slate-900 pb-6 gap-6">
|
| 1146 |
+
<div>
|
| 1147 |
+
<span className="text-xs font-bold bg-slate-900 text-white px-2 py-1 rounded">
|
| 1148 |
+
Haider Brother Traders
|
| 1149 |
+
</span>
|
| 1150 |
+
<h3 className="text-xl font-bold uppercase mt-2 text-slate-950">Tire Shop & Auto Vault</h3>
|
| 1151 |
+
<div className="text-[11px] text-slate-500 mt-1">
|
| 1152 |
+
<p>{settings.shopAddress}</p>
|
| 1153 |
+
<p>Phone: {settings.shopPhone}</p>
|
| 1154 |
+
</div>
|
| 1155 |
+
</div>
|
| 1156 |
+
|
| 1157 |
+
<div className="text-left md:text-right text-xs font-mono">
|
| 1158 |
+
<h4 className="text-sm font-bold font-sans">DUPLICATE CUSTOMER RECEIPT</h4>
|
| 1159 |
+
<div><strong>Invoice No:</strong> {selectedInvoiceForView.invoiceNumber}</div>
|
| 1160 |
+
<div><strong>Date Filed:</strong> {selectedInvoiceForView.dateTime}</div>
|
| 1161 |
+
<div><strong>Compiled By:</strong> {selectedInvoiceForView.cashierName}</div>
|
| 1162 |
+
</div>
|
| 1163 |
+
</div>
|
| 1164 |
+
|
| 1165 |
+
{/* Specs */}
|
| 1166 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 py-4 border-b border-slate-150 text-xs">
|
| 1167 |
+
<div>
|
| 1168 |
+
<span className="text-[10px] text-slate-400 uppercase font-bold block">Account Client</span>
|
| 1169 |
+
<p className="font-bold text-slate-900">{selectedInvoiceForView.customerName}</p>
|
| 1170 |
+
<p className="text-slate-500">{selectedInvoiceForView.customerPhone || 'N/A'}</p>
|
| 1171 |
+
</div>
|
| 1172 |
+
|
| 1173 |
+
<div>
|
| 1174 |
+
<span className="text-[10px] text-slate-400 uppercase font-bold block">Vehicle Details</span>
|
| 1175 |
+
<p className="font-bold text-slate-900">{selectedInvoiceForView.customerVehicle || 'N/A'}</p>
|
| 1176 |
+
<p className="text-slate-500">Service: Tires Replacement</p>
|
| 1177 |
+
</div>
|
| 1178 |
+
|
| 1179 |
+
<div>
|
| 1180 |
+
<span className="text-[10px] text-slate-400 uppercase font-bold block">Receipt Token</span>
|
| 1181 |
+
<p className="font-bold text-slate-900 flex items-center gap-1">
|
| 1182 |
+
<span className="bg-emerald-100 text-emerald-800 text-[10px] px-1.5 py-0.5 rounded">PAID</span>
|
| 1183 |
+
<span className="font-mono text-xs">{selectedInvoiceForView.paymentMethod}</span>
|
| 1184 |
+
</p>
|
| 1185 |
+
{selectedInvoiceForView.gatewayInfo && (
|
| 1186 |
+
<p className="text-[10px] text-slate-500 font-mono">
|
| 1187 |
+
Ref: {selectedInvoiceForView.gatewayInfo.transactionId}
|
| 1188 |
+
</p>
|
| 1189 |
+
)}
|
| 1190 |
+
</div>
|
| 1191 |
+
</div>
|
| 1192 |
+
|
| 1193 |
+
{/* Items List */}
|
| 1194 |
+
<div className="py-4">
|
| 1195 |
+
<table className="w-full text-left text-xs font-sans">
|
| 1196 |
+
<thead>
|
| 1197 |
+
<tr className="border-b border-slate-200">
|
| 1198 |
+
<th className="py-2 text-slate-500 font-bold block">Tire Brand & Size Specs</th>
|
| 1199 |
+
<th className="py-2 text-slate-500 font-bold text-center">Qty</th>
|
| 1200 |
+
<th className="py-2 text-slate-500 font-bold text-right">Rate</th>
|
| 1201 |
+
<th className="py-2 text-slate-500 font-bold text-right">Amount</th>
|
| 1202 |
+
</tr>
|
| 1203 |
+
</thead>
|
| 1204 |
+
<tbody className="divide-y divide-slate-100">
|
| 1205 |
+
{selectedInvoiceForView.items.map((item) => (
|
| 1206 |
+
<tr key={item.id}>
|
| 1207 |
+
<td className="py-3">
|
| 1208 |
+
<div className="font-semibold text-slate-900">{item.brand} {item.model}</div>
|
| 1209 |
+
<div className="text-[10px] text-slate-400 font-mono">{item.size}</div>
|
| 1210 |
+
</td>
|
| 1211 |
+
<td className="py-3 text-center font-bold font-mono">{item.quantity}</td>
|
| 1212 |
+
<td className="py-3 text-right font-mono">{selectedInvoiceForView.currencySymbol}{item.unitPrice.toLocaleString()}</td>
|
| 1213 |
+
<td className="py-3 text-right font-bold font-mono">{selectedInvoiceForView.currencySymbol}{item.totalPrice.toLocaleString()}</td>
|
| 1214 |
+
</tr>
|
| 1215 |
+
))}
|
| 1216 |
+
</tbody>
|
| 1217 |
+
</table>
|
| 1218 |
+
</div>
|
| 1219 |
+
|
| 1220 |
+
{/* Total calculations */}
|
| 1221 |
+
<div className="flex flex-col sm:flex-row justify-between items-start border-t border-slate-200 pt-4 gap-4 text-xs font-sans">
|
| 1222 |
+
<div className="max-w-xs text-slate-400 italic text-[10px]">
|
| 1223 |
+
Notes: {selectedInvoiceForView.notes || "All tires sold are certified under factory DOT guidelines. Alignment calibrated."}
|
| 1224 |
+
</div>
|
| 1225 |
+
|
| 1226 |
+
<div className="w-full sm:w-64 space-y-1.5 text-right ml-auto">
|
| 1227 |
+
<div className="flex justify-between text-slate-500">
|
| 1228 |
+
<span>Subtotal:</span>
|
| 1229 |
+
<span>{selectedInvoiceForView.currencySymbol} {selectedInvoiceForView.subtotal.toLocaleString()}</span>
|
| 1230 |
+
</div>
|
| 1231 |
+
<div className="flex justify-between text-slate-500">
|
| 1232 |
+
<span>{settings.taxName} ({selectedInvoiceForView.taxRate}%):</span>
|
| 1233 |
+
<span>{selectedInvoiceForView.currencySymbol} {selectedInvoiceForView.taxAmount.toLocaleString()}</span>
|
| 1234 |
+
</div>
|
| 1235 |
+
<div className="flex justify-between text-slate-500">
|
| 1236 |
+
<span>Discount applied:</span>
|
| 1237 |
+
<span className="text-red-600">-{selectedInvoiceForView.currencySymbol} {selectedInvoiceForView.discount.toLocaleString()}</span>
|
| 1238 |
+
</div>
|
| 1239 |
+
<div className="flex justify-between items-center border-t border-slate-900 pt-1.5 font-bold text-slate-900">
|
| 1240 |
+
<span>Paid Total:</span>
|
| 1241 |
+
<span className="text-sm font-black font-mono">
|
| 1242 |
+
{selectedInvoiceForView.currencySymbol} {selectedInvoiceForView.total.toLocaleString()}
|
| 1243 |
+
</span>
|
| 1244 |
+
</div>
|
| 1245 |
+
</div>
|
| 1246 |
+
</div>
|
| 1247 |
+
</div>
|
| 1248 |
+
|
| 1249 |
+
{/* Floating duplicate panel actions */}
|
| 1250 |
+
<div className="flex gap-3 justify-center pt-2">
|
| 1251 |
+
<button
|
| 1252 |
+
onClick={handleManualPrint}
|
| 1253 |
+
className="bg-slate-900 hover:bg-slate-800 text-white text-xs font-bold px-4 py-2 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer"
|
| 1254 |
+
>
|
| 1255 |
+
<Printer className="w-3.5 h-3.5" />
|
| 1256 |
+
Print Duplicate
|
| 1257 |
+
</button>
|
| 1258 |
+
<button
|
| 1259 |
+
onClick={() => exportOfflineHTMLReceipt(selectedInvoiceForView)}
|
| 1260 |
+
className="bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold px-4 py-2 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer"
|
| 1261 |
+
>
|
| 1262 |
+
<Download className="w-3.5 h-3.5" />
|
| 1263 |
+
Save File Block
|
| 1264 |
+
</button>
|
| 1265 |
+
<button
|
| 1266 |
+
onClick={() => copyShareText(selectedInvoiceForView)}
|
| 1267 |
+
className="bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-bold px-4 py-2 rounded-xl border border-slate-200"
|
| 1268 |
+
>
|
| 1269 |
+
Copy text share
|
| 1270 |
+
</button>
|
| 1271 |
+
</div>
|
| 1272 |
+
</div>
|
| 1273 |
+
</div>
|
| 1274 |
+
)}
|
| 1275 |
+
</div>
|
| 1276 |
+
);
|
| 1277 |
+
}
|
src/components/Settings.tsx
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @license
|
| 3 |
+
* SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import React, { useState } from 'react';
|
| 7 |
+
import { motion } from 'motion/react';
|
| 8 |
+
import {
|
| 9 |
+
Settings,
|
| 10 |
+
Database,
|
| 11 |
+
Cloud,
|
| 12 |
+
Upload,
|
| 13 |
+
Download,
|
| 14 |
+
Check,
|
| 15 |
+
AlertCircle,
|
| 16 |
+
Lock,
|
| 17 |
+
Wifi,
|
| 18 |
+
WifiOff,
|
| 19 |
+
RefreshCw,
|
| 20 |
+
Coins,
|
| 21 |
+
Receipt,
|
| 22 |
+
RotateCcw
|
| 23 |
+
} from 'lucide-react';
|
| 24 |
+
import { SystemSettings } from '../types';
|
| 25 |
+
import { encryptData, decryptData } from '../utils/backup';
|
| 26 |
+
|
| 27 |
+
interface SettingsProps {
|
| 28 |
+
settings: SystemSettings;
|
| 29 |
+
isOnline: boolean;
|
| 30 |
+
onUpdateSettings: (settings: SystemSettings) => void;
|
| 31 |
+
onToggleOnlineMode: () => void;
|
| 32 |
+
onRestoreDatabase: (restoredData: {
|
| 33 |
+
settings: SystemSettings;
|
| 34 |
+
products: any[];
|
| 35 |
+
invoices: any[];
|
| 36 |
+
history: any[];
|
| 37 |
+
staff: any[];
|
| 38 |
+
}) => void;
|
| 39 |
+
onClearCacheToDefault: () => void;
|
| 40 |
+
// Passing actual values for backup compilation
|
| 41 |
+
activeProducts: any[];
|
| 42 |
+
activeInvoices: any[];
|
| 43 |
+
activeHistory: any[];
|
| 44 |
+
activeStaff: any[];
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export default function SettingsComponent({
|
| 48 |
+
settings,
|
| 49 |
+
isOnline,
|
| 50 |
+
onUpdateSettings,
|
| 51 |
+
onToggleOnlineMode,
|
| 52 |
+
onRestoreDatabase,
|
| 53 |
+
onClearCacheToDefault,
|
| 54 |
+
activeProducts,
|
| 55 |
+
activeInvoices,
|
| 56 |
+
activeHistory,
|
| 57 |
+
activeStaff
|
| 58 |
+
}: SettingsProps) {
|
| 59 |
+
// Input states
|
| 60 |
+
const [shopName, setShopName] = useState(settings.shopName);
|
| 61 |
+
const [shopAddress, setShopAddress] = useState(settings.shopAddress);
|
| 62 |
+
const [shopPhone, setShopPhone] = useState(settings.shopPhone);
|
| 63 |
+
const [taxName, setTaxName] = useState(settings.taxName);
|
| 64 |
+
const [taxRate, setTaxRate] = useState(settings.taxRate);
|
| 65 |
+
const [currencyCode, setCurrencyCode] = useState(settings.currencyCode);
|
| 66 |
+
const [currencySymbol, setCurrencySymbol] = useState(settings.currencySymbol);
|
| 67 |
+
|
| 68 |
+
// Backup vault password block
|
| 69 |
+
const [vaultPassword, setVaultPassword] = useState('haider_secret_2026');
|
| 70 |
+
const [uploadedCipherString, setUploadedCipherString] = useState('');
|
| 71 |
+
const [backupLogs, setBackupLogs] = useState<string[]>(["Secure Vault active. Ready for ledger encryption."]);
|
| 72 |
+
const [syncingCloud, setSyncingCloud] = useState(false);
|
| 73 |
+
|
| 74 |
+
const handleSaveSettingsSubmit = (e: React.FormEvent) => {
|
| 75 |
+
e.preventDefault();
|
| 76 |
+
if (!shopName.trim() || !shopAddress.trim()) {
|
| 77 |
+
alert("Corporate name and address parameters are required.");
|
| 78 |
+
return;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
onUpdateSettings({
|
| 82 |
+
shopName: shopName.trim(),
|
| 83 |
+
shopAddress: shopAddress.trim(),
|
| 84 |
+
shopPhone: shopPhone.trim(),
|
| 85 |
+
taxName: taxName.trim(),
|
| 86 |
+
taxRate: Math.max(0, parseFloat(taxRate.toString()) || 0),
|
| 87 |
+
currencyCode: currencyCode.trim(),
|
| 88 |
+
currencySymbol: currencySymbol.trim()
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
alert("Corporate shop parameters synced across live terminal databases.");
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
// Compile full database and export as locally encrypted block file (.hbtback)
|
| 95 |
+
const compileAndDownloadBackup = () => {
|
| 96 |
+
try {
|
| 97 |
+
const fullPackage = {
|
| 98 |
+
meta: {
|
| 99 |
+
timestamp: new Date().toISOString(),
|
| 100 |
+
version: "HBT-VAULT-2.0.0",
|
| 101 |
+
author: "Haider Brother Traders"
|
| 102 |
+
},
|
| 103 |
+
settings,
|
| 104 |
+
products: activeProducts,
|
| 105 |
+
invoices: activeInvoices,
|
| 106 |
+
history: activeHistory,
|
| 107 |
+
staff: activeStaff
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
const plainText = JSON.stringify(fullPackage);
|
| 111 |
+
const encrypted = encryptData(plainText, vaultPassword);
|
| 112 |
+
|
| 113 |
+
// Create download elements
|
| 114 |
+
const blob = new Blob([encrypted], { type: 'text/plain;charset=utf-8' });
|
| 115 |
+
const link = document.createElement('a');
|
| 116 |
+
link.href = URL.createObjectURL(blob);
|
| 117 |
+
const dateStr = new Date().toISOString().split('T')[0];
|
| 118 |
+
link.download = `HBT_Vault_Backup_${dateStr}.hbtback`;
|
| 119 |
+
link.click();
|
| 120 |
+
|
| 121 |
+
setBackupLogs(prev => [
|
| 122 |
+
`[${new Date().toLocaleTimeString()}] Secure Backup compiled: Encrypted ${activeInvoices.length} transactions and ${activeProducts.length} tire SKUs.`,
|
| 123 |
+
...prev
|
| 124 |
+
]);
|
| 125 |
+
alert("Ledger Vault encrypted file downloaded to client machine successfully!");
|
| 126 |
+
|
| 127 |
+
} catch (err: any) {
|
| 128 |
+
alert("Encryption failure: " + err.message);
|
| 129 |
+
}
|
| 130 |
+
};
|
| 131 |
+
|
| 132 |
+
// Read decrypted cipher files and trigger restore
|
| 133 |
+
const handleUploadAndRestore = (fileContent: string) => {
|
| 134 |
+
if (!fileContent.trim()) {
|
| 135 |
+
alert("File is empty. Select a valid .hbtback backup.");
|
| 136 |
+
return;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
try {
|
| 140 |
+
const plainText = decryptData(fileContent, vaultPassword);
|
| 141 |
+
const decoded = JSON.parse(plainText);
|
| 142 |
+
|
| 143 |
+
// Simple schema structure audit
|
| 144 |
+
if (!decoded.settings || !decoded.products || !decoded.invoices || !decoded.history || !decoded.staff) {
|
| 145 |
+
throw new Error("Target file has correct password but missing necessary entity logs.");
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
onRestoreDatabase({
|
| 149 |
+
settings: decoded.settings,
|
| 150 |
+
products: decoded.products,
|
| 151 |
+
invoices: decoded.invoices,
|
| 152 |
+
history: decoded.history,
|
| 153 |
+
staff: decoded.staff
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
// Update local values instantly
|
| 157 |
+
setShopName(decoded.settings.shopName);
|
| 158 |
+
setShopAddress(decoded.settings.shopAddress);
|
| 159 |
+
setShopPhone(decoded.settings.shopPhone);
|
| 160 |
+
setTaxName(decoded.settings.taxName);
|
| 161 |
+
setTaxRate(decoded.settings.taxRate);
|
| 162 |
+
setCurrencyCode(decoded.settings.currencyCode);
|
| 163 |
+
setCurrencySymbol(decoded.settings.currencySymbol);
|
| 164 |
+
|
| 165 |
+
setBackupLogs(prev => [
|
| 166 |
+
`[${new Date().toLocaleTimeString()}] Restore complete: Imported ${decoded.invoices.length} invoices, ${decoded.products.length} tires and verified cashier registers.`,
|
| 167 |
+
...prev
|
| 168 |
+
]);
|
| 169 |
+
alert("Encrypted vault records imported and restored successfully!");
|
| 170 |
+
} catch (err: any) {
|
| 171 |
+
alert("Restoration Blocked! Error: " + err.message);
|
| 172 |
+
}
|
| 173 |
+
};
|
| 174 |
+
|
| 175 |
+
// Drag and drop file reader
|
| 176 |
+
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 177 |
+
const file = e.target.files?.[0];
|
| 178 |
+
if (!file) return;
|
| 179 |
+
|
| 180 |
+
const reader = new FileReader();
|
| 181 |
+
reader.onload = (event) => {
|
| 182 |
+
const txt = event.target?.result as string;
|
| 183 |
+
handleUploadAndRestore(txt);
|
| 184 |
+
};
|
| 185 |
+
reader.readAsText(file);
|
| 186 |
+
};
|
| 187 |
+
|
| 188 |
+
// Cloud Backups sync mock log
|
| 189 |
+
const handleCloudSyncTrigger = () => {
|
| 190 |
+
setSyncingCloud(true);
|
| 191 |
+
setBackupLogs(prev => [`[${new Date().toLocaleTimeString()}] Securing cloud payload connection to Google Cloud Run servers...`, ...prev]);
|
| 192 |
+
|
| 193 |
+
setTimeout(() => {
|
| 194 |
+
setSyncingCloud(false);
|
| 195 |
+
setBackupLogs(prev => [
|
| 196 |
+
`[${new Date().toLocaleTimeString()}] Cloud backup synced! Encrypted cache saved under vault block. Transaction status: EXCELLENT.`,
|
| 197 |
+
...prev
|
| 198 |
+
]);
|
| 199 |
+
alert("Cloud Backup Synced! All sensitive business records securely logged to secondary vault storage.");
|
| 200 |
+
}, 1500);
|
| 201 |
+
};
|
| 202 |
+
|
| 203 |
+
const handleResetConfirm = () => {
|
| 204 |
+
if (confirm("Are you absolutely sure you want to restore the entire tire shop back to default factory parameters? This deletes custom added items, current stock manual adjustments, and any newly compiled invoice draft data.")) {
|
| 205 |
+
onClearCacheToDefault();
|
| 206 |
+
window.location.reload();
|
| 207 |
+
}
|
| 208 |
+
};
|
| 209 |
+
|
| 210 |
+
// Quick Currency Preset selection
|
| 211 |
+
const selectCurrencyPreset = (code: string, symbol: string) => {
|
| 212 |
+
setCurrencyCode(code);
|
| 213 |
+
setCurrencySymbol(symbol);
|
| 214 |
+
};
|
| 215 |
+
|
| 216 |
+
return (
|
| 217 |
+
<div className="space-y-6" id="terminal-settings-layout">
|
| 218 |
+
{/* Splits layout settings versus encrypted backups */}
|
| 219 |
+
<div className="grid grid-cols-1 xl:grid-cols-12 gap-6">
|
| 220 |
+
{/* LEFT COLUMN: Corporate & System settings */}
|
| 221 |
+
<div className="xl:col-span-7 bg-white border border-slate-200 rounded-xl p-5 shadow-sm space-y-6">
|
| 222 |
+
<div className="flex items-center gap-2 border-b border-slate-100 pb-4 uppercase">
|
| 223 |
+
<Settings className="w-5 h-5 text-slate-700 animate-spin" style={{ animationDuration: '6s' }} />
|
| 224 |
+
<span className="text-xs font-bold text-slate-900">Corporate Terminal Configurations</span>
|
| 225 |
+
</div>
|
| 226 |
+
|
| 227 |
+
<form onSubmit={handleSaveSettingsSubmit} className="space-y-4 text-xs font-sans">
|
| 228 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
| 229 |
+
<div className="space-y-1">
|
| 230 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Corporate Shop Name</label>
|
| 231 |
+
<input
|
| 232 |
+
type="text"
|
| 233 |
+
value={shopName}
|
| 234 |
+
onChange={(e)=>setShopName(e.target.value)}
|
| 235 |
+
className="w-full border border-slate-200 bg-white outline-none focus:border-slate-800 rounded-lg p-2.5 font-semibold transition"
|
| 236 |
+
/>
|
| 237 |
+
</div>
|
| 238 |
+
|
| 239 |
+
<div className="space-y-1">
|
| 240 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Shop Phone Helpline</label>
|
| 241 |
+
<input
|
| 242 |
+
type="text"
|
| 243 |
+
value={shopPhone}
|
| 244 |
+
onChange={(e)=>setShopPhone(e.target.value)}
|
| 245 |
+
className="w-full border border-slate-200 bg-white outline-none focus:border-slate-800 rounded-lg p-2.5 font-semibold transition"
|
| 246 |
+
/>
|
| 247 |
+
</div>
|
| 248 |
+
</div>
|
| 249 |
+
|
| 250 |
+
<div className="space-y-1">
|
| 251 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Physical Address Coordinates</label>
|
| 252 |
+
<input
|
| 253 |
+
type="text"
|
| 254 |
+
value={shopAddress}
|
| 255 |
+
onChange={(e)=>setShopAddress(e.target.value)}
|
| 256 |
+
className="w-full border border-slate-200 bg-white outline-none focus:border-slate-800 rounded-lg p-2.5 font-semibold transition"
|
| 257 |
+
/>
|
| 258 |
+
</div>
|
| 259 |
+
|
| 260 |
+
{/* CURRENCY INTEGRATION CHANGER */}
|
| 261 |
+
<div className="bg-slate-50 border border-slate-200 p-4 rounded-lg space-y-3">
|
| 262 |
+
<div className="flex items-center justify-between">
|
| 263 |
+
<span className="text-xs font-bold text-slate-850 flex items-center gap-1">
|
| 264 |
+
<Coins className="w-4 h-4 text-slate-700" />
|
| 265 |
+
Local Currency Integration
|
| 266 |
+
</span>
|
| 267 |
+
<span className="text-[10px] bg-slate-950 text-white font-mono font-medium px-2 py-0.5 rounded-md">
|
| 268 |
+
Active Preset: {currencyCode} ({currencySymbol})
|
| 269 |
+
</span>
|
| 270 |
+
</div>
|
| 271 |
+
|
| 272 |
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
| 273 |
+
<div className="space-y-1">
|
| 274 |
+
<label className="text-[9px] text-slate-400 font-bold uppercase">Currency Symbol</label>
|
| 275 |
+
<input
|
| 276 |
+
type="text"
|
| 277 |
+
value={currencySymbol}
|
| 278 |
+
placeholder="e.g. ₨"
|
| 279 |
+
onChange={(e)=>setCurrencySymbol(e.target.value)}
|
| 280 |
+
className="w-full border border-slate-200 bg-white outline-none focus:border-slate-800 rounded-lg p-2 font-mono text-center font-bold"
|
| 281 |
+
/>
|
| 282 |
+
</div>
|
| 283 |
+
|
| 284 |
+
<div className="space-y-1">
|
| 285 |
+
<label className="text-[9px] text-slate-400 font-bold uppercase">Currency Code</label>
|
| 286 |
+
<input
|
| 287 |
+
type="text"
|
| 288 |
+
value={currencyCode}
|
| 289 |
+
placeholder="e.g. PKR"
|
| 290 |
+
onChange={(e)=>setCurrencyCode(e.target.value)}
|
| 291 |
+
className="w-full border border-slate-200 bg-white outline-none focus:border-slate-800 rounded-lg p-2 font-mono text-center font-bold"
|
| 292 |
+
/>
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
{/* Instant Presets */}
|
| 296 |
+
<div className="space-y-1 flex flex-col justify-end">
|
| 297 |
+
<label className="text-[9px] text-slate-400 font-bold uppercase leading-none block mb-1">Fast Symbol Presets</label>
|
| 298 |
+
<div className="flex gap-1 justify-between font-mono font-semibold">
|
| 299 |
+
<button
|
| 300 |
+
type="button"
|
| 301 |
+
onClick={() => selectCurrencyPreset('PKR', '₨')}
|
| 302 |
+
className="px-2 py-1.5 border border-slate-200 bg-white hover:bg-slate-50 text-[10px] rounded-md flex-1 cursor-pointer"
|
| 303 |
+
>
|
| 304 |
+
₨ PKR
|
| 305 |
+
</button>
|
| 306 |
+
<button
|
| 307 |
+
type="button"
|
| 308 |
+
onClick={() => selectCurrencyPreset('USD', '$')}
|
| 309 |
+
className="px-2 py-1.5 border border-slate-200 bg-white hover:bg-slate-50 text-[10px] rounded-md flex-1 cursor-pointer"
|
| 310 |
+
>
|
| 311 |
+
$ USD
|
| 312 |
+
</button>
|
| 313 |
+
<button
|
| 314 |
+
type="button"
|
| 315 |
+
onClick={() => selectCurrencyPreset('AED', 'د.إ')}
|
| 316 |
+
className="px-2 py-1.5 border border-slate-200 bg-white hover:bg-slate-50 text-[10px] rounded-md flex-1 cursor-pointer"
|
| 317 |
+
>
|
| 318 |
+
د.إ AED
|
| 319 |
+
</button>
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
</div>
|
| 324 |
+
|
| 325 |
+
{/* TAX COMPILATION STRUCTURE */}
|
| 326 |
+
<div className="bg-slate-50 border border-slate-200 p-4 rounded-lg space-y-3">
|
| 327 |
+
<div className="flex items-center gap-1.5 text-xs font-bold text-slate-850">
|
| 328 |
+
<Receipt className="w-4 h-4 text-slate-700 font-medium" />
|
| 329 |
+
Automated Tax Calculation Engine
|
| 330 |
+
</div>
|
| 331 |
+
|
| 332 |
+
<div className="grid grid-cols-2 gap-4">
|
| 333 |
+
<div className="space-y-1">
|
| 334 |
+
<label className="text-[9px] text-slate-400 font-bold uppercase">Tax Designation Name</label>
|
| 335 |
+
<input
|
| 336 |
+
type="text"
|
| 337 |
+
value={taxName}
|
| 338 |
+
placeholder="e.g. Sales Tax (SRB)"
|
| 339 |
+
onChange={(e)=>setTaxName(e.target.value)}
|
| 340 |
+
className="w-full border border-slate-200 bg-white outline-none focus:border-slate-800 rounded-lg p-2"
|
| 341 |
+
/>
|
| 342 |
+
</div>
|
| 343 |
+
|
| 344 |
+
<div className="space-y-1">
|
| 345 |
+
<label className="text-[9px] text-slate-400 font-bold uppercase">Combined Tax Rate (%)</label>
|
| 346 |
+
<input
|
| 347 |
+
type="number"
|
| 348 |
+
step="0.1"
|
| 349 |
+
min="0"
|
| 350 |
+
max="90"
|
| 351 |
+
value={taxRate}
|
| 352 |
+
onChange={(e)=>setTaxRate(Math.min(90, Math.max(0, parseFloat(e.target.value) || 0)))}
|
| 353 |
+
className="w-full border border-slate-200 bg-white outline-none focus:border-slate-800 rounded-lg p-2 font-mono font-bold"
|
| 354 |
+
/>
|
| 355 |
+
</div>
|
| 356 |
+
</div>
|
| 357 |
+
<p className="text-[10px] text-slate-400 leading-normal italic">
|
| 358 |
+
* When set, invoice generator computes total by multiplying subtotal against rating index, subtracting items discounts instantly.
|
| 359 |
+
</p>
|
| 360 |
+
</div>
|
| 361 |
+
|
| 362 |
+
{/* OFFLINE MANAGE STATE */}
|
| 363 |
+
<div className="bg-slate-50 border border-slate-200 p-4 rounded-lg flex items-center justify-between gap-4">
|
| 364 |
+
<div className="space-y-1">
|
| 365 |
+
<span className="text-xs font-bold text-slate-850 flex items-center gap-1">
|
| 366 |
+
{isOnline ? <Wifi className="w-4 h-4 text-emerald-600 animate-pulse" /> : <WifiOff className="w-4 h-4 text-orange-655" />}
|
| 367 |
+
Offline Mode Simulator
|
| 368 |
+
</span>
|
| 369 |
+
<p className="text-[10px] text-slate-400 leading-normal">
|
| 370 |
+
Turn offline mode on/off to test local caching queue and receipt buffer logs.
|
| 371 |
+
</p>
|
| 372 |
+
</div>
|
| 373 |
+
|
| 374 |
+
<button
|
| 375 |
+
type="button"
|
| 376 |
+
onClick={onToggleOnlineMode}
|
| 377 |
+
className={`text-xs font-semibold px-3.5 py-2.5 rounded-lg border transition shadow-sm cursor-pointer ${
|
| 378 |
+
isOnline
|
| 379 |
+
? 'bg-slate-900 border-transparent text-white hover:bg-slate-800'
|
| 380 |
+
: 'bg-orange-600 border-transparent text-white hover:bg-orange-550'
|
| 381 |
+
}`}
|
| 382 |
+
>
|
| 383 |
+
{isOnline ? "Simulate Disconnection" : "Connect Online Mode"}
|
| 384 |
+
</button>
|
| 385 |
+
</div>
|
| 386 |
+
|
| 387 |
+
<div className="flex gap-4">
|
| 388 |
+
<button
|
| 389 |
+
type="button"
|
| 390 |
+
onClick={handleResetConfirm}
|
| 391 |
+
className="bg-slate-50 hover:bg-slate-100 text-red-650 hover:text-red-750 font-medium border border-slate-200 px-4 py-2.5 rounded-lg transition cursor-pointer flex items-center gap-1.5 shadow-sm"
|
| 392 |
+
>
|
| 393 |
+
<RotateCcw className="w-4 h-4" />
|
| 394 |
+
Wipe Local Database
|
| 395 |
+
</button>
|
| 396 |
+
<button
|
| 397 |
+
type="submit"
|
| 398 |
+
className="flex-1 bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs px-6 py-2.5 rounded-lg transition cursor-pointer shadow flex items-center justify-center gap-2"
|
| 399 |
+
>
|
| 400 |
+
<Check className="w-4 h-4" />
|
| 401 |
+
Commit Corporate Settings
|
| 402 |
+
</button>
|
| 403 |
+
</div>
|
| 404 |
+
</form>
|
| 405 |
+
</div>
|
| 406 |
+
|
| 407 |
+
{/* RIGHT COLUMN: Encrypted Cloud Backups */}
|
| 408 |
+
<div className="xl:col-span-5 bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex flex-col justify-between">
|
| 409 |
+
<div className="space-y-5">
|
| 410 |
+
<div className="flex items-center gap-2 border-b border-slate-100 pb-4 uppercase">
|
| 411 |
+
<Database className="w-4.5 h-4.5 text-slate-800 font-bold" />
|
| 412 |
+
<span className="text-xs font-bold text-slate-900">Encrypted Backups Vault</span>
|
| 413 |
+
</div>
|
| 414 |
+
|
| 415 |
+
{/* Explainer warning */}
|
| 416 |
+
<div className="bg-slate-50 border border-slate-200 p-4 rounded-lg space-y-1.5 text-xs text-slate-500">
|
| 417 |
+
<span className="font-extrabold text-slate-800 flex items-center gap-1 uppercase tracking-wider text-[10px]">
|
| 418 |
+
<Lock className="w-3.5 h-3.5 text-slate-705" />
|
| 419 |
+
End-To-End Security Enforced
|
| 420 |
+
</span>
|
| 421 |
+
<p className="leading-relaxed text-[11px] text-slate-400">
|
| 422 |
+
Under air-gapped terminal paradigms, Haider Brother Traders database is encrypted using rot-salt character ciphers with customized credentials keyframes. Passwords block third-party parsing.
|
| 423 |
+
</p>
|
| 424 |
+
</div>
|
| 425 |
+
|
| 426 |
+
<div className="space-y-3 font-sans text-xs">
|
| 427 |
+
<div className="space-y-1">
|
| 428 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase block text-center">Master Encryption Key / Cipher password</label>
|
| 429 |
+
<input
|
| 430 |
+
type="password"
|
| 431 |
+
value={vaultPassword}
|
| 432 |
+
onChange={(e) => setVaultPassword(e.target.value)}
|
| 433 |
+
className="w-full border border-slate-200 bg-white p-2.5 font-mono text-slate-800 rounded-lg outline-none focus:border-slate-800 shadow-sm text-center tracking-widest text-sm"
|
| 434 |
+
/>
|
| 435 |
+
</div>
|
| 436 |
+
|
| 437 |
+
{/* Action buttons */}
|
| 438 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
| 439 |
+
<button
|
| 440 |
+
type="button"
|
| 441 |
+
onClick={compileAndDownloadBackup}
|
| 442 |
+
className="bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium py-2.5 px-3 rounded-lg flex items-center justify-center gap-1.5 shadow-sm transition cursor-pointer"
|
| 443 |
+
>
|
| 444 |
+
<Download className="w-4 h-4" />
|
| 445 |
+
Save Secure Backup File
|
| 446 |
+
</button>
|
| 447 |
+
|
| 448 |
+
{/* Simulated live cloud database upload sync */}
|
| 449 |
+
<button
|
| 450 |
+
type="button"
|
| 451 |
+
disabled={syncingCloud}
|
| 452 |
+
onClick={handleCloudSyncTrigger}
|
| 453 |
+
className="bg-white hover:bg-slate-100 text-slate-800 border border-slate-200 text-xs font-medium py-2.5 px-3 rounded-lg flex items-center justify-center gap-1.5 shadow-sm transition cursor-pointer"
|
| 454 |
+
>
|
| 455 |
+
<Cloud className="w-4 h-4 text-slate-755" />
|
| 456 |
+
{syncingCloud ? "Uploading cipher..." : "Syndicate Cloud Backup"}
|
| 457 |
+
</button>
|
| 458 |
+
</div>
|
| 459 |
+
|
| 460 |
+
{/* Restore drag files */}
|
| 461 |
+
<div className="space-y-1 pt-2">
|
| 462 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-1">Import & Restore Encrypted hbtback file</label>
|
| 463 |
+
<div className="border border-dashed border-slate-200 rounded-lg p-4 text-center hover:bg-slate-50 bg-slate-50/50 transition relative flex flex-col items-center justify-center gap-2">
|
| 464 |
+
<Upload className="w-5 h-5 text-slate-400 font-light" />
|
| 465 |
+
<span className="text-[10px] text-slate-400">Drag or select a certified (.hbtback) document</span>
|
| 466 |
+
<input
|
| 467 |
+
type="file"
|
| 468 |
+
accept=".hbtback,.txt"
|
| 469 |
+
onChange={handleFileSelect}
|
| 470 |
+
className="absolute inset-0 opacity-0 cursor-pointer"
|
| 471 |
+
/>
|
| 472 |
+
</div>
|
| 473 |
+
</div>
|
| 474 |
+
</div>
|
| 475 |
+
</div>
|
| 476 |
+
|
| 477 |
+
{/* Secure cryptographic logs console view */}
|
| 478 |
+
<div className="space-y-2 mt-6">
|
| 479 |
+
<span className="text-[10px] text-slate-500 font-bold uppercase tracking-wider block">Cryptographic Secure Actions Telemetry</span>
|
| 480 |
+
<div className="bg-slate-900 text-emerald-400 font-mono text-[10px] p-4 rounded-lg max-h-40 overflow-y-auto pr-1 leading-relaxed border border-slate-850 shadow-inner">
|
| 481 |
+
{backupLogs.map((log, i) => (
|
| 482 |
+
<div key={i} className="pb-1 border-b border-white/5 last:border-0">{log}</div>
|
| 483 |
+
))}
|
| 484 |
+
</div>
|
| 485 |
+
</div>
|
| 486 |
+
</div>
|
| 487 |
+
</div>
|
| 488 |
+
</div>
|
| 489 |
+
);
|
| 490 |
+
}
|
src/components/SocialPromoter.tsx
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @license
|
| 3 |
+
* SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import React, { useState, useMemo } from 'react';
|
| 7 |
+
import { motion } from 'motion/react';
|
| 8 |
+
import {
|
| 9 |
+
Megaphone,
|
| 10 |
+
Share2,
|
| 11 |
+
Copy,
|
| 12 |
+
Check,
|
| 13 |
+
Download,
|
| 14 |
+
MessageSquare,
|
| 15 |
+
Sparkles,
|
| 16 |
+
ChevronRight,
|
| 17 |
+
FileImage,
|
| 18 |
+
Tag,
|
| 19 |
+
Percent,
|
| 20 |
+
Search
|
| 21 |
+
} from 'lucide-react';
|
| 22 |
+
import { TireProduct, SystemSettings } from '../types';
|
| 23 |
+
|
| 24 |
+
interface SocialPromoterProps {
|
| 25 |
+
products: TireProduct[];
|
| 26 |
+
settings: SystemSettings;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export default function SocialPromoter({ products, settings }: SocialPromoterProps) {
|
| 30 |
+
const [selectedProdId, setSelectedProdId] = useState<string>(products[0]?.id || '');
|
| 31 |
+
const [marketingTopic, setMarketingTopic] = useState<'DISCOUNT' | 'SAFETY' | 'WINTER' | 'PERFORMANCE'>('DISCOUNT');
|
| 32 |
+
const [promoCode, setPromoCode] = useState('HAIDER_TREATS_26');
|
| 33 |
+
const [customDiscountPercent, setCustomDiscountPercent] = useState(10);
|
| 34 |
+
const [copied, setCopied] = useState(false);
|
| 35 |
+
const [promSearch, setPromSearch] = useState('');
|
| 36 |
+
|
| 37 |
+
const selectedProduct = useMemo(() => {
|
| 38 |
+
return products.find(p => p.id === selectedProdId) || products[0] || null;
|
| 39 |
+
}, [products, selectedProdId]);
|
| 40 |
+
|
| 41 |
+
// Dynamic search list
|
| 42 |
+
const filteredProducts = useMemo(() => {
|
| 43 |
+
return products.filter(p =>
|
| 44 |
+
p.brand.toLowerCase().includes(promSearch.toLowerCase()) ||
|
| 45 |
+
p.model.toLowerCase().includes(promSearch.toLowerCase()) ||
|
| 46 |
+
p.size.toLowerCase().includes(promSearch.toLowerCase())
|
| 47 |
+
);
|
| 48 |
+
}, [products, promSearch]);
|
| 49 |
+
|
| 50 |
+
// Math calculated prices
|
| 51 |
+
const promoPrice = useMemo(() => {
|
| 52 |
+
if (!selectedProduct) return 0;
|
| 53 |
+
const computed = selectedProduct.price * (1 - (customDiscountPercent / 100));
|
| 54 |
+
return Math.round(computed);
|
| 55 |
+
}, [selectedProduct, customDiscountPercent]);
|
| 56 |
+
|
| 57 |
+
// High conversion copy algorithms based on topic
|
| 58 |
+
const promoCopyText = useMemo(() => {
|
| 59 |
+
if (!selectedProduct) return '';
|
| 60 |
+
const brand = selectedProduct.brand.toUpperCase();
|
| 61 |
+
const model = selectedProduct.model;
|
| 62 |
+
const size = selectedProduct.size;
|
| 63 |
+
const oldPriceStr = `${settings.currencySymbol}${selectedProduct.price.toLocaleString()}`;
|
| 64 |
+
const newPriceStr = `${settings.currencySymbol}${promoPrice.toLocaleString()}`;
|
| 65 |
+
|
| 66 |
+
switch(marketingTopic) {
|
| 67 |
+
case 'DISCOUNT':
|
| 68 |
+
return `🔥 *MEGA TIRE DEAL AT HAIDER BROTHER TRADERS* 🔥\n\nUpgrade your ride with premium *${brand} ${model}* tires today! Under our limited-time warehouse clearance festival, enjoy unprecedented markdowns!\n\n🛞 *Profile Specs:* ${size}\n💰 *Regular MSRP:* ~${oldPriceStr}~\n📉 *Promotional rate:* *${newPriceStr}* (SAVE ${customDiscountPercent}%!)\n🎟️ *Use checkout token:* \`${promoCode}\` for free nitrogen fill!\n\n🏢 *Branch Address:* ${settings.shopAddress}\n📞 *Order Line Call/WhatsApp:* ${settings.shopPhone}\n\n_Drive safe, drive on certified treads! #HaiderBrotherTraders #PremiumTyreClearance #SafeJourneys_`;
|
| 69 |
+
|
| 70 |
+
case 'SAFETY':
|
| 71 |
+
return `🌧️ *MONSOON ROAD SAFETY INITIATIVE — HAIDER BROTHERS* 🌧️\n\nDid you inspect your tire tread lines recently? Worn tire grooves are highly dangerous under wet road conditions!\n\nEquip your vehicle with fresh high-traction *${brand} ${model}* SUV/Passenger radial tires today. Maximum hydroplaning resistance:\n\n🛞 *Specs Size:* ${size}\n⚡ *Active Grips Price:* *${newPriceStr}* per piece (includes balancing check!)\n🎖️ *Features:* Steel Belted, Deep wet-road grip grooves.\n\n🏢 *Stop By:* ${settings.shopAddress}\n✨ *Hotline:* ${settings.shopPhone}\n\n_Protect your family. Invest in solid road friction! #TyreSafetyCheck #HaiderTradersQuetta_`;
|
| 72 |
+
|
| 73 |
+
case 'WINTER':
|
| 74 |
+
return `❄️ *AGGRESSIVE TERRAIN GRIP SPECIALISTS* ❄️\n\nConquer unpredictable paths and mountain highways with rugged All-Terrain *${brand} ${model}* treads!\n\nEngineered to withstand extreme temperature ranges and gravel trails:\n\n🛞 *Tire profile size:* ${size}\n💰 *Promo cost:* *${newPriceStr}* (Original MSRP: ~${oldPriceStr}~)\n🚜 *Ideal for:* SUV Crossovers, Light Freight rigs, and heavy loading.\n\n🏢 *Stop by our Auto Vault:* ${settings.shopAddress}\n📞 *Call for booking advice:* ${settings.shopPhone}\n\n_No territory is too hard. Travel fearlessly! #TerrainMastery #SailunTerramax #BridgestonePak #HaiderBrothers_`;
|
| 75 |
+
|
| 76 |
+
case 'PERFORMANCE':
|
| 77 |
+
return `🏎️ *ELITE RACING COMPOUND HANDLINGS — HIGH PERFORMANCE* 🏎️\n\nUnleash extreme tarmac performance and responsive steering control with premium racing radial *${brand} ${model}*!\n\nEngineered by Yokohama/Bridgestone for high cornering friction matrices:\n\n🛞 *Dimension size:* ${size}\n🏁 *Precision Rate:* *${newPriceStr}* (Limited retail sets available!)\n🛠️ *Complimentary layout:* Balancing, high-durability valves and nitrogen check done on spot.\n\n📞 *Reserve yours now:* ${settings.shopPhone}\n🏢 *Haider Brother Traders* — Quetta's No. 1 premium hub.\n\n_Upgrade handling, transform high-speed braking thresholds! #AdvanComfort #YokohamaDB #PremiumRadials_`;
|
| 78 |
+
}
|
| 79 |
+
}, [selectedProduct, marketingTopic, promoPrice, customDiscountPercent, promoCode, settings]);
|
| 80 |
+
|
| 81 |
+
const handleCopyClipboard = () => {
|
| 82 |
+
navigator.clipboard.writeText(promoCopyText);
|
| 83 |
+
setCopied(true);
|
| 84 |
+
setTimeout(() => setCopied(false), 2000);
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
const shareWhatsApp = () => {
|
| 88 |
+
const encoded = encodeURIComponent(promoCopyText);
|
| 89 |
+
window.open(`https://api.whatsapp.com/send?text=${encoded}`, '_blank');
|
| 90 |
+
};
|
| 91 |
+
|
| 92 |
+
const shareFacebook = () => {
|
| 93 |
+
const link = "https://www.facebook.com/sharer/sharer.php?u=" + encodeURIComponent("https://haidertraders.com");
|
| 94 |
+
window.open(link, '_blank');
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
// Compile a highly polished self-contained single-flyer visual page HTML and download it!
|
| 98 |
+
const downloadFlyerHTML = () => {
|
| 99 |
+
if (!selectedProduct) return;
|
| 100 |
+
|
| 101 |
+
// Theme colors predicated on campaign topic
|
| 102 |
+
let gradient = "linear-gradient(135deg, #1e3a8a, #0f172a)"; // blue
|
| 103 |
+
if (marketingTopic === 'SAFETY') gradient = "linear-gradient(135deg, #0f766e, #0f172a)"; // teal
|
| 104 |
+
if (marketingTopic === 'WINTER') gradient = "linear-gradient(135deg, #4338ca, #111827)"; // dark indigo
|
| 105 |
+
if (marketingTopic === 'PERFORMANCE') gradient = "linear-gradient(135deg, #b91c1c, #0f172a)"; // red
|
| 106 |
+
|
| 107 |
+
const flyerHTML = `
|
| 108 |
+
<!DOCTYPE html>
|
| 109 |
+
<html>
|
| 110 |
+
<head>
|
| 111 |
+
<title>Marketing Poster - ${selectedProduct.brand}</title>
|
| 112 |
+
<style>
|
| 113 |
+
body { margin: 0; padding: 0; background-color: #f1f5f9; display: flex; justify-content: center; align-items: center; min-height: 100vh; font-family: system-ui, sans-serif; }
|
| 114 |
+
.poster { width: 500px; height: 600px; background: ${gradient}; color: #fff; border-radius: 20px; overflow: hidden; padding: 40px; display: flex; flex-col; justify-content: space-between; box-sizing: border-box; position: relative; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
|
| 115 |
+
.badge { background: #e11d48; color: white; display: inline-block; padding: 6px 15px; border-radius: 99px; font-weight: bold; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; }
|
| 116 |
+
.title { font-size: 36px; font-weight: 900; line-height: 1.1; text-transform: uppercase; margin-top: 10px; }
|
| 117 |
+
.sub { font-size: 16px; opacity: 0.8; margin-top: 5px; }
|
| 118 |
+
.size { font-family: monospace; font-size: 24px; color: #60a5fa; font-weight: bold; border-left: 4px solid #60a5fa; padding-left: 10px; margin: 25px 0; }
|
| 119 |
+
.description { font-size: 13px; opacity: 0.7; max-width: 80%; line-height: 1.5; }
|
| 120 |
+
.pricing { background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 15px; padding: 20px; display: flex; justify-content: space-between; align-items: center; }
|
| 121 |
+
.old-price { font-size: 14px; opacity: 0.5; text-decoration: line-through; }
|
| 122 |
+
.new-price { font-size: 32px; font-weight: 900; color: #4ade80; font-family:-apple-system, sans-serif; }
|
| 123 |
+
.footer { font-size: 11px; opacity: 0.6; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 15px; display: flex; justify-content: space-between; }
|
| 124 |
+
</style>
|
| 125 |
+
</head>
|
| 126 |
+
<body>
|
| 127 |
+
<div class="poster">
|
| 128 |
+
<div style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;">
|
| 129 |
+
<div>
|
| 130 |
+
<span class="badge">${marketingTopic} SPECIAL</span>
|
| 131 |
+
<div class="title">${selectedProduct.brand}<br/><span style="color:#fcd34d">${selectedProduct.model}</span></div>
|
| 132 |
+
<div class="sub">Haider Brother Traders Premium Radial</div>
|
| 133 |
+
<div class="size">${selectedProduct.size}</div>
|
| 134 |
+
<p class="description">${selectedProduct.description || "Incomparably robust tread design built for extreme wear and wet grip."}</p>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
<div style="margin-top:auto; display:flex; flex-direction:column; gap:20px;">
|
| 138 |
+
<div class="pricing">
|
| 139 |
+
<div>
|
| 140 |
+
<div class="old-price">MSRP: ${settings.currencySymbol}${selectedProduct.price.toLocaleString()}</div>
|
| 141 |
+
<div style="font-size:11px; opacity:0.8; font-weight:700;">PROMO DEAL</div>
|
| 142 |
+
</div>
|
| 143 |
+
<div class="new-price">${settings.currencySymbol}${promoPrice.toLocaleString()}</div>
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
<div class="footer">
|
| 147 |
+
<div>🏢 Showroom: Main Chaman Road, Quetta</div>
|
| 148 |
+
<div>📞 Call/WA: ${settings.shopPhone}</div>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
</body>
|
| 154 |
+
</html>
|
| 155 |
+
`;
|
| 156 |
+
|
| 157 |
+
const blob = new Blob([flyerHTML], { type: 'text/html' });
|
| 158 |
+
const link = document.createElement('a');
|
| 159 |
+
link.href = URL.createObjectURL(blob);
|
| 160 |
+
link.download = `Promo flyer_${selectedProduct.brand}_${selectedProduct.model}.html`;
|
| 161 |
+
link.click();
|
| 162 |
+
};
|
| 163 |
+
|
| 164 |
+
return (
|
| 165 |
+
<div className="space-y-6 animate-fade-in" id="social-integration-cockpit">
|
| 166 |
+
{/* Mega header explaining direct shares */}
|
| 167 |
+
<div className="bg-white border border-slate-200 rounded-xl p-5 md:p-6 text-slate-950 flex flex-col md:flex-row justify-between items-start md:items-center gap-6 shadow-sm">
|
| 168 |
+
<div className="space-y-1.5 max-w-xl">
|
| 169 |
+
<div className="bg-slate-150 text-slate-700 text-[10px] font-bold px-2.5 py-0.5 rounded-md border border-slate-200/30 w-fit uppercase">
|
| 170 |
+
🚀 SOCIAL PROMOTION MULTIPLIER
|
| 171 |
+
</div>
|
| 172 |
+
<h2 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Social Marketing Campaign Wizard</h2>
|
| 173 |
+
<p className="text-slate-400 text-xs md:text-sm">
|
| 174 |
+
Generate high-converting ads and social outreach posts directly from live stock indices to WhatsApp, FB, and downloadable digital pamphlets.
|
| 175 |
+
</p>
|
| 176 |
+
</div>
|
| 177 |
+
<div className="p-3 bg-slate-50 border border-slate-250/60 rounded-xl text-slate-850 self-center">
|
| 178 |
+
<Megaphone className="w-6 h-6" />
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
<div className="grid grid-cols-1 xl:grid-cols-12 gap-6">
|
| 183 |
+
{/* LEFT COLUMN: Setup flyer parameters */}
|
| 184 |
+
<div className="xl:col-span-4 bg-white border border-slate-200 rounded-xl p-5 shadow-sm space-y-5">
|
| 185 |
+
<div className="flex items-center gap-1.5 pb-3 border-b border-slate-100 uppercase">
|
| 186 |
+
<Sparkles className="w-4 h-4 text-slate-700" />
|
| 187 |
+
<span className="text-xs font-bold text-slate-900">Campaign Parameters</span>
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
{/* Search match */}
|
| 191 |
+
<div className="space-y-1">
|
| 192 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">1. Search & Select Tire Product</label>
|
| 193 |
+
<div className="relative mb-2">
|
| 194 |
+
<span className="absolute left-2.5 top-2.5 text-slate-400">
|
| 195 |
+
<Search className="w-3.5 h-3.5" />
|
| 196 |
+
</span>
|
| 197 |
+
<input
|
| 198 |
+
type="text"
|
| 199 |
+
placeholder="Search warehouse tires..."
|
| 200 |
+
value={promSearch}
|
| 201 |
+
onChange={(e) => setPromSearch(e.target.value)}
|
| 202 |
+
className="w-full text-xs border border-slate-200 p-2 pl-8 rounded-lg outline-none focus:border-slate-800 bg-white transition"
|
| 203 |
+
/>
|
| 204 |
+
</div>
|
| 205 |
+
|
| 206 |
+
<select
|
| 207 |
+
value={selectedProdId}
|
| 208 |
+
onChange={(e) => setSelectedProdId(e.target.value)}
|
| 209 |
+
className="w-full text-xs font-sans border border-slate-200 bg-white px-3 py-2 rounded-lg outline-none focus:border-slate-800 transition"
|
| 210 |
+
>
|
| 211 |
+
{filteredProducts.map(p => (
|
| 212 |
+
<option key={p.id} value={p.id}>
|
| 213 |
+
{p.brand} {p.model} ({p.size}) — {settings.currencySymbol}{p.price.toLocaleString()}
|
| 214 |
+
</option>
|
| 215 |
+
))}
|
| 216 |
+
</select>
|
| 217 |
+
</div>
|
| 218 |
+
|
| 219 |
+
{/* Marketing Theme Selection */}
|
| 220 |
+
<div className="space-y-1">
|
| 221 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">2. Select Campaign Vibe</label>
|
| 222 |
+
<div className="grid grid-cols-2 gap-2 text-xs font-sans">
|
| 223 |
+
<button
|
| 224 |
+
onClick={() => setMarketingTopic('DISCOUNT')}
|
| 225 |
+
className={`p-2 border rounded-lg transition text-left cursor-pointer font-bold ${marketingTopic === 'DISCOUNT' ? 'bg-slate-900 text-white shadow' : 'bg-slate-50 text-slate-600 hover:bg-slate-100 border-slate-200'}`}
|
| 226 |
+
>
|
| 227 |
+
🔥 MEGA Clearance
|
| 228 |
+
</button>
|
| 229 |
+
<button
|
| 230 |
+
onClick={() => setMarketingTopic('SAFETY')}
|
| 231 |
+
className={`p-2 border rounded-lg transition text-left cursor-pointer font-bold ${marketingTopic === 'SAFETY' ? 'bg-slate-900 text-white shadow' : 'bg-slate-50 text-slate-600 hover:bg-slate-100 border-slate-200'}`}
|
| 232 |
+
>
|
| 233 |
+
💧 Wet-Road Safety
|
| 234 |
+
</button>
|
| 235 |
+
<button
|
| 236 |
+
onClick={() => setMarketingTopic('WINTER')}
|
| 237 |
+
className={`p-2 border rounded-lg transition text-left cursor-pointer font-bold ${marketingTopic === 'WINTER' ? 'bg-slate-900 text-white shadow' : 'bg-slate-50 text-slate-600 hover:bg-slate-100 border-slate-200'}`}
|
| 238 |
+
>
|
| 239 |
+
❄️ All-Terrain Grip
|
| 240 |
+
</button>
|
| 241 |
+
<button
|
| 242 |
+
onClick={() => setMarketingTopic('PERFORMANCE')}
|
| 243 |
+
className={`p-2 border rounded-lg transition text-left cursor-pointer font-bold ${marketingTopic === 'PERFORMANCE' ? 'bg-slate-900 text-white shadow' : 'bg-slate-50 text-slate-600 hover:bg-slate-100 border-slate-200'}`}
|
| 244 |
+
>
|
| 245 |
+
🏁 Cornering Sport
|
| 246 |
+
</button>
|
| 247 |
+
</div>
|
| 248 |
+
</div>
|
| 249 |
+
|
| 250 |
+
{/* Pricing configurations & promo codes */}
|
| 251 |
+
<div className="grid grid-cols-2 gap-3 pb-2">
|
| 252 |
+
<div className="space-y-1">
|
| 253 |
+
<label className="text-[10px] text-slate-550 font-bold uppercase">Coupon discount</label>
|
| 254 |
+
<div className="relative">
|
| 255 |
+
<span className="absolute right-3 top-2 text-slate-400">
|
| 256 |
+
<Percent className="w-3.5 h-3.5" />
|
| 257 |
+
</span>
|
| 258 |
+
<input
|
| 259 |
+
type="number"
|
| 260 |
+
min="0"
|
| 261 |
+
max="90"
|
| 262 |
+
value={customDiscountPercent}
|
| 263 |
+
onChange={(e) => setCustomDiscountPercent(Math.min(90, Math.max(0, parseInt(e.target.value) || 0)))}
|
| 264 |
+
className="w-full text-xs font-mono border border-slate-200 bg-white px-3 py-2 rounded-lg outline-none focus:border-slate-800 transition"
|
| 265 |
+
/>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
|
| 269 |
+
<div className="space-y-1">
|
| 270 |
+
<label className="text-[10px] text-slate-550 font-bold uppercase">Promo code token</label>
|
| 271 |
+
<input
|
| 272 |
+
type="text"
|
| 273 |
+
value={promoCode}
|
| 274 |
+
onChange={(e) => setPromoCode(e.target.value.toUpperCase())}
|
| 275 |
+
className="w-full text-xs font-mono font-bold border border-slate-200 bg-white px-3 py-2 rounded-lg outline-none text-slate-900 focus:border-slate-800 transition"
|
| 276 |
+
/>
|
| 277 |
+
</div>
|
| 278 |
+
</div>
|
| 279 |
+
</div>
|
| 280 |
+
|
| 281 |
+
{/* MIDDLE COLUMN: Text marketing output copy block */}
|
| 282 |
+
<div className="xl:col-span-5 bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex flex-col justify-between">
|
| 283 |
+
<div className="space-y-3">
|
| 284 |
+
<div className="flex justify-between items-center border-b border-slate-100 pb-3 h-8">
|
| 285 |
+
<span className="text-xs font-bold text-slate-900 uppercase flex items-center gap-1.5">
|
| 286 |
+
<Megaphone className="w-4 h-4 text-slate-700" />
|
| 287 |
+
Social Outgoing Copy (Refined)
|
| 288 |
+
</span>
|
| 289 |
+
<button
|
| 290 |
+
type="button"
|
| 291 |
+
onClick={handleCopyClipboard}
|
| 292 |
+
className="text-xs text-slate-800 bg-slate-100 hover:bg-slate-200/80 border border-slate-250/50 px-2.5 py-1 rounded-md flex items-center gap-1 cursor-pointer transition shadow-sm"
|
| 293 |
+
>
|
| 294 |
+
{copied ? (
|
| 295 |
+
<>
|
| 296 |
+
<Check className="w-3.5 h-3.5 text-emerald-600" />
|
| 297 |
+
Copied!
|
| 298 |
+
</>
|
| 299 |
+
) : (
|
| 300 |
+
<>
|
| 301 |
+
<Copy className="w-3.5 h-3.5" />
|
| 302 |
+
Copy Code
|
| 303 |
+
</>
|
| 304 |
+
)}
|
| 305 |
+
</button>
|
| 306 |
+
</div>
|
| 307 |
+
|
| 308 |
+
<textarea
|
| 309 |
+
rows={14}
|
| 310 |
+
readOnly
|
| 311 |
+
value={promoCopyText}
|
| 312 |
+
className="w-full text-xs bg-slate-50 border border-slate-200 rounded-lg p-3 font-mono leading-relaxed resize-none outline-none focus:border-slate-800 transition"
|
| 313 |
+
/>
|
| 314 |
+
</div>
|
| 315 |
+
|
| 316 |
+
<div className="grid grid-cols-2 gap-3 pt-4">
|
| 317 |
+
<button
|
| 318 |
+
onClick={shareWhatsApp}
|
| 319 |
+
className="py-2.5 bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs rounded-lg flex items-center justify-center gap-1.5 shadow-sm transition cursor-pointer"
|
| 320 |
+
>
|
| 321 |
+
<MessageSquare className="w-3.5 h-3.5" />
|
| 322 |
+
WhatsApp Blast
|
| 323 |
+
</button>
|
| 324 |
+
<button
|
| 325 |
+
onClick={shareFacebook}
|
| 326 |
+
className="py-2.5 bg-white hover:bg-slate-100 text-slate-800 font-semibold border border-slate-200 text-xs rounded-lg flex items-center justify-center gap-1.5 shadow-xs transition cursor-pointer"
|
| 327 |
+
>
|
| 328 |
+
<Share2 className="w-3.5 h-3.5" />
|
| 329 |
+
Facebook Feed
|
| 330 |
+
</button>
|
| 331 |
+
</div>
|
| 332 |
+
</div>
|
| 333 |
+
|
| 334 |
+
{/* RIGHT COLUMN: Real-time rendered poster image display container */}
|
| 335 |
+
<div className="xl:col-span-3 bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex flex-col justify-between">
|
| 336 |
+
<div className="space-y-4">
|
| 337 |
+
<span className="text-xs font-bold text-slate-900 uppercase tracking-tight block border-b border-slate-100 pb-3">Poster Preview (500x600 px)</span>
|
| 338 |
+
|
| 339 |
+
{/* Visual flyer preview board */}
|
| 340 |
+
{selectedProduct ? (
|
| 341 |
+
<div
|
| 342 |
+
className={`rounded-xl p-6 text-white h-96 flex flex-col justify-between relative overflow-hidden shadow-md font-sans ${
|
| 343 |
+
marketingTopic === 'DISCOUNT' ? 'bg-gradient-to-b from-blue-900 to-slate-950 border border-blue-800' :
|
| 344 |
+
marketingTopic === 'SAFETY' ? 'bg-gradient-to-b from-teal-800 to-slate-950 border border-teal-700' :
|
| 345 |
+
marketingTopic === 'WINTER' ? 'bg-gradient-to-b from-indigo-900 to-slate-950 border border-indigo-800' :
|
| 346 |
+
'bg-gradient-to-b from-red-800 to-slate-950 border border-red-700'
|
| 347 |
+
}`}
|
| 348 |
+
>
|
| 349 |
+
<div className="space-y-2">
|
| 350 |
+
<span className="bg-red-600 text-white font-bold text-[9px] uppercase tracking-widest px-2 py-0.5 rounded-full">
|
| 351 |
+
{marketingTopic} Campaign
|
| 352 |
+
</span>
|
| 353 |
+
|
| 354 |
+
<div>
|
| 355 |
+
<h5 className="text-2xl font-black uppercase tracking-tight leading-none text-white">{selectedProduct.brand}</h5>
|
| 356 |
+
<p className="text-xs font-medium text-amber-300 leading-tight">{selectedProduct.model}</p>
|
| 357 |
+
</div>
|
| 358 |
+
|
| 359 |
+
<p className="font-mono text-xs font-bold text-blue-300 border-l-2 border-blue-400 pl-2">
|
| 360 |
+
{selectedProduct.size}
|
| 361 |
+
</p>
|
| 362 |
+
</div>
|
| 363 |
+
|
| 364 |
+
<div className="space-y-3 relative z-10">
|
| 365 |
+
<div className="bg-white/10 border border-white/10 rounded-xl p-3 flex justify-between items-center text-xs">
|
| 366 |
+
<div className="space-y-0.5">
|
| 367 |
+
<span className="text-[10px] text-white/50 text-decoration-line-through">MSRP: {selectedProduct.price.toLocaleString()}</span>
|
| 368 |
+
<p className="font-black text-white text-[10px]">CLEARANCE VALUE</p>
|
| 369 |
+
</div>
|
| 370 |
+
<span className="text-xl font-black text-emerald-400 font-mono">
|
| 371 |
+
{settings.currencySymbol}{promoPrice.toLocaleString()}
|
| 372 |
+
</span>
|
| 373 |
+
</div>
|
| 374 |
+
|
| 375 |
+
<div className="text-[9px] text-white/50 text-center leading-normal">
|
| 376 |
+
{settings.shopName} • {settings.shopPhone}
|
| 377 |
+
</div>
|
| 378 |
+
</div>
|
| 379 |
+
|
| 380 |
+
{/* Geometric tire tread watermarks on poster bg */}
|
| 381 |
+
<div className="absolute right-[-20px] bottom-10 opacity-5 pointer-events-none transform rotate-45 scale-125">
|
| 382 |
+
<div className="w-40 h-40 border-[15px] border-white rounded-full border-dashed"></div>
|
| 383 |
+
</div>
|
| 384 |
+
</div>
|
| 385 |
+
) : (
|
| 386 |
+
<div className="bg-slate-50 h-96 border rounded-2xl flex items-center justify-center text-slate-400 text-xs">
|
| 387 |
+
Select a warehouse product to preview flyer.
|
| 388 |
+
</div>
|
| 389 |
+
)}
|
| 390 |
+
</div>
|
| 391 |
+
|
| 392 |
+
<button
|
| 393 |
+
onClick={downloadFlyerHTML}
|
| 394 |
+
disabled={!selectedProduct}
|
| 395 |
+
className={`w-full py-2.5 mt-4 text-xs font-semibold rounded-lg flex items-center justify-center gap-1.5 cursor-pointer shadow-sm transition border ${
|
| 396 |
+
!selectedProduct
|
| 397 |
+
? 'bg-slate-100 text-slate-400 border-slate-200 cursor-not-allowed shadow-none'
|
| 398 |
+
: 'bg-slate-900 border-slate-900 hover:bg-slate-800 text-white'
|
| 399 |
+
}`}
|
| 400 |
+
>
|
| 401 |
+
<Download className="w-4.5 h-4.5" />
|
| 402 |
+
Download Poster Template
|
| 403 |
+
</button>
|
| 404 |
+
</div>
|
| 405 |
+
</div>
|
| 406 |
+
</div>
|
| 407 |
+
);
|
| 408 |
+
}
|
src/components/StaffManager.tsx
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @license
|
| 3 |
+
* SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import React, { useState } from 'react';
|
| 7 |
+
import { motion, AnimatePresence } from 'motion/react';
|
| 8 |
+
import {
|
| 9 |
+
Users,
|
| 10 |
+
UserCheck,
|
| 11 |
+
Lock,
|
| 12 |
+
Shield,
|
| 13 |
+
Trash2,
|
| 14 |
+
UserPlus,
|
| 15 |
+
Fingerprint,
|
| 16 |
+
CheckCircle2,
|
| 17 |
+
XCircle,
|
| 18 |
+
ToggleLeft,
|
| 19 |
+
KeyRound,
|
| 20 |
+
Sliders,
|
| 21 |
+
Settings
|
| 22 |
+
} from 'lucide-react';
|
| 23 |
+
import { StaffUser, StaffRole } from '../types';
|
| 24 |
+
|
| 25 |
+
interface StaffManagerProps {
|
| 26 |
+
staffList: StaffUser[];
|
| 27 |
+
currentStaff: StaffUser;
|
| 28 |
+
onSetCurrentStaff: (staff: StaffUser) => void;
|
| 29 |
+
onAddStaffMember: (user: StaffUser) => void;
|
| 30 |
+
onToggleStaffStatus: (staffId: string) => void;
|
| 31 |
+
onUpdateStaffRole: (staffId: string, role: StaffRole) => void;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export default function StaffManager({
|
| 35 |
+
staffList,
|
| 36 |
+
currentStaff,
|
| 37 |
+
onSetCurrentStaff,
|
| 38 |
+
onAddStaffMember,
|
| 39 |
+
onToggleStaffStatus,
|
| 40 |
+
onUpdateStaffRole
|
| 41 |
+
}: StaffManagerProps) {
|
| 42 |
+
const [showAddStaff, setShowAddStaff] = useState(false);
|
| 43 |
+
|
| 44 |
+
// Create staff states
|
| 45 |
+
const [newName, setNewName] = useState('');
|
| 46 |
+
const [newEmail, setNewEmail] = useState('');
|
| 47 |
+
const [newRole, setNewRole] = useState<StaffRole>('cashier');
|
| 48 |
+
|
| 49 |
+
const isOwner = currentStaff.role === 'owner';
|
| 50 |
+
const isManagerOrOwner = currentStaff.role === 'owner' || currentStaff.role === 'manager';
|
| 51 |
+
|
| 52 |
+
const handleCreateStaff = (e: React.FormEvent) => {
|
| 53 |
+
e.preventDefault();
|
| 54 |
+
if (!newName.trim() || !newEmail.trim()) {
|
| 55 |
+
alert("Name and email references are mandatory.");
|
| 56 |
+
return;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
if (staffList.some(s => s.email.toLowerCase() === newEmail.toLowerCase())) {
|
| 60 |
+
alert("An account is already configured under this email register.");
|
| 61 |
+
return;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
const newUser: StaffUser = {
|
| 65 |
+
id: 'staff_' + Math.random().toString(36).substr(2, 9),
|
| 66 |
+
name: newName,
|
| 67 |
+
email: newEmail,
|
| 68 |
+
role: newRole,
|
| 69 |
+
active: true
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
onAddStaffMember(newUser);
|
| 73 |
+
setNewName('');
|
| 74 |
+
setNewEmail('');
|
| 75 |
+
setNewRole('cashier');
|
| 76 |
+
setShowAddStaff(false);
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
const handleSwitchOperator = (user: StaffUser) => {
|
| 80 |
+
if (!user.active) {
|
| 81 |
+
alert("Cannot switch to inactive staff profile. Owner must restore authorization status first.");
|
| 82 |
+
return;
|
| 83 |
+
}
|
| 84 |
+
onSetCurrentStaff(user);
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
// Role details mapping for easy visual display
|
| 88 |
+
const getRoleBadgeColor = (role: StaffRole) => {
|
| 89 |
+
switch(role) {
|
| 90 |
+
case 'owner': return 'bg-slate-900 text-white border-transparent';
|
| 91 |
+
case 'manager': return 'bg-slate-100 text-slate-800 border-slate-200';
|
| 92 |
+
case 'cashier': return 'bg-slate-50 text-slate-650 border-slate-200/60';
|
| 93 |
+
}
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
return (
|
| 97 |
+
<div className="space-y-6" id="role-vault-cockpit">
|
| 98 |
+
{/* Visual Identity Widget - Active privileges indicator */}
|
| 99 |
+
<div className="bg-white border border-slate-200 rounded-xl p-5 flex flex-col md:flex-row items-start md:items-center justify-between gap-6 shadow-sm">
|
| 100 |
+
<div className="flex items-center gap-4">
|
| 101 |
+
<div className="p-3.5 bg-slate-900 text-white rounded-lg shadow-sm">
|
| 102 |
+
<Fingerprint className="w-6 h-6 text-slate-100" />
|
| 103 |
+
</div>
|
| 104 |
+
<div className="space-y-1">
|
| 105 |
+
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold block">Authorized Terminal Cashier</span>
|
| 106 |
+
<div className="flex items-center gap-2">
|
| 107 |
+
<h3 className="text-base font-bold text-slate-900 leading-none">{currentStaff.name}</h3>
|
| 108 |
+
<span className={`text-[9px] font-bold px-2 py-0.5 border rounded-md capitalize ${getRoleBadgeColor(currentStaff.role)}`}>
|
| 109 |
+
{currentStaff.role} MODE
|
| 110 |
+
</span>
|
| 111 |
+
</div>
|
| 112 |
+
<p className="text-xs text-slate-400">Security ledger email: {currentStaff.email}</p>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<div className="bg-slate-50 border border-slate-200/80 p-3.5 rounded-lg space-y-1 md:max-w-xs text-xs font-sans">
|
| 117 |
+
<span className="font-bold text-slate-800 flex items-center gap-1.5 uppercase tracking-wider text-[10px]">
|
| 118 |
+
<Shield className="w-3.5 h-3.5 text-slate-755" />
|
| 119 |
+
Enforced Role Clearances
|
| 120 |
+
</span>
|
| 121 |
+
<p className="text-slate-550 leading-relaxed text-[11px]">
|
| 122 |
+
{currentStaff.role === 'owner' ? "Owner clearance active. Unlimited access of warehouse multipliers, revenue parameters, and full backups." :
|
| 123 |
+
currentStaff.role === 'manager' ? "Manager clearance active. Full access to manual stock adjusters, inventory updates, and cashier rosters." :
|
| 124 |
+
"Cashier clearance active. Invoice generation, payments checkout, and outreach promotions. Stock manual modifications locked."}
|
| 125 |
+
</p>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
|
| 129 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
| 130 |
+
{/* LEFT COLUMN: Switch Active Terminal Operator Block */}
|
| 131 |
+
<div className="lg:col-span-4 bg-white border border-slate-200 rounded-xl shadow-sm p-5 space-y-4">
|
| 132 |
+
<div className="space-y-1 border-b border-slate-100 pb-3">
|
| 133 |
+
<h3 className="text-xs font-bold text-slate-950 uppercase flex items-center gap-1.5">
|
| 134 |
+
<KeyRound className="w-4 h-4 text-slate-705" />
|
| 135 |
+
Switch Terminal Operator
|
| 136 |
+
</h3>
|
| 137 |
+
<p className="text-xs text-slate-400">Review security flows by logging into alternative staff portfolios</p>
|
| 138 |
+
</div>
|
| 139 |
+
|
| 140 |
+
<div className="space-y-2.5">
|
| 141 |
+
{staffList.map((user) => {
|
| 142 |
+
const isActiveLocal = user.id === currentStaff.id;
|
| 143 |
+
|
| 144 |
+
return (
|
| 145 |
+
<div
|
| 146 |
+
key={user.id}
|
| 147 |
+
onClick={() => handleSwitchOperator(user)}
|
| 148 |
+
className={`border p-3 rounded-lg flex items-center justify-between transition cursor-pointer ${
|
| 149 |
+
!user.active ? 'bg-slate-50 border-slate-100 opacity-55 cursor-not-allowed' :
|
| 150 |
+
isActiveLocal
|
| 151 |
+
? 'bg-slate-900 text-white border-transparent shadow-sm font-semibold'
|
| 152 |
+
: 'bg-white hover:border-slate-350 hover:bg-slate-50 border-slate-200'
|
| 153 |
+
}`}
|
| 154 |
+
>
|
| 155 |
+
<div className="space-y-0.5">
|
| 156 |
+
<p className={`text-xs ${isActiveLocal ? 'text-white' : 'text-slate-900'} font-bold`}>{user.name}</p>
|
| 157 |
+
<span className={`text-[10px] capitalize block ${isActiveLocal ? 'text-slate-300 font-medium' : 'text-slate-400'}`}>
|
| 158 |
+
{user.role} • <span>{user.active ? 'Authorized' : 'Suspended'}</span>
|
| 159 |
+
</span>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
{isActiveLocal ? (
|
| 163 |
+
<div className="bg-white/10 p-1 rounded-md text-white">
|
| 164 |
+
<UserCheck className="w-4 h-4" />
|
| 165 |
+
</div>
|
| 166 |
+
) : (
|
| 167 |
+
<span className="text-[10px] text-slate-800 font-semibold underline hover:no-underline">Log in</span>
|
| 168 |
+
)}
|
| 169 |
+
</div>
|
| 170 |
+
);
|
| 171 |
+
})}
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
+
{/* RIGHT COLUMN: Full Staff registers with Bounded Controls */}
|
| 176 |
+
<div className="lg:col-span-8 bg-white border border-slate-200 rounded-xl shadow-sm p-5 space-y-4">
|
| 177 |
+
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
|
| 178 |
+
<div className="space-y-1">
|
| 179 |
+
<h3 className="text-xs font-bold text-slate-950 uppercase flex items-center gap-1.5">
|
| 180 |
+
<Users className="w-4 h-4 text-slate-705" />
|
| 181 |
+
Staff Directory & RBAC Registers
|
| 182 |
+
</h3>
|
| 183 |
+
<p className="text-xs text-slate-400">Edit employee details, adjust authorization levels, and configure status settings</p>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
{isManagerOrOwner && (
|
| 187 |
+
<button
|
| 188 |
+
type="button"
|
| 189 |
+
onClick={() => setShowAddStaff(true)}
|
| 190 |
+
className="bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs px-3 py-2 rounded-lg transition shadow-sm flex items-center gap-1.5 cursor-pointer"
|
| 191 |
+
>
|
| 192 |
+
<UserPlus className="w-4 h-4" />
|
| 193 |
+
Register Staff
|
| 194 |
+
</button>
|
| 195 |
+
)}
|
| 196 |
+
</div>
|
| 197 |
+
|
| 198 |
+
<div className="overflow-x-auto">
|
| 199 |
+
<table className="w-full text-left border-collapse text-xs font-sans">
|
| 200 |
+
<thead className="bg-slate-50 font-bold uppercase text-[9px] tracking-widest text-slate-500 leading-none">
|
| 201 |
+
<tr className="border-b border-slate-150">
|
| 202 |
+
<th className="py-3 px-4">Employee Card</th>
|
| 203 |
+
<th className="py-3 px-4">System Access Tier</th>
|
| 204 |
+
<th className="py-3 px-4 text-center">Duty Status</th>
|
| 205 |
+
<th className="py-3 px-4 text-right">Administrative overrides</th>
|
| 206 |
+
</tr>
|
| 207 |
+
</thead>
|
| 208 |
+
<tbody className="divide-y divide-slate-100 align-middle">
|
| 209 |
+
{staffList.map((user) => (
|
| 210 |
+
<tr key={user.id} className={!user.active ? 'opacity-60 bg-slate-50/50' : ''}>
|
| 211 |
+
<td className="py-3.5 px-4">
|
| 212 |
+
<div className="space-y-0.5">
|
| 213 |
+
<p className="font-bold text-slate-900">{user.name}</p>
|
| 214 |
+
<p className="text-[10px] text-slate-500">{user.email}</p>
|
| 215 |
+
</div>
|
| 216 |
+
</td>
|
| 217 |
+
|
| 218 |
+
<td className="py-3.5 px-4 font-sans font-bold">
|
| 219 |
+
{isOwner && user.id !== currentStaff.id ? (
|
| 220 |
+
<select
|
| 221 |
+
value={user.role}
|
| 222 |
+
onChange={(e) => onUpdateStaffRole(user.id, e.target.value as StaffRole)}
|
| 223 |
+
className="border border-slate-200 bg-white p-1 rounded font-sans text-xs outline-none"
|
| 224 |
+
>
|
| 225 |
+
<option value="owner">Owner</option>
|
| 226 |
+
<option value="manager">Manager</option>
|
| 227 |
+
<option value="cashier">Cashier</option>
|
| 228 |
+
</select>
|
| 229 |
+
) : (
|
| 230 |
+
<span className={`text-[9px] font-bold px-2 py-0.5 border rounded-full capitalize ${getRoleBadgeColor(user.role)}`}>
|
| 231 |
+
{user.role}
|
| 232 |
+
</span>
|
| 233 |
+
)}
|
| 234 |
+
</td>
|
| 235 |
+
|
| 236 |
+
<td className="py-3.5 px-4 text-center">
|
| 237 |
+
<div className="flex items-center justify-center gap-1">
|
| 238 |
+
{user.active ? (
|
| 239 |
+
<span className="bg-emerald-50 text-emerald-800 border border-emerald-100 text-[10px] px-2 py-0.5 rounded-full font-semibold flex items-center gap-1">
|
| 240 |
+
<CheckCircle2 className="w-3 h-3 text-emerald-600" />
|
| 241 |
+
Active
|
| 242 |
+
</span>
|
| 243 |
+
) : (
|
| 244 |
+
<span className="bg-red-50 text-red-800 border border-red-100 text-[10px] px-2 py-0.5 rounded-full font-semibold flex items-center gap-1">
|
| 245 |
+
<XCircle className="w-3 h-3 text-red-500 animate-pulse" />
|
| 246 |
+
Suspended
|
| 247 |
+
</span>
|
| 248 |
+
)}
|
| 249 |
+
</div>
|
| 250 |
+
</td>
|
| 251 |
+
|
| 252 |
+
<td className="py-3.5 px-4 text-right">
|
| 253 |
+
{user.id === currentStaff.id ? (
|
| 254 |
+
<span className="text-[10px] text-slate-400 italic">Operating account</span>
|
| 255 |
+
) : isManagerOrOwner ? (
|
| 256 |
+
<button
|
| 257 |
+
type="button"
|
| 258 |
+
onClick={() => onToggleStaffStatus(user.id)}
|
| 259 |
+
className={`text-xs font-semibold px-3 py-1 rounded-lg transition border cursor-pointer ${
|
| 260 |
+
user.active
|
| 261 |
+
? 'text-red-600 bg-red-50 hover:bg-red-100 border-red-150'
|
| 262 |
+
: 'text-emerald-700 bg-emerald-50 hover:bg-emerald-100 border-emerald-150'
|
| 263 |
+
}`}
|
| 264 |
+
>
|
| 265 |
+
{user.active ? 'Deauthorize' : 'Authorize Duty'}
|
| 266 |
+
</button>
|
| 267 |
+
) : (
|
| 268 |
+
<span className="text-[10px] text-slate-400 italic flex items-center gap-1 justify-end">
|
| 269 |
+
<Lock className="w-3.5 h-3.5" /> Locked
|
| 270 |
+
</span>
|
| 271 |
+
)}
|
| 272 |
+
</td>
|
| 273 |
+
</tr>
|
| 274 |
+
))}
|
| 275 |
+
</tbody>
|
| 276 |
+
</table>
|
| 277 |
+
</div>
|
| 278 |
+
</div>
|
| 279 |
+
</div>
|
| 280 |
+
|
| 281 |
+
{/* CORE MODAL: REGISTER STAFF MEMBER */}
|
| 282 |
+
{showAddStaff && (
|
| 283 |
+
<div className="fixed inset-0 bg-slate-900/40 backdrop-blur-[2px] z-50 flex items-center justify-center p-4">
|
| 284 |
+
<div className="bg-white rounded-xl border border-slate-200 max-w-md w-full p-6 space-y-5 relative shadow-xl">
|
| 285 |
+
<button
|
| 286 |
+
onClick={() => setShowAddStaff(false)}
|
| 287 |
+
className="absolute top-4 right-4 bg-slate-50 hover:bg-slate-150 text-slate-400 hover:text-slate-800 font-bold px-2.5 py-1 rounded text-xs cursor-pointer"
|
| 288 |
+
>
|
| 289 |
+
✕
|
| 290 |
+
</button>
|
| 291 |
+
|
| 292 |
+
<div className="pb-2 border-b border-slate-100">
|
| 293 |
+
<h3 className="text-sm font-bold text-slate-900 uppercase">Register New Terminal Account</h3>
|
| 294 |
+
<p className="text-xs text-slate-400">Provide credentials and role permission clearings</p>
|
| 295 |
+
</div>
|
| 296 |
+
|
| 297 |
+
<form onSubmit={handleCreateStaff} className="space-y-4 text-xs font-sans">
|
| 298 |
+
<div className="space-y-1">
|
| 299 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Name / Identification</label>
|
| 300 |
+
<input
|
| 301 |
+
type="text"
|
| 302 |
+
required
|
| 303 |
+
placeholder="e.g. Zain Brother"
|
| 304 |
+
value={newName}
|
| 305 |
+
onChange={(e) => setNewName(e.target.value)}
|
| 306 |
+
className="w-full border border-slate-200 bg-white px-3 py-2 rounded-lg outline-none focus:border-slate-800 transition"
|
| 307 |
+
/>
|
| 308 |
+
</div>
|
| 309 |
+
|
| 310 |
+
<div className="space-y-1">
|
| 311 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Secure Email register</label>
|
| 312 |
+
<input
|
| 313 |
+
type="email"
|
| 314 |
+
required
|
| 315 |
+
placeholder="e.g. zain@traders.com"
|
| 316 |
+
value={newEmail}
|
| 317 |
+
onChange={(e) => setNewEmail(e.target.value)}
|
| 318 |
+
className="w-full border border-slate-200 bg-white px-3 py-2 rounded-lg outline-none focus:border-slate-800 transition"
|
| 319 |
+
/>
|
| 320 |
+
</div>
|
| 321 |
+
|
| 322 |
+
<div className="space-y-1">
|
| 323 |
+
<label className="text-[10px] text-slate-500 font-bold uppercase">Assigned Security Role Clearance</label>
|
| 324 |
+
<select
|
| 325 |
+
value={newRole}
|
| 326 |
+
onChange={(e) => setNewRole(e.target.value as StaffRole)}
|
| 327 |
+
className="w-full border border-slate-200 bg-white px-3 py-2 rounded-lg outline-none focus:border-slate-800 transition text-xs"
|
| 328 |
+
>
|
| 329 |
+
<option value="cashier">Cashier clearance level (Draft bills & Payments)</option>
|
| 330 |
+
<option value="manager">Manager clearance level (Adjustments & Inventory config)</option>
|
| 331 |
+
<option value="owner">Owner credentials clearance (Total configuration overrides)</option>
|
| 332 |
+
</select>
|
| 333 |
+
</div>
|
| 334 |
+
|
| 335 |
+
<button
|
| 336 |
+
type="submit"
|
| 337 |
+
className="w-full bg-slate-900 hover:bg-slate-800 text-white font-medium py-2.5 rounded-lg shadow-sm transition cursor-pointer text-center"
|
| 338 |
+
>
|
| 339 |
+
Register Employee Account
|
| 340 |
+
</button>
|
| 341 |
+
</form>
|
| 342 |
+
</div>
|
| 343 |
+
</div>
|
| 344 |
+
)}
|
| 345 |
+
</div>
|
| 346 |
+
);
|
| 347 |
+
}
|
src/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
src/initialData.ts
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @license
|
| 3 |
+
* SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { TireProduct, StaffUser, Invoice, StockHistoryItem, SystemSettings } from './types';
|
| 7 |
+
|
| 8 |
+
export const INITIAL_STAFF: StaffUser[] = [
|
| 9 |
+
{ id: '1', name: 'Haider Ali', email: 'haider@traders.com', role: 'owner', active: true },
|
| 10 |
+
{ id: '2', name: 'Zain Brother', email: 'zain@traders.com', role: 'manager', active: true },
|
| 11 |
+
{ id: '3', name: 'Kamran Ali', email: 'kamran@traders.com', role: 'cashier', active: true },
|
| 12 |
+
{ id: '4', name: 'Ahmad Shah', email: 'ahmad@traders.com', role: 'cashier', active: false },
|
| 13 |
+
];
|
| 14 |
+
|
| 15 |
+
export const INITIAL_SETTINGS: SystemSettings = {
|
| 16 |
+
shopName: 'Haider Brother Traders Tire Shop',
|
| 17 |
+
shopAddress: 'Plot 4, Main Chaman Road, Quetta, Pakistan',
|
| 18 |
+
shopPhone: '+92 300 1234567',
|
| 19 |
+
taxName: 'Sales Tax (SRB)',
|
| 20 |
+
taxRate: 15,
|
| 21 |
+
currencyCode: 'PKR',
|
| 22 |
+
currencySymbol: '₨',
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
export const INITIAL_PRODUCTS: TireProduct[] = [
|
| 26 |
+
{
|
| 27 |
+
id: 'p1',
|
| 28 |
+
sku: 'YOKO2055516',
|
| 29 |
+
brand: 'Yokohama',
|
| 30 |
+
model: 'Advan DB V552',
|
| 31 |
+
size: '205/55 R16',
|
| 32 |
+
category: 'Passenger',
|
| 33 |
+
stock: 42,
|
| 34 |
+
minStock: 10,
|
| 35 |
+
price: 32500,
|
| 36 |
+
purchasePrice: 26000,
|
| 37 |
+
description: 'Ultra quiet premium comfort radial tire. Excellent wet of dry grip.',
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
id: 'p2',
|
| 41 |
+
sku: 'BRIDGE1956515',
|
| 42 |
+
brand: 'Bridgestone',
|
| 43 |
+
model: 'Ecopia EP150',
|
| 44 |
+
size: '195/65 R15',
|
| 45 |
+
category: 'Passenger',
|
| 46 |
+
stock: 8, // Trigger active alert!
|
| 47 |
+
minStock: 15,
|
| 48 |
+
price: 24000,
|
| 49 |
+
purchasePrice: 19500,
|
| 50 |
+
description: 'Fuel-efficient eco tire with reliable braking and rolling resistance features.',
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
id: 'p3',
|
| 54 |
+
sku: 'DUN1856514',
|
| 55 |
+
brand: 'Dunlop',
|
| 56 |
+
model: 'SP Touring R1',
|
| 57 |
+
size: '185/65 R14',
|
| 58 |
+
category: 'Passenger',
|
| 59 |
+
stock: 65,
|
| 60 |
+
minStock: 20,
|
| 61 |
+
price: 18500,
|
| 62 |
+
purchasePrice: 14800,
|
| 63 |
+
description: 'Robust on-road performance, ideal for daily commutes on variable road surfaces.',
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
id: 'p4',
|
| 67 |
+
sku: 'GEN1757013',
|
| 68 |
+
brand: 'General Tire',
|
| 69 |
+
model: 'Euro Star',
|
| 70 |
+
size: '175/70 R13',
|
| 71 |
+
category: 'Passenger',
|
| 72 |
+
stock: 120,
|
| 73 |
+
minStock: 30,
|
| 74 |
+
price: 14500,
|
| 75 |
+
purchasePrice: 11200,
|
| 76 |
+
description: 'Extremely durable, steel belted local radial built for Pakistani road terrain.',
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
id: 'p5',
|
| 80 |
+
sku: 'MIC2256517',
|
| 81 |
+
brand: 'Michelin',
|
| 82 |
+
model: 'Primacy SUV',
|
| 83 |
+
size: '225/65 R17',
|
| 84 |
+
category: 'SUV',
|
| 85 |
+
stock: 18,
|
| 86 |
+
minStock: 8,
|
| 87 |
+
price: 49000,
|
| 88 |
+
purchasePrice: 41000,
|
| 89 |
+
description: 'Exceptional safety and handling for crossover SUVs. Extremely smooth ride.',
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
id: 'p6',
|
| 93 |
+
sku: 'SAIL2657016',
|
| 94 |
+
brand: 'Sailun',
|
| 95 |
+
model: 'Terramax A/T',
|
| 96 |
+
size: '265/70 R16',
|
| 97 |
+
category: 'SUV',
|
| 98 |
+
stock: 14,
|
| 99 |
+
minStock: 6,
|
| 100 |
+
price: 38000,
|
| 101 |
+
purchasePrice: 29800,
|
| 102 |
+
description: 'Rugged all-terrain tire with deep multi-step grooves for high dirt-road traction.',
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
id: 'p7',
|
| 106 |
+
sku: 'DB3158022',
|
| 107 |
+
brand: 'Double Star',
|
| 108 |
+
model: 'DS806 Radial',
|
| 109 |
+
size: '315/80 R22.5',
|
| 110 |
+
category: 'Truck/Bus',
|
| 111 |
+
stock: 3, // TRIGGER ACTIVE STOCK ALERT!
|
| 112 |
+
minStock: 5,
|
| 113 |
+
price: 88500,
|
| 114 |
+
purchasePrice: 74000,
|
| 115 |
+
description: 'Commercial steer & trailer tire with maximum ply rating and heavy load design.',
|
| 116 |
+
},
|
| 117 |
+
];
|
| 118 |
+
|
| 119 |
+
export const INITIAL_HISTORY: StockHistoryItem[] = [
|
| 120 |
+
{
|
| 121 |
+
id: 'h1',
|
| 122 |
+
productId: 'p2',
|
| 123 |
+
productLabel: 'Bridgestone Ecopia EP150 (195/65 R15)',
|
| 124 |
+
dateTime: '2026-05-15T09:30:00Z',
|
| 125 |
+
type: 'STOCK_IN',
|
| 126 |
+
quantity: 40,
|
| 127 |
+
resultingStock: 48,
|
| 128 |
+
adjustedBy: 'Zain Brother (Manager)',
|
| 129 |
+
reason: 'Import batch cargo clearance directly from Port Qasim.',
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
id: 'h2',
|
| 133 |
+
productId: 'p2',
|
| 134 |
+
productLabel: 'Bridgestone Ecopia EP150 (195/65 R15)',
|
| 135 |
+
dateTime: '2026-05-28T14:15:00Z',
|
| 136 |
+
type: 'STOCK_OUT',
|
| 137 |
+
quantity: 40,
|
| 138 |
+
resultingStock: 8,
|
| 139 |
+
adjustedBy: 'Kamran Ali (Cashier)',
|
| 140 |
+
reason: 'Bulk invoice sale to Al-Sadiq Rent-a-Car fleet.',
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
id: 'h3',
|
| 144 |
+
productId: 'p4',
|
| 145 |
+
productLabel: 'General Tire Euro Star (175/70 R13)',
|
| 146 |
+
dateTime: '2026-06-01T11:00:00Z',
|
| 147 |
+
type: 'MANUAL_ADJUSTMENT',
|
| 148 |
+
quantity: 50,
|
| 149 |
+
resultingStock: 120,
|
| 150 |
+
adjustedBy: 'Haider Ali (Owner)',
|
| 151 |
+
reason: 'Physical mid-year audit recount: matched warehouse count variance.',
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
id: 'h4',
|
| 155 |
+
productId: 'p7',
|
| 156 |
+
productLabel: 'Double Star DS806 Radial (315/80 R22.5)',
|
| 157 |
+
dateTime: '2026-06-02T16:45:00Z',
|
| 158 |
+
type: 'STOCK_OUT',
|
| 159 |
+
quantity: 2,
|
| 160 |
+
resultingStock: 3,
|
| 161 |
+
adjustedBy: 'Kamran Ali (Cashier)',
|
| 162 |
+
reason: 'Direct retail sale & installation on Hino Dump Truck.',
|
| 163 |
+
},
|
| 164 |
+
];
|
| 165 |
+
|
| 166 |
+
// Let's seed realistic monthly invoicing for 2026 trends, supporting clean charts
|
| 167 |
+
export const INITIAL_INVOICES: Invoice[] = [
|
| 168 |
+
{
|
| 169 |
+
id: 'inv1',
|
| 170 |
+
invoiceNumber: 'HBT-2026-1001',
|
| 171 |
+
dateTime: '24-01-2026 10:15:00', // support readable and parsing
|
| 172 |
+
customerName: 'Mehmood Khan',
|
| 173 |
+
customerPhone: '0333-8889912',
|
| 174 |
+
customerVehicle: 'Toyota Corolla (AJK-552)',
|
| 175 |
+
items: [
|
| 176 |
+
{
|
| 177 |
+
id: 'li1',
|
| 178 |
+
productId: 'p2',
|
| 179 |
+
brand: 'Bridgestone',
|
| 180 |
+
model: 'Ecopia EP150',
|
| 181 |
+
size: '195/65 R15',
|
| 182 |
+
quantity: 4,
|
| 183 |
+
unitPrice: 23500,
|
| 184 |
+
totalPrice: 94000,
|
| 185 |
+
}
|
| 186 |
+
],
|
| 187 |
+
subtotal: 94000,
|
| 188 |
+
taxRate: 15,
|
| 189 |
+
taxAmount: 14100,
|
| 190 |
+
discount: 5000,
|
| 191 |
+
total: 103100,
|
| 192 |
+
currencySymbol: '₨',
|
| 193 |
+
currencyCode: 'PKR',
|
| 194 |
+
paymentMethod: 'CASH',
|
| 195 |
+
paymentStatus: 'PAID',
|
| 196 |
+
cashierName: 'Kamran Ali',
|
| 197 |
+
notes: 'Free wheel balancing & tire valves supplied.',
|
| 198 |
+
dateTimeOriginalISO: '2026-01-24T10:15:00Z',
|
| 199 |
+
},
|
| 200 |
+
{
|
| 201 |
+
id: 'inv2',
|
| 202 |
+
invoiceNumber: 'HBT-2026-1002',
|
| 203 |
+
dateTime: '12-02-2026 14:45:00',
|
| 204 |
+
customerName: 'Sufyan Transport Ltd',
|
| 205 |
+
customerPhone: '0300-5557711',
|
| 206 |
+
customerVehicle: 'Fuso Falcon (ST-9002)',
|
| 207 |
+
items: [
|
| 208 |
+
{
|
| 209 |
+
id: 'li2',
|
| 210 |
+
productId: 'p7',
|
| 211 |
+
brand: 'Double Star',
|
| 212 |
+
model: 'DS806 Radial',
|
| 213 |
+
size: '315/80 R22.5',
|
| 214 |
+
quantity: 6,
|
| 215 |
+
unitPrice: 87000,
|
| 216 |
+
totalPrice: 522000,
|
| 217 |
+
}
|
| 218 |
+
],
|
| 219 |
+
subtotal: 522000,
|
| 220 |
+
taxRate: 15,
|
| 221 |
+
taxAmount: 78300,
|
| 222 |
+
discount: 22000,
|
| 223 |
+
total: 578300,
|
| 224 |
+
currencySymbol: '₨',
|
| 225 |
+
currencyCode: 'PKR',
|
| 226 |
+
paymentMethod: 'BANK_TRANSFER',
|
| 227 |
+
paymentStatus: 'PAID',
|
| 228 |
+
gatewayInfo: {
|
| 229 |
+
gatewayName: 'Bank',
|
| 230 |
+
transactionId: 'IBFT-MBL-88192003',
|
| 231 |
+
authTime: '2026-02-12T15:00:00Z',
|
| 232 |
+
},
|
| 233 |
+
cashierName: 'Zain Brother',
|
| 234 |
+
notes: 'Institutional customer discount applied.',
|
| 235 |
+
dateTimeOriginalISO: '2026-02-12T14:45:00Z',
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
id: 'inv3',
|
| 239 |
+
invoiceNumber: 'HBT-2026-1003',
|
| 240 |
+
dateTime: '05-03-2026 11:20:00',
|
| 241 |
+
customerName: 'Dr. Tariq Jamil',
|
| 242 |
+
customerPhone: '0312-9988111',
|
| 243 |
+
customerVehicle: 'Honda Civic (ICT-LE-720)',
|
| 244 |
+
items: [
|
| 245 |
+
{
|
| 246 |
+
id: 'li3',
|
| 247 |
+
productId: 'p1',
|
| 248 |
+
brand: 'Yokohama',
|
| 249 |
+
model: 'Advan DB V552',
|
| 250 |
+
size: '205/55 R16',
|
| 251 |
+
quantity: 4,
|
| 252 |
+
unitPrice: 32000,
|
| 253 |
+
totalPrice: 128000,
|
| 254 |
+
}
|
| 255 |
+
],
|
| 256 |
+
subtotal: 128000,
|
| 257 |
+
taxRate: 15,
|
| 258 |
+
taxAmount: 19200,
|
| 259 |
+
discount: 3000,
|
| 260 |
+
total: 144200,
|
| 261 |
+
currencySymbol: '₨',
|
| 262 |
+
currencyCode: 'PKR',
|
| 263 |
+
paymentMethod: 'CARD',
|
| 264 |
+
paymentStatus: 'PAID',
|
| 265 |
+
gatewayInfo: {
|
| 266 |
+
gatewayName: 'Stripe',
|
| 267 |
+
transactionId: 'ch_stripe_99a8b77c11',
|
| 268 |
+
authTime: '2026-03-05T11:22:15Z',
|
| 269 |
+
},
|
| 270 |
+
cashierName: 'Kamran Ali',
|
| 271 |
+
dateTimeOriginalISO: '2026-03-05T11:20:00Z',
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
id: 'inv4',
|
| 275 |
+
invoiceNumber: 'HBT-2026-1004',
|
| 276 |
+
dateTime: '18-04-2026 16:30:00',
|
| 277 |
+
customerName: 'Bilal Khan Achakzai',
|
| 278 |
+
customerPhone: '0336-7788199',
|
| 279 |
+
customerVehicle: 'Toyota Land Cruiser (V8-6600)',
|
| 280 |
+
items: [
|
| 281 |
+
{
|
| 282 |
+
id: 'li4',
|
| 283 |
+
productId: 'p6',
|
| 284 |
+
brand: 'Sailun',
|
| 285 |
+
model: 'Terramax A/T',
|
| 286 |
+
size: '265/70 R16',
|
| 287 |
+
quantity: 4,
|
| 288 |
+
unitPrice: 38000,
|
| 289 |
+
totalPrice: 152000,
|
| 290 |
+
}
|
| 291 |
+
],
|
| 292 |
+
subtotal: 152000,
|
| 293 |
+
taxRate: 15,
|
| 294 |
+
taxAmount: 22800,
|
| 295 |
+
discount: 0,
|
| 296 |
+
total: 174800,
|
| 297 |
+
currencySymbol: '₨',
|
| 298 |
+
currencyCode: 'PKR',
|
| 299 |
+
paymentMethod: 'MOBILE_WALLET',
|
| 300 |
+
paymentStatus: 'PAID',
|
| 301 |
+
gatewayInfo: {
|
| 302 |
+
gatewayName: 'EasyPaisa',
|
| 303 |
+
transactionId: 'EP-92200192',
|
| 304 |
+
authTime: '2026-04-18T16:35:00Z',
|
| 305 |
+
},
|
| 306 |
+
cashierName: 'Kamran Ali',
|
| 307 |
+
dateTimeOriginalISO: '2026-04-18T16:30:00Z',
|
| 308 |
+
},
|
| 309 |
+
{
|
| 310 |
+
id: 'inv5',
|
| 311 |
+
invoiceNumber: 'HBT-2026-1005',
|
| 312 |
+
dateTime: '22-05-2026 13:00:00',
|
| 313 |
+
customerName: 'Sardar Yaqoob',
|
| 314 |
+
customerPhone: '0303-3399120',
|
| 315 |
+
customerVehicle: 'Suzuki Alto (QA-991)',
|
| 316 |
+
items: [
|
| 317 |
+
{
|
| 318 |
+
id: 'li5',
|
| 319 |
+
productId: 'p4',
|
| 320 |
+
brand: 'General Tire',
|
| 321 |
+
model: 'Euro Star',
|
| 322 |
+
size: '175/70 R13',
|
| 323 |
+
quantity: 2,
|
| 324 |
+
unitPrice: 14500,
|
| 325 |
+
totalPrice: 29000,
|
| 326 |
+
}
|
| 327 |
+
],
|
| 328 |
+
subtotal: 29000,
|
| 329 |
+
taxRate: 15,
|
| 330 |
+
taxAmount: 4350,
|
| 331 |
+
discount: 1000,
|
| 332 |
+
total: 32350,
|
| 333 |
+
currencySymbol: '₨',
|
| 334 |
+
currencyCode: 'PKR',
|
| 335 |
+
paymentMethod: 'CASH',
|
| 336 |
+
paymentStatus: 'PAID',
|
| 337 |
+
cashierName: 'Ahmad Shah',
|
| 338 |
+
dateTimeOriginalISO: '2026-05-22T13:00:00Z',
|
| 339 |
+
},
|
| 340 |
+
{
|
| 341 |
+
id: 'inv6',
|
| 342 |
+
invoiceNumber: 'HBT-2026-1006',
|
| 343 |
+
dateTime: '02-06-2026 11:45:00', // Current month
|
| 344 |
+
customerName: 'Asadullah Quetta Club',
|
| 345 |
+
customerPhone: '0345-2211440',
|
| 346 |
+
customerVehicle: 'Toyota Prado (BE-777)',
|
| 347 |
+
items: [
|
| 348 |
+
{
|
| 349 |
+
id: 'li6',
|
| 350 |
+
productId: 'p5',
|
| 351 |
+
brand: 'Michelin',
|
| 352 |
+
model: 'Primacy SUV',
|
| 353 |
+
size: '225/65 R17',
|
| 354 |
+
quantity: 4,
|
| 355 |
+
unitPrice: 48500,
|
| 356 |
+
totalPrice: 194000,
|
| 357 |
+
},
|
| 358 |
+
{
|
| 359 |
+
id: 'li7',
|
| 360 |
+
productId: 'p1',
|
| 361 |
+
brand: 'Yokohama',
|
| 362 |
+
model: 'Advan DB V552',
|
| 363 |
+
size: '205/55 R16',
|
| 364 |
+
quantity: 2,
|
| 365 |
+
unitPrice: 32500,
|
| 366 |
+
totalPrice: 65000,
|
| 367 |
+
}
|
| 368 |
+
],
|
| 369 |
+
subtotal: 259000,
|
| 370 |
+
taxRate: 15,
|
| 371 |
+
taxAmount: 38850,
|
| 372 |
+
discount: 7850,
|
| 373 |
+
total: 290000,
|
| 374 |
+
currencySymbol: '₨',
|
| 375 |
+
currencyCode: 'PKR',
|
| 376 |
+
paymentMethod: 'CARD',
|
| 377 |
+
paymentStatus: 'PAID',
|
| 378 |
+
gatewayInfo: {
|
| 379 |
+
gatewayName: 'Stripe',
|
| 380 |
+
transactionId: 'ch_stripe_11x90cc',
|
| 381 |
+
authTime: '2026-06-02T11:51:00Z',
|
| 382 |
+
},
|
| 383 |
+
cashierName: 'Kamran Ali',
|
| 384 |
+
notes: 'Premium nitrogen inflation filled for Prado tyres.',
|
| 385 |
+
dateTimeOriginalISO: '2026-06-02T11:45:00Z',
|
| 386 |
+
}
|
| 387 |
+
] as (Invoice & { dateTimeOriginalISO: string })[];
|
src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {StrictMode} from 'react';
|
| 2 |
+
import {createRoot} from 'react-dom/client';
|
| 3 |
+
import App from './App.tsx';
|
| 4 |
+
import './index.css';
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
);
|
src/types.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @license
|
| 3 |
+
* SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export type StaffRole = 'owner' | 'manager' | 'cashier';
|
| 7 |
+
|
| 8 |
+
export interface StaffUser {
|
| 9 |
+
id: string;
|
| 10 |
+
name: string;
|
| 11 |
+
email: string;
|
| 12 |
+
role: StaffRole;
|
| 13 |
+
active: boolean;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export interface TireProduct {
|
| 17 |
+
id: string;
|
| 18 |
+
sku: string;
|
| 19 |
+
brand: string;
|
| 20 |
+
model: string;
|
| 21 |
+
size: string; // e.g., 195/65 R15
|
| 22 |
+
category: 'Passenger' | 'SUV' | 'Commercial' | 'Light Truck' | 'Truck/Bus';
|
| 23 |
+
stock: number;
|
| 24 |
+
minStock: number; // Alerts triggered when stock <= minStock
|
| 25 |
+
price: number; // Selling price
|
| 26 |
+
purchasePrice: number; // Cost price (for profit computation)
|
| 27 |
+
description?: string;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export interface StockHistoryItem {
|
| 31 |
+
id: string;
|
| 32 |
+
productId: string;
|
| 33 |
+
productLabel: string; // Saved snapshot for history search resilience
|
| 34 |
+
dateTime: string;
|
| 35 |
+
type: 'STOCK_IN' | 'STOCK_OUT' | 'MANUAL_ADJUSTMENT';
|
| 36 |
+
quantity: number; // Signed or unsigned change
|
| 37 |
+
resultingStock: number;
|
| 38 |
+
adjustedBy: string; // Staff user name/role
|
| 39 |
+
reason: string;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export interface InvoiceLineItem {
|
| 43 |
+
id: string;
|
| 44 |
+
productId: string;
|
| 45 |
+
brand: string;
|
| 46 |
+
model: string;
|
| 47 |
+
size: string;
|
| 48 |
+
quantity: number;
|
| 49 |
+
unitPrice: number;
|
| 50 |
+
totalPrice: number;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
export interface Invoice {
|
| 54 |
+
id: string;
|
| 55 |
+
invoiceNumber: string; // e.g. HBT-2026-1001
|
| 56 |
+
dateTime: string;
|
| 57 |
+
customerName: string;
|
| 58 |
+
customerPhone: string;
|
| 59 |
+
customerVehicle?: string; // Car model / registration number (highly relevant for a tire shop!)
|
| 60 |
+
items: InvoiceLineItem[];
|
| 61 |
+
subtotal: number;
|
| 62 |
+
taxRate: number; // e.g., 17 for 17%
|
| 63 |
+
taxAmount: number;
|
| 64 |
+
discount: number;
|
| 65 |
+
total: number;
|
| 66 |
+
currencySymbol: string;
|
| 67 |
+
currencyCode: string;
|
| 68 |
+
paymentMethod: 'CASH' | 'CARD' | 'BANK_TRANSFER' | 'MOBILE_WALLET';
|
| 69 |
+
paymentStatus: 'PAID' | 'UNPAID' | 'PARTIAL';
|
| 70 |
+
amountPaid: number;
|
| 71 |
+
gatewayInfo?: {
|
| 72 |
+
gatewayName: 'Stripe' | 'PayPal' | 'EasyPaisa' | 'JazzCash' | 'Bank';
|
| 73 |
+
transactionId: string;
|
| 74 |
+
authTime: string;
|
| 75 |
+
};
|
| 76 |
+
cashierName: string;
|
| 77 |
+
notes?: string;
|
| 78 |
+
isOfflineCreated?: boolean; // Tracking for offline-first resilience
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
export interface SystemSettings {
|
| 82 |
+
shopName: string;
|
| 83 |
+
shopAddress: string;
|
| 84 |
+
shopPhone: string;
|
| 85 |
+
taxName: string; // e.g., "GST" or "VAT" or "Sales Tax"
|
| 86 |
+
taxRate: number; // Percentage
|
| 87 |
+
currencyCode: string; // PKR, USD, EUR etc.
|
| 88 |
+
currencySymbol: string; // ₨, $, € etc.
|
| 89 |
+
stripePublicKey?: string;
|
| 90 |
+
easyPaisaMerchantId?: string;
|
| 91 |
+
}
|
src/utils/backup.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @license
|
| 3 |
+
* SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Encrypts a plain text string utilizing a rotation and salt key
|
| 8 |
+
* to prevent exposure of sensitive transaction / invoice details.
|
| 9 |
+
*/
|
| 10 |
+
export function encryptData(plainText: string, secretKey: string): string {
|
| 11 |
+
const salt = "HBT-SECURE-VAULT-" + secretKey.length;
|
| 12 |
+
const combined = salt + "||" + plainText;
|
| 13 |
+
|
| 14 |
+
// Convert secretKey into a simple numeric pad
|
| 15 |
+
let pad = 0;
|
| 16 |
+
for (let i = 0; i < secretKey.length; i++) {
|
| 17 |
+
pad += secretKey.charCodeAt(i);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// Rotate characters for obfuscation
|
| 21 |
+
const chars: string[] = [];
|
| 22 |
+
for (let i = 0; i < combined.length; i++) {
|
| 23 |
+
let charCode = combined.charCodeAt(i);
|
| 24 |
+
// Apply rotational key padding
|
| 25 |
+
charCode = (charCode + pad + i) % 65535;
|
| 26 |
+
chars.push(String.fromCharCode(charCode));
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const scrambled = chars.join('');
|
| 30 |
+
// Encode as Base64 safely under browser context
|
| 31 |
+
try {
|
| 32 |
+
return btoa(unescape(encodeURIComponent(scrambled)));
|
| 33 |
+
} catch (e) {
|
| 34 |
+
// Fallback if there are broad unicode failures
|
| 35 |
+
return btoa(scrambled);
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/**
|
| 40 |
+
* Decrypts a scrambled cipher string back to original plain text
|
| 41 |
+
* using the validated passkey. Throws error on fail.
|
| 42 |
+
*/
|
| 43 |
+
export function decryptData(cipherText: string, secretKey: string): string {
|
| 44 |
+
let scrambled = '';
|
| 45 |
+
try {
|
| 46 |
+
scrambled = decodeURIComponent(escape(atob(cipherText)));
|
| 47 |
+
} catch (e) {
|
| 48 |
+
try {
|
| 49 |
+
scrambled = atob(cipherText);
|
| 50 |
+
} catch (err) {
|
| 51 |
+
throw new Error("Invalid file structure: Decryption failed.");
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
let pad = 0;
|
| 56 |
+
for (let i = 0; i < secretKey.length; i++) {
|
| 57 |
+
pad += secretKey.charCodeAt(i);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
const chars: string[] = [];
|
| 61 |
+
for (let i = 0; i < scrambled.length; i++) {
|
| 62 |
+
let charCode = scrambled.charCodeAt(i);
|
| 63 |
+
// Reverse rotation
|
| 64 |
+
charCode = (charCode - pad - i) % 65535;
|
| 65 |
+
while (charCode < 0) charCode += 65535;
|
| 66 |
+
chars.push(String.fromCharCode(charCode));
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
const decrypted = chars.join('');
|
| 70 |
+
// Validate presence of our prefix
|
| 71 |
+
if (!decrypted.includes("||")) {
|
| 72 |
+
throw new Error("Invalid password: Secure decryption key mismatch.");
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
const parts = decrypted.split("||");
|
| 76 |
+
const salt = parts[0];
|
| 77 |
+
const payload = parts.slice(1).join("||");
|
| 78 |
+
|
| 79 |
+
if (!salt.startsWith("HBT-SECURE-VAULT-")) {
|
| 80 |
+
throw new Error("Corrupted backup data block.");
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
return payload;
|
| 84 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"moduleResolution": "bundler",
|
| 14 |
+
"isolatedModules": true,
|
| 15 |
+
"moduleDetection": "force",
|
| 16 |
+
"allowJs": true,
|
| 17 |
+
"jsx": "react-jsx",
|
| 18 |
+
"paths": {
|
| 19 |
+
"@/*": [
|
| 20 |
+
"./*"
|
| 21 |
+
]
|
| 22 |
+
},
|
| 23 |
+
"allowImportingTsExtensions": true,
|
| 24 |
+
"noEmit": true
|
| 25 |
+
}
|
| 26 |
+
}
|
vite.config.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tailwindcss from '@tailwindcss/vite';
|
| 2 |
+
import react from '@vitejs/plugin-react';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
import {defineConfig} from 'vite';
|
| 5 |
+
|
| 6 |
+
export default defineConfig(() => {
|
| 7 |
+
return {
|
| 8 |
+
plugins: [react(), tailwindcss()],
|
| 9 |
+
resolve: {
|
| 10 |
+
alias: {
|
| 11 |
+
'@': path.resolve(__dirname, '.'),
|
| 12 |
+
},
|
| 13 |
+
},
|
| 14 |
+
server: {
|
| 15 |
+
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
| 16 |
+
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
| 17 |
+
hmr: process.env.DISABLE_HMR !== 'true',
|
| 18 |
+
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
|
| 19 |
+
watch: process.env.DISABLE_HMR === 'true' ? null : {},
|
| 20 |
+
},
|
| 21 |
+
};
|
| 22 |
+
});
|