Compare commits

...

131 Commits

Author SHA1 Message Date
sazzadulalambd
3603f2191c feat: expand RBAC system with granular permissions and update role definitions and UI configuration 2026-05-21 20:22:32 +06:00
sazzadulalambd
c85b609797 feat: add employee management module with roster, search, and filtering to hub details page 2026-05-21 20:16:42 +06:00
sazzadulalambd
916eec0f72 feat: implement automated battery rental invoicing and journal entry tracking with print support 2026-05-21 12:01:23 +06:00
sazzadulalambd
b83325b8e3 refactor: overhaul project structure, update configuration, and improve consistency across admin and investor dashboard components. 2026-05-21 11:40:03 +06:00
sazzadulalambd
7332f85512 refactor: clean up catch block formatting and comment out version display in Sidebar 2026-05-20 14:58:38 +06:00
sazzadulalambd
989221f953 feat: add service center management module with CRUD functionality and status filtering 2026-05-20 14:48:29 +06:00
sazzadulalambd
8669da78d6 refactor: adjust UI state management and OCR locking logic in maintenance page 2026-05-20 14:10:18 +06:00
sazzadulalambd
bb561e493b feat: add AI OCR processing state and conditional UI locking for maintenance records 2026-05-20 13:26:25 +06:00
sazzadulalambd
3933141140 feat: add withdraw category to notifications and improve UI sidebar layout 2026-05-19 21:02:57 +06:00
sazzadulalambd
bcb319ca71 refactor: rename FOCO to FICO model and enhance notification system with new templates, metadata, and category filtering. 2026-05-19 20:53:50 +06:00
sazzadulalambd
9442e64a86 update 2026-05-19 20:45:10 +06:00
sazzadulalambd
233327e488 fix: update notification titles and messages to reflect damage alerts for specific bikes 2026-05-19 20:29:10 +06:00
sazzadulalambd
08958a8722 refactor: update bike detail links to point to admin fleet route instead of public bike view 2026-05-19 20:27:26 +06:00
sazzadulalambd
16c299ae7f feat: add battery and bike maintenance history pages and update navigation links to include source tracking 2026-05-19 20:25:01 +06:00
sazzadulalambd
9126d3dfa2 feat: implement advanced rental transaction history table with filtering, sorting, and pagination in fleet and battery views 2026-05-19 20:01:36 +06:00
sazzadulalambd
8ae1c8316b feat: implement navigation to rental history page for individual assets and create associated detail view route 2026-05-19 19:35:27 +06:00
sazzadulalambd
123ba98c9e feat: add asset management modals for assigning bikes and batteries and confirming unassignments 2026-05-19 19:27:03 +06:00
sazzadulalambd
b1dd4b0683 feat: add animated bike icon to assign bike modal header and fix indentation formatting 2026-05-19 19:10:12 +06:00
sazzadulalambd
7ced7f8ed4 refactor: remove register bike and battery assignment buttons from investor detail page 2026-05-19 19:02:15 +06:00
sazzadulalambd
646068dbe3 refactor: dynamic display of asset details and investment data based on investment type 2026-05-19 18:55:50 +06:00
sazzadulalambd
f0d92f31ff feat: implement dynamic payment tracking with local storage and update transaction UI for battery investments 2026-05-19 18:22:06 +06:00
sazzadulalambd
cd6d6e4386 feat: implement persistent local storage state for batteries, bikes, and investors with dynamic patching logic 2026-05-19 18:11:35 +06:00
sazzadulalambd
2a891df398 feat: add active batteries stat to investor profile and update grid layout for payment selection 2026-05-19 17:33:09 +06:00
sazzadulalambd
be137d65df feat: add battery management functionality with assignment, registration, and investment tracking to investor dashboard 2026-05-19 17:25:32 +06:00
sazzadulalambd
623500d845 feat: implement battery asset management and assignment functionality for investors 2026-05-19 16:46:39 +06:00
sazzadulalambd
04423603c0 refactor: replace detail button with Link for battery navigation and static label 2026-05-19 16:37:08 +06:00
sazzadulalambd
2645aeca30 feat: add investor co-ownership tracking and management fields to battery details view 2026-05-19 16:35:11 +06:00
sazzadulalambd
c0ae111c8c feat: replace monthly rent with daily rent and deposit fields in battery schema and UI 2026-05-19 16:33:08 +06:00
sazzadulalambd
c6a9fd960e refactor: rename investment plans to EV investment plans across UI components 2026-05-19 16:20:31 +06:00
sazzadulalambd
5d1a5454c0 feat: implement battery investment settings tab and configuration component 2026-05-19 16:17:20 +06:00
sazzadulalambd
3297daf124 refactor: simplify payment workflow by removing amount input and update sidebar profile navigation 2026-05-19 16:00:27 +06:00
sazzadulalambd
3edcfbc654 feat: comment out new investment entry points in dashboard and plans pages 2026-05-19 15:12:27 +06:00
sazzadulalambd
5aded5bdc6 feat: implement multi-role dashboard state with hub management, ticketing, and accounting features 2026-05-17 23:50:11 +06:00
sazzadulalambd
f8a745ad42 feat: enhance admin dashboard with real-time telematics, audit logs, and interactive management tools 2026-05-17 23:45:38 +06:00
sazzadulalambd
6870ca6b0f feat: add real-time notification count to sidebar and implement dedicated admin notification management dashboard 2026-05-17 23:26:10 +06:00
sazzadulalambd
aaf91255bb feat: add hub selection to battery form and update data schema 2026-05-17 20:35:10 +06:00
sazzadulalambd
9370b71b25 feat: integrate battery selection and sync battery level from mock data in fleet management form 2026-05-17 20:32:54 +06:00
sazzadulalambd
a4ff86b953 feat: add battery management tab with support for viewing, adding, editing, and deleting batteries in hub dashboard 2026-05-17 20:24:47 +06:00
sazzadulalambd
89300a457e feat: add withdrawal management tab and request processing functionality to accounting page 2026-05-17 20:13:33 +06:00
sazzadulalambd
8f445857a9 feat: enhance maintenance details page with issue history navigation and responsive layout improvements 2026-05-17 19:06:24 +06:00
sazzadulalambd
440a87f0b5 feat: add category-based filtering and enhanced dashboard stats for damage and maintenance records 2026-05-17 15:19:31 +06:00
sazzadulalambd
fb1eff4931 feat: implement maintenance invoice creation flow with dynamic pricing, quantity editing, and PDF breakdown reporting 2026-05-17 00:45:04 +06:00
sazzadulalambd
0274e9a90b feat: enhance maintenance records with structured part management, service cost tracking, and dynamic invoice calculation 2026-05-17 00:23:08 +06:00
sazzadulalambd
48fd93fea8 feat: implement real-time rental map dashboard and integrate location tracking updates into admin modules 2026-05-16 22:34:44 +06:00
sazzadulalambd
1feab1fa23 feat: add live rental tracking map view with GPS coordinates and real-time refresh 2026-05-16 21:47:01 +06:00
sazzadulalambd
4c25990e70 feat: add support for battery swapping status and bike image gallery management in fleet details 2026-05-16 20:54:17 +06:00
sazzadulalambd
de9499b567 feat: enhance bike overview with detailed battery tracking and rental subscription data 2026-05-16 20:44:19 +06:00
sazzadulalambd
ec487f6d27 feat: add hub tracking to damage and maintenance records with selection UI 2026-05-16 20:19:23 +06:00
sazzadulalambd
36b12772b7 feat: include hub information in battery damage and maintenance records 2026-05-16 20:00:28 +06:00
sazzadulalambd
4b1ff96db2 feat: implement PWA install prompt banner for investor dashboard 2026-05-16 19:46:43 +06:00
sazzadulalambd
bd18c265ca feat: add damage and maintenance history tracking with CRUD functionality to battery detail page 2026-05-16 19:33:28 +06:00
sazzadulalambd
ce4bbfaf81 fix: include batteryRentPending in rental interface and sanitize pending rent calculation 2026-05-16 15:28:33 +06:00
sazzadulalambd
adbcded611 feat: add battery rent support and integrate adjusted battery costs into rental billing calculations 2026-05-16 15:09:19 +06:00
sazzadulalambd
21c408f828 feat: integrate battery selection and rental billing calculations into rental details view 2026-05-16 15:09:12 +06:00
sazzadulalambd
1882cfbb91 refactor: remove battery manual entry fields in biker profile and add battery rental history table to rental details 2026-05-16 14:06:33 +06:00
sazzadulalambd
41530a4691 feat: add manual BMS data refresh functionality and expand battery purchase form with accounting fields 2026-05-16 12:36:00 +06:00
sazzadulalambd
e932e6b817 refactor: remove RefreshCw icon and unused BMS refresh functionality from battery detail page 2026-05-16 12:20:18 +06:00
sazzadulalambd
b39f3981fc feat: implement battery editing functionality with modal and state management 2026-05-16 12:19:54 +06:00
sazzadulalambd
f5cd411a05 feat: implement battery management module with list and detail views 2026-05-16 12:10:49 +06:00
sazzadulalambd
62b8d567bd refactor: improve responsive layout and add payment history section to investment details page 2026-05-16 10:45:06 +06:00
sazzadulalambd
d8e82cef19 feat: add payment tracking and manual payment submission to investment details and configure standalone deployment mode 2026-05-16 10:20:12 +06:00
sazzadulalambd
5e59909e8e feat: add payment modal and due amount functionality to investment details page 2026-05-15 22:37:42 +06:00
sazzadulalambd
78a51ca60a feat: replace ROI metric with total bike count and add item counts to investment detail tabs 2026-05-15 19:18:07 +06:00
sazzadulalambd
18b29d535b feat: update investment details page with mock data, navigation enhancements, and refined layout 2026-05-15 18:49:26 +06:00
sazzadulalambd
e25bfa91a5 feat: add notifications page and update navigation to redirect notifications to a dedicated route 2026-05-15 18:25:29 +06:00
sazzadulalambd
ad6d5e26ad feat: implement sidebar notification drawer and update investor page padding layouts 2026-05-15 18:07:15 +06:00
sazzadulalambd
845ae91d64 feat: add InvestorNotification component and integrate it across investor dashboard pages 2026-05-15 17:56:16 +06:00
sazzadulalambd
bb1d4628ee feat: implement radio-based payment selection UI and rename paymentOption to paymentType 2026-05-15 12:25:24 +06:00
sazzadulalambd
cab01a25ac feat: add partial payment support for investments and update mock data fields 2026-05-15 03:10:37 +06:00
sazzadulalambd
44745d0252 refactor: migrate investor dashboard to dedicated subfolder and update navigation paths 2026-05-15 02:47:48 +06:00
sazzadulalambd
0c1a6e3a18 feat: add Investments link to investor sidebar and fix indentation and alignment 2026-05-15 02:39:13 +06:00
sazzadulalambd
dfc69faf48 style: standardize header icons and update sidebar navigation labels and icons 2026-05-15 02:37:31 +06:00
sazzadulalambd
16e08c930a feat: add rental history dashboard for investors and update navigation menu 2026-05-15 02:29:41 +06:00
sazzadulalambd
4909826c24 feat: add document upload state and improve responsive design of investor profile page 2026-05-15 02:16:32 +06:00
sazzadulalambd
918b6c543d refactor: add pagination, filtering, and responsive design improvements to withdrawal request page 2026-05-15 02:02:45 +06:00
sazzadulalambd
12f1e5c445 feat: expand container width and reorganize withdrawal request UI components 2026-05-15 01:53:35 +06:00
sazzadulalambd
775299fc6c refactor: redesign withdrawal page layout and UI components for improved clarity 2026-05-15 01:48:56 +06:00
sazzadulalambd
0ff4fcf351 feat: implement bike-based withdrawal system with bulk selection and auto-withdraw settings 2026-05-15 01:47:10 +06:00
sazzadulalambd
c1ab1eb0a3 feat: implement investor withdrawal workflow and add new navigation routes for investments, plans, and withdrawals 2026-05-15 01:31:21 +06:00
sazzadulalambd
1fd8c5153a feat: implement investor withdrawal flow and add necessary profile, investment, and planning pages 2026-05-15 01:25:48 +06:00
sazzadulalambd
f6b394aded refactor: remove investor portfolio and withdrawal pages and clean up related sidebar and admin navigation links. 2026-05-14 23:15:10 +06:00
sazzadulalambd
38c8461f0d feat: comment out statement link in investor details page 2026-05-14 22:30:13 +06:00
sazzadulalambd
1f595e5d72 feat: comment out edit and delete controls in investor details page 2026-05-14 22:28:46 +06:00
sazzadulalambd
e57118d040 feat: comment out edit and journal tab buttons on investment detail page 2026-05-14 22:26:27 +06:00
sazzadulalambd
d54e270fb4 feat: update investor profile with rental history tab, withdrawal request modal, and refined financial dashboard metrics 2026-05-14 22:25:24 +06:00
sazzadulalambd
3cf729f59c feat: transition from single bank record to multi-account bank support in investor profiles 2026-05-14 20:15:39 +06:00
sazzadulalambd
d9a879f53e feat: add investment success modal and redesign investor bike cards with plan-based styling 2026-05-14 19:49:57 +06:00
sazzadulalambd
456e7200fc feat: implement editable profile sections and update layout in investor details page 2026-05-14 13:54:02 +06:00
sazzadulalambd
92554c177c refactor: simplify investor management UI and remove complex CRUD logic for table view 2026-05-13 15:05:58 +06:00
sazzadulalambd
e89f9319b3 feat: rename Templates tab to Email & SMS Templates 2026-05-13 13:59:39 +06:00
sazzadulalambd
5afe5b13f3 feat: add Email and SMS template management configuration page to admin settings 2026-05-13 02:37:29 +06:00
sazzadulalambd
aa7cb65cf8 feat: add profile image support and upload functionality to biker profiles 2026-05-13 02:18:22 +06:00
sazzadulalambd
5aecfc061b feat: add deposit field and enable inline editing for membership type in biker profile 2026-05-13 02:12:40 +06:00
sazzadulalambd
cc8663a05c feat: extend Biker interface with battery details and refactor plan selection UI 2026-05-13 02:06:28 +06:00
sazzadulalambd
78d1924c2c refactor: implement responsive mobile view and hide add biker button for bikers management page 2026-05-12 20:39:10 +06:00
sazzadulalambd
dfd1918cfb refactor: update rental payment display to show detailed due amount and penalty breakdown 2026-05-12 20:27:46 +06:00
sazzadulalambd
9e907ec6ce feat: add rental payment history section and interface to admin rental details page 2026-05-12 19:59:32 +06:00
sazzadulalambd
8e6eadfac5 feat: add mobile-responsive rental table view and interactive status filter cards with lock/unlock actions 2026-05-12 19:04:26 +06:00
sazzadulalambd
63f689d1da feat: add visibility toggles for app and web in company policy settings and fix routing typo 2026-05-10 18:24:24 +06:00
sazzadulalambd
7f5c7f3f65 feat: add inline editing and EV model selection to plan management components 2026-05-10 17:52:21 +06:00
sazzadulalambd
c8b4f8ff0f feat: modularize admin settings into individual components and standardize swapstation role naming 2026-05-10 17:17:37 +06:00
sazzadulalambd
3d89b38417 feat: replace multi-select dropdowns with custom checkbox-based multi-select menus for plan EV models 2026-05-10 03:41:16 +06:00
sazzadulalambd
100b567e3a feat: add document upload modal and handling logic to rental details page 2026-05-10 03:33:38 +06:00
sazzadulalambd
bf3e1c8931 feat: add damage reporting and in-app SMS functionality to rental management page 2026-05-10 03:31:17 +06:00
sazzadulalambd
a62923f1a1 feat: enable dynamic deposit calculation from rental input or plan settings and add validation for hub selection during creation 2026-05-10 03:24:48 +06:00
sazzadulalambd
4ee6944f9d feat: add EV model selection and deposit tracking to rental management workflow 2026-05-10 03:11:49 +06:00
sazzadulalambd
2e7bf23752 feat: replace hardcoded rental conditions with dynamic plan configurations and update rental creation flow 2026-05-10 02:47:04 +06:00
sazzadulalambd
0d7b684c77 feat: refactor rental plans to use IDs, add support for EV models, and enable dynamic plan creation 2026-05-10 02:27:17 +06:00
sazzadulalambd
70f97b374b feat: introduce rental condition tiers (Premium, Standard, Economy) with dynamic pricing and UI selection 2026-05-10 01:37:43 +06:00
sazzadulalambd
93e1d289ca feat: add pendingRent and pendingRentDays fields to rental interface and update status badge logic 2026-05-10 01:31:22 +06:00
sazzadulalambd
0924d84983 feat: update rental data models, mock data, and auth access controls for enhanced tracking and management 2026-05-10 01:22:17 +06:00
sazzadulalambd
20ce14ae68 refactor: remove redundant penalty input fields from the share event settings form 2026-05-10 00:32:03 +06:00
sazzadulalambd
dcb4dfa581 chore: clarify penalty labels in admin settings rent plans 2026-05-10 00:31:02 +06:00
sazzadulalambd
4f8c7ec53d chore: update plan penalty input labels for clarity in admin settings 2026-05-10 00:27:01 +06:00
sazzadulalambd
d7db85595c feat: add penalty tier fields to rental plans and update settings configuration UI 2026-05-10 00:23:50 +06:00
sazzadulalambd
af2c86d919 feat: add FICO share fields to investment plans and update UI for granular configuration 2026-05-09 23:24:17 +06:00
sazzadulalambd
fd7b1ab824 refactor: reorder return fields in investment plan settings UI 2026-05-09 23:07:27 +06:00
sazzadulalambd
58f54a738f refactor: remove redundant share percentage inputs and summary UI from rental and EV plan settings 2026-05-09 22:59:49 +06:00
sazzadulalambd
25a421e7a7 refactor: update user roles, clean up admin settings UI, and adjust dashboard navigation logic 2026-05-09 22:49:26 +06:00
sazzadulalambd
e8ccfb9879 style: remove numerical prefixes from plan tab labels in admin settings 2026-05-09 17:17:13 +06:00
sazzadulalambd
ecdcbd1f89 feat: add editable description textareas to plan configuration settings 2026-05-09 17:13:17 +06:00
sazzadulalambd
fec4b95e47 feat: add contract duration management to plan settings and update UI controls 2026-05-09 16:34:09 +06:00
sazzadulalambd
f8b4d7e2ea fix: update rental tab ID to rentalType for consistency 2026-05-09 13:18:26 +06:00
sazzadulalambd
53fb6d9e67 chore: remove CLAUDE.md and ignore additional documentation directories 2026-05-09 12:52:40 +06:00
sazzadulalambd
c0916cd3a2 feat: implement role-based access control for KYC workflows and add permissions documentation 2026-05-09 12:51:28 +06:00
sazzadulalambd
4f58fba36d chore: comment out swap-station login option in login page 2026-05-07 16:24:12 +06:00
sazzadulalambd
89ed9975ca feat: enable swap station login, update admin manager credentials, and improve UI labels and navigation 2026-05-07 16:23:27 +06:00
sazzadulalambd
0fe8b7f50f feat: comment out swap-station login option in login page configuration 2026-05-07 16:15:44 +06:00
sazzadulalambd
bbb1514231 refactor: update admin manager role credentials and simplify login password UI 2026-05-07 16:15:18 +06:00
sazzadulalambd
7846cacc9d feat: implement super_admin role, add swap station user, and update login routing logic 2026-05-07 16:14:33 +06:00
sazzadulalambd
9687a71570 feat: implement authentication flow with login page, middleware protection, and session-based role management 2026-05-07 16:08:18 +06:00
71 changed files with 34724 additions and 6378 deletions

6
.gitignore vendored
View File

@@ -47,3 +47,9 @@ next-env.d.ts
**/public/sw.js.map **/public/sw.js.map
**/public/workbox-*.js.map **/public/workbox-*.js.map
**/public/worker-*.js.map **/public/worker-*.js.map
**/docs
**/.docs
**/deploy.zip
**/deploy

View File

@@ -1 +0,0 @@
@AGENTS.md

1
JML Submodule

Submodule JML added at 7332f85512

20
deploy.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
# Build the project
echo "Building project..."
npm run build
# Create a deployment folder
echo "Preparing deployment files..."
mkdir -p deploy
cp -r .next deploy/
cp -r public deploy/
cp server.js deploy/
cp package.json deploy/
cp next.config.ts deploy/
# Optional: Zip the files
echo "Zipping deployment files..."
cd deploy && zip -r ../deploy.zip . && cd ..
echo "Done! Upload 'deploy.zip' to your cPanel directory and follow the guide."

View File

@@ -8,6 +8,7 @@ const withPWA = withPWAInit({
} as any); } as any);
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: 'standalone',
images: { images: {
remotePatterns: [ remotePatterns: [
{ {

19
server.js Normal file
View File

@@ -0,0 +1,19 @@
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
const port = process.env.PORT || 3000
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true)
handle(req, res, parsedUrl)
}).listen(port, (err) => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
})
})

View File

@@ -6,7 +6,8 @@ import {
DollarSign, Calendar, FileText, ArrowDownLeft, ArrowUpRight, Building, DollarSign, Calendar, FileText, ArrowDownLeft, ArrowUpRight, Building,
ChevronLeft, ChevronRight, Wallet, Receipt, BookOpen, PieChart, List, ChevronLeft, ChevronRight, Wallet, Receipt, BookOpen, PieChart, List,
Banknote, Smartphone, Users, Home, Wrench, Printer, FileSpreadsheet, Banknote, Smartphone, Users, Home, Wrench, Printer, FileSpreadsheet,
Filter, ShoppingCart, Tag, Move, Calculator, Save, CreditCard, Bike Filter, ShoppingCart, Tag, Move, Calculator, Save, CreditCard, Bike,
Clock, Check, CheckCircle
} from 'lucide-react'; } from 'lucide-react';
export type AccountType = 'asset' | 'liability' | 'equity' | 'income' | 'expense'; export type AccountType = 'asset' | 'liability' | 'equity' | 'income' | 'expense';
@@ -71,6 +72,20 @@ export interface AccountingTransaction {
createdBy: string; createdBy: string;
} }
export interface WithdrawRequest {
id: string;
investorId: string;
investorName: string;
phone: string;
amount: number;
requestDate: string;
status: 'pending' | 'approved' | 'completed' | 'rejected';
bankName: string;
accountNo: string;
processedDate?: string;
paymentMethod?: string;
}
const defaultAccounts: ChartOfAccount[] = [ const defaultAccounts: ChartOfAccount[] = [
{ id: 'ASSET-001', code: '1000', name: 'Assets', type: 'asset', isActive: true, balance: 0 }, { id: 'ASSET-001', code: '1000', name: 'Assets', type: 'asset', isActive: true, balance: 0 },
{ id: 'ASSET-101', code: '1100', name: 'Cash in Hand', type: 'asset', parentId: 'ASSET-001', isActive: true, balance: 85000 }, { id: 'ASSET-101', code: '1100', name: 'Cash in Hand', type: 'asset', parentId: 'ASSET-001', isActive: true, balance: 85000 },
@@ -223,7 +238,7 @@ function generateAutoJournalEntries(type: TransactionType, amount: number, descr
} }
export default function AccountingPage() { export default function AccountingPage() {
const [activeTab, setActiveTab] = useState<'dashboard' | 'transactions' | 'journal' | 'ledger' | 'accounts'>('dashboard'); const [activeTab, setActiveTab] = useState<'dashboard' | 'transactions' | 'journal' | 'ledger' | 'accounts' | 'withdraw'>('dashboard');
const [transactions, setTransactions] = useState(mockTransactions); const [transactions, setTransactions] = useState(mockTransactions);
const [accounts] = useState(defaultAccounts); const [accounts] = useState(defaultAccounts);
const [journalEntries, setJournalEntries] = useState(mockJournalEntries); const [journalEntries, setJournalEntries] = useState(mockJournalEntries);
@@ -234,6 +249,14 @@ export default function AccountingPage() {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [editingTransaction, setEditingTransaction] = useState<AccountingTransaction | null>(null); const [editingTransaction, setEditingTransaction] = useState<AccountingTransaction | null>(null);
const [viewingTransaction, setViewingTransaction] = useState<AccountingTransaction | null>(null); const [viewingTransaction, setViewingTransaction] = useState<AccountingTransaction | null>(null);
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
const [payNowModal, setPayNowModal] = useState<WithdrawRequest | null>(null);
const [paymentForm, setPaymentForm] = useState({ method: 'bank', reference: '', notes: '', date: new Date().toISOString().split('T')[0] });
const [withdrawRequests, setWithdrawRequests] = useState<WithdrawRequest[]>([
{ id: 'WDR-001', investorId: 'INV-001', investorName: 'Mohammad Islam', phone: '01987654321', amount: 15000, requestDate: '2024-03-20', status: 'pending', bankName: 'City Bank', accountNo: '1234567890' },
{ id: 'WDR-002', investorId: 'INV-002', investorName: 'Rahima Begum', phone: '01876543210', amount: 25000, requestDate: '2024-03-18', status: 'approved', bankName: 'DBBL', accountNo: '9876543210', processedDate: '2024-03-19', paymentMethod: 'bank' },
{ id: 'WDR-003', investorId: 'INV-003', investorName: 'Ahmed Hassan', phone: '01765432109', amount: 8000, requestDate: '2024-03-15', status: 'completed', bankName: 'bKash', accountNo: '01765432109', processedDate: '2024-03-16', paymentMethod: 'mobile' },
]);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10; const itemsPerPage = 10;
@@ -417,7 +440,8 @@ export default function AccountingPage() {
{ id: 'transactions', label: 'Transactions', icon: Receipt }, { id: 'transactions', label: 'Transactions', icon: Receipt },
{ id: 'journal', label: 'Journal', icon: BookOpen }, { id: 'journal', label: 'Journal', icon: BookOpen },
{ id: 'ledger', label: 'Ledger', icon: List }, { id: 'ledger', label: 'Ledger', icon: List },
{ id: 'accounts', label: 'Chart of Accounts', icon: Calculator }, { id: 'accounts', label: 'Accounts', icon: Calculator },
{ id: 'withdraw', label: 'Withdraw', icon: ArrowDownLeft },
]; ];
return ( return (
@@ -453,6 +477,11 @@ export default function AccountingPage() {
> >
<Icon className="w-4 h-4" /> <Icon className="w-4 h-4" />
{tab.label} {tab.label}
{tab.id === 'withdraw' && withdrawRequests.filter(w => w.status === 'pending').length > 0 && (
<span className="ml-1 px-1.5 py-0.5 text-xs font-medium bg-orange-100 text-orange-700 rounded-full">
{withdrawRequests.filter(w => w.status === 'pending').length}
</span>
)}
</button> </button>
); );
})} })}
@@ -547,6 +576,289 @@ export default function AccountingPage() {
{activeTab === 'ledger' && <LedgerView accounts={accounts} journalEntries={journalEntries} dateFrom={dateFrom} dateTo={dateTo} />} {activeTab === 'ledger' && <LedgerView accounts={accounts} journalEntries={journalEntries} dateFrom={dateFrom} dateTo={dateTo} />}
{activeTab === 'accounts' && <AccountsView accounts={accounts} />} {activeTab === 'accounts' && <AccountsView accounts={accounts} />}
{activeTab === 'withdraw' && (
<div className="space-y-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div>
<h2 className="text-xl font-bold text-slate-800">Withdraw Management</h2>
<p className="text-sm text-slate-500">Process investor withdrawal requests</p>
</div>
<button
onClick={() => setShowWithdrawModal(true)}
className="inline-flex items-center gap-2 px-4 py-2.5 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
<Plus className="w-5 h-5" />
<span>New Withdraw Request</span>
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
<div className="bg-orange-50 rounded-xl p-4 border border-orange-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center">
<Clock className="w-5 h-5 text-orange-600" />
</div>
<div>
<p className="text-xl font-bold text-slate-800">{withdrawRequests.filter(w => w.status === 'pending').length}</p>
<p className="text-sm text-slate-500">Pending</p>
</div>
</div>
</div>
<div className="bg-blue-50 rounded-xl p-4 border border-blue-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
<Check className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-xl font-bold text-slate-800">{withdrawRequests.filter(w => w.status === 'approved').length}</p>
<p className="text-sm text-slate-500">Approved</p>
</div>
</div>
</div>
<div className="bg-green-50 rounded-xl p-4 border border-green-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
<DollarSign className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xl font-bold text-slate-800">{withdrawRequests.filter(w => w.status === 'completed').length}</p>
<p className="text-sm text-slate-500">Completed</p>
</div>
</div>
</div>
<div className="bg-purple-50 rounded-xl p-4 border border-purple-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
<TrendingDown className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-xl font-bold text-slate-800">{withdrawRequests.reduce((sum, w) => sum + w.amount, 0).toLocaleString()}</p>
<p className="text-sm text-slate-500">Total Amount</p>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-50 border-b border-slate-100">
<tr>
<th className="px-4 py-3 text-left font-semibold text-slate-600">ID</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Investor</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Phone</th>
<th className="px-4 py-3 text-right font-semibold text-slate-600">Amount</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Bank/Method</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Request Date</th>
<th className="px-4 py-3 text-left font-semibold text-slate-600">Status</th>
<th className="px-4 py-3 text-center font-semibold text-slate-600">Actions</th>
</tr>
</thead>
<tbody>
{withdrawRequests.map((req) => (
<tr key={req.id} className="border-b border-slate-50 hover:bg-slate-50">
<td className="px-4 py-3 font-medium text-slate-800">{req.id}</td>
<td className="px-4 py-3">
<div>
<p className="font-medium text-slate-800">{req.investorName}</p>
<p className="text-xs text-slate-500">{req.investorId}</p>
</div>
</td>
<td className="px-4 py-3 text-slate-600">{req.phone}</td>
<td className="px-4 py-3 text-right font-bold text-slate-800">{req.amount.toLocaleString()}</td>
<td className="px-4 py-3">
<p className="text-slate-600">{req.bankName}</p>
<p className="text-xs text-slate-400">{req.accountNo}</p>
</td>
<td className="px-4 py-3 text-slate-600">{req.requestDate}</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full ${
req.status === 'pending' ? 'bg-orange-100 text-orange-700' :
req.status === 'approved' ? 'bg-blue-100 text-blue-700' :
req.status === 'completed' ? 'bg-green-100 text-green-700' :
'bg-red-100 text-red-700'
}`}>
{req.status === 'pending' && <Clock className="w-3 h-3" />}
{req.status === 'approved' && <Check className="w-3 h-3" />}
{req.status === 'completed' && <CheckCircle className="w-3 h-3" />}
{req.status}
</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center justify-center gap-1">
{req.status === 'pending' && (
<>
<button
onClick={() => setWithdrawRequests(prev => prev.map(w => w.id === req.id ? { ...w, status: 'approved', processedDate: new Date().toISOString().split('T')[0] } : w))}
className="p-1.5 bg-blue-100 text-blue-600 rounded-lg hover:bg-blue-200"
title="Approve"
>
<Check className="w-4 h-4" />
</button>
<button
onClick={() => setWithdrawRequests(prev => prev.map(w => w.id === req.id ? { ...w, status: 'rejected' } : w))}
className="p-1.5 bg-red-100 text-red-600 rounded-lg hover:bg-red-200"
title="Reject"
>
<X className="w-4 h-4" />
</button>
</>
)}
{req.status === 'approved' && (
<button
onClick={() => { setPayNowModal(req); setPaymentForm({ method: 'bank', reference: `PAY-${req.id}`, notes: '', date: new Date().toISOString().split('T')[0] }); }}
className="px-3 py-1.5 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 flex items-center gap-1"
>
<DollarSign className="w-3 h-3" /> Pay Now
</button>
)}
{req.status === 'completed' && (
<div className="flex items-center gap-1">
<span className="text-xs text-green-600 font-medium">Paid</span>
<button onClick={() => window.print()} className="p-1 text-slate-400 hover:text-blue-600" title="Print Invoice">
<Printer className="w-4 h-4" />
</button>
</div>
)}
{req.status === 'rejected' && (
<span className="text-xs text-red-600 font-medium">Rejected</span>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{showWithdrawModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg">
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">New Withdraw Request</h3>
<button onClick={() => setShowWithdrawModal(false)} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Investor ID</label>
<input type="text" placeholder="INV-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Phone</label>
<input type="tel" placeholder="01XXXXXXXXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Investor Name</label>
<input type="text" placeholder="Enter name" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Amount ()</label>
<input type="number" placeholder="0" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Payment Method</label>
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="bank">Bank Transfer</option>
<option value="mobile">Mobile Banking</option>
<option value="cash">Cash</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Bank Name</label>
<input type="text" placeholder="Bank name" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Account Number</label>
<input type="text" placeholder="Account number" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setShowWithdrawModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button onClick={() => setShowWithdrawModal(false)} className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">Submit Request</button>
</div>
</div>
</div>
)}
</div>
)}
{payNowModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">Process Payment</h3>
<button onClick={() => setPayNowModal(null)} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div className="bg-slate-50 p-3 rounded-lg">
<p className="text-sm text-slate-500">Paying to</p>
<p className="font-medium text-slate-800">{payNowModal.investorName}</p>
<p className="text-sm text-slate-600">{payNowModal.bankName} - {payNowModal.accountNo}</p>
</div>
<div className="bg-blue-50 p-3 rounded-lg">
<p className="text-sm text-slate-500">Amount</p>
<p className="text-xl font-bold text-blue-600">{payNowModal.amount.toLocaleString()}</p>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Payment Date</label>
<input type="date" value={paymentForm.date} onChange={(e) => setPaymentForm(p => ({ ...p, date: e.target.value }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Payment Method</label>
<select value={paymentForm.method} onChange={(e) => setPaymentForm(p => ({ ...p, method: e.target.value }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="bank">Bank Transfer</option>
<option value="mobile">Mobile Banking</option>
<option value="cash">Cash</option>
</select>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Reference No.</label>
<input type="text" value={paymentForm.reference} onChange={(e) => setPaymentForm(p => ({ ...p, reference: e.target.value }))} placeholder="e.g. TRX-123456" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Notes (Optional)</label>
<textarea value={paymentForm.notes} onChange={(e) => setPaymentForm(p => ({ ...p, notes: e.target.value }))} placeholder="Add any notes..." className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} />
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setPayNowModal(null)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button onClick={() => {
const newTransaction: AccountingTransaction = {
id: `TXN-${Date.now()}`,
date: paymentForm.date,
type: 'investor_withdraw',
amount: payNowModal.amount,
description: `Investor Withdrawal - ${payNowModal.investorName}`,
beneficiary: payNowModal.investorName,
beneficiaryPhone: payNowModal.phone,
paymentMethod: paymentForm.method as 'cash' | 'bank' | 'mobile',
reference: paymentForm.reference,
notes: paymentForm.notes,
createdAt: new Date().toISOString(),
createdBy: 'Admin'
};
setTransactions(prev => [newTransaction, ...prev]);
setWithdrawRequests(prev => prev.map(w => w.id === payNowModal.id ? { ...w, status: 'completed' as const, processedDate: paymentForm.date, paymentMethod: paymentForm.method } : w));
setPayNowModal(null);
}} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
<Banknote className="w-4 h-4" /> Complete Payment
</button>
</div>
</div>
</div>
)}
<TransactionModal <TransactionModal
isOpen={showModal} isOpen={showModal}
onClose={() => { setShowModal(false); setEditingTransaction(null); }} onClose={() => { setShowModal(false); setEditingTransaction(null); }}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -81,9 +81,23 @@ interface Document {
verifiedAt?: string; verifiedAt?: string;
} }
interface BikerBattery {
id: string;
brand: string;
model: string;
voltage: string;
capacity: string;
health: number;
isExtra: boolean;
isRented: boolean;
rentPrice?: number;
addedAt: string;
}
interface Biker { interface Biker {
id: string; id: string;
name: string; name: string;
profileImage?: string;
email: string; email: string;
phone: string; phone: string;
alternatePhone?: string; alternatePhone?: string;
@@ -140,12 +154,14 @@ interface Biker {
pendingRent?: number; pendingRent?: number;
pendingRentDays?: number; pendingRentDays?: number;
lastRentPaidAt?: string; lastRentPaidAt?: string;
batteries?: BikerBattery[];
} }
const mockBikers: Biker[] = [ const mockBikers: Biker[] = [
{ {
id: 'B001', id: 'B001',
name: 'Rahim Ahmed', name: 'Rahim Ahmed',
profileImage: 'https://picsum.photos/200/200?random=1',
email: 'rahim@email.com', email: 'rahim@email.com',
phone: '01712345678', phone: '01712345678',
alternatePhone: '01912345678', alternatePhone: '01912345678',
@@ -198,11 +214,16 @@ const mockBikers: Biker[] = [
pendingRent: 0, pendingRent: 0,
pendingRentDays: 0, pendingRentDays: 0,
lastRentPaidAt: '2024-03-21', lastRentPaidAt: '2024-03-21',
batteries: [
{ id: 'BAT-001234', brand: 'PowerMax', model: 'PM-60V-30Ah', voltage: '60V', capacity: '30Ah', health: 92, isExtra: false, isRented: false, addedAt: '2024-01-15' },
{ id: 'BAT-001235', brand: 'PowerMax', model: 'PM-60V-30Ah', voltage: '60V', capacity: '30Ah', health: 88, isExtra: true, isRented: true, rentPrice: 30, addedAt: '2024-02-01' },
],
}, },
{ {
id: 'B002', id: 'B002',
name: 'Karim Hasan', name: 'Karim Hasan',
email: 'karim@email.com', email: 'karim@email.com',
profileImage: 'https://picsum.photos/200/200?random=1',
phone: '01712345679', phone: '01712345679',
status: 'active', status: 'active',
createdAt: '2024-02-20', createdAt: '2024-02-20',
@@ -243,10 +264,14 @@ const mockBikers: Biker[] = [
insuranceStatus: 'active', insuranceStatus: 'active',
insuranceExpiry: '2025-02-19', insuranceExpiry: '2025-02-19',
joinedFrom: 'Website', joinedFrom: 'Website',
batteries: [
{ id: 'BAT-002345', brand: 'Yadea', model: 'YA-48V-20Ah', voltage: '48V', capacity: '20Ah', health: 85, isExtra: false, isRented: false, addedAt: '2024-02-20' },
],
}, },
{ {
id: 'B003', id: 'B003',
name: 'Jamal Mahmud', name: 'Jamal Mahmud',
profileImage: 'https://picsum.photos/200/200?random=3',
email: 'jamal@email.com', email: 'jamal@email.com',
phone: '01712345680', phone: '01712345680',
status: 'pending', status: 'pending',
@@ -288,6 +313,7 @@ const mockBikers: Biker[] = [
{ {
id: 'B004', id: 'B004',
name: 'Ali Rahman', name: 'Ali Rahman',
profileImage: 'https://picsum.photos/200/200?random=4',
email: 'ali@email.com', email: 'ali@email.com',
phone: '01712345681', phone: '01712345681',
status: 'active', status: 'active',
@@ -333,6 +359,11 @@ const mockBikers: Biker[] = [
pendingRent: 150, pendingRent: 150,
pendingRentDays: 3, pendingRentDays: 3,
lastRentPaidAt: '2024-03-18', lastRentPaidAt: '2024-03-18',
batteries: [
{ id: 'BAT-003456', brand: 'AIMA', model: 'AIMA-60V-40Ah', voltage: '60V', capacity: '40Ah', health: 95, isExtra: false, isRented: false, addedAt: '2023-12-01' },
{ id: 'BAT-003457', brand: 'AIMA', model: 'AIMA-60V-40Ah', voltage: '60V', capacity: '40Ah', health: 90, isExtra: true, isRented: true, rentPrice: 35, addedAt: '2024-01-15' },
{ id: 'BAT-003458', brand: 'AIMA', model: 'AIMA-60V-40Ah', voltage: '60V', capacity: '40Ah', health: 82, isExtra: true, isRented: false, rentPrice: 35, addedAt: '2024-02-20' },
],
}, },
{ {
id: 'B005', id: 'B005',
@@ -375,6 +406,9 @@ const mockBikers: Biker[] = [
insuranceStatus: 'expired', insuranceStatus: 'expired',
insuranceExpiry: '2024-01-14', insuranceExpiry: '2024-01-14',
joinedFrom: 'App Store', joinedFrom: 'App Store',
batteries: [
{ id: 'BAT-004567', brand: 'Etron', model: 'ET-48V-24Ah', voltage: '48V', capacity: '24Ah', health: 65, isExtra: false, isRented: false, addedAt: '2023-08-15' },
],
}, },
]; ];
@@ -531,12 +565,12 @@ export default function BikersPage() {
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Bikers Management</h1> <h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Bikers Management</h1>
<p className="text-sm text-slate-500 mt-1">Manage registered biker accounts with full details</p> <p className="text-sm text-slate-500 mt-1">Manage registered biker accounts with full details</p>
</div> </div>
<button {/* <button
onClick={handleAddBiker} onClick={handleAddBiker}
className="py-2.5 px-4 bg-accent text-white rounded-lg font-semibold text-sm hover:bg-accent-dark transition-colors flex items-center gap-2" className="py-2.5 px-4 bg-accent text-white rounded-lg font-semibold text-sm hover:bg-accent-dark transition-colors flex items-center gap-2"
> >
<Plus className="w-4 h-4" /> Add New Biker <Plus className="w-4 h-4" /> Add New Biker
</button> </button> */}
</div> </div>
<div className="grid grid-cols-2 lg:grid-cols-6 gap-4 mb-6"> <div className="grid grid-cols-2 lg:grid-cols-6 gap-4 mb-6">
@@ -636,115 +670,200 @@ export default function BikersPage() {
</div> </div>
{viewMode === 'table' ? ( {viewMode === 'table' ? (
<div className="overflow-x-auto"> <>
<table className="w-full"> <div className="hidden md:block overflow-x-auto">
<thead className="bg-slate-50"> <table className="w-full">
<tr> <thead className="bg-slate-50">
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Biker</th> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">License & GPS</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Biker</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Location</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">License & GPS</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Rides & Distance</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Location</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Financial</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Rides & Distance</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Member</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Financial</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">KYC</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Member</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Rent Status</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">KYC</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Rent Status</th>
</tr> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
</thead> </tr>
<tbody className="divide-y divide-slate-50"> </thead>
<tbody className="divide-y divide-slate-50">
{sortedBikers.map(biker => (
<tr key={biker.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3">
<Link href={`/admin/bikers/${biker.id}`} className="flex items-center gap-3">
{biker.profileImage ? (
<img src={biker.profileImage} alt={biker.name} className="w-10 h-10 rounded-full object-cover" />
) : (
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
<span className="text-sm font-bold text-blue-600">{biker.name.charAt(0)}</span>
</div>
)}
<div>
<p className="text-sm font-medium text-slate-700">{biker.name}</p>
<p className="text-xs text-slate-400">{biker.phone}</p>
</div>
</Link>
</td>
<td className="px-4 py-3">
<p className="text-xs text-slate-600 flex items-center gap-1">
<Car className="w-3 h-3" /> {biker.drivingLicense.number || 'No License'}
</p>
<p className="text-xs text-slate-400 flex items-center gap-1">
<Navigation className="w-3 h-3" /> GPS: {biker.gpsDeviceId || 'No GPS'}
</p>
<p className="text-xs text-slate-400">Class: {biker.drivingLicense.class || 'N/A'}</p>
</td>
<td className="px-4 py-3">
<p className="text-sm text-slate-600 text-xs">{biker.location}</p>
</td>
<td className="px-4 py-3">
<p className="text-sm font-medium text-slate-700">{biker.totalRides} rides</p>
<p className="text-xs text-slate-400">{biker.totalDistance.toLocaleString()} km</p>
</td>
<td className="px-4 py-3">
<p className="text-sm font-semibold text-green-600">{biker.totalSpent.toLocaleString()}</p>
<p className="text-xs text-slate-400">Wallet: {biker.walletBalance}</p>
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${membershipColors[biker.membershipType]}`}>
{biker.membershipType}
</span>
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${kycColors[biker.kycStatus]}`}>
{biker.kycStatus === 'verified' && <CheckCircle className="w-3 h-3" />}
{biker.kycStatus === 'pending' && <Clock className="w-3 h-3" />}
{biker.kycStatus === 'rejected' && <XCircle className="w-3 h-3" />}
{biker.kycStatus}
</span>
</td>
<td className="px-4 py-3">
{(biker.pendingRent && biker.pendingRent > 0) ? (
<div>
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-amber-100 text-amber-700">
<AlertTriangle className="w-3 h-3" /> Pending
</span>
<p className="text-xs text-amber-600 mt-1">{biker.pendingRent} ({biker.pendingRentDays}d)</p>
</div>
) : (
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-green-100 text-green-700">
<CheckCircle className="w-3 h-3" /> Clear
</span>
)}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1">
<a href={`tel:${biker.phone}`} className="p-2 hover:bg-green-50 rounded-lg" title="Call">
<PhoneCall className="w-4 h-4 text-green-600" />
</a>
<a href={`sms:${biker.phone}`} className="p-2 hover:bg-blue-50 rounded-lg" title="Message">
<MessageCircle className="w-4 h-4 text-blue-500" />
</a>
<Link href={`/admin/bikers/${biker.id}`} className="p-2 hover:bg-slate-100 rounded-lg" title="View Details">
<Eye className="w-4 h-4 text-blue-500" />
</Link>
<Link href={`/admin/bikers/${biker.id}`} className="p-2 hover:bg-slate-100 rounded-lg" title="Edit">
<Edit className="w-4 h-4 text-slate-400" />
</Link>
<button onClick={() => handleDeleteBiker(biker.id)} className="p-2 hover:bg-red-50 rounded-lg" title="Delete">
<Trash2 className="w-4 h-4 text-red-400" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="md:hidden p-4">
<div className="grid grid-cols-1 gap-4">
{sortedBikers.map(biker => ( {sortedBikers.map(biker => (
<tr key={biker.id} className="hover:bg-slate-50 transition-colors"> <Link key={biker.id} href={`/admin/bikers/${biker.id}`} className="block bg-white rounded-xl border border-slate-200 p-4 hover:shadow-md transition-shadow">
<td className="px-4 py-3"> <div className="flex items-start justify-between mb-3">
<Link href={`/admin/bikers/${biker.id}`} className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center"> {biker.profileImage ? (
<span className="text-sm font-bold text-blue-600">{biker.name.charAt(0)}</span> <img src={biker.profileImage} alt={biker.name} className="w-12 h-12 rounded-full object-cover" />
</div> ) : (
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center">
<span className="text-lg font-bold text-blue-600">{biker.name.charAt(0)}</span>
</div>
)}
<div> <div>
<p className="text-sm font-medium text-slate-700">{biker.name}</p> <p className="font-semibold text-slate-700">{biker.name}</p>
<p className="text-xs text-slate-400">{biker.phone}</p> <p className="text-sm text-slate-400">{biker.phone}</p>
</div> </div>
</Link> </div>
</td> </div>
<td className="px-4 py-3"> <div className="flex flex-wrap gap-2 mb-3">
<p className="text-xs text-slate-600 flex items-center gap-1"> <span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[biker.status]}`}>
<Car className="w-3 h-3" /> {biker.drivingLicense.number || 'No License'} {statusLabels[biker.status] || biker.status}
</p> </span>
<p className="text-xs text-slate-400 flex items-center gap-1"> <span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${kycColors[biker.kycStatus]}`}>
<Navigation className="w-3 h-3" /> GPS: {biker.gpsDeviceId || 'No GPS'} {biker.kycStatus}
</p> </span>
<p className="text-xs text-slate-400">Class: {biker.drivingLicense.class || 'N/A'}</p>
</td>
<td className="px-4 py-3">
<p className="text-sm text-slate-600 text-xs">{biker.location}</p>
</td>
<td className="px-4 py-3">
<p className="text-sm font-medium text-slate-700">{biker.totalRides} rides</p>
<p className="text-xs text-slate-400">{biker.totalDistance.toLocaleString()} km</p>
</td>
<td className="px-4 py-3">
<p className="text-sm font-semibold text-green-600">{biker.totalSpent.toLocaleString()}</p>
<p className="text-xs text-slate-400">Wallet: {biker.walletBalance}</p>
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${membershipColors[biker.membershipType]}`}> <span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${membershipColors[biker.membershipType]}`}>
{biker.membershipType} {biker.membershipType}
</span> </span>
</td> {biker.rating > 0 && (
<td className="px-4 py-3"> <span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-amber-100 text-amber-700">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${kycColors[biker.kycStatus]}`}> {biker.rating}
{biker.kycStatus === 'verified' && <CheckCircle className="w-3 h-3" />}
{biker.kycStatus === 'pending' && <Clock className="w-3 h-3" />}
{biker.kycStatus === 'rejected' && <XCircle className="w-3 h-3" />}
{biker.kycStatus}
</span>
</td>
<td className="px-4 py-3">
{(biker.pendingRent && biker.pendingRent > 0) ? (
<div>
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-amber-100 text-amber-700">
<AlertTriangle className="w-3 h-3" /> Pending
</span>
<p className="text-xs text-amber-600 mt-1">{biker.pendingRent} ({biker.pendingRentDays}d)</p>
</div>
) : (
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-green-100 text-green-700">
<CheckCircle className="w-3 h-3" /> Clear
</span> </span>
)} )}
</td> </div>
<td className="px-4 py-3"> <div className="grid grid-cols-2 gap-2 text-xs mb-3">
<div className="flex items-center gap-1"> <div className="bg-slate-50 rounded-lg p-2">
<a href={`tel:${biker.phone}`} className="p-2 hover:bg-green-50 rounded-lg" title="Call"> <p className="text-slate-400">Total Rides</p>
<PhoneCall className="w-4 h-4 text-green-600" /> <p className="font-medium text-slate-700">{biker.totalRides}</p>
</a>
<a href={`sms:${biker.phone}`} className="p-2 hover:bg-blue-50 rounded-lg" title="Message">
<MessageCircle className="w-4 h-4 text-blue-500" />
</a>
<button onClick={() => handleViewDetails(biker)} className="p-2 hover:bg-slate-100 rounded-lg" title="View Details">
<Eye className="w-4 h-4 text-blue-500" />
</button>
<button onClick={() => handleEditBiker(biker)} className="p-2 hover:bg-slate-100 rounded-lg" title="Edit">
<Edit className="w-4 h-4 text-slate-400" />
</button>
<button onClick={() => handleDeleteBiker(biker.id)} className="p-2 hover:bg-red-50 rounded-lg" title="Delete">
<Trash2 className="w-4 h-4 text-red-400" />
</button>
</div> </div>
</td> <div className="bg-slate-50 rounded-lg p-2">
</tr> <p className="text-slate-400">Distance</p>
<p className="font-medium text-slate-700">{biker.totalDistance.toLocaleString()} km</p>
</div>
<div className="bg-slate-50 rounded-lg p-2">
<p className="text-slate-400">Active Rentals</p>
<p className="font-medium text-slate-700">-</p>
</div>
<div className="bg-slate-50 rounded-lg p-2">
<p className="text-slate-400">Completed</p>
<p className="font-medium text-slate-700">{biker.totalRides}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-xs mb-3">
<div className="bg-slate-50 rounded-lg p-2">
<p className="text-slate-400">License</p>
<p className="font-medium text-slate-700 truncate">{biker.drivingLicense.number || 'N/A'}</p>
</div>
<div className="bg-slate-50 rounded-lg p-2">
<p className="text-slate-400">Location</p>
<p className="font-medium text-slate-700 truncate">{biker.location}</p>
</div>
</div>
<div className="flex items-center justify-between pt-3 border-t border-slate-100">
<div>
<p className="text-sm font-semibold text-green-600">{biker.walletBalance.toLocaleString()}</p>
<p className="text-xs text-slate-400">Wallet</p>
</div>
<span className="text-xs text-blue-600 font-medium">View Details </span>
</div>
</Link>
))} ))}
</tbody> </div>
</table> </div>
</div> </>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
{sortedBikers.map(biker => ( {sortedBikers.map(biker => (
<Link key={biker.id} href={`/admin/bikers/${biker.id}`} className="block bg-white rounded-xl border border-slate-200 p-4 hover:shadow-md transition-shadow"> <Link key={biker.id} href={`/admin/bikers/${biker.id}`} className="block bg-white rounded-xl border border-slate-200 p-4 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center"> {biker.profileImage ? (
<span className="text-lg font-bold text-blue-600">{biker.name.charAt(0)}</span> <img src={biker.profileImage} alt={biker.name} className="w-12 h-12 rounded-full object-cover" />
</div> ) : (
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center">
<span className="text-lg font-bold text-blue-600">{biker.name.charAt(0)}</span>
</div>
)}
<div> <div>
<p className="font-semibold text-slate-700">{biker.name}</p> <p className="font-semibold text-slate-700">{biker.name}</p>
<p className="text-sm text-slate-400">{biker.phone}</p> <p className="text-sm text-slate-400">{biker.phone}</p>
@@ -1161,9 +1280,13 @@ export default function BikersPage() {
<div className="grid grid-cols-2 gap-6"> <div className="grid grid-cols-2 gap-6">
<div> <div>
<div className="flex items-center gap-4 mb-4"> <div className="flex items-center gap-4 mb-4">
<div className="w-20 h-20 rounded-full bg-blue-100 flex items-center justify-center"> {selectedBiker.profileImage ? (
<span className="text-3xl font-bold text-blue-600">{selectedBiker.name.charAt(0)}</span> <img src={selectedBiker.profileImage} alt={selectedBiker.name} className="w-20 h-20 rounded-full object-cover" />
</div> ) : (
<div className="w-20 h-20 rounded-full bg-blue-100 flex items-center justify-center">
<span className="text-3xl font-bold text-blue-600">{selectedBiker.name.charAt(0)}</span>
</div>
)}
<div> <div>
<h3 className="text-xl font-bold text-slate-800">{selectedBiker.name}</h3> <h3 className="text-xl font-bold text-slate-800">{selectedBiker.name}</h3>
<p className="text-sm text-slate-500">ID: {selectedBiker.id}</p> <p className="text-sm text-slate-500">ID: {selectedBiker.id}</p>

View File

@@ -9,7 +9,8 @@ import {
GaugeCircle, CheckCircle, AlertTriangle, Activity, Award, TrendingUp, Wallet, GaugeCircle, CheckCircle, AlertTriangle, Activity, Award, TrendingUp, Wallet,
MoreHorizontal, Map, Navigation2, Satellite, FileCheck, FileX, Clock3, MoreHorizontal, Map, Navigation2, Satellite, FileCheck, FileX, Clock3,
History, CreditCard, User2, Phone, Mail, MapPinned, ExternalLink, Plus, History, CreditCard, User2, Phone, Mail, MapPinned, ExternalLink, Plus,
AlertCircle, Image as ImageIcon AlertCircle, Image as ImageIcon, Camera, Search, ArrowUpDown, ChevronLeft,
ChevronRight, RefreshCw, CheckCircle2, XCircle
} from 'lucide-react'; } from 'lucide-react';
interface GPSDevice { interface GPSDevice {
@@ -74,6 +75,8 @@ interface DamageRecord {
estimatedCost?: number; estimatedCost?: number;
actualCost?: number; actualCost?: number;
status: 'reported' | 'under_repair' | 'repaired' | 'claim_rejected'; status: 'reported' | 'under_repair' | 'repaired' | 'claim_rejected';
hubId?: string;
hubName?: string;
images?: string[]; images?: string[];
billImage?: string; billImage?: string;
resolvedAt?: string; resolvedAt?: string;
@@ -89,9 +92,26 @@ interface MaintenanceRecord {
cost: number; cost: number;
nextDueDate?: string; nextDueDate?: string;
status: 'scheduled' | 'in_progress' | 'completed'; status: 'scheduled' | 'in_progress' | 'completed';
hubId?: string;
hubName?: string;
notes?: string; notes?: string;
} }
interface BatteryHistory {
id: string;
batteryId: string;
brand: string;
model: string;
serialNumber: string;
assignedDate: string;
returnedDate?: string;
swappedToBatteryId?: string;
status: 'active' | 'returned' | 'swapped';
socStart: number;
socEnd?: number;
monthlyRent?: number;
}
interface Bike { interface Bike {
id: string; id: string;
model: string; model: string;
@@ -100,10 +120,19 @@ interface Bike {
plateNumber: string; plateNumber: string;
status: 'available' | 'rented' | 'maintenance' | 'retired'; status: 'available' | 'rented' | 'maintenance' | 'retired';
batteryLevel: number; batteryLevel: number;
location?: string; // deprecated - use hubId/hubName currentBatteryId?: string;
currentBatteryBrand?: string;
currentBatteryModel?: string;
location?: string;
hubId?: string; hubId?: string;
hubName?: string; hubName?: string;
assignedTo?: string; assignedTo?: string;
renterPhone?: string;
renterNid?: string;
rentalStartDate?: string;
subscriptionType?: 'daily' | 'weekly' | 'monthly';
weeklyRent?: number;
monthlyRent?: number;
investorId?: string; investorId?: string;
investorName?: string; investorName?: string;
purchaseDate?: string; purchaseDate?: string;
@@ -124,11 +153,18 @@ interface Bike {
assignmentHistory?: BikeAssignment[]; assignmentHistory?: BikeAssignment[];
damageHistory?: DamageRecord[]; damageHistory?: DamageRecord[];
maintenanceHistory?: MaintenanceRecord[]; maintenanceHistory?: MaintenanceRecord[];
batteryHistory?: BatteryHistory[];
bikeImages?: {
front?: string;
back?: string;
left?: string;
right?: string;
};
} }
const mockBikes: Bike[] = [ const mockBikes: Bike[] = [
{ {
id: 'EV001', model: 'Etron ET50', brand: 'Etron', image: '', plateNumber: 'Dhaka Metro Cha-A-1234', status: 'rented', batteryLevel: 78, location: 'Gulshan 1', assignedTo: 'Rahim Ahmed', hubId: 'HUB-001', hubName: 'JAIBEN Head Office', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-01-15', purchasePrice: 125000, currentRent: 350, totalRides: 156, totalDistance: 2340, totalEarnings: 54600, lastService: '2024-03-01', nextService: '2024-04-01', insuranceExpiry: '2025-01-15', registrationExpiry: '2026-01-15', id: 'EV001', model: 'Etron ET50', brand: 'Etron', image: '', plateNumber: 'Dhaka Metro Cha-A-1234', status: 'rented', batteryLevel: 78, currentBatteryId: 'BAT-001', currentBatteryBrand: 'EVE Energy', currentBatteryModel: 'Li-Ion 60V50Ah', location: 'Gulshan 1', assignedTo: 'Rahim Ahmed', renterPhone: '01712345678', renterNid: '1234567890', rentalStartDate: '2024-03-01', subscriptionType: 'weekly', weeklyRent: 2400, hubId: 'HUB-001', hubName: 'JAIBEN Head Office', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-01-15', purchasePrice: 125000, currentRent: 350, totalRides: 156, totalDistance: 2340, totalEarnings: 54600, lastService: '2024-03-01', nextService: '2024-04-01', insuranceExpiry: '2025-01-15', registrationExpiry: '2026-01-15',
gpsDevice: { id: 'GPS001', phone: '01712345601', imei: '861234567890123', lastActive: '2024-03-21 14:30', signal: 85, battery: 72 }, gpsDevice: { id: 'GPS001', phone: '01712345601', imei: '861234567890123', lastActive: '2024-03-21 14:30', signal: 85, battery: 72 },
documents: [ documents: [
{ type: 'registration', number: 'REG-EV001-2024', issueDate: '2024-01-15', expiryDate: '2026-01-15', verified: true }, { type: 'registration', number: 'REG-EV001-2024', issueDate: '2024-01-15', expiryDate: '2026-01-15', verified: true },
@@ -149,14 +185,36 @@ const mockBikes: Bike[] = [
{ id: 'A005', action: 'Insurance Renewed', details: 'Insurance renewed for 1 year', date: '2024-01-15', by: 'Admin' }, { id: 'A005', action: 'Insurance Renewed', details: 'Insurance renewed for 1 year', date: '2024-01-15', by: 'Admin' },
], ],
damageHistory: [ damageHistory: [
{ id: 'DMG001', date: '2024-02-10', type: 'accident', description: 'Minor collision at Mirpur intersection', reportedBy: 'Jamal Khan', reportedAt: '2024-02-10 14:30', estimatedCost: 5000, actualCost: 4500, status: 'repaired', resolvedAt: '2024-02-15' }, { id: 'DMG001', date: '2024-02-10', type: 'accident', description: 'Minor collision at Mirpur intersection', reportedBy: 'Jamal Khan', reportedAt: '2024-02-10 14:30', estimatedCost: 5000, actualCost: 4500, status: 'repaired', resolvedAt: '2024-02-15', hubId: 'HUB-001', hubName: 'Gulshan Hub' },
{ id: 'DMG002', date: '2024-03-15', type: 'wear_tear', description: 'Front tire wear - replaced', reportedBy: 'Rahim Ahmed', reportedAt: '2024-03-15 09:00', estimatedCost: 2500, actualCost: 2200, status: 'repaired', resolvedAt: '2024-03-16' }, { id: 'DMG002', date: '2024-03-15', type: 'wear_tear', description: 'Front tire wear - replaced', reportedBy: 'Rahim Ahmed', reportedAt: '2024-03-15 09:00', estimatedCost: 2500, actualCost: 2200, status: 'repaired', resolvedAt: '2024-03-16', hubId: 'HUB-002', hubName: 'Banani Hub' },
], ],
maintenanceHistory: [ maintenanceHistory: [
{ id: 'MNT001', date: '2024-03-01', type: 'routine', description: 'Full service - oil change, brake check, tire rotation', performedBy: 'Service Center', cost: 1500, nextDueDate: '2024-04-01', status: 'completed' }, { id: 'MNT001', date: '2024-03-01', type: 'routine', description: 'Full service - oil change, brake check, tire rotation', performedBy: 'Service Center', cost: 1500, nextDueDate: '2024-04-01', status: 'completed', hubId: 'HUB-001', hubName: 'Gulshan Hub' },
{ id: 'MNT002', date: '2024-02-15', type: 'battery', description: 'Battery health check and terminal cleaning', performedBy: 'Service Center', cost: 500, nextDueDate: '2024-05-15', status: 'completed' }, { id: 'MNT002', date: '2024-02-15', type: 'battery', description: 'Battery health check and terminal cleaning', performedBy: 'Service Center', cost: 500, nextDueDate: '2024-05-15', status: 'completed', hubId: 'HUB-003', hubName: 'Uttara Hub' },
{ id: 'MNT003', date: '2024-01-20', type: 'tire', description: 'Tire pressure check and inflation', performedBy: 'Service Center', cost: 300, nextDueDate: '2024-04-20', status: 'completed' }, { id: 'MNT003', date: '2024-01-20', type: 'tire', description: 'Tire pressure check and inflation', performedBy: 'Service Center', cost: 300, nextDueDate: '2024-04-20', status: 'completed', hubId: 'HUB-004', hubName: 'Mirpur Hub' },
] ],
batteryHistory: [
{ id: 'BH001', batteryId: 'BAT-001', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', serialNumber: 'SN-2024-00001', assignedDate: '2024-03-15', status: 'active', socStart: 85, monthlyRent: 1500 },
{ id: 'BH002', batteryId: 'BAT-005', brand: 'Samsung SDI', model: 'Li-Ion 60V45Ah', serialNumber: 'SN-2023-00045', assignedDate: '2024-01-10', returnedDate: '2024-03-14', status: 'returned', socStart: 80, socEnd: 45, monthlyRent: 1200 },
{ id: 'BH003', batteryId: 'BAT-008', brand: 'LG Chem', model: 'Li-Ion 48V40Ah', serialNumber: 'SN-2023-00012', assignedDate: '2023-11-05', returnedDate: '2024-01-09', status: 'swapped', socStart: 90, socEnd: 60, monthlyRent: 1000 },
{ id: 'BH004', batteryId: 'BAT-003', brand: 'Panasonic', model: 'Li-Ion 60V50Ah', serialNumber: 'SN-2023-00078', assignedDate: '2023-08-20', returnedDate: '2023-11-04', status: 'swapped', socStart: 88, socEnd: 55, monthlyRent: 1500 },
{ id: 'BH005', batteryId: 'BAT-002', brand: 'Sony', model: 'Li-Ion 48V35Ah', serialNumber: 'SN-2023-00034', assignedDate: '2023-05-15', returnedDate: '2023-08-19', status: 'returned', socStart: 75, socEnd: 40, monthlyRent: 900 },
{ id: 'BH006', batteryId: 'BAT-012', brand: 'BYD', model: 'LiFePO4 60V40Ah', serialNumber: 'SN-2023-00056', assignedDate: '2024-04-20', returnedDate: '2024-06-15', status: 'returned', socStart: 92, socEnd: 35, monthlyRent: 1300 },
{ id: 'BH007', batteryId: 'BAT-015', brand: 'CATL', model: 'Li-Ion 72V50Ah', serialNumber: 'SN-2024-00089', assignedDate: '2024-06-20', status: 'active', socStart: 88, monthlyRent: 1800 },
{ id: 'BH008', batteryId: 'BAT-009', brand: 'Tongsheng', model: 'Li-Ion 48V45Ah', serialNumber: 'SN-2023-00023', assignedDate: '2023-02-10', returnedDate: '2023-05-14', status: 'returned', socStart: 82, socEnd: 50, monthlyRent: 1100 },
{ id: 'BH009', batteryId: 'BAT-011', brand: 'Binek', model: 'Li-Ion 60V48Ah', serialNumber: 'SN-2023-00067', assignedDate: '2024-02-01', returnedDate: '2024-04-18', status: 'returned', socStart: 78, socEnd: 42, monthlyRent: 1400 },
{ id: 'BH010', batteryId: 'BAT-007', brand: 'Kexin', model: 'Li-Ion 48V36Ah', serialNumber: 'SN-2022-00045', assignedDate: '2022-12-05', returnedDate: '2023-02-08', status: 'returned', socStart: 85, socEnd: 55, monthlyRent: 850 },
{ id: 'BH011', batteryId: 'BAT-004', brand: 'Faraday', model: 'LiFePO4 48V42Ah', serialNumber: 'SN-2022-00089', assignedDate: '2022-09-15', returnedDate: '2022-12-04', status: 'returned', socStart: 90, socEnd: 48, monthlyRent: 1200 },
{ id: 'BH012', batteryId: 'BAT-006', brand: 'Reliance', model: 'Lead Acid 48V32Ah', serialNumber: 'SN-2022-00034', assignedDate: '2022-06-20', returnedDate: '2022-09-14', status: 'returned', socStart: 95, socEnd: 30, monthlyRent: 600 },
{ id: 'BH013', batteryId: 'BAT-020', brand: 'Maxell', model: 'Li-Ion 60V45Ah', serialNumber: 'SN-2024-00123', assignedDate: '2024-07-10', swappedToBatteryId: 'BAT-025', status: 'swapped', socStart: 75, socEnd: 65, monthlyRent: 1400 },
{ id: 'BH014', batteryId: 'BAT-018', brand: 'Nikola', model: 'LiFePO4 48V40Ah', serialNumber: 'SN-2023-00078', assignedDate: '2024-05-05', swappedToBatteryId: 'BAT-022', status: 'swapped', socStart: 82, socEnd: 55, monthlyRent: 1150 },
],
bikeImages: {
front: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop',
back: 'https://images.unsplash.com/photo-1591637333184-19aa84de3fbd?w=400&h=300&fit=crop',
left: 'https://images.unsplash.com/photo-1622185135505-2d795043906a?w=400&h=300&fit=crop',
right: 'https://images.unsplash.com/photo-1609630875171-b1321377ee53?w=400&h=300&fit=crop',
}
}, },
{ {
id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', image: '', plateNumber: 'Dhaka Metro Cha-A-5678', status: 'available', batteryLevel: 95, location: 'Banani', hubId: 'HUB-002', hubName: 'Banani Hub', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-02-01', purchasePrice: 118000, totalRides: 89, totalDistance: 1567, totalEarnings: 31150, lastService: '2024-03-15', nextService: '2024-04-15', insuranceExpiry: '2025-02-01', registrationExpiry: '2026-02-01', id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', image: '', plateNumber: 'Dhaka Metro Cha-A-5678', status: 'available', batteryLevel: 95, location: 'Banani', hubId: 'HUB-002', hubName: 'Banani Hub', investorId: 'inv1', investorName: 'Mr. Hasan (Investor)', purchaseDate: '2024-02-01', purchasePrice: 118000, totalRides: 89, totalDistance: 1567, totalEarnings: 31150, lastService: '2024-03-15', nextService: '2024-04-15', insuranceExpiry: '2025-02-01', registrationExpiry: '2026-02-01',
@@ -258,6 +316,13 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
{ value: 'other', label: 'Other' }, { value: 'other', label: 'Other' },
]; ];
const mockHubs = [
{ id: 'HUB-001', name: 'Gulshan Hub' },
{ id: 'HUB-002', name: 'Banani Hub' },
{ id: 'HUB-003', name: 'Uttara Hub' },
{ id: 'HUB-004', name: 'Mirpur Hub' },
];
const handleAddDamage = (damage: DamageRecord) => { const handleAddDamage = (damage: DamageRecord) => {
setBikes(bikes.map(b => { setBikes(bikes.map(b => {
if (b.id === bike.id) { if (b.id === bike.id) {
@@ -415,6 +480,7 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Date</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Date</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Description</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Description</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Hub</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Reported By</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Reported By</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Est. Cost</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Est. Cost</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Actual Cost</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Actual Cost</th>
@@ -430,14 +496,15 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
<span className="text-sm text-slate-700 capitalize">{damage.type.replace('_', ' ')}</span> <span className="text-sm text-slate-700 capitalize">{damage.type.replace('_', ' ')}</span>
</td> </td>
<td className="px-4 py-3 text-sm text-slate-600 max-w-xs truncate">{damage.description}</td> <td className="px-4 py-3 text-sm text-slate-600 max-w-xs truncate">{damage.description}</td>
<td className="px-4 py-3 text-sm text-slate-600">{damage.hubName || '-'}</td>
<td className="px-4 py-3 text-sm text-slate-600">{damage.reportedBy}</td> <td className="px-4 py-3 text-sm text-slate-600">{damage.reportedBy}</td>
<td className="px-4 py-3 text-sm text-slate-600">{damage.estimatedCost || 0}</td> <td className="px-4 py-3 text-sm text-slate-600">{damage.estimatedCost || 0}</td>
<td className="px-4 py-3 text-sm font-medium text-slate-700">{damage.actualCost || '-'}</td> <td className="px-4 py-3 text-sm font-medium text-slate-700">{damage.actualCost || '-'}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${damage.status === 'repaired' ? 'bg-green-100 text-green-700' : <span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${damage.status === 'repaired' ? 'bg-green-100 text-green-700' :
damage.status === 'under_repair' ? 'bg-amber-100 text-amber-700' : damage.status === 'under_repair' ? 'bg-amber-100 text-amber-700' :
damage.status === 'claim_rejected' ? 'bg-red-100 text-red-700' : damage.status === 'claim_rejected' ? 'bg-red-100 text-red-700' :
'bg-slate-100 text-slate-700' 'bg-slate-100 text-slate-700'
}`}> }`}>
{damage.status.replace('_', ' ')} {damage.status.replace('_', ' ')}
</span> </span>
@@ -498,6 +565,7 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Date</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Date</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Description</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Description</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Hub</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Performed By</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Performed By</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Cost</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Cost</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Next Due</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Next Due</th>
@@ -513,13 +581,14 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
<span className="text-sm text-slate-700 capitalize">{maintenance.type}</span> <span className="text-sm text-slate-700 capitalize">{maintenance.type}</span>
</td> </td>
<td className="px-4 py-3 text-sm text-slate-600 max-w-xs truncate">{maintenance.description}</td> <td className="px-4 py-3 text-sm text-slate-600 max-w-xs truncate">{maintenance.description}</td>
<td className="px-4 py-3 text-sm text-slate-600">{maintenance.hubName || '-'}</td>
<td className="px-4 py-3 text-sm text-slate-600">{maintenance.performedBy}</td> <td className="px-4 py-3 text-sm text-slate-600">{maintenance.performedBy}</td>
<td className="px-4 py-3 text-sm font-medium text-slate-700">{maintenance.cost}</td> <td className="px-4 py-3 text-sm font-medium text-slate-700">{maintenance.cost}</td>
<td className="px-4 py-3 text-sm text-slate-600">{maintenance.nextDueDate || '-'}</td> <td className="px-4 py-3 text-sm text-slate-600">{maintenance.nextDueDate || '-'}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${maintenance.status === 'completed' ? 'bg-green-100 text-green-700' : <span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${maintenance.status === 'completed' ? 'bg-green-100 text-green-700' :
maintenance.status === 'in_progress' ? 'bg-amber-100 text-amber-700' : maintenance.status === 'in_progress' ? 'bg-amber-100 text-amber-700' :
'bg-slate-100 text-slate-700' 'bg-slate-100 text-slate-700'
}`}> }`}>
{maintenance.status.replace('_', ' ')} {maintenance.status.replace('_', ' ')}
</span> </span>
@@ -591,6 +660,15 @@ export default function FleetDetailPage({ params }: { params: Promise<{ id: stri
} }
function OverviewTab({ bike }: { bike: Bike }) { function OverviewTab({ bike }: { bike: Bike }) {
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 5;
const batteryHistory = bike.batteryHistory || [];
const totalPages = Math.ceil(batteryHistory.length / itemsPerPage);
const paginatedHistory = batteryHistory.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100"> <div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
@@ -614,20 +692,38 @@ function OverviewTab({ bike }: { bike: Bike }) {
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div className="bg-slate-50 rounded-lg p-3"> <div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Plate Number</p> <p className="text-xs text-slate-500">Bike</p>
<p className="font-semibold text-slate-700">{bike.plateNumber}</p> <p className="font-semibold text-slate-700">{bike.brand} {bike.model}</p>
<p className="text-xs text-slate-500 mt-1">{bike.plateNumber}</p>
</div> </div>
<div className="bg-slate-50 rounded-lg p-3"> <div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Battery</p> <p className="text-xs text-slate-500">Battery</p>
<p className={`font-semibold ${bike.batteryLevel > 50 ? 'text-green-600' : bike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>{bike.batteryLevel}%</p> <p className={`font-semibold text-lg ${bike.batteryLevel > 50 ? 'text-green-600' : bike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>{bike.batteryLevel}%</p>
{bike.currentBatteryId && (
<p className="text-xs font-medium text-slate-700 mt-1">{bike.currentBatteryId}</p>
)}
{bike.currentBatteryBrand && (
<p className="text-xs text-slate-500">{bike.currentBatteryBrand} {bike.currentBatteryModel}</p>
)}
</div> </div>
<div className="bg-slate-50 rounded-lg p-3"> <div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Current Renter</p> <p className="text-xs text-slate-500">Current Renter</p>
<p className="font-semibold text-slate-700">{bike.assignedTo || 'Available'}</p> <p className="font-semibold text-slate-700">{bike.assignedTo || 'Available'}</p>
{bike.renterPhone && <p className="text-xs text-slate-500 mt-1">{bike.renterPhone}</p>}
{bike.rentalStartDate && <p className="text-xs text-slate-400 mt-1">Since: {bike.rentalStartDate}</p>}
</div> </div>
<div className="bg-slate-50 rounded-lg p-3"> <div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Daily Rate</p> <p className="text-xs text-slate-500">Subscription</p>
<p className="font-semibold text-green-600">{bike.currentRent || 0}</p> <p className="font-semibold text-slate-700 capitalize">{bike.subscriptionType || 'Daily'}</p>
{bike.subscriptionType === 'weekly' && bike.weeklyRent && (
<p className="text-xs text-green-600 mt-1">{bike.weeklyRent}/week</p>
)}
{bike.subscriptionType === 'monthly' && bike.monthlyRent && (
<p className="text-xs text-green-600 mt-1">{bike.monthlyRent}/month</p>
)}
{bike.subscriptionType === 'daily' && (
<p className="text-xs text-green-600 mt-1">{bike.currentRent || 0}/day</p>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -681,6 +777,91 @@ function OverviewTab({ bike }: { bike: Bike }) {
</div> </div>
)} )}
</div> </div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-4">Battery History</h3>
{batteryHistory.length > 0 ? (
<>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Battery ID</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Brand/Model</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Assigned Date</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Returned/Swapped</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Start SOC</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">End SOC</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Monthly Rent</th>
<th className="px-3 py-2 text-left text-xs font-semibold text-slate-500">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedHistory.map(bh => (
<tr key={bh.id} className="hover:bg-slate-50">
<td className="px-3 py-2 text-sm text-slate-700 font-medium">{bh.batteryId}</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.brand} {bh.model}</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.assignedDate}</td>
<td className="px-3 py-2 text-sm text-slate-600">
{bh.status === 'swapped' ? (
<>
<span className="text-blue-600 block">Swapped to {bh.swappedToBatteryId}</span>
<span className="text-blue-600 text-[10px] block opacity-75">{bh.returnedDate}</span>
</>
) : bh.returnedDate || '-'}
</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.socStart}%</td>
<td className="px-3 py-2 text-sm text-slate-600">{bh.socEnd ? `${bh.socEnd}%` : '-'}</td>
<td className="px-3 py-2 text-sm text-green-600">{bh.monthlyRent}</td>
<td className="px-3 py-2">
<span className={`text-xs font-medium px-2 py-1 rounded-full ${bh.status === 'active' ? 'bg-green-100 text-green-700' : bh.status === 'swapped' ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-500'}`}>
{bh.status === 'active' ? 'Active' : bh.status === 'swapped' ? 'Swapped' : 'Returned'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{totalPages > 1 && (
<div className="flex items-center justify-between mt-4 pt-4 border-t border-slate-100">
<p className="text-sm text-slate-500">
Showing {(currentPage - 1) * itemsPerPage + 1} to {Math.min(currentPage * itemsPerPage, batteryHistory.length)} of {batteryHistory.length} batteries
</p>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-3 py-1 text-sm border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`px-3 py-1 text-sm rounded-lg ${currentPage === page ? 'bg-accent text-white' : 'border border-slate-200 hover:bg-slate-50'}`}
>
{page}
</button>
))}
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="px-3 py-1 text-sm border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
)}
</>
) : (
<div className="text-center py-8 text-slate-500">
No battery history found
</div>
)}
</div>
</div> </div>
); );
} }
@@ -768,9 +949,50 @@ function GPSTab({ bike }: { bike: Bike }) {
function DocumentsTab({ bike }: { bike: Bike }) { function DocumentsTab({ bike }: { bike: Bike }) {
const docs = bike.documents || []; const docs = bike.documents || [];
const [images, setImages] = useState(bike.bikeImages || { front: '', back: '', left: '', right: '' });
const [uploading, setUploading] = useState<string | null>(null);
const handleImageUpload = (view: 'front' | 'back' | 'left' | 'right', e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setUploading(view);
const reader = new FileReader();
reader.onloadend = () => {
setImages(prev => ({ ...prev, [view]: reader.result as string }));
setUploading(null);
};
reader.readAsDataURL(file);
}
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-4">Bike Images</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{(['front', 'back', 'left', 'right'] as const).map(view => (
<div key={view} className="space-y-2">
<p className="text-xs font-medium text-slate-500 capitalize">{view} View</p>
<div className="relative aspect-video bg-slate-100 rounded-lg overflow-hidden border border-slate-200">
{images[view] ? (
<img src={images[view]} alt={`${view} view`} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center">
<Camera className="w-8 h-8 text-slate-300" />
</div>
)}
<label className="absolute inset-0 cursor-pointer hover:bg-black/20 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
<span className="bg-white px-3 py-1 rounded-full text-xs font-medium text-slate-700 shadow">
{uploading === view ? 'Uploading...' : 'Upload'}
</span>
<input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(view, e)} />
</label>
</div>
</div>
))}
</div>
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100"> <div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-4">Bike Documents</h3> <h3 className="font-semibold text-slate-700 mb-4">Bike Documents</h3>
{docs.length === 0 ? ( {docs.length === 0 ? (
@@ -827,93 +1049,395 @@ function DocumentsTab({ bike }: { bike: Bike }) {
} }
function RentalTab({ bike }: { bike: Bike }) { function RentalTab({ bike }: { bike: Bike }) {
const history = bike.rentalHistory || []; // Generate highly realistic rent transaction history
const [transactions] = useState<any[]>(() => {
const list: any[] = [];
const riders = [bike.assignedTo || 'Karim Ahmed', 'Sajib Islam', 'Nayeem Chowdhury', 'Rakib Hasan', 'Kamal Hossain'];
const methods = ['bKash', 'Nagad', 'Rocket', 'Bank Transfer'];
const getRateDisplay = (type: string, rate: number) => { const days = 25;
switch (type) { const baseDate = new Date();
case 'single': return `৳${rate}/day`;
case 'shared': return `৳${rate / 2}+${rate / 2} (2 person)`; for (let i = 0; i < days; i++) {
case 'rent-to-own': return `৳${rate}/day`; const date = new Date();
default: return `৳${rate}`; date.setDate(baseDate.getDate() - i);
const dateString = date.toISOString().split('T')[0];
const riderIndex = (i) % riders.length;
const methodIndex = (i + 1) % methods.length;
// status distribution
let status: 'paid' | 'pending' | 'failed' = 'paid';
if (i === 1) status = 'pending';
else if (i === 5) status = 'failed';
const amount = bike.currentRent || 350;
list.push({
id: `TX-BK-${10200 + i}`,
date: dateString,
riderName: riders[riderIndex],
duration: '1 Day',
amount: amount,
status: status,
payoutMethod: methods[methodIndex]
});
} }
return list;
});
// Filter & Sorting State
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [sortBy, setSortBy] = useState<'date' | 'amount' | 'rider'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [page, setPage] = useState(1);
const pageSize = 8;
// Handler functions
const handleSort = (field: 'date' | 'amount' | 'rider') => {
if (sortBy === field) {
setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'));
} else {
setSortBy(field);
setSortOrder('desc');
}
setPage(1);
}; };
// Filter Logic
const filteredTransactions = transactions.filter(tx => {
if (statusFilter !== 'all' && tx.status !== statusFilter) return false;
if (searchQuery && !tx.riderName.toLowerCase().includes(searchQuery.toLowerCase()) && !tx.id.toLowerCase().includes(searchQuery.toLowerCase())) return false;
if (dateFrom && new Date(tx.date) < new Date(dateFrom)) return false;
if (dateTo && new Date(tx.date) > new Date(dateTo)) return false;
return true;
});
// Sort Logic
const sortedTransactions = [...filteredTransactions].sort((a, b) => {
let comparison = 0;
if (sortBy === 'date') {
comparison = new Date(a.date).getTime() - new Date(b.date).getTime();
} else if (sortBy === 'amount') {
comparison = a.amount - b.amount;
} else if (sortBy === 'rider') {
comparison = a.riderName.localeCompare(b.riderName);
}
return sortOrder === 'desc' ? -comparison : comparison;
});
// Pagination
const totalPages = Math.ceil(sortedTransactions.length / pageSize);
const paginatedTransactions = sortedTransactions.slice((page - 1) * pageSize, page * pageSize);
const statusConfig: Record<string, { label: string; bg: string; color: string; icon: any }> = {
paid: { label: 'Paid', bg: 'bg-green-100', color: 'text-green-700', icon: CheckCircle2 },
pending: { label: 'Pending', bg: 'bg-amber-100', color: 'text-amber-700', icon: Clock },
failed: { label: 'Failed', bg: 'bg-red-100', color: 'text-red-700', icon: XCircle },
};
const totalCollected = filteredTransactions
.filter(t => t.status === 'paid')
.reduce((sum, t) => sum + t.amount, 0);
const pendingAmount = filteredTransactions
.filter(t => t.status === 'pending')
.reduce((sum, t) => sum + t.amount, 0);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100"> {/* Dynamic Rental Metrics - Sleek and Responsive */}
<h3 className="font-semibold text-slate-700 mb-3">Rental History</h3> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{history.length === 0 ? ( <div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-center py-8"> <div className="flex items-center gap-3">
<History className="w-12 h-12 text-slate-300 mx-auto mb-4" /> <div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0">
<p className="text-sm text-slate-500">No rental history yet.</p> <DollarSign className="w-5 h-5 text-green-600" />
</div>
) : (
<div className="space-y-3">
{history.map(rental => (
<div key={rental.id} className="p-4 border border-slate-200 rounded-lg">
<div className="flex items-start justify-between mb-2">
<div>
<p className="font-medium text-slate-700">{rental.bikerName}</p>
<p className="text-xs text-slate-500">ID: {rental.id}</p>
</div>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${rental.status === 'active' ? 'bg-green-100 text-green-700' :
rental.status === 'completed' ? 'bg-blue-100 text-blue-700' :
'bg-red-100 text-red-700'
}`}>
{rental.status}
</span>
</div>
<div className="flex flex-wrap gap-3 text-xs">
<span className="bg-slate-100 px-2 py-1 rounded text-slate-600">
{rental.type === 'single' ? 'Single (350/day)' :
rental.type === 'shared' ? 'Shared (60/day)' :
'Rent-to-Own (450/day)'}
</span>
<span className="text-slate-500">
{rental.startDate} {rental.endDate && `to ${rental.endDate}`}
</span>
</div>
<div className="flex justify-between mt-2 pt-2 border-t border-slate-100">
<span className="text-xs text-slate-500">{rental.rideCount} rides</span>
<span className="text-sm font-semibold text-green-600">৳{rental.totalPaid.toLocaleString()}</span>
</div>
</div>
))}
</div>
)}
</div>
<div className="bg-white rounded-xl p-4 lg:p-6 shadow-sm border border-slate-100">
<h3 className="font-semibold text-slate-700 mb-3">Rental Rates Info</h3>
<div className="space-y-2">
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center">
<span className="text-xs font-bold text-green-600">1</span>
</div>
<span className="font-medium text-slate-700">Single</span>
</div> </div>
<span className="font-semibold text-green-600">৳350/day</span> <div>
</div> <p className="text-xs text-slate-500">Total Collected</p>
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg"> <p className="text-lg font-bold text-green-600">৳{totalCollected.toLocaleString()}</p>
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center">
<span className="text-xs font-bold text-blue-600">2</span>
</div>
<span className="font-medium text-slate-700">Shared (2 Person)</span>
</div> </div>
<span className="font-semibold text-green-600">৳60/day (৳30+৳30)</span>
</div>
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center">
<span className="text-xs font-bold text-purple-600">3</span>
</div>
<span className="font-medium text-slate-700">Rent-to-Own</span>
</div>
<span className="font-semibold text-green-600">৳450/day</span>
</div> </div>
</div> </div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center shrink-0">
<Zap className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-xs text-slate-500">Active Rate</p>
<p className="text-lg font-bold text-slate-800">৳{bike.currentRent || 350}/day</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-100 rounded-lg flex items-center justify-center shrink-0">
<Clock className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="text-xs text-slate-500">Pending Amount</p>
<p className="text-lg font-bold text-amber-600">৳{pendingAmount.toLocaleString()}</p>
</div>
</div>
</div>
</div>
{/* Main Table Container */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
{/* Filters */}
<div className="p-4 border-b border-slate-100 bg-slate-50/20">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
type="text"
placeholder="Search by rider or ref..."
value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setPage(1); }}
className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64 bg-white focus:outline-none focus:border-slate-400 transition-colors"
/>
</div>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer"
>
<option value="all">All Status</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
<option value="failed">Failed</option>
</select>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1">
<input
type="date"
value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
<span className="text-slate-400">to</span>
<input
type="date"
value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
</div>
{(dateFrom || dateTo) && (
<button
onClick={() => { setDateFrom(''); setDateTo(''); setPage(1); }}
className="px-2.5 py-1.5 text-xs text-red-500 hover:bg-red-50 rounded font-semibold transition-colors"
>
Clear
</button>
)}
<button
onClick={() => {
setSearchQuery('');
setStatusFilter('all');
setDateFrom('');
setDateTo('');
setSortBy('date');
setSortOrder('desc');
setPage(1);
}}
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 bg-white"
title="Reset filters"
>
<RefreshCw className="w-4 h-4 text-slate-500" />
</button>
</div>
</div>
</div>
{/* Desktop Table View */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-slate-50 border-b border-slate-100">
<tr>
<th
onClick={() => handleSort('date')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
>
<div className="flex items-center gap-1">
Date {sortBy === 'date' && <span className="text-accent">{sortOrder === 'asc' ? '' : ''}</span>}
</div>
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Transaction ID
</th>
<th
onClick={() => handleSort('rider')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
>
<div className="flex items-center gap-1">
Rider {sortBy === 'rider' && <span className="text-accent">{sortOrder === 'asc' ? '' : ''}</span>}
</div>
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Duration
</th>
<th
onClick={() => handleSort('amount')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors text-right"
>
<div className="flex items-center gap-1 justify-end">
Amount {sortBy === 'amount' && <span className="text-accent">{sortOrder === 'asc' ? '' : ''}</span>}
</div>
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Method
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Status
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedTransactions.length > 0 ? (
paginatedTransactions.map((tx) => {
const status = statusConfig[tx.status] || statusConfig.pending;
const StatusIcon = status.icon;
return (
<tr key={tx.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3 text-sm text-slate-800 font-medium">
{tx.date}
</td>
<td className="px-4 py-3 text-xs font-mono font-semibold text-slate-400">
{tx.id}
</td>
<td className="px-4 py-3 text-sm">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-slate-400" />
<span className="font-semibold text-slate-700">{tx.riderName}</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-slate-500 font-medium">
{tx.duration}
</td>
<td className="px-4 py-3 text-sm font-bold text-slate-800 text-right">
৳{tx.amount.toLocaleString()}
</td>
<td className="px-4 py-3 text-sm text-slate-600 font-medium capitalize">
{tx.payoutMethod}
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold ${status.bg} ${status.color}`}>
<StatusIcon className="w-3.5 h-3.5" />
{status.label}
</span>
</td>
</tr>
);
})
) : (
<tr>
<td colSpan={7} className="px-4 py-12 text-center text-slate-400">
<AlertCircle className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p className="text-sm font-semibold">No rental transactions found</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Mobile View */}
<div className="lg:hidden divide-y divide-slate-100">
{paginatedTransactions.length > 0 ? (
paginatedTransactions.map((tx) => {
const status = statusConfig[tx.status] || statusConfig.pending;
const StatusIcon = status.icon;
return (
<div key={tx.id} className="p-4 hover:bg-slate-50 transition-colors">
<div className="flex items-start justify-between gap-3 mb-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-slate-400 shrink-0" />
<p className="text-sm font-semibold text-slate-800 truncate">{tx.riderName}</p>
</div>
<p className="text-xs text-slate-400 ml-6">Ref: {tx.id}</p>
</div>
<div className="text-right shrink-0">
<p className="text-base font-bold text-slate-800">৳{tx.amount.toLocaleString()}</p>
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-semibold mt-1 ${status.bg} ${status.color}`}>
<StatusIcon className="w-3 h-3" /> {status.label}
</span>
</div>
</div>
<div className="flex items-center justify-between ml-6 text-xs text-slate-400">
<span>{tx.date}</span>
<span className="capitalize">{tx.payoutMethod}</span>
</div>
</div>
);
})
) : (
<div className="p-8 text-center text-slate-500">
<AlertCircle className="w-10 h-10 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</div>
)}
</div>
{/* Paginated Footer */}
{sortedTransactions.length > pageSize && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3 bg-slate-50/10">
<p className="text-xs sm:text-sm text-slate-500">
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedTransactions.length)} of {sortedTransactions.length} records
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
>
<ChevronLeft className="w-4 h-4 text-slate-600" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`w-8 h-8 rounded-lg text-sm font-semibold transition-colors ${
page === pageNum
? 'bg-accent text-white'
: 'border border-slate-200 hover:bg-slate-50 bg-white text-slate-600'
}`}
>
{pageNum}
</button>
))}
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
>
<ChevronRight className="w-4 h-4 text-slate-600" />
</button>
</div>
</div>
)}
</div> </div>
</div> </div>
); );
@@ -1305,8 +1829,17 @@ function DamageModal({ bike, damage, onClose, onSave }: { bike: Bike; damage: Da
estimatedCost: damage?.estimatedCost || 0, estimatedCost: damage?.estimatedCost || 0,
actualCost: damage?.actualCost || 0, actualCost: damage?.actualCost || 0,
status: damage?.status || 'reported', status: damage?.status || 'reported',
hubId: damage?.hubId || '',
hubName: damage?.hubName || '',
}); });
const mockHubs = [
{ id: 'HUB-001', name: 'Gulshan Hub' },
{ id: 'HUB-002', name: 'Banani Hub' },
{ id: 'HUB-003', name: 'Uttara Hub' },
{ id: 'HUB-004', name: 'Mirpur Hub' },
];
const damageTypes = [ const damageTypes = [
{ value: 'accident', label: 'Accident' }, { value: 'accident', label: 'Accident' },
{ value: 'theft', label: 'Theft' }, { value: 'theft', label: 'Theft' },
@@ -1379,6 +1912,19 @@ function DamageModal({ bike, damage, onClose, onSave }: { bike: Bike; damage: Da
required required
/> />
</div> </div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Hub</label>
<select
value={formData.hubId}
onChange={(e) => setFormData({ ...formData, hubId: e.target.value, hubName: mockHubs.find(h => h.id === e.target.value)?.name || '' })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">Select Hub</option>
{mockHubs.map(hub => (
<option key={hub.id} value={hub.id}>{hub.name}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Estimated Cost (৳)</label> <label className="text-sm font-medium text-slate-600 mb-1 block">Estimated Cost (৳)</label>
@@ -1435,8 +1981,17 @@ function MaintenanceModal({ bike, maintenance, onClose, onSave }: { bike: Bike;
cost: maintenance?.cost || 0, cost: maintenance?.cost || 0,
nextDueDate: maintenance?.nextDueDate || '', nextDueDate: maintenance?.nextDueDate || '',
status: maintenance?.status || 'completed', status: maintenance?.status || 'completed',
hubId: maintenance?.hubId || '',
hubName: maintenance?.hubName || '',
}); });
const mockHubs = [
{ id: 'HUB-001', name: 'Gulshan Hub' },
{ id: 'HUB-002', name: 'Banani Hub' },
{ id: 'HUB-003', name: 'Uttara Hub' },
{ id: 'HUB-004', name: 'Mirpur Hub' },
];
const maintenanceTypes = [ const maintenanceTypes = [
{ value: 'routine', label: 'Routine Service' }, { value: 'routine', label: 'Routine Service' },
{ value: 'battery', label: 'Battery' }, { value: 'battery', label: 'Battery' },
@@ -1510,6 +2065,19 @@ function MaintenanceModal({ bike, maintenance, onClose, onSave }: { bike: Bike;
required required
/> />
</div> </div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Hub</label>
<select
value={formData.hubId}
onChange={(e) => setFormData({ ...formData, hubId: e.target.value, hubName: mockHubs.find(h => h.id === e.target.value)?.name || '' })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">Select Hub</option>
{mockHubs.map(hub => (
<option key={hub.id} value={hub.id}>{hub.name}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Cost (৳)</label> <label className="text-sm font-medium text-slate-600 mb-1 block">Cost (৳)</label>

View File

@@ -17,6 +17,7 @@ interface Bike {
plateNumber: string; plateNumber: string;
status: 'available' | 'rented' | 'maintenance' | 'retired'; status: 'available' | 'rented' | 'maintenance' | 'retired';
batteryLevel: number; batteryLevel: number;
batteryId?: string;
location?: string; // deprecated - use hubId/hubName location?: string; // deprecated - use hubId/hubName
hubId?: string; hubId?: string;
hubName?: string; hubName?: string;
@@ -56,6 +57,15 @@ const hubs = [
{ id: 'HUB-004', name: 'Mirpur Hub' }, { id: 'HUB-004', name: 'Mirpur Hub' },
]; ];
const mockBatteries = [
{ id: 'BAT-001', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-001', chargeLevel: 95 },
{ id: 'BAT-002', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-002', chargeLevel: 75 },
{ id: 'BAT-003', brand: 'Lithium', model: '60V/40Ah', serialNumber: 'LTH-2024-003', chargeLevel: 45 },
{ id: 'BAT-004', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-004', chargeLevel: 88 },
{ id: 'BAT-005', brand: 'Lithium', model: '48V/25Ah', serialNumber: 'LTH-2024-005', chargeLevel: 62 },
{ id: 'BAT-006', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-006', chargeLevel: 100 },
];
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
available: 'bg-green-100 text-green-700', available: 'bg-green-100 text-green-700',
rented: 'bg-blue-100 text-blue-700', rented: 'bg-blue-100 text-blue-700',
@@ -159,9 +169,9 @@ export default function FleetPage() {
<p className="font-semibold text-slate-700">{selectedMapBike.model}</p> <p className="font-semibold text-slate-700">{selectedMapBike.model}</p>
<p className="text-xs text-slate-500">{selectedMapBike.brand} {selectedMapBike.id}</p> <p className="text-xs text-slate-500">{selectedMapBike.brand} {selectedMapBike.id}</p>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full mt-1 ${selectedMapBike.status === 'available' ? 'bg-green-100 text-green-700' : <span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full mt-1 ${selectedMapBike.status === 'available' ? 'bg-green-100 text-green-700' :
selectedMapBike.status === 'rented' ? 'bg-blue-100 text-blue-700' : selectedMapBike.status === 'rented' ? 'bg-blue-100 text-blue-700' :
selectedMapBike.status === 'maintenance' ? 'bg-amber-100 text-amber-700' : selectedMapBike.status === 'maintenance' ? 'bg-amber-100 text-amber-700' :
'bg-slate-100 text-slate-500' 'bg-slate-100 text-slate-500'
}`}> }`}>
{selectedMapBike.status} {selectedMapBike.status}
</span> </span>
@@ -182,7 +192,7 @@ export default function FleetPage() {
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-slate-500">Battery</span> <span className="text-slate-500">Battery</span>
<span className={`font-medium ${selectedMapBike.batteryLevel > 50 ? 'text-green-600' : <span className={`font-medium ${selectedMapBike.batteryLevel > 50 ? 'text-green-600' :
selectedMapBike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600' selectedMapBike.batteryLevel > 20 ? 'text-amber-600' : 'text-red-600'
}`}>{selectedMapBike.batteryLevel}%</span> }`}>{selectedMapBike.batteryLevel}%</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
@@ -488,6 +498,7 @@ function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike
plateNumber: '', plateNumber: '',
status: 'available', status: 'available',
batteryLevel: 100, batteryLevel: 100,
batteryId: '',
location: '', // deprecated location: '', // deprecated
hubId: '', hubId: '',
hubName: '', hubName: '',
@@ -563,14 +574,27 @@ function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Battery Level (%)</label> <label className="block text-sm font-medium text-slate-700 mb-1">Battery</label>
<input <select
type="number" value={formData.batteryId || ''}
value={formData.batteryLevel} onChange={(e) => {
onChange={(e) => handleChange('batteryLevel', parseInt(e.target.value))} const battery = mockBatteries.find(b => b.id === e.target.value);
handleChange('batteryId', e.target.value);
if (battery) {
handleChange('batteryLevel', battery.chargeLevel);
}
}}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/> >
<option value="">Select Battery...</option>
{mockBatteries.map(battery => (
<option key={battery.id} value={battery.id}>
{battery.brand} {battery.model} - {battery.serialNumber} ({battery.chargeLevel}%)
</option>
))}
</select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Hub *</label> <label className="block text-sm font-medium text-slate-700 mb-1">Hub *</label>
<select <select
@@ -588,6 +612,16 @@ function BikeForm({ bike, onSave, onCancel }: { bike: Bike | null; onSave: (bike
))} ))}
</select> </select>
</div> </div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Battery Level (%)</label>
<input
type="number"
value={formData.batteryLevel}
onChange={(e) => handleChange('batteryLevel', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Assigned To</label> <label className="block text-sm font-medium text-slate-700 mb-1">Assigned To</label>
<input <input
@@ -812,9 +846,9 @@ function FleetMap({ bikes, onSelectBike, selectedBike, large }: { bikes: Bike[];
<path <path
d={large ? "M0,0 L-3,-6 A3.5,3.5 0 1,1 3,-6 L0,0 Z" : "M0,0 L-2,-4 A2.5,2.5 0 1,1 2,-4 L0,0 Z"} d={large ? "M0,0 L-3,-6 A3.5,3.5 0 1,1 3,-6 L0,0 Z" : "M0,0 L-2,-4 A2.5,2.5 0 1,1 2,-4 L0,0 Z"}
className={`${data.bikes[0].status === 'available' ? 'fill-green-500' : className={`${data.bikes[0].status === 'available' ? 'fill-green-500' :
data.bikes[0].status === 'rented' ? 'fill-blue-500' : data.bikes[0].status === 'rented' ? 'fill-blue-500' :
data.bikes[0].status === 'maintenance' ? 'fill-amber-500' : data.bikes[0].status === 'maintenance' ? 'fill-amber-500' :
'fill-slate-400' 'fill-slate-400'
}`} }`}
filter="url(#shadow)" filter="url(#shadow)"
/> />

View File

@@ -4,7 +4,8 @@ import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { import {
ArrowLeft, MapPin, Phone, Clock, Bike, Plus, X, Edit, Save, Trash2, ArrowLeft, MapPin, Phone, Clock, Bike, Plus, X, Edit, Save, Trash2,
Navigation, User, Wallet, DollarSign, CheckCircle, AlertTriangle Navigation, User, Wallet, DollarSign, CheckCircle, AlertTriangle, Battery,
Mail, Calendar, Briefcase, Users, Search, UserPlus
} from 'lucide-react'; } from 'lucide-react';
interface Hub { interface Hub {
@@ -29,6 +30,17 @@ interface BikeInfo {
status: 'available' | 'rented' | 'maintenance'; status: 'available' | 'rented' | 'maintenance';
} }
interface BatteryInfo {
id: string;
brand: string;
model: string;
serialNumber: string;
status: 'available' | 'charging' | 'deployed' | 'maintenance';
chargeLevel: number;
assignedBike?: string;
assignedDate?: string;
}
const mockHub: Hub = { const mockHub: Hub = {
id: 'HUB-001', id: 'HUB-001',
name: 'JAIBEN Head Office', name: 'JAIBEN Head Office',
@@ -52,6 +64,15 @@ const mockHubBikes: BikeInfo[] = [
{ id: 'BIKE-005', model: 'Yadea G5', plate: 'Dhaka Metro Ha-5678', status: 'maintenance' }, { id: 'BIKE-005', model: 'Yadea G5', plate: 'Dhaka Metro Ha-5678', status: 'maintenance' },
]; ];
const mockHubBatteries: BatteryInfo[] = [
{ id: 'BAT-001', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-001', status: 'available', chargeLevel: 95 },
{ id: 'BAT-002', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-002', status: 'charging', chargeLevel: 75 },
{ id: 'BAT-003', brand: 'Lithium', model: '60V/40Ah', serialNumber: 'LTH-2024-003', status: 'deployed', chargeLevel: 45, assignedBike: 'BIKE-002', assignedDate: '2024-03-15' },
{ id: 'BAT-004', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-004', status: 'available', chargeLevel: 88 },
{ id: 'BAT-005', brand: 'Lithium', model: '48V/25Ah', serialNumber: 'LTH-2024-005', status: 'maintenance', chargeLevel: 0 },
{ id: 'BAT-006', brand: 'Lithium', model: '60V/30Ah', serialNumber: 'LTH-2024-006', status: 'deployed', chargeLevel: 62, assignedBike: 'BIKE-004', assignedDate: '2024-03-18' },
];
interface RentalInfo { interface RentalInfo {
id: string; id: string;
userName: string; userName: string;
@@ -70,6 +91,70 @@ const mockHubRentals: RentalInfo[] = [
{ id: 'RNT-003', userName: 'Jamal Uddin', bike: 'AIMA EM5', plate: 'Dhaka Metro Ko-1234', startDate: '2024-02-01', type: 'shared', status: 'pending', dailyRate: 150, totalPaid: 450 }, { id: 'RNT-003', userName: 'Jamal Uddin', bike: 'AIMA EM5', plate: 'Dhaka Metro Ko-1234', startDate: '2024-02-01', type: 'shared', status: 'pending', dailyRate: 150, totalPaid: 450 },
]; ];
interface Employee {
id: string;
name: string;
role: 'Manager' | 'Accountant' | 'Staff' | 'Technician' | 'Support';
email: string;
phone: string;
status: 'Active' | 'On Leave' | 'Inactive';
joiningDate: string;
shift: 'Morning' | 'Evening' | 'Night' | 'Full-time';
}
const mockHubEmployees: Employee[] = [
{
id: 'EMP-001',
name: 'Arif Rahman',
role: 'Manager',
email: 'arif.rahman@jaiben.com',
phone: '+8801711223344',
status: 'Active',
joiningDate: '2023-01-10',
shift: 'Full-time',
},
{
id: 'EMP-002',
name: 'Tasmia Chowdhury',
role: 'Accountant',
email: 'tasmia.c@jaiben.com',
phone: '+8801722334455',
status: 'Active',
joiningDate: '2023-03-15',
shift: 'Morning',
},
{
id: 'EMP-003',
name: 'Kamrul Islam',
role: 'Staff',
email: 'kamrul.i@jaiben.com',
phone: '+8801733445566',
status: 'Active',
joiningDate: '2023-06-20',
shift: 'Evening',
},
{
id: 'EMP-004',
name: 'Mizanur Rahman',
role: 'Technician',
email: 'mizan.r@jaiben.com',
phone: '+8801744556677',
status: 'Active',
joiningDate: '2023-08-01',
shift: 'Morning',
},
{
id: 'EMP-005',
name: 'Sujon Ali',
role: 'Support',
email: 'sujon.a@jaiben.com',
phone: '+8801755667788',
status: 'On Leave',
joiningDate: '2023-11-15',
shift: 'Night',
},
];
export default function HubDetailPage() { export default function HubDetailPage() {
const params = useParams(); const params = useParams();
const router = useRouter(); const router = useRouter();
@@ -77,10 +162,36 @@ export default function HubDetailPage() {
const [hub, setHub] = useState<Hub>(mockHub); const [hub, setHub] = useState<Hub>(mockHub);
const [bikes, setBikes] = useState<BikeInfo[]>(mockHubBikes); const [bikes, setBikes] = useState<BikeInfo[]>(mockHubBikes);
const [batteries, setBatteries] = useState<BatteryInfo[]>(mockHubBatteries);
const [rentals, setRentals] = useState<RentalInfo[]>(mockHubRentals); const [rentals, setRentals] = useState<RentalInfo[]>(mockHubRentals);
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
const [editForm, setEditForm] = useState(hub); const [editForm, setEditForm] = useState(hub);
const [activeTab, setActiveTab] = useState<'overview' | 'bikes' | 'rentals'>('overview'); const [activeTab, setActiveTab] = useState<'overview' | 'employees' | 'bikes' | 'batteries' | 'rentals'>('overview');
const [employees, setEmployees] = useState<Employee[]>(mockHubEmployees);
const [employeeSearch, setEmployeeSearch] = useState('');
const [roleFilter, setRoleFilter] = useState<string>('All');
const [statusFilter, setStatusFilter] = useState<string>('All');
const [addEmployeeModal, setAddEmployeeModal] = useState(false);
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null);
const [employeeForm, setEmployeeForm] = useState<Omit<Employee, 'id'>>({
name: '',
role: 'Staff',
email: '',
phone: '',
status: 'Active',
joiningDate: new Date().toISOString().split('T')[0],
shift: 'Full-time'
});
const [deleteEmployeeModal, setDeleteEmployeeModal] = useState<Employee | null>(null);
const [assignModal, setAssignModal] = useState<BatteryInfo | null>(null);
const [selectedBike, setSelectedBike] = useState('');
const [addBikeModal, setAddBikeModal] = useState(false);
const [addBatteryModal, setAddBatteryModal] = useState(false);
const [editingBike, setEditingBike] = useState<BikeInfo | null>(null);
const [editingBattery, setEditingBattery] = useState<BatteryInfo | null>(null);
const [bikeForm, setBikeForm] = useState<{ model: string; plate: string; status: 'available' | 'rented' | 'maintenance' }>({ model: '', plate: '', status: 'available' });
const [batteryForm, setBatteryForm] = useState<{ brand: string; model: string; serialNumber: string; chargeLevel: number; status: 'available' | 'charging' | 'deployed' | 'maintenance' }>({ brand: '', model: '', serialNumber: '', chargeLevel: 100, status: 'available' });
const [deleteModal, setDeleteModal] = useState<{ type: 'bike' | 'battery'; item: BikeInfo | BatteryInfo } | null>(null);
const handleSaveEdit = () => { const handleSaveEdit = () => {
setHub(editForm); setHub(editForm);
@@ -155,26 +266,44 @@ export default function HubDetailPage() {
<button <button
onClick={() => setActiveTab('overview')} onClick={() => setActiveTab('overview')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'overview' className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'overview'
? 'border-accent text-accent' ? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700' : 'border-transparent text-slate-500 hover:text-slate-700'
}`} }`}
> >
Overview Overview
</button> </button>
<button
onClick={() => setActiveTab('employees')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'employees'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
Employees ({employees.length})
</button>
<button <button
onClick={() => setActiveTab('bikes')} onClick={() => setActiveTab('bikes')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'bikes' className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'bikes'
? 'border-accent text-accent' ? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700' : 'border-transparent text-slate-500 hover:text-slate-700'
}`} }`}
> >
Bikes ({bikes.length}) Bikes ({bikes.length})
</button> </button>
<button
onClick={() => setActiveTab('batteries')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'batteries'
? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
Batteries ({batteries.length})
</button>
<button <button
onClick={() => setActiveTab('rentals')} onClick={() => setActiveTab('rentals')}
className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'rentals' className={`py-4 text-sm font-medium border-b-2 transition-colors ${activeTab === 'rentals'
? 'border-accent text-accent' ? 'border-accent text-accent'
: 'border-transparent text-slate-500 hover:text-slate-700' : 'border-transparent text-slate-500 hover:text-slate-700'
}`} }`}
> >
Rentals ({rentals.length}) Rentals ({rentals.length})
@@ -337,11 +466,199 @@ export default function HubDetailPage() {
</div> </div>
)} )}
{activeTab === 'employees' && (
<div>
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
<div>
<h3 className="font-bold text-slate-800 text-lg">Hub Employees ({employees.length})</h3>
<p className="text-sm text-slate-500 mt-0.5">Manage and track hub operational personnel and roles</p>
</div>
<button
onClick={() => {
setEditingEmployee(null);
setEmployeeForm({
name: '',
role: 'Staff',
email: '',
phone: '',
status: 'Active',
joiningDate: new Date().toISOString().split('T')[0],
shift: 'Full-time'
});
setAddEmployeeModal(true);
}}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm font-medium flex items-center gap-2 hover:opacity-90 transition-all shadow-sm self-start md:self-auto"
>
<UserPlus className="w-4 h-4" /> Add Employee
</button>
</div>
{/* Search & Filter Toolbar */}
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100 flex flex-col md:flex-row gap-3 mb-6">
<div className="flex-1 relative">
<Search className="absolute left-3 top-2.5 w-4 h-4 text-slate-400" />
<input
type="text"
value={employeeSearch}
onChange={(e) => setEmployeeSearch(e.target.value)}
placeholder="Search by name, email, phone or ID..."
className="w-full pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
/>
</div>
<div className="flex gap-3">
<select
value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-sm text-slate-600 bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="All">All Roles</option>
<option value="Manager">Managers</option>
<option value="Accountant">Accountants</option>
<option value="Staff">Operations Staff</option>
<option value="Technician">Technicians</option>
<option value="Support">Support Staff</option>
</select>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-sm text-slate-600 bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="All">All Statuses</option>
<option value="Active">Active / On Duty</option>
<option value="On Leave">On Leave</option>
<option value="Inactive">Inactive</option>
</select>
</div>
</div>
{/* Roster Cards Grid */}
{employees.filter(emp => {
const matchesSearch =
emp.name.toLowerCase().includes(employeeSearch.toLowerCase()) ||
emp.email.toLowerCase().includes(employeeSearch.toLowerCase()) ||
emp.phone.includes(employeeSearch) ||
emp.id.toLowerCase().includes(employeeSearch.toLowerCase());
const matchesRole = roleFilter === 'All' || emp.role === roleFilter;
const matchesStatus = statusFilter === 'All' || emp.status === statusFilter;
return matchesSearch && matchesRole && matchesStatus;
}).length === 0 ? (
<div className="text-center py-12 bg-slate-50 rounded-xl border border-slate-100">
<Users className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<p className="text-slate-500 text-sm">No employees match your search or filter criteria.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{employees.filter(emp => {
const matchesSearch =
emp.name.toLowerCase().includes(employeeSearch.toLowerCase()) ||
emp.email.toLowerCase().includes(employeeSearch.toLowerCase()) ||
emp.phone.includes(employeeSearch) ||
emp.id.toLowerCase().includes(employeeSearch.toLowerCase());
const matchesRole = roleFilter === 'All' || emp.role === roleFilter;
const matchesStatus = statusFilter === 'All' || emp.status === statusFilter;
return matchesSearch && matchesRole && matchesStatus;
}).map(emp => {
const roleConfig: Record<string, { badge: string; circle: string; text: string }> = {
Manager: { badge: 'bg-emerald-100 text-emerald-800 border-emerald-200', circle: 'bg-emerald-50 text-emerald-600', text: 'text-emerald-700' },
Accountant: { badge: 'bg-blue-100 text-blue-800 border-blue-200', circle: 'bg-blue-50 text-blue-600', text: 'text-blue-700' },
Staff: { badge: 'bg-purple-100 text-purple-800 border-purple-200', circle: 'bg-purple-50 text-purple-600', text: 'text-purple-700' },
Technician: { badge: 'bg-amber-100 text-amber-800 border-amber-200', circle: 'bg-amber-50 text-amber-600', text: 'text-amber-700' },
Support: { badge: 'bg-orange-100 text-orange-800 border-orange-200', circle: 'bg-orange-50 text-orange-600', text: 'text-orange-700' },
};
const style = roleConfig[emp.role] || roleConfig.Staff;
return (
<div key={emp.id} className="bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden hover:shadow-md transition-all flex flex-col justify-between">
<div className="p-5">
<div className="flex items-start justify-between gap-3 mb-4">
<div className="flex items-center gap-3">
<div className={`w-12 h-12 rounded-full flex items-center justify-center font-bold text-lg ${style.circle}`}>
{emp.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase()}
</div>
<div>
<h4 className="font-bold text-slate-800 hover:text-accent transition-colors">{emp.name}</h4>
<span className="text-xs text-slate-400 font-mono">{emp.id}</span>
</div>
</div>
<span className={`inline-flex items-center text-xs font-semibold px-2.5 py-0.5 rounded-full border ${style.badge}`}>
{emp.role}
</span>
</div>
<div className="space-y-2.5 my-4 border-t border-b border-slate-50 py-3">
<div className="flex items-center gap-2 text-sm text-slate-600">
<Mail className="w-4 h-4 text-slate-400 flex-shrink-0" />
<a href={`mailto:${emp.email}`} className="truncate hover:text-accent hover:underline">{emp.email}</a>
</div>
<div className="flex items-center gap-2 text-sm text-slate-600">
<Phone className="w-4 h-4 text-slate-400 flex-shrink-0" />
<a href={`tel:${emp.phone}`} className="hover:text-accent hover:underline">{emp.phone}</a>
</div>
<div className="flex items-center gap-2 text-sm text-slate-600">
<Clock className="w-4 h-4 text-slate-400 flex-shrink-0" />
<span>Shift: <span className="font-medium text-slate-700">{emp.shift}</span></span>
</div>
<div className="flex items-center gap-2 text-sm text-slate-600">
<Calendar className="w-4 h-4 text-slate-400 flex-shrink-0" />
<span>Joined: <span className="font-medium text-slate-700">{emp.joiningDate}</span></span>
</div>
</div>
</div>
<div className="px-5 pb-5 pt-1 border-t border-slate-50 flex items-center justify-between bg-slate-50/50">
<span className={`inline-flex items-center gap-1.5 text-xs font-semibold px-2.5 py-1 rounded-full ${
emp.status === 'Active' ? 'bg-green-100 text-green-700' :
emp.status === 'On Leave' ? 'bg-amber-100 text-amber-700' :
'bg-slate-200 text-slate-600'
}`}>
<span className={`w-1.5 h-1.5 rounded-full ${
emp.status === 'Active' ? 'bg-green-500' :
emp.status === 'On Leave' ? 'bg-amber-500' :
'bg-slate-500'
}`} />
{emp.status}
</span>
<div className="flex gap-2">
<button
onClick={() => {
setEditingEmployee(emp);
setEmployeeForm({
name: emp.name,
role: emp.role,
email: emp.email,
phone: emp.phone,
status: emp.status,
joiningDate: emp.joiningDate,
shift: emp.shift
});
setAddEmployeeModal(true);
}}
className="px-2.5 py-1.5 text-xs font-semibold text-blue-600 hover:text-blue-700 bg-white border border-blue-100 hover:border-blue-200 rounded-lg hover:shadow-sm transition-all"
>
Edit
</button>
<button
onClick={() => setDeleteEmployeeModal(emp)}
className="px-2.5 py-1.5 text-xs font-semibold text-red-600 hover:text-red-700 bg-white border border-red-100 hover:border-red-200 rounded-lg hover:shadow-sm transition-all"
>
Delete
</button>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
)}
{activeTab === 'bikes' && ( {activeTab === 'bikes' && (
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-800">Hub Bikes ({bikes.length})</h3> <h3 className="font-semibold text-slate-800">Hub Bikes ({bikes.length})</h3>
<button className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2"> <button onClick={() => { setBikeForm({ model: '', plate: '', status: 'available' }); setAddBikeModal(true); }} className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2">
<Plus className="w-4 h-4" /> Add Bike <Plus className="w-4 h-4" /> Add Bike
</button> </button>
</div> </div>
@@ -351,8 +668,8 @@ export default function HubDetailPage() {
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<Bike className="w-5 h-5 text-slate-400" /> <Bike className="w-5 h-5 text-slate-400" />
<span className={`text-xs font-medium px-2 py-1 rounded-full ${bike.status === 'available' ? 'bg-green-100 text-green-700' : <span className={`text-xs font-medium px-2 py-1 rounded-full ${bike.status === 'available' ? 'bg-green-100 text-green-700' :
bike.status === 'rented' ? 'bg-amber-100 text-amber-700' : bike.status === 'rented' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700' 'bg-red-100 text-red-700'
}`}> }`}>
{bike.status} {bike.status}
</span> </span>
@@ -360,6 +677,73 @@ export default function HubDetailPage() {
<p className="font-medium text-slate-800">{bike.model}</p> <p className="font-medium text-slate-800">{bike.model}</p>
<p className="text-sm text-slate-500">{bike.plate}</p> <p className="text-sm text-slate-500">{bike.plate}</p>
<p className="text-xs text-slate-400 mt-2">ID: {bike.id}</p> <p className="text-xs text-slate-400 mt-2">ID: {bike.id}</p>
<div className="flex gap-2 mt-3">
<button onClick={() => { setEditingBike(bike); setBikeForm({ model: bike.model, plate: bike.plate, status: bike.status }); setAddBikeModal(true); }} className="flex-1 py-1.5 text-xs font-medium text-blue-600 border border-blue-200 rounded-lg hover:bg-blue-50">Edit</button>
<button onClick={() => setDeleteModal({ type: 'bike', item: bike })} className="flex-1 py-1.5 text-xs font-medium text-red-600 border border-red-200 rounded-lg hover:bg-red-50">Delete</button>
</div>
</div>
))}
</div>
</div>
)}
{activeTab === 'batteries' && (
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-800">Hub Batteries ({batteries.length})</h3>
<button onClick={() => { setBatteryForm({ brand: '', model: '', serialNumber: '', chargeLevel: 100, status: 'available' }); setAddBatteryModal(true); }} className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2">
<Plus className="w-4 h-4" /> Add Battery
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{batteries.map(battery => (
<div key={battery.id} className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<div className="flex items-center justify-between mb-2">
<Battery className="w-5 h-5 text-slate-400" />
<span className={`text-xs font-medium px-2 py-1 rounded-full ${battery.status === 'available' ? 'bg-green-100 text-green-700' :
battery.status === 'charging' ? 'bg-blue-100 text-blue-700' :
battery.status === 'deployed' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700'
}`}>
{battery.status}
</span>
</div>
<p className="font-medium text-slate-800">{battery.brand} {battery.model}</p>
<p className="text-sm text-slate-500">SN: {battery.serialNumber}</p>
<div className="mt-2">
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-slate-500">Charge</span>
<span className={`font-medium ${battery.chargeLevel > 50 ? 'text-green-600' : battery.chargeLevel > 20 ? 'text-amber-600' : 'text-red-600'}`}>
{battery.chargeLevel}%
</span>
</div>
<div className="h-1.5 bg-slate-200 rounded-full overflow-hidden">
<div className={`h-full rounded-full ${battery.chargeLevel > 50 ? 'bg-green-500' : battery.chargeLevel > 20 ? 'bg-amber-500' : 'bg-red-500'}`} style={{ width: `${battery.chargeLevel}%` }} />
</div>
</div>
{battery.assignedBike && (
<p className="text-xs text-slate-400 mt-2">Assigned to: {battery.assignedBike}</p>
)}
<div className="mt-2 flex gap-2">
<button
onClick={() => { setAssignModal(battery); setSelectedBike(battery.assignedBike || ''); }}
className="flex-1 py-1.5 text-xs font-medium text-accent border border-accent rounded-lg hover:bg-accent hover:text-white transition-colors"
>
{battery.assignedBike ? 'Reassign' : 'Assign'}
</button>
{battery.assignedBike && (
<button
onClick={() => { setBatteries(prev => prev.map(b => b.id === battery.id ? { ...b, assignedBike: undefined, assignedDate: undefined, status: 'available' as const } : b)); }}
className="py-1.5 px-2 text-xs font-medium text-orange-600 border border-orange-200 rounded-lg hover:bg-orange-50"
>
Unassign
</button>
)}
</div>
<div className="flex gap-2 mt-2">
<button onClick={() => { setEditingBattery(battery); setBatteryForm({ brand: battery.brand, model: battery.model, serialNumber: battery.serialNumber, chargeLevel: battery.chargeLevel, status: battery.status }); setAddBatteryModal(true); }} className="flex-1 py-1.5 text-xs font-medium text-blue-600 border border-blue-200 rounded-lg hover:bg-blue-50">Edit</button>
<button onClick={() => setDeleteModal({ type: 'battery', item: battery })} className="flex-1 py-1.5 text-xs font-medium text-red-600 border border-red-200 rounded-lg hover:bg-red-50">Delete</button>
</div>
</div> </div>
))} ))}
</div> </div>
@@ -370,9 +754,9 @@ export default function HubDetailPage() {
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-800">Hub Rentals ({rentals.length})</h3> <h3 className="font-semibold text-slate-800">Hub Rentals ({rentals.length})</h3>
<button className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2"> {/* <button className="px-4 py-2 bg-accent text-white rounded-lg text-sm flex items-center gap-2">
<Plus className="w-4 h-4" /> New Rental <Plus className="w-4 h-4" /> New Rental
</button> </button> */}
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
@@ -416,11 +800,10 @@ export default function HubDetailPage() {
<span className="text-sm font-medium text-green-600">৳{rental.totalPaid.toLocaleString()}</span> <span className="text-sm font-medium text-green-600">৳{rental.totalPaid.toLocaleString()}</span>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${ <span className={`text-xs font-medium px-2.5 py-1 rounded-full ${rental.status === 'active' ? 'bg-green-100 text-green-700' :
rental.status === 'active' ? 'bg-green-100 text-green-700' : rental.status === 'pending' ? 'bg-amber-100 text-amber-700' :
rental.status === 'pending' ? 'bg-amber-100 text-amber-700' : 'bg-blue-100 text-blue-700'
'bg-blue-100 text-blue-700' }`}>
}`}>
{rental.status} {rental.status}
</span> </span>
</td> </td>
@@ -433,6 +816,382 @@ export default function HubDetailPage() {
)} )}
</div> </div>
</div> </div>
{assignModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">Assign Battery to Bike</h3>
<button onClick={() => setAssignModal(null)} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div className="bg-slate-50 p-3 rounded-lg">
<p className="text-sm text-slate-500">Battery</p>
<p className="font-medium text-slate-800">{assignModal.brand} {assignModal.model}</p>
<p className="text-xs text-slate-500">SN: {assignModal.serialNumber}</p>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Select Bike</label>
<select
value={selectedBike}
onChange={(e) => setSelectedBike(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="">-- Select a bike --</option>
{bikes.filter(b => b.status !== 'maintenance').map(bike => (
<option key={bike.id} value={bike.id}>
{bike.model} - {bike.plate}
</option>
))}
</select>
</div>
{selectedBike && (
<div className="bg-green-50 p-3 rounded-lg">
<p className="text-sm text-green-600">Battery will be assigned to:</p>
<p className="font-medium text-green-800">
{bikes.find(b => b.id === selectedBike)?.model} ({bikes.find(b => b.id === selectedBike)?.plate})
</p>
</div>
)}
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setAssignModal(null)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button
onClick={() => {
setBatteries(prev => prev.map(b => b.id === assignModal.id ? {
...b,
assignedBike: selectedBike || undefined,
assignedDate: selectedBike ? new Date().toISOString().split('T')[0] : undefined,
status: selectedBike ? 'deployed' as const : 'available' as const
} : b));
setAssignModal(null);
}}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark"
>
{assignModal.assignedBike ? 'Update Assignment' : 'Assign Battery'}
</button>
</div>
</div>
</div>
)}
{addBikeModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">{editingBike ? 'Edit Bike' : 'Add New Bike'}</h3>
<button onClick={() => { setAddBikeModal(false); setEditingBike(null); }} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Model</label>
<input type="text" value={bikeForm.model} onChange={(e) => setBikeForm(f => ({ ...f, model: e.target.value }))} placeholder="e.g. AIMA Lightning" disabled={!!editingBike} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">License Plate</label>
<input type="text" value={bikeForm.plate} onChange={(e) => setBikeForm(f => ({ ...f, plate: e.target.value }))} placeholder="e.g. Dhaka Metro Cha-1234" disabled={!!editingBike} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Status</label>
<select value={bikeForm.status} onChange={(e) => setBikeForm(f => ({ ...f, status: e.target.value as any }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="available">Available</option>
<option value="rented">Rented</option>
<option value="maintenance">Maintenance</option>
</select>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => { setAddBikeModal(false); setEditingBike(null); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button
onClick={() => {
if (editingBike) {
setBikes(prev => prev.map(b => b.id === editingBike.id ? { ...b, ...bikeForm } : b));
} else {
const newBike: BikeInfo = { id: `BIKE-${Date.now()}`, ...bikeForm };
setBikes(prev => [...prev, newBike]);
}
setAddBikeModal(false);
setEditingBike(null);
}}
disabled={!bikeForm.model || !bikeForm.plate}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark disabled:opacity-50"
>
{editingBike ? 'Update Bike' : 'Add Bike'}
</button>
</div>
</div>
</div>
)}
{addBatteryModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">{editingBattery ? 'Edit Battery' : 'Add New Battery'}</h3>
<button onClick={() => { setAddBatteryModal(false); setEditingBattery(null); }} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Brand</label>
<input type="text" value={batteryForm.brand} onChange={(e) => setBatteryForm(f => ({ ...f, brand: e.target.value }))} placeholder="e.g. Lithium" disabled={!!editingBattery} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Model</label>
<input type="text" value={batteryForm.model} onChange={(e) => setBatteryForm(f => ({ ...f, model: e.target.value }))} placeholder="e.g. 60V/30Ah" disabled={!!editingBattery} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Serial Number</label>
<input type="text" value={batteryForm.serialNumber} onChange={(e) => setBatteryForm(f => ({ ...f, serialNumber: e.target.value }))} placeholder="e.g. LTH-2024-001" disabled={!!editingBattery} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm disabled:bg-slate-100 disabled:text-slate-500" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Charge Level (%)</label>
<input type="number" min="0" max="100" value={batteryForm.chargeLevel} onChange={(e) => setBatteryForm(f => ({ ...f, chargeLevel: parseInt(e.target.value) || 0 }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Status</label>
<select value={batteryForm.status} onChange={(e) => setBatteryForm(f => ({ ...f, status: e.target.value as any }))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="available">Available</option>
<option value="charging">Charging</option>
<option value="deployed">Deployed</option>
<option value="maintenance">Maintenance</option>
</select>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => { setAddBatteryModal(false); setEditingBattery(null); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button
onClick={() => {
if (editingBattery) {
setBatteries(prev => prev.map(b => b.id === editingBattery.id ? { ...b, ...batteryForm } : b));
} else {
const newBattery: BatteryInfo = { id: `BAT-${Date.now()}`, ...batteryForm, assignedBike: undefined, assignedDate: undefined };
setBatteries(prev => [...prev, newBattery]);
}
setAddBatteryModal(false);
setEditingBattery(null);
}}
disabled={!batteryForm.brand || !batteryForm.model || !batteryForm.serialNumber}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark disabled:opacity-50"
>
{editingBattery ? 'Update Battery' : 'Add Battery'}
</button>
</div>
</div>
</div>
)}
{deleteModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-sm">
<div className="p-6 text-center">
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<AlertTriangle className="w-6 h-6 text-red-600" />
</div>
<h3 className="font-semibold text-slate-800 mb-2">Confirm Delete</h3>
<p className="text-sm text-slate-500">
Are you sure you want to delete this {deleteModal.type === 'bike' ? 'bike' : 'battery'}?
{deleteModal.type === 'bike' && (
<span className="block mt-1 font-medium text-slate-700">{(deleteModal.item as BikeInfo).model} - {(deleteModal.item as BikeInfo).plate}</span>
)}
{deleteModal.type === 'battery' && (
<span className="block mt-1 font-medium text-slate-700">{(deleteModal.item as BatteryInfo).brand} {(deleteModal.item as BatteryInfo).model}</span>
)}
</p>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
<button onClick={() => setDeleteModal(null)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button
onClick={() => {
if (deleteModal.type === 'bike') {
setBikes(prev => prev.filter(b => b.id !== (deleteModal.item as BikeInfo).id));
} else {
setBatteries(prev => prev.filter(b => b.id !== (deleteModal.item as BatteryInfo).id));
}
setDeleteModal(null);
}}
className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700"
>
Delete
</button>
</div>
</div>
</div>
)}
{addEmployeeModal && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md overflow-hidden">
<div className="p-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
<h3 className="font-bold text-slate-800">{editingEmployee ? 'Edit Employee Details' : 'Register New Employee'}</h3>
<button onClick={() => { setAddEmployeeModal(false); setEditingEmployee(null); }} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4 max-h-[80vh] overflow-y-auto">
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Full Name</label>
<input
type="text"
value={employeeForm.name}
onChange={(e) => setEmployeeForm(f => ({ ...f, name: e.target.value }))}
placeholder="e.g. Arif Rahman"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Role</label>
<select
value={employeeForm.role}
onChange={(e) => setEmployeeForm(f => ({ ...f, role: e.target.value as any }))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="Manager">Manager</option>
<option value="Accountant">Accountant</option>
<option value="Staff">Operations Staff</option>
<option value="Technician">Technician</option>
<option value="Support">Support Staff</option>
</select>
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Shift</label>
<select
value={employeeForm.shift}
onChange={(e) => setEmployeeForm(f => ({ ...f, shift: e.target.value as any }))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="Full-time">Full-time</option>
<option value="Morning">Morning</option>
<option value="Evening">Evening</option>
<option value="Night">Night</option>
</select>
</div>
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Email Address</label>
<input
type="email"
value={employeeForm.email}
onChange={(e) => setEmployeeForm(f => ({ ...f, email: e.target.value }))}
placeholder="e.g. arif.rahman@jaiben.com"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
/>
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Phone Number</label>
<input
type="text"
value={employeeForm.phone}
onChange={(e) => setEmployeeForm(f => ({ ...f, phone: e.target.value }))}
placeholder="e.g. +8801711223344"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Status</label>
<select
value={employeeForm.status}
onChange={(e) => setEmployeeForm(f => ({ ...f, status: e.target.value as any }))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="Active">Active / On Duty</option>
<option value="On Leave">On Leave</option>
<option value="Inactive">Inactive</option>
</select>
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider block mb-1">Joining Date</label>
<input
type="date"
value={employeeForm.joiningDate}
onChange={(e) => setEmployeeForm(f => ({ ...f, joiningDate: e.target.value }))}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
/>
</div>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2 bg-slate-50">
<button
onClick={() => { setAddEmployeeModal(false); setEditingEmployee(null); }}
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-semibold hover:bg-slate-100 transition-all"
>
Cancel
</button>
<button
onClick={() => {
if (editingEmployee) {
setEmployees(prev => prev.map(emp => emp.id === editingEmployee.id ? { ...emp, ...employeeForm } : emp));
} else {
const nextIdNum = employees.length > 0
? Math.max(...employees.map(e => parseInt(e.id.split('-')[1]) || 0)) + 1
: 1;
const nextIdStr = `EMP-${nextIdNum.toString().padStart(3, '0')}`;
const newEmployee: Employee = {
id: nextIdStr,
...employeeForm
};
setEmployees(prev => [...prev, newEmployee]);
}
setAddEmployeeModal(false);
setEditingEmployee(null);
}}
disabled={!employeeForm.name || !employeeForm.email || !employeeForm.phone}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm font-semibold hover:opacity-90 disabled:opacity-50 transition-all"
>
{editingEmployee ? 'Save Changes' : 'Register Employee'}
</button>
</div>
</div>
</div>
)}
{deleteEmployeeModal && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-sm overflow-hidden">
<div className="p-6 text-center">
<div className="w-12 h-12 bg-red-50 border border-red-200 rounded-full flex items-center justify-center mx-auto mb-4">
<AlertTriangle className="w-6 h-6 text-red-600" />
</div>
<h3 className="font-bold text-slate-800 text-lg mb-2">Remove Employee</h3>
<p className="text-sm text-slate-500">
Are you sure you want to remove <span className="font-semibold text-slate-700">{deleteEmployeeModal.name}</span> from Gulshan Head Office's operational roster?
</p>
<div className="mt-3 bg-slate-50 p-3 rounded-lg border border-slate-100 text-left">
<p className="text-xs text-slate-400 font-mono">ID: {deleteEmployeeModal.id}</p>
<p className="text-xs font-semibold text-slate-700 capitalize mt-1">Role: {deleteEmployeeModal.role}</p>
</div>
</div>
<div className="p-4 border-t border-slate-100 flex justify-end gap-2 bg-slate-50">
<button
onClick={() => setDeleteEmployeeModal(null)}
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-semibold hover:bg-slate-100 transition-all"
>
Cancel
</button>
<button
onClick={() => {
setEmployees(prev => prev.filter(emp => emp.id !== deleteEmployeeModal.id));
setDeleteEmployeeModal(null);
}}
className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-semibold hover:bg-red-700 transition-all"
>
Confirm Delete
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,191 @@
'use client';
import { useState, useEffect } from 'react';
import { Battery, X } from 'lucide-react';
import toast from 'react-hot-toast';
interface AssignBatteryModalProps {
isOpen: boolean;
onClose: () => void;
investor: any;
batteries: any[];
unassignedBatteries: any[];
preselectedPlanId?: string;
onAssign: (planId: string, batteryIds: string[]) => void;
}
export default function AssignBatteryModal({
isOpen,
onClose,
investor,
batteries,
unassignedBatteries,
preselectedPlanId = '',
onAssign
}: AssignBatteryModalProps) {
const [selectedPlanId, setSelectedPlanId] = useState(preselectedPlanId);
const [selectedBatteryIds, setSelectedBatteryIds] = useState<string[]>([]);
useEffect(() => {
if (isOpen) {
setSelectedPlanId(preselectedPlanId);
setSelectedBatteryIds([]);
}
}, [isOpen, preselectedPlanId]);
if (!isOpen || !investor) return null;
const getPlanTargetAssetCount = (plan: any) => {
if (!plan) return 1;
if (plan.assetType === 'battery' || plan.planName?.toLowerCase().includes('battery')) {
if (plan.id === 'ip3') return 2;
const nameLower = plan.planName?.toLowerCase() || '';
if (nameLower.includes('10')) return 10;
if (nameLower.includes('5')) return 5;
return 1;
} else {
if (plan.id === 'ip1' || plan.id === 'ip2') return 1;
const nameLower = plan.planName?.toLowerCase() || '';
if (nameLower.includes('10')) return 10;
if (nameLower.includes('5')) return 5;
return 1;
}
};
const selectedPlan = investor.investments?.find((inv: any) => inv.id === selectedPlanId);
const assignedCount = selectedPlan ? batteries.filter(b => b.investmentId === selectedPlan.id).length : 0;
const targetCount = selectedPlan ? getPlanTargetAssetCount(selectedPlan) : 0;
const remainingCapacity = selectedPlan ? Math.max(0, targetCount - assignedCount) : 0;
const handleAssignSubmit = () => {
if (!selectedPlanId) {
toast.error('Please select an investment plan');
return;
}
if (selectedBatteryIds.length === 0) {
toast.error('Please select at least one battery');
return;
}
onAssign(selectedPlanId, selectedBatteryIds);
onClose();
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md overflow-hidden flex flex-col">
<div className="p-5 border-b border-emerald-100 bg-emerald-50 flex items-center justify-between">
<h2 className="text-lg font-bold text-emerald-800 flex items-center gap-2">
<Battery className="w-5 h-5 text-emerald-600 animate-bounce" />
Assign Battery to Partner
</h2>
<button onClick={onClose} className="p-2 hover:bg-emerald-100 rounded-lg text-emerald-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-5 space-y-4 overflow-y-auto max-h-[70vh]">
<div>
<label className="text-sm font-medium text-slate-600 mb-1.5 block">Link to Investment Plan *</label>
<select
value={selectedPlanId}
disabled={!!preselectedPlanId}
onChange={(e) => {
setSelectedPlanId(e.target.value);
setSelectedBatteryIds([]);
}}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-emerald-500 disabled:bg-slate-50 disabled:text-slate-500"
>
<option value="">Select plan</option>
{investor.investments?.filter((inv: any) => inv.assetType === 'battery' || inv.planName.toLowerCase().includes('battery')).map((inv: any) => {
const curAssigned = batteries.filter(b => b.investmentId === inv.id).length;
const target = getPlanTargetAssetCount(inv);
const rem = Math.max(0, target - curAssigned);
return (
<option key={inv.id} value={inv.id}>
{inv.planName} (Remaining: {rem} / {target} Pack{target !== 1 ? 's' : ''})
</option>
);
})}
</select>
</div>
{selectedPlanId && (
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="text-sm font-semibold text-slate-700 block">Select Battery Pack(s) *</label>
<span className="text-xs font-bold text-emerald-700 bg-emerald-100 px-2 py-0.5 rounded-full">
Selected: {selectedBatteryIds.length} / {remainingCapacity}
</span>
</div>
{remainingCapacity === 0 ? (
<div className="p-3 text-center text-xs text-amber-700 bg-amber-50 rounded-lg border border-amber-200">
This plan has reached its full capacity of {targetCount} battery pack(s). Unassign some batteries first to assign new ones.
</div>
) : (
<div className="border border-slate-200 rounded-lg max-h-56 overflow-y-auto divide-y divide-slate-100">
{unassignedBatteries.map(bat => {
const isChecked = selectedBatteryIds.includes(bat.id);
const isDisabled = !isChecked && selectedBatteryIds.length >= remainingCapacity;
return (
<label
key={bat.id}
className={`flex items-center justify-between p-3 cursor-pointer text-sm transition-colors ${
isChecked ? 'bg-emerald-50/50' : isDisabled ? 'opacity-50 cursor-not-allowed bg-slate-50' : 'hover:bg-slate-50'
}`}
>
<div className="flex items-center gap-2.5">
<input
type="checkbox"
checked={isChecked}
disabled={isDisabled}
onChange={(e) => {
if (e.target.checked) {
if (selectedBatteryIds.length < remainingCapacity) {
setSelectedBatteryIds([...selectedBatteryIds, bat.id]);
} else {
toast.error(`Cannot select more than ${remainingCapacity} batteries`);
}
} else {
setSelectedBatteryIds(selectedBatteryIds.filter(id => id !== bat.id));
}
}}
className="rounded text-emerald-600 focus:ring-emerald-500 border-slate-300 w-4 h-4"
/>
<div>
<p className="font-semibold text-slate-800">{bat.brand} {bat.model}</p>
<p className="text-xs text-slate-500">SN: {bat.serialNumber}</p>
</div>
</div>
<span className="text-slate-600 font-medium text-xs">{bat.purchasePrice?.toLocaleString() || 0}</span>
</label>
);
})}
{unassignedBatteries.length === 0 && (
<div className="p-4 text-center text-slate-400 text-sm">No unassigned batteries available</div>
)}
</div>
)}
</div>
)}
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3 bg-slate-50">
<button
onClick={onClose}
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-semibold hover:bg-slate-100 bg-white"
>
Cancel
</button>
<button
onClick={handleAssignSubmit}
disabled={!selectedPlanId || selectedBatteryIds.length === 0}
className="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm font-semibold hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm transition-colors"
>
Assign {selectedBatteryIds.length > 0 ? `${selectedBatteryIds.length} Battery/ies` : 'Battery'}
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,191 @@
'use client';
import { useState, useEffect } from 'react';
import { Bike, X } from 'lucide-react';
import toast from 'react-hot-toast';
interface AssignBikeModalProps {
isOpen: boolean;
onClose: () => void;
investor: any;
bikes: any[];
preselectedPlanId?: string;
onAssign: (planId: string, bikeIds: string[]) => void;
}
export default function AssignBikeModal({
isOpen,
onClose,
investor,
bikes,
preselectedPlanId = '',
onAssign
}: AssignBikeModalProps) {
const [selectedPlanId, setSelectedPlanId] = useState(preselectedPlanId);
const [selectedBikeIds, setSelectedBikeIds] = useState<string[]>([]);
useEffect(() => {
if (isOpen) {
setSelectedPlanId(preselectedPlanId);
setSelectedBikeIds([]);
}
}, [isOpen, preselectedPlanId]);
if (!isOpen || !investor) return null;
const getPlanTargetAssetCount = (plan: any) => {
if (!plan) return 1;
if (plan.assetType === 'battery' || plan.planName?.toLowerCase().includes('battery')) {
if (plan.id === 'ip3') return 2;
const nameLower = plan.planName?.toLowerCase() || '';
if (nameLower.includes('10')) return 10;
if (nameLower.includes('5')) return 5;
return 1;
} else {
if (plan.id === 'ip1' || plan.id === 'ip2') return 1;
const nameLower = plan.planName?.toLowerCase() || '';
if (nameLower.includes('10')) return 10;
if (nameLower.includes('5')) return 5;
return 1;
}
};
const selectedPlan = investor.investments?.find((inv: any) => inv.id === selectedPlanId);
const assignedCount = selectedPlan ? bikes.filter(b => b.investmentId === selectedPlan.id).length : 0;
const targetCount = selectedPlan ? getPlanTargetAssetCount(selectedPlan) : 0;
const remainingCapacity = selectedPlan ? Math.max(0, targetCount - assignedCount) : 0;
const availableBikes = bikes.filter(b => !b.investorId && b.status === 'available');
const handleAssignSubmit = () => {
if (!selectedPlanId) {
toast.error('Please select an investment plan');
return;
}
if (selectedBikeIds.length === 0) {
toast.error('Please select at least one bike');
return;
}
onAssign(selectedPlanId, selectedBikeIds);
onClose();
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md overflow-hidden flex flex-col">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="text-lg font-bold text-investor flex items-center gap-2">
<Bike className="w-5 h-5 text-investor animate-bounce" />
Assign Bike to Investor
</h2>
<button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-5 space-y-4 overflow-y-auto max-h-[70vh]">
<div>
<label className="text-sm font-medium text-slate-600 mb-1.5 block">Select Investment Plan *</label>
<select
value={selectedPlanId}
disabled={!!preselectedPlanId}
onChange={(e) => {
setSelectedPlanId(e.target.value);
setSelectedBikeIds([]);
}}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-investor disabled:bg-slate-50 disabled:text-slate-500"
>
<option value="">Choose an active plan</option>
{investor.investments?.filter((inv: any) => inv.assetType === 'bike' || !inv.assetType || inv.planName.toLowerCase().includes('ev') || inv.planName.toLowerCase().includes('bike')).map((inv: any) => {
const curAssigned = bikes.filter(b => b.investmentId === inv.id).length;
const target = getPlanTargetAssetCount(inv);
const rem = Math.max(0, target - curAssigned);
return (
<option key={inv.id} value={inv.id}>
{inv.planName} (Remaining: {rem} / {target} Bike{target !== 1 ? 's' : ''})
</option>
);
})}
</select>
</div>
{selectedPlanId && (
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="text-sm font-semibold text-slate-700 block">Select Bike(s) *</label>
<span className="text-xs font-bold text-investor bg-investor/10 px-2 py-0.5 rounded-full">
Selected: {selectedBikeIds.length} / {remainingCapacity}
</span>
</div>
{remainingCapacity === 0 ? (
<div className="p-3 text-center text-xs text-amber-700 bg-amber-50 rounded-lg border border-amber-200">
This plan has reached its full capacity of {targetCount} bike(s). Unassign some bikes first to assign new ones.
</div>
) : (
<div className="border border-slate-200 rounded-lg max-h-48 overflow-y-auto divide-y divide-slate-100">
{availableBikes.map(bike => {
const isChecked = selectedBikeIds.includes(bike.id);
const isDisabled = !isChecked && selectedBikeIds.length >= remainingCapacity;
return (
<label
key={bike.id}
className={`flex items-center justify-between p-3 cursor-pointer text-sm transition-colors ${
isChecked ? 'bg-investor/5' : isDisabled ? 'opacity-50 cursor-not-allowed bg-slate-50' : 'hover:bg-slate-50'
}`}
>
<div className="flex items-center gap-2.5">
<input
type="checkbox"
checked={isChecked}
disabled={isDisabled}
onChange={(e) => {
if (e.target.checked) {
if (selectedBikeIds.length < remainingCapacity) {
setSelectedBikeIds([...selectedBikeIds, bike.id]);
} else {
toast.error(`Cannot select more than ${remainingCapacity} bikes`);
}
} else {
setSelectedBikeIds(selectedBikeIds.filter(id => id !== bike.id));
}
}}
className="rounded text-investor focus:ring-investor border-slate-300 w-4 h-4"
/>
<div>
<p className="font-semibold text-slate-800">{bike.model} {bike.brand}</p>
<p className="text-xs text-slate-500">{bike.plateNumber}</p>
</div>
</div>
<span className="text-slate-600 font-medium text-xs">{bike.purchasePrice?.toLocaleString() || 0}</span>
</label>
);
})}
{availableBikes.length === 0 && (
<div className="p-4 text-center text-slate-400 text-sm">No unassigned available bikes found</div>
)}
</div>
)}
</div>
)}
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3 bg-slate-50">
<button
onClick={onClose}
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-semibold hover:bg-slate-100 bg-white"
>
Cancel
</button>
<button
onClick={handleAssignSubmit}
disabled={!selectedPlanId || selectedBikeIds.length === 0}
className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-semibold hover:bg-investor-dark disabled:opacity-50 disabled:cursor-not-allowed shadow-sm transition-colors"
>
Assign {selectedBikeIds.length > 0 ? `${selectedBikeIds.length} Bike(s)` : 'Bike'}
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,57 @@
'use client';
import { AlertTriangle } from 'lucide-react';
interface UnassignConfirmModalProps {
isOpen: boolean;
onClose: () => void;
type: 'bike' | 'battery';
name: string;
details: string;
onConfirm: () => void;
}
export default function UnassignConfirmModal({
isOpen,
onClose,
type,
name,
details,
onConfirm
}: UnassignConfirmModalProps) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4 animate-in fade-in zoom-in-95 duration-200">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden border border-slate-100">
<div className="p-6 text-center">
<div className="w-16 h-16 bg-red-50 rounded-full flex items-center justify-center mx-auto mb-4 border border-red-100">
<AlertTriangle className="w-8 h-8 text-red-600 animate-pulse" />
</div>
<h3 className="text-lg font-bold text-slate-900 mb-2">Unassign Confirmation</h3>
<p className="text-sm text-slate-500 mb-6 px-1">
Are you sure you want to unassign {type} <span className="font-semibold text-slate-800">{name}</span> ({details})?
</p>
<div className="flex gap-3 justify-center">
<button
onClick={onClose}
className="px-5 py-2.5 border border-slate-200 text-slate-600 rounded-xl text-sm font-semibold hover:bg-slate-50 transition-colors flex-1"
>
Cancel
</button>
<button
onClick={() => {
onConfirm();
onClose();
}}
className="px-5 py-2.5 bg-red-600 text-white rounded-xl text-sm font-semibold hover:bg-red-700 hover:shadow-lg transition-all flex-1"
>
Unassign
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -2,9 +2,12 @@
import { useState } from 'react'; import { useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { investors as initialInvestors, bikes as initialBikes, transactions } from '@/data/mockData'; import { investors as initialInvestors, bikes as initialBikes } from '@/data/mockData';
import type { Investor } from '@/data/mockData'; import type { Investor } from '@/data/mockData';
import { Plus, Search, Filter, Download, Upload, Eye, Edit, Trash2, X, LayoutGrid, List, Phone, Mail, MapPin, Calendar, Banknote, TrendingUp, Wallet, AlertTriangle, User, FileText, CreditCard, Smartphone, Shield, Star, ExternalLink } from 'lucide-react';
type InvestorWithImage = Investor & { profileImage: string };
import { Search, Download, Upload, Edit, Trash2, X, LayoutGrid, List, Phone, Mail, Wallet, TrendingUp, Banknote, Bike, Eye } from 'lucide-react';
import toast from 'react-hot-toast';
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
active: 'bg-green-100 text-green-700', active: 'bg-green-100 text-green-700',
@@ -27,164 +30,47 @@ const kycColors: Record<string, string> = {
not_submitted: 'bg-slate-100 text-slate-500', not_submitted: 'bg-slate-100 text-slate-500',
}; };
const investorsWithImages = initialInvestors.map((inv, idx) => ({
...inv,
profileImage: `https://picsum.photos/200/200?random=${idx + 1}`,
}));
export default function InvestorsPage() { export default function InvestorsPage() {
const [investors, setInvestors] = useState<Investor[]>(initialInvestors); const [investors, setInvestors] = useState<InvestorWithImage[]>(investorsWithImages as InvestorWithImage[]);
const [bikes, setBikes] = useState(initialBikes);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all');
const [planFilter, setPlanFilter] = useState('all'); const [kycFilter, setKycFilter] = useState('all');
const [selectedInvestor, setSelectedInvestor] = useState<Investor | null>(null);
const [showModal, setShowModal] = useState(false);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [editingInvestor, setEditingInvestor] = useState<Investor | null>(null);
const [activeTab, setActiveTab] = useState('personal');
const [sortField, setSortField] = useState<string>('name');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table'); const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
const [showAssignBikeModal, setShowAssignBikeModal] = useState(false); const [deleteModal, setDeleteModal] = useState<{ show: boolean; investor: InvestorWithImage | null }>({ show: false, investor: null });
const [selectedBikeId, setSelectedBikeId] = useState('');
const filteredInvestors = investors.filter(inv => { const filteredInvestors = investors.filter(inv => {
const matchesSearch = inv.name.toLowerCase().includes(searchQuery.toLowerCase()) || const matchesSearch = inv.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
inv.email.toLowerCase().includes(searchQuery.toLowerCase()) || inv.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
inv.phone.includes(searchQuery) || inv.phone.includes(searchQuery);
inv.id.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = statusFilter === 'all' || inv.status === statusFilter; const matchesStatus = statusFilter === 'all' || inv.status === statusFilter;
const matchesPlan = planFilter === 'all' || (inv.investments?.some(inv => inv.planType === planFilter) ?? false); const matchesKyc = kycFilter === 'all' || inv.kycStatus === kycFilter;
return matchesSearch && matchesStatus && matchesPlan; return matchesSearch && matchesStatus && matchesKyc;
}); });
const sortedInvestors = [...filteredInvestors].sort((a, b) => { const handleDeleteInvestor = () => {
const aVal = String(a[sortField as keyof Investor] || ''); if (deleteModal.investor) {
const bVal = String(b[sortField as keyof Investor] || ''); setInvestors(investors.filter(i => i.id !== deleteModal.investor!.id));
if (sortOrder === 'asc') return aVal.localeCompare(bVal); setDeleteModal({ show: false, investor: null });
return bVal.localeCompare(aVal); toast.success('Investor deleted successfully');
});
const handleSort = (field: string) => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortOrder('asc');
} }
}; };
const handleAddInvestor = () => { const getInitials = (name: string) => {
setEditingInvestor({ return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
id: `INV${String(investors.length + 1).padStart(3, '0')}`,
userId: `u${100 + investors.length + 1}`,
name: '',
email: '',
phone: '',
address: '',
totalInvested: 0,
totalEarnings: 0,
activeBikes: 0,
withdrawalPending: 0,
totalWithdrawn: 0,
pendingEarnings: 0,
roi: 0,
status: 'pending',
createdAt: new Date().toISOString().split('T')[0],
kycStatus: 'not_submitted',
riskLevel: 'low',
totalReferrals: 0,
referralEarnings: 0,
investments: [],
});
setShowModal(true);
}; };
const handleEditInvestor = (inv: Investor) => { const isMobile = typeof window !== 'undefined' && window.innerWidth < 1024;
setEditingInvestor(JSON.parse(JSON.stringify(inv)));
setShowModal(true);
};
const handleDeleteInvestor = (id: string) => {
if (confirm('Are you sure you want to delete this investor?')) {
setInvestors(investors.filter(i => i.id !== id));
}
};
const handleSaveInvestor = () => {
if (editingInvestor) {
const existingIndex = investors.findIndex(i => i.id === editingInvestor.id);
if (existingIndex >= 0) {
const newInvestors = [...investors];
newInvestors[existingIndex] = editingInvestor;
setInvestors(newInvestors);
} else {
setInvestors([...investors, editingInvestor]);
}
}
setShowModal(false);
setEditingInvestor(null);
};
const handleAssignBike = () => {
if (selectedInvestor && selectedBikeId) {
const bikeIndex = bikes.findIndex(b => b.id === selectedBikeId);
if (bikeIndex >= 0) {
const updatedBikes = [...bikes];
updatedBikes[bikeIndex] = { ...updatedBikes[bikeIndex], investorId: selectedInvestor.id };
setBikes(updatedBikes);
const investorIndex = investors.findIndex(i => i.id === selectedInvestor.id);
if (investorIndex >= 0) {
const updatedInvestors = [...investors];
updatedInvestors[investorIndex] = {
...updatedInvestors[investorIndex],
activeBikes: updatedInvestors[investorIndex].activeBikes + 1,
totalInvested: updatedInvestors[investorIndex].totalInvested + (bikes[bikeIndex].purchasePrice || 0),
};
setInvestors(updatedInvestors);
}
}
setShowAssignBikeModal(false);
setSelectedBikeId('');
}
};
const handleUnassignBike = (bikeId: string) => {
if (selectedInvestor && confirm('Are you sure you want to unassign this bike from the investor?')) {
const bikeIndex = bikes.findIndex(b => b.id === bikeId);
if (bikeIndex >= 0) {
const bike = bikes[bikeIndex];
const updatedBikes = [...bikes];
updatedBikes[bikeIndex] = { ...updatedBikes[bikeIndex], investorId: undefined };
setBikes(updatedBikes);
const investorIndex = investors.findIndex(i => i.id === selectedInvestor.id);
if (investorIndex >= 0) {
const updatedInvestors = [...investors];
updatedInvestors[investorIndex] = {
...updatedInvestors[investorIndex],
activeBikes: Math.max(0, updatedInvestors[investorIndex].activeBikes - 1),
totalInvested: updatedInvestors[investorIndex].totalInvested - (bike.purchasePrice || 0),
};
setInvestors(updatedInvestors);
}
}
}
};
const availableBikesForAssignment = bikes.filter(b => !b.investorId && b.status === 'available');
const assignedBikes = bikes.filter(b => b.investorId === selectedInvestor?.id);
return ( return (
<div className="p-4 lg:p-6"> <div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6"> <div className="mb-6">
<div> <h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Investors</h1>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Investor Management</h1> <p className="text-sm text-slate-500 mt-1">Manage investor accounts and investments</p>
<p className="text-sm text-slate-500 mt-1">Manage investor accounts and investments</p>
</div>
<div className="flex items-center gap-2">
<button onClick={handleAddInvestor} className="py-2.5 px-4 bg-investor text-white rounded-lg font-semibold text-sm hover:bg-investor-dark transition-colors flex items-center gap-2">
<Plus className="w-4 h-4" /> Add Investor
</button>
</div>
</div> </div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
@@ -213,7 +99,7 @@ export default function InvestorsPage() {
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100"> <div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center"> <div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
<Banknote className="w-6 h-6 text-blue-600" /> <Bike className="w-6 h-6 text-blue-600" />
</div> </div>
<div> <div>
<p className="text-2xl font-extrabold text-slate-800">{investors.reduce((sum, i) => sum + i.activeBikes, 0)}</p> <p className="text-2xl font-extrabold text-slate-800">{investors.reduce((sum, i) => sum + i.activeBikes, 0)}</p>
@@ -224,7 +110,7 @@ export default function InvestorsPage() {
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100"> <div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-amber-50 flex items-center justify-center"> <div className="w-12 h-12 rounded-xl bg-amber-50 flex items-center justify-center">
<Calendar className="w-6 h-6 text-amber-600" /> <Banknote className="w-6 h-6 text-amber-600" />
</div> </div>
<div> <div>
<p className="text-2xl font-extrabold text-slate-800">{investors.length}</p> <p className="text-2xl font-extrabold text-slate-800">{investors.length}</p>
@@ -240,17 +126,17 @@ export default function InvestorsPage() {
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input <input
type="text" type="text"
placeholder="Search investors by name, email, phone..." placeholder="Search by name, email, phone..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent" className="w-full pl-10 pr-4 py-2.5 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
/> />
</div> </div>
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<select <select
value={statusFilter} value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)} onChange={(e) => setStatusFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 bg-white" className="py-2.5 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 bg-white"
> >
<option value="all">All Status</option> <option value="all">All Status</option>
<option value="active">Active</option> <option value="active">Active</option>
@@ -259,37 +145,134 @@ export default function InvestorsPage() {
<option value="suspended">Suspended</option> <option value="suspended">Suspended</option>
</select> </select>
<select <select
value={planFilter} value={kycFilter}
onChange={(e) => setPlanFilter(e.target.value)} onChange={(e) => setKycFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 bg-white" className="py-2.5 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 bg-white"
> >
<option value="all">All Plans</option> <option value="all">All KYC</option>
<option value="silver">Silver</option> <option value="verified">Verified</option>
<option value="gold">Gold</option> <option value="pending">Pending</option>
<option value="platinum">Platinum</option>
<option value="diamond">Diamond</option>
</select> </select>
<button <button
onClick={() => setViewMode(viewMode === 'table' ? 'cards' : 'table')} onClick={() => setViewMode(viewMode === 'table' ? 'cards' : 'table')}
className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2" className="py-2.5 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2"
> >
{viewMode === 'table' ? <LayoutGrid className="w-4 h-4" /> : <List className="w-4 h-4" />} <LayoutGrid className="w-4 h-4" />
{viewMode === 'table' ? 'Cards' : 'Table'} {viewMode === 'table' ? 'Cards' : 'Table'}
</button> </button>
<button className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2"> <button className="py-2.5 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2">
<Download className="w-4 h-4" /> Export <Download className="w-4 h-4" /> Export
</button> </button>
<button className="py-2 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2"> <button className="py-2.5 px-3 border border-slate-200 rounded-lg text-sm font-medium text-slate-600 hover:bg-slate-50 flex items-center gap-2">
<Upload className="w-4 h-4" /> Import <Upload className="w-4 h-4" /> Import
</button> </button>
</div> </div>
</div> </div>
{viewMode === 'table' ? ( {(viewMode === 'cards' || isMobile) ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 p-4">
{filteredInvestors.map(inv => (
<div key={inv.id} className="bg-white rounded-xl border border-slate-200 p-5 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
{inv.profileImage ? (
<img
src={inv.profileImage}
alt={inv.name}
className="w-12 h-12 rounded-full object-cover"
/>
) : (
<div className="w-12 h-12 rounded-full bg-purple-100 flex items-center justify-center">
<span className="text-sm font-bold text-purple-600">{getInitials(inv.name)}</span>
</div>
)}
<div>
<p className="font-semibold text-slate-800">{inv.name}</p>
<p className="text-xs text-slate-400">{inv.id}</p>
</div>
</div>
</div>
<div className="flex flex-wrap gap-2 mb-4">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full capitalize ${kycColors[inv.kycStatus]}`}>
{inv.kycStatus}
</span>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full capitalize ${statusColors[inv.status]}`}>
{inv.status}
</span>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-center gap-2 text-sm text-slate-500">
<Phone className="w-4 h-4" />
<span>{inv.phone}</span>
</div>
<div className="flex items-center gap-2 text-sm text-slate-500">
<Mail className="w-4 h-4" />
<span className="truncate">{inv.email}</span>
</div>
</div>
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-400">Investment</p>
<p className="font-semibold text-purple-600">{inv.totalInvested.toLocaleString()}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-400">Earnings</p>
<p className="font-semibold text-green-600">{inv.totalEarnings.toLocaleString()}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-400">ROI</p>
<p className="font-semibold text-slate-700">{inv.roi}%</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-400">Bikes</p>
<p className="font-semibold text-slate-700">{inv.activeBikes}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-400">Plans</p>
<p className="font-semibold text-slate-700">{inv.investments?.length || 0}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-400">Plan Type</p>
<span className={`inline-flex text-xs font-medium px-2 py-0.5 rounded-full capitalize ${planColors[inv.investments?.[0]?.planType || 'silver']}`}>
{inv.investments?.[0]?.planType || 'silver'}
</span>
</div>
</div>
<div className="flex items-center gap-2 pt-3 border-t border-slate-100">
<Link
href={`/admin/investors/${inv.id}`}
className="flex-1 py-2 px-3 bg-slate-100 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-200 text-center"
>
View Details
</Link>
<Link
href={`/admin/investors/${inv.id}`}
className="p-2 hover:bg-slate-100 rounded-lg"
title="Edit"
>
<Edit className="w-4 h-4 text-slate-400" />
</Link>
<button
onClick={() => setDeleteModal({ show: true, investor: inv })}
className="p-2 hover:bg-red-50 rounded-lg"
title="Delete"
>
<Trash2 className="w-4 h-4 text-red-400" />
</button>
</div>
</div>
))}
</div>
) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead className="bg-slate-50"> <thead className="bg-slate-50">
<tr> <tr>
{/* <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Avatar</th> */}
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Investor</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Investor</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Contact</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Contact</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Investment</th> <th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Investment</th>
@@ -302,13 +285,21 @@ export default function InvestorsPage() {
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-50"> <tbody className="divide-y divide-slate-50">
{sortedInvestors.map(inv => ( {filteredInvestors.map(inv => (
<tr key={inv.id} className="hover:bg-slate-50 transition-colors"> <tr key={inv.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3"> <td className="px-4 py-3">
<Link href={`/admin/investors/${inv.id}`} className="flex items-center gap-3 hover:bg-slate-50 -m-2 p-2 rounded-lg"> <Link href={`/admin/investors/${inv.id}`} className="flex items-center gap-3 hover:bg-slate-50 -m-2 p-2 rounded-lg">
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center"> {inv.profileImage ? (
<span className="text-sm font-bold text-purple-600">{inv.name.charAt(0)}</span> <img
</div> src={inv.profileImage}
alt={inv.name}
className="w-10 h-10 rounded-full object-cover"
/>
) : (
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center">
<span className="text-sm font-bold text-purple-600">{getInitials(inv.name)}</span>
</div>
)}
<div> <div>
<p className="text-sm font-medium text-slate-700">{inv.name}</p> <p className="text-sm font-medium text-slate-700">{inv.name}</p>
<p className="text-xs text-slate-400">{inv.id}</p> <p className="text-xs text-slate-400">{inv.id}</p>
@@ -331,28 +322,32 @@ export default function InvestorsPage() {
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full capitalize ${planColors[inv.investments?.[0]?.planType || 'silver']}`}> <span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full capitalize ${planColors[inv.investments?.[0]?.planType || 'silver']}`}>
{inv.investments?.length || 0} Plan{inv.investments?.length !== 1 ? 's' : ''} {inv.investments?.[0]?.planType || 'silver'}
</span> </span>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${kycColors[inv.kycStatus]}`}> <span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full capitalize ${kycColors[inv.kycStatus]}`}>
{inv.kycStatus} {inv.kycStatus}
</span> </span>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[inv.status]}`}> <span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full capitalize ${statusColors[inv.status]}`}>
{inv.status} {inv.status}
</span> </span>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button onClick={() => { setSelectedInvestor(inv); setShowDetailsModal(true); }} className="p-2 hover:bg-slate-100 rounded-lg" title="View Details"> <Link href={`/admin/investors/${inv.id}`} className="p-2 hover:bg-slate-100 rounded-lg" title="View Details">
<Eye className="w-4 h-4 text-blue-500" /> <Eye className="w-4 h-4 text-slate-400" />
</button> </Link>
<button onClick={() => handleEditInvestor(inv)} className="p-2 hover:bg-slate-100 rounded-lg" title="Edit"> <Link href={`/admin/investors/${inv.id}`} className="p-2 hover:bg-slate-100 rounded-lg" title="Edit">
<Edit className="w-4 h-4 text-slate-400" /> <Edit className="w-4 h-4 text-slate-400" />
</button> </Link>
<button onClick={() => handleDeleteInvestor(inv.id)} className="p-2 hover:bg-red-50 rounded-lg" title="Delete"> <button
onClick={() => setDeleteModal({ show: true, investor: inv })}
className="p-2 hover:bg-red-50 rounded-lg"
title="Delete"
>
<Trash2 className="w-4 h-4 text-red-400" /> <Trash2 className="w-4 h-4 text-red-400" />
</button> </button>
</div> </div>
@@ -362,510 +357,35 @@ export default function InvestorsPage() {
</tbody> </tbody>
</table> </table>
</div> </div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
{sortedInvestors.map(inv => (
<Link key={inv.id} href={`/admin/investors/${inv.id}`} className="block bg-slate-50 rounded-xl p-4 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-white flex items-center justify-center shadow-sm">
<span className="text-lg font-bold text-purple-600">{inv.name.charAt(0)}</span>
</div>
<div>
<p className="font-semibold text-slate-700">{inv.name}</p>
<p className="text-xs text-slate-400">{inv.id}</p>
</div>
</div>
<button onClick={(e) => { e.preventDefault(); setSelectedInvestor(inv); setShowDetailsModal(true); }} className="p-1.5 hover:bg-white rounded-lg">
<Eye className="w-4 h-4 text-blue-500" />
</button>
</div>
<div className="flex flex-wrap gap-2 mb-3">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[inv.status]}`}>
{inv.status}
</span>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${planColors[inv.investments?.[0]?.planType || 'silver']}`}>
{inv.investments?.length || 0} Plans
</span>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${kycColors[inv.kycStatus]}`}>
{inv.kycStatus}
</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="bg-white rounded-lg p-2">
<p className="text-slate-400">Invested</p>
<p className="font-medium text-purple-600">{inv.totalInvested.toLocaleString()}</p>
</div>
<div className="bg-white rounded-lg p-2">
<p className="text-slate-400">Earnings</p>
<p className="font-medium text-green-600">{inv.totalEarnings.toLocaleString()}</p>
</div>
<div className="bg-white rounded-lg p-2">
<p className="text-slate-400">Bikes</p>
<p className="font-medium text-slate-700">{inv.activeBikes}</p>
</div>
<div className="bg-white rounded-lg p-2">
<p className="text-slate-400">ROI</p>
<p className="font-medium text-slate-700">{inv.roi}%</p>
</div>
</div>
</Link>
))}
</div>
)} )}
</div> </div>
{showModal && editingInvestor && ( {deleteModal.show && deleteModal.investor && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="text-lg font-bold text-slate-800">
{investors.find(i => i.id === editingInvestor.id) ? 'Edit Investor' : 'Add New Investor'}
</h2>
<button onClick={() => setShowModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="border-b border-slate-100 flex overflow-x-auto">
<button
onClick={() => setActiveTab('personal')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'personal' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
>
<User className="w-4 h-4 inline mr-1" /> Personal
</button>
<button
onClick={() => setActiveTab('financial')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'financial' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
>
<Banknote className="w-4 h-4 inline mr-1" /> Financial
</button>
<button
onClick={() => setActiveTab('payment')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'payment' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
>
<CreditCard className="w-4 h-4 inline mr-1" /> Payment
</button>
<button
onClick={() => setActiveTab('documents')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'documents' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
>
<FileText className="w-4 h-4 inline mr-1" /> Documents
</button>
<button
onClick={() => setActiveTab('investment')}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap ${activeTab === 'investment' ? 'border-b-2 border-accent text-accent' : 'text-slate-500'}`}
>
<TrendingUp className="w-4 h-4 inline mr-1" /> Investment
</button>
</div>
<div className="p-5 overflow-y-auto flex-1">
{activeTab === 'personal' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Full Name *</label>
<input type="text" value={editingInvestor.name} onChange={(e) => setEditingInvestor({ ...editingInvestor, name: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Enter full name" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Phone Number *</label>
<input type="text" value={editingInvestor.phone} onChange={(e) => setEditingInvestor({ ...editingInvestor, phone: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="01XXXXXXXXX" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Alternate Phone</label>
<input type="text" value={editingInvestor.phoneAlt || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, phoneAlt: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Optional" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Email *</label>
<input type="email" value={editingInvestor.email} onChange={(e) => setEditingInvestor({ ...editingInvestor, email: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="email@example.com" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Date of Birth</label>
<input type="date" value={editingInvestor.dateOfBirth || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, dateOfBirth: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">NID Number</label>
<input type="text" value={editingInvestor.nidNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, nidNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="NID Number" />
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Address</label>
<textarea value={editingInvestor.address} onChange={(e) => setEditingInvestor({ ...editingInvestor, address: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter full address" />
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Emergency Contact</label>
<input type="text" value={editingInvestor.emergencyContactName || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, emergencyContactName: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Contact Name" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Relation</label>
<input type="text" value={editingInvestor.emergencyContactRelation || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, emergencyContactRelation: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Relation" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Phone</label>
<input type="text" value={editingInvestor.emergencyContactPhone || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, emergencyContactPhone: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Phone" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Status</label>
<select value={editingInvestor.status} onChange={(e) => setEditingInvestor({ ...editingInvestor, status: e.target.value as any })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="inactive">Inactive</option>
<option value="suspended">Suspended</option>
</select>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">KYC Status</label>
<select value={editingInvestor.kycStatus} onChange={(e) => setEditingInvestor({ ...editingInvestor, kycStatus: e.target.value as any })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="verified">Verified</option>
<option value="pending">Pending</option>
<option value="rejected">Rejected</option>
<option value="not_submitted">Not Submitted</option>
</select>
</div>
</div>
</div>
)}
{activeTab === 'financial' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Total Invested ()</label>
<input type="number" value={editingInvestor.totalInvested} onChange={(e) => setEditingInvestor({ ...editingInvestor, totalInvested: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Total Earnings ()</label>
<input type="number" value={editingInvestor.totalEarnings} onChange={(e) => setEditingInvestor({ ...editingInvestor, totalEarnings: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Total Withdrawn ()</label>
<input type="number" value={editingInvestor.totalWithdrawn} onChange={(e) => setEditingInvestor({ ...editingInvestor, totalWithdrawn: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Pending Earnings ()</label>
<input type="number" value={editingInvestor.pendingEarnings} onChange={(e) => setEditingInvestor({ ...editingInvestor, pendingEarnings: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Withdrawal Pending ()</label>
<input type="number" value={editingInvestor.withdrawalPending} onChange={(e) => setEditingInvestor({ ...editingInvestor, withdrawalPending: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">ROI (%)</label>
<input type="number" value={editingInvestor.roi} onChange={(e) => setEditingInvestor({ ...editingInvestor, roi: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Referral Earnings ()</label>
<input type="number" value={editingInvestor.referralEarnings} onChange={(e) => setEditingInvestor({ ...editingInvestor, referralEarnings: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Total Referrals</label>
<input type="number" value={editingInvestor.totalReferrals} onChange={(e) => setEditingInvestor({ ...editingInvestor, totalReferrals: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
)}
{activeTab === 'payment' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Bank Name</label>
<input type="text" value={editingInvestor.bankName || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, bankName: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Bank Name" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Bank Branch</label>
<input type="text" value={editingInvestor.bankBranch || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, bankBranch: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Branch" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Account Name</label>
<input type="text" value={editingInvestor.bankAccountName || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, bankAccountName: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Account Name" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Account Number</label>
<input type="text" value={editingInvestor.bankAccountNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, bankAccountNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Account Number" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Mobile Banking</label>
<select value={editingInvestor.mobileBanking || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, mobileBanking: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="">Select</option>
<option value="Bkash">Bkash</option>
<option value="Nagad">Nagad</option>
<option value="Rocket">Rocket</option>
<option value="Upay">Upay</option>
</select>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Mobile Number</label>
<input type="text" value={editingInvestor.mobileBankingNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, mobileBankingNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Mobile Number" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">TIN Number</label>
<input type="text" value={editingInvestor.tinNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, tinNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="TIN Number" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Passport Number</label>
<input type="text" value={editingInvestor.passportNumber || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, passportNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Passport Number" />
</div>
</div>
</div>
)}
{activeTab === 'investment' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Active Bikes</label>
<input type="number" value={editingInvestor.activeBikes} onChange={(e) => setEditingInvestor({ ...editingInvestor, activeBikes: Number(e.target.value) })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Risk Level</label>
<select value={editingInvestor.riskLevel} onChange={(e) => setEditingInvestor({ ...editingInvestor, riskLevel: e.target.value as any })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-2 block">Investments ({editingInvestor.investments?.length || 0})</label>
<div className="space-y-2">
{editingInvestor.investments?.map((inv, idx) => (
<div key={idx} className="p-3 bg-slate-50 rounded-lg">
<div className="flex items-center justify-between">
<span className="font-medium text-sm">{inv.planName}</span>
<span className={`text-xs px-2 py-1 rounded-full ${planColors[inv.planType]}`}>{inv.planType}</span>
</div>
<div className="grid grid-cols-2 gap-2 mt-2 text-xs">
<div>
<span className="text-slate-400">Investment:</span>
<span className="font-medium ml-1">{inv.totalInvestment.toLocaleString()}</span>
</div>
<div>
<span className="text-slate-400">Monthly:</span>
<span className="font-medium ml-1">{inv.monthlyReturn.toLocaleString()}</span>
</div>
</div>
</div>
))}
<p className="text-sm text-slate-500 text-center py-2">Add investments from the single investor page</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Referral Code</label>
<input type="text" value={editingInvestor.referralCode || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, referralCode: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Referral Code" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Referred By</label>
<input type="text" value={editingInvestor.referredBy || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, referredBy: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Referrer ID" />
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Notes</label>
<textarea value={editingInvestor.notes || ''} onChange={(e) => setEditingInvestor({ ...editingInvestor, notes: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={3} placeholder="Additional notes..." />
</div>
</div>
)}
{activeTab === 'documents' && (
<div className="space-y-4">
<div className="border-2 border-dashed border-slate-200 rounded-lg p-8 text-center">
<FileText className="w-12 h-12 text-slate-300 mx-auto mb-2" />
<p className="text-sm text-slate-500">Drag and drop files here, or click to browse</p>
<button className="mt-2 px-4 py-2 bg-slate-100 text-slate-600 rounded-lg text-sm hover:bg-slate-200">Browse Files</button>
</div>
<div className="space-y-2">
{editingInvestor.kycDocuments?.map((doc, idx) => (
<div key={idx} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-slate-400" />
<div>
<p className="text-sm font-medium text-slate-700 capitalize">{doc.type.replace('_', ' ')}</p>
<p className="text-xs text-slate-400">{doc.number || 'No number'}</p>
</div>
</div>
<span className={`text-xs font-medium px-2 py-1 rounded-full ${doc.verified ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
{doc.verified ? 'Verified' : 'Pending'}
</span>
</div>
))}
</div>
</div>
)}
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
<button onClick={() => setShowModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button onClick={handleSaveInvestor} className="px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark">Save Investor</button>
</div>
</div>
</div>
)}
{showDetailsModal && selectedInvestor && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="text-lg font-bold text-slate-800">Investor Details</h2>
<button onClick={() => setShowDetailsModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-5 overflow-y-auto max-h-[60vh]">
<div className="flex items-center gap-4 mb-6">
<div className="w-16 h-16 rounded-full bg-purple-100 flex items-center justify-center">
<span className="text-2xl font-bold text-purple-600">{selectedInvestor.name.charAt(0)}</span>
</div>
<div>
<h3 className="text-xl font-bold text-slate-800">{selectedInvestor.name}</h3>
<p className="text-sm text-slate-500">{selectedInvestor.id}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-slate-50 rounded-lg p-4">
<p className="text-sm text-slate-500 mb-1">Total Invested</p>
<p className="text-xl font-bold text-purple-600">{selectedInvestor.totalInvested.toLocaleString()}</p>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<p className="text-sm text-slate-500 mb-1">Total Earnings</p>
<p className="text-xl font-bold text-green-600">{selectedInvestor.totalEarnings.toLocaleString()}</p>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<p className="text-sm text-slate-500 mb-1">Active Bikes</p>
<p className="text-xl font-bold text-slate-800">{selectedInvestor.activeBikes}</p>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<p className="text-sm text-slate-500 mb-1">ROI</p>
<p className="text-xl font-bold text-slate-800">{selectedInvestor.roi}%</p>
</div>
</div>
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-slate-500">Phone</span>
<span className="font-medium">{selectedInvestor.phone}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">Email</span>
<span className="font-medium">{selectedInvestor.email}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">Investment Plans</span>
<span className="font-medium capitalize">{selectedInvestor.investments?.length || 0} Plans</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">Status</span>
<span className={`font-medium capitalize ${selectedInvestor.status === 'active' ? 'text-green-600' : 'text-amber-600'}`}>{selectedInvestor.status}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">KYC</span>
<span className={`font-medium capitalize ${selectedInvestor.kycStatus === 'verified' ? 'text-green-600' : 'text-amber-600'}`}>{selectedInvestor.kycStatus}</span>
</div>
</div>
<div className="mt-6 pt-4 border-t border-slate-100">
<div className="flex items-center justify-between mb-3">
<h4 className="font-semibold text-slate-700">Assigned Bikes ({assignedBikes.length})</h4>
<button
onClick={() => setShowAssignBikeModal(true)}
className="text-sm text-investor hover:underline flex items-center gap-1"
>
<Plus className="w-4 h-4" /> Add Bike
</button>
</div>
<div className="space-y-2">
{assignedBikes.map(bike => (
<div key={bike.id} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-white flex items-center justify-center">
<Banknote className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-sm font-medium text-slate-700">{bike.model}</p>
<p className="text-xs text-slate-400">{bike.plateNumber}</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className={`text-xs font-medium px-2 py-1 rounded-full ${bike.status === 'rented' ? 'bg-green-100 text-green-700' : 'bg-blue-100 text-blue-700'}`}>
{bike.status}
</span>
<button onClick={() => handleUnassignBike(bike.id)} className="p-1 hover:bg-red-50 rounded-lg text-red-400">
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
{assignedBikes.length === 0 && (
<p className="text-center text-slate-400 py-4">No bikes assigned</p>
)}
</div>
</div>
</div>
<div className="p-5 border-t border-slate-100 flex justify-between">
<button onClick={() => handleDeleteInvestor(selectedInvestor.id)} className="px-4 py-2 border border-red-200 text-red-600 rounded-lg text-sm hover:bg-red-50">Delete</button>
<div className="flex gap-2">
<button onClick={() => { handleEditInvestor(selectedInvestor); setShowDetailsModal(false); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Edit</button>
<Link href={`/admin/investors/${selectedInvestor.id}`} className="px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark flex items-center gap-1">
View Full Page <ExternalLink className="w-4 h-4" />
</Link>
</div>
</div>
</div>
</div>
)}
{showAssignBikeModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md"> <div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-5 border-b border-slate-100 flex items-center justify-between"> <div className="p-6 text-center">
<h2 className="text-lg font-bold text-slate-800">Assign Bike to Investor</h2> <div className="w-12 h-12 rounded-full bg-red-100 flex items-center justify-center mx-auto mb-4">
<button onClick={() => setShowAssignBikeModal(false)} className="p-2 hover:bg-slate-100 rounded-lg"> <Trash2 className="w-6 h-6 text-red-600" />
<X className="w-5 h-5 text-slate-400" /> </div>
</button> <h3 className="text-lg font-bold text-slate-800 mb-2">Delete Investor</h3>
<p className="text-slate-500 mb-1">
Are you sure you want to delete <span className="font-semibold text-slate-700">{deleteModal.investor.name}</span>?
</p>
<p className="text-sm text-red-500">This action cannot be undone. All investor data will be permanently removed.</p>
</div> </div>
<div className="p-4 border-t border-slate-100 flex gap-3">
<div className="p-5"> <button
<label className="text-sm font-medium text-slate-600 mb-1 block">Select Bike</label> onClick={() => setDeleteModal({ show: false, investor: null })}
<select className="flex-1 py-2.5 px-4 border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-50"
value={selectedBikeId}
onChange={(e) => setSelectedBikeId(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
> >
<option value="">Select a bike</option> Cancel
{availableBikesForAssignment.map(bike => ( </button>
<option key={bike.id} value={bike.id}> <button
{bike.model} - {bike.plateNumber} ({bike.purchasePrice?.toLocaleString() || 0}) onClick={handleDeleteInvestor}
</option> className="flex-1 py-2.5 px-4 bg-red-500 text-white rounded-lg text-sm font-medium hover:bg-red-600"
))} >
</select> Delete
</div> </button>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
<button onClick={() => setShowAssignBikeModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
<button onClick={handleAssignBike} disabled={!selectedBikeId} className="px-4 py-2 bg-investor text-white rounded-lg text-sm hover:bg-investor-dark disabled:opacity-50">Assign Bike</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,9 +7,10 @@ import {
Shield, Check, Clock, Bike, User, Phone, Shield, Check, Clock, Bike, User, Phone,
MapPin, FileText, Image, DollarSign, Wrench, Battery, MapPin, FileText, Image, DollarSign, Wrench, Battery,
CheckCircle, XCircle, ArrowLeft, Save, Printer, Send, CheckCircle, XCircle, ArrowLeft, Save, Printer, Send,
MessageSquare, Edit, UserCheck, Wallet, Store, Globe, Calendar, Briefcase, Plus, Upload MessageSquare, Edit, UserCheck, Wallet, Store, Globe, Calendar, Briefcase, Plus, Upload, Lock
} from 'lucide-react'; } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { hasPermission, canApproveKycDocument, canRejectKycDocument, canMakeValidUser } from '@/lib/auth';
type ApplicationSource = 'app' | 'web' | 'walkin' | 'referral'; type ApplicationSource = 'app' | 'web' | 'walkin' | 'referral';
type KYCType = 'biker' | 'investor' | 'swapstation' | 'merchant' | 'general'; type KYCType = 'biker' | 'investor' | 'swapstation' | 'merchant' | 'general';
@@ -266,6 +267,18 @@ export default function KYCDetailPage() {
const [rejectReason, setRejectReason] = useState(''); const [rejectReason, setRejectReason] = useState('');
const [uploadDocId, setUploadDocId] = useState<string | null>(null); const [uploadDocId, setUploadDocId] = useState<string | null>(null);
const [permApprove, setPermApprove] = useState(false);
const [permReject, setPermReject] = useState(false);
const [permMakeValid, setPermMakeValid] = useState(false);
const [permDocUpload, setPermDocUpload] = useState(false);
useEffect(() => {
setPermApprove(canApproveKycDocument());
setPermReject(canRejectKycDocument());
setPermMakeValid(canMakeValidUser());
setPermDocUpload(hasPermission('kyc.doc_upload'));
}, []);
useEffect(() => { useEffect(() => {
const found = mockRequests.find(r => r.id === id); const found = mockRequests.find(r => r.id === id);
if (found) { if (found) {
@@ -425,7 +438,7 @@ export default function KYCDetailPage() {
<> <>
{/* Top Row on Mobile: Make [Type] Button */} {/* Top Row on Mobile: Make [Type] Button */}
<div className="flex gap-2 w-full sm:w-auto"> <div className="flex gap-2 w-full sm:w-auto">
{request.type === 'biker' && request.status !== 'approved' && ( {permMakeValid && request.type === 'biker' && request.status !== 'approved' && (
<button <button
onClick={() => setShowApproveModal(true)} onClick={() => setShowApproveModal(true)}
className="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center justify-center gap-2" className="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center justify-center gap-2"
@@ -433,7 +446,7 @@ export default function KYCDetailPage() {
<Bike className="w-4 h-4" /> <span>Make Biker</span> <Bike className="w-4 h-4" /> <span>Make Biker</span>
</button> </button>
)} )}
{request.type === 'investor' && request.status !== 'approved' && ( {permMakeValid && request.type === 'investor' && request.status !== 'approved' && (
<button <button
onClick={() => { onClick={() => {
if (confirm('Approve this request and create investor profile?')) { if (confirm('Approve this request and create investor profile?')) {
@@ -446,7 +459,7 @@ export default function KYCDetailPage() {
<DollarSign className="w-4 h-4" /> <span>Make Investor</span> <DollarSign className="w-4 h-4" /> <span>Make Investor</span>
</button> </button>
)} )}
{request.type === 'swapstation' && request.status !== 'approved' && ( {permMakeValid && request.type === 'swapstation' && request.status !== 'approved' && (
<button <button
onClick={() => { onClick={() => {
if (confirm('Approve this request and create shop profile?')) { if (confirm('Approve this request and create shop profile?')) {
@@ -459,7 +472,7 @@ export default function KYCDetailPage() {
<Store className="w-4 h-4" /> <span>Make Shop</span> <Store className="w-4 h-4" /> <span>Make Shop</span>
</button> </button>
)} )}
{request.type === 'merchant' && request.status !== 'approved' && ( {permMakeValid && request.type === 'merchant' && request.status !== 'approved' && (
<button <button
onClick={() => { onClick={() => {
if (confirm('Approve this request and create merchant profile?')) { if (confirm('Approve this request and create merchant profile?')) {
@@ -472,6 +485,11 @@ export default function KYCDetailPage() {
<User className="w-4 h-4" /> <span>Make Merchant</span> <User className="w-4 h-4" /> <span>Make Merchant</span>
</button> </button>
)} )}
{!permMakeValid && (
<div className="flex items-center gap-1.5 px-3 sm:px-4 py-2 bg-slate-100 text-slate-400 rounded-lg text-xs">
<Lock className="w-3 h-3" /> <span>Make Valid User</span>
</div>
)}
</div> </div>
{/* Bottom Row on Mobile: Edit, Note, SMS */} {/* Bottom Row on Mobile: Edit, Note, SMS */}
@@ -663,15 +681,29 @@ export default function KYCDetailPage() {
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{doc.status === 'pending' && ( {doc.status === 'pending' && (
<button onClick={() => openUploadModal(doc.id)} className="p-1 bg-amber-100 text-amber-600 rounded hover:bg-amber-200" title="Upload"><Upload className="w-4 h-4" /></button> <>
{permDocUpload ? (
<button onClick={() => openUploadModal(doc.id)} className="p-1 bg-amber-100 text-amber-600 rounded hover:bg-amber-200" title="Upload"><Upload className="w-4 h-4" /></button>
) : (
<span className="p-1 bg-amber-50 text-amber-300 rounded cursor-not-allowed" title="No permission"><Upload className="w-4 h-4" /></span>
)}
</>
)} )}
{(doc.status === 'uploaded' || doc.status === 'approved') && doc.imageUrl && ( {(doc.status === 'uploaded' || doc.status === 'approved') && doc.imageUrl && (
<button onClick={() => window.open(doc.imageUrl, '_blank')} className="p-1 bg-blue-100 text-blue-600 rounded hover:bg-blue-200" title="View"><Image className="w-4 h-4" /></button> <button onClick={() => window.open(doc.imageUrl, '_blank')} className="p-1 bg-blue-100 text-blue-600 rounded hover:bg-blue-200" title="View"><Image className="w-4 h-4" /></button>
)} )}
{doc.status === 'uploaded' && ( {doc.status === 'uploaded' && (
<> <>
<button onClick={() => handleApproveDocument(doc.id)} className="p-1 bg-green-100 text-green-600 rounded hover:bg-green-200" title="Approve"><CheckCircle className="w-4 h-4" /></button> {permApprove ? (
<button onClick={() => openRejectDocModal(doc.id)} className="p-1 bg-red-100 text-red-600 rounded hover:bg-red-200" title="Reject"><XCircle className="w-4 h-4" /></button> <button onClick={() => handleApproveDocument(doc.id)} className="p-1 bg-green-100 text-green-600 rounded hover:bg-green-200" title="Approve"><CheckCircle className="w-4 h-4" /></button>
) : (
<span className="p-1 bg-green-50 text-green-300 rounded cursor-not-allowed" title="No permission"><CheckCircle className="w-4 h-4" /></span>
)}
{permReject ? (
<button onClick={() => openRejectDocModal(doc.id)} className="p-1 bg-red-100 text-red-600 rounded hover:bg-red-200" title="Reject"><XCircle className="w-4 h-4" /></button>
) : (
<span className="p-1 bg-red-50 text-red-300 rounded cursor-not-allowed" title="No permission"><XCircle className="w-4 h-4" /></span>
)}
</> </>
)} )}
{doc.status === 'approved' && <span className="text-xs px-2 py-1 bg-green-100 text-green-700 rounded-full">Approved</span>} {doc.status === 'approved' && <span className="text-xs px-2 py-1 bg-green-100 text-green-700 rounded-full">Approved</span>}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,541 @@
'use client';
import { useState, use } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import {
Wrench, ArrowLeft, Battery, AlertTriangle, Calendar, DollarSign, Clock,
CheckCircle, XCircle, Search, RefreshCw, ChevronLeft, ChevronRight,
Eye, User, FileText, ArrowRight, ShieldAlert, Sparkles
} from 'lucide-react';
interface HistoryRecord {
id: string;
date: string;
type: 'damage' | 'maintenance' | 'service' | 'repair' | 'inspection' | 'battery_swap';
severity: 'critical' | 'major' | 'minor' | 'cosmetic';
status: 'reported' | 'in_progress' | 'parts_ordered' | 'completed' | 'cancelled';
description: string;
cost: number;
reporter: string;
resolvedAt?: string;
partsUsed?: string[];
}
export default function BatteryMaintenanceHistoryPage({ params }: { params: Promise<{ id: string }> }) {
const router = useRouter();
const searchParams = useSearchParams();
const batteryId = use(params).id;
const fromRecord = searchParams.get('from');
// Realistic mock data for a specific battery's maintenance and damage history
const [historyList] = useState<HistoryRecord[]>(() => {
return [
{
id: 'MNT-003',
date: '2024-03-19',
type: 'battery_swap',
severity: 'minor',
status: 'completed',
description: 'Battery not holding charge properly - swapped under warranty.',
cost: 0,
reporter: 'Jamal (Biker)',
resolvedAt: '2024-03-19',
partsUsed: []
},
{
id: 'MNT-009',
date: '2024-03-05',
type: 'repair',
severity: 'major',
status: 'completed',
description: 'Battery port connector pin replacement & calibration.',
cost: 1200,
reporter: 'Uttara Hub Staff',
resolvedAt: '2024-03-06',
partsUsed: ['Connector Pins', 'Silicone Seals']
},
{
id: 'MNT-015',
date: '2024-02-15',
type: 'service',
severity: 'minor',
status: 'completed',
description: 'Cell rebalancing and firmware upgrade for BMS.',
cost: 800,
reporter: 'Authorized Service Center',
resolvedAt: '2024-02-15',
partsUsed: []
},
{
id: 'MNT-020',
date: '2024-01-22',
type: 'damage',
severity: 'critical',
status: 'completed',
description: 'Cell thermal runaway inspection due to temperature alert.',
cost: 1500,
reporter: 'System Alert',
resolvedAt: '2024-01-24',
partsUsed: ['BMS Module']
},
{
id: 'MNT-025',
date: '2023-12-01',
type: 'inspection',
severity: 'cosmetic',
status: 'completed',
description: 'Outer plastic protective case scratch audit and hub cleanup.',
cost: 0,
reporter: 'Kamal Ahmed (Biker)',
resolvedAt: '2023-12-01',
partsUsed: []
}
];
});
// Client Side Filter & Sorting States
const [searchQuery, setSearchQuery] = useState('');
const [typeFilter, setTypeFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [sortBy, setSortBy] = useState<'date' | 'cost' | 'severity'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [page, setPage] = useState(1);
const pageSize = 5;
const handleSort = (field: 'date' | 'cost' | 'severity') => {
if (sortBy === field) {
setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'));
} else {
setSortBy(field);
setSortOrder('desc');
}
setPage(1);
};
// Logic
const filteredList = historyList.filter(item => {
if (typeFilter !== 'all') {
if (typeFilter === 'damage_all') {
if (item.type !== 'damage') return false;
} else if (typeFilter === 'damage_cosmetic') {
if (item.type !== 'damage' || item.severity !== 'cosmetic') return false;
} else if (typeFilter === 'damage_minor') {
if (item.type !== 'damage' || item.severity !== 'minor') return false;
} else if (typeFilter === 'damage_major') {
if (item.type !== 'damage' || item.severity !== 'major') return false;
} else if (typeFilter === 'damage_critical') {
if (item.type !== 'damage' || item.severity !== 'critical') return false;
} else if (typeFilter === 'maintenance_all') {
if (item.type === 'damage') return false;
} else if (typeFilter === 'maintenance_service') {
if (item.type !== 'service') return false;
} else if (typeFilter === 'maintenance_repair') {
if (item.type !== 'repair') return false;
} else if (typeFilter === 'maintenance_inspection') {
if (item.type !== 'inspection') return false;
} else if (typeFilter === 'maintenance_battery_swap') {
if (item.type !== 'battery_swap') return false;
}
}
if (statusFilter !== 'all' && item.status !== statusFilter) return false;
if (searchQuery && !item.description.toLowerCase().includes(searchQuery.toLowerCase()) && !item.id.toLowerCase().includes(searchQuery.toLowerCase())) return false;
if (dateFrom && new Date(item.date) < new Date(dateFrom)) return false;
if (dateTo && new Date(item.date) > new Date(dateTo)) return false;
return true;
});
const sortedList = [...filteredList].sort((a, b) => {
let comp = 0;
if (sortBy === 'date') {
comp = new Date(a.date).getTime() - new Date(b.date).getTime();
} else if (sortBy === 'cost') {
comp = a.cost - b.cost;
} else if (sortBy === 'severity') {
const ranks = { critical: 4, major: 3, minor: 2, cosmetic: 1 };
comp = ranks[a.severity] - ranks[b.severity];
}
return sortOrder === 'desc' ? -comp : comp;
});
// Pagination
const totalPages = Math.ceil(sortedList.length / pageSize);
const paginatedList = sortedList.slice((page - 1) * pageSize, page * pageSize);
// Status/Severity Badge Colors
const severityColors = {
critical: 'bg-red-100 text-red-700',
major: 'bg-orange-100 text-orange-700',
minor: 'bg-amber-100 text-amber-700',
cosmetic: 'bg-slate-100 text-slate-700',
};
const statusColors = {
reported: 'bg-amber-50 text-amber-700 border border-amber-200',
in_progress: 'bg-blue-50 text-blue-700 border border-blue-200',
parts_ordered: 'bg-purple-50 text-purple-700 border border-purple-200',
completed: 'bg-green-50 text-green-700 border border-green-200',
cancelled: 'bg-red-50 text-red-700 border border-red-200',
};
const totalCost = filteredList.reduce((sum, item) => sum + item.cost, 0);
const criticalCount = filteredList.filter(item => item.severity === 'critical' || item.severity === 'major').length;
return (
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
{/* Back navigation links */}
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => {
if (fromRecord) {
router.push(`/admin/maintenance/${fromRecord}`);
} else {
router.push('/admin/maintenance');
}
}}
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors bg-white shadow-sm"
>
<ArrowLeft className="w-4 h-4 text-slate-600" />
</button>
<div>
<h1 className="text-2xl font-extrabold text-slate-800 flex items-center gap-2">
<Battery className="w-6 h-6 text-green-600" /> Battery History Ledger
</h1>
<p className="text-xs text-slate-500">
Viewing comprehensive damage & maintenance history for Battery <span className="font-semibold text-green-600">{batteryId}</span>
</p>
</div>
</div>
{/* Top Metrics Row */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0">
<Wrench className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xs text-slate-500">Total Events</p>
<p className="text-lg font-bold text-slate-800">{filteredList.length}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center shrink-0">
<ShieldAlert className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-xs text-slate-500">Major / Critical</p>
<p className="text-lg font-bold text-red-600">{criticalCount}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center shrink-0">
<DollarSign className="w-5 h-5 text-emerald-600" />
</div>
<div>
<p className="text-xs text-slate-500">Accumulated Cost</p>
<p className="text-lg font-bold text-emerald-600">{totalCost.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm bg-gradient-to-br from-teal-50 to-green-50">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-teal-100 rounded-lg flex items-center justify-center shrink-0">
<Sparkles className="w-5 h-5 text-teal-600" />
</div>
<div>
<p className="text-xs text-teal-500">Health Status</p>
<p className="text-lg font-bold text-emerald-700">Excellent</p>
</div>
</div>
</div>
</div>
{/* Main Ledger Content */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
{/* Advanced Filters */}
<div className="p-4 border-b border-slate-100 bg-slate-50/30">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
type="text"
placeholder="Search description or reference..."
value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setPage(1); }}
className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64 bg-white focus:outline-none focus:border-slate-400 transition-colors"
/>
</div>
<select
value={typeFilter}
onChange={(e) => { setTypeFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer font-medium"
>
<option value="all">All Types</option>
<optgroup label="Damage Levels">
<option value="damage_all">All Damages</option>
<option value="damage_cosmetic">Damage - Cosmetic</option>
<option value="damage_minor">Damage - Minor</option>
<option value="damage_major">Damage - Major</option>
<option value="damage_critical">Damage - Critical</option>
</optgroup>
<optgroup label="Maintenance Types">
<option value="maintenance_all">All Maintenance</option>
<option value="maintenance_service">Service</option>
<option value="maintenance_repair">Repair</option>
<option value="maintenance_inspection">Inspection</option>
<option value="maintenance_battery_swap">Battery Swap</option>
</optgroup>
</select>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer"
>
<option value="all">All Status</option>
<option value="reported">Reported</option>
<option value="in_progress">In Progress</option>
<option value="parts_ordered">Parts Ordered</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1">
<input
type="date"
value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
<span className="text-slate-400">to</span>
<input
type="date"
value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
</div>
{(dateFrom || dateTo || searchQuery || typeFilter !== 'all' || statusFilter !== 'all') && (
<button
onClick={() => {
setSearchQuery('');
setTypeFilter('all');
setStatusFilter('all');
setDateFrom('');
setDateTo('');
setPage(1);
}}
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 bg-white"
title="Reset Filters"
>
<RefreshCw className="w-4 h-4 text-slate-500" />
</button>
)}
</div>
</div>
</div>
{/* Desktop View */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-slate-50 border-b border-slate-100">
<tr>
<th
onClick={() => handleSort('date')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
>
Date {sortBy === 'date' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Reference ID
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Type
</th>
<th
onClick={() => handleSort('severity')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
>
Severity {sortBy === 'severity' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Description
</th>
<th
onClick={() => handleSort('cost')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors text-right"
>
Cost {sortBy === 'cost' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Status
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider text-center">
Action
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedList.length > 0 ? (
paginatedList.map(item => (
<tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
<td className="px-4 py-3.5 text-sm text-slate-600 font-medium">
{item.date}
</td>
<td className="px-4 py-3.5 text-xs font-mono font-semibold text-slate-400">
{item.id}
</td>
<td className="px-4 py-3.5 text-sm text-slate-700 capitalize font-medium">
{item.type.replace('_', ' ')}
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex px-2 py-0.5 rounded-full text-xs font-semibold ${severityColors[item.severity]}`}>
{item.severity}
</span>
</td>
<td className="px-4 py-3.5 text-sm text-slate-600 max-w-xs">
<p className="truncate" title={item.description}>{item.description}</p>
{item.partsUsed && item.partsUsed.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{item.partsUsed.map(p => (
<span key={p} className="text-[10px] bg-slate-100 text-slate-600 px-1.5 py-0.2 rounded">
{p}
</span>
))}
</div>
)}
</td>
<td className="px-4 py-3.5 text-sm font-bold text-slate-800 text-right">
{item.cost.toLocaleString()}
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold capitalize ${statusColors[item.status]}`}>
{item.status.replace('_', ' ')}
</span>
</td>
<td className="px-4 py-3.5 text-center">
<Link
href={`/admin/maintenance/${item.id}`}
className="inline-flex p-1.5 hover:bg-slate-100 rounded-lg text-slate-500 hover:text-slate-700 transition-colors"
>
<Eye className="w-4 h-4" />
</Link>
</td>
</tr>
))
) : (
<tr>
<td colSpan={8} className="px-4 py-12 text-center text-slate-400">
<AlertTriangle className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p className="text-sm font-semibold">No maintenance logs found</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Mobile View */}
<div className="lg:hidden divide-y divide-slate-100">
{paginatedList.length > 0 ? (
paginatedList.map(item => (
<div key={item.id} className="p-4 hover:bg-slate-50 transition-colors">
<div className="flex justify-between items-start mb-2">
<div>
<span className="text-xs text-slate-400 font-mono font-semibold">{item.id}</span>
<h4 className="text-sm font-bold text-slate-800 capitalize mt-0.5">
{item.type.replace('_', ' ')}
</h4>
</div>
<div className="text-right">
<span className="text-sm font-extrabold text-slate-900">{item.cost.toLocaleString()}</span>
<span className={`block text-[10px] font-semibold mt-1 px-1.5 py-0.5 rounded capitalize ${statusColors[item.status]}`}>
{item.status.replace('_', ' ')}
</span>
</div>
</div>
<p className="text-xs text-slate-500 mb-2">{item.description}</p>
<div className="flex justify-between items-center text-[11px] text-slate-400">
<span>{item.date}</span>
<Link
href={`/admin/maintenance/${item.id}`}
className="text-accent font-semibold hover:underline flex items-center gap-0.5"
>
Details <ArrowRight className="w-3 h-3" />
</Link>
</div>
</div>
))
) : (
<div className="p-8 text-center text-slate-500">
<AlertTriangle className="w-10 h-10 mx-auto mb-2 text-slate-300" />
<p>No maintenance logs found</p>
</div>
)}
</div>
{/* Footer Pagination */}
{sortedList.length > pageSize && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3 bg-slate-50/10">
<p className="text-xs sm:text-sm text-slate-500">
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedList.length)} of {sortedList.length} records
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
>
<ChevronLeft className="w-4 h-4 text-slate-600" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`w-8 h-8 rounded-lg text-sm font-semibold transition-colors ${page === pageNum
? 'bg-accent text-white'
: 'border border-slate-200 hover:bg-slate-50 bg-white text-slate-600'
}`}
>
{pageNum}
</button>
))}
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
>
<ChevronRight className="w-4 h-4 text-slate-600" />
</button>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,537 @@
'use client';
import { useState, use } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import {
Wrench, ArrowLeft, Bike, AlertTriangle, Calendar, DollarSign, Clock,
CheckCircle, XCircle, Search, RefreshCw, ChevronLeft, ChevronRight,
Eye, User, FileText, ArrowRight, ShieldAlert, Sparkles
} from 'lucide-react';
interface HistoryRecord {
id: string;
date: string;
type: 'damage' | 'maintenance' | 'service' | 'repair' | 'inspection';
severity: 'critical' | 'major' | 'minor' | 'cosmetic';
status: 'reported' | 'in_progress' | 'parts_ordered' | 'completed' | 'cancelled';
description: string;
cost: number;
reporter: string;
resolvedAt?: string;
partsUsed?: string[];
}
export default function BikeMaintenanceHistoryPage({ params }: { params: Promise<{ id: string }> }) {
const router = useRouter();
const searchParams = useSearchParams();
const bikeId = use(params).id;
const fromRecord = searchParams.get('from');
// Realistic mock data for a specific bike's maintenance and damage history
const [historyList] = useState<HistoryRecord[]>(() => {
return [
{
id: 'MNT-001',
date: '2024-03-21',
type: 'damage',
severity: 'major',
status: 'in_progress',
description: 'Front fender damaged in minor collision at Gulshan signal.',
cost: 3200,
reporter: 'Sofiq Rahman (Biker)',
partsUsed: ['Front fender', 'Mounting brackets']
},
{
id: 'MNT-005',
date: '2024-03-17',
type: 'inspection',
severity: 'minor',
status: 'completed',
description: 'Monthly scheduled routine vehicle inspection.',
cost: 250,
reporter: 'Gulshan Hub Staff',
resolvedAt: '2024-03-17',
partsUsed: []
},
{
id: 'MNT-012',
date: '2024-02-10',
type: 'repair',
severity: 'critical',
status: 'completed',
description: 'Motor controller overheating check & throttle replacement.',
cost: 7500,
reporter: 'System Alert',
resolvedAt: '2024-02-12',
partsUsed: ['Throttle Assembly', 'Controller Fan']
},
{
id: 'MNT-018',
date: '2024-01-15',
type: 'service',
severity: 'minor',
status: 'completed',
description: 'Chain lubrication, brake shoe calibration, and mirror tightening.',
cost: 600,
reporter: 'Kamal Ahmed (Biker)',
resolvedAt: '2024-01-15',
partsUsed: ['Brake Shoe Set']
},
{
id: 'MNT-022',
date: '2023-12-05',
type: 'damage',
severity: 'cosmetic',
status: 'completed',
description: 'Side mirror cracked due to parking slip.',
cost: 800,
reporter: 'Kamal Ahmed (Biker)',
resolvedAt: '2023-12-06',
partsUsed: ['Left Side Mirror']
}
];
});
// Client Side Filter & Sorting States
const [searchQuery, setSearchQuery] = useState('');
const [typeFilter, setTypeFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [sortBy, setSortBy] = useState<'date' | 'cost' | 'severity'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [page, setPage] = useState(1);
const pageSize = 5;
const handleSort = (field: 'date' | 'cost' | 'severity') => {
if (sortBy === field) {
setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'));
} else {
setSortBy(field);
setSortOrder('desc');
}
setPage(1);
};
// Logic
const filteredList = historyList.filter(item => {
if (typeFilter !== 'all') {
if (typeFilter === 'damage_all') {
if (item.type !== 'damage') return false;
} else if (typeFilter === 'damage_cosmetic') {
if (item.type !== 'damage' || item.severity !== 'cosmetic') return false;
} else if (typeFilter === 'damage_minor') {
if (item.type !== 'damage' || item.severity !== 'minor') return false;
} else if (typeFilter === 'damage_major') {
if (item.type !== 'damage' || item.severity !== 'major') return false;
} else if (typeFilter === 'damage_critical') {
if (item.type !== 'damage' || item.severity !== 'critical') return false;
} else if (typeFilter === 'maintenance_all') {
if (item.type === 'damage') return false;
} else if (typeFilter === 'maintenance_service') {
if (item.type !== 'service') return false;
} else if (typeFilter === 'maintenance_repair') {
if (item.type !== 'repair') return false;
} else if (typeFilter === 'maintenance_inspection') {
if (item.type !== 'inspection') return false;
}
}
if (statusFilter !== 'all' && item.status !== statusFilter) return false;
if (searchQuery && !item.description.toLowerCase().includes(searchQuery.toLowerCase()) && !item.id.toLowerCase().includes(searchQuery.toLowerCase())) return false;
if (dateFrom && new Date(item.date) < new Date(dateFrom)) return false;
if (dateTo && new Date(item.date) > new Date(dateTo)) return false;
return true;
});
const sortedList = [...filteredList].sort((a, b) => {
let comp = 0;
if (sortBy === 'date') {
comp = new Date(a.date).getTime() - new Date(b.date).getTime();
} else if (sortBy === 'cost') {
comp = a.cost - b.cost;
} else if (sortBy === 'severity') {
const ranks = { critical: 4, major: 3, minor: 2, cosmetic: 1 };
comp = ranks[a.severity] - ranks[b.severity];
}
return sortOrder === 'desc' ? -comp : comp;
});
// Pagination
const totalPages = Math.ceil(sortedList.length / pageSize);
const paginatedList = sortedList.slice((page - 1) * pageSize, page * pageSize);
// Status/Severity Badge Colors
const severityColors = {
critical: 'bg-red-100 text-red-700',
major: 'bg-orange-100 text-orange-700',
minor: 'bg-amber-100 text-amber-700',
cosmetic: 'bg-slate-100 text-slate-700',
};
const statusColors = {
reported: 'bg-amber-50 text-amber-700 border border-amber-200',
in_progress: 'bg-blue-50 text-blue-700 border border-blue-200',
parts_ordered: 'bg-purple-50 text-purple-700 border border-purple-200',
completed: 'bg-green-50 text-green-700 border border-green-200',
cancelled: 'bg-red-50 text-red-700 border border-red-200',
};
const totalCost = filteredList.reduce((sum, item) => sum + item.cost, 0);
const criticalCount = filteredList.filter(item => item.severity === 'critical' || item.severity === 'major').length;
return (
<div className="p-4 lg:p-6 max-w-8xl mx-auto">
{/* Back navigation links */}
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => {
if (fromRecord) {
router.push(`/admin/maintenance/${fromRecord}`);
} else {
router.push('/admin/maintenance');
}
}}
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors bg-white shadow-sm"
>
<ArrowLeft className="w-4 h-4 text-slate-600" />
</button>
<div>
<h1 className="text-2xl font-extrabold text-slate-800 flex items-center gap-2">
<Bike className="w-6 h-6 text-purple-600" /> History Ledger
</h1>
<p className="text-xs text-slate-500">
Viewing comprehensive damage & maintenance history for Bike <span className="font-semibold text-purple-600">{bikeId}</span>
</p>
</div>
</div>
{/* Top Metrics Row */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center shrink-0">
<Wrench className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-xs text-slate-500">Total Events</p>
<p className="text-lg font-bold text-slate-800">{filteredList.length}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center shrink-0">
<ShieldAlert className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-xs text-slate-500">Major / Critical</p>
<p className="text-lg font-bold text-red-600">{criticalCount}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0">
<DollarSign className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xs text-slate-500">Accumulated Cost</p>
<p className="text-lg font-bold text-green-600">{totalCost.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm bg-gradient-to-br from-indigo-50 to-purple-50">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center shrink-0">
<Sparkles className="w-5 h-5 text-indigo-600" />
</div>
<div>
<p className="text-xs text-indigo-500">Service Status</p>
<p className="text-lg font-bold text-indigo-700">Healthy</p>
</div>
</div>
</div>
</div>
{/* Main Ledger Content */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
{/* Advanced Filters */}
<div className="p-4 border-b border-slate-100 bg-slate-50/30">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
type="text"
placeholder="Search description or reference..."
value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setPage(1); }}
className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64 bg-white focus:outline-none focus:border-slate-400 transition-colors"
/>
</div>
<select
value={typeFilter}
onChange={(e) => { setTypeFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer font-medium"
>
<option value="all">All Types</option>
<optgroup label="Damage Levels">
<option value="damage_all">All Damages</option>
<option value="damage_cosmetic">Damage - Cosmetic</option>
<option value="damage_minor">Damage - Minor</option>
<option value="damage_major">Damage - Major</option>
<option value="damage_critical">Damage - Critical</option>
</optgroup>
<optgroup label="Maintenance Types">
<option value="maintenance_all">All Maintenance</option>
<option value="maintenance_service">Service</option>
<option value="maintenance_repair">Repair</option>
<option value="maintenance_inspection">Inspection</option>
</optgroup>
</select>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer"
>
<option value="all">All Status</option>
<option value="reported">Reported</option>
<option value="in_progress">In Progress</option>
<option value="parts_ordered">Parts Ordered</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1">
<input
type="date"
value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
<span className="text-slate-400">to</span>
<input
type="date"
value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
</div>
{(dateFrom || dateTo || searchQuery || typeFilter !== 'all' || statusFilter !== 'all') && (
<button
onClick={() => {
setSearchQuery('');
setTypeFilter('all');
setStatusFilter('all');
setDateFrom('');
setDateTo('');
setPage(1);
}}
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 bg-white"
title="Reset Filters"
>
<RefreshCw className="w-4 h-4 text-slate-500" />
</button>
)}
</div>
</div>
</div>
{/* Desktop View */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-slate-50 border-b border-slate-100">
<tr>
<th
onClick={() => handleSort('date')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
>
Date {sortBy === 'date' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Reference ID
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Type
</th>
<th
onClick={() => handleSort('severity')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors"
>
Severity {sortBy === 'severity' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Description
</th>
<th
onClick={() => handleSort('cost')}
className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors text-right"
>
Cost {sortBy === 'cost' && <span className="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">
Status
</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider text-center">
Action
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedList.length > 0 ? (
paginatedList.map(item => (
<tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
<td className="px-4 py-3.5 text-sm text-slate-600 font-medium">
{item.date}
</td>
<td className="px-4 py-3.5 text-xs font-mono font-semibold text-slate-400">
{item.id}
</td>
<td className="px-4 py-3.5 text-sm text-slate-700 capitalize font-medium">
{item.type.replace('_', ' ')}
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex px-2 py-0.5 rounded-full text-xs font-semibold ${severityColors[item.severity]}`}>
{item.severity}
</span>
</td>
<td className="px-4 py-3.5 text-sm text-slate-600 max-w-xs">
<p className="truncate" title={item.description}>{item.description}</p>
{item.partsUsed && item.partsUsed.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{item.partsUsed.map(p => (
<span key={p} className="text-[10px] bg-slate-100 text-slate-600 px-1.5 py-0.2 rounded">
{p}
</span>
))}
</div>
)}
</td>
<td className="px-4 py-3.5 text-sm font-bold text-slate-800 text-right">
{item.cost.toLocaleString()}
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold capitalize ${statusColors[item.status]}`}>
{item.status.replace('_', ' ')}
</span>
</td>
<td className="px-4 py-3.5 text-center">
<Link
href={`/admin/maintenance/${item.id}`}
className="inline-flex p-1.5 hover:bg-slate-100 rounded-lg text-slate-500 hover:text-slate-700 transition-colors"
>
<Eye className="w-4 h-4" />
</Link>
</td>
</tr>
))
) : (
<tr>
<td colSpan={8} className="px-4 py-12 text-center text-slate-400">
<AlertTriangle className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p className="text-sm font-semibold">No maintenance logs found</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Mobile View */}
<div className="lg:hidden divide-y divide-slate-100">
{paginatedList.length > 0 ? (
paginatedList.map(item => (
<div key={item.id} className="p-4 hover:bg-slate-50 transition-colors">
<div className="flex justify-between items-start mb-2">
<div>
<span className="text-xs text-slate-400 font-mono font-semibold">{item.id}</span>
<h4 className="text-sm font-bold text-slate-800 capitalize mt-0.5">
{item.type.replace('_', ' ')}
</h4>
</div>
<div className="text-right">
<span className="text-sm font-extrabold text-slate-900">{item.cost.toLocaleString()}</span>
<span className={`block text-[10px] font-semibold mt-1 px-1.5 py-0.5 rounded capitalize ${statusColors[item.status]}`}>
{item.status.replace('_', ' ')}
</span>
</div>
</div>
<p className="text-xs text-slate-500 mb-2">{item.description}</p>
<div className="flex justify-between items-center text-[11px] text-slate-400">
<span>{item.date}</span>
<Link
href={`/admin/maintenance/${item.id}`}
className="text-accent font-semibold hover:underline flex items-center gap-0.5"
>
Details <ArrowRight className="w-3 h-3" />
</Link>
</div>
</div>
))
) : (
<div className="p-8 text-center text-slate-500">
<AlertTriangle className="w-10 h-10 mx-auto mb-2 text-slate-300" />
<p>No maintenance logs found</p>
</div>
)}
</div>
{/* Footer Pagination */}
{sortedList.length > pageSize && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3 bg-slate-50/10">
<p className="text-xs sm:text-sm text-slate-500">
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedList.length)} of {sortedList.length} records
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
>
<ChevronLeft className="w-4 h-4 text-slate-600" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`w-8 h-8 rounded-lg text-sm font-semibold transition-colors ${page === pageNum
? 'bg-accent text-white'
: 'border border-slate-200 hover:bg-slate-50 bg-white text-slate-600'
}`}
>
{pageNum}
</button>
))}
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50 bg-white"
>
<ChevronRight className="w-4 h-4 text-slate-600" />
</button>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,13 +1,14 @@
'use client'; 'use client';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { import {
AlertTriangle, Search, Plus, X, Check, Clock, Bike, User, Phone, AlertTriangle, Search, Plus, X, Check, Clock, Bike, User, Phone,
MapPin, FileText, Image, DollarSign, Wrench, Battery, Key, MapPin, FileText, Image as ImageIcon, DollarSign, Wrench, Battery, Key,
CheckCircle, XCircle, ChevronDown, ChevronUp, Download, Eye, Edit, CheckCircle, XCircle, ChevronDown, ChevronUp, Download, Eye, Edit,
MessageSquare, Filter, Calendar, Save, Printer, Send MessageSquare, Filter, Calendar, Save, Printer, Send, Activity
} from 'lucide-react'; } from 'lucide-react';
type TransactionType = 'deposit' | 'rent_income' | 'investor_funding' | 'investor_withdraw' | 'salary' | 'rent_expense' | 'utility' | 'maintenance' | 'bike_purchase' | 'bike_sale' | 'other_income' | 'other_expense'; type TransactionType = 'deposit' | 'rent_income' | 'investor_funding' | 'investor_withdraw' | 'salary' | 'rent_expense' | 'utility' | 'maintenance' | 'bike_purchase' | 'bike_sale' | 'other_income' | 'other_expense';
@@ -245,7 +246,9 @@ const typeIcons: Record<string, any> = {
}; };
export default function MaintenancePage() { export default function MaintenancePage() {
const [activeTab, setActiveTab] = useState<'all' | MaintenanceType>('all'); const router = useRouter();
const [mainCategory, setMainCategory] = useState<'damage' | 'maintenance'>('damage');
const [targetType, setTargetType] = useState<'all' | 'battery' | 'fleet'>('all');
const [records, setRecords] = useState<MaintenanceRecord[]>(mockMaintenance); const [records, setRecords] = useState<MaintenanceRecord[]>(mockMaintenance);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all');
@@ -259,20 +262,45 @@ export default function MaintenancePage() {
const [expandedNotes, setExpandedNotes] = useState<string[]>([]); const [expandedNotes, setExpandedNotes] = useState<string[]>([]);
const [newNoteText, setNewNoteText] = useState(''); const [newNoteText, setNewNoteText] = useState('');
const [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({}); const [editForm, setEditForm] = useState<Partial<MaintenanceRecord>>({});
const [reportType, setReportType] = useState<'damage' | 'maintenance'>('damage');
const [showSuccessModal, setShowSuccessModal] = useState(false);
const filteredRecords = records.filter(r => { const filteredRecords = records.filter(r => {
const matchesTab = activeTab === 'all' || r.type === activeTab; const isDamage = r.type === 'damage';
const matchesCategory = mainCategory === 'damage' ? isDamage : !isDamage;
const matchesTarget = targetType === 'all' ||
(targetType === 'battery' && r.batteryId) ||
(targetType === 'fleet' && r.bikeId && !r.batteryId);
const matchesSearch = !searchQuery || const matchesSearch = !searchQuery ||
r.bikeId.toLowerCase().includes(searchQuery.toLowerCase()) || r.bikeId.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.bikeModel.toLowerCase().includes(searchQuery.toLowerCase()) || r.bikeModel.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.bikePlate.toLowerCase().includes(searchQuery.toLowerCase()) || r.bikePlate.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.reporterName.toLowerCase().includes(searchQuery.toLowerCase()) || r.reporterName.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.id.toLowerCase().includes(searchQuery.toLowerCase()); r.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
(r.batteryId && r.batteryId.toLowerCase().includes(searchQuery.toLowerCase()));
const matchesStatus = statusFilter === 'all' || r.status === statusFilter; const matchesStatus = statusFilter === 'all' || r.status === statusFilter;
return matchesTab && matchesSearch && matchesStatus; return matchesCategory && matchesTarget && matchesSearch && matchesStatus;
}); });
const damageRecords = records.filter(r => r.type === 'damage');
const maintenanceRecords = records.filter(r => r.type !== 'damage');
const currentMonth = new Date().toISOString().slice(0, 7);
const stats = { const stats = {
damageCount: damageRecords.length,
maintenanceCount: maintenanceRecords.length,
damageThisMonth: damageRecords.filter(r => r.date?.slice(0, 7) === currentMonth).length,
maintenanceThisMonth: maintenanceRecords.filter(r => r.date?.slice(0, 7) === currentMonth).length,
completedThisMonth: records.filter(r => r.status === 'completed' && r.resolvedAt?.slice(0, 7) === currentMonth).length,
batteryDamage: damageRecords.filter(r => r.batteryId).length,
fleetDamage: damageRecords.filter(r => r.bikeId && !r.batteryId).length,
batteryMaintenance: maintenanceRecords.filter(r => r.batteryId).length,
fleetMaintenance: maintenanceRecords.filter(r => r.bikeId && !r.batteryId).length,
upcomingBattery: maintenanceRecords.filter(r => r.batteryId && r.status === 'reported').length,
upcomingFleet: maintenanceRecords.filter(r => r.bikeId && !r.batteryId && r.status === 'reported').length,
ongoingBattery: maintenanceRecords.filter(r => r.batteryId && r.status === 'in_progress').length,
ongoingFleet: maintenanceRecords.filter(r => r.bikeId && !r.batteryId && r.status === 'in_progress').length,
pendingMaintenance: maintenanceRecords.filter(r => r.status === 'reported' || r.status === 'in_progress').length,
completedMaintenance: maintenanceRecords.filter(r => r.status === 'completed').length,
critical: records.filter(r => r.severity === 'critical' && r.status !== 'completed').length, critical: records.filter(r => r.severity === 'critical' && r.status !== 'completed').length,
inProgress: records.filter(r => r.status === 'in_progress' || r.status === 'parts_ordered').length, inProgress: records.filter(r => r.status === 'in_progress' || r.status === 'parts_ordered').length,
completed: records.filter(r => r.status === 'completed').length, completed: records.filter(r => r.status === 'completed').length,
@@ -397,7 +425,7 @@ export default function MaintenancePage() {
}; };
return ( return (
<div className="p-4 lg:p-6"> <div className="p-4 lg:p-6 mb-6 lg:mb-0">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6"> <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div> <div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Damage & Maintenance</h1> <h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Damage & Maintenance</h1>
@@ -413,36 +441,45 @@ export default function MaintenancePage() {
</div> </div>
</div> </div>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-6"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100"> <div className="bg-white rounded-xl p-5 shadow-sm border border-red-100">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center"> <div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center">
<AlertTriangle className="w-6 h-6 text-red-600" /> <AlertTriangle className="w-6 h-6 text-red-600" />
</div> </div>
<div> <div className="flex-1">
<p className="text-2xl font-extrabold text-slate-800">{stats.critical}</p> <div className="flex items-baseline justify-between gap-2">
<p className="text-sm text-slate-500">Critical</p> <p className="text-2xl font-extrabold text-slate-800">{stats.damageCount}</p>
<span className="text-xs text-red-600 font-medium bg-red-50 px-2 py-0.5 rounded">{stats.damageThisMonth} this month</span>
</div>
<p className="text-sm text-slate-500">Total Damage</p>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100"> <div className="bg-white rounded-xl p-5 shadow-sm border border-blue-100">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center"> <div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
<Wrench className="w-6 h-6 text-blue-600" /> <Wrench className="w-6 h-6 text-blue-600" />
</div> </div>
<div> <div className="flex-1">
<p className="text-2xl font-extrabold text-slate-800">{stats.inProgress}</p> <div className="flex items-baseline justify-between gap-2">
<p className="text-sm text-slate-500">In Progress</p> <p className="text-2xl font-extrabold text-slate-800">{stats.maintenanceCount}</p>
<span className="text-xs text-blue-600 font-medium bg-blue-50 px-2 py-0.5 rounded">{stats.maintenanceThisMonth} this month</span>
</div>
<p className="text-sm text-slate-500">Total Maintenance</p>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100"> <div className="bg-white rounded-xl p-5 shadow-sm border border-green-100">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-green-50 flex items-center justify-center"> <div className="w-12 h-12 rounded-xl bg-green-50 flex items-center justify-center">
<CheckCircle className="w-6 h-6 text-green-600" /> <CheckCircle className="w-6 h-6 text-green-600" />
</div> </div>
<div> <div className="flex-1">
<p className="text-2xl font-extrabold text-slate-800">{stats.completed}</p> <div className="flex items-baseline justify-between gap-2">
<p className="text-2xl font-extrabold text-slate-800">{stats.completed}</p>
<span className="text-xs text-green-600 font-medium bg-green-50 px-2 py-0.5 rounded">{stats.completedThisMonth} this month</span>
</div>
<p className="text-sm text-slate-500">Completed</p> <p className="text-sm text-slate-500">Completed</p>
</div> </div>
</div> </div>
@@ -458,7 +495,7 @@ export default function MaintenancePage() {
</div> </div>
</div> </div>
</div> </div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100"> {/* <div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-purple-50 flex items-center justify-center"> <div className="w-12 h-12 rounded-xl bg-purple-50 flex items-center justify-center">
<DollarSign className="w-6 h-6 text-purple-600" /> <DollarSign className="w-6 h-6 text-purple-600" />
@@ -468,43 +505,145 @@ export default function MaintenancePage() {
<p className="text-sm text-slate-500">Total Cost</p> <p className="text-sm text-slate-500">Total Cost</p>
</div> </div>
</div> </div>
</div> </div> */}
</div> </div>
{mainCategory === 'damage' && (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-red-50 rounded-xl p-4 border border-red-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-red-100 flex items-center justify-center">
<AlertTriangle className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.damageCount}</p>
<p className="text-sm text-slate-500">Total Damage</p>
</div>
</div>
</div>
<div className="bg-green-50 rounded-xl p-4 border border-green-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
<Battery className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.batteryDamage}</p>
<p className="text-sm text-slate-500">Battery Damage</p>
</div>
</div>
</div>
<div className="bg-purple-50 rounded-xl p-4 border border-purple-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
<Bike className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.fleetDamage}</p>
<p className="text-sm text-slate-500">Fleet Damage</p>
</div>
</div>
</div>
<div className="bg-orange-50 rounded-xl p-4 border border-orange-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center">
<Clock className="w-5 h-5 text-orange-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.critical}</p>
<p className="text-sm text-slate-500">Critical Damage</p>
</div>
</div>
</div>
</div>
)}
{mainCategory === 'maintenance' && (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-blue-50 rounded-xl p-4 border border-blue-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
<Wrench className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.maintenanceCount}</p>
<p className="text-sm text-slate-500">Total Maintenance</p>
</div>
</div>
</div>
<div className="bg-purple-50 rounded-xl p-4 border border-purple-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
<Bike className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.upcomingFleet}</p>
<p className="text-sm text-slate-500">Upcoming Fleet</p>
</div>
</div>
</div>
<div className="bg-green-50 rounded-xl p-4 border border-green-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
<Battery className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.upcomingBattery}</p>
<p className="text-sm text-slate-500">Upcoming Battery</p>
</div>
</div>
</div>
<div className="bg-orange-50 rounded-xl p-4 border border-orange-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center">
<Activity className="w-5 h-5 text-orange-600" />
</div>
<div>
<p className="text-lg font-bold text-slate-800">{stats.ongoingBattery + stats.ongoingFleet}</p>
<p className="text-sm text-slate-500">Ongoing Maintenance</p>
</div>
</div>
</div>
</div>
)}
<div className="bg-white rounded-xl shadow-sm border border-slate-100 mb-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-100 mb-6">
<div className="p-4 border-b border-slate-100"> <div className="p-4 border-b border-slate-100">
<div className="flex flex-col lg:flex-row lg:items-center gap-4"> <div className="flex flex-col lg:flex-row lg:items-center gap-4">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex flex-col sm:flex-row items-start sm:items-center gap-3">
<button <div className="flex items-center gap-1 bg-slate-100 p-1 rounded-lg">
onClick={() => setActiveTab('all')} <button
className={`px-3 py-1.5 rounded-lg text-sm font-medium ${activeTab === 'all' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`} onClick={() => setMainCategory('damage')}
> className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 ${mainCategory === 'damage' ? 'bg-red-600 text-white shadow-sm' : 'text-slate-600 hover:bg-white hover:shadow-sm'}`}
All >
</button> <AlertTriangle className="w-4 h-4" /> Damage
<button </button>
onClick={() => setActiveTab('damage')} <button
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'damage' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`} onClick={() => setMainCategory('maintenance')}
> className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 ${mainCategory === 'maintenance' ? 'bg-blue-600 text-white shadow-sm' : 'text-slate-600 hover:bg-white hover:shadow-sm'}`}
<AlertTriangle className="w-4 h-4" /> Damage >
</button> <Wrench className="w-4 h-4" /> Maintenance
<button </button>
onClick={() => setActiveTab('repair')} </div>
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'repair' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`} <div className="flex items-center gap-1">
> <button
<Wrench className="w-4 h-4" /> Repair onClick={() => setTargetType('all')}
</button> className={`px-3 py-1.5 rounded-lg text-sm font-medium ${targetType === 'all' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
<button >
onClick={() => setActiveTab('service')} All
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'service' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`} </button>
> <button
<Wrench className="w-4 h-4" /> Service onClick={() => setTargetType('battery')}
</button> className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${targetType === 'battery' ? 'bg-green-600 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
<button >
onClick={() => setActiveTab('battery_swap')} <Battery className="w-4 h-4" /> Battery
className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${activeTab === 'battery_swap' ? 'bg-slate-800 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`} </button>
> <button
<Battery className="w-4 h-4" /> Battery onClick={() => setTargetType('fleet')}
</button> className={`px-3 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 ${targetType === 'fleet' ? 'bg-purple-600 text-white' : 'border border-slate-200 text-slate-600 hover:bg-slate-50'}`}
>
<Bike className="w-4 h-4" /> Fleet
</button>
</div>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="relative"> <div className="relative">
@@ -537,47 +676,59 @@ export default function MaintenancePage() {
{filteredRecords.map(record => { {filteredRecords.map(record => {
const TypeIcon = typeIcons[record.type]; const TypeIcon = typeIcons[record.type];
return ( return (
<Link key={record.id} href={`/admin/maintenance/${record.id}`} className="block p-5 hover:bg-slate-50 transition-colors"> <Link key={record.id} href={`/admin/maintenance/${record.id}`} className="block p-4 lg:p-5 hover:bg-slate-50 transition-colors">
<div className="flex flex-col lg:flex-row lg:items-start gap-4"> <div className="flex flex-col lg:flex-row lg:items-start gap-3 lg:gap-4">
<div className="flex items-center gap-4"> <div className="flex items-start gap-3">
<div className="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center"> <div className="w-10 h-10 lg:w-12 lg:h-12 rounded-lg lg:rounded-xl bg-slate-100 flex items-center justify-center flex-shrink-0">
<TypeIcon className="w-6 h-6 text-slate-600" /> <TypeIcon className="w-5 h-5 lg:w-6 lg:h-6 text-slate-600" />
</div> </div>
<div> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex flex-wrap items-center gap-1.5 lg:gap-2">
<p className="font-semibold text-slate-800">{record.id}</p> <p className="font-semibold text-slate-800 text-sm lg:text-base">{record.id}</p>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full ${severityColors[record.severity]}`}> {record.batteryId && (
<span className="inline-flex items-center gap-1 text-xs font-medium px-1.5 py-0.5 rounded-full bg-green-100 text-green-700">
<Battery className="w-3 h-3" /> Battery
</span>
)}
{!record.batteryId && record.bikeId && (
<span className="inline-flex items-center gap-1 text-xs font-medium px-1.5 py-0.5 rounded-full bg-purple-100 text-purple-700">
<Bike className="w-3 h-3" /> Fleet
</span>
)}
<span className={`inline-flex items-center gap-1 text-xs font-medium px-1.5 py-0.5 rounded-full ${severityColors[record.severity]}`}>
{record.severity} {record.severity}
</span> </span>
</div> </div>
<p className="text-sm text-slate-500 flex items-center gap-2"> <p className="text-xs lg:text-sm text-slate-500 flex flex-wrap items-center gap-x-1 lg:gap-x-2">
<Bike className="w-3 h-3" /> {record.bikeModel} ({record.bikePlate}) <span className="flex items-center gap-1"><Bike className="w-3 h-3" /> {record.bikeModel}</span>
<span className="text-slate-300">|</span> <span className="hidden sm:inline text-slate-300">|</span>
<User className="w-3 h-3" /> {record.reporterName} <span className="text-xs">{record.bikePlate}</span>
<span className="hidden lg:inline text-slate-300">|</span>
<span className="flex items-center gap-1"><User className="w-3 h-3" /> {record.reporterName}</span>
</p> </p>
</div> </div>
</div> </div>
<div className="flex-1"> <div className="flex-1 min-w-0">
<p className="text-sm text-slate-700">{record.description}</p> <p className="text-sm text-slate-700 line-clamp-2">{record.description}</p>
<div className="flex flex-wrap gap-4 text-sm text-slate-500 mt-1"> <div className="flex flex-wrap gap-2 lg:gap-4 text-xs lg:text-sm text-slate-500 mt-1.5">
<p className="flex items-center gap-1"> <p className="flex items-center gap-1">
<Clock className="w-3 h-3" /> {record.date} <Clock className="w-3 h-3" /> {record.date}
</p> </p>
<p className="flex items-center gap-1"> <p className="flex items-center gap-1 truncate max-w-[100px] lg:max-w-none">
<MapPin className="w-3 h-3" /> {record.location} <MapPin className="w-3 h-3 flex-shrink-0" /> <span className="truncate">{record.location}</span>
</p> </p>
{record.images.length > 0 && ( {record.images.length > 0 && (
<p className="flex items-center gap-1 text-blue-600"> <p className="flex items-center gap-1 text-blue-600">
<Image className="w-3 h-3" /> {record.images.length} photos <ImageIcon className="w-3 h-3" /> {record.images.length}
</p> </p>
)} )}
{record.notes.length > 0 && ( {record.notes.length > 0 && (
<button <button
onClick={() => toggleNotes(record.id)} onClick={(e) => { e.preventDefault(); toggleNotes(record.id); }}
className="flex items-center gap-1 text-purple-600" className="flex items-center gap-1 text-purple-600"
> >
<MessageSquare className="w-3 h-3" /> {record.notes.length} notes <MessageSquare className="w-3 h-3" /> {record.notes.length}
{expandedNotes.includes(record.id) ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />} {expandedNotes.includes(record.id) ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
</button> </button>
)} )}
@@ -592,24 +743,24 @@ export default function MaintenancePage() {
)} )}
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center justify-between lg:justify-end gap-2 lg:gap-3 mt-2 lg:mt-0">
<div className="text-right"> <div className="text-left lg:text-right">
<p className="text-sm font-medium text-slate-700">{record.actualCost || record.estimatedCost}</p> <p className="text-sm font-medium text-slate-700">{record.actualCost || record.estimatedCost}</p>
<p className="text-xs text-slate-500">{record.paymentStatus === 'paid' ? 'Paid' : record.paymentStatus === 'approved' ? 'Approved' : 'Payment ' + record.paymentStatus}</p> <p className="text-xs text-slate-500">{record.paymentStatus === 'paid' ? 'Paid' : record.paymentStatus === 'approved' ? 'Approved' : 'Payment ' + record.paymentStatus}</p>
</div> </div>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${statusColors[record.status]}`}> <div className="flex items-center gap-1.5 lg:gap-2">
{record.status === 'reported' && <Clock className="w-3 h-3" />} <span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full ${statusColors[record.status]}`}>
{record.status === 'in_progress' && <Wrench className="w-3 h-3" />} {record.status === 'reported' && <Clock className="w-3 h-3" />}
{record.status === 'parts_ordered' && <AlertTriangle className="w-3 h-3" />} {record.status === 'in_progress' && <Wrench className="w-3 h-3" />}
{record.status === 'completed' && <CheckCircle className="w-3 h-3" />} {record.status === 'parts_ordered' && <AlertTriangle className="w-3 h-3" />}
{record.status === 'cancelled' && <XCircle className="w-3 h-3" />} {record.status === 'completed' && <CheckCircle className="w-3 h-3" />}
{record.status.replace('_', ' ')} {record.status === 'cancelled' && <XCircle className="w-3 h-3" />}
</span> <span className="hidden sm:inline">{record.status.replace('_', ' ')}</span>
</span>
<div className="flex gap-1">
<button <button
onClick={(e) => { e.preventDefault(); setSelectedRecord(record); setShowDetailsModal(true); }} onClick={(e) => { e.preventDefault(); router.push(`/admin/maintenance/${record.id}`); }}
className="p-2 hover:bg-slate-100 rounded-lg text-slate-500" className="p-1.5 lg:p-2 hover:bg-slate-100 rounded-lg text-slate-500"
title="View Details" title="View Details"
> >
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
@@ -714,7 +865,7 @@ export default function MaintenancePage() {
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-4 gap-2">
{selectedRecord.images.map((img) => ( {selectedRecord.images.map((img) => (
<div key={img.id} className="aspect-square bg-slate-100 rounded-lg flex flex-col items-center justify-center"> <div key={img.id} className="aspect-square bg-slate-100 rounded-lg flex flex-col items-center justify-center">
<Image className="w-8 h-8 text-slate-400" /> <ImageIcon className="w-8 h-8 text-slate-400" />
<span className="text-xs text-slate-500 mt-1">{img.name}</span> <span className="text-xs text-slate-500 mt-1">{img.name}</span>
</div> </div>
))} ))}
@@ -794,53 +945,146 @@ export default function MaintenancePage() {
</button> </button>
</div> </div>
<div className="p-4 overflow-y-auto max-h-[70vh] space-y-4"> <div className="p-4 overflow-y-auto max-h-[70vh] space-y-4">
<div>
<label className="text-sm font-semibold text-slate-700 mb-2 block">Report Type *</label>
<div className="flex gap-2 mb-4">
<button
type="button"
onClick={() => setReportType('damage')}
className={`flex-1 py-3 px-4 rounded-xl border-2 flex items-center justify-center gap-2 font-medium transition-colors ${reportType === 'damage' ? 'border-red-500 bg-red-50 text-red-700' : 'border-slate-200 text-slate-600 hover:border-red-300'}`}
>
<AlertTriangle className="w-5 h-5" /> Damage
</button>
<button
type="button"
onClick={() => setReportType('maintenance')}
className={`flex-1 py-3 px-4 rounded-xl border-2 flex items-center justify-center gap-2 font-medium transition-colors ${reportType === 'maintenance' ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-slate-200 text-slate-600 hover:border-blue-300'}`}
>
<Wrench className="w-5 h-5" /> Maintenance
</button>
</div>
</div>
<div>
<label className="text-sm font-semibold text-slate-700 mb-2 block">Select Target *</label>
<div className="grid grid-cols-2 gap-3 mb-4">
<button
type="button"
className="p-4 border-2 rounded-xl flex flex-col items-center gap-2 hover:border-green-500 hover:bg-green-50 transition-colors"
>
<Battery className="w-8 h-8 text-green-600" />
<span className="font-medium text-slate-700">Battery</span>
<span className="text-xs text-slate-500">For battery issues</span>
</button>
<button
type="button"
className="p-4 border-2 rounded-xl flex flex-col items-center gap-2 hover:border-purple-500 hover:bg-purple-50 transition-colors"
>
<Bike className="w-8 h-8 text-purple-600" />
<span className="font-medium text-slate-700">Fleet (Bike)</span>
<span className="text-xs text-slate-500">For bike issues</span>
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Issue Type *</label> <label className="text-xs font-medium text-slate-600 mb-1 block">Category *</label>
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"> <select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="damage">Damage</option> <option value="">Select Category</option>
<option value="repair">Repair</option> {reportType === 'damage' ? (
<option value="service">Service</option> <>
<option value="battery_swap">Battery Swap</option> <option value="damage">Damage</option>
<option value="inspection">Inspection</option> <option value="repair">Repair</option>
<option value="other">Other</option> <option value="accident">Accident</option>
<option value="theft">Theft</option>
<option value="vandalism">Vandalism</option>
</>
) : (
<>
<option value="service">Service</option>
<option value="routine_service">Routine Service</option>
<option value="repair">Repair</option>
<option value="battery_swap">Battery Swap</option>
<option value="inspection">Inspection</option>
</>
)}
</select> </select>
</div> </div>
<div> {reportType === 'damage' ? (
<label className="text-xs font-medium text-slate-600 mb-1 block">Severity *</label> <div>
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"> <label className="text-xs font-medium text-slate-600 mb-1 block">Severity *</label>
<option value="critical">Critical</option> <select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="major">Major</option> <option value="critical">Critical</option>
<option value="minor">Minor</option> <option value="major">Major</option>
<option value="cosmetic">Cosmetic</option> <option value="minor">Minor</option>
</select> <option value="cosmetic">Cosmetic</option>
</div> </select>
<div> </div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Bike ID *</label> ) : (
<input type="text" placeholder="EV-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" /> <div>
</div> <label className="text-xs font-medium text-slate-600 mb-1 block">Status</label>
<select className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm">
<option value="pending">Pending</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
</select>
</div>
)}
<div> <div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Battery ID</label> <label className="text-xs font-medium text-slate-600 mb-1 block">Battery ID</label>
<input type="text" placeholder="BAT-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" /> <input type="text" placeholder="BAT-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div> </div>
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Bike ID</label>
<input type="text" placeholder="EV-XXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
{reportType === 'damage' ? (
<>
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Reporter Name *</label>
<input type="text" placeholder="Enter name" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Reporter Phone *</label>
<input type="tel" placeholder="01XXXXXXXXX" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</>
) : (
<>
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Performed By</label>
<input type="text" placeholder="Technician name" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Next Due Date</label>
<input type="date" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</>
)}
</div> </div>
<div> <div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Description *</label> <label className="text-xs font-medium text-slate-600 mb-1 block">Description *</label>
<textarea rows={3} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Describe the issue..." /> <textarea rows={3} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Describe the issue in detail..." />
</div> </div>
<div> <div className="grid grid-cols-2 gap-4">
<label className="text-xs font-medium text-slate-600 mb-1 block">Location *</label> <div>
<input type="text" placeholder="Where did the issue occur?" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" /> <label className="text-xs font-medium text-slate-600 mb-1 block">Location / Hub *</label>
</div> <input type="text" placeholder="e.g., Gulshan Hub" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
<div> </div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Estimated Cost</label> <div>
<input type="number" placeholder="0" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" /> <label className="text-xs font-medium text-slate-600 mb-1 block">
{reportType === 'damage' ? 'Estimated Cost (৳)' : 'Service Cost (৳)'}
</label>
<input type="number" placeholder="0" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div> </div>
<div> <div>
<label className="text-xs font-medium text-slate-600 mb-1 block">Upload Images</label> <label className="text-xs font-medium text-slate-600 mb-1 block">Upload Images</label>
<div className="border-2 border-dashed border-slate-200 rounded-lg p-6 text-center"> <div className="border-2 border-dashed border-slate-200 rounded-lg p-6 text-center hover:border-accent cursor-pointer">
<Image className="w-8 h-8 text-slate-300 mx-auto mb-2" /> <ImageIcon className="w-8 h-8 text-slate-300 mx-auto mb-2" />
<p className="text-sm text-slate-500">Drag and drop or click to upload</p> <p className="text-sm text-slate-500">Click to upload images</p>
<p className="text-xs text-slate-400">JPG, PNG up to 5MB</p>
</div> </div>
</div> </div>
</div> </div>
@@ -848,7 +1092,7 @@ export default function MaintenancePage() {
<button onClick={() => setShowNewModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50"> <button onClick={() => setShowNewModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
Cancel Cancel
</button> </button>
<button onClick={() => { setShowNewModal(false); alert('Issue reported successfully!'); }} className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark"> <button onClick={() => { setShowNewModal(false); setShowSuccessModal(true); }} className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent-dark">
Submit Report Submit Report
</button> </button>
</div> </div>
@@ -953,6 +1197,24 @@ export default function MaintenancePage() {
</div> </div>
</div> </div>
)} )}
{showSuccessModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md p-6 text-center">
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<h3 className="text-xl font-bold text-slate-800 mb-2">Issue Reported Successfully!</h3>
<p className="text-slate-500 mb-6">Your issue has been submitted and will be reviewed shortly.</p>
<button
onClick={() => setShowSuccessModal(false)}
className="px-6 py-2 bg-accent text-white rounded-lg hover:bg-accent-dark"
>
OK
</button>
</div>
</div>
)}
</div> </div>
); );
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,382 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import {
ArrowLeft, Search, Bike, User, MapPin, Battery,
Phone, MessageCircle, X, Navigation, Clock, RefreshCw
} from 'lucide-react';
interface Rental {
id: string;
bikeId: string;
userId: string;
userName: string;
userPhone: string;
bikeModel: string;
bikePlate: string;
bikeBattery: number;
status: 'pending' | 'accepted' | 'active' | 'completed' | 'cancelled' | 'locked';
type: 'single' | 'shared' | 'rent-to-own';
hubId: string;
hubName: string;
location?: {
lat: number;
lng: number;
address?: string;
lastUpdate?: string;
speed?: number;
heading?: number;
};
}
const mockRentals: Rental[] = [
{
id: 'RNT-001',
bikeId: 'BIKE-001',
userId: 'USR-003',
userName: 'Jamal Uddin',
userPhone: '+8801912345678',
bikeModel: 'AIMA Lightning',
bikePlate: 'Dhaka Metro Cha-9012',
bikeBattery: 87,
status: 'active',
type: 'single',
hubId: 'HUB-001',
hubName: 'Gulshan Hub',
location: { lat: 23.7925, lng: 90.4074, address: 'Gulshan 1, Dhaka', lastUpdate: '2024-03-28 14:30:00', speed: 0, heading: 180 },
},
{
id: 'RNT-002',
bikeId: 'BIKE-002',
userId: 'USR-004',
userName: 'Rafiq Islam',
userPhone: '+8801512345678',
bikeModel: 'Yadea DT3',
bikePlate: 'Dhaka Metro Ba-5521',
bikeBattery: 65,
status: 'active',
type: 'shared',
hubId: 'HUB-002',
hubName: 'Banani Hub',
location: { lat: 23.8041, lng: 90.4152, address: 'Banani, Dhaka', lastUpdate: '2024-03-28 14:28:00', speed: 15, heading: 90 },
},
{
id: 'RNT-003',
bikeId: 'BIKE-003',
userId: 'USR-001',
userName: 'Rahim Ahmed',
userPhone: '+8801712345678',
bikeModel: 'AIMA EM5',
bikePlate: 'Dhaka Metro Ko-1234',
bikeBattery: 92,
status: 'active',
type: 'rent-to-own',
hubId: 'HUB-003',
hubName: 'Uttara Hub',
location: { lat: 23.8776, lng: 90.4014, address: 'Uttara Sector 11, Dhaka', lastUpdate: '2024-03-28 14:25:00', speed: 25, heading: 270 },
},
{
id: 'RNT-004',
bikeId: 'BIKE-005',
userId: 'USR-005',
userName: 'Farid Ahmed',
userPhone: '+8801612345678',
bikeModel: 'Yadea G5',
bikePlate: 'Dhaka Metro Ha-5678',
bikeBattery: 45,
status: 'active',
type: 'single',
hubId: 'HUB-004',
hubName: 'Mirpur Hub',
location: { lat: 23.8222, lng: 90.3639, address: 'Mirpur 10, Dhaka', lastUpdate: '2024-03-28 14:20:00', speed: 0, heading: 0 },
},
{
id: 'RNT-005',
bikeId: 'BIKE-001',
userId: 'USR-002',
userName: 'Karim Hasan',
userPhone: '+8801812345678',
bikeModel: 'AIMA Lightning',
bikePlate: 'Dhaka Metro Cha-9012',
bikeBattery: 78,
status: 'pending',
type: 'single',
hubId: 'HUB-001',
hubName: 'Gulshan Hub',
location: { lat: 23.7889, lng: 90.4025, address: 'Dhanmondi 27, Dhaka', lastUpdate: '2024-03-28 14:15:00', speed: 8, heading: 45 },
},
];
const statusColors: Record<string, { bg: string; text: string }> = {
active: { bg: 'bg-green-100', text: 'text-green-700' },
pending: { bg: 'bg-amber-100', text: 'text-amber-700' },
accepted: { bg: 'bg-blue-100', text: 'text-blue-700' },
completed: { bg: 'bg-indigo-100', text: 'text-indigo-700' },
cancelled: { bg: 'bg-slate-100', text: 'text-slate-600' },
locked: { bg: 'bg-red-100', text: 'text-red-700' },
};
export default function RentalMapPage() {
const [rentals, setRentals] = useState<Rental[]>(mockRentals);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [selectedRental, setSelectedRental] = useState<Rental | null>(null);
const [lastRefresh, setLastRefresh] = useState(new Date());
const [liveLocations, setLiveLocations] = useState<Record<string, { lat: number; lng: number; speed: number }>>({});
const filteredRentals = rentals.filter(r => {
const matchesSearch = r.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.userName.toLowerCase().includes(searchQuery.toLowerCase()) ||
r.bikeModel.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = statusFilter === 'all' || r.status === statusFilter;
return matchesSearch && matchesStatus && r.location;
});
const simulateLiveUpdate = () => {
const newLocations: Record<string, { lat: number; lng: number; speed: number }> = {};
rentals.forEach(rental => {
if (rental.location && rental.status === 'active') {
const movement = Math.random() * 0.002 - 0.001;
newLocations[rental.id] = {
lat: rental.location.lat + movement,
lng: rental.location.lng + movement,
speed: Math.floor(Math.random() * 30),
};
}
});
setLiveLocations(newLocations);
setLastRefresh(new Date());
};
useEffect(() => {
const interval = setInterval(simulateLiveUpdate, 5000);
return () => clearInterval(interval);
}, [rentals]);
const getMarkerPosition = (rental: Rental) => {
if (liveLocations[rental.id]) {
return { lat: liveLocations[rental.id].lat, lng: liveLocations[rental.id].lng };
}
return { lat: rental.location?.lat || 0, lng: rental.location?.lng || 0 };
};
return (
<div className="min-h-screen bg-slate-100">
<div className="p-4 bg-white shadow-sm border-b border-slate-200">
<div className="flex items-center gap-3 mb-3">
<Link href="/admin/rentals" className="p-2 hover:bg-slate-100 rounded-lg">
<ArrowLeft className="w-5 h-5 text-slate-600" />
</Link>
<div className="flex-1">
<h1 className="text-xl font-extrabold text-slate-800">Live Rental Map</h1>
</div>
<button
onClick={simulateLiveUpdate}
className="p-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700"
>
<RefreshCw className="w-5 h-5" />
</button>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm text-slate-600"
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="py-2 px-4 border border-slate-200 rounded-lg text-sm font-medium text-slate-600"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="completed">Completed</option>
</select>
</div>
</div>
<div className="p-4 space-y-4">
<div className="bg-white rounded-xl shadow-sm border border-slate-100 relative overflow-hidden h-64 sm:h-80 lg:h-96">
<div className="absolute inset-0 bg-slate-100">
<div className="relative w-full h-full">
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center">
<Navigation className="w-16 h-16 text-emerald-400 mx-auto mb-4" />
<p className="text-lg font-semibold text-slate-600">Live Map View</p>
<p className="text-sm text-slate-500 mt-1">
Showing {filteredRentals.length} rentals on map
</p>
<p className="text-xs text-slate-400 mt-2">
Last updated: {lastRefresh.toLocaleTimeString()}
</p>
</div>
</div>
{filteredRentals.map((rental, index) => {
const pos = getMarkerPosition(rental);
const x = ((pos.lng - 90.35) / 0.15) * 100;
const y = ((23.95 - pos.lat) / 0.2) * 100;
return (
<div
key={rental.id}
className={`absolute transform -translate-x-1/2 -translate-y-1/2 cursor-pointer ${selectedRental?.id === rental.id ? 'z-20' : 'z-10'}`}
style={{ left: `${Math.min(95, Math.max(5, x))}%`, top: `${Math.min(90, Math.max(10, y))}%` }}
onClick={() => setSelectedRental(rental)}
>
<div className={`relative w-10 h-10 rounded-full flex items-center justify-center shadow-lg ${rental.status === 'active' ? 'bg-green-500' : rental.status === 'pending' ? 'bg-amber-500' : 'bg-slate-400'}`}>
<Bike className="w-5 h-5 text-white" />
<span className="absolute -top-1 -right-1 w-4 h-4 bg-white rounded-full flex items-center justify-center text-[10px] font-bold text-slate-700">
{index + 1}
</span>
</div>
<div className="absolute top-12 left-1/2 -translate-x-1/2 bg-white px-2 py-1 rounded-lg shadow text-xs font-medium text-slate-700 whitespace-nowrap">
{rental.id}
</div>
</div>
);
})}
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden flex flex-col max-h-96">
<div className="p-3 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-700 text-sm">
Rental List ({filteredRentals.length})
</h3>
<div className="flex items-center gap-1 text-xs text-slate-500">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
Live
</div>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-2">
{filteredRentals.map((rental, index) => (
<div
key={rental.id}
onClick={() => setSelectedRental(rental)}
className={`p-3 rounded-lg border cursor-pointer transition-all ${selectedRental?.id === rental.id ? 'border-emerald-500 bg-emerald-50' : 'border-slate-200 hover:border-emerald-300'}`}
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-6 h-6 rounded-full bg-emerald-100 flex items-center justify-center text-xs font-bold text-emerald-700">
{index + 1}
</span>
<Link href={`/admin/rentals/${rental.id}`} className="text-sm font-medium text-emerald-600 hover:underline">
{rental.id}
</Link>
</div>
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${statusColors[rental.status].bg} ${statusColors[rental.status].text}`}>
{rental.status}
</span>
</div>
<div className="flex items-center gap-2 mb-2">
<Bike className="w-4 h-4 text-slate-400" />
<span className="text-sm text-slate-600">{rental.bikeModel}</span>
</div>
<div className="flex items-center gap-2 mb-2">
<User className="w-4 h-4 text-slate-400" />
<span className="text-sm text-slate-600">{rental.userName}</span>
</div>
<div className="flex items-center justify-between text-xs text-slate-500">
<div className="flex items-center gap-1">
<MapPin className="w-3 h-3" />
{rental.location?.address || rental.hubName}
</div>
<div className="flex items-center gap-1">
<Battery className="w-3 h-3" />
{rental.bikeBattery}%
</div>
</div>
{liveLocations[rental.id] && (
<div className="mt-2 pt-2 border-t border-slate-100 flex items-center justify-between text-xs">
<span className="text-slate-400">
Speed: {liveLocations[rental.id].speed} km/h
</span>
<span className="text-slate-400 flex items-center gap-1">
<Clock className="w-3 h-3" /> {rental.location?.lastUpdate}
</span>
</div>
)}
</div>
))}
</div>
</div>
</div>
{selectedRental && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={() => setSelectedRental(null)}>
<div className="bg-white rounded-xl shadow-xl w-full max-w-md" onClick={e => e.stopPropagation()}>
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-800">Rental Details</h3>
<button onClick={() => setSelectedRental(null)} className="text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-4">
<div className="flex items-center justify-between">
<span className="text-lg font-bold text-emerald-600">{selectedRental.id}</span>
<span className={`text-xs font-medium px-2 py-1 rounded-full ${statusColors[selectedRental.status].bg} ${statusColors[selectedRental.status].text}`}>
{selectedRental.status}
</span>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Bike</p>
<p className="font-medium text-slate-700">{selectedRental.bikeModel}</p>
<p className="text-xs text-slate-400">{selectedRental.bikePlate}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Battery</p>
<p className={`font-medium ${selectedRental.bikeBattery > 50 ? 'text-green-600' : selectedRental.bikeBattery > 20 ? 'text-amber-600' : 'text-red-600'}`}>
{selectedRental.bikeBattery}%
</p>
</div>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500 mb-1">Renter</p>
<p className="font-medium text-slate-700">{selectedRental.userName}</p>
<p className="text-sm text-slate-500">{selectedRental.userPhone}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500 mb-1">Location</p>
<p className="font-medium text-slate-700">{selectedRental.location?.address}</p>
<p className="text-xs text-slate-400">Lat: {selectedRental.location?.lat.toFixed(4)}, Lng: {selectedRental.location?.lng.toFixed(4)}</p>
{selectedRental.location?.lastUpdate && (
<p className="text-xs text-slate-400 mt-1">Last Update: {selectedRental.location.lastUpdate}</p>
)}
</div>
<div className="flex gap-2">
<a href={`tel:${selectedRental.userPhone}`} className="flex-1 py-2 px-4 bg-emerald-100 text-emerald-700 rounded-lg text-sm font-medium text-center flex items-center justify-center gap-2 hover:bg-emerald-200">
<Phone className="w-4 h-4" /> Call
</a>
<a href={`sms:${selectedRental.userPhone}`} className="flex-1 py-2 px-4 bg-blue-100 text-blue-700 rounded-lg text-sm font-medium text-center flex items-center justify-center gap-2 hover:bg-blue-200">
<MessageCircle className="w-4 h-4" /> SMS
</a>
</div>
<Link href={`/admin/rentals/${selectedRental.id}`} className="block w-full py-2 px-4 bg-slate-100 text-slate-700 rounded-lg text-sm font-medium text-center hover:bg-slate-200">
View Full Details
</Link>
</div>
</div>
</div>
)}
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Shield, Plus, Search, X, Edit, Trash2, Copy, Check, ChevronDown, ChevronRight, BookOpen, FileSearch, Settings, BarChart3, Bike, Users, Briefcase, Truck, Store, BatteryCharging, Building2, Wrench, DollarSign, TrendingUp, UserCog } from 'lucide-react'; import { Shield, Plus, Search, X, Edit, Trash2, Copy, Check, ChevronDown, ChevronRight, BookOpen, FileSearch, Settings, BarChart3, Bike, Users, Briefcase, Truck, Store, BatteryCharging, Building2, Wrench, DollarSign, TrendingUp, UserCog, Bell, MessageSquare } from 'lucide-react';
interface Permission { interface Permission {
key: string; key: string;
@@ -35,11 +35,10 @@ interface Role {
} }
const buildDefaultGroups = (): PermissionGroup[] => [ const buildDefaultGroups = (): PermissionGroup[] => [
{ {
id: 'kyc', id: 'kyc',
title: 'KYC Requests & Verification', title: 'KYC Requests & Verification',
description: 'The Biker user will request from the app. Investor, Shop, Merchant will request from the website. Front desk officers (hub/head office) can request for Biker, Investor, Shop, Merchant and can upload remaining documents. Admin officers (head office) will approve or reject documents with notes and "make a Biker | Investor | Shop | Merchant".', description: 'The Biker user will request from the app. Biker, Investor, Shop, Merchant will request from the website. Front desk officers (hub/head office) can request for Biker, Investor, Shop, Merchant, and can upload remaining documents. Admin managers (head office) will approve or reject documents with notes and "make a Biker | Investor | Shop | Merchant".',
icon: FileSearch, icon: FileSearch,
permissions: [ permissions: [
{ key: 'kyc.request', label: 'KYC Request', enabled: false }, { key: 'kyc.request', label: 'KYC Request', enabled: false },
@@ -47,7 +46,7 @@ const buildDefaultGroups = (): PermissionGroup[] => [
{ key: 'kyc.doc_upload', label: 'Document Upload', enabled: false }, { key: 'kyc.doc_upload', label: 'Document Upload', enabled: false },
{ key: 'kyc.doc_approve', label: 'Document Approve', enabled: false }, { key: 'kyc.doc_approve', label: 'Document Approve', enabled: false },
{ key: 'kyc.doc_reject', label: 'Document Reject', enabled: false }, { key: 'kyc.doc_reject', label: 'Document Reject', enabled: false },
{ key: 'kyc.make_valid_user', label: 'Make a Biker | Make an Investor | Make a Shop | Make a Merchant', enabled: false }, { key: 'kyc.make_valid_user', label: 'Make a Biker | Make an Investor | Make a Swap Station | Make a Merchant', enabled: false },
] ]
}, },
{ {
@@ -62,15 +61,20 @@ const buildDefaultGroups = (): PermissionGroup[] => [
edit: { key: 'settings.kyc_documents_config', label: 'Config', enabled: false }, edit: { key: 'settings.kyc_documents_config', label: 'Config', enabled: false },
}, },
{ {
label: 'Plan Selection with Condition', label: 'Plan Selection with EV Condition',
view: { key: 'settings.plan_selection_with_condition_view', label: 'View', enabled: false }, view: { key: 'settings.plan_selection_with_condition_view', label: 'View', enabled: false },
edit: { key: 'settings.plan_selection_with_condition_config', label: 'Config', enabled: false }, edit: { key: 'settings.plan_selection_with_condition_config', label: 'Config', enabled: false },
}, },
{ {
label: 'Investment Plan', label: 'EV Investment Plan',
view: { key: 'settings.investment_plan_view', label: 'View', enabled: false }, view: { key: 'settings.investment_plan_view', label: 'View', enabled: false },
edit: { key: 'settings.investment_plan_config', label: 'Config', enabled: false }, edit: { key: 'settings.investment_plan_config', label: 'Config', enabled: false },
}, },
{
label: 'Battery Investment Plan',
view: { key: 'settings.battery_investment_plan_view', label: 'View', enabled: false },
edit: { key: 'settings.battery_investment_plan_config', label: 'Config', enabled: false },
},
{ {
label: 'Swap Station Plan', label: 'Swap Station Plan',
view: { key: 'settings.swap_station_plan_view', label: 'View', enabled: false }, view: { key: 'settings.swap_station_plan_view', label: 'View', enabled: false },
@@ -86,13 +90,23 @@ const buildDefaultGroups = (): PermissionGroup[] => [
view: { key: 'settings.company_policy_view', label: 'View', enabled: false }, view: { key: 'settings.company_policy_view', label: 'View', enabled: false },
edit: { key: 'settings.company_policy_config', label: 'Config', enabled: false }, edit: { key: 'settings.company_policy_config', label: 'Config', enabled: false },
}, },
{
label: 'Email & SMS Templates',
view: { key: 'settings.es_templates_view', label: 'View', enabled: false },
edit: { key: 'settings.es_templates_config', label: 'Config', enabled: false },
},
{
label: 'EV Parts',
view: { key: 'settings.ev_parts_view', label: 'View', enabled: false },
edit: { key: 'settings.ev_parts_config', label: 'Config', enabled: false },
},
], ],
permissions: [], permissions: [],
}, },
{ {
id: 'dashboard', id: 'dashboard',
title: 'Dashboard', title: 'Dashboard',
description: 'Access to main dashboard', description: 'Access to main dashboard insights and summary metrics.',
icon: BarChart3, icon: BarChart3,
permissions: [ permissions: [
{ key: 'dashboard.view', label: 'View Dashboard', enabled: false }, { key: 'dashboard.view', label: 'View Dashboard', enabled: false },
@@ -101,133 +115,188 @@ const buildDefaultGroups = (): PermissionGroup[] => [
{ {
id: 'rentals', id: 'rentals',
title: 'Rentals', title: 'Rentals',
description: 'Manage rental operations', description: 'Front desk officers can select user, rental type, contract duration, bike, hub and payment methods to create rentals. Bikers can accept or reject rentals. Managers can edit, cancel, lock, or unlock rentals and view default and penalty fee tracking.',
icon: Bike, icon: Bike,
permissions: [ permissions: [
{ key: 'rentals.view', label: 'View', enabled: false }, { key: 'rental.view', label: 'View Rentals', enabled: false },
{ key: 'rentals.create', label: 'Create', enabled: false }, { key: 'rental.create', label: 'Create Rental', enabled: false },
{ key: 'rentals.edit', label: 'Edit', enabled: false }, { key: 'rental.requset', label: 'Rental Request', enabled: false },
{ key: 'rentals.delete', label: 'Delete', enabled: false }, { key: 'rental.accept', label: 'Accept Rental', enabled: false },
{ key: 'rental.reject', label: 'Reject Rental', enabled: false },
{ key: 'rental.edit', label: 'Edit Rental', enabled: false },
{ key: 'rental.cancel', label: 'Cancel Rental', enabled: false },
{ key: 'rental.image_approve', label: 'Image Approve', enabled: false },
{ key: 'rental.lock', label: 'Lock Rental', enabled: false },
{ key: 'rental.unlock', label: 'Unlock Rental', enabled: false },
] ]
}, },
{ {
id: 'bikers', id: 'bikers',
title: 'Bikers', title: 'Bikers Management',
description: 'Manage bikers', description: 'Handle automatic biker profile creation after KYC approval, basic detail updates, status/membership changes, document uploads/removals, internal notes and activity tracking.',
icon: Users, icon: Users,
permissions: [ permissions: [
{ key: 'bikers.view', label: 'View', enabled: false }, { key: 'biker.view', label: 'View Biker Profile', enabled: false },
{ key: 'bikers.create', label: 'Create', enabled: false }, { key: 'biker.create', label: 'Create Biker Profile', enabled: false },
{ key: 'bikers.edit', label: 'Edit', enabled: false }, { key: 'biker.edit', label: 'Edit Biker Profile', enabled: false },
{ key: 'bikers.delete', label: 'Delete', enabled: false }, { key: 'biker.delete', label: 'Soft Delete / Deactivate Biker', enabled: false },
{ key: 'biker.status_change', label: 'Change Biker Status', enabled: false },
{ key: 'biker.membership_change', label: 'Change Biker Membership', enabled: false },
{ key: 'biker.kyc_view', label: 'View Biker KYC Info', enabled: false },
{ key: 'biker.kyc_update', label: 'Update Biker KYC Info', enabled: false },
{ key: 'biker.activity_view', label: 'View Biker Activity Logs', enabled: false },
{ key: 'biker.document_view', label: 'View Biker Documents', enabled: false },
{ key: 'biker.document_upload', label: 'Upload Biker Documents', enabled: false },
{ key: 'biker.document_delete', label: 'Remove Biker Documents', enabled: false },
{ key: 'biker.rental_history_view', label: 'View Biker Rental History', enabled: false },
{ key: 'biker.payment_history_view', label: 'View Biker Payment History', enabled: false },
{ key: 'biker.wallet_view', label: 'View Biker Wallet Balance', enabled: false },
{ key: 'biker.note_add', label: 'Add Biker Internal Notes', enabled: false },
{ key: 'biker.note_view', label: 'View Biker Internal Notes', enabled: false },
{ key: 'biker.export', label: 'Export Biker Reports & Data', enabled: false },
{ key: 'biker.make_valid_user', label: 'Make Biker Active & Valid', enabled: false },
{ key: 'biker.lock', label: 'Lock Biker Account', enabled: false },
{ key: 'biker.unlock', label: 'Unlock Biker Account', enabled: false },
] ]
}, },
{ {
id: 'investors', id: 'investors',
title: 'Investors', title: 'Investors Management',
description: 'Manage investors', description: 'Manage automatically created profiles, assign EV & Battery Investment plans, oversee bank accounts/tax logs, track daily rental shares, process withdrawal requests and notifications.',
icon: Briefcase, icon: Briefcase,
permissions: [ permissions: [
{ key: 'investors.view', label: 'View', enabled: false }, { key: 'investor.view', label: 'View Investors', enabled: false },
{ key: 'investors.create', label: 'Create', enabled: false }, { key: 'investor.create', label: 'Create Investor Profile', enabled: false },
{ key: 'investors.edit', label: 'Edit', enabled: false }, { key: 'investor.edit', label: 'Edit Investor Details', enabled: false },
{ key: 'investors.delete', label: 'Delete', enabled: false }, { key: 'investor.delete', label: 'Soft Delete Investor', enabled: false },
{ key: 'investor.plan_assign', label: 'Assign Investment Plans', enabled: false },
{ key: 'investor.bank_edit', label: 'Manage Bank & Tax Info', enabled: false },
{ key: 'investor.withdraw_request', label: 'Withdrawal Requests', enabled: false },
{ key: 'investor.document_upload', label: 'Upload Investor Documents', enabled: false },
{ key: 'investor.document_approve', label: 'Approve Investor Documents', enabled: false },
{ key: 'investor.notification_view', label: 'View Investor Notifications', enabled: false },
]
},
{
id: 'battery',
title: 'Battery Management',
description: 'Add new battery assets with pricing, deposit, daily rent, and BMS identifiers. Oversee ownership history, rental transactions, damage logs, maintenance, and data exports.',
icon: BatteryCharging,
permissions: [
{ key: 'battery.view', label: 'View Batteries', enabled: false },
{ key: 'battery.create', label: 'Add Battery & BMS Config', enabled: false },
{ key: 'battery.edit', label: 'Edit Battery details', enabled: false },
{ key: 'battery.delete', label: 'Delete Battery Record', enabled: false },
{ key: 'battery.export', label: 'Export Battery History', enabled: false },
] ]
}, },
{ {
id: 'fleet', id: 'fleet',
title: 'Fleet', title: 'Fleet Management',
description: 'Manage fleet vehicles', description: 'Register new EV bikes, configure GPS details, associate battery packs, view rental transactions, maintenance, damage history, investor ownership logs, and activity trackers.',
icon: Truck, icon: Truck,
permissions: [ permissions: [
{ key: 'fleet.view', label: 'View', enabled: false }, { key: 'fleet.view', label: 'View Fleet Vehicles', enabled: false },
{ key: 'fleet.create', label: 'Create', enabled: false }, { key: 'fleet.create', label: 'Register New Bike', enabled: false },
{ key: 'fleet.edit', label: 'Edit', enabled: false }, { key: 'fleet.edit', label: 'Edit Fleet Vehicle', enabled: false },
{ key: 'fleet.delete', label: 'Delete', enabled: false }, { key: 'fleet.delete', label: 'Soft Delete Bike', enabled: false },
{ key: 'fleet.gps_config', label: 'Configure Vehicle GPS', enabled: false },
{ key: 'fleet.export', label: 'Export Fleet Records', enabled: false },
] ]
}, },
{ {
id: 'merchants', id: 'service_centers',
title: 'Merchants', title: 'Service Centers',
description: 'Manage merchants', description: 'Add and manage service center listings, locations, and capacity. Track fleet service logs, maintenance logs, and uploaded dealer/biker invoices.',
icon: Store, icon: Wrench,
permissions: [ permissions: [
{ key: 'merchants.view', label: 'View', enabled: false }, { key: 'service_center.view', label: 'View Service Centers', enabled: false },
{ key: 'merchants.create', label: 'Create', enabled: false }, { key: 'service_center.create', label: 'Create Service Center', enabled: false },
{ key: 'merchants.edit', label: 'Edit', enabled: false }, { key: 'service_center.edit', label: 'Edit Service Center Details', enabled: false },
{ key: 'merchants.delete', label: 'Delete', enabled: false }, { key: 'service_center.delete', label: 'Remove Service Center', enabled: false },
]
},
{
id: 'swap_stations',
title: 'Swap Stations',
description: 'Manage swap stations',
icon: BatteryCharging,
permissions: [
{ key: 'swap_stations.view', label: 'View', enabled: false },
{ key: 'swap_stations.create', label: 'Create', enabled: false },
{ key: 'swap_stations.edit', label: 'Edit', enabled: false },
{ key: 'swap_stations.delete', label: 'Delete', enabled: false },
]
},
{
id: 'hubs',
title: 'Hubs',
description: 'Manage hubs',
icon: Building2,
permissions: [
{ key: 'hubs.view', label: 'View', enabled: false },
{ key: 'hubs.create', label: 'Create', enabled: false },
{ key: 'hubs.edit', label: 'Edit', enabled: false },
{ key: 'hubs.delete', label: 'Delete', enabled: false },
] ]
}, },
{ {
id: 'maintenance', id: 'maintenance',
title: 'Maintenance', title: 'Damage & Maintenance',
description: 'Manage maintenance requests', description: 'Report damage or technical issues from the biker side, and manage repairs, spare parts inventory, claim notifications, and service schedules on the admin side.',
icon: Wrench, icon: Settings,
permissions: [ permissions: [
{ key: 'maintenance.view', label: 'View', enabled: false }, { key: 'maintenance.view', label: 'View Damage Logs', enabled: false },
{ key: 'maintenance.create', label: 'Create', enabled: false }, { key: 'maintenance.create', label: 'Report Damage/Claim Free Service', enabled: false },
{ key: 'maintenance.edit', label: 'Edit', enabled: false }, { key: 'maintenance.edit', label: 'Update Repair Details', enabled: false },
{ key: 'maintenance.delete', label: 'Delete', enabled: false }, { key: 'maintenance.delete', label: 'Delete Repair Log', enabled: false },
] ]
}, },
{ {
id: 'accounting', id: 'accounting',
title: 'Accounting', title: 'Accounting',
description: 'Manage financial records', description: 'Oversee automatic journals generated by rentals, verify battery rental deposits, maintain general ledgers, and process and verify investor withdrawal requests.',
icon: DollarSign, icon: DollarSign,
permissions: [ permissions: [
{ key: 'accounting.view', label: 'View', enabled: false }, { key: 'accounting.view', label: 'View Financial Ledger', enabled: false },
{ key: 'accounting.create', label: 'Create', enabled: false }, { key: 'accounting.create', label: 'Create Financial Entry', enabled: false },
{ key: 'accounting.edit', label: 'Edit', enabled: false }, { key: 'accounting.edit', label: 'Modify Financial Entry', enabled: false },
{ key: 'accounting.delete', label: 'Delete', enabled: false }, { key: 'accounting.delete', label: 'Void Financial Entry', enabled: false },
{ key: 'accounting.withdraw_process', label: 'Process & Pay Withdrawal', enabled: false },
]
},
{
id: 'hubs',
title: 'Hubs Management',
description: 'Admin can create and manage geographic hubs, assign dedicated Hub Managers, allocate bike/battery inventory, and process hub-specific rentals and KYC checkups.',
icon: Building2,
permissions: [
{ key: 'hub.view', label: 'View Hubs', enabled: false },
{ key: 'hub.create', label: 'Create New Hub', enabled: false },
{ key: 'hub.edit', label: 'Edit Hub Details & Staff', enabled: false },
{ key: 'hub.delete', label: 'Deactivate Hub', enabled: false },
] ]
}, },
{ {
id: 'reports', id: 'reports',
title: 'Reports', title: 'Reports & Analytics',
description: 'View and generate reports', description: 'Generate, schedule, view, and export comprehensive administrative reports for KYC, fleet, investment payouts, rentals, and general ledger operations.',
icon: TrendingUp, icon: TrendingUp,
permissions: [ permissions: [
{ key: 'reports.view', label: 'View', enabled: false }, { key: 'reports.view', label: 'View Reports', enabled: false },
{ key: 'reports.export', label: 'Export', enabled: false }, { key: 'reports.export', label: 'Export Reports (CSV/PDF)', enabled: false },
] ]
}, },
{ {
id: 'users', id: 'users',
title: 'Users', title: 'Users & Staff Management',
description: 'Manage system users', description: 'Admin-only module for managing internal administrative users, hub managers, front desk staff, accountants, and Super Admins.',
icon: UserCog, icon: UserCog,
permissions: [ permissions: [
{ key: 'users.view', label: 'View', enabled: false }, { key: 'users.view', label: 'View System Users', enabled: false },
{ key: 'users.create', label: 'Create', enabled: false }, { key: 'users.create', label: 'Create Staff User', enabled: false },
{ key: 'users.edit', label: 'Edit', enabled: false }, { key: 'users.edit', label: 'Edit Staff details', enabled: false },
{ key: 'users.delete', label: 'Delete', enabled: false }, { key: 'users.delete', label: 'Deactivate Staff User', enabled: false },
] ]
}, },
{
id: 'roles',
title: 'Roles & Access Permissions',
description: 'Super Admin-only module to construct custom administrative roles and configure fine-grained permissions.',
icon: Shield,
permissions: [
{ key: 'roles.view', label: 'View Roles list', enabled: false },
{ key: 'roles.config', label: 'Configure & Update Roles', enabled: false },
]
},
{
id: 'notifications',
title: 'Notifications & Messaging',
description: 'Access system logs, alerts (KYC Verification, Rentals, Vehicle Service, Cabinet, Ledger), compose custom system notifications, broadcast messages, and schedule notifications.',
icon: Bell,
permissions: [
{ key: 'notifications.view', label: 'View Notifications', enabled: false },
{ key: 'messaging.compose', label: 'Compose Message', enabled: false },
{ key: 'messaging.broadcast', label: 'Broadcast message', enabled: false },
{ key: 'messaging.schedule', label: 'Schedule notification', enabled: false },
]
}
]; ];
const mockRoles: Role[] = [ const mockRoles: Role[] = [
@@ -238,7 +307,12 @@ const mockRoles: Role[] = [
isDefault: false, isDefault: false,
permissionGroups: buildDefaultGroups().map(g => ({ permissionGroups: buildDefaultGroups().map(g => ({
...g, ...g,
permissions: g.permissions.map(p => ({ ...p, enabled: true })) permissions: g.permissions.map(p => ({ ...p, enabled: true })),
permissionPairs: g.permissionPairs?.map(p => ({
...p,
view: { ...p.view, enabled: true },
edit: { ...p.edit, enabled: true }
}))
})) }))
}, },
{ {
@@ -251,13 +325,18 @@ const mockRoles: Role[] = [
permissions: g.permissions.map(p => ({ permissions: g.permissions.map(p => ({
...p, ...p,
enabled: !p.key.includes('delete') enabled: !p.key.includes('delete')
})),
permissionPairs: g.permissionPairs?.map(p => ({
...p,
view: { ...p.view, enabled: true },
edit: { ...p.edit, enabled: !p.edit.key.includes('config') && !p.edit.key.includes('delete') }
})) }))
})) }))
}, },
{ {
id: 'ROLE-003', id: 'ROLE-003',
name: 'Front Desk Officer', name: 'Front Desk Officer',
description: 'Hub/head office officer - can request KYC and upload documents', description: 'Hub/head office officer - can request KYC, create rentals and upload documents',
isDefault: false, isDefault: false,
permissionGroups: buildDefaultGroups().map(g => { permissionGroups: buildDefaultGroups().map(g => {
if (g.id === 'kyc') { if (g.id === 'kyc') {
@@ -269,9 +348,55 @@ const mockRoles: Role[] = [
})) }))
}; };
} }
if (g.id === 'rentals') {
return {
...g,
permissions: g.permissions.map(p => ({
...p,
enabled: ['rental.view', 'rental.create', 'rental.image_approve'].includes(p.key)
}))
};
}
if (g.id === 'bikers') {
return {
...g,
permissions: g.permissions.map(p => ({
...p,
enabled: ['biker.view', 'biker.edit', 'biker.kyc_view', 'biker.kyc_update', 'biker.activity_view', 'biker.document_view', 'biker.document_upload', 'biker.rental_history_view', 'biker.payment_history_view', 'biker.wallet_view', 'biker.note_add', 'biker.note_view'].includes(p.key)
}))
};
}
if (g.id === 'investors') {
return {
...g,
permissions: g.permissions.map(p => ({
...p,
enabled: ['investor.view', 'investor.document_upload'].includes(p.key)
}))
};
}
if (g.id === 'settings') {
return {
...g,
permissionPairs: g.permissionPairs?.map(p => ({
...p,
view: { ...p.view, enabled: true },
edit: { ...p.edit, enabled: false }
}))
};
}
if (g.id === 'dashboard') { if (g.id === 'dashboard') {
return { ...g, permissions: g.permissions.map(p => ({ ...p, enabled: true })) }; return { ...g, permissions: g.permissions.map(p => ({ ...p, enabled: true })) };
} }
if (['battery', 'fleet', 'service_centers', 'maintenance', 'accounting', 'hubs', 'reports'].includes(g.id)) {
return {
...g,
permissions: g.permissions.map(p => ({
...p,
enabled: p.key.includes('view')
}))
};
}
return g; return g;
}) })
}, },
@@ -319,7 +444,16 @@ const mockRoles: Role[] = [
...g, ...g,
permissions: g.permissions.map(p => ({ permissions: g.permissions.map(p => ({
...p, ...p,
enabled: ['rentals.view', 'rentals.create'].includes(p.key) enabled: ['rental.requset', 'rental.accept', 'rental.reject', 'rental.view'].includes(p.key)
}))
};
}
if (g.id === 'bikers') {
return {
...g,
permissions: g.permissions.map(p => ({
...p,
enabled: ['biker.view'].includes(p.key)
})) }))
}; };
} }

View File

@@ -0,0 +1,801 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import {
ArrowLeft, Building2, Star, Phone, Mail, MapPin, Activity, Wrench,
Battery, Bike, DollarSign, CheckCircle2, Clock, AlertTriangle, Search,
SlidersHorizontal, ArrowUpDown, User, Calendar, Shield, Tag, Plus, Eye,
BarChart3, Percent, ChevronRight, ExternalLink
} from 'lucide-react';
import Link from 'next/link';
import { ServiceCenter } from '../page';
// Interface for Maintenance History Record
interface HistoryRecord {
id: string;
date: string;
assetId: string;
assetType: 'EV Bike' | 'Battery';
serviceType: 'Damage' | 'Repair' | 'Service' | 'Battery Swap' | 'Inspection';
description: string;
severity: 'critical' | 'major' | 'minor' | 'cosmetic';
status: 'completed' | 'in_progress' | 'parts_ordered';
estimatedCost: number;
actualCost: number;
partsUsed: { name: string; qty: number; price: number }[];
laborCost: number;
technician: string;
}
// Generate realistic mock history data based on Center ID/Name
const getMockHistoryData = (centerName: string): HistoryRecord[] => {
const baseHistory: HistoryRecord[] = [
{
id: 'MNT-101',
date: '2024-03-21',
assetId: 'EV-004',
assetType: 'EV Bike',
serviceType: 'Damage',
description: 'Front fender shattered in traffic collision. Replaced brackets and front wheel.',
severity: 'major',
status: 'in_progress',
estimatedCost: 3500,
actualCost: 3200,
partsUsed: [
{ name: 'Front fender', qty: 1, price: 1500 },
{ name: 'Mounting brackets', qty: 2, price: 800 },
{ name: 'Brake pads', qty: 1, price: 600 }
],
laborCost: 1200,
technician: 'Sabbir Ahmed'
},
{
id: 'MNT-102',
date: '2024-03-18',
assetId: 'BAT-044',
assetType: 'Battery',
serviceType: 'Battery Swap',
description: 'Internal diagnostic showing rapid voltage degradation. Replaced cells and recalibrated BMS.',
severity: 'critical',
status: 'completed',
estimatedCost: 12000,
actualCost: 11500,
partsUsed: [
{ name: 'Battery 60V cell pack', qty: 1, price: 9500 },
{ name: 'BMS Controller Board', qty: 1, price: 2000 }
],
laborCost: 2500,
technician: 'Kamrul Hasan'
},
{
id: 'MNT-103',
date: '2024-03-15',
assetId: 'EV-012',
assetType: 'EV Bike',
serviceType: 'Service',
description: 'Routine 5,000km periodic maintenance. Calibrated drum brakes and greased chassis bearings.',
severity: 'minor',
status: 'completed',
estimatedCost: 1500,
actualCost: 1450,
partsUsed: [
{ name: 'Brake Cable', qty: 1, price: 250 },
{ name: 'Sprocket kit', qty: 1, price: 450 }
],
laborCost: 750,
technician: 'Sabbir Ahmed'
},
{
id: 'MNT-104',
date: '2024-03-10',
assetId: 'BAT-021',
assetType: 'Battery',
serviceType: 'Inspection',
description: 'Thermal warning flag during hyper-charging cycle. Terminals cleaned and thermal gel reapplied.',
severity: 'cosmetic',
status: 'completed',
estimatedCost: 500,
actualCost: 400,
partsUsed: [
{ name: 'Thermal paste', qty: 1, price: 150 }
],
laborCost: 250,
technician: 'Kamrul Hasan'
},
{
id: 'MNT-105',
date: '2024-03-05',
assetId: 'EV-009',
assetType: 'EV Bike',
serviceType: 'Repair',
description: 'Throttle failure reported by delivery driver. Replaced magnetic sensor assembly.',
severity: 'major',
status: 'completed',
estimatedCost: 1800,
actualCost: 2100,
partsUsed: [
{ name: 'Throttle control assembly', qty: 1, price: 800 },
{ name: 'Wiring loom adapter', qty: 1, price: 450 }
],
laborCost: 850,
technician: 'Rafiqul Islam'
},
{
id: 'MNT-106',
date: '2024-02-28',
assetId: 'EV-017',
assetType: 'EV Bike',
serviceType: 'Damage',
description: 'Rear tire blowout due to road debris. Replacement and alignment completed.',
severity: 'minor',
status: 'completed',
estimatedCost: 2800,
actualCost: 2750,
partsUsed: [
{ name: 'Rear Tire tubeless', qty: 1, price: 2200 },
{ name: 'Chain replacement', qty: 1, price: 400 }
],
laborCost: 500,
technician: 'Sabbir Ahmed'
},
{
id: 'MNT-107',
date: '2024-02-20',
assetId: 'BAT-089',
assetType: 'Battery',
serviceType: 'Battery Swap',
description: 'Dead module replacement under premium warranty. Replaced sub-assemblies.',
severity: 'critical',
status: 'parts_ordered',
estimatedCost: 15000,
actualCost: 0,
partsUsed: [
{ name: 'Battery 48V cell pack', qty: 1, price: 8000 }
],
laborCost: 1500,
technician: 'Kamrul Hasan'
}
];
// Variations in records based on Center's specialty & size to make data dynamic
if (centerName.includes('Gulshan') || centerName.includes('Center A')) {
return baseHistory;
} else if (centerName.includes('Banani') || centerName.includes('Center B')) {
return baseHistory.filter(h => h.serviceType === 'Battery Swap' || h.serviceType === 'Service' || h.serviceType === 'Inspection').map(h => ({
...h,
id: h.id.replace('10', '20'),
technician: 'Tanvir Rahman'
}));
} else {
// Uttara / Authorized
return baseHistory.filter(h => h.serviceType === 'Inspection' || h.serviceType === 'Repair').map(h => ({
...h,
id: h.id.replace('10', '30'),
technician: 'Arif Chowdhury'
}));
}
};
export default function ServiceCenterDetailsPage() {
const params = useParams();
const router = useRouter();
const id = params.id as string;
const [isMounted, setIsMounted] = useState(false);
const [center, setCenter] = useState<ServiceCenter | null>(null);
const [history, setHistory] = useState<HistoryRecord[]>([]);
// Filtering / Sorting / Search states for history
const [searchQuery, setSearchQuery] = useState('');
const [assetFilter, setAssetFilter] = useState('all');
const [typeFilter, setTypeFilter] = useState('all');
const [severityFilter, setSeverityFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [sortBy, setSortBy] = useState<'date-desc' | 'date-asc' | 'cost-desc' | 'cost-asc' | 'urgency-desc' | 'urgency-asc'>('date-desc');
// Map Interactive detail popup state
const [mapPopup, setMapPopup] = useState<string | null>(null);
useEffect(() => {
setIsMounted(true);
// Load Service Centers from localStorage
const stored = localStorage.getItem('jaiben_service_centers');
let foundCenter: ServiceCenter | null = null;
if (stored) {
try {
const centers: ServiceCenter[] = JSON.parse(stored);
foundCenter = centers.find(c => c.id === id) || null;
} catch (e) {}
}
if (foundCenter) {
setCenter(foundCenter);
setHistory(getMockHistoryData(foundCenter.name));
} else {
router.push('/admin/service-centers');
}
}, [id, router]);
if (!isMounted || !center) return null;
// Filter History records
const filteredHistory = history.filter(h => {
const matchesSearch = h.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
h.assetId.toLowerCase().includes(searchQuery.toLowerCase()) ||
h.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
h.technician.toLowerCase().includes(searchQuery.toLowerCase());
const matchesAsset = assetFilter === 'all' || h.assetType === assetFilter;
const matchesType = typeFilter === 'all' || h.serviceType === typeFilter;
const matchesSeverity = severityFilter === 'all' || h.severity === severityFilter;
const matchesStatus = statusFilter === 'all' || h.status === statusFilter;
return matchesSearch && matchesAsset && matchesType && matchesSeverity && matchesStatus;
});
// Sort History records
const severityWeights = { cosmetic: 1, minor: 2, major: 3, critical: 4 };
const sortedHistory = [...filteredHistory].sort((a, b) => {
switch (sortBy) {
case 'date-desc':
return new Date(b.date).getTime() - new Date(a.date).getTime();
case 'date-asc':
return new Date(a.date).getTime() - new Date(b.date).getTime();
case 'cost-desc':
return (b.actualCost || b.estimatedCost) - (a.actualCost || a.estimatedCost);
case 'cost-asc':
return (a.actualCost || a.estimatedCost) - (b.actualCost || b.estimatedCost);
case 'urgency-desc':
return severityWeights[b.severity] - severityWeights[a.severity];
case 'urgency-asc':
return severityWeights[a.severity] - severityWeights[b.severity];
default:
return 0;
}
});
// Financial & Aggregate calculations
const totalRepairs = history.length;
const completedRepairs = history.filter(h => h.status === 'completed');
const totalEstimatedCost = completedRepairs.reduce((sum, h) => sum + h.estimatedCost, 0);
const totalActualCost = completedRepairs.reduce((sum, h) => sum + h.actualCost, 0);
const costVariance = totalActualCost - totalEstimatedCost;
const totalPartsCost = completedRepairs.reduce((sum, h) => sum + h.partsUsed.reduce((s, p) => s + (p.price * p.qty), 0), 0);
const totalLaborCost = completedRepairs.reduce((sum, h) => sum + h.laborCost, 0);
const totalSpend = totalPartsCost + totalLaborCost;
// Aggregate Parts Utilized Log
const partsAggregated: { name: string; totalQty: number; totalCost: number }[] = [];
history.forEach(h => {
h.partsUsed.forEach(part => {
const existing = partsAggregated.find(p => p.name === part.name);
if (existing) {
existing.totalQty += part.qty;
existing.totalCost += part.price * part.qty;
} else {
partsAggregated.push({
name: part.name,
totalQty: part.qty,
totalCost: part.price * part.qty
});
}
});
});
const topParts = partsAggregated.sort((a, b) => b.totalQty - a.totalQty).slice(0, 5);
const statusColors = {
active: 'bg-emerald-100 text-emerald-700',
busy: 'bg-amber-100 text-amber-700',
inactive: 'bg-slate-100 text-slate-700'
};
const severityColors = {
critical: 'bg-red-100 text-red-700',
major: 'bg-orange-100 text-orange-700',
minor: 'bg-amber-100 text-amber-700',
cosmetic: 'bg-slate-100 text-slate-700'
};
const statusHistoryColors = {
completed: 'bg-emerald-100 text-emerald-700',
in_progress: 'bg-blue-100 text-blue-700',
parts_ordered: 'bg-purple-100 text-purple-700'
};
return (
<div className="p-4 lg:p-6 mb-6 lg:mb-0 space-y-6 max-w-8xl mx-auto">
{/* Navigation Top - standard layout of other detail profiles */}
<div className="flex items-center justify-between border-b border-slate-100 pb-4 mb-4">
<button
onClick={() => router.push('/admin/service-centers')}
className="flex items-center gap-2 text-slate-600 hover:text-slate-900 text-sm font-semibold transition-colors duration-200"
>
<ArrowLeft className="w-4 h-4 text-slate-400" /> Back to Service Centers
</button>
<span className="text-xs font-bold text-slate-400">
Node Registry: {center.id}
</span>
</div>
{/* Main Profile Info Header - rounded-xl alignment matching maintenance page */}
<div className="bg-white rounded-xl p-6 border border-slate-100 shadow-sm space-y-6 relative overflow-hidden">
{/* Glow effect decorative */}
<div className="absolute top-0 right-0 w-64 h-64 bg-accent/5 rounded-full blur-3xl -mr-16 -mt-16 pointer-events-none" />
<div className="flex flex-col lg:flex-row lg:items-start justify-between gap-6 relative z-10">
<div className="space-y-4 flex-1">
<div className="flex flex-wrap items-center gap-3">
<div className="w-14 h-14 rounded-xl bg-slate-50 border border-slate-100 flex items-center justify-center flex-shrink-0">
<Building2 className="w-7 h-7 text-slate-600" />
</div>
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800 leading-tight">{center.name}</h1>
<div className="flex flex-wrap items-center gap-2 mt-1">
<span className={`px-2.5 py-0.5 text-xs font-semibold rounded-full border border-transparent ${statusColors[center.status]}`}>
{center.status}
</span>
<div className="flex items-center gap-1 text-yellow-500 font-bold text-sm bg-yellow-50 px-2 py-0.5 rounded border border-yellow-100">
<Star className="w-3.5 h-3.5 fill-yellow-500" />
<span>{center.rating.toFixed(1)}</span>
</div>
</div>
</div>
</div>
{/* Profile items - clean, consistent spacing */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-y-3 gap-x-6 text-sm text-slate-600 pt-2">
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-slate-400" />
<div className="flex flex-wrap items-center gap-x-2">
<span>{center.address}</span>
{center.googleMapLink && (
<a
href={center.googleMapLink}
target="_blank"
rel="noopener noreferrer"
className="text-accent hover:underline flex items-center gap-0.5 font-bold"
>
Map Link <ExternalLink className="w-3 h-3" />
</a>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Phone className="w-4 h-4 text-slate-400" />
<span>{center.phone}</span>
</div>
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-slate-400" />
<span>{center.email}</span>
</div>
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-slate-400" />
<span>Staff: <strong className="font-semibold text-slate-800">{center.staffCount} technicians</strong></span>
</div>
<div className="flex items-center gap-2">
<Wrench className="w-4 h-4 text-slate-400" />
<span>Capacity: <strong className="font-semibold text-slate-800">{center.capacity}</strong> total slots</span>
</div>
</div>
{/* Specialization List Header */}
<div className="space-y-1.5 pt-2">
<span className="text-[10px] font-extrabold uppercase text-slate-400 tracking-wider">Node Specializations</span>
<div className="flex flex-wrap gap-1.5">
{center.specialization.map(spec => (
<span key={spec} className="px-2.5 py-1 bg-slate-50 border border-slate-100 rounded-lg text-xs font-bold text-slate-600">
{spec}
</span>
))}
</div>
</div>
</div>
{/* Quick Stats Header Summary - simplified matching maintenance specs, occupancy not needed */}
<div className="grid grid-cols-2 gap-4 bg-slate-50 p-5 rounded-xl border border-slate-100 w-full lg:max-w-xs flex-shrink-0">
<div className="space-y-1">
<p className="text-xs font-bold text-slate-400 uppercase tracking-wide">Repairs Logs</p>
<p className="text-2xl font-extrabold text-slate-800">{totalRepairs}</p>
<p className="text-[10px] text-slate-500">{completedRepairs.length} completed</p>
</div>
<div className="space-y-1">
<p className="text-xs font-bold text-slate-400 uppercase tracking-wide">Capacity</p>
<p className="text-2xl font-extrabold text-slate-800">{center.capacity}</p>
<p className="text-[10px] text-slate-500">Service slots registered</p>
</div>
</div>
</div>
</div>
{/* Analytics: Map Mockup & Cost Breakdown Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* INTERACTIVE STYLIZED MAP CONTAINER - aligned clean white rounded-xl styles */}
<div className="bg-white rounded-xl p-6 border border-slate-100 shadow-sm space-y-4 flex flex-col justify-between lg:col-span-1 min-h-[380px]">
<div className="space-y-1">
<h3 className="font-extrabold text-slate-800 text-lg">Node Location Map</h3>
<p className="text-xs text-slate-400">Dhaka city arterial coverage grid mockup</p>
</div>
{/* Map canvas container */}
<div className="flex-1 bg-slate-900 rounded-xl relative overflow-hidden border border-slate-850 shadow-inner flex items-center justify-center min-h-[220px]">
{/* Pulsating target coordinate representing the Center */}
<div
className="absolute w-8 h-8 flex items-center justify-center cursor-pointer group z-20"
style={{ top: '45%', left: '50%', transform: 'translate(-50%, -50%)' }}
onClick={() => setMapPopup(center.name)}
>
<span className="absolute inline-flex h-full w-full rounded-full bg-accent opacity-75 animate-ping" />
<div className="relative w-4 h-4 bg-accent border-2 border-white rounded-full flex items-center justify-center shadow-lg group-hover:scale-125 transition-transform">
<div className="w-1.5 h-1.5 bg-white rounded-full" />
</div>
</div>
{/* Stylized Dhaka grids & landmarks using SVGs */}
<svg className="w-full h-full absolute inset-0 opacity-40 select-none pointer-events-none" viewBox="0 0 100 100" preserveAspectRatio="none">
{/* Arterial Highways */}
<line x1="20" y1="0" x2="20" y2="100" stroke="#475569" strokeWidth="0.75" strokeDasharray="2" />
<line x1="50" y1="0" x2="50" y2="100" stroke="#475569" strokeWidth="1.5" />
<line x1="80" y1="0" x2="80" y2="100" stroke="#475569" strokeWidth="0.75" />
<line x1="0" y1="30" x2="100" y2="30" stroke="#475569" strokeWidth="0.75" />
<line x1="0" y1="50" x2="100" y2="50" stroke="#475569" strokeWidth="1.5" />
<line x1="0" y1="80" x2="100" y2="80" stroke="#475569" strokeWidth="0.75" />
{/* Waterway (Gulshan Lake) */}
<path d="M 50,0 Q 52,25 48,50 T 54,100" fill="none" stroke="#1e3a8a" strokeWidth="3" opacity="0.3" />
<path d="M 47,40 Q 70,45 80,42" fill="none" stroke="#1e3a8a" strokeWidth="2.5" opacity="0.3" />
{/* Other hubs mockup dots */}
<circle cx="20" cy="30" r="1.5" fill="#4f46e5" />
<circle cx="80" cy="30" r="1.5" fill="#4f46e5" />
<circle cx="20" cy="80" r="1.5" fill="#4f46e5" />
<circle cx="80" cy="80" r="1.5" fill="#4f46e5" />
</svg>
{/* Scale watermark */}
<div className="absolute bottom-2 left-2 text-[9px] text-slate-500 font-bold bg-slate-950/75 px-1.5 py-0.5 rounded border border-slate-800">
GPS: {center.latitude.toFixed(4)}°N, {center.longitude.toFixed(4)}°E
</div>
{/* Stylized popup when clicked */}
{mapPopup && (
<div className="absolute top-2 right-2 left-2 bg-slate-950/90 border border-slate-800 rounded-lg p-2.5 text-xs text-white z-30 animate-fadeIn space-y-1">
<div className="flex items-center justify-between">
<span className="font-extrabold text-accent">{center.name}</span>
<button onClick={() => setMapPopup(null)} className="text-slate-400 hover:text-white font-bold">×</button>
</div>
<p className="text-[10px] text-slate-400">{center.address}</p>
<div className="flex justify-between pt-1 border-t border-slate-800 text-[9px] text-slate-400 font-bold">
<span>Capacity: {center.capacity} slots</span>
<span>Rating: {center.rating.toFixed(1)}</span>
</div>
</div>
)}
{/* Custom street labels */}
<div className="absolute top-4 left-[53%] text-[8px] font-bold text-slate-600 tracking-widest uppercase origin-center rotate-90 select-none">
Gulshan Lake Road
</div>
<div className="absolute top-[52%] left-4 text-[8px] font-bold text-slate-600 tracking-widest uppercase select-none">
Tejgaon-Gulshan Link Road
</div>
</div>
<div className="text-center text-[10px] font-bold text-slate-400 uppercase tracking-widest pt-1">
📍 Click GPS coordinate node for telemetry details
</div>
</div>
{/* FINANCIAL PERFORMANCE & EXPENSE TRACKING */}
<div className="bg-white rounded-xl p-6 border border-slate-100 shadow-sm space-y-6 lg:col-span-2">
<div className="space-y-1">
<h3 className="font-extrabold text-slate-800 text-lg">Financial Performance & Cost Margins</h3>
<p className="text-xs text-slate-400">Aggregated historical metrics from completed maintenance invoices</p>
</div>
{/* Financial details panel */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-slate-50 border border-slate-100 p-4 rounded-xl flex flex-col justify-between">
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider block">Estimated Spend</span>
<div className="mt-2">
<p className="text-2xl font-extrabold text-slate-800">{totalEstimatedCost.toLocaleString()}</p>
<p className="text-[10px] text-slate-500 mt-0.5">Budgeted repairs cost</p>
</div>
</div>
<div className="bg-indigo-50/50 border border-indigo-100/50 p-4 rounded-xl flex flex-col justify-between">
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider block">Actual Invoice Spend</span>
<div className="mt-2">
<p className="text-2xl font-extrabold text-indigo-700">{totalActualCost.toLocaleString()}</p>
<p className="text-[10px] text-indigo-500 mt-0.5">Billed repair totals</p>
</div>
</div>
<div className={`p-4 rounded-xl border flex flex-col justify-between ${costVariance > 0 ? 'bg-rose-50 border-rose-100' : 'bg-emerald-50 border-emerald-100'}`}>
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider block">Cost Variance</span>
<div className="mt-2">
<p className={`text-2xl font-extrabold ${costVariance > 0 ? 'text-rose-700' : 'text-emerald-700'}`}>
{costVariance > 0 ? `+৳${costVariance.toLocaleString()}` : `-৳${Math.abs(costVariance).toLocaleString()}`}
</p>
<p className="text-[10px] text-slate-500 mt-0.5">
{costVariance > 0 ? 'Over budget invoices' : 'Under budget savings!'}
</p>
</div>
</div>
</div>
{/* Parts Used Aggregates & Labor Breakdown */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-2">
{/* Margins */}
<div className="space-y-4">
<h4 className="text-xs font-extrabold uppercase text-slate-400 tracking-wider">Expense Margin Distribution</h4>
<div className="space-y-3">
{/* Parts Spend Bar */}
<div className="space-y-1">
<div className="flex items-center justify-between text-xs font-bold text-slate-600">
<span className="flex items-center gap-1"><Battery className="w-3.5 h-3.5 text-indigo-500" /> Spare Parts Cost</span>
<span>{totalPartsCost.toLocaleString()} ({totalSpend > 0 ? Math.round((totalPartsCost/totalSpend)*100) : 0}%)</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div
className="bg-indigo-650 h-full rounded-full"
style={{ width: `${totalSpend > 0 ? (totalPartsCost/totalSpend)*100 : 0}%` }}
/>
</div>
</div>
{/* Labor Spend Bar */}
<div className="space-y-1">
<div className="flex items-center justify-between text-xs font-bold text-slate-600">
<span className="flex items-center gap-1"><Wrench className="w-3.5 h-3.5 text-emerald-500" /> Labor Costs</span>
<span>{totalLaborCost.toLocaleString()} ({totalSpend > 0 ? Math.round((totalLaborCost/totalSpend)*100) : 0}%)</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div
className="bg-emerald-550 h-full rounded-full"
style={{ width: `${totalSpend > 0 ? (totalLaborCost/totalSpend)*100 : 0}%` }}
/>
</div>
</div>
</div>
{/* General Health Tip */}
<div className="p-3 bg-slate-50 border border-slate-100 rounded-lg text-xs text-slate-500 flex items-start gap-2">
<AlertTriangle className="w-4 h-4 text-amber-500 flex-shrink-0 mt-0.5" />
<span>
<strong>Cost Ratio Notice:</strong> This node maintains a healthy parts-to-labor ratio of {totalSpend > 0 ? Math.round((totalPartsCost/totalSpend)*100) : 0}:{totalSpend > 0 ? Math.round((totalLaborCost/totalSpend)*100) : 0}. Lower labor ratios reflect technician efficiency.
</span>
</div>
</div>
{/* Parts utilized list */}
<div className="space-y-3">
<h4 className="text-xs font-extrabold uppercase text-slate-400 tracking-wider">Top Spare Parts Log</h4>
<div className="divide-y divide-slate-100 border border-slate-100 rounded-xl overflow-hidden bg-slate-50/50">
{topParts.length === 0 ? (
<p className="text-xs text-slate-400 p-4 text-center">No spare parts recorded yet</p>
) : topParts.map(part => (
<div key={part.name} className="p-2.5 flex items-center justify-between text-xs text-slate-600">
<span className="font-semibold text-slate-700">{part.name}</span>
<div className="flex items-center gap-4 text-right">
<span className="font-bold text-slate-500">Qty: {part.totalQty}</span>
<span className="font-extrabold text-slate-800">{part.totalCost.toLocaleString()}</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
{/* Interactive History Log - aligned standard filters and table headers */}
<div className="bg-white rounded-xl p-6 border border-slate-100 shadow-sm space-y-6">
{/* Section title */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 border-b border-slate-100 pb-4">
<div className="space-y-1">
<h3 className="font-extrabold text-slate-800 text-lg flex items-center gap-2">
<Activity className="w-5 h-5 text-indigo-500" />
<span>Serviced Fleet History Log</span>
</h3>
<p className="text-xs text-slate-400">Integrated audit list for EV bikes and Battery Swap maintenance nodes</p>
</div>
{/* Quick Counter */}
<span className="px-3 py-1 bg-indigo-50 border border-indigo-100/50 rounded-lg text-xs font-extrabold text-indigo-700 self-start lg:self-auto">
{sortedHistory.length} Matches Found
</span>
</div>
{/* Filter Controls Panel */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-3 bg-slate-50 p-4 rounded-lg border border-slate-100">
{/* Search bar */}
<div className="relative col-span-1 lg:col-span-2">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
type="text"
placeholder="Search by ID, tech, description..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-3 py-2 border border-slate-200 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all bg-white"
/>
</div>
{/* Filter 1: Asset Type */}
<select
value={assetFilter}
onChange={(e) => setAssetFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-xs font-semibold text-slate-600 bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="all">All Asset Types</option>
<option value="EV Bike">EV Bike</option>
<option value="Battery">Battery</option>
</select>
{/* Filter 2: Service Type */}
<select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
className="py-2 px-3 border border-slate-200 rounded-lg text-xs font-semibold text-slate-600 bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="all">All Service Types</option>
<option value="Damage">Damage</option>
<option value="Repair">Repair</option>
<option value="Service">Service</option>
<option value="Battery Swap">Battery Swap</option>
<option value="Inspection">Inspection</option>
</select>
{/* Sorting Dropdown */}
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="py-2 px-3 border border-slate-200 rounded-lg text-xs font-bold text-indigo-600 bg-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="date-desc">📆 Date: Newest First</option>
<option value="date-asc">📆 Date: Oldest First</option>
<option value="cost-desc"> Cost: Highest First</option>
<option value="cost-asc"> Cost: Lowest First</option>
<option value="urgency-desc"> Severity: Critical First</option>
<option value="urgency-asc"> Severity: Cosmetic First</option>
</select>
</div>
{/* Table / List representation */}
{sortedHistory.length === 0 ? (
<div className="text-center py-12 bg-slate-50 rounded-lg border border-dashed border-slate-200">
<Activity className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<p className="text-sm font-semibold text-slate-500">No matching service records</p>
<p className="text-xs text-slate-400 mt-0.5">Try clearing filters or search variables</p>
</div>
) : (
<div className="overflow-x-auto border border-slate-100 rounded-lg">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50 border-b border-slate-100 text-xs font-semibold uppercase tracking-wider text-slate-500">
<th className="py-3 px-4">Record ID</th>
<th className="py-3 px-4">Asset Code</th>
<th className="py-3 px-4">Service Type</th>
<th className="py-3 px-4">Description</th>
<th className="py-3 px-4 text-center">Severity</th>
<th className="py-3 px-4 text-center">Status</th>
<th className="py-3 px-4 text-right">Invoice cost</th>
<th className="py-3 px-4 text-right">Details</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 text-sm text-slate-700">
{sortedHistory.map(record => (
<tr key={record.id} className="hover:bg-slate-50/50 transition-colors">
<td className="py-4 px-4 font-bold text-slate-400">{record.id}</td>
<td className="py-4 px-4">
<div className="flex items-center gap-1.5">
{record.assetType === 'EV Bike' ? (
<Bike className="w-4 h-4 text-purple-600 flex-shrink-0" />
) : (
<Battery className="w-4 h-4 text-green-600 flex-shrink-0" />
)}
<div className="space-y-0.5">
<span className="font-extrabold text-slate-800">{record.assetId}</span>
<span className="text-[10px] text-slate-400 uppercase font-semibold block">{record.assetType}</span>
</div>
</div>
</td>
<td className="py-4 px-4 font-semibold text-slate-700">{record.serviceType}</td>
<td className="py-4 px-4 max-w-sm">
<div className="space-y-1">
<p className="text-xs text-slate-600 line-clamp-2 leading-relaxed">{record.description}</p>
<div className="flex items-center gap-3 text-[10px] text-slate-400 font-semibold">
<span className="flex items-center gap-0.5"><User className="w-3 h-3" /> Tech: {record.technician}</span>
<span className="flex items-center gap-0.5"><Calendar className="w-3 h-3" /> {record.date}</span>
</div>
</div>
</td>
<td className="py-4 px-4 text-center">
<span className={`px-2 py-0.5 text-xs font-semibold rounded-full border inline-block ${severityColors[record.severity]}`}>
{record.severity}
</span>
</td>
<td className="py-4 px-4 text-center">
<span className={`px-2.5 py-0.5 text-xs font-semibold rounded-full border inline-block ${statusHistoryColors[record.status]}`}>
{record.status.replace('_', ' ')}
</span>
</td>
<td className="py-4 px-4 text-right font-extrabold text-slate-800">
{record.actualCost > 0 ? (
<span>{record.actualCost.toLocaleString()}</span>
) : (
<span className="text-slate-400 font-normal italic text-xs">Pending invoice</span>
)}
</td>
<td className="py-4 px-4 text-right">
{/* Deep link details */}
<Link
href={`/admin/maintenance/${record.id}`}
className="p-1.5 hover:bg-slate-100 rounded-lg text-slate-400 hover:text-slate-700 transition-colors inline-block"
title="Open full maintenance record"
>
<Eye className="w-4 h-4" />
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,271 @@
'use client';
import { Plus, X, Save, Battery } from 'lucide-react';
import type { CompanySettings } from '../page';
interface BatteryInvestmentSettingsProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
activeBatteryTab: number;
setActiveBatteryTab: (n: number) => void;
addBatteryPlan: boolean;
setAddBatteryPlan: (v: boolean) => void;
newBatteryName: string;
setNewBatteryName: (v: string) => void;
newBatteryStatus: string;
setNewBatteryStatus: (v: string) => void;
newBatteryTarget: number;
setNewBatteryTarget: (n: number) => void;
newBatteryStart: string;
setNewBatteryStart: (v: string) => void;
newBatteryEnd: string;
setNewBatteryEnd: (v: string) => void;
newBatteryMin: number;
setNewBatteryMin: (n: number) => void;
newBatteryMax: number;
setNewBatteryMax: (n: number) => void;
newBatteryDuration: number;
setNewBatteryDuration: (n: number) => void;
newBatteryLock: number;
setNewBatteryLock: (n: number) => void;
newBatteryPenalty: number;
setNewBatteryPenalty: (n: number) => void;
newBatteryProfitShare: number;
setNewBatteryProfitShare: (n: number) => void;
newBatteryDesc: string;
setNewBatteryDesc: (v: string) => void;
newBatteryBasePrice: number;
setNewBatteryBasePrice: (n: number) => void;
newBatteryMinQuantity: number;
setNewBatteryMinQuantity: (n: number) => void;
createBatteryPlan: () => void;
handleSave: () => void;
}
export default function BatteryInvestmentSettings({
settings, setSettings,
activeBatteryTab, setActiveBatteryTab,
addBatteryPlan, setAddBatteryPlan,
newBatteryName, setNewBatteryName,
newBatteryStatus, setNewBatteryStatus,
newBatteryTarget, setNewBatteryTarget,
newBatteryStart, setNewBatteryStart,
newBatteryEnd, setNewBatteryEnd,
newBatteryMin, setNewBatteryMin,
newBatteryMax, setNewBatteryMax,
newBatteryDuration, setNewBatteryDuration,
newBatteryLock, setNewBatteryLock,
newBatteryPenalty, setNewBatteryPenalty,
newBatteryProfitShare, setNewBatteryProfitShare,
newBatteryDesc, setNewBatteryDesc,
newBatteryBasePrice, setNewBatteryBasePrice,
newBatteryMinQuantity, setNewBatteryMinQuantity,
createBatteryPlan, handleSave,
}: BatteryInvestmentSettingsProps) {
const calculatedMinInvestment = newBatteryMinQuantity * newBatteryBasePrice;
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-800 flex items-center gap-2">
<Battery className="w-5 h-5 text-emerald-600 animate-pulse" />
Battery Investment Plans
</h3>
</div>
<div className="flex items-center justify-between bg-emerald-50 border border-emerald-200 rounded-xl p-4">
<div>
<h4 className="font-semibold text-emerald-800">Battery Investment Plans ({(settings.plans as any).batteryInvestment?.length || 0})</h4>
<p className="text-sm text-emerald-600">Manage high-yield battery pack EV Investment Plans for partners</p>
</div>
<button onClick={() => { setAddBatteryPlan(true); setNewBatteryName(''); }} className="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm font-medium flex items-center gap-2 hover:bg-emerald-700 transition-colors">
<Plus className="w-4 h-4" /> New Plan
</button>
</div>
{addBatteryPlan && (
<div className="bg-white rounded-xl border border-emerald-300 overflow-hidden shadow-sm">
<div className="bg-emerald-50 px-4 py-3 border-b border-emerald-100 flex items-center justify-between">
<div>
<h4 className="font-semibold text-emerald-800">New Battery Investment Plan</h4>
<p className="text-sm text-emerald-600 mt-1">Configure high-yield battery fleet assets</p>
</div>
<button onClick={() => setAddBatteryPlan(false)} className="text-emerald-600 hover:text-emerald-800">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4">
<div className="grid lg:grid-cols-3 gap-4">
<div>
<label className="text-sm text-slate-600">Plan Name</label>
<input type="text" value={newBatteryName} onChange={(e) => setNewBatteryName(e.target.value)} placeholder="e.g., Standard Battery Plan" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Status</label>
<select value={newBatteryStatus} onChange={(e) => setNewBatteryStatus(e.target.value)} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1">
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="closed">Closed</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Target Amount ()</label>
<input type="number" value={newBatteryTarget} onChange={(e) => setNewBatteryTarget(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Start Date</label>
<input type="date" value={newBatteryStart} onChange={(e) => setNewBatteryStart(e.target.value)} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">End Date</label>
<input type="date" value={newBatteryEnd} onChange={(e) => setNewBatteryEnd(e.target.value)} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Battery Base Price ()</label>
<input type="number" value={newBatteryBasePrice} onChange={(e) => setNewBatteryBasePrice(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" placeholder="Single battery unit cost" />
</div>
<div>
<label className="text-sm text-slate-600">Minimum Quantity (Packs)</label>
<input type="number" value={newBatteryMinQuantity} onChange={(e) => setNewBatteryMinQuantity(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" placeholder="Min battery packs to invest" />
</div>
<div>
<label className="text-sm text-slate-600">Min Investment ()</label>
<div className="flex items-center gap-2 mt-1">
<input type="number" value={calculatedMinInvestment} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100" />
<span className="text-xs text-slate-500 whitespace-nowrap">= Qty × Base Price</span>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Max Investment ()</label>
<input type="number" value={newBatteryMax} onChange={(e) => setNewBatteryMax(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Duration (Months)</label>
<input type="number" value={newBatteryDuration} onChange={(e) => setNewBatteryDuration(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Lock-in Period (Months)</label>
<input type="number" value={newBatteryLock} onChange={(e) => setNewBatteryLock(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Early Exit Penalty (%)</label>
<input type="number" value={newBatteryPenalty} onChange={(e) => setNewBatteryPenalty(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Profit Share Percent (%)</label>
<input type="number" value={newBatteryProfitShare} onChange={(e) => setNewBatteryProfitShare(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
</div>
<div className="mt-4">
<label className="text-sm text-slate-600">Description</label>
<textarea value={newBatteryDesc} onChange={(e) => setNewBatteryDesc(e.target.value)} placeholder="Enter battery investment plan description" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" rows={2} />
</div>
<button onClick={createBatteryPlan} className="mt-4 px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm font-medium hover:bg-emerald-700 transition-colors">Create Plan</button>
</div>
</div>
)}
<div className="flex gap-2 border-b border-slate-200">
{((settings.plans as any).batteryInvestment || []).map((plan: any, idx: number) => (
<button key={idx} onClick={() => setActiveBatteryTab(idx)} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activeBatteryTab === idx ? 'border-emerald-500 text-emerald-600 font-bold' : 'border-transparent text-slate-500 hover:text-slate-700'}`}> {plan.name}</button>
))}
</div>
{((settings.plans as any).batteryInvestment || []).length > 0 && ((settings.plans as any).batteryInvestment || []).map((plan: any, idx: number) => idx === activeBatteryTab && (
<div key={idx} className="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
<div className="bg-emerald-50/50 px-4 py-3 border-b border-emerald-100 flex items-center justify-between">
<div>
<h4 className="font-semibold text-emerald-800">{plan.name}</h4>
<p className="text-sm text-emerald-600 mt-1">{plan.description}</p>
</div>
<div className="flex items-center gap-2">
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${plan.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>{plan.status}</span>
<button
onClick={() => {
const updated = ((settings.plans as any).batteryInvestment || []).filter((_: any, i: number) => i !== idx);
setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } });
setActiveBatteryTab(0);
}}
className="p-1.5 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Delete Plan"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
<div className="p-4">
<div className="grid lg:grid-cols-3 gap-4">
<div>
<label className="text-sm text-slate-600">Plan Name</label>
<input type="text" value={plan.name} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].name = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Status</label>
<select value={plan.status} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].status = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1">
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="closed">Closed</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Target Amount ()</label>
<input type="number" value={plan.targetAmount} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].targetAmount = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Start Date</label>
<input type="date" value={plan.startDate} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].startDate = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">End Date</label>
<input type="date" value={plan.endDate} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].endDate = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Battery Base Price ()</label>
<input type="number" value={plan.batteryBasePrice} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].batteryBasePrice = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Minimum Quantity (Packs)</label>
<input type="number" value={plan.minQuantity} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].minQuantity = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Min Investment ()</label>
<div className="flex items-center gap-2 mt-1">
<input type="number" value={plan.batteryBasePrice * plan.minQuantity} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100" />
<span className="text-xs text-slate-500 whitespace-nowrap">= Qty × Price</span>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Max Investment ()</label>
<input type="number" value={plan.maxInvestment} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].maxInvestment = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Duration (Months)</label>
<input type="number" value={plan.durationMonths} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].durationMonths = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Lock-in Period (Months)</label>
<input type="number" value={plan.lockInMonths} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].lockInMonths = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Early Exit Penalty (%)</label>
<input type="number" value={plan.earlyExitPenalty} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].earlyExitPenalty = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Profit Share Percent (%)</label>
<input type="number" value={plan.profitSharePercent} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].profitSharePercent = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
</div>
<div className="mt-4">
<label className="text-sm text-slate-600">Description</label>
<textarea value={plan.description} onChange={(e) => { const updated = [...((settings.plans as any).batteryInvestment || [])]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, batteryInvestment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" rows={2} />
</div>
<div className="mt-4 flex justify-end">
<button onClick={handleSave} className="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm font-medium flex items-center gap-2 hover:bg-emerald-700 transition-colors shadow-sm">
<Save className="w-4 h-4" /> Save Changes
</button>
</div>
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,119 @@
'use client';
import { Image, Camera, Ruler } from 'lucide-react';
import { CompanySettings } from '../page';
interface BrandingSettingsProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
}
export default function BrandingSettings({ settings, setSettings }: BrandingSettingsProps) {
return (
<div className="p-6 space-y-6">
<h3 className="text-lg font-semibold text-slate-800">Branding & Design</h3>
<div className="grid lg:grid-cols-2 gap-6">
<div>
<label className="text-sm text-slate-600">Company Logo</label>
<div className="mt-2 border-2 border-dashed border-slate-200 rounded-lg p-6 text-center hover:border-accent cursor-pointer">
<Image className="w-12 h-12 mx-auto text-slate-400" />
<p className="text-sm text-slate-500 mt-2">Upload Logo (PNG, SVG)</p>
<p className="text-xs text-slate-400">Recommended: 200x50px</p>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Favicon</label>
<div className="mt-2 border-2 border-dashed border-slate-200 rounded-lg p-6 text-center hover:border-accent cursor-pointer">
<Camera className="w-12 h-12 mx-auto text-slate-400" />
<p className="text-sm text-slate-500 mt-2">Upload Favicon</p>
<p className="text-xs text-slate-400">Recommended: 32x32px</p>
</div>
</div>
</div>
<div className="grid lg:grid-cols-2 gap-6 pt-4">
<div>
<label className="text-sm text-slate-600">Primary Color</label>
<div className="flex items-center gap-3 mt-2">
<input
type="color"
value={settings.primaryColor}
onChange={(e) => setSettings({ ...settings, primaryColor: e.target.value })}
className="w-12 h-12 rounded-lg cursor-pointer border-0"
/>
<input
type="text"
value={settings.primaryColor}
onChange={(e) => setSettings({ ...settings, primaryColor: e.target.value })}
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Secondary Color</label>
<div className="flex items-center gap-3 mt-2">
<input
type="color"
value={settings.secondaryColor}
onChange={(e) => setSettings({ ...settings, secondaryColor: e.target.value })}
className="w-12 h-12 rounded-lg cursor-pointer border-0"
/>
<input
type="text"
value={settings.secondaryColor}
onChange={(e) => setSettings({ ...settings, secondaryColor: e.target.value })}
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Border Radius</label>
<div className="flex items-center gap-4 mt-2">
<Ruler className="w-5 h-5 text-slate-400" />
<input
type="range"
min={0}
max={20}
value={settings.borderRadius}
onChange={(e) => setSettings({ ...settings, borderRadius: parseInt(e.target.value) })}
className="flex-1"
/>
<span className="text-sm text-slate-600">{settings.borderRadius}px</span>
</div>
<div className="flex gap-2 mt-4">
{[4, 8, 12, 16].map(radius => (
<button
key={radius}
onClick={() => setSettings({ ...settings, borderRadius: radius })}
className={`px-3 py-2 text-sm border ${settings.borderRadius === radius ? 'bg-accent text-white' : 'border-slate-200'}`}
style={{ borderRadius: radius }}
>
{radius}px
</button>
))}
</div>
</div>
<div>
<label className="text-sm text-slate-600">Preview</label>
<div className="mt-2 p-6 bg-slate-100 rounded-lg flex items-center justify-center gap-4">
<div
className="px-4 py-2 text-white font-bold"
style={{ backgroundColor: settings.primaryColor, borderRadius: settings.borderRadius }}
>
Primary
</div>
<div
className="px-4 py-2 text-white font-bold"
style={{ backgroundColor: settings.secondaryColor, borderRadius: settings.borderRadius }}
>
Secondary
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,512 @@
'use client';
import { Plus, Pencil, Trash2 } from 'lucide-react';
import RichTextEditor from '@/components/RichTextEditor';
import { CompanySettings } from '../page';
interface CompanyPolicySettingsProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
activeMasterTab: 'investor' | 'merchant' | 'swapstation' | 'rentalType';
setActiveMasterTab: (tab: 'investor' | 'merchant' | 'swapstation' | 'rentalType') => void;
activeRentalTypeTab: 'single' | 'shared' | 'renttoown';
setActiveRentalTypeTab: (tab: 'single' | 'shared' | 'renttoown') => void;
editingPolicy: { tab: string; index: number } | null;
setEditingPolicy: (policy: { tab: string; index: number } | null) => void;
editPolicyName: string;
setEditPolicyName: (name: string) => void;
editPolicyDesc: string;
setEditPolicyDesc: (desc: string) => void;
editPolicyDescHtml: string;
setEditPolicyDescHtml: (desc: string) => void;
editPolicyShowApp: boolean;
setEditPolicyShowApp: (v: boolean) => void;
editPolicyShowWeb: boolean;
setEditPolicyShowWeb: (v: boolean) => void;
showAddPolicy: boolean;
setShowAddPolicy: (show: boolean) => void;
newPolicyName: string;
setNewPolicyName: (name: string) => void;
newPolicyDesc: string;
setNewPolicyDesc: (desc: string) => void;
newPolicyShowApp: boolean;
setNewPolicyShowApp: (v: boolean) => void;
newPolicyShowWeb: boolean;
setNewPolicyShowWeb: (v: boolean) => void;
}
export default function CompanyPolicySettings({
settings,
setSettings,
activeMasterTab,
setActiveMasterTab,
activeRentalTypeTab,
setActiveRentalTypeTab,
editingPolicy,
setEditingPolicy,
editPolicyName,
setEditPolicyName,
editPolicyDesc,
setEditPolicyDesc,
editPolicyDescHtml,
setEditPolicyDescHtml,
editPolicyShowApp,
setEditPolicyShowApp,
editPolicyShowWeb,
setEditPolicyShowWeb,
showAddPolicy,
setShowAddPolicy,
newPolicyName,
setNewPolicyName,
newPolicyDesc,
setNewPolicyDesc,
newPolicyShowApp,
setNewPolicyShowApp,
newPolicyShowWeb,
setNewPolicyShowWeb,
}: CompanyPolicySettingsProps) {
return (
<div className="p-6 space-y-6">
<h3 className="text-lg font-semibold text-slate-800">Company's Policy</h3>
<div className="flex gap-2 border-b border-slate-200">
<button onClick={() => setActiveMasterTab('investor')} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activeMasterTab === 'investor' ? 'border-blue-500 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>Investor</button>
<button onClick={() => setActiveMasterTab('merchant')} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activeMasterTab === 'merchant' ? 'border-blue-500 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>Merchant</button>
<button onClick={() => setActiveMasterTab('swapstation')} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activeMasterTab === 'swapstation' ? 'border-blue-500 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>Swap Station</button>
<button onClick={() => setActiveMasterTab('rentalType')} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activeMasterTab === 'rentalType' ? 'border-blue-500 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>Rental Types</button>
</div>
{activeMasterTab === 'investor' && (
<div className="space-y-4">
<div className="flex items-center justify-between mb-2">
<label className="text-sm text-slate-600 font-medium">Policy List</label>
<button onClick={() => setShowAddPolicy(true)} className="text-xs text-blue-600 hover:text-blue-700 flex items-center gap-1">
<Plus className="w-3 h-3" /> Add Policy
</button>
</div>
{showAddPolicy && (
<div className="p-3 bg-white rounded-lg border border-blue-200 space-y-3">
<input type="text" value={newPolicyName} onChange={(e) => setNewPolicyName(e.target.value)} className="w-full px-2 py-1.5 border border-slate-200 rounded text-sm" placeholder="Policy Title" />
<RichTextEditor value={newPolicyDesc} onChange={(val) => setNewPolicyDesc(val)} placeholder="Policy Description..." minHeight={100} />
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<button type="button" onClick={() => setNewPolicyShowApp(!newPolicyShowApp)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${newPolicyShowApp ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${newPolicyShowApp ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on App</span>
</div>
<div className="flex items-center gap-2">
<button type="button" onClick={() => setNewPolicyShowWeb(!newPolicyShowWeb)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${newPolicyShowWeb ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${newPolicyShowWeb ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on Web</span>
</div>
</div>
<div className="flex gap-2">
<button onClick={() => {
if (!newPolicyName.trim()) return;
const newPolicy = { title: newPolicyName, description: newPolicyDesc, showApp: newPolicyShowApp, showWeb: newPolicyShowWeb };
setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, investor: [...settings.companyPolicy.investor, newPolicy as typeof settings.companyPolicy.investor[number]] } });
setNewPolicyName('');
setNewPolicyDesc('');
setNewPolicyShowApp(true);
setNewPolicyShowWeb(true);
setShowAddPolicy(false);
}} className="px-2 py-1 bg-blue-600 text-white rounded text-xs">Add</button>
<button onClick={() => { setShowAddPolicy(false); setNewPolicyName(''); setNewPolicyDesc(''); setNewPolicyShowApp(true); setNewPolicyShowWeb(true); }} className="px-2 py-1 bg-slate-200 text-slate-600 rounded text-xs">Cancel</button>
</div>
</div>
)}
<div className="space-y-2">
{(settings.companyPolicy?.investor || []).map((policy, i) => (
<div key={i} className="p-3 bg-slate-50 rounded-lg border border-slate-200">
{editingPolicy?.tab === 'investor' && editingPolicy?.index === i ? (
<div className="space-y-2">
<input type="text" value={editPolicyName} onChange={(e) => setEditPolicyName(e.target.value)} className="w-full px-2 py-1.5 border border-slate-200 rounded text-sm" placeholder="Policy Title" />
<RichTextEditor value={editPolicyDescHtml} onChange={(val) => setEditPolicyDescHtml(val)} placeholder="Policy Description..." minHeight={100} />
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<button type="button" onClick={() => setEditPolicyShowApp(!editPolicyShowApp)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${editPolicyShowApp ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${editPolicyShowApp ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on App</span>
</div>
<div className="flex items-center gap-2">
<button type="button" onClick={() => setEditPolicyShowWeb(!editPolicyShowWeb)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${editPolicyShowWeb ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${editPolicyShowWeb ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on Web</span>
</div>
</div>
<div className="flex gap-2">
<button onClick={() => {
const updated = [...settings.companyPolicy.investor];
updated[i] = { title: editPolicyName, description: editPolicyDescHtml, showApp: editPolicyShowApp, showWeb: editPolicyShowWeb };
setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, investor: updated } });
setEditingPolicy(null);
setEditPolicyDescHtml('');
}} className="px-2 py-1 bg-blue-600 text-white rounded text-xs">Save</button>
<button onClick={() => { setEditingPolicy(null); setEditPolicyDescHtml(''); }} className="px-2 py-1 bg-slate-200 text-slate-600 rounded text-xs">Cancel</button>
</div>
</div>
) : (
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-medium text-slate-700">{policy.title}</span>
{(policy as { showApp?: boolean; showWeb?: boolean }).showApp && (
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-blue-100 text-blue-600 rounded">App</span>
)}
{(policy as { showApp?: boolean; showWeb?: boolean }).showWeb && (
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-green-100 text-green-600 rounded">Web</span>
)}
</div>
<div className="text-xs text-slate-500 prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: policy.description }} />
</div>
<div className="flex items-center gap-1 ml-2">
<button onClick={() => { setEditingPolicy({ tab: 'investor', index: i }); setEditPolicyName(policy.title); setEditPolicyDescHtml(policy.description); setEditPolicyShowApp((policy as { showApp?: boolean }).showApp ?? true); setEditPolicyShowWeb((policy as { showWeb?: boolean }).showWeb ?? true); }} className="p-1 text-slate-400 hover:text-blue-600">
<Pencil className="w-3.5 h-3.5" />
</button>
<button onClick={() => {
const updated = settings.companyPolicy.investor.filter((_, idx) => idx !== i);
setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, investor: updated } });
}} className="p-1 text-slate-400 hover:text-red-600">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
)}
</div>
))}
</div>
</div>
)}
{activeMasterTab === 'merchant' && (
<div className="space-y-4">
<div className="flex items-center justify-between mb-2">
<label className="text-sm text-slate-600 font-medium">Policy List</label>
<button onClick={() => setShowAddPolicy(true)} className="text-xs text-blue-600 hover:text-blue-700 flex items-center gap-1">
<Plus className="w-3 h-3" /> Add Policy
</button>
</div>
{showAddPolicy && (
<div className="p-3 bg-white rounded-lg border border-blue-200 space-y-3">
<input type="text" value={newPolicyName} onChange={(e) => setNewPolicyName(e.target.value)} className="w-full px-2 py-1.5 border border-slate-200 rounded text-sm" placeholder="Policy Title" />
<RichTextEditor value={newPolicyDesc} onChange={(val) => setNewPolicyDesc(val)} placeholder="Policy Description..." minHeight={100} />
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<button type="button" onClick={() => setNewPolicyShowApp(!newPolicyShowApp)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${newPolicyShowApp ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${newPolicyShowApp ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on App</span>
</div>
<div className="flex items-center gap-2">
<button type="button" onClick={() => setNewPolicyShowWeb(!newPolicyShowWeb)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${newPolicyShowWeb ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${newPolicyShowWeb ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on Web</span>
</div>
</div>
<div className="flex gap-2">
<button onClick={() => {
if (!newPolicyName.trim()) return;
const newPolicy = { title: newPolicyName, description: newPolicyDesc, showApp: newPolicyShowApp, showWeb: newPolicyShowWeb };
setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, merchant: [...settings.companyPolicy.merchant, newPolicy as typeof settings.companyPolicy.merchant[number]] } });
setNewPolicyName('');
setNewPolicyDesc('');
setNewPolicyShowApp(true);
setNewPolicyShowWeb(true);
setShowAddPolicy(false);
}} className="px-2 py-1 bg-blue-600 text-white rounded text-xs">Add</button>
<button onClick={() => { setShowAddPolicy(false); setNewPolicyName(''); setNewPolicyDesc(''); setNewPolicyShowApp(true); setNewPolicyShowWeb(true); }} className="px-2 py-1 bg-slate-200 text-slate-600 rounded text-xs">Cancel</button>
</div>
</div>
)}
<div className="space-y-2">
{(settings.companyPolicy?.merchant || []).map((policy, i) => (
<div key={i} className="p-3 bg-slate-50 rounded-lg border border-slate-200">
{editingPolicy?.tab === 'merchant' && editingPolicy?.index === i ? (
<div className="space-y-2">
<input type="text" value={editPolicyName} onChange={(e) => setEditPolicyName(e.target.value)} className="w-full px-2 py-1.5 border border-slate-200 rounded text-sm" placeholder="Policy Title" />
<RichTextEditor value={editPolicyDescHtml} onChange={(val) => setEditPolicyDescHtml(val)} placeholder="Policy Description..." minHeight={100} />
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<button type="button" onClick={() => setEditPolicyShowApp(!editPolicyShowApp)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${editPolicyShowApp ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${editPolicyShowApp ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on App</span>
</div>
<div className="flex items-center gap-2">
<button type="button" onClick={() => setEditPolicyShowWeb(!editPolicyShowWeb)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${editPolicyShowWeb ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${editPolicyShowWeb ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on Web</span>
</div>
</div>
<div className="flex gap-2">
<button onClick={() => {
const updated = [...settings.companyPolicy.merchant];
updated[i] = { title: editPolicyName, description: editPolicyDescHtml, showApp: editPolicyShowApp, showWeb: editPolicyShowWeb };
setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, merchant: updated } });
setEditingPolicy(null);
setEditPolicyDescHtml('');
}} className="px-2 py-1 bg-blue-600 text-white rounded text-xs">Save</button>
<button onClick={() => { setEditingPolicy(null); setEditPolicyDescHtml(''); }} className="px-2 py-1 bg-slate-200 text-slate-600 rounded text-xs">Cancel</button>
</div>
</div>
) : (
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-medium text-slate-700">{policy.title}</span>
{(policy as { showApp?: boolean; showWeb?: boolean }).showApp && (
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-blue-100 text-blue-600 rounded">App</span>
)}
{(policy as { showApp?: boolean; showWeb?: boolean }).showWeb && (
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-green-100 text-green-600 rounded">Web</span>
)}
</div>
<div className="text-xs text-slate-500 prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: policy.description }} />
</div>
<div className="flex items-center gap-1 ml-2">
<button onClick={() => { setEditingPolicy({ tab: 'merchant', index: i }); setEditPolicyName(policy.title); setEditPolicyDescHtml(policy.description); setEditPolicyShowApp((policy as { showApp?: boolean }).showApp ?? true); setEditPolicyShowWeb((policy as { showWeb?: boolean }).showWeb ?? true); }} className="p-1 text-slate-400 hover:text-blue-600">
<Pencil className="w-3.5 h-3.5" />
</button>
<button onClick={() => {
const updated = settings.companyPolicy.merchant.filter((_, idx) => idx !== i);
setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, merchant: updated } });
}} className="p-1 text-slate-400 hover:text-red-600">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
)}
</div>
))}
</div>
</div>
)}
{activeMasterTab === 'swapstation' && (
<div className="space-y-4">
<div className="flex items-center justify-between mb-2">
<label className="text-sm text-slate-600 font-medium">Policy List</label>
<button onClick={() => setShowAddPolicy(true)} className="text-xs text-blue-600 hover:text-blue-700 flex items-center gap-1">
<Plus className="w-3 h-3" /> Add Policy
</button>
</div>
{showAddPolicy && (
<div className="p-3 bg-white rounded-lg border border-blue-200 space-y-3">
<input type="text" value={newPolicyName} onChange={(e) => setNewPolicyName(e.target.value)} className="w-full px-2 py-1.5 border border-slate-200 rounded text-sm" placeholder="Policy Title" />
<RichTextEditor value={newPolicyDesc} onChange={(val) => setNewPolicyDesc(val)} placeholder="Policy Description..." minHeight={100} />
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<button type="button" onClick={() => setNewPolicyShowApp(!newPolicyShowApp)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${newPolicyShowApp ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${newPolicyShowApp ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on App</span>
</div>
<div className="flex items-center gap-2">
<button type="button" onClick={() => setNewPolicyShowWeb(!newPolicyShowWeb)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${newPolicyShowWeb ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${newPolicyShowWeb ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on Web</span>
</div>
</div>
<div className="flex gap-2">
<button onClick={() => {
if (!newPolicyName.trim()) return;
const newPolicy = { title: newPolicyName, description: newPolicyDesc, showApp: newPolicyShowApp, showWeb: newPolicyShowWeb };
setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, swapStation: [...settings.companyPolicy.swapStation, newPolicy as typeof settings.companyPolicy.swapStation[number]] } });
setNewPolicyName('');
setNewPolicyDesc('');
setNewPolicyShowApp(true);
setNewPolicyShowWeb(true);
setShowAddPolicy(false);
}} className="px-2 py-1 bg-blue-600 text-white rounded text-xs">Add</button>
<button onClick={() => { setShowAddPolicy(false); setNewPolicyName(''); setNewPolicyDesc(''); setNewPolicyShowApp(true); setNewPolicyShowWeb(true); }} className="px-2 py-1 bg-slate-200 text-slate-600 rounded text-xs">Cancel</button>
</div>
</div>
)}
<div className="space-y-2">
{(settings.companyPolicy?.swapStation || []).map((policy, i) => (
<div key={i} className="p-3 bg-slate-50 rounded-lg border border-slate-200">
{editingPolicy?.tab === 'swapstation' && editingPolicy?.index === i ? (
<div className="space-y-2">
<input type="text" value={editPolicyName} onChange={(e) => setEditPolicyName(e.target.value)} className="w-full px-2 py-1.5 border border-slate-200 rounded text-sm" placeholder="Policy Title" />
<RichTextEditor value={editPolicyDescHtml} onChange={(val) => setEditPolicyDescHtml(val)} placeholder="Policy Description..." minHeight={100} />
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<button type="button" onClick={() => setEditPolicyShowApp(!editPolicyShowApp)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${editPolicyShowApp ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${editPolicyShowApp ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on App</span>
</div>
<div className="flex items-center gap-2">
<button type="button" onClick={() => setEditPolicyShowWeb(!editPolicyShowWeb)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${editPolicyShowWeb ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${editPolicyShowWeb ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on Web</span>
</div>
</div>
<div className="flex gap-2">
<button onClick={() => {
const updated = [...settings.companyPolicy.swapStation];
updated[i] = { title: editPolicyName, description: editPolicyDescHtml, showApp: editPolicyShowApp, showWeb: editPolicyShowWeb };
setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, swapStation: updated } });
setEditingPolicy(null);
setEditPolicyDescHtml('');
}} className="px-2 py-1 bg-blue-600 text-white rounded text-xs">Save</button>
<button onClick={() => { setEditingPolicy(null); setEditPolicyDescHtml(''); }} className="px-2 py-1 bg-slate-200 text-slate-600 rounded text-xs">Cancel</button>
</div>
</div>
) : (
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-medium text-slate-700">{policy.title}</span>
{(policy as { showApp?: boolean; showWeb?: boolean }).showApp && (
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-blue-100 text-blue-600 rounded">App</span>
)}
{(policy as { showApp?: boolean; showWeb?: boolean }).showWeb && (
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-green-100 text-green-600 rounded">Web</span>
)}
</div>
<div className="text-xs text-slate-500 prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: policy.description }} />
</div>
<div className="flex items-center gap-1 ml-2">
<button onClick={() => { setEditingPolicy({ tab: 'swapstation', index: i }); setEditPolicyName(policy.title); setEditPolicyDescHtml(policy.description); setEditPolicyShowApp((policy as { showApp?: boolean }).showApp ?? true); setEditPolicyShowWeb((policy as { showWeb?: boolean }).showWeb ?? true); }} className="p-1 text-slate-400 hover:text-blue-600">
<Pencil className="w-3.5 h-3.5" />
</button>
<button onClick={() => {
const updated = settings.companyPolicy.swapStation.filter((_, idx) => idx !== i);
setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, swapStation: updated } });
}} className="p-1 text-slate-400 hover:text-red-600">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
)}
</div>
))}
</div>
</div>
)}
{activeMasterTab === 'rentalType' && (
<div className="space-y-4">
<div className="flex gap-2 border-b border-slate-200">
<button onClick={() => setActiveRentalTypeTab('single')} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activeRentalTypeTab === 'single' ? 'border-blue-500 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>Rental (Single)</button>
<button onClick={() => setActiveRentalTypeTab('shared')} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activeRentalTypeTab === 'shared' ? 'border-blue-500 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>Rental (2 Person Shared)</button>
<button onClick={() => setActiveRentalTypeTab('renttoown')} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activeRentalTypeTab === 'renttoown' ? 'border-blue-500 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>Rent-to-Own</button>
</div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm text-slate-600 font-medium">Policy List</label>
<button onClick={() => setShowAddPolicy(true)} className="text-xs text-blue-600 hover:text-blue-700 flex items-center gap-1">
<Plus className="w-3 h-3" /> Add Policy
</button>
</div>
{showAddPolicy && (
<div className="p-3 bg-white rounded-lg border border-blue-200 space-y-3">
<input type="text" value={newPolicyName} onChange={(e) => setNewPolicyName(e.target.value)} className="w-full px-2 py-1.5 border border-slate-200 rounded text-sm" placeholder="Policy Title" />
<RichTextEditor value={newPolicyDesc} onChange={(val) => setNewPolicyDesc(val)} placeholder="Policy Description..." minHeight={100} />
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<button type="button" onClick={() => setNewPolicyShowApp(!newPolicyShowApp)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${newPolicyShowApp ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${newPolicyShowApp ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on App</span>
</div>
<div className="flex items-center gap-2">
<button type="button" onClick={() => setNewPolicyShowWeb(!newPolicyShowWeb)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${newPolicyShowWeb ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${newPolicyShowWeb ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on Web</span>
</div>
</div>
<div className="flex gap-2">
<button onClick={() => {
if (!newPolicyName.trim()) return;
const updated = { ...settings.companyPolicy.rentalTypes };
updated[activeRentalTypeTab] = [...updated[activeRentalTypeTab], { title: newPolicyName, description: newPolicyDesc, showApp: newPolicyShowApp, showWeb: newPolicyShowWeb }];
setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, rentalTypes: updated } });
setNewPolicyName('');
setNewPolicyDesc('');
setNewPolicyShowApp(true);
setNewPolicyShowWeb(true);
setShowAddPolicy(false);
}} className="px-2 py-1 bg-blue-600 text-white rounded text-xs">Add</button>
<button onClick={() => { setShowAddPolicy(false); setNewPolicyName(''); setNewPolicyDesc(''); setNewPolicyShowApp(true); setNewPolicyShowWeb(true); }} className="px-2 py-1 bg-slate-200 text-slate-600 rounded text-xs">Cancel</button>
</div>
</div>
)}
<div className="space-y-2">
{(settings.companyPolicy?.rentalTypes[activeRentalTypeTab] || []).map((policy, i) => (
<div key={i} className="p-3 bg-slate-50 rounded-lg border border-slate-200">
{editingPolicy?.tab === 'rentalType' && editingPolicy?.index === i ? (
<div className="space-y-2">
<input type="text" value={editPolicyName} onChange={(e) => setEditPolicyName(e.target.value)} className="w-full px-2 py-1.5 border border-slate-200 rounded text-sm" placeholder="Policy Title" />
<RichTextEditor value={editPolicyDescHtml} onChange={(val) => setEditPolicyDescHtml(val)} placeholder="Policy Description..." minHeight={100} />
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<button type="button" onClick={() => setEditPolicyShowApp(!editPolicyShowApp)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${editPolicyShowApp ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${editPolicyShowApp ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on App</span>
</div>
<div className="flex items-center gap-2">
<button type="button" onClick={() => setEditPolicyShowWeb(!editPolicyShowWeb)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${editPolicyShowWeb ? 'bg-blue-600' : 'bg-slate-300'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${editPolicyShowWeb ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
<span className="text-xs font-medium text-slate-600">Show on Web</span>
</div>
</div>
<div className="flex gap-2">
<button onClick={() => {
const updated = { ...settings.companyPolicy.rentalTypes };
updated[activeRentalTypeTab][i] = { title: editPolicyName, description: editPolicyDescHtml, showApp: editPolicyShowApp, showWeb: editPolicyShowWeb };
setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, rentalTypes: updated } });
setEditingPolicy(null);
setEditPolicyDescHtml('');
}} className="px-2 py-1 bg-blue-600 text-white rounded text-xs">Save</button>
<button onClick={() => { setEditingPolicy(null); setEditPolicyDescHtml(''); }} className="px-2 py-1 bg-slate-200 text-slate-600 rounded text-xs">Cancel</button>
</div>
</div>
) : (
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-medium text-slate-700">{policy.title}</span>
{(policy as { showApp?: boolean; showWeb?: boolean }).showApp && (
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-blue-100 text-blue-600 rounded">App</span>
)}
{(policy as { showApp?: boolean; showWeb?: boolean }).showWeb && (
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-green-100 text-green-600 rounded">Web</span>
)}
</div>
<div className="text-xs text-slate-500 prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: policy.description }} />
</div>
<div className="flex items-center gap-1 ml-2">
<button onClick={() => { setEditingPolicy({ tab: 'rentalType', index: i }); setEditPolicyName(policy.title); setEditPolicyDescHtml(policy.description); setEditPolicyShowApp((policy as { showApp?: boolean }).showApp ?? true); setEditPolicyShowWeb((policy as { showWeb?: boolean }).showWeb ?? true); }} className="p-1 text-slate-400 hover:text-blue-600">
<Pencil className="w-3.5 h-3.5" />
</button>
<button onClick={() => {
const updated = { ...settings.companyPolicy.rentalTypes };
updated[activeRentalTypeTab] = updated[activeRentalTypeTab].filter((_, idx) => idx !== i);
setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, rentalTypes: updated } });
}} className="p-1 text-slate-400 hover:text-red-600">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,673 @@
'use client';
import { useState } from 'react';
import { Mail, MessageSquare, Pencil, Eye, X, Check, Plus } from 'lucide-react';
import toast from 'react-hot-toast';
interface Template {
id: string;
name: string;
subject?: string;
body: string;
enabled: boolean;
}
interface EmailSMSTemplatesProps {
settings: any;
setSettings: any;
}
const defaultEmailTemplates: Template[] = [
{
id: 'welcome',
name: 'Welcome Email',
subject: 'Welcome to JAIBEN Mobility - Your Journey Starts Here!',
body: `Dear {name},
Welcome to JAIBEN Mobility!
We're thrilled to have you join our community of eco-friendly commuters. Your account has been successfully created.
Your Login Details:
- Phone: {phone}
- Email: {email}
What's Next?
1. Complete your KYC verification to unlock all features
2. Browse our EV rental plans
3. Choose your perfect electric vehicle
If you have any questions, our support team is here to help!
Best regards,
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'password_reset',
name: 'Password Reset',
subject: 'Reset Your JAIBEN Password',
body: `Dear {name},
We received a request to reset your password.
Your OTP code is: {otp}
This code will expire in {expiry_minutes} minutes.
If you didn't request this, please ignore this email or contact support immediately.
Note: Never share this OTP with anyone.
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'rental_confirmation',
name: 'Rental Confirmation',
subject: 'Rental Confirmed - Your EV is Ready!',
body: `Dear {name},
Your rental has been confirmed!
Booking Details:
- Bike: {bike}
- Plan: {plan}
- Start Date: {start_date}
- End Date: {end_date}
- Amount: {amount}
- Deposit: {deposit}
Pickup Location: {pickup_location}
Important:
- Bring your valid driving license
- Carry NID card for verification
- Arrive 15 minutes before scheduled time
Enjoy your ride!
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'payment_reminder',
name: 'Payment Reminder',
subject: 'Payment Due Soon - {amount}',
body: `Dear {name},
This is a friendly reminder that your payment is due.
Amount Due: {amount}
Due Date: {due_date}
Rental: {bike}
Please make your payment to avoid late fees.
Payment Methods:
- bKash: {bkash_number}
- Nagad: {nagad_number}
- Bank Transfer: {bank_account}
Need help? Contact us anytime!
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'due_notice',
name: 'Due Notice',
subject: 'Payment Overdue - Action Required',
body: `Dear {name},
Your payment for the rental of {bike} is overdue.
Overdue Amount: {amount}
Days Overdue: {days_overdue}
Late Fee: {late_fee}
Please make immediate payment to avoid service interruption.
Total Due: {total_due}
If you've already made the payment, please ignore this message or contact us to confirm.
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'kyc_verification',
name: 'KYC Verification',
subject: 'KYC Verification {status}',
body: `Dear {name},
Your KYC verification has been {status}.
{status_message}
{status === 'approved' ? 'You can now access all features and rent EVs!' : 'Please resubmit your documents with correct information.'}
Documents Submitted:
{documents_list}
If you have questions, contact our support team.
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'rental_termination',
name: 'Rental Termination',
subject: 'Rental Agreement Terminated',
body: `Dear {name},
Your rental agreement for {bike} has been terminated.
Termination Details:
- End Date: {end_date}
- Final Amount: {final_amount}
- Refund: {refund_amount}
Please return the vehicle to {return_location} by {return_date}.
Any outstanding charges will be deducted from your deposit.
Thank you for choosing JAIBEN!
JAIBEN Mobility Team`,
enabled: true,
},
{
id: 'damage_report',
name: 'Damage Report',
subject: 'Vehicle Damage Report - {bike}',
body: `Dear {name},
A damage report has been filed for your rented vehicle.
Incident Details:
- Date: {incident_date}
- Location: {location}
- Description: {description}
- Estimated Cost: {repair_cost}
Your current deposit: {deposit}
Remaining after repair: {remaining_deposit}
Please contact us within 24 hours if you have any concerns.
JAIBEN Mobility Team`,
enabled: true,
},
];
const defaultSmsTemplates: Template[] = [
{
id: 'otp',
name: 'OTP Code',
body: `JAIBEN: Your OTP is {otp}. Valid for {expiry_minutes} mins. Don't share this code.`,
enabled: true,
},
{
id: 'rental_reminder',
name: 'Rental Reminder',
body: `JAIBEN: Reminder! Your {bike} rental starts on {start_date}. Pick up from {location}. Reply CONFIRM to proceed.`,
enabled: true,
},
{
id: 'payment_due',
name: 'Payment Due',
body: `JAIBEN: Payment of {amount} due on {due_date} for your {bike} rental. Pay via bKash: {bkash}. Avoid late fees!`,
enabled: true,
},
{
id: 'battery_low',
name: 'Battery Low Warning',
body: `JAIBEN: Your {bike} battery is low ({battery_level}%). Visit nearest swap station or charge point. Stay safe!`,
enabled: true,
},
{
id: 'damage_report_sms',
name: 'Damage Report',
body: `JAIBEN: Damage reported on your {bike}. Est. cost: {repair_cost}. Contact {support_phone} within 24hrs.`,
enabled: true,
},
{
id: 'welcome_sms',
name: 'Welcome Message',
body: `Welcome to JAIBEN Mobility {name}! Your account is ready. Download our app or visit {app_link} to rent your first EV!`,
enabled: true,
},
{
id: 'rental_start',
name: 'Rental Started',
body: `JAIBEN: Your {bike} rental has started! Enjoy your ride. Return by {return_time}. Ride safe!`,
enabled: true,
},
{
id: 'rental_end',
name: 'Rental Ending',
body: `JAIBEN: Your {bike} rental ends on {end_date}. Extend at {app_link} or return to {location}. Thanks!`,
enabled: true,
},
{
id: 'swap_reminder',
name: 'Battery Swap Reminder',
body: `JAIBEN: Your battery at {location} is low. Swap station: {station_name}, Distance: {distance}km. Free swap with your plan!`,
enabled: true,
},
{
id: 'kyc_approved',
name: 'KYC Approved',
body: `JAIBEN: Great news! Your KYC is verified. You can now rent EVs. Download app: {app_link} or visit nearest hub!`,
enabled: true,
},
];
const commonVariables = [
{ name: 'name', label: 'Name' },
{ name: 'phone', label: 'Phone' },
{ name: 'email', label: 'Email' },
{ name: 'amount', label: 'Amount' },
{ name: 'date', label: 'Date' },
{ name: 'bike', label: 'Bike Name' },
{ name: 'plan', label: 'Plan' },
{ name: 'otp', label: 'OTP' },
{ name: 'deposit', label: 'Deposit' },
{ name: 'start_date', label: 'Start Date' },
{ name: 'end_date', label: 'End Date' },
{ name: 'location', label: 'Location' },
];
export default function EmailSMSTemplates({ settings, setSettings }: EmailSMSTemplatesProps) {
const [activeTemplateTab, setActiveTemplateTab] = useState<'email' | 'sms'>('email');
const [emailTemplates, setEmailTemplates] = useState<Template[]>(
settings.emailTemplates || defaultEmailTemplates
);
const [smsTemplates, setSmsTemplates] = useState<Template[]>(
settings.smsTemplates || defaultSmsTemplates
);
const [showModal, setShowModal] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<Template | null>(null);
const [editSubject, setEditSubject] = useState('');
const [editBody, setEditBody] = useState('');
const [previewMode, setPreviewMode] = useState(false);
const handleToggleEnabled = (type: 'email' | 'sms', id: string) => {
if (type === 'email') {
const updated = emailTemplates.map(t =>
t.id === id ? { ...t, enabled: !t.enabled } : t
);
setEmailTemplates(updated);
setSettings({ ...settings, emailTemplates: updated });
} else {
const updated = smsTemplates.map(t =>
t.id === id ? { ...t, enabled: !t.enabled } : t
);
setSmsTemplates(updated);
setSettings({ ...settings, smsTemplates: updated });
}
toast.success('Template status updated');
};
const handleEdit = (template: Template, isEmail: boolean) => {
setEditingTemplate(template);
setEditSubject(template.subject || '');
setEditBody(template.body);
setPreviewMode(false);
setShowModal(true);
};
const handleSave = () => {
if (!editingTemplate) return;
const updatedTemplate = {
...editingTemplate,
subject: editSubject,
body: editBody,
};
const newId = updatedTemplate.id || `template_${Date.now()}`;
const templateWithId = { ...updatedTemplate, id: newId };
if (activeTemplateTab === 'email') {
if (emailTemplates.find(t => t.id === editingTemplate.id)) {
const updated = emailTemplates.map(t =>
t.id === editingTemplate.id ? templateWithId : t
);
setEmailTemplates(updated);
setSettings({ ...settings, emailTemplates: updated });
} else {
const updated = [...emailTemplates, templateWithId];
setEmailTemplates(updated);
setSettings({ ...settings, emailTemplates: updated });
}
} else {
if (smsTemplates.find(t => t.id === editingTemplate.id)) {
const updated = smsTemplates.map(t =>
t.id === editingTemplate.id ? templateWithId : t
);
setSmsTemplates(updated);
setSettings({ ...settings, smsTemplates: updated });
} else {
const updated = [...smsTemplates, templateWithId];
setSmsTemplates(updated);
setSettings({ ...settings, smsTemplates: updated });
}
}
setShowModal(false);
toast.success('Template saved successfully');
};
const insertVariable = (varName: string) => {
setEditBody(prev => prev + `{${varName}}`);
};
const previewContent = (body: string) => {
let preview = body
.replace(/{name}/g, 'John Doe')
.replace(/{phone}/g, '+880 1234 567890')
.replace(/{email}/g, 'john@example.com')
.replace(/{amount}/g, '৳12,000')
.replace(/{date}/g, '15 May 2026')
.replace(/{bike}/g, 'EVO Lite')
.replace(/{plan}/g, 'Monthly Premium')
.replace(/{otp}/g, '123456')
.replace(/{deposit}/g, '৳5,000')
.replace(/{start_date}/g, '01 June 2026')
.replace(/{end_date}/g, '30 June 2026')
.replace(/{location}/g, 'Gulshan Hub')
.replace(/{bkash}/g, '01712345678')
.replace(/{nagad}/g, '01712345679')
.replace(/{expiry_minutes}/g, '10')
.replace(/{due_date}/g, '25 May 2026')
.replace(/{days_overdue}/g, '3')
.replace(/{late_fee}/g, '৳600')
.replace(/{total_due}/g, '৳12,600')
.replace(/{status}/g, 'Approved')
.replace(/{status_message}/g, 'Your documents have been verified successfully.')
.replace(/{documents_list}/g, '- NID Card\n- Driving License\n- Photo')
.replace(/{final_amount}/g, '৳10,000')
.replace(/{refund_amount}/g, '৳3,000')
.replace(/{return_date}/g, '20 May 2026')
.replace(/{incident_date}/g, '10 May 2026')
.replace(/{description}/g, 'Front panel scratch')
.replace(/{repair_cost}/g, '৳2,500')
.replace(/{remaining_deposit}/g, '৳2,500')
.replace(/{battery_level}/g, '15%')
.replace(/{return_time}/g, '8:00 PM')
.replace(/{app_link}/g, 'jaiben.com/app')
.replace(/{support_phone}/g, '+880 9611 222 333')
.replace(/{station_name}/g, 'Gulshan Swap Station')
.replace(/{distance}/g, '1.2')
.replace(/{bank_account}/g, 'AC: 1234567890, Bank: City Bank');
return preview;
};
return (
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-slate-800">Email & SMS Templates</h2>
<button
onClick={() => {
setActiveTemplateTab(activeTemplateTab);
setEditingTemplate({ id: '', name: '', subject: '', body: '', enabled: true });
setEditSubject('');
setEditBody('');
setPreviewMode(false);
setShowModal(true);
}}
className="flex items-center gap-2 px-4 py-2 bg-accent text-white rounded-lg text-sm font-medium hover:bg-accent/90 transition-colors"
>
<Plus className="w-4 h-4" />
Add Template
</button>
</div>
<div className="flex gap-2 mb-6 p-1 bg-slate-100 rounded-lg w-fit">
<button
onClick={() => setActiveTemplateTab('email')}
className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${activeTemplateTab === 'email'
? 'bg-white text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-accent'
}`}
>
<Mail className="w-4 h-4" />
Email Templates
</button>
<button
onClick={() => setActiveTemplateTab('sms')}
className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${activeTemplateTab === 'sms'
? 'bg-white text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-accent'
}`}
>
<MessageSquare className="w-4 h-4" />
SMS Templates
</button>
</div>
<div className="space-y-3">
{activeTemplateTab === 'email' ? (
emailTemplates.map(template => (
<div
key={template.id}
className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-200"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3">
<h3 className="font-semibold text-slate-800">{template.name}</h3>
<span
className={`px-2 py-0.5 text-xs rounded-full ${template.enabled
? 'bg-green-100 text-green-700'
: 'bg-slate-200 text-slate-500'
}`}
>
{template.enabled ? 'Active' : 'Inactive'}
</span>
</div>
<p className="text-sm text-slate-500 mt-1 truncate">{template.subject || template.body.substring(0, 60)}...</p>
</div>
<div className="flex items-center gap-2 ml-4">
<button
onClick={() => handleToggleEnabled('email', template.id)}
className={`relative w-11 h-6 rounded-full transition-colors ${template.enabled ? 'bg-accent' : 'bg-slate-300'
}`}
>
<span
className={`absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow transition-transform ${template.enabled ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
<button
onClick={() => { setActiveTemplateTab('email'); handleEdit(template, true); }}
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded-lg transition-colors"
>
<Pencil className="w-4 h-4" />
</button>
<button
onClick={() => { setActiveTemplateTab('email'); setEditingTemplate(template); setPreviewMode(true); setShowModal(true); }}
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded-lg transition-colors"
>
<Eye className="w-4 h-4" />
</button>
</div>
</div>
))
) : (
smsTemplates.map(template => (
<div
key={template.id}
className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-200"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3">
<h3 className="font-semibold text-slate-800">{template.name}</h3>
<span
className={`px-2 py-0.5 text-xs rounded-full ${template.enabled
? 'bg-green-100 text-green-700'
: 'bg-slate-200 text-slate-500'
}`}
>
{template.enabled ? 'Active' : 'Inactive'}
</span>
</div>
<p className="text-sm text-slate-500 mt-1 truncate">{template.body.substring(0, 60)}...</p>
</div>
<div className="flex items-center gap-2 ml-4">
<button
onClick={() => handleToggleEnabled('sms', template.id)}
className={`relative w-11 h-6 rounded-full transition-colors ${template.enabled ? 'bg-accent' : 'bg-slate-300'
}`}
>
<span
className={`absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow transition-transform ${template.enabled ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
<button
onClick={() => { setActiveTemplateTab('sms'); handleEdit(template, false); }}
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded-lg transition-colors"
>
<Pencil className="w-4 h-4" />
</button>
<button
onClick={() => { setActiveTemplateTab('sms'); setEditingTemplate(template); setPreviewMode(true); setShowModal(true); }}
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded-lg transition-colors"
>
<Eye className="w-4 h-4" />
</button>
</div>
</div>
))
)}
</div>
{showModal && editingTemplate && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden">
<div className="p-4 border-b border-slate-200 flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="text-lg font-bold text-slate-800">
{previewMode ? 'Preview' : editingTemplate.id ? 'Edit' : 'Add'} {editingTemplate.id ? '- ' + editingTemplate.name : 'Template'}
</h3>
{previewMode && (
<button
onClick={() => setPreviewMode(false)}
className="ml-2 px-2 py-1 text-xs bg-accent text-white rounded hover:bg-accent/90"
>
Edit
</button>
)}
</div>
<button
onClick={() => setShowModal(false)}
className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 max-h-[calc(90vh-180px)] overflow-y-auto">
{previewMode ? (
<div className="bg-slate-50 p-4 rounded-lg border border-slate-200">
<div className="flex items-center gap-2 mb-3 text-slate-500 text-sm">
<Mail className="w-4 h-4" />
<span>Subject: {editingTemplate.subject || 'N/A'}</span>
</div>
<pre className="whitespace-pre-wrap text-sm text-slate-700 font-sans">
{previewContent(editingTemplate.body)}
</pre>
</div>
) : (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Template Name <span className="text-red-500">*</span>
</label>
<input
type="text"
value={editingTemplate.name}
onChange={e => setEditingTemplate({ ...editingTemplate, name: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent"
placeholder="e.g. Welcome Email"
/>
</div>
{activeTemplateTab === 'email' && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Subject
</label>
<input
type="text"
value={editSubject}
onChange={e => setEditSubject(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent"
placeholder="Enter email subject..."
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Body {activeTemplateTab === 'sms' && '(SMS)'}
</label>
<textarea
value={editBody}
onChange={e => setEditBody(e.target.value)}
rows={12}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent font-mono"
placeholder="Enter template body..."
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Insert Variable
</label>
<div className="flex flex-wrap gap-2">
{commonVariables.map(v => (
<button
key={v.name}
onClick={() => insertVariable(v.name)}
className="px-3 py-1.5 bg-slate-100 text-slate-600 text-sm rounded-lg hover:bg-slate-200 transition-colors"
>
{`{${v.name}}`}
</button>
))}
</div>
</div>
<div className="p-3 bg-blue-50 rounded-lg">
<p className="text-xs text-blue-700">
<strong>Tip:</strong> Variables like {'{name}'}, {'{phone}'}, {'{amount}'} will be replaced with actual values when sending.
</p>
</div>
</div>
)}
</div>
<div className="p-4 border-t border-slate-200 bg-slate-50 flex justify-end gap-3">
<button
onClick={() => setShowModal(false)}
className="px-4 py-2 bg-white border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-100 transition-colors"
>
Cancel
</button>
{!previewMode && (
<button
onClick={handleSave}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm font-medium hover:bg-accent-dark transition-colors flex items-center gap-2"
>
<Check className="w-4 h-4" />
Save Template
</button>
)}
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,171 @@
'use client';
import { Upload } from 'lucide-react';
import { CompanySettings } from '../page';
interface GeneralSettingsProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
}
export default function GeneralSettings({ settings, setSettings }: GeneralSettingsProps) {
return (
<div className="p-6 space-y-6">
<h3 className="text-lg font-semibold text-slate-800">General Information</h3>
<div className="grid lg:grid-cols-2 gap-4">
<div>
<label className="text-sm text-slate-600">Company Name</label>
<input
type="text"
value={settings.name}
onChange={(e) => setSettings({ ...settings, name: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">Short Name</label>
<input
type="text"
value={settings.shortName}
onChange={(e) => setSettings({ ...settings, shortName: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">Tagline</label>
<input
type="text"
value={settings.tagLine}
onChange={(e) => setSettings({ ...settings, tagLine: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">Website</label>
<input
type="text"
value={settings.website}
onChange={(e) => setSettings({ ...settings, website: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Description</label>
<textarea
value={settings.description}
onChange={(e) => setSettings({ ...settings, description: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
rows={3}
/>
</div>
<div className="grid lg:grid-cols-2 gap-4">
<div>
<label className="text-sm text-slate-600">Email</label>
<input
type="email"
value={settings.email}
onChange={(e) => setSettings({ ...settings, email: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">Phone</label>
<input
type="text"
value={settings.phone}
onChange={(e) => setSettings({ ...settings, phone: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Address</label>
<textarea
value={settings.address}
onChange={(e) => setSettings({ ...settings, address: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
rows={2}
/>
</div>
<div className="grid lg:grid-cols-3 gap-4">
<div>
<label className="text-sm text-slate-600">City</label>
<input
type="text"
value={settings.city}
onChange={(e) => setSettings({ ...settings, city: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">Country</label>
<input
type="text"
value={settings.country}
onChange={(e) => setSettings({ ...settings, country: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">Timezone</label>
<select
value={settings.timezone}
onChange={(e) => setSettings({ ...settings, timezone: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
>
<option value="Asia/Dhaka">Asia/Dhaka (GMT+6)</option>
<option value="Asia/Kolkata">Asia/Kolkata (GMT+5:30)</option>
<option value="UTC">UTC (GMT+0)</option>
</select>
</div>
</div>
<h3 className="text-lg font-semibold text-slate-800 pt-4 border-t">Business Registration</h3>
<div className="grid lg:grid-cols-3 gap-4">
<div>
<label className="text-sm text-slate-600">TIN Number</label>
<input
type="text"
value={settings.tin}
onChange={(e) => setSettings({ ...settings, tin: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">BIN Number</label>
<input
type="text"
value={settings.bin}
onChange={(e) => setSettings({ ...settings, bin: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">Trade License</label>
<input
type="text"
value={settings.tradeLicense}
onChange={(e) => setSettings({ ...settings, tradeLicense: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Documents</label>
<div className="mt-2 p-4 border-2 border-dashed border-slate-200 rounded-lg text-center hover:border-accent cursor-pointer">
<Upload className="w-8 h-8 mx-auto text-slate-400" />
<p className="text-sm text-slate-500 mt-1">Drop files here or click to upload</p>
<p className="text-xs text-slate-400">PDF, JPG, PNG up to 10MB</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,135 @@
'use client';
import { CompanySettings } from '../page';
interface IntegrationSettingsProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
}
export default function IntegrationSettings({ settings, setSettings }: IntegrationSettingsProps) {
return (
<div className="p-6 space-y-6">
<h3 className="text-lg font-semibold text-slate-800">Email Integration (SMTP)</h3>
<div className="grid lg:grid-cols-2 gap-4">
<div>
<label className="text-sm text-slate-600">SMTP Host</label>
<input
type="text"
value={settings.smtp.host}
onChange={(e) => setSettings({ ...settings, smtp: { ...settings.smtp, host: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">SMTP Port</label>
<input
type="number"
value={settings.smtp.port}
onChange={(e) => setSettings({ ...settings, smtp: { ...settings.smtp, port: parseInt(e.target.value) } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">Username</label>
<input
type="text"
value={settings.smtp.user}
onChange={(e) => setSettings({ ...settings, smtp: { ...settings.smtp, user: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">Password</label>
<input
type="password"
value={settings.smtp.password}
onChange={(e) => setSettings({ ...settings, smtp: { ...settings.smtp, password: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">From Email</label>
<input
type="email"
value={settings.smtp.fromEmail}
onChange={(e) => setSettings({ ...settings, smtp: { ...settings.smtp, fromEmail: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">From Name</label>
<input
type="text"
value={settings.smtp.fromName}
onChange={(e) => setSettings({ ...settings, smtp: { ...settings.smtp, fromName: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Encryption</label>
<div className="flex gap-3 mt-2">
{(['ssl', 'tls', 'none'] as const).map(enc => (
<button
key={enc}
onClick={() => setSettings({ ...settings, smtp: { ...settings.smtp, encryption: enc } })}
className={`px-4 py-2 text-sm rounded-lg border ${settings.smtp.encryption === enc
? 'bg-accent text-white border-accent'
: 'border-slate-200 text-slate-600'
}`}
>
{enc.toUpperCase()}
</button>
))}
</div>
</div>
<h3 className="text-lg font-semibold text-slate-800 pt-4 border-t">SMS Integration</h3>
<div className="grid lg:grid-cols-2 gap-4">
<div>
<label className="text-sm text-slate-600">Provider</label>
<select
value={settings.sms.provider}
onChange={(e) => setSettings({ ...settings, sms: { ...settings.sms, provider: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
>
<option value="BulkSMS">BulkSMS BD</option>
<option value="Twilio">Twilio</option>
<option value="Msg91">Msg91</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Sender ID</label>
<input
type="text"
value={settings.sms.senderId}
onChange={(e) => setSettings({ ...settings, sms: { ...settings.sms, senderId: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div className="lg:col-span-2">
<label className="text-sm text-slate-600">API Key</label>
<input
type="password"
value={settings.sms.apiKey}
onChange={(e) => setSettings({ ...settings, sms: { ...settings.sms, apiKey: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div className="lg:col-span-2">
<label className="text-sm text-slate-600">API URL</label>
<input
type="text"
value={settings.sms.url}
onChange={(e) => setSettings({ ...settings, sms: { ...settings.sms, url: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,281 @@
'use client';
import { Plus, X, Save } from 'lucide-react';
import type { CompanySettings } from '../page';
interface InvestmentSettingsProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
activeInvestTab: number;
setActiveInvestTab: (n: number) => void;
addInvestPlan: boolean;
setAddInvestPlan: (v: boolean) => void;
newInvestName: string;
setNewInvestName: (v: string) => void;
newInvestStatus: string;
setNewInvestStatus: (v: string) => void;
newInvestTarget: number;
setNewInvestTarget: (n: number) => void;
newInvestStart: string;
setNewInvestStart: (v: string) => void;
newInvestEnd: string;
setNewInvestEnd: (v: string) => void;
newInvestMin: number;
setNewInvestMin: (n: number) => void;
newInvestMax: number;
setNewInvestMax: (n: number) => void;
newInvestDuration: number;
setNewInvestDuration: (n: number) => void;
newInvestLock: number;
setNewInvestLock: (n: number) => void;
newInvestPenalty: number;
setNewInvestPenalty: (n: number) => void;
newInvestFicoSingleRent: number;
setNewInvestFicoSingleRent: (n: number) => void;
newInvestFicoRentToOwn: number;
setNewInvestFicoRentToOwn: (n: number) => void;
newInvestFicoShareEv: number;
setNewInvestFicoShareEv: (n: number) => void;
newInvestDesc: string;
setNewInvestDesc: (v: string) => void;
newInvestEvBasePrice: number;
setNewInvestEvBasePrice: (n: number) => void;
newInvestMinQuantity: number;
setNewInvestMinQuantity: (n: number) => void;
createInvestPlan: () => void;
handleSave: () => void;
}
export default function InvestmentSettings({
settings, setSettings,
activeInvestTab, setActiveInvestTab,
addInvestPlan, setAddInvestPlan,
newInvestName, setNewInvestName,
newInvestStatus, setNewInvestStatus,
newInvestTarget, setNewInvestTarget,
newInvestStart, setNewInvestStart,
newInvestEnd, setNewInvestEnd,
newInvestMin, setNewInvestMin,
newInvestMax, setNewInvestMax,
newInvestDuration, setNewInvestDuration,
newInvestLock, setNewInvestLock,
newInvestPenalty, setNewInvestPenalty,
newInvestFicoSingleRent, setNewInvestFicoSingleRent,
newInvestFicoRentToOwn, setNewInvestFicoRentToOwn,
newInvestFicoShareEv, setNewInvestFicoShareEv,
newInvestDesc, setNewInvestDesc,
newInvestEvBasePrice, setNewInvestEvBasePrice,
newInvestMinQuantity, setNewInvestMinQuantity,
createInvestPlan, handleSave,
}: InvestmentSettingsProps) {
const calculatedMinInvestment = newInvestMinQuantity * newInvestEvBasePrice;
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-800">EV Investment Plans</h3>
</div>
<div className="flex items-center justify-between bg-amber-50 border border-amber-200 rounded-xl p-4">
<div>
<h4 className="font-semibold text-amber-800">EV Investment Plans ({settings.plans.investment.length})</h4>
<p className="text-sm text-amber-600">Manage EV Investment Plans for investors</p>
</div>
<button onClick={() => { setAddInvestPlan(true); setNewInvestName(''); }} className="px-4 py-2 bg-amber-600 text-white rounded-lg text-sm font-medium flex items-center gap-2">
<Plus className="w-4 h-4" /> New Plan
</button>
</div>
{addInvestPlan && (
<div className="bg-white rounded-xl border border-amber-300 overflow-hidden">
<div className="bg-amber-100 px-4 py-3 border-b border-amber-200 flex items-center justify-between">
<div>
<h4 className="font-semibold text-amber-800">New Investment Plan</h4>
<p className="text-sm text-amber-600 mt-1">Fill in the details below</p>
</div>
<button onClick={() => setAddInvestPlan(false)} className="text-amber-600 hover:text-amber-800">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4">
<div className="grid lg:grid-cols-3 gap-4">
<div>
<label className="text-sm text-slate-600">Plan Condition</label>
<input type="text" value={newInvestName} onChange={(e) => setNewInvestName(e.target.value)} placeholder="e.g., 1 Bike Plan" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Status</label>
<select value={newInvestStatus} onChange={(e) => setNewInvestStatus(e.target.value)} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1">
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="closed">Closed</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Target Amount ()</label>
<input type="number" value={newInvestTarget} onChange={(e) => setNewInvestTarget(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Start Date</label>
<input type="date" value={newInvestStart} onChange={(e) => setNewInvestStart(e.target.value)} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">End Date</label>
<input type="date" value={newInvestEnd} onChange={(e) => setNewInvestEnd(e.target.value)} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">EV Base Price ()</label>
<input type="number" value={newInvestEvBasePrice} onChange={(e) => setNewInvestEvBasePrice(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" placeholder="Single EV cost" />
</div>
<div>
<label className="text-sm text-slate-600">Minimum Quantity (Bikes)</label>
<input type="number" value={newInvestMinQuantity} onChange={(e) => setNewInvestMinQuantity(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" placeholder="Min bikes to invest" />
</div>
<div>
<label className="text-sm text-slate-600">Min Investment ()</label>
<div className="flex items-center gap-2 mt-1">
<input type="number" value={calculatedMinInvestment} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100" />
<span className="text-xs text-slate-500 whitespace-nowrap">= Qty × Base Price</span>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Max Investment ()</label>
<input type="number" value={newInvestMax} onChange={(e) => setNewInvestMax(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Duration (Months)</label>
<input type="number" value={newInvestDuration} onChange={(e) => setNewInvestDuration(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Lock-in Period (Months)</label>
<input type="number" value={newInvestLock} onChange={(e) => setNewInvestLock(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Early Exit Penalty (%)</label>
<input type="number" value={newInvestPenalty} onChange={(e) => setNewInvestPenalty(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
</div>
<div className="grid grid-cols-3 gap-4 mt-4">
<div>
<label className="text-sm text-slate-600">FICO Share (%) - Single Rent</label>
<input type="number" value={newInvestFicoSingleRent} onChange={(e) => setNewInvestFicoSingleRent(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">FICO Share (%) - Rent to Own</label>
<input type="number" value={newInvestFicoRentToOwn} onChange={(e) => setNewInvestFicoRentToOwn(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">FICO Share (%) - Share an EV</label>
<input type="number" value={newInvestFicoShareEv} onChange={(e) => setNewInvestFicoShareEv(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
</div>
<div className="mt-4">
<label className="text-sm text-slate-600">Description</label>
<textarea value={newInvestDesc} onChange={(e) => setNewInvestDesc(e.target.value)} placeholder="Enter plan description" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" rows={2} />
</div>
<button onClick={createInvestPlan} className="mt-4 px-4 py-2 bg-amber-600 text-white rounded-lg text-sm font-medium">Create Plan</button>
</div>
</div>
)}
<div className="flex gap-2 border-b border-slate-200">
{settings.plans.investment.map((plan, idx) => (
<button key={idx} onClick={() => setActiveInvestTab(idx)} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activeInvestTab === idx ? 'border-amber-500 text-amber-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}> {plan.name}</button>
))}
</div>
{settings.plans.investment.length > 0 && settings.plans.investment.map((plan, idx) => idx === activeInvestTab && (
<div key={idx} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="bg-amber-50 px-4 py-3 border-b border-amber-100 flex items-center justify-between">
<div>
<h4 className="font-semibold text-amber-800">{plan.name}</h4>
<p className="text-sm text-amber-600 mt-1">{plan.description}</p>
</div>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${plan.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>{plan.status}</span>
</div>
<div className="p-4">
<div className="grid lg:grid-cols-3 gap-4">
<div>
<label className="text-sm text-slate-600">Plan Condition</label>
<input type="text" value={plan.name} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].name = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Status</label>
<select value={plan.status} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].status = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1">
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="closed">Closed</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Target Amount ()</label>
<input type="number" value={plan.targetAmount} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].targetAmount = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Start Date</label>
<input type="date" value={plan.startDate} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].startDate = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">End Date</label>
<input type="date" value={plan.endDate} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].endDate = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">EV Base Price ()</label>
<input type="number" value={plan.evBasePrice} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].evBasePrice = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Minimum Quantity (Bikes)</label>
<input type="number" value={plan.minQuantity} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].minQuantity = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Min Investment ()</label>
<div className="flex items-center gap-2 mt-1">
<input type="number" value={plan.evBasePrice * plan.minQuantity} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100" />
<span className="text-xs text-slate-500 whitespace-nowrap">= Qty × Base Price</span>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Max Investment ()</label>
<input type="number" value={plan.maxInvestment} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].maxInvestment = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Duration (Months)</label>
<input type="number" value={plan.durationMonths} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].durationMonths = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Lock-in Period (Months)</label>
<input type="number" value={plan.lockInMonths} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].lockInMonths = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Early Exit Penalty (%)</label>
<input type="number" value={plan.earlyExitPenalty} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].earlyExitPenalty = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
</div>
<div className="grid grid-cols-3 gap-4 mt-4">
<div>
<label className="text-sm text-slate-600">FICO Share (%) - Single Rent</label>
<input type="number" value={(plan as any).ficoSingleRent} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].ficoSingleRent = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">FICO Share (%) - Rent to Own</label>
<input type="number" value={(plan as any).ficoRentToOwn} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].ficoRentToOwn = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">FICO Share (%) - Share an EV</label>
<input type="number" value={(plan as any).ficoShareEv} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].ficoShareEv = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
</div>
<div className="mt-4">
<label className="text-sm text-slate-600">Description</label>
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.investment]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, investment: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" rows={2} />
</div>
<div className="mt-4 flex justify-end">
<button onClick={handleSave} className="px-4 py-2 bg-amber-600 text-white rounded-lg text-sm font-medium flex items-center gap-2">
<Save className="w-4 h-4" /> Save Changes
</button>
</div>
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,401 @@
'use client';
import { CompanySettings } from '../page';
interface KycSettingsProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
activeMasterTab: 'investor' | 'merchant' | 'swapstation' | 'rentalType';
setActiveMasterTab: (tab: 'investor' | 'merchant' | 'swapstation' | 'rentalType') => void;
addDocType: 'investor' | 'merchant' | 'swapstation' | 'rental' | null;
setAddDocType: (type: 'investor' | 'merchant' | 'swapstation' | 'rental' | null) => void;
newDocName: string;
setNewDocName: (name: string) => void;
newDocDesc: string;
setNewDocDesc: (desc: string) => void;
}
export default function KycSettings({
settings,
setSettings,
activeMasterTab,
setActiveMasterTab,
addDocType,
setAddDocType,
newDocName,
setNewDocName,
newDocDesc,
setNewDocDesc,
}: KycSettingsProps) {
return (
<div className="p-6 space-y-6">
<h3 className="text-lg font-semibold text-slate-800">KYC Documents</h3>
<div className="flex flex-wrap gap-2 border-b border-slate-200 pb-2">
{[
{ id: 'investor', label: 'Investor' },
{ id: 'merchant', label: 'Merchant' },
{ id: 'swapstation', label: 'Swap Station' },
{ id: 'rentalType', label: 'Rental Types' },
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveMasterTab(tab.id as typeof activeMasterTab)}
className={`px-3 py-1.5 text-sm rounded-lg ${activeMasterTab === tab.id
? 'bg-accent text-white'
: 'text-slate-600 hover:bg-slate-50'
}`}
>
{tab.label}
</button>
))}
</div>
{activeMasterTab === 'investor' && (
<div>
<h4 className="font-medium text-slate-700 mb-3">Investor Documents</h4>
<div className="space-y-2">
{settings.masterData.investorDocuments.map((doc, i) => (
<div key={doc.id} className="flex items-center gap-3 p-3 border border-slate-200 rounded-lg">
<input
type="checkbox"
checked={doc.required}
onChange={(e) => {
const updated = [...settings.masterData.investorDocuments];
updated[i].required = e.target.checked;
setSettings({ ...settings, masterData: { ...settings.masterData, investorDocuments: updated } });
}}
className="w-4 h-4"
/>
<div className="flex-1">
<p className="text-sm font-medium">{doc.name}</p>
<p className="text-xs text-slate-500">{doc.description}</p>
</div>
<span className={`text-xs px-2 py-0.5 rounded ${doc.required ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-500'}`}>
{doc.required ? 'Required' : 'Optional'}
</span>
</div>
))}
</div>
<button
onClick={() => setAddDocType('investor')}
className="mt-4 text-sm text-accent hover:underline"
>
+ Add Document
</button>
{addDocType === 'investor' && (
<div className="mt-3 p-3 bg-slate-50 rounded-lg border border-slate-200">
<input
type="text"
placeholder="Document name"
value={newDocName}
onChange={(e) => setNewDocName(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mb-2"
/>
<input
type="text"
placeholder="Description"
value={newDocDesc}
onChange={(e) => setNewDocDesc(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mb-2"
/>
<div className="flex gap-2">
<button
onClick={() => {
if (newDocName.trim()) {
const updated = [...settings.masterData.investorDocuments, {
id: `inv_${Date.now()}`,
name: newDocName,
required: false,
description: newDocDesc || ''
}];
setSettings({ ...settings, masterData: { ...settings.masterData, investorDocuments: updated } });
setNewDocName('');
setNewDocDesc('');
setAddDocType(null);
}
}}
className="px-3 py-1.5 bg-accent text-white text-sm rounded-lg"
>
Add
</button>
<button
onClick={() => {
setAddDocType(null);
setNewDocName('');
setNewDocDesc('');
}}
className="px-3 py-1.5 border border-slate-200 text-slate-600 text-sm rounded-lg"
>
Cancel
</button>
</div>
</div>
)}
</div>
)}
{activeMasterTab === 'merchant' && (
<div>
<h4 className="font-medium text-slate-700 mb-3">Merchant Documents</h4>
<div className="space-y-2">
{settings.masterData.merchantDocuments.map((doc, i) => (
<div key={doc.id} className="flex items-center gap-3 p-3 border border-slate-200 rounded-lg">
<input
type="checkbox"
checked={doc.required}
onChange={(e) => {
const updated = [...settings.masterData.merchantDocuments];
updated[i].required = e.target.checked;
setSettings({ ...settings, masterData: { ...settings.masterData, merchantDocuments: updated } });
}}
className="w-4 h-4"
/>
<div className="flex-1">
<p className="text-sm font-medium">{doc.name}</p>
<p className="text-xs text-slate-500">{doc.description}</p>
</div>
<span className={`text-xs px-2 py-0.5 rounded ${doc.required ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-500'}`}>
{doc.required ? 'Required' : 'Optional'}
</span>
</div>
))}
</div>
<button
onClick={() => setAddDocType('merchant')}
className="mt-4 text-sm text-accent hover:underline"
>
+ Add Document
</button>
{addDocType === 'merchant' && (
<div className="mt-3 p-3 bg-slate-50 rounded-lg border border-slate-200">
<input
type="text"
placeholder="Document name"
value={newDocName}
onChange={(e) => setNewDocName(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mb-2"
/>
<input
type="text"
placeholder="Description"
value={newDocDesc}
onChange={(e) => setNewDocDesc(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mb-2"
/>
<div className="flex gap-2">
<button
onClick={() => {
if (newDocName.trim()) {
const updated = [...settings.masterData.merchantDocuments, {
id: `mer_${Date.now()}`,
name: newDocName,
required: false,
description: newDocDesc || ''
}];
setSettings({ ...settings, masterData: { ...settings.masterData, merchantDocuments: updated } });
setNewDocName('');
setNewDocDesc('');
setAddDocType(null);
}
}}
className="px-3 py-1.5 bg-accent text-white text-sm rounded-lg"
>
Add
</button>
<button
onClick={() => {
setAddDocType(null);
setNewDocName('');
setNewDocDesc('');
}}
className="px-3 py-1.5 border border-slate-200 text-slate-600 text-sm rounded-lg"
>
Cancel
</button>
</div>
</div>
)}
</div>
)}
{activeMasterTab === 'swapstation' && (
<div>
<h4 className="font-medium text-slate-700 mb-3">Swap Station Documents</h4>
<div className="space-y-2">
{settings.masterData.swapStationDocuments.map((doc, i) => (
<div key={doc.id} className="flex items-center gap-3 p-3 border border-slate-200 rounded-lg">
<input
type="checkbox"
checked={doc.required}
onChange={(e) => {
const updated = [...settings.masterData.swapStationDocuments];
updated[i].required = e.target.checked;
setSettings({ ...settings, masterData: { ...settings.masterData, swapStationDocuments: updated } });
}}
className="w-4 h-4"
/>
<div className="flex-1">
<p className="text-sm font-medium">{doc.name}</p>
<p className="text-xs text-slate-500">{doc.description}</p>
</div>
<span className={`text-xs px-2 py-0.5 rounded ${doc.required ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-500'}`}>
{doc.required ? 'Required' : 'Optional'}
</span>
</div>
))}
</div>
<button
onClick={() => setAddDocType('swapstation')}
className="mt-4 text-sm text-accent hover:underline"
>
+ Add Document
</button>
{addDocType === 'swapstation' && (
<div className="mt-3 p-3 bg-slate-50 rounded-lg border border-slate-200">
<input
type="text"
placeholder="Document name"
value={newDocName}
onChange={(e) => setNewDocName(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mb-2"
/>
<input
type="text"
placeholder="Description"
value={newDocDesc}
onChange={(e) => setNewDocDesc(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mb-2"
/>
<div className="flex gap-2">
<button
onClick={() => {
if (newDocName.trim()) {
const updated = [...settings.masterData.swapStationDocuments, {
id: `ss_${Date.now()}`,
name: newDocName,
required: false,
description: newDocDesc || ''
}];
setSettings({ ...settings, masterData: { ...settings.masterData, swapStationDocuments: updated } });
setNewDocName('');
setNewDocDesc('');
setAddDocType(null);
}
}}
className="px-3 py-1.5 bg-accent text-white text-sm rounded-lg"
>
Add
</button>
<button
onClick={() => {
setAddDocType(null);
setNewDocName('');
setNewDocDesc('');
}}
className="px-3 py-1.5 border border-slate-200 text-slate-600 text-sm rounded-lg"
>
Cancel
</button>
</div>
</div>
)}
</div>
)}
{activeMasterTab === 'rentalType' && (
<div className="space-y-6">
{settings.masterData.rentalDocuments.map((rental, ri) => (
<div key={rental.type} className="border border-slate-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-slate-700">{rental.name}</h4>
<span className="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded">{rental.documents.length} docs</span>
</div>
<div className="space-y-2">
{rental.documents.map((doc, di) => (
<div key={doc.id} className="flex items-center gap-3 p-2 bg-slate-50 rounded-lg">
<input
type="checkbox"
checked={doc.required}
onChange={(e) => {
const updated = [...settings.masterData.rentalDocuments];
updated[ri].documents[di].required = e.target.checked;
setSettings({ ...settings, masterData: { ...settings.masterData, rentalDocuments: updated } });
}}
className="w-4 h-4"
/>
<div className="flex-1">
<p className="text-sm">{doc.name}</p>
<p className="text-xs text-slate-500">{doc.description}</p>
</div>
<span className={`text-xs px-2 py-0.5 rounded ${doc.required ? 'bg-red-100 text-red-700' : 'bg-slate-200 text-slate-600'}`}>
{doc.required ? 'Required' : 'Optional'}
</span>
</div>
))}
</div>
<button
onClick={() => setAddDocType('rental')}
className="mt-3 text-sm text-accent hover:underline"
>
+ Add Document
</button>
{addDocType === 'rental' && (
<div className="mt-3 p-3 bg-slate-50 rounded-lg border border-slate-200">
<input
type="text"
placeholder="Document name"
value={newDocName}
onChange={(e) => setNewDocName(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mb-2"
/>
<input
type="text"
placeholder="Description"
value={newDocDesc}
onChange={(e) => setNewDocDesc(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mb-2"
/>
<div className="flex gap-2">
<button
onClick={() => {
if (newDocName.trim()) {
const updated = [...settings.masterData.rentalDocuments];
updated.forEach((rental) => {
rental.documents.push({
id: `rent_${Date.now()}`,
name: newDocName,
required: false,
description: newDocDesc || ''
});
});
setSettings({ ...settings, masterData: { ...settings.masterData, rentalDocuments: updated } });
setNewDocName('');
setNewDocDesc('');
setAddDocType(null);
}
}}
className="px-3 py-1.5 bg-accent text-white text-sm rounded-lg"
>
Add to All
</button>
<button
onClick={() => {
setAddDocType(null);
setNewDocName('');
setNewDocDesc('');
}}
className="px-3 py-1.5 border border-slate-200 text-slate-600 text-sm rounded-lg"
>
Cancel
</button>
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,93 @@
'use client';
import { Image } from 'lucide-react';
import { CompanySettings } from '../page';
interface LandingSettingsProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
}
export default function LandingSettings({ settings, setSettings }: LandingSettingsProps) {
return (
<div className="p-6 space-y-6">
<h3 className="text-lg font-semibold text-slate-800">Landing Page Hero Section</h3>
<div className="grid lg:grid-cols-2 gap-4">
<div>
<label className="text-sm text-slate-600">Hero Title</label>
<input
type="text"
value={settings.heroData.title}
onChange={(e) => setSettings({ ...settings, heroData: { ...settings.heroData, title: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">Subtitle</label>
<input
type="text"
value={settings.heroData.subtitle}
onChange={(e) => setSettings({ ...settings, heroData: { ...settings.heroData, subtitle: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
</div>
<div className="grid lg:grid-cols-2 gap-4">
<div>
<label className="text-sm text-slate-600">CTA Button Text</label>
<input
type="text"
value={settings.heroData.ctaText}
onChange={(e) => setSettings({ ...settings, heroData: { ...settings.heroData, ctaText: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
<div>
<label className="text-sm text-slate-600">CTA Link</label>
<input
type="text"
value={settings.heroData.ctaLink}
onChange={(e) => setSettings({ ...settings, heroData: { ...settings.heroData, ctaLink: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
/>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Background Image</label>
<div className="mt-2 border-2 border-dashed border-slate-200 rounded-lg p-6 text-center hover:border-accent cursor-pointer">
<Image className="w-12 h-12 mx-auto text-slate-400" />
<p className="text-sm text-slate-500 mt-2">Upload Background Image</p>
<p className="text-xs text-slate-400">Recommended: 1920x1080px</p>
</div>
</div>
<div>
<label className="text-sm text-slate-600">Video URL (Optional)</label>
<input
type="text"
value={settings.heroData.videoUrl}
onChange={(e) => setSettings({ ...settings, heroData: { ...settings.heroData, videoUrl: e.target.value } })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
placeholder="https://youtube.com/watch?v=..."
/>
</div>
<div className="pt-4 border-t">
<label className="text-sm text-slate-600">Preview</label>
<div className="mt-2 p-8 bg-gradient-to-br from-slate-800 to-slate-900 rounded-lg text-center">
<h4 className="text-2xl font-bold text-white">{settings.heroData.title}</h4>
<p className="text-slate-300 mt-2">{settings.heroData.subtitle}</p>
<button
className="mt-4 px-6 py-2 bg-accent text-white rounded-lg"
style={{ borderRadius: settings.borderRadius }}
>
{settings.heroData.ctaText}
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,127 @@
'use client';
import { Plus, Pencil, Trash2 } from 'lucide-react';
import { CompanySettings } from '../page';
interface PartsSettingsProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
showAddPolicy: boolean;
setShowAddPolicy: (show: boolean) => void;
newPolicyName: string;
setNewPolicyName: (name: string) => void;
newPolicyDesc: string;
setNewPolicyDesc: (desc: string) => void;
editPolicyDescHtml: string;
setEditPolicyDescHtml: (desc: string) => void;
editingPolicy: { tab: string; index: number } | null;
setEditingPolicy: (policy: { tab: string; index: number } | null) => void;
editPolicyName: string;
setEditPolicyName: (name: string) => void;
editPolicyDesc: string;
setEditPolicyDesc: (desc: string) => void;
}
export default function PartsSettings({
settings,
setSettings,
showAddPolicy,
setShowAddPolicy,
newPolicyName,
setNewPolicyName,
newPolicyDesc,
setNewPolicyDesc,
editPolicyDescHtml,
setEditPolicyDescHtml,
editingPolicy,
setEditingPolicy,
editPolicyName,
setEditPolicyName,
editPolicyDesc,
setEditPolicyDesc,
}: PartsSettingsProps) {
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-800">EV Parts</h3>
<button onClick={() => setShowAddPolicy(true)} className="text-xs text-blue-600 hover:text-blue-700 flex items-center gap-1">
<Plus className="w-3 h-3" /> Add Part
</button>
</div>
{showAddPolicy && (
<div className="p-3 bg-white rounded-lg border border-blue-200">
<input type="text" value={newPolicyName} onChange={(e) => setNewPolicyName(e.target.value)} className="w-full px-2 py-1.5 border border-slate-200 rounded text-sm mb-2" placeholder="Part Name" />
<div className="grid grid-cols-2 gap-2 mb-2">
<input type="number" value={newPolicyDesc} onChange={(e) => setNewPolicyDesc(e.target.value)} className="px-2 py-1.5 border border-slate-200 rounded text-sm" placeholder="Buying Price" />
<input type="number" value={editPolicyDescHtml} onChange={(e) => setEditPolicyDescHtml(e.target.value)} className="px-2 py-1.5 border border-slate-200 rounded text-sm" placeholder="Selling Price" />
</div>
<div className="flex gap-2">
<button onClick={() => {
if (!newPolicyName.trim()) return;
const newId = 'PRT-' + String(settings.parts.length + 1).padStart(3, '0');
setSettings({ ...settings, parts: [...settings.parts, { id: newId, name: newPolicyName, buyingPrice: Number(newPolicyDesc), sellingPrice: Number(editPolicyDescHtml) }] });
setNewPolicyName('');
setNewPolicyDesc('');
setEditPolicyDescHtml('');
setShowAddPolicy(false);
}} className="px-3 py-1.5 bg-blue-600 text-white rounded text-sm">Add</button>
<button onClick={() => { setNewPolicyName(''); setNewPolicyDesc(''); setEditPolicyDescHtml(''); setShowAddPolicy(false); }} className="px-3 py-1.5 bg-slate-200 text-slate-600 rounded text-sm">Cancel</button>
</div>
</div>
)}
<div className="space-y-2">
{settings.parts.map((part, i) => (
<div key={part.id} className="p-3 bg-slate-50 rounded-lg border border-slate-200">
{editingPolicy?.tab === 'parts' && editingPolicy?.index === i ? (
<div className="space-y-2">
<input type="text" value={editPolicyName} onChange={(e) => setEditPolicyName(e.target.value)} className="w-full px-2 py-1.5 border border-slate-200 rounded text-sm" placeholder="Part Name" />
<div className="grid grid-cols-2 gap-2">
<input type="number" value={editPolicyDesc} onChange={(e) => setEditPolicyDesc(e.target.value)} className="px-2 py-1.5 border border-slate-200 rounded text-sm" placeholder="Buying Price" />
<input type="number" value={newPolicyName} onChange={(e) => setNewPolicyName(e.target.value)} className="px-2 py-1.5 border border-slate-200 rounded text-sm" placeholder="Selling Price" />
</div>
<div className="flex gap-2">
<button onClick={() => {
const updated = [...settings.parts];
updated[i] = { ...part, name: editPolicyName, buyingPrice: Number(editPolicyDesc), sellingPrice: Number(newPolicyName) };
setSettings({ ...settings, parts: updated });
setEditingPolicy(null);
setEditPolicyName('');
setEditPolicyDesc('');
setNewPolicyName('');
}} className="px-2 py-1 bg-blue-600 text-white rounded text-xs">Save</button>
<button onClick={() => { setEditingPolicy(null); setEditPolicyName(''); setEditPolicyDesc(''); setNewPolicyName(''); }} className="px-2 py-1 bg-slate-200 text-slate-600 rounded text-xs">Cancel</button>
</div>
</div>
) : (
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-slate-700">{part.name}</span>
<span className="text-xs text-slate-400">({part.id})</span>
</div>
<div className="flex items-center gap-4 mt-1">
<span className="text-xs text-slate-500">Buying: <span className="text-green-600">{part.buyingPrice}</span></span>
<span className="text-xs text-slate-500">Selling: <span className="text-amber-600">{part.sellingPrice}</span></span>
</div>
</div>
<div className="flex items-center gap-1 ml-2">
<button onClick={() => { setEditingPolicy({ tab: 'parts', index: i }); setEditPolicyName(part.name); setEditPolicyDesc(String(part.buyingPrice)); setNewPolicyName(String(part.sellingPrice)); }} className="p-1 text-slate-400 hover:text-blue-600">
<Pencil className="w-3.5 h-3.5" />
</button>
<button onClick={() => {
const updated = settings.parts.filter((_, idx) => idx !== i);
setSettings({ ...settings, parts: updated });
}} className="p-1 text-slate-400 hover:text-red-600">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
)}
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,622 @@
'use client';
import { useState } from 'react';
import { Plus, Save, Trash2, X, Gift } from 'lucide-react';
import { CompanySettings } from '../page';
interface PlanSelectionProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
activePlanTab: 'singleRent' | 'rentToOwn' | 'shareEv';
setActivePlanTab: (tab: 'singleRent' | 'rentToOwn' | 'shareEv') => void;
handleSave: () => void;
addNewPlan: (type: 'singleRent' | 'rentToOwn' | 'shareEv') => void;
isDirty?: boolean;
}
// Reusable Free Service Conditions editor
function FreeServiceConditions({
conditions,
accentColor,
onChange,
}: {
conditions: { months: number; freeServices: number }[];
accentColor: string;
onChange: (updated: { months: number; freeServices: number }[]) => void;
}) {
const addCondition = () => {
onChange([...conditions, { months: 3, freeServices: 1 }]);
};
const removeCondition = (i: number) => {
onChange(conditions.filter((_, idx) => idx !== i));
};
const updateCondition = (i: number, field: 'months' | 'freeServices', value: number) => {
const updated = conditions.map((c, idx) => idx === i ? { ...c, [field]: value } : c);
onChange(updated);
};
return (
<div className="bg-amber-50 border border-amber-100 rounded-xl p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Gift className="w-4 h-4 text-amber-600" />
<label className="text-xs font-semibold text-amber-700 uppercase tracking-wide">
Free Service Conditions
</label>
<span className="text-[10px] text-amber-500 font-medium bg-amber-100 px-2 py-0.5 rounded-full">
e.g. "3 months → 2 free services"
</span>
</div>
<button
type="button"
onClick={addCondition}
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-semibold transition-all ${accentColor} text-white hover:opacity-90`}
>
<Plus className="w-3 h-3" /> Add Condition
</button>
</div>
{conditions.length === 0 && (
<p className="text-xs text-amber-400 italic text-center py-2">
No free service conditions set. Click "Add Condition" to add one.
</p>
)}
<div className="space-y-2">
{conditions.map((cond, i) => (
<div key={i} className="flex items-center gap-3 bg-white border border-amber-100 rounded-lg px-3 py-2 group">
{/* Month input */}
<div className="flex items-center gap-1.5">
<label className="text-xs text-slate-500 font-medium shrink-0">Month:</label>
<input
type="number"
min={1}
max={999}
value={cond.months}
onChange={(e) => updateCondition(i, 'months', parseInt(e.target.value) || 1)}
className="w-16 px-2 py-1 border border-slate-200 rounded-md text-xs text-slate-800 text-center font-semibold focus:outline-none focus:ring-1 focus:ring-amber-400"
/>
</div>
<span className="text-slate-300 text-sm"></span>
{/* Free services input */}
<div className="flex items-center gap-1.5">
<label className="text-xs text-slate-500 font-medium shrink-0">Free Services:</label>
<input
type="number"
min={1}
max={99}
value={cond.freeServices}
onChange={(e) => updateCondition(i, 'freeServices', parseInt(e.target.value) || 1)}
className="w-16 px-2 py-1 border border-slate-200 rounded-md text-xs text-slate-800 text-center font-semibold focus:outline-none focus:ring-1 focus:ring-amber-400"
/>
</div>
{/* Preview badge */}
<span className="flex-1 text-[10px] font-bold text-amber-700 bg-amber-50 border border-amber-100 rounded-full px-2.5 py-1 text-center truncate">
{cond.months} {cond.months === 1 ? 'month' : 'months'} {cond.freeServices} free service{cond.freeServices !== 1 ? 's' : ''} free
</span>
{/* Remove */}
<button
type="button"
onClick={() => removeCondition(i)}
className="p-1 text-slate-300 hover:text-red-500 hover:bg-red-50 rounded-md transition-all opacity-0 group-hover:opacity-100"
title="Remove condition"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
</div>
);
}
export default function PlanSelection({
settings,
setSettings,
activePlanTab,
setActivePlanTab,
handleSave,
addNewPlan,
}: PlanSelectionProps) {
const [deleteModal, setDeleteModal] = useState<{ type: 'singleRent' | 'rentToOwn' | 'shareEv' | null; idx: number | null }>({ type: null, idx: null });
const handleDeletePlan = () => {
if (deleteModal.type !== null && deleteModal.idx !== null) {
const updated = settings.plans[deleteModal.type].filter((_, i) => i !== deleteModal.idx);
setSettings({ ...settings, plans: { ...settings.plans, [deleteModal.type]: updated } });
setDeleteModal({ type: null, idx: null });
}
};
return (
<>
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-800">Plan Selection</h3>
</div>
<div className="flex gap-2 border-b border-slate-200">
<button onClick={() => setActivePlanTab('singleRent')} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activePlanTab === 'singleRent' ? 'border-blue-500 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>Single Rent</button>
<button onClick={() => setActivePlanTab('rentToOwn')} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activePlanTab === 'rentToOwn' ? 'border-purple-500 text-purple-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>Rent to Own</button>
<button onClick={() => setActivePlanTab('shareEv')} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activePlanTab === 'shareEv' ? 'border-green-500 text-green-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>Share an EV</button>
</div>
{activePlanTab === 'singleRent' && (
<div className="space-y-6">
{settings.plans.singleRent.map((plan, idx) => (
<div key={plan.id} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="bg-blue-50 px-4 py-3 border-b border-blue-100 flex items-center justify-between">
<div>
<h4 className="font-semibold text-blue-800">{plan.name} - {plan.dailyRent}/day</h4>
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full bg-transparent text-sm text-blue-600 mt-1 border-0 resize-none p-0 focus:ring-0" rows={1} placeholder="Plan description..." />
</div>
<div className="flex items-center gap-2 ml-3">
<button onClick={handleSave} className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-xs font-medium flex items-center gap-1 hover:bg-blue-700">
<Save className="w-3 h-3" /> Save
</button>
<button onClick={() => setDeleteModal({ type: 'singleRent', idx })} className="px-2 py-1.5 bg-red-50 text-red-600 rounded-lg text-xs font-medium flex items-center gap-1 hover:bg-red-100">
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
<div className="p-5 space-y-5">
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Plan Condition</label>
<input type="text" value={plan.name} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].name = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Plan Condition" />
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">EV Model Numbers</label>
<div className="relative">
<button type="button" onClick={() => { const el = document.getElementById(`ev-models-${idx}`); if (el) el.classList.toggle('hidden'); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white text-left flex items-center justify-between cursor-pointer hover:border-slate-300">
<span className={plan.evModels.length > 0 ? 'text-slate-800' : 'text-slate-400'}>
{plan.evModels.length > 0 ? `${plan.evModels.length} selected` : 'Select EV Models...'}
</span>
<svg className="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
</button>
<div id={`ev-models-${idx}`} className="hidden absolute z-50 w-full mt-1 bg-white border border-slate-200 rounded-lg shadow-lg max-h-60 overflow-y-auto">
{['Etron ET50', 'Yadea DT3', 'AIMA Lightning', 'AIMA EM5', 'Yadea G5', 'TVS iQube', 'Bajaj Chetak', 'Hero Photon', 'Okinawa Praise', 'Ampere Magnus', 'Benling Aura', 'Lectrix LXS', 'Revolt RV400'].map(model => (
<label key={model} className="flex items-center gap-2 px-3 py-2 hover:bg-slate-50 cursor-pointer text-sm">
<input type="checkbox" checked={plan.evModels.includes(model)} onChange={() => { const updated = [...settings.plans.singleRent]; updated[idx].evModels = updated[idx].evModels.includes(model) ? updated[idx].evModels.filter(m => m !== model) : [...updated[idx].evModels, model]; setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="rounded border-slate-300 text-emerald-600" />
<span>{model}</span>
</label>
))}
</div>
</div>
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Deposit ()</label>
<input type="number" value={plan.deposit} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].deposit = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 space-y-3">
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide">Daily Rent</label>
<div className="grid grid-cols-4 gap-3">
<div>
<label className="text-xs text-slate-500">Base ()</label>
<input type="number" value={plan.dailyRent} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].dailyRent = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">1st Day Penalty ()</label>
<input type="number" value={plan.dailyRentPenalty1} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].dailyRentPenalty1 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">2nd Day Penalty ()</label>
<input type="number" value={plan.dailyRentPenalty2} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].dailyRentPenalty2 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">3rd Day Penalty + Bike Lock ()</label>
<input type="number" value={plan.dailyRentPenalty3} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].dailyRentPenalty3 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 space-y-3">
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide">Weekly Subscription</label>
<div className="grid grid-cols-4 gap-3">
<div>
<label className="text-xs text-slate-500">Base ()</label>
<input type="number" value={plan.weeklySubscription} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].weeklySubscription = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">1st Day Penalty ()</label>
<input type="number" value={plan.weeklyPenalty1} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].weeklyPenalty1 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">2nd Day Penalty ()</label>
<input type="number" value={plan.weeklyPenalty2} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].weeklyPenalty2 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">3rd Day Penalty + Bike Lock ()</label>
<input type="number" value={plan.weeklyPenalty3} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].weeklyPenalty3 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 space-y-3">
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide">Monthly Subscription</label>
<div className="grid grid-cols-4 gap-3">
<div>
<label className="text-xs text-slate-500">Base ()</label>
<input type="number" value={plan.monthlySubscription} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].monthlySubscription = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">1st Day Penalty ()</label>
<input type="number" value={plan.monthlyPenalty1} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].monthlyPenalty1 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">2nd Day Penalty ()</label>
<input type="number" value={plan.monthlyPenalty2} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].monthlyPenalty2 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">3rd Day Penalty + Bike Lock ()</label>
<input type="number" value={plan.monthlyPenalty3} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].monthlyPenalty3 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide block mb-2">Contract Duration (Months)</label>
<div className="flex flex-wrap gap-2">
{plan.contractMonths.map(month => (
<button key={month} onClick={() => { const updated = [...settings.plans.singleRent]; updated[idx].contractMonths = updated[idx].contractMonths.filter(m => m !== month); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="px-3 py-1.5 rounded-lg text-xs font-medium bg-blue-600 text-white hover:bg-red-500 transition-all flex items-center gap-1">
{month} {month === 1 ? 'Month' : 'Months'}
<span className="ml-1 font-bold">×</span>
</button>
))}
<div className="flex items-center gap-1">
<input type="number" min="1" placeholder="Add" className="w-20 px-2 py-1.5 border border-slate-200 rounded-lg text-xs" onKeyDown={(e) => { if (e.key === 'Enter') { const val = parseInt((e.target as HTMLInputElement).value); if (val > 0 && !plan.contractMonths.includes(val)) { const updated = [...settings.plans.singleRent]; updated[idx].contractMonths = [...updated[idx].contractMonths, val].sort((a, b) => a - b); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); (e.target as HTMLInputElement).value = ''; } } }} />
<button onClick={(e) => { const input = (e.currentTarget.previousElementSibling as HTMLInputElement); const val = parseInt(input.value); if (val > 0 && !plan.contractMonths.includes(val)) { const updated = [...settings.plans.singleRent]; updated[idx].contractMonths = [...updated[idx].contractMonths, val].sort((a, b) => a - b); setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); input.value = ''; } }} className="px-2 py-1.5 bg-blue-600 text-white rounded-lg text-xs hover:bg-blue-700">+</button>
</div>
</div>
{plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>}
</div>
{/* Free Service Conditions */}
<FreeServiceConditions
conditions={plan.freeServiceConditions ?? []}
accentColor="bg-blue-600"
onChange={(updated) => {
const plans = [...settings.plans.singleRent];
plans[idx] = { ...plans[idx], freeServiceConditions: updated };
setSettings({ ...settings, plans: { ...settings.plans, singleRent: plans } });
}}
/>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Description</label>
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.singleRent]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, singleRent: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter plan description..." />
</div>
</div>
</div>
))}
<button onClick={() => addNewPlan('singleRent')} className="w-full py-3 border-2 border-dashed border-slate-300 rounded-xl text-slate-500 hover:border-blue-400 hover:text-blue-500 hover:bg-blue-50 transition-all flex items-center justify-center gap-2 text-sm font-medium">
<Plus className="w-4 h-4" /> Add New Plan
</button>
</div>
)}
{activePlanTab === 'rentToOwn' && (
<div className="space-y-6">
{settings.plans.rentToOwn.map((plan, idx) => (
<div key={plan.id} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="bg-purple-50 px-4 py-3 border-b border-purple-100 flex items-center justify-between">
<div>
<h4 className="font-semibold text-purple-800">{plan.name} - {plan.dailyRent}/day</h4>
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full bg-transparent text-sm text-purple-600 mt-1 border-0 resize-none p-0 focus:ring-0" rows={1} placeholder="Plan description..." />
</div>
<div className="flex items-center gap-2 ml-3">
<button onClick={handleSave} className="px-3 py-1.5 bg-purple-600 text-white rounded-lg text-xs font-medium flex items-center gap-1 hover:bg-purple-700">
<Save className="w-3 h-3" /> Save
</button>
<button onClick={() => setDeleteModal({ type: 'rentToOwn', idx })} className="px-2 py-1.5 bg-red-50 text-red-600 rounded-lg text-xs font-medium flex items-center gap-1 hover:bg-red-100">
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
<div className="p-5 space-y-5">
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Plan Condition</label>
<input type="text" value={plan.name} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].name = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Plan Condition" />
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">EV Model Numbers</label>
<div className="relative">
<button type="button" onClick={() => { const el = document.getElementById(`ev-models-rto-${idx}`); if (el) el.classList.toggle('hidden'); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white text-left flex items-center justify-between cursor-pointer hover:border-slate-300">
<span className={plan.evModels.length > 0 ? 'text-slate-800' : 'text-slate-400'}>
{plan.evModels.length > 0 ? `${plan.evModels.length} selected` : 'Select EV Models...'}
</span>
<svg className="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
</button>
<div id={`ev-models-rto-${idx}`} className="hidden absolute z-50 w-full mt-1 bg-white border border-slate-200 rounded-lg shadow-lg max-h-60 overflow-y-auto">
{['Etron ET50', 'Yadea DT3', 'AIMA Lightning', 'AIMA EM5', 'Yadea G5', 'TVS iQube', 'Bajaj Chetak', 'Hero Photon', 'Okinawa Praise', 'Ampere Magnus', 'Benling Aura', 'Lectrix LXS', 'Revolt RV400'].map(model => (
<label key={model} className="flex items-center gap-2 px-3 py-2 hover:bg-slate-50 cursor-pointer text-sm">
<input type="checkbox" checked={plan.evModels.includes(model)} onChange={() => { const updated = [...settings.plans.rentToOwn]; updated[idx].evModels = updated[idx].evModels.includes(model) ? updated[idx].evModels.filter(m => m !== model) : [...updated[idx].evModels, model]; setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="rounded border-slate-300 text-emerald-600" />
<span>{model}</span>
</label>
))}
</div>
</div>
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Deposit ()</label>
<input type="number" value={plan.deposit} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].deposit = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 space-y-3">
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide">Daily Rent</label>
<div className="grid grid-cols-4 gap-3">
<div>
<label className="text-xs text-slate-500">Base ()</label>
<input type="number" value={plan.dailyRent} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].dailyRent = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">1st Day Penalty ()</label>
<input type="number" value={plan.dailyRentPenalty1} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].dailyRentPenalty1 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">2nd Day Penalty ()</label>
<input type="number" value={plan.dailyRentPenalty2} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].dailyRentPenalty2 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">3rd Day Penalty + Bike Lock ()</label>
<input type="number" value={plan.dailyRentPenalty3} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].dailyRentPenalty3 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 space-y-3">
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide">Weekly Subscription</label>
<div className="grid grid-cols-4 gap-3">
<div>
<label className="text-xs text-slate-500">Base ()</label>
<input type="number" value={plan.weeklySubscription} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].weeklySubscription = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">1st Day Penalty ()</label>
<input type="number" value={plan.weeklyPenalty1} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].weeklyPenalty1 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">2nd Day Penalty ()</label>
<input type="number" value={plan.weeklyPenalty2} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].weeklyPenalty2 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">3rd Day Penalty + Bike Lock ()</label>
<input type="number" value={plan.weeklyPenalty3} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].weeklyPenalty3 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 space-y-3">
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide">Monthly Subscription</label>
<div className="grid grid-cols-4 gap-3">
<div>
<label className="text-xs text-slate-500">Base ()</label>
<input type="number" value={plan.monthlySubscription} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].monthlySubscription = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">1st Day Penalty ()</label>
<input type="number" value={plan.monthlyPenalty1} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].monthlyPenalty1 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">2nd Day Penalty ()</label>
<input type="number" value={plan.monthlyPenalty2} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].monthlyPenalty2 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">3rd Day Penalty + Bike Lock ()</label>
<input type="number" value={plan.monthlyPenalty3} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].monthlyPenalty3 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide block mb-2">Contract Duration (Months)</label>
<div className="flex flex-wrap gap-2">
{plan.contractMonths.map(month => (
<button key={month} onClick={() => { const updated = [...settings.plans.rentToOwn]; updated[idx].contractMonths = updated[idx].contractMonths.filter(m => m !== month); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="px-3 py-1.5 rounded-lg text-xs font-medium bg-purple-600 text-white hover:bg-red-500 transition-all flex items-center gap-1">
{month} {month === 1 ? 'Month' : 'Months'}
<span className="ml-1 font-bold">×</span>
</button>
))}
<div className="flex items-center gap-1">
<input type="number" min="1" placeholder="Add" className="w-20 px-2 py-1.5 border border-slate-200 rounded-lg text-xs" onKeyDown={(e) => { if (e.key === 'Enter') { const val = parseInt((e.target as HTMLInputElement).value); if (val > 0 && !plan.contractMonths.includes(val)) { const updated = [...settings.plans.rentToOwn]; updated[idx].contractMonths = [...updated[idx].contractMonths, val].sort((a, b) => a - b); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); (e.target as HTMLInputElement).value = ''; } } }} />
<button onClick={(e) => { const input = (e.currentTarget.previousElementSibling as HTMLInputElement); const val = parseInt(input.value); if (val > 0 && !plan.contractMonths.includes(val)) { const updated = [...settings.plans.rentToOwn]; updated[idx].contractMonths = [...updated[idx].contractMonths, val].sort((a, b) => a - b); setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); input.value = ''; } }} className="px-2 py-1.5 bg-purple-600 text-white rounded-lg text-xs hover:bg-purple-700">+</button>
</div>
</div>
{plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>}
</div>
{/* Free Service Conditions */}
<FreeServiceConditions
conditions={plan.freeServiceConditions ?? []}
accentColor="bg-purple-600"
onChange={(updated) => {
const plans = [...settings.plans.rentToOwn];
plans[idx] = { ...plans[idx], freeServiceConditions: updated };
setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: plans } });
}}
/>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Description</label>
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.rentToOwn]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, rentToOwn: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter plan description..." />
</div>
</div>
</div>
))}
<button onClick={() => addNewPlan('rentToOwn')} className="w-full py-3 border-2 border-dashed border-slate-300 rounded-xl text-slate-500 hover:border-purple-400 hover:text-purple-500 hover:bg-purple-50 transition-all flex items-center justify-center gap-2 text-sm font-medium">
<Plus className="w-4 h-4" /> Add New Plan
</button>
</div>
)}
{activePlanTab === 'shareEv' && (
<div className="space-y-6">
{settings.plans.shareEv.map((plan, idx) => (
<div key={plan.id} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="bg-green-50 px-4 py-3 border-b border-green-100 flex items-center justify-between">
<div>
<h4 className="font-semibold text-green-800">{plan.name} - {plan.dailyRentEach}/day each</h4>
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full bg-transparent text-sm text-green-600 mt-1 border-0 resize-none p-0 focus:ring-0" rows={1} placeholder="Plan description..." />
</div>
<div className="flex items-center gap-2 ml-3">
<button onClick={handleSave} className="px-3 py-1.5 bg-green-600 text-white rounded-lg text-xs font-medium flex items-center gap-1 hover:bg-green-700">
<Save className="w-3 h-3" /> Save
</button>
<button onClick={() => setDeleteModal({ type: 'shareEv', idx })} className="px-2 py-1.5 bg-red-50 text-red-600 rounded-lg text-xs font-medium flex items-center gap-1 hover:bg-red-100">
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
<div className="p-5 space-y-5">
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Plan Condition</label>
<input type="text" value={plan.name} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].name = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Plan Condition" />
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">EV Model Numbers</label>
<div className="relative">
<button type="button" onClick={() => { const el = document.getElementById(`ev-models-se-${idx}`); if (el) el.classList.toggle('hidden'); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white text-left flex items-center justify-between cursor-pointer hover:border-slate-300">
<span className={plan.evModels.length > 0 ? 'text-slate-800' : 'text-slate-400'}>
{plan.evModels.length > 0 ? `${plan.evModels.length} selected` : 'Select EV Models...'}
</span>
<svg className="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
</button>
<div id={`ev-models-se-${idx}`} className="hidden absolute z-50 w-full mt-1 bg-white border border-slate-200 rounded-lg shadow-lg max-h-60 overflow-y-auto">
{['Etron ET50', 'Yadea DT3', 'AIMA Lightning', 'AIMA EM5', 'Yadea G5', 'TVS iQube', 'Bajaj Chetak', 'Hero Photon', 'Okinawa Praise', 'Ampere Magnus', 'Benling Aura', 'Lectrix LXS', 'Revolt RV400'].map(model => (
<label key={model} className="flex items-center gap-2 px-3 py-2 hover:bg-slate-50 cursor-pointer text-sm">
<input type="checkbox" checked={plan.evModels.includes(model)} onChange={() => { const updated = [...settings.plans.shareEv]; updated[idx].evModels = updated[idx].evModels.includes(model) ? updated[idx].evModels.filter(m => m !== model) : [...updated[idx].evModels, model]; setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="rounded border-slate-300 text-emerald-600" />
<span>{model}</span>
</label>
))}
</div>
</div>
</div>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Deposit Each ()</label>
<input type="number" value={plan.depositEach} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].depositEach = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 space-y-3">
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide">Daily Rent (Each)</label>
<div className="grid grid-cols-4 gap-3">
<div>
<label className="text-xs text-slate-500">Base ()</label>
<input type="number" value={plan.dailyRentEach} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].dailyRentEach = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">1st Day Penalty ()</label>
<input type="number" value={plan.dailyRentPenalty1} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].dailyRentPenalty1 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">2nd Day Penalty ()</label>
<input type="number" value={plan.dailyRentPenalty2} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].dailyRentPenalty2 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">3rd Day Penalty + Bike Lock ()</label>
<input type="number" value={plan.dailyRentPenalty3} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].dailyRentPenalty3 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 space-y-3">
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide">Weekly Subscription (Each)</label>
<div className="grid grid-cols-4 gap-3">
<div>
<label className="text-xs text-slate-500">Base ()</label>
<input type="number" value={plan.weeklySubscriptionEach} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].weeklySubscriptionEach = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">1st Day Penalty ()</label>
<input type="number" value={plan.weeklyPenalty1} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].weeklyPenalty1 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">2nd Day Penalty ()</label>
<input type="number" value={plan.weeklyPenalty2} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].weeklyPenalty2 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">3rd Day Penalty + Bike Lock ()</label>
<input type="number" value={plan.weeklyPenalty3} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].weeklyPenalty3 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 space-y-3">
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide">Monthly Subscription (Each)</label>
<div className="grid grid-cols-4 gap-3">
<div>
<label className="text-xs text-slate-500">Base ()</label>
<input type="number" value={plan.monthlySubscriptionEach} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].monthlySubscriptionEach = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">1st Day Penalty ()</label>
<input type="number" value={plan.monthlyPenalty1} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].monthlyPenalty1 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">2nd Day Penalty ()</label>
<input type="number" value={plan.monthlyPenalty2} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].monthlyPenalty2 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-xs text-slate-500">3rd Day Penalty + Bike Lock ()</label>
<input type="number" value={plan.monthlyPenalty3} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].monthlyPenalty3 = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-2 py-1.5 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide block mb-2">Contract Duration (Months)</label>
<div className="flex flex-wrap gap-2">
{plan.contractMonths.map(month => (
<button key={month} onClick={() => { const updated = [...settings.plans.shareEv]; updated[idx].contractMonths = updated[idx].contractMonths.filter(m => m !== month); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="px-3 py-1.5 rounded-lg text-xs font-medium bg-green-600 text-white hover:bg-red-500 transition-all flex items-center gap-1">
{month} {month === 1 ? 'Month' : 'Months'}
<span className="ml-1 font-bold">×</span>
</button>
))}
<div className="flex items-center gap-1">
<input type="number" min="1" placeholder="Add" className="w-20 px-2 py-1.5 border border-slate-200 rounded-lg text-xs" onKeyDown={(e) => { if (e.key === 'Enter') { const val = parseInt((e.target as HTMLInputElement).value); if (val > 0 && !plan.contractMonths.includes(val)) { const updated = [...settings.plans.shareEv]; updated[idx].contractMonths = [...updated[idx].contractMonths, val].sort((a, b) => a - b); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); (e.target as HTMLInputElement).value = ''; } } }} />
<button onClick={(e) => { const input = (e.currentTarget.previousElementSibling as HTMLInputElement); const val = parseInt(input.value); if (val > 0 && !plan.contractMonths.includes(val)) { const updated = [...settings.plans.shareEv]; updated[idx].contractMonths = [...updated[idx].contractMonths, val].sort((a, b) => a - b); setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); input.value = ''; } }} className="px-2 py-1.5 bg-green-600 text-white rounded-lg text-xs hover:bg-green-700">+</button>
</div>
</div>
{plan.contractMonths.length === 0 && <p className="text-xs text-slate-400 mt-2">No contract months selected.</p>}
</div>
{/* Free Service Conditions */}
<FreeServiceConditions
conditions={plan.freeServiceConditions ?? []}
accentColor="bg-green-600"
onChange={(updated) => {
const plans = [...settings.plans.shareEv];
plans[idx] = { ...plans[idx], freeServiceConditions: updated };
setSettings({ ...settings, plans: { ...settings.plans, shareEv: plans } });
}}
/>
<div>
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 block">Description</label>
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.shareEv]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, shareEv: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Enter plan description..." />
</div>
</div>
</div>
))}
<button onClick={() => addNewPlan('shareEv')} className="w-full py-3 border-2 border-dashed border-slate-300 rounded-xl text-slate-500 hover:border-green-400 hover:text-green-500 hover:bg-green-50 transition-all flex items-center justify-center gap-2 text-sm font-medium">
<Plus className="w-4 h-4" /> Add New Plan
</button>
</div>
)}
</div>
{/* Delete Confirmation Modal */}
{deleteModal.type !== null && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md p-6 space-y-4">
<h3 className="text-lg font-bold text-slate-800">Delete Plan?</h3>
<p className="text-sm text-slate-500">This will permanently remove the plan. This action cannot be undone.</p>
<div className="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
<button onClick={() => setDeleteModal({ type: null, idx: null })} className="px-4 py-2 border border-slate-200 text-slate-500 rounded-lg text-sm font-medium hover:bg-slate-50">Cancel</button>
<button onClick={handleDeletePlan} className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-medium hover:bg-red-700">Delete Plan</button>
</div>
</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,234 @@
'use client';
import { Plus, X, Save } from 'lucide-react';
import type { CompanySettings } from '../page';
interface RiderRequestSettingsProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
activeRiderTab: number;
setActiveRiderTab: (n: number) => void;
addRiderPlan: boolean;
setAddRiderPlan: (v: boolean) => void;
newRiderName: string;
setNewRiderName: (v: string) => void;
newRiderTier: string;
setNewRiderTier: (v: string) => void;
newRiderStatus: string;
setNewRiderStatus: (v: string) => void;
newRiderMin: number;
setNewRiderMin: (n: number) => void;
newRiderMax: number;
setNewRiderMax: (n: number) => void;
newRiderMonthly: number;
setNewRiderMonthly: (n: number) => void;
newRiderDaily: number;
setNewRiderDaily: (n: number) => void;
newRiderDeposit: number;
setNewRiderDeposit: (n: number) => void;
newRiderCommission: number;
setNewRiderCommission: (n: number) => void;
newRiderTarget: number;
setNewRiderTarget: (n: number) => void;
newRiderHoliday: number;
setNewRiderHoliday: (n: number) => void;
newRiderDesc: string;
setNewRiderDesc: (v: string) => void;
createRiderPlan: () => void;
handleSave: () => void;
}
export default function RiderRequestSettings({
settings, setSettings,
activeRiderTab, setActiveRiderTab,
addRiderPlan, setAddRiderPlan,
newRiderName, setNewRiderName,
newRiderTier, setNewRiderTier,
newRiderStatus, setNewRiderStatus,
newRiderMin, setNewRiderMin,
newRiderMax, setNewRiderMax,
newRiderMonthly, setNewRiderMonthly,
newRiderDaily, setNewRiderDaily,
newRiderDeposit, setNewRiderDeposit,
newRiderCommission, setNewRiderCommission,
newRiderTarget, setNewRiderTarget,
newRiderHoliday, setNewRiderHoliday,
newRiderDesc, setNewRiderDesc,
createRiderPlan, handleSave,
}: RiderRequestSettingsProps) {
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-800">Rider Request Plans</h3>
</div>
<div className="flex items-center justify-between bg-rose-50 border border-rose-200 rounded-xl p-4">
<div>
<h4 className="font-semibold text-rose-800">Rider Request Plans ({settings.plans.riderRequest.length})</h4>
<p className="text-sm text-rose-600">Manage rider request plans for operators</p>
</div>
<button onClick={() => { setAddRiderPlan(true); setNewRiderName(''); }} className="px-4 py-2 bg-rose-600 text-white rounded-lg text-sm font-medium flex items-center gap-2">
<Plus className="w-4 h-4" /> New Plan
</button>
</div>
{addRiderPlan && (
<div className="bg-white rounded-xl border border-rose-300 overflow-hidden">
<div className="bg-rose-100 px-4 py-3 border-b border-rose-200 flex items-center justify-between">
<div>
<h4 className="font-semibold text-rose-800">New Rider Request Plan</h4>
<p className="text-sm text-rose-600 mt-1">Fill in the details below</p>
</div>
<button onClick={() => setAddRiderPlan(false)} className="text-rose-600 hover:text-rose-800">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4">
<div className="grid lg:grid-cols-3 gap-4">
<div>
<label className="text-sm text-slate-600">Plan Condition</label>
<input type="text" value={newRiderName} onChange={(e) => setNewRiderName(e.target.value)} placeholder="e.g., Premium Rider Plan" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Tier</label>
<select value={newRiderTier} onChange={(e) => setNewRiderTier(e.target.value)} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1">
<option value="Premium">Premium</option>
<option value="Standard">Standard</option>
<option value="Economy">Economy</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Status</label>
<select value={newRiderStatus} onChange={(e) => setNewRiderStatus(e.target.value)} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1">
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="closed">Closed</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Min Riders</label>
<input type="number" value={newRiderMin} onChange={(e) => setNewRiderMin(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Max Riders</label>
<input type="number" value={newRiderMax} onChange={(e) => setNewRiderMax(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Monthly Subscription ()</label>
<input type="number" value={newRiderMonthly} onChange={(e) => setNewRiderMonthly(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Daily Subscription ()</label>
<input type="number" value={newRiderDaily} onChange={(e) => setNewRiderDaily(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Deposit ()</label>
<input type="number" value={newRiderDeposit} onChange={(e) => setNewRiderDeposit(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Commission (%)</label>
<input type="number" value={newRiderCommission} onChange={(e) => setNewRiderCommission(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Daily Ride Target</label>
<input type="number" value={newRiderTarget} onChange={(e) => setNewRiderTarget(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Weekly Holiday</label>
<input type="number" value={newRiderHoliday} onChange={(e) => setNewRiderHoliday(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
</div>
<div className="mt-4">
<label className="text-sm text-slate-600">Description</label>
<textarea value={newRiderDesc} onChange={(e) => setNewRiderDesc(e.target.value)} placeholder="Enter plan description" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" rows={2} />
</div>
<button onClick={createRiderPlan} className="mt-4 px-4 py-2 bg-rose-600 text-white rounded-lg text-sm font-medium">Create Plan</button>
</div>
</div>
)}
<div className="flex gap-2 border-b border-slate-200">
{settings.plans.riderRequest.map((plan, idx) => (
<button key={idx} onClick={() => setActiveRiderTab(idx)} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activeRiderTab === idx ? 'border-rose-500 text-rose-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>{idx + 1}. {plan.name}</button>
))}
</div>
{settings.plans.riderRequest.length > 0 && settings.plans.riderRequest.map((plan, idx) => idx === activeRiderTab && (
<div key={idx} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="bg-rose-50 px-4 py-3 border-b border-rose-100 flex items-center justify-between">
<div>
<h4 className="font-semibold text-rose-800">{plan.name} - {plan.tier}</h4>
<p className="text-sm text-rose-600 mt-1">{plan.description}</p>
</div>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${plan.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>{plan.status}</span>
</div>
<div className="p-4">
<div className="grid lg:grid-cols-3 gap-4">
<div>
<label className="text-sm text-slate-600">Plan Condition</label>
<input type="text" value={plan.name} onChange={(e) => { const updated = [...settings.plans.riderRequest]; updated[idx].name = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, riderRequest: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Tier</label>
<select value={plan.tier} onChange={(e) => { const updated = [...settings.plans.riderRequest]; updated[idx].tier = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, riderRequest: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1">
<option value="Premium">Premium</option>
<option value="Standard">Standard</option>
<option value="Economy">Economy</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Status</label>
<select value={plan.status} onChange={(e) => { const updated = [...settings.plans.riderRequest]; updated[idx].status = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, riderRequest: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1">
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="closed">Closed</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Min Riders</label>
<input type="number" value={plan.minRiders} onChange={(e) => { const updated = [...settings.plans.riderRequest]; updated[idx].minRiders = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, riderRequest: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Max Riders</label>
<input type="number" value={plan.maxRiders} onChange={(e) => { const updated = [...settings.plans.riderRequest]; updated[idx].maxRiders = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, riderRequest: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Monthly Subscription ()</label>
<input type="number" value={plan.monthlySubscription} onChange={(e) => { const updated = [...settings.plans.riderRequest]; updated[idx].monthlySubscription = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, riderRequest: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Daily Subscription ()</label>
<input type="number" value={plan.dailySubscription} onChange={(e) => { const updated = [...settings.plans.riderRequest]; updated[idx].dailySubscription = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, riderRequest: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Deposit ()</label>
<input type="number" value={plan.deposit} onChange={(e) => { const updated = [...settings.plans.riderRequest]; updated[idx].deposit = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, riderRequest: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Commission (%)</label>
<input type="number" value={plan.commissionPercent} onChange={(e) => { const updated = [...settings.plans.riderRequest]; updated[idx].commissionPercent = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, riderRequest: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Daily Ride Target</label>
<input type="number" value={plan.dailyRideTarget} onChange={(e) => { const updated = [...settings.plans.riderRequest]; updated[idx].dailyRideTarget = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, riderRequest: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Weekly Holiday</label>
<input type="number" value={plan.weeklyHoliday} onChange={(e) => { const updated = [...settings.plans.riderRequest]; updated[idx].weeklyHoliday = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, riderRequest: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
</div>
<div className="mt-4">
<label className="text-sm text-slate-600">Description</label>
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.riderRequest]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, riderRequest: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" rows={2} />
</div>
<div className="mt-4 flex justify-end">
<button onClick={handleSave} className="px-4 py-2 bg-rose-600 text-white rounded-lg text-sm font-medium flex items-center gap-2">
<Save className="w-4 h-4" /> Save Changes
</button>
</div>
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,74 @@
'use client';
import { CompanySettings } from '../page';
interface SocialSettingsProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
}
export default function SocialSettings({ settings, setSettings }: SocialSettingsProps) {
return (
<div className="p-6 space-y-6">
<h3 className="text-lg font-semibold text-slate-800">Social Media Links</h3>
<div className="space-y-4">
<div className="flex items-center gap-3">
<span className="w-5 text-blue-600 font-bold text-sm">FB</span>
<label className="w-24 text-sm text-slate-600">Facebook</label>
<input
type="text"
value={settings.socialLinks.facebook}
onChange={(e) => setSettings({ ...settings, socialLinks: { ...settings.socialLinks, facebook: e.target.value } })}
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm"
placeholder="https://facebook.com/yourpage"
/>
</div>
<div className="flex items-center gap-3">
<span className="w-5 text-sky-500 font-bold text-sm">X</span>
<label className="w-24 text-sm text-slate-600">Twitter</label>
<input
type="text"
value={settings.socialLinks.twitter}
onChange={(e) => setSettings({ ...settings, socialLinks: { ...settings.socialLinks, twitter: e.target.value } })}
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm"
placeholder="https://twitter.com/yourpage"
/>
</div>
<div className="flex items-center gap-3">
<span className="w-5 text-pink-600 font-bold text-sm">IG</span>
<label className="w-24 text-sm text-slate-600">Instagram</label>
<input
type="text"
value={settings.socialLinks.instagram}
onChange={(e) => setSettings({ ...settings, socialLinks: { ...settings.socialLinks, instagram: e.target.value } })}
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm"
placeholder="https://instagram.com/yourpage"
/>
</div>
<div className="flex items-center gap-3">
<span className="w-5 text-blue-700 font-bold text-sm">IN</span>
<label className="w-24 text-sm text-slate-600">LinkedIn</label>
<input
type="text"
value={settings.socialLinks.linkedin}
onChange={(e) => setSettings({ ...settings, socialLinks: { ...settings.socialLinks, linkedin: e.target.value } })}
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm"
placeholder="https://linkedin.com/company/yourpage"
/>
</div>
<div className="flex items-center gap-3">
<span className="w-5 text-red-600 font-bold text-sm">YT</span>
<label className="w-24 text-sm text-slate-600">YouTube</label>
<input
type="text"
value={settings.socialLinks.youtube}
onChange={(e) => setSettings({ ...settings, socialLinks: { ...settings.socialLinks, youtube: e.target.value } })}
className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm"
placeholder="https://youtube.com/@yourchannel"
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,204 @@
'use client';
import { Plus, X, Save } from 'lucide-react';
import type { CompanySettings } from '../page';
interface SwapStationSettingsProps {
settings: CompanySettings;
setSettings: React.Dispatch<React.SetStateAction<CompanySettings>>;
activeSwapTab: number;
setActiveSwapTab: (n: number) => void;
addSwapStationPlan: boolean;
setAddSwapStationPlan: (v: boolean) => void;
newSwapName: string;
setNewSwapName: (v: string) => void;
newSwapStatus: string;
setNewSwapStatus: (v: string) => void;
newSwapBatteryCount: number;
setNewSwapBatteryCount: (n: number) => void;
newSwapPrice: number;
setNewSwapPrice: (n: number) => void;
newSwapMonthly: number;
setNewSwapMonthly: (n: number) => void;
newSwapDaily: number;
setNewSwapDaily: (n: number) => void;
newSwapMin: number;
setNewSwapMin: (n: number) => void;
newSwapMax: number;
setNewSwapMax: (n: number) => void;
newSwapProfit: number;
setNewSwapProfit: (n: number) => void;
newSwapDesc: string;
setNewSwapDesc: (v: string) => void;
createSwapStationPlan: () => void;
handleSave: () => void;
}
export default function SwapStationSettings({
settings, setSettings,
activeSwapTab, setActiveSwapTab,
addSwapStationPlan, setAddSwapStationPlan,
newSwapName, setNewSwapName,
newSwapStatus, setNewSwapStatus,
newSwapBatteryCount, setNewSwapBatteryCount,
newSwapPrice, setNewSwapPrice,
newSwapMonthly, setNewSwapMonthly,
newSwapDaily, setNewSwapDaily,
newSwapMin, setNewSwapMin,
newSwapMax, setNewSwapMax,
newSwapProfit, setNewSwapProfit,
newSwapDesc, setNewSwapDesc,
createSwapStationPlan, handleSave,
}: SwapStationSettingsProps) {
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-800">Swap Station Plans</h3>
</div>
<div className="flex items-center justify-between bg-blue-50 border border-blue-200 rounded-xl p-4">
<div>
<h4 className="font-semibold text-blue-800">Swap Station Plans ({settings.plans.swapStation.length})</h4>
<p className="text-sm text-blue-600">Manage swap station plans for operators</p>
</div>
<button onClick={() => { setAddSwapStationPlan(true); setNewSwapName(''); }} className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium flex items-center gap-2">
<Plus className="w-4 h-4" /> New Plan
</button>
</div>
{addSwapStationPlan && (
<div className="bg-white rounded-xl border border-blue-300 overflow-hidden">
<div className="bg-blue-100 px-4 py-3 border-b border-blue-200 flex items-center justify-between">
<div>
<h4 className="font-semibold text-blue-800">New Swap Station Plan</h4>
<p className="text-sm text-blue-600 mt-1">Fill in the details below</p>
</div>
<button onClick={() => setAddSwapStationPlan(false)} className="text-blue-600 hover:text-blue-800">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4">
<div className="grid lg:grid-cols-3 gap-4">
<div>
<label className="text-sm text-slate-600">Plan Condition</label>
<input type="text" value={newSwapName} onChange={(e) => setNewSwapName(e.target.value)} placeholder="e.g., Standard Station" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Status</label>
<select value={newSwapStatus} onChange={(e) => setNewSwapStatus(e.target.value)} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1">
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="closed">Closed</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Battery Count</label>
<input type="number" value={newSwapBatteryCount} onChange={(e) => setNewSwapBatteryCount(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Swap Price ()</label>
<input type="number" value={newSwapPrice} onChange={(e) => setNewSwapPrice(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Monthly Subscription ()</label>
<input type="number" value={newSwapMonthly} onChange={(e) => setNewSwapMonthly(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Daily Subscription ()</label>
<input type="number" value={newSwapDaily} onChange={(e) => setNewSwapDaily(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Min Batteries</label>
<input type="number" value={newSwapMin} onChange={(e) => setNewSwapMin(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Max Batteries</label>
<input type="number" value={newSwapMax} onChange={(e) => setNewSwapMax(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Profit Share (%)</label>
<input type="number" value={newSwapProfit} onChange={(e) => setNewSwapProfit(parseInt(e.target.value))} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
</div>
<div className="mt-4">
<label className="text-sm text-slate-600">Description</label>
<textarea value={newSwapDesc} onChange={(e) => setNewSwapDesc(e.target.value)} placeholder="Enter plan description" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" rows={2} />
</div>
<button onClick={createSwapStationPlan} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium">Create Plan</button>
</div>
</div>
)}
<div className="flex gap-2 border-b border-slate-200">
{settings.plans.swapStation.map((plan, idx) => (
<button key={idx} onClick={() => setActiveSwapTab(idx)} className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${activeSwapTab === idx ? 'border-blue-500 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}> {plan.name}</button>
))}
</div>
{settings.plans.swapStation.length > 0 && settings.plans.swapStation.map((plan, idx) => idx === activeSwapTab && (
<div key={idx} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="bg-blue-50 px-4 py-3 border-b border-blue-100 flex items-center justify-between">
<div>
<h4 className="font-semibold text-blue-800">{plan.name}</h4>
<p className="text-sm text-blue-600 mt-1">{plan.description}</p>
</div>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${plan.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>{plan.status}</span>
</div>
<div className="p-4">
<div className="grid lg:grid-cols-3 gap-4">
<div>
<label className="text-sm text-slate-600">Plan Condition</label>
<input type="text" value={plan.name} onChange={(e) => { const updated = [...settings.plans.swapStation]; updated[idx].name = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, swapStation: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Status</label>
<select value={plan.status} onChange={(e) => { const updated = [...settings.plans.swapStation]; updated[idx].status = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, swapStation: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1">
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="closed">Closed</option>
</select>
</div>
<div>
<label className="text-sm text-slate-600">Battery Count</label>
<input type="number" value={plan.batteryCount} onChange={(e) => { const updated = [...settings.plans.swapStation]; updated[idx].batteryCount = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, swapStation: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Swap Price ()</label>
<input type="number" value={plan.swapPrice} onChange={(e) => { const updated = [...settings.plans.swapStation]; updated[idx].swapPrice = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, swapStation: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Monthly Subscription ()</label>
<input type="number" value={plan.monthlySubscription} onChange={(e) => { const updated = [...settings.plans.swapStation]; updated[idx].monthlySubscription = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, swapStation: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Daily Subscription ()</label>
<input type="number" value={plan.dailySubscription} onChange={(e) => { const updated = [...settings.plans.swapStation]; updated[idx].dailySubscription = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, swapStation: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Min Batteries</label>
<input type="number" value={plan.minBatteries} onChange={(e) => { const updated = [...settings.plans.swapStation]; updated[idx].minBatteries = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, swapStation: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Max Batteries</label>
<input type="number" value={plan.maxBatteries} onChange={(e) => { const updated = [...settings.plans.swapStation]; updated[idx].maxBatteries = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, swapStation: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
<div>
<label className="text-sm text-slate-600">Profit Share (%)</label>
<input type="number" value={plan.profitSharePercent} onChange={(e) => { const updated = [...settings.plans.swapStation]; updated[idx].profitSharePercent = parseInt(e.target.value); setSettings({ ...settings, plans: { ...settings.plans, swapStation: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" />
</div>
</div>
<div className="mt-4">
<label className="text-sm text-slate-600">Description</label>
<textarea value={plan.description} onChange={(e) => { const updated = [...settings.plans.swapStation]; updated[idx].description = e.target.value; setSettings({ ...settings, plans: { ...settings.plans, swapStation: updated } }); }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" rows={2} />
</div>
<div className="mt-4 flex justify-end">
<button onClick={handleSave} className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium flex items-center gap-2">
<Save className="w-4 h-4" /> Save Changes
</button>
</div>
</div>
</div>
))}
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ import { useState, use } from 'react';
import { Users, ArrowLeft, Mail, Phone, MapPin, Shield, Edit, Trash2, KeyRound, Eye, EyeOff, Wallet, Bike, Zap, Store, CheckCircle, XCircle, Clock, Link2 } from 'lucide-react'; import { Users, ArrowLeft, Mail, Phone, MapPin, Shield, Edit, Trash2, KeyRound, Eye, EyeOff, Wallet, Bike, Zap, Store, CheckCircle, XCircle, Clock, Link2 } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
type UserRole = 'admin' | 'manager' | 'biker' | 'investor' | 'shop' | 'merchant'; type UserRole = 'admin' | 'manager' | 'biker' | 'investor' | 'swapstation' | 'merchant';
type UserStatus = 'active' | 'inactive' | 'suspended'; type UserStatus = 'active' | 'inactive' | 'suspended';
interface User { interface User {
@@ -18,7 +18,7 @@ interface User {
lastLogin: string; lastLogin: string;
createdAt: string; createdAt: string;
linkedProfile?: { linkedProfile?: {
type: 'biker' | 'investor' | 'shop' | 'merchant' | 'swap-station'; type: 'biker' | 'investor' | 'swapstation' | 'merchant';
id: string; id: string;
name: string; name: string;
}; };
@@ -40,7 +40,7 @@ const mockUser: User = {
const mockUsers: User[] = [ const mockUsers: User[] = [
{ id: 'USR-003', name: 'Jamal Biker', email: 'jamal@biker.com', phone: '+8801912345678', role: 'biker', status: 'active', password: '********', lastLogin: '2024-02-15 18:00', createdAt: '2023-09-20', linkedProfile: { type: 'biker', id: 'BIK-001', name: 'Jamal Delivery' } }, { id: 'USR-003', name: 'Jamal Biker', email: 'jamal@biker.com', phone: '+8801912345678', role: 'biker', status: 'active', password: '********', lastLogin: '2024-02-15 18:00', createdAt: '2023-09-20', linkedProfile: { type: 'biker', id: 'BIK-001', name: 'Jamal Delivery' } },
{ id: 'USR-004', name: 'Rahim Investor', email: 'rahim@investor.com', phone: '+8801512345678', role: 'investor', status: 'active', password: '********', lastLogin: '2024-02-14 12:00', createdAt: '2023-03-10', linkedProfile: { type: 'investor', id: 'INV-001', name: 'Rahim Investments' } }, { id: 'USR-004', name: 'Rahim Investor', email: 'rahim@investor.com', phone: '+8801512345678', role: 'investor', status: 'active', password: '********', lastLogin: '2024-02-14 12:00', createdAt: '2023-03-10', linkedProfile: { type: 'investor', id: 'INV-001', name: 'Rahim Investments' } },
{ id: 'USR-005', name: 'Shop Owner', email: 'shop@owner.com', phone: '+8801612345678', role: 'shop', status: 'active', password: '********', lastLogin: '2024-02-15 14:30', createdAt: '2023-11-05', linkedProfile: { type: 'shop', id: 'SHP-001', name: 'Jaiben Shop' } }, { id: 'USR-005', name: 'Swap Station Owner', email: 'swapstation@owner.com', phone: '+8801612345678', role: 'swapstation', status: 'active', password: '********', lastLogin: '2024-02-15 14:30', createdAt: '2023-11-05', linkedProfile: { type: 'swapstation', id: 'SHP-001', name: 'Jaiben Swap Station' } },
{ id: 'USR-006', name: 'Merchant User', email: 'merchant@jaiben.com', phone: '+8801412345678', role: 'merchant', status: 'active', password: '********', lastLogin: '2024-02-15 16:00', createdAt: '2023-08-15', linkedProfile: { type: 'merchant', id: 'MCH-001', name: 'Rahim Delivery Service' } }, { id: 'USR-006', name: 'Merchant User', email: 'merchant@jaiben.com', phone: '+8801412345678', role: 'merchant', status: 'active', password: '********', lastLogin: '2024-02-15 16:00', createdAt: '2023-08-15', linkedProfile: { type: 'merchant', id: 'MCH-001', name: 'Rahim Delivery Service' } },
]; ];
@@ -49,7 +49,7 @@ const roleColors: Record<UserRole, string> = {
manager: 'bg-purple-100 text-purple-700', manager: 'bg-purple-100 text-purple-700',
biker: 'bg-blue-100 text-blue-700', biker: 'bg-blue-100 text-blue-700',
investor: 'bg-green-100 text-green-700', investor: 'bg-green-100 text-green-700',
shop: 'bg-amber-100 text-amber-700', swapstation: 'bg-amber-100 text-amber-700',
merchant: 'bg-cyan-100 text-cyan-700' merchant: 'bg-cyan-100 text-cyan-700'
}; };
@@ -64,7 +64,7 @@ const roleLabels: Record<UserRole, string> = {
manager: 'Manager', manager: 'Manager',
biker: 'Biker', biker: 'Biker',
investor: 'Investor', investor: 'Investor',
shop: 'Shop', swapstation: 'Swap Station',
merchant: 'Merchant' merchant: 'Merchant'
}; };
@@ -282,7 +282,7 @@ export default function UserDetailPage({ params }: { params: Promise<{ id: strin
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{user.linkedProfile.type === 'biker' && <Bike className="w-5 h-5 text-blue-500" />} {user.linkedProfile.type === 'biker' && <Bike className="w-5 h-5 text-blue-500" />}
{user.linkedProfile.type === 'investor' && <Wallet className="w-5 h-5 text-green-500" />} {user.linkedProfile.type === 'investor' && <Wallet className="w-5 h-5 text-green-500" />}
{user.linkedProfile.type === 'shop' && <Store className="w-5 h-5 text-amber-500" />} {user.linkedProfile.type === 'swapstation' && <Store className="w-5 h-5 text-amber-500" />}
{user.linkedProfile.type === 'merchant' && <Zap className="w-5 h-5 text-cyan-500" />} {user.linkedProfile.type === 'merchant' && <Zap className="w-5 h-5 text-cyan-500" />}
<div> <div>
<p className="text-sm font-medium text-slate-700">{user.linkedProfile.name}</p> <p className="text-sm font-medium text-slate-700">{user.linkedProfile.name}</p>

View File

@@ -4,7 +4,7 @@ import { useState } from 'react';
import { Users, Plus, Search, X, Eye, Edit, Trash2, Shield, Mail, Phone, MapPin, CheckCircle, XCircle } from 'lucide-react'; import { Users, Plus, Search, X, Eye, Edit, Trash2, Shield, Mail, Phone, MapPin, CheckCircle, XCircle } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
type UserRole = 'admin' | 'manager' | 'biker' | 'investor' | 'shop' | 'merchant'; type UserRole = 'admin' | 'manager' | 'biker' | 'investor' | 'swapstation' | 'merchant';
type UserStatus = 'active' | 'inactive' | 'suspended'; type UserStatus = 'active' | 'inactive' | 'suspended';
interface User { interface User {
@@ -18,7 +18,7 @@ interface User {
lastLogin: string; lastLogin: string;
createdAt: string; createdAt: string;
linkedProfile?: { linkedProfile?: {
type: 'biker' | 'investor' | 'shop' | 'merchant' | 'swap-station'; type: 'biker' | 'investor' | 'swapstation' | 'merchant';
id: string; id: string;
name: string; name: string;
}; };
@@ -29,7 +29,7 @@ const mockUsers: User[] = [
{ id: 'USR-002', name: 'Karim Manager', email: 'karim@jaiben.com', phone: '+8801812345678', role: 'manager', status: 'active', lastLogin: '2024-02-16 09:15', createdAt: '2023-06-15' }, { id: 'USR-002', name: 'Karim Manager', email: 'karim@jaiben.com', phone: '+8801812345678', role: 'manager', status: 'active', lastLogin: '2024-02-16 09:15', createdAt: '2023-06-15' },
{ id: 'USR-003', name: 'Jamal Biker', email: 'jamal@biker.com', phone: '+8801912345678', role: 'biker', status: 'active', lastLogin: '2024-02-15 18:00', createdAt: '2023-09-20', linkedProfile: { type: 'biker', id: 'BIK-001', name: 'Jamal Delivery' } }, { id: 'USR-003', name: 'Jamal Biker', email: 'jamal@biker.com', phone: '+8801912345678', role: 'biker', status: 'active', lastLogin: '2024-02-15 18:00', createdAt: '2023-09-20', linkedProfile: { type: 'biker', id: 'BIK-001', name: 'Jamal Delivery' } },
{ id: 'USR-004', name: 'Rahim Investor', email: 'rahim@investor.com', phone: '+8801512345678', role: 'investor', status: 'active', lastLogin: '2024-02-14 12:00', createdAt: '2023-03-10', linkedProfile: { type: 'investor', id: 'INV-001', name: 'Rahim Investments' } }, { id: 'USR-004', name: 'Rahim Investor', email: 'rahim@investor.com', phone: '+8801512345678', role: 'investor', status: 'active', lastLogin: '2024-02-14 12:00', createdAt: '2023-03-10', linkedProfile: { type: 'investor', id: 'INV-001', name: 'Rahim Investments' } },
{ id: 'USR-005', name: 'Shop Owner', email: 'shop@owner.com', phone: '+8801612345678', role: 'shop', status: 'active', lastLogin: '2024-02-15 14:30', createdAt: '2023-11-05', linkedProfile: { type: 'shop', id: 'SHP-001', name: 'Jaiben Shop' } }, { id: 'USR-005', name: 'Swap Station Owner', email: 'swapstation@owner.com', phone: '+8801612345678', role: 'swapstation', status: 'active', lastLogin: '2024-02-15 14:30', createdAt: '2023-11-05', linkedProfile: { type: 'swapstation', id: 'SHP-001', name: 'Jaiben Swap Station' } },
{ id: 'USR-006', name: 'Merchant User', email: 'merchant@jaiben.com', phone: '+8801412345678', role: 'merchant', status: 'active', lastLogin: '2024-02-15 16:00', createdAt: '2023-08-15', linkedProfile: { type: 'merchant', id: 'MCH-001', name: 'Rahim Delivery Service' } }, { id: 'USR-006', name: 'Merchant User', email: 'merchant@jaiben.com', phone: '+8801412345678', role: 'merchant', status: 'active', lastLogin: '2024-02-15 16:00', createdAt: '2023-08-15', linkedProfile: { type: 'merchant', id: 'MCH-001', name: 'Rahim Delivery Service' } },
]; ];
@@ -38,7 +38,7 @@ const roleColors: Record<UserRole, string> = {
manager: 'bg-purple-100 text-purple-700', manager: 'bg-purple-100 text-purple-700',
biker: 'bg-blue-100 text-blue-700', biker: 'bg-blue-100 text-blue-700',
investor: 'bg-green-100 text-green-700', investor: 'bg-green-100 text-green-700',
shop: 'bg-amber-100 text-amber-700', swapstation: 'bg-amber-100 text-amber-700',
merchant: 'bg-cyan-100 text-cyan-700' merchant: 'bg-cyan-100 text-cyan-700'
}; };
@@ -53,7 +53,7 @@ const roleLabels: Record<UserRole, string> = {
manager: 'Manager', manager: 'Manager',
biker: 'Biker', biker: 'Biker',
investor: 'Investor', investor: 'Investor',
shop: 'Shop', swapstation: 'Swap Station',
merchant: 'Merchant' merchant: 'Merchant'
}; };
@@ -195,7 +195,7 @@ export default function UsersPage() {
<option value="manager">Manager</option> <option value="manager">Manager</option>
<option value="biker">Biker</option> <option value="biker">Biker</option>
<option value="investor">Investor</option> <option value="investor">Investor</option>
<option value="shop">Shop</option> <option value="swapstation">Swap Station</option>
<option value="merchant">Merchant</option> <option value="merchant">Merchant</option>
</select> </select>
<select <select
@@ -331,7 +331,7 @@ export default function UsersPage() {
<option value="manager">Manager</option> <option value="manager">Manager</option>
<option value="biker">Biker</option> <option value="biker">Biker</option>
<option value="investor">Investor</option> <option value="investor">Investor</option>
<option value="shop">Shop</option> <option value="swapstation">Swap Station</option>
<option value="merchant">Merchant</option> <option value="merchant">Merchant</option>
</select> </select>
</div> </div>

View File

@@ -0,0 +1,249 @@
'use client';
import { useState, useEffect } from 'react';
import { investors, bikes, transactions } from '@/data/mockData';
import { Wallet, TrendingUp, Bike, Target, DollarSign, FileText, Phone, BarChart3, Clock, ArrowRight, ShieldCheck, Zap, AlertCircle, Download, X, ExternalLink } from 'lucide-react';
import Link from 'next/link';
import TransactionList from '@/components/TransactionList';
import InvestorNotification from '@/components/InvestorNotification';
export default function InvestorDashboardPage() {
const investor = investors[0];
const investorBikes = bikes.filter(b => b.investorId === investor?.id);
const recentTransactions = transactions.filter(t => t.investorId === investor.id).slice(0, 5);
const availableBalance = investor.totalEarnings - investor.totalWithdrawn - investor.withdrawalPending;
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
const [showInstallBanner, setShowInstallBanner] = useState(false);
useEffect(() => {
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e);
setShowInstallBanner(true);
};
window.addEventListener('beforeinstallprompt', handler);
return () => window.removeEventListener('beforeinstallprompt', handler);
}, []);
const handleInstall = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
setDeferredPrompt(null);
}
setShowInstallBanner(false);
};
const dismissBanner = () => {
setShowInstallBanner(false);
localStorage.setItem('pwa_install_dismissed', 'true');
};
useEffect(() => {
if (localStorage.getItem('pwa_install_dismissed') === 'true') {
setShowInstallBanner(false);
}
}, []);
return (
<div className="min-h-screen lg:pt-6 pt-0">
<InvestorNotification isMobile />
<div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 mb-6">
<div>
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">Welcome back, {investor.name.split(' ')[0]} 👋</h1>
<p className="text-sm text-slate-500">Here&apos;s what&apos;s happening with your investments today.</p>
</div>
<div className="flex items-center gap-2">
{investor.kycStatus === 'verified' ? (
<span className="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg bg-green-50 border border-green-100 text-green-700 text-xs font-bold">
<ShieldCheck className="w-4 h-4" /> KYC Verified
</span>
) : (
<Link href="/investor/profile" className="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg bg-amber-50 border border-amber-200 text-amber-700 text-xs font-bold hover:bg-amber-100 transition-colors">
<AlertCircle className="w-4 h-4" /> Complete KYC
</Link>
)}
{/* <Link href="/investor/plans" className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-bold hover:bg-investor-dark transition-colors flex items-center gap-2 shadow-sm shadow-investor/20">
<Zap className="w-4 h-4" /> New Investment
</Link> */}
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden group hover:border-investor/30 transition-all">
<div className="absolute -right-4 -top-4 w-16 h-16 bg-purple-50 rounded-full group-hover:scale-150 transition-transform duration-500"></div>
<div className="flex items-center gap-3 mb-2 relative z-10">
<div className="w-10 h-10 bg-purple-100 rounded-xl flex items-center justify-center">
<Wallet className="w-5 h-5 text-purple-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Total Invested</p>
</div>
<p className="text-2xl font-extrabold text-slate-800 relative z-10">{(investor.totalInvested / 1000).toFixed(0)}k</p>
</div>
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden group hover:border-green-500/30 transition-all">
<div className="absolute -right-4 -top-4 w-16 h-16 bg-green-50 rounded-full group-hover:scale-150 transition-transform duration-500"></div>
<div className="flex items-center gap-3 mb-2 relative z-10">
<div className="w-10 h-10 bg-green-100 rounded-xl flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-green-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Total Earnings</p>
</div>
<p className="text-2xl font-extrabold text-green-600 relative z-10">{(investor.totalEarnings / 1000).toFixed(1)}k</p>
</div>
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden group hover:border-amber-500/30 transition-all">
<div className="absolute -right-4 -top-4 w-16 h-16 bg-amber-50 rounded-full group-hover:scale-150 transition-transform duration-500"></div>
<div className="flex items-center gap-3 mb-2 relative z-10">
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center">
<Bike className="w-5 h-5 text-amber-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Active Bikes</p>
</div>
<p className="text-2xl font-extrabold text-amber-600 relative z-10">{investor.activeBikes}</p>
</div>
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm relative overflow-hidden group hover:border-blue-500/30 transition-all">
<div className="absolute -right-4 -top-4 w-16 h-16 bg-blue-50 rounded-full group-hover:scale-150 transition-transform duration-500"></div>
<div className="flex items-center gap-3 mb-2 relative z-10">
<div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center">
<Target className="w-5 h-5 text-blue-600" />
</div>
<p className="text-sm text-slate-500 font-medium">Avg. ROI</p>
</div>
<p className="text-2xl font-extrabold text-blue-600 relative z-10">{investor.roi}%</p>
</div>
</div>
{/* PWA Install Banner */}
{showInstallBanner && (
<div className="bg-gradient-to-r from-investor/10 to-purple-50 border border-investor/20 rounded-2xl p-4 mb-6 animate-in fade-in slide-in-from-bottom-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-investor to-investor-light rounded-xl flex items-center justify-center shrink-0 shadow-lg">
<Download className="w-6 h-6 text-white" />
</div>
<div className="flex-1 min-w-0">
<p className="font-bold text-slate-800">Install JML Investor App</p>
<p className="text-xs text-slate-500 mt-0.5">Add to home screen for quick access & offline support</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button onClick={dismissBanner} className="p-2 hover:bg-slate-100 rounded-lg text-slate-400 transition-colors">
<X className="w-5 h-5" />
</button>
<button onClick={handleInstall} className="px-4 py-2 bg-investor text-white rounded-lg font-bold text-sm hover:bg-investor-dark transition-colors flex items-center gap-2 shadow-sm">
<ExternalLink className="w-4 h-4" /> Install
</button>
</div>
</div>
</div>
)}
<div className="grid lg:grid-cols-3 gap-6 mb-6">
<div className="lg:col-span-2 bg-white rounded-2xl border border-slate-200 shadow-sm flex flex-col">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="font-bold text-slate-800 flex items-center gap-2">
<Bike className="w-5 h-5 text-slate-400" /> My Portfolio Overview
</h2>
<Link href="/investor/plans" className="text-sm font-semibold text-investor hover:text-investor-dark flex items-center gap-1">
View All <ArrowRight className="w-4 h-4" />
</Link>
</div>
<div className="p-5 flex-1">
{investorBikes.length > 0 ? (
<div className="space-y-4">
{investorBikes.slice(0, 3).map(bike => (
<Link
key={bike.id}
href={`/investor/investments/${investor.investments?.[0].id || 'ip1'}`}
className="flex items-center gap-4 p-3 hover:bg-slate-50 rounded-xl transition-colors border border-transparent hover:border-slate-100 group"
>
<div className="w-12 h-12 bg-slate-100 rounded-lg overflow-hidden shrink-0">
<img src={bike.image} alt={bike.model} className="w-full h-full object-cover" />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-bold text-slate-800 truncate group-hover:text-investor transition-colors">{bike.model}</h4>
<p className="text-xs text-slate-500">{bike.plateNumber}</p>
</div>
<div className="text-right">
<p className="font-bold text-green-600">{bike.currentRent || 0}</p>
<p className="text-[10px] text-slate-400 uppercase">Daily Rent</p>
</div>
<div>
<span className={`inline-flex px-2 py-1 rounded text-[10px] font-bold uppercase ${bike.status === 'rented' ? 'bg-green-100 text-green-700' :
bike.status === 'available' ? 'bg-blue-100 text-blue-700' : 'bg-amber-100 text-amber-700'
}`}>
{bike.status}
</span>
</div>
</Link>
))}
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-center py-8">
<div className="w-12 h-12 bg-slate-50 rounded-full flex items-center justify-center mb-3">
<Bike className="w-6 h-6 text-slate-300" />
</div>
<p className="text-sm font-semibold text-slate-700">No bikes assigned yet</p>
<p className="text-xs text-slate-500 mt-1 max-w-[200px]">Once you make an investment, assigned bikes will appear here.</p>
</div>
)}
</div>
</div>
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm flex flex-col">
<div className="p-5 border-b border-slate-100">
<h2 className="font-bold text-slate-800">Quick Actions</h2>
</div>
<div className="p-5 flex-1 flex flex-col gap-3">
<div className="bg-investor/5 border border-investor/10 rounded-xl p-4 mb-2">
<p className="text-xs text-investor font-semibold mb-1 uppercase">Available to Withdraw</p>
<p className="text-2xl font-extrabold text-investor mb-3">{availableBalance.toLocaleString()}</p>
<Link href="/investor/withdraw" className="w-full py-2.5 bg-investor text-white rounded-lg font-bold text-sm hover:bg-investor-dark flex items-center justify-center gap-2 shadow-sm shadow-investor/20 transition-all">
<DollarSign className="w-4 h-4" /> Withdraw Funds
</Link>
</div>
<Link href="/investor/profile" className="flex items-center gap-3 p-3 border border-slate-100 rounded-xl hover:bg-slate-50 transition-colors group">
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center group-hover:bg-white group-hover:shadow-sm transition-all">
<FileText className="w-5 h-5 text-slate-500 group-hover:text-investor" />
</div>
<div className="flex-1">
<p className="font-semibold text-sm text-slate-800">Update KYC</p>
<p className="text-xs text-slate-500">Manage documents</p>
</div>
</Link>
<Link href="/investor/plans" className="flex items-center gap-3 p-3 border border-slate-100 rounded-xl hover:bg-slate-50 transition-colors group">
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center group-hover:bg-white group-hover:shadow-sm transition-all">
<BarChart3 className="w-5 h-5 text-slate-500 group-hover:text-investor" />
</div>
<div className="flex-1">
<p className="font-semibold text-sm text-slate-800">Earnings & P&L</p>
<p className="text-xs text-slate-500">View detailed reports</p>
</div>
</Link>
</div>
</div>
</div>
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h2 className="font-bold text-slate-800 flex items-center gap-2">
<Clock className="w-5 h-5 text-slate-400" /> Recent Transactions
</h2>
<button className="text-sm font-semibold text-investor hover:text-investor-dark flex items-center gap-1">
View All <ArrowRight className="w-4 h-4" />
</button>
</div>
<div className="p-0">
<TransactionList transactions={recentTransactions} />
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,593 @@
'use client';
import { useState, use } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import {
ArrowLeft, TrendingUp, Bike, DollarSign, Calendar,
CreditCard, FileText, Download, Check, X,
Printer, BarChart3, Wallet, Clock, Shield,
Receipt, Zap, Smartphone, ChevronRight, Target, Battery, MapPin, Gauge, Building2
} from 'lucide-react';
import { investors } from '@/data/mockData';
import toast from 'react-hot-toast';
import InvestorNotification from '@/components/InvestorNotification';
interface PaymentRecord {
id: string;
date: string;
amount: number;
installmentNo: number | null;
type: 'full' | 'partial' | 'installment';
method: string;
status: 'completed' | 'pending';
}
export default function InvestorInvestmentDetailPage({ params }: { params: Promise<{ id: string }> }) {
const resolvedParams = use(params);
const { id: investmentId } = resolvedParams;
const router = useRouter();
const investor = investors[0];
const investment = investor.investments?.find((inv: any) => inv.id === investmentId);
const [activeTab, setActiveTab] = useState('overview');
const [showPaymentModal, setShowPaymentModal] = useState(false);
if (!investment) {
return (
<div className="min-h-screen pb-20 lg:pb-0">
<InvestorNotification isMobile />
<div className="pt-18 lg:pt-0 p-4 lg:p-6">
<div className="text-center py-12">
<h2 className="text-xl font-bold text-slate-800">Investment Not Found</h2>
<p className="text-sm text-slate-500 mt-2">The investment you're looking for doesn't exist.</p>
<Link href="/investor/plans" className="mt-4 inline-flex items-center gap-2 text-investor hover:underline">
<ArrowLeft className="w-4 h-4" /> Back to Investments
</Link>
</div>
</div>
</div>
);
}
const isBattery = investment.assetType === 'battery' || investment.planName?.toLowerCase().includes('battery');
const paymentHistory: PaymentRecord[] = [
{
id: `pay_${investment.id}`,
date: investment.startDate || '2024-02-01',
amount: investment.totalInvestment,
installmentNo: null,
type: 'full',
method: investment.paymentMethod === 'bank' ? 'Bank Transfer' : 'bKash',
status: 'completed'
}
];
const totalPaid = paymentHistory.reduce((sum, p) => p.status === 'completed' ? sum + p.amount : sum, 0);
const dueAmount = investment.totalInvestment - totalPaid;
const planConfig: Record<string, { badge: string }> = {
silver: { badge: 'bg-slate-200 text-slate-700' },
gold: { badge: 'bg-amber-100 text-amber-700' },
platinum: { badge: 'bg-purple-100 text-purple-700' },
diamond: { badge: 'bg-blue-100 text-blue-700' },
};
const style = planConfig[investment.planType] || planConfig.gold;
const bikeIds = investment.bikeIds || (investmentId === 'ip1' ? ['b1'] : investmentId === 'ip2' ? ['b2'] : []);
const batteryIds = investment.batteryIds || (isBattery ? ['BAT-001', 'BAT-002', 'BAT-005'] : []);
const assignedBikes = [
{ id: 'b1', model: 'Etron ET50', brand: 'Etron', plateNumber: 'Dhaka Metro Cha-1234', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop', status: 'rented', currentRent: 350, totalEarnings: 114250, batteryLevel: 78, range: 60, location: 'Gulshan 1' },
{ id: 'b2', model: 'Yadea DT3', brand: 'Yadea', plateNumber: 'Dhaka Metro Cha-5678', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop', status: 'available', currentRent: 300, totalEarnings: 12750, batteryLevel: 95, range: 75, location: 'Banani' }
].filter(bike => bikeIds.includes(bike.id));
const assignedBatteries = [
{ id: 'BAT-001', serialNumber: 'SN-2024-00001', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', status: 'In-Use', cycleCount: 156, soc: '78% / 95%', earnings: 4500, batteryLevel: 78, location: 'Dhaka Central Hub' },
{ id: 'BAT-002', serialNumber: 'SN-2024-00002', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', status: 'In-Use', cycleCount: 142, soc: '82% / 96%', earnings: 4500, batteryLevel: 82, location: 'Dhaka Central Hub' },
{ id: 'BAT-005', serialNumber: 'SN-2024-00005', brand: 'EVE Energy', model: 'Li-Ion 60V50Ah', status: 'Available', cycleCount: 88, soc: '91% / 98%', earnings: 0, batteryLevel: 91, location: 'Dhaka Central Hub' }
].filter(bat => batteryIds.includes(bat.id));
const demoPnl = isBattery ? { grossRevenue: 22500, platformFee: 13500, insurance: 0, maintenance: 0, netProfit: 9000 } : { grossRevenue: 185000, platformFee: 83250, insurance: 15000, maintenance: 8500, netProfit: 78250 };
const demoTransactions = isBattery ? [
{ id: 'tx-bat-1', date: '2024-03-01', description: 'Monthly Yield Share - BAT-001', amount: 4500, status: 'completed' },
{ id: 'tx-bat-2', date: '2024-02-15', description: 'Monthly Yield Share - BAT-002', amount: 4500, status: 'completed' },
{ id: 'tx-bat-funded', date: investment.startDate || '2024-02-01', description: 'Investment Funded - Standard Battery Plan', amount: investment.totalInvestment, status: 'completed' }
] : [
{ id: 'tx1', date: '2024-05-15', description: 'Rental Income - Bike Dhaka Metro Cha-1234', amount: 350, status: 'completed' },
{ id: 'tx2', date: '2024-05-14', description: 'Rental Income - Bike Dhaka Metro Cha-5678', amount: 300, status: 'completed' },
{ id: 'tx3', date: '2024-05-13', description: 'Rental Income - Bike Dhaka Metro Cha-1234', amount: 350, status: 'completed' },
{ id: 'tx4', date: '2024-05-12', description: 'Rental Income - Bike Dhaka Metro Cha-5678', amount: 300, status: 'completed' },
{ id: 'tx-funded', date: investment.startDate || '2024-01-15', description: `Investment Funded - ${investment.planName}`, amount: investment.totalInvestment, status: 'completed' }
];
const handlePaymentSubmit = () => {
const amount = dueAmount;
toast.success(`Payment of ৳${amount.toLocaleString()} submitted successfully!`);
setShowPaymentModal(false);
};
return (
<div className="min-h-screen lg:pt-6 pt-0">
<InvestorNotification isMobile />
<div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
<div className="mb-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-3">
<button onClick={() => router.back()} className="p-2 hover:bg-slate-100 rounded-lg transition-colors border border-slate-200">
<ArrowLeft className="w-5 h-5 text-slate-600" />
</button>
<div>
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<h1 className="text-xl lg:text-2xl font-bold text-slate-800 flex items-center gap-2">
<Target className="w-5 h-5 lg:w-6 lg:h-6 text-investor" />
{investment.planName}
</h1>
<div className="flex flex-wrap items-center gap-2">
<span
className={`px-2.5 py-1 rounded-full text-xs font-bold ${style.badge} capitalize`}
>
{investment.planType}
</span>
<span
className={`px-2.5 py-1 rounded-full text-xs font-bold ${investment.status === 'active'
? 'bg-green-100 text-green-700'
: 'bg-slate-100 text-slate-600'
} capitalize`}
>
{investment.status}
</span>
</div>
</div>
<p className="text-sm text-slate-500 mt-1">ID: #{investment.id?.toUpperCase()} Started: {investment.startDate}</p>
</div>
</div>
<button onClick={() => toast.success('Download started')} className="px-4 py-2.5 bg-white border border-slate-200 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-50 flex justify-center lg:justify-start items-center gap-2 shadow-sm">
<Download className="w-4 h-4" /> Download
</button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 lg:gap-4 mb-6">
<div className="bg-white rounded-xl p-4 lg:p-5 border border-slate-200 shadow-sm">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 bg-purple-100 rounded-lg flex items-center justify-center">
<Wallet className="w-4 h-4 text-purple-600" />
</div>
<p className="text-xs text-slate-500 font-medium">Total Investment</p>
</div>
<p className="text-xl lg:text-2xl font-bold text-slate-800">{investment.totalInvestment.toLocaleString()}</p>
</div>
<div className="bg-white rounded-xl p-4 lg:p-5 border border-slate-200 shadow-sm bg-green-50/10">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
<Check className="w-4 h-4 text-green-600" />
</div>
<p className="text-xs text-slate-500 font-medium">Already Paid</p>
</div>
<p className="text-xl lg:text-2xl font-bold text-green-600">{totalPaid.toLocaleString()}</p>
</div>
<div className="bg-white rounded-xl p-4 lg:p-5 border border-slate-200 shadow-sm">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
<TrendingUp className="w-4 h-4 text-green-600" />
</div>
<p className="text-xs text-slate-500 font-medium">Total Return</p>
</div>
<p className="text-xl lg:text-2xl font-bold text-green-600">{investment.actualEarnings.toLocaleString()}</p>
</div>
<div className="bg-white rounded-xl p-4 lg:p-5 border border-slate-200 shadow-sm">
<div className="flex items-center gap-2 mb-2">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${isBattery ? 'bg-emerald-100' : 'bg-blue-100'}`}>
{isBattery ? (
<Battery className="w-4 h-4 text-emerald-600" />
) : (
<Bike className="w-4 h-4 text-blue-600" />
)}
</div>
<p className="text-xs text-slate-500 font-medium">{isBattery ? 'Batteries' : 'Bikes'}</p>
</div>
<p className={`text-xl lg:text-2xl font-bold ${isBattery ? 'text-emerald-600' : 'text-blue-600'}`}>
{isBattery ? assignedBatteries.length : assignedBikes.length}
</p>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="flex overflow-x-auto border-b border-slate-100 justify-between sm:justify-start px-4 lg:px-0">
{[
{ key: 'overview', label: 'Overview', icon: FileText, count: null },
{ key: 'bikes', label: isBattery ? 'Batteries' : 'Bikes', icon: isBattery ? Battery : Bike, count: isBattery ? assignedBatteries.length : assignedBikes.length },
{ key: 'transactions', label: 'Transactions', icon: CreditCard, count: demoTransactions.length },
{ key: 'statement', label: 'Statement', icon: Receipt, count: null },
].map((tab) => {
const Icon = tab.icon;
return (
<button key={tab.key} onClick={() => setActiveTab(tab.key)}
className={`px-4 lg:px-6 py-3 w-full lg:w-auto text-sm font-semibold capitalize whitespace-nowrap transition-colors flex justify-center items-center gap-2 ${activeTab === tab.key ? 'text-investor bg-investor/5 border-b-2 border-investor' : 'text-slate-500 hover:text-slate-700 hover:bg-slate-50'}`}>
<Icon className="w-4 h-4 " />
<span className="hidden lg:inline">{tab.label}</span>
{tab.count !== null && <span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${activeTab === tab.key ? 'bg-investor text-white' : 'bg-slate-200 text-slate-600'}`}>{tab.count}</span>}
</button>
);
})}
</div>
<div className="p-4 lg:p-6">
{activeTab === 'overview' && (
<div className="space-y-6">
<div className="grid md:grid-cols-2 gap-4">
<div className="bg-slate-50 rounded-xl p-5 border border-slate-200">
<h4 className="font-semibold text-slate-800 mb-4 flex items-center gap-2">
<FileText className="w-4 h-4 text-slate-400" /> Investment Details
</h4>
<div className="space-y-3 text-sm">
<div className="flex justify-between border-b border-slate-200 pb-2">
<span className="text-slate-500">Plan Name</span>
<span className="font-semibold text-slate-800">{investment.planName}</span>
</div>
<div className="flex justify-between border-b border-slate-200 pb-2">
<span className="text-slate-500">Plan Type</span>
<span className="font-semibold text-slate-800 capitalize">{investment.planType}</span>
</div>
<div className="flex justify-between border-b border-slate-200 pb-2">
<span className="text-slate-500">Period</span>
<span className="font-semibold text-slate-800">{investment.startDate} - {investment.endDate || 'Ongoing'}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Payment Method</span>
<span className="font-semibold text-slate-800 capitalize">{investment.paymentMethod}</span>
</div>
</div>
</div>
<div className="bg-slate-50 rounded-xl p-5 border border-slate-200">
<h4 className="font-semibold text-slate-800 mb-4 flex items-center gap-2">
<Shield className="w-4 h-4 text-slate-400" /> Plan Policy
</h4>
<div className="space-y-3 text-sm">
<div className="flex justify-between border-b border-slate-200 pb-2">
<span className="text-slate-500">Min Duration</span>
<span className="font-semibold text-slate-800">12 Months</span>
</div>
<div className="flex justify-between border-b border-slate-200 pb-2">
<span className="text-slate-500">Lock-in Period</span>
<span className="font-semibold text-slate-800">3 Months</span>
</div>
<div className="flex justify-between border-b border-slate-200 pb-2">
<span className="text-slate-500">Exit Penalty</span>
<span className="font-semibold text-red-500">10%</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Maintenance</span>
<span className="font-semibold text-green-600">Included</span>
</div>
</div>
</div>
</div>
{isBattery ? (
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-5">
<h4 className="font-semibold text-emerald-800 mb-4 flex items-center gap-2">
<Percent className="w-4 h-4 text-emerald-600" /> Profit Sharing
</h4>
<p className="text-xs text-emerald-600 mb-4 font-medium">Profit sharing ratio when batteries are utilized</p>
<div className="max-w-xs">
<div className="bg-white rounded-lg p-3 text-center border border-emerald-200">
<p className="text-xs text-slate-500 mb-1 font-medium">Profit Share</p>
<p className="text-xl font-bold text-slate-800">40%</p>
</div>
</div>
</div>
) : (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-5">
<h4 className="font-semibold text-amber-800 mb-4 flex items-center gap-2">
<Percent className="w-4 h-4 text-amber-600" /> Profit Sharing
</h4>
<p className="text-xs text-amber-600 mb-4 font-medium">Your share based on rental model</p>
<div className="grid grid-cols-3 gap-3">
<div className="bg-white rounded-lg p-3 text-center border border-amber-200">
<p className="text-xs text-slate-500 mb-1 font-medium">Single Rent</p>
<p className="text-xl font-bold text-slate-800">40%</p>
</div>
<div className="bg-white rounded-lg p-3 text-center border border-amber-200">
<p className="text-xs text-slate-500 mb-1 font-medium">Rent to Own</p>
<p className="text-xl font-bold text-slate-800">50%</p>
</div>
<div className="bg-white rounded-lg p-3 text-center border border-amber-200">
<p className="text-xs text-slate-500 mb-1 font-medium">Share EV</p>
<p className="text-xl font-bold text-slate-800">55%</p>
</div>
</div>
</div>
)}
</div>
)}
{activeTab === 'bikes' && (
isBattery ? (
<div className="space-y-4">
<p className="text-sm text-slate-500">{assignedBatteries.length} battery pack{assignedBatteries.length !== 1 ? 's' : ''} assigned to this investment</p>
{assignedBatteries.map((battery) => (
<Link
href={`/investor/investments/${investmentId}/rental-history/${battery.id}`}
key={battery.id}
className="p-4 bg-white rounded-xl border border-slate-200 flex flex-col md:flex-row items-start gap-4 hover:border-emerald-500 hover:shadow-md transition-all block group cursor-pointer"
>
<div className="w-16 h-16 bg-emerald-50 rounded-xl flex items-center justify-center shrink-0 border border-emerald-100 group-hover:scale-105 transition-transform duration-300">
<Battery className="w-8 h-8 text-emerald-600 animate-pulse" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h5 className="font-semibold text-slate-800 group-hover:text-emerald-700 transition-colors">{battery.model}</h5>
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${battery.status === 'In-Use' ? 'bg-green-100 text-green-700' : 'bg-blue-100 text-blue-700'}`}>{battery.status}</span>
</div>
<p className="text-sm text-slate-500">{battery.serialNumber} {battery.brand}</p>
<div className="mt-2 flex flex-wrap gap-3 text-xs text-slate-500">
<span className="flex items-center gap-1">Cycle Count: {battery.cycleCount}</span>
<span className="flex items-center gap-1">SoC / Health: {battery.soc}</span>
<span className="flex items-center gap-1"><MapPin className="w-3 h-3" /> {battery.location}</span>
</div>
</div>
<div className="flex items-center gap-3 self-center shrink-0">
<div className="text-right">
<p className="text-xs text-slate-500 mb-1">Est. Monthly Return</p>
<p className="text-lg font-bold text-slate-800">{(investment.monthlyReturn / assignedBatteries.length).toLocaleString()}</p>
<p className="text-xs text-slate-400 mt-1">Total: {battery.earnings.toLocaleString()}</p>
</div>
<ChevronRight className="w-5 h-5 text-slate-400 group-hover:text-emerald-600 group-hover:translate-x-1 transition-all" />
</div>
</Link>
))}
</div>
) : (
<div className="space-y-4">
<p className="text-sm text-slate-500">{assignedBikes.length} bike{assignedBikes.length !== 1 ? 's' : ''} assigned to this investment</p>
{assignedBikes.map((bike) => (
<Link
href={`/investor/investments/${investmentId}/rental-history/${bike.id}`}
key={bike.id}
className="p-4 bg-white rounded-xl border border-slate-200 flex flex-col md:flex-row items-start gap-4 hover:border-investor hover:shadow-md transition-all block group cursor-pointer"
>
<div className="w-24 h-20 bg-slate-100 rounded-lg overflow-hidden shrink-0 group-hover:scale-102 transition-transform duration-300">
<img src={bike.image} alt={bike.model} className="w-full h-full object-cover animate-fade-in" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h5 className="font-semibold text-slate-800 group-hover:text-investor transition-colors">{bike.model}</h5>
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${bike.status === 'rented' ? 'bg-green-100 text-green-700' : bike.status === 'available' ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700'}`}>{bike.status}</span>
</div>
<p className="text-sm text-slate-500">{bike.plateNumber} {bike.brand}</p>
<div className="mt-2 flex flex-wrap gap-3 text-xs text-slate-500">
<span className="flex items-center gap-1"><Battery className="w-3 h-3" /> {bike.batteryLevel}%</span>
<span className="flex items-center gap-1"><MapPin className="w-3 h-3" /> {bike.location}</span>
</div>
</div>
<div className="flex items-center gap-3 self-center shrink-0">
<div className="text-right">
<p className="text-xs text-slate-500 mb-1">Est. Monthly Return</p>
<p className="text-lg font-bold text-slate-800">{bike.currentRent}</p>
<p className="text-xs text-slate-400 mt-1">Total: {bike.totalEarnings.toLocaleString()}</p>
</div>
<ChevronRight className="w-5 h-5 text-slate-400 group-hover:text-investor group-hover:translate-x-1 transition-all" />
</div>
</Link>
))}
</div>
)
)}
{activeTab === 'statement' && (
<div className="max-w-2xl mx-auto">
<div className="bg-slate-50 rounded-xl p-5 border border-slate-200">
<h3 className="font-semibold text-slate-800 mb-5 text-center text-lg">Profit & Loss Statement</h3>
<div className="space-y-4">
<div className="flex justify-between items-center pb-3 border-b border-slate-200">
<span className="font-medium text-slate-600">Gross Rental Revenue</span>
<span className="text-xl font-bold text-slate-800">{demoPnl.grossRevenue.toLocaleString()}</span>
</div>
<div className="space-y-2 py-2">
<div className="flex justify-between text-sm">
<span className="text-slate-500">Platform Fee {isBattery ? '(60%)' : '(45%)'}</span>
<span className="font-medium text-red-500">-{demoPnl.platformFee.toLocaleString()}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">Insurance Coverage</span>
<span className="font-medium text-slate-600">-{demoPnl.insurance.toLocaleString()}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-500">Maintenance</span>
<span className="font-medium text-slate-600">-{demoPnl.maintenance.toLocaleString()}</span>
</div>
</div>
<div className="flex justify-between items-center pt-4 border-t-2 border-slate-300">
<span className="text-lg font-semibold text-slate-800">Your Share {isBattery ? '(40%)' : '(55%)'}</span>
<span className="text-2xl font-bold text-green-600">{demoPnl.netProfit.toLocaleString()}</span>
</div>
</div>
</div>
</div>
)}
{activeTab === 'transactions' && (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<p className="text-sm text-slate-500">{demoTransactions.length} transactions found</p>
<div className="flex items-center gap-2">
<select className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"><option>All Types</option><option>Income</option><option>Withdrawal</option></select>
<select className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"><option>Last 30 Days</option><option>Last 7 Days</option><option>This Month</option><option>All Time</option></select>
</div>
</div>
<div className="lg:overflow-x-auto">
<table className="hidden lg:table w-full text-left">
<thead className="bg-slate-50">
<tr><th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Date</th><th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Description</th><th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase text-right">Amount</th><th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Status</th></tr>
</thead>
<tbody className="divide-y divide-slate-100">
{demoTransactions.map((tx) => (
<tr key={tx.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">{tx.date}</td>
<td className="px-4 py-3"><p className="text-sm font-medium text-slate-800">{tx.description}</p><p className="text-xs text-slate-400">Ref: {tx.id}</p></td>
<td className={`px-4 py-3 text-sm font-bold text-right ${tx.amount > 0 ? 'text-green-600' : 'text-red-500'}`}>{tx.amount > 0 ? '+' : ''}{tx.amount.toLocaleString()}</td>
<td className="px-4 py-3"><span className="inline-flex px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-green-100 text-green-700">{tx.status}</span></td>
</tr>
))}
</tbody>
</table>
</div>
<div className="lg:hidden space-y-3">
{demoTransactions.map((tx) => (
<div key={tx.id} className="p-4 bg-white rounded-xl border border-slate-200">
<div className="flex items-start justify-between mb-2">
<div><p className="text-sm font-medium text-slate-800">{tx.description}</p><p className="text-xs text-slate-400">Ref: {tx.id} {tx.date}</p></div>
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${tx.status === 'completed' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>{tx.status}</span>
</div>
<p className={`text-lg font-bold text-right ${tx.amount > 0 ? 'text-green-600' : 'text-red-500'}`}>{tx.amount > 0 ? '+' : ''}{tx.amount.toLocaleString()}</p>
</div>
))}
</div>
<div className="flex items-center justify-between pt-4 border-t border-slate-100">
<p className="text-sm text-slate-500">Showing 1-8 of 8</p>
<div className="flex items-center gap-2">
<button className="px-3 py-1.5 border border-slate-200 rounded-lg text-sm text-slate-600 hover:bg-slate-50 disabled:opacity-50" disabled>Previous</button>
<button className="px-3 py-1.5 border border-slate-200 rounded-lg text-sm text-slate-600 hover:bg-slate-50 disabled:opacity-50" disabled>Next</button>
</div>
</div>
</div>
)}
{activeTab === 'payments' && (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<p className="text-sm text-slate-500">{paymentHistory.length} payments made</p>
{dueAmount > 0 && (
<button onClick={() => setShowPaymentModal(true)} className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-2">
<DollarSign className="w-4 h-4" /> Make Payment
</button>
)}
</div>
<div className="overflow-x-auto">
<table className="hidden lg:table w-full text-left">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Date</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Type</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Installment</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Method</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase text-right">Amount</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paymentHistory.map((payment) => (
<tr key={payment.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">{payment.date}</td>
<td className="px-4 py-3"><span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-slate-100 text-slate-600">{payment.type}</span></td>
<td className="px-4 py-3 text-sm text-slate-600">{payment.installmentNo ? `#${payment.installmentNo}` : '-'}</td>
<td className="px-4 py-3 text-sm text-slate-600">{payment.method}</td>
<td className="px-4 py-3 text-sm font-bold text-green-600 text-right">{payment.amount.toLocaleString()}</td>
<td className="px-4 py-3"><span className="inline-flex px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-green-100 text-green-700">{payment.status}</span></td>
</tr>
))}
</tbody>
</table>
</div>
<div className="lg:hidden space-y-3">
{paymentHistory.map((payment) => (
<div key={payment.id} className="p-4 bg-white rounded-xl border border-slate-200">
<div className="flex items-start justify-between mb-2">
<div>
<p className="text-sm font-medium text-slate-800">{payment.amount.toLocaleString()}</p>
<p className="text-xs text-slate-400">{payment.date} {payment.method}</p>
</div>
<span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-green-100 text-green-700">{payment.status}</span>
</div>
<div className="flex gap-2 mt-2">
<span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-slate-100 text-slate-600">{payment.type}</span>
{payment.installmentNo && <span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-blue-100 text-blue-600">Inst #{payment.installmentNo}</span>}
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
{showPaymentModal && dueAmount > 0 && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl w-full max-w-md shadow-2xl">
<div className="flex items-center justify-between p-5 border-b border-slate-100">
<h3 className="text-lg font-bold text-slate-800">Make Payment</h3>
<button onClick={() => setShowPaymentModal(false)} className="p-1 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-5 space-y-4">
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-500">Total Investment</span>
<span className="text-sm font-semibold text-slate-800">{investment.totalInvestment.toLocaleString()}</span>
</div>
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-500">Already Paid</span>
<span className="text-sm font-semibold text-green-600">{totalPaid.toLocaleString()}</span>
</div>
<div className="flex justify-between pt-2 border-t border-slate-200">
<span className="text-sm font-semibold text-slate-800">Due Amount</span>
<span className="text-sm font-bold text-amber-600">{dueAmount.toLocaleString()}</span>
</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 text-center">
<p className="text-sm text-amber-600 mb-1">Fixed Payment Amount</p>
<p className="text-3xl font-bold text-amber-700">{dueAmount.toLocaleString()}</p>
</div>
<div className="pt-2">
<label className="text-sm font-semibold text-slate-700 mb-2 block">Payment Method</label>
<div className="flex gap-2">
<button className="flex-1 p-3 border border-slate-200 rounded-lg text-center hover:border-investor hover:bg-investor/5 transition-colors">
<Building2 className="w-5 h-5 mx-auto text-slate-600 mb-1" />
<span className="text-xs font-medium text-slate-700">Bank</span>
</button>
<button className="flex-1 p-3 border border-slate-200 rounded-lg text-center hover:border-investor hover:bg-investor/5 transition-colors">
<Smartphone className="w-5 h-5 mx-auto text-slate-600 mb-1" />
<span className="text-xs font-medium text-slate-700">bKash</span>
</button>
</div>
</div>
<button onClick={handlePaymentSubmit} className="w-full py-3 bg-investor text-white rounded-xl font-bold hover:bg-investor-dark transition-colors">
Pay {dueAmount.toLocaleString()}
</button>
</div>
</div>
</div>
)}
</div>
);
}
import { Percent } from 'lucide-react';

View File

@@ -0,0 +1,551 @@
'use client';
import { useState, use } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import {
ArrowLeft, Battery, Bike, Calendar, Clock, Download, MapPin, Search,
TrendingUp, User, XCircle, CheckCircle2, ChevronLeft, ChevronRight,
Shield, RefreshCw, DollarSign, Activity, AlertCircle, CalendarRange
} from 'lucide-react';
import { investors } from '@/data/mockData';
import toast from 'react-hot-toast';
import InvestorNotification from '@/components/InvestorNotification';
interface RentalTransaction {
id: string;
date: string;
riderName: string;
duration: string;
amount: number;
status: 'paid' | 'pending' | 'failed';
payoutMethod: string;
swapCount?: number;
}
export default function AssetRentalHistoryPage({ params }: { params: Promise<{ id: string; assetId: string }> }) {
const resolvedParams = use(params);
const { id: investmentId, assetId } = resolvedParams;
const router = useRouter();
const investor = investors[0];
const investment = investor.investments?.find((inv: any) => inv.id === investmentId);
// Asset verification
const isBattery = assetId.startsWith('BAT-') || assetId.startsWith('bat-') || assetId.toLowerCase().includes('battery');
// Specific Asset Details
const bikeDetails = {
id: 'b1',
model: 'Etron ET50',
brand: 'Etron',
plateNumber: 'Dhaka Metro Cha-1234',
image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=800&h=600&fit=crop',
status: 'rented',
currentRent: 350,
totalEarnings: 114250,
batteryLevel: 78,
range: '60 km',
location: 'Gulshan 1 Hub',
purchasePrice: 85000,
utilization: '94%',
avgDailyHours: '8.4 hrs',
};
const batteryDetails: Record<string, any> = {
'BAT-001': {
id: 'BAT-001',
serialNumber: 'SN-2024-00001',
brand: 'EVE Energy',
model: 'Li-Ion 60V50Ah',
status: 'In-Use',
cycleCount: 156,
soc: '78% / 95%',
earnings: 4500,
location: 'Dhaka Central Hub',
purchasePrice: 45000,
utilization: '97%',
dailyRent: 150,
},
'BAT-002': {
id: 'BAT-002',
serialNumber: 'SN-2024-00002',
brand: 'EVE Energy',
model: 'Li-Ion 60V50Ah',
status: 'In-Use',
cycleCount: 142,
soc: '82% / 96%',
earnings: 4500,
location: 'Dhaka Central Hub',
purchasePrice: 45000,
utilization: '95%',
dailyRent: 150,
}
};
const currentBattery = batteryDetails[assetId] || batteryDetails['BAT-001'];
const assetDisplayName = isBattery ? currentBattery.model : bikeDetails.model;
const assetSubName = isBattery ? currentBattery.serialNumber : bikeDetails.plateNumber;
// Generate highly realistic rent transaction history
const [transactions] = useState<RentalTransaction[]>(() => {
const list: RentalTransaction[] = [];
const riders = ['Sajib Islam', 'Nayeem Chowdhury', 'Rakib Hasan', 'Kamal Hossain', 'Arifur Rahman'];
const methods = ['bKash', 'Nagad', 'Rocket', 'Bank Transfer'];
const days = isBattery ? 30 : 20;
const baseDate = new Date();
for (let i = 0; i < days; i++) {
const date = new Date();
date.setDate(baseDate.getDate() - i);
const dateString = date.toISOString().split('T')[0];
const riderIndex = (i + 3) % riders.length;
const methodIndex = (i + 1) % methods.length;
// status distribution
let status: 'paid' | 'pending' | 'failed' = 'paid';
if (i === 1) status = 'pending';
else if (i === 5) status = 'failed';
const amount = isBattery ? currentBattery.dailyRent : bikeDetails.currentRent;
list.push({
id: `TX-${isBattery ? 'BAT' : 'BK'}-${10000 + i}`,
date: dateString,
riderName: riders[riderIndex],
duration: '1 Day',
amount: amount,
status: status,
payoutMethod: methods[methodIndex],
swapCount: isBattery ? 2 + (i % 3) : undefined
});
}
return list;
});
// Filter & Sorting State
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [sortBy, setSortBy] = useState<'date' | 'amount' | 'rider'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [page, setPage] = useState(1);
const pageSize = 10;
// Handler functions
const handleSort = (field: 'date' | 'amount' | 'rider') => {
if (sortBy === field) {
setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'));
} else {
setSortBy(field);
setSortOrder('desc');
}
setPage(1);
};
// Filter Logic
const filteredTransactions = transactions.filter(tx => {
if (statusFilter !== 'all' && tx.status !== statusFilter) return false;
if (searchQuery && !tx.riderName.toLowerCase().includes(searchQuery.toLowerCase()) && !tx.id.toLowerCase().includes(searchQuery.toLowerCase())) return false;
if (dateFrom && new Date(tx.date) < new Date(dateFrom)) return false;
if (dateTo && new Date(tx.date) > new Date(dateTo)) return false;
return true;
});
// Sort Logic
const sortedTransactions = [...filteredTransactions].sort((a, b) => {
let comparison = 0;
if (sortBy === 'date') {
comparison = new Date(a.date).getTime() - new Date(b.date).getTime();
} else if (sortBy === 'amount') {
comparison = a.amount - b.amount;
} else if (sortBy === 'rider') {
comparison = a.riderName.localeCompare(b.riderName);
}
return sortOrder === 'desc' ? -comparison : comparison;
});
// Pagination
const totalPages = Math.ceil(sortedTransactions.length / pageSize);
const paginatedTransactions = sortedTransactions.slice((page - 1) * pageSize, page * pageSize);
const statusConfig: Record<string, { label: string; bg: string; color: string; icon: any }> = {
paid: { label: 'Paid', bg: 'bg-green-100', color: 'text-green-700', icon: CheckCircle2 },
pending: { label: 'Pending', bg: 'bg-amber-100', color: 'text-amber-700', icon: Clock },
failed: { label: 'Failed', bg: 'bg-red-100', color: 'text-red-700', icon: XCircle },
};
const totalCollected = filteredTransactions
.filter(t => t.status === 'paid')
.reduce((sum, t) => sum + t.amount, 0);
const pendingAmount = filteredTransactions
.filter(t => t.status === 'pending')
.reduce((sum, t) => sum + t.amount, 0);
return (
<div className="min-h-screen lg:pt-6 pt-0">
<InvestorNotification isMobile />
<div className="pt-18 lg:pt-0 p-4 sm:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
{/* Header - EXACT copy of other page's spacing & structure */}
<div className="mb-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-3">
<button
onClick={() => router.back()}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors border border-slate-200 bg-white"
>
<ArrowLeft className="w-5 h-5 text-slate-600" />
</button>
<div>
<h1 className="text-xl sm:text-2xl font-bold text-slate-800 flex items-center gap-2">
{isBattery ? <Battery className="w-5 h-5 sm:w-6 sm:h-6 text-investor" /> : <Bike className="w-5 h-5 sm:w-6 sm:h-6 text-investor" />}
{assetDisplayName}
</h1>
<p className="text-sm text-slate-500 mt-1">
ID: #{assetId.toUpperCase()} {isBattery ? `Serial Number: ${currentBattery.serialNumber}` : `Plate: ${bikeDetails.plateNumber}`}
</p>
</div>
</div>
<button
onClick={() => toast.success('Exporting history...')}
className="px-4 py-2.5 bg-white border border-slate-200 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-50 flex justify-center lg:justify-start items-center gap-2 shadow-sm w-fit"
>
<Download className="w-4 h-4 text-slate-500" /> Export Ledger
</button>
</div>
</div>
{/* Info Grid & Stats Cards - MATCHES OTHER PAGES PRECISELY */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0">
<DollarSign className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xs text-slate-500">Total Collected</p>
<p className="text-lg font-bold text-green-600">{totalCollected.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center shrink-0">
<Activity className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-xs text-slate-500">Utilization Rate</p>
<p className="text-lg font-bold text-slate-800">{isBattery ? currentBattery.utilization : bikeDetails.utilization}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-100 rounded-lg flex items-center justify-center shrink-0">
<Clock className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="text-xs text-slate-500">Pending</p>
<p className="text-lg font-bold text-amber-600">{pendingAmount.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center shrink-0">
<Shield className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-xs text-slate-500">Hub Location</p>
<p className="text-sm font-bold text-slate-800 truncate max-w-[140px] sm:max-w-none">
{isBattery ? currentBattery.location.split(' ')[0] : bikeDetails.location.split(' ')[0]}
</p>
</div>
</div>
</div>
</div>
{/* Main Table Card - MATCHES OTHER PAGES PRECISELY */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
{/* Filters - MATCHES OTHER PAGES PRECISELY */}
<div className="p-4 border-b border-slate-100">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
type="text"
placeholder="Search rider..."
value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setPage(1); }}
className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64 focus:outline-none"
/>
</div>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white cursor-pointer"
>
<option value="all">All Status</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
<option value="failed">Failed</option>
</select>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 text-xs text-slate-500 font-medium">
<CalendarRange className="w-4 h-4" /> Date Range
</div>
<div className="flex items-center gap-1">
<input
type="date"
value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
<span className="text-slate-400">to</span>
<input
type="date"
value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
</div>
{(dateFrom || dateTo) && (
<button
onClick={() => { setDateFrom(''); setDateTo(''); setPage(1); }}
className="px-2 py-1 text-xs text-red-500 hover:bg-red-50 rounded"
>
Clear
</button>
)}
<button
onClick={() => {
setSearchQuery('');
setStatusFilter('all');
setDateFrom('');
setDateTo('');
setSortBy('date');
setSortOrder('desc');
setPage(1);
toast.success('Filters cleared!');
}}
className="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 bg-white"
title="Reset filters"
>
<RefreshCw className="w-4 h-4 text-slate-500" />
</button>
</div>
</div>
</div>
{/* Card View - Mobile/Tablet */}
<div className="lg:hidden divide-y divide-slate-100">
{paginatedTransactions.length > 0 ? (
paginatedTransactions.map((tx) => {
const status = statusConfig[tx.status] || statusConfig.pending;
const StatusIcon = status.icon;
return (
<div key={tx.id} className="p-4 hover:bg-slate-50">
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<User className="w-4 h-4 text-slate-400 shrink-0" />
<p className="text-sm font-semibold text-slate-800 truncate">{tx.riderName}</p>
</div>
<p className="text-xs text-slate-400 ml-6">Ref: {tx.id}</p>
{isBattery && (
<p className="text-xs text-slate-600 mt-1 ml-6 font-semibold">Swaps: {tx.swapCount}</p>
)}
</div>
<div className="text-right shrink-0">
<p className="text-base font-bold text-slate-800">{tx.amount.toLocaleString()}</p>
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium mt-1 ${status.bg} ${status.color}`}>
<StatusIcon className="w-3 h-3" /> {status.label}
</span>
</div>
</div>
<div className="flex items-center justify-between ml-6 text-xs text-slate-400">
<span>{tx.date}</span>
<span className="capitalize">{tx.payoutMethod}</span>
</div>
</div>
);
})
) : (
<div className="p-8 text-center text-slate-500">
<AlertCircle className="w-10 h-10 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</div>
)}
</div>
{/* Table View - Desktop */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th
onClick={() => handleSort('date')}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Date {sortBy === 'date' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">
Transaction ID
</th>
<th
onClick={() => handleSort('rider')}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Rider (Biker) {sortBy === 'rider' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
{isBattery && (
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">
Swaps
</th>
)}
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">
Duration
</th>
<th
onClick={() => handleSort('amount')}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Amount {sortBy === 'amount' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">
Method
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">
Status
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedTransactions.length > 0 ? (
paginatedTransactions.map((tx) => {
const status = statusConfig[tx.status] || statusConfig.pending;
const StatusIcon = status.icon;
return (
<tr key={tx.id} className="hover:bg-slate-50">
<td className="px-4 py-3">
<div>
<p className="text-sm font-medium text-slate-800">{tx.date}</p>
</div>
</td>
<td className="px-4 py-3 text-sm text-slate-500 font-mono">
{tx.id}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-slate-400" />
<span className="text-sm text-slate-700 font-medium">{tx.riderName}</span>
</div>
</td>
{isBattery && (
<td className="px-4 py-3 text-sm text-slate-600">
{tx.swapCount} Swaps
</td>
)}
<td className="px-4 py-3 text-sm text-slate-600">
{tx.duration}
</td>
<td className="px-4 py-3">
<p className="text-sm font-bold text-slate-800">{tx.amount.toLocaleString()}</p>
</td>
<td className="px-4 py-3 text-sm text-slate-600 capitalize">
{tx.payoutMethod}
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium ${status.bg} ${status.color}`}>
<StatusIcon className="w-3 h-3" />
{status.label}
</span>
</td>
</tr>
);
})
) : (
<tr>
<td colSpan={isBattery ? 8 : 7} className="px-4 py-12 text-center text-slate-500">
<AlertCircle className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination - MATCHES OTHER PAGES PRECISELY */}
{sortedTransactions.length > pageSize && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3">
<p className="text-xs sm:text-sm text-slate-500">
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedTransactions.length)} of {sortedTransactions.length}
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronLeft className="w-4 h-4" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`w-8 h-8 rounded-lg text-sm font-medium ${
page === pageNum
? 'bg-investor text-white'
: 'border border-slate-200 hover:bg-slate-50'
}`}
>
{pageNum}
</button>
))}
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,206 @@
'use client';
import { useState } from 'react';
import { Bell, X, ArrowRight, Package, DollarSign, Bike, AlertCircle, CheckCircle, Filter, Check } from 'lucide-react';
import Link from 'next/link';
const mockNotifications = [
{
id: '1',
type: 'rental',
title: 'New Rental Started',
message: 'Your bike AB-1234 has been rented by rider MR-456',
time: '5 min ago',
read: false,
},
{
id: '2',
type: 'earning',
title: 'Earning Credited',
message: '৳450 has been added to your wallet from bike CD-5678',
time: '2 hours ago',
read: false,
},
{
id: '3',
type: 'success',
title: 'Withdrawal Complete',
message: 'Your withdrawal of ৳5,000 has been processed successfully',
time: '1 day ago',
read: true,
},
{
id: '4',
type: 'alert',
title: 'Damage Alert',
message: 'Minor scratch damage has been reported on your bike XY-9012.',
time: '2 days ago',
read: true,
},
{
id: '5',
type: 'rental',
title: 'Rental Ended',
message: 'Bike AB-1234 has been returned by rider MR-456',
time: '3 hours ago',
read: false,
},
{
id: '6',
type: 'earning',
title: 'Daily Earning Summary',
message: 'You earned ৳1,250 from 5 rentals today',
time: 'Yesterday',
read: true,
},
{
id: '7',
type: 'alert',
title: 'Damage Alert',
message: 'Cracked indicator light has been reported on your bike EF-3456.',
time: '5 hours ago',
read: true,
},
{
id: '8',
type: 'success',
title: 'Investment Renewed',
message: 'Your Gold Plan investment has been automatically renewed',
time: '3 days ago',
read: true,
},
];
const iconMap: Record<string, any> = {
rental: Bike,
earning: DollarSign,
success: CheckCircle,
alert: AlertCircle,
default: Package,
};
const typeColors: Record<string, string> = {
rental: 'bg-blue-100 text-blue-600',
earning: 'bg-green-100 text-green-600',
success: 'bg-emerald-100 text-emerald-600',
alert: 'bg-amber-100 text-amber-600',
default: 'bg-slate-100 text-slate-600',
};
export default function InvestorNotificationsPage() {
const [notifications, setNotifications] = useState(mockNotifications);
const [filter, setFilter] = useState<'all' | 'unread'>('all');
const filteredNotifications = filter === 'unread'
? notifications.filter(n => !n.read)
: notifications;
const unreadCount = notifications.filter(n => !n.read).length;
const markAsRead = (id: string) => {
setNotifications(prev => prev.map(n =>
n.id === id ? { ...n, read: true } : n
));
};
const markAllAsRead = () => {
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
};
return (
<div className="min-h-screen lg:pt-6 pt-0">
<InvestorNotification isMobile />
<div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
<div className="mb-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-xl lg:text-2xl font-bold text-slate-800 flex items-center gap-2">
<Bell className="w-5 h-5 lg:w-6 lg:h-6 text-investor" /> Notifications
</h1>
<p className="text-sm text-slate-500 mt-1">Stay updated with your investment activities</p>
</div>
{unreadCount > 0 && (
<button
onClick={markAllAsRead}
className="px-4 py-2 bg-white border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-50 flex items-center gap-2 shadow-sm"
>
<Check className="w-4 h-4" /> Mark all as read
</button>
)}
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
<div className="flex items-center gap-2">
<button
onClick={() => setFilter('all')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${filter === 'all'
? 'bg-investor text-white'
: 'text-slate-600 hover:bg-slate-100'
}`}
>
All
</button>
<button
onClick={() => setFilter('unread')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors flex items-center gap-1.5 ${filter === 'unread'
? 'bg-investor text-white'
: 'text-slate-600 hover:bg-slate-100'
}`}
>
Unread
{unreadCount > 0 && (
<span className="px-1.5 py-0.5 bg-red-500 text-white text-xs font-bold rounded-full">
{unreadCount}
</span>
)}
</button>
</div>
</div>
<div className="divide-y divide-slate-100">
{filteredNotifications.length === 0 ? (
<div className="p-8 text-center">
<Bell className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<p className="text-slate-500">No notifications found</p>
</div>
) : (
filteredNotifications.map((notif) => {
const Icon = iconMap[notif.type] || iconMap.default;
return (
<div
key={notif.id}
onClick={() => markAsRead(notif.id)}
className={`p-4 cursor-pointer transition-all hover:bg-slate-50 ${!notif.read ? 'bg-blue-50/50' : ''
}`}
>
<div className="flex items-start gap-4">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 ${typeColors[notif.type]}`}>
<Icon className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<p className={`text-sm font-semibold ${notif.read ? 'text-slate-600' : 'text-slate-800'}`}>
{notif.title}
</p>
{!notif.read && (
<div className="w-2 h-2 bg-investor rounded-full" />
)}
</div>
<p className="text-sm text-slate-500 line-clamp-2">{notif.message}</p>
<p className="text-xs text-slate-400 mt-1">{notif.time}</p>
</div>
</div>
</div>
);
})
)}
</div>
</div>
</div>
</div>
);
}
import InvestorNotification from '@/components/InvestorNotification';

View File

@@ -1,76 +0,0 @@
import StatCard from '@/components/StatCard';
import TransactionList from '@/components/TransactionList';
import { investors, bikes, transactions } from '@/data/mockData';
import { Wallet, TrendingUp, Bike, Target, DollarSign, FileText, Phone, BarChart3, Send, ArrowDownToLine } from 'lucide-react';
export default function InvestorPage() {
const investor = investors[0];
const investorBikes = bikes.filter(b => b.investorId === investor?.id || b.status === 'rented');
return (
<div className="p-4 lg:p-6">
<div className="mb-6">
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">Investor Dashboard</h1>
<p className="text-sm text-slate-500">Track your investments and earnings</p>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<StatCard label="Total Invested" value={`${(investor.totalInvested / 1000).toFixed(0)}k`} icon={Wallet} color="text-investor" />
<StatCard label="Total Earnings" value={`${(investor.totalEarnings / 1000).toFixed(1)}k`} icon={TrendingUp} color="text-green-600" trend="+2.3%" trendUp />
<StatCard label="Active Bikes" value={investor.activeBikes} icon={Bike} color="text-biker" />
<StatCard label="ROI" value={`${investor.roi}%`} icon={Target} color="text-purple-600" />
</div>
<div className="grid lg:grid-cols-2 gap-6 mb-6">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center justify-between mb-4">
<h2 className="font-bold text-slate-800">My Portfolio</h2>
<span className="text-xs font-semibold px-2 py-1 bg-purple-100 text-purple-700 rounded-full">
{investorBikes.length} bikes
</span>
</div>
<div className="space-y-3">
{investorBikes.map(bike => (
<div key={bike.id} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div>
<p className="font-medium text-slate-700">{bike.model}</p>
<p className="text-xs text-slate-400">{bike.plateNumber}</p>
</div>
<span className={`text-xs font-medium px-2 py-1 rounded-full ${
bike.status === 'rented' ? 'bg-green-100 text-green-700' :
bike.status === 'available' ? 'bg-blue-100 text-blue-700' :
'bg-amber-100 text-amber-700'
}`}>
{bike.status}
</span>
</div>
))}
</div>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<h2 className="font-bold text-slate-800 mb-4">Quick Actions</h2>
<div className="grid grid-cols-2 gap-3">
<button className="py-3 bg-investor text-white rounded-lg font-semibold text-sm hover:bg-investor-dark flex items-center justify-center gap-2">
<DollarSign className="w-4 h-4" /> Withdraw
</button>
<button className="py-3 bg-purple-600 text-white rounded-lg font-semibold text-sm hover:bg-purple-700 flex items-center justify-center gap-2">
<FileText className="w-4 h-4" /> Statement
</button>
<button className="py-3 border border-slate-200 text-slate-600 rounded-lg font-semibold text-sm hover:bg-slate-50 flex items-center justify-center gap-2">
<Phone className="w-4 h-4" /> Support
</button>
<button className="py-3 border border-slate-200 text-slate-600 rounded-lg font-semibold text-sm hover:bg-slate-50 flex items-center justify-center gap-2">
<BarChart3 className="w-4 h-4" /> Analytics
</button>
</div>
</div>
</div>
<div>
<h2 className="text-lg font-bold text-slate-800 mb-3">Transaction History</h2>
<TransactionList transactions={transactions.filter(t => t.userId === 'u7')} />
</div>
</div>
);
}

View File

@@ -0,0 +1,502 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { Target, Plus, Zap, ChevronRight, ArrowRight, Edit, Trash2, Eye, TrendingUp, X, CreditCard, Battery } from 'lucide-react';
import { investors } from '@/data/mockData';
import toast from 'react-hot-toast';
import InvestorNotification from '@/components/InvestorNotification';
export default function MyInvestmentsPage() {
const investor = investors[0];
const [showCreateModal, setShowCreateModal] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState<any>(null);
const [newInvestment, setNewInvestment] = useState({
planName: '', planType: 'gold', assetType: 'bike', totalInvestment: 0, initialPayment: 0, paymentType: 'full',
startDate: '', endDate: '', paymentMethod: 'bank', transactionReference: '', notes: ''
});
const PLAN_TEMPLATES = [
{ id: '1bike', name: '1 Bike Plan', tier: 'Standard', evBasePrice: 200000, minQuantity: 1, duration: 12, maxInvestment: 1000000, type: 'bike' },
{ id: '5bike', name: '5 Bike Plan', tier: 'Premium', evBasePrice: 180000, minQuantity: 5, duration: 24, maxInvestment: 5000000, type: 'bike' },
{ id: '10bike', name: '10 Bike Plan', tier: 'Enterprise', evBasePrice: 170000, minQuantity: 10, duration: 36, maxInvestment: 10000000, type: 'bike' },
];
const BATTERY_TEMPLATES = [
{ id: '1battery', name: '1 Battery Pack Plan', tier: 'Standard', evBasePrice: 45000, minQuantity: 1, duration: 12, maxInvestment: 500000, type: 'battery' },
{ id: '5battery', name: '5 Battery Pack Plan', tier: 'Premium', evBasePrice: 42000, minQuantity: 5, duration: 18, maxInvestment: 2000000, type: 'battery' },
{ id: '10battery', name: '10 Battery Pack Fleet', tier: 'Enterprise', evBasePrice: 40000, minQuantity: 10, duration: 24, maxInvestment: 5000000, type: 'battery' },
];
const planConfig: Record<string, { bg: string; border: string; icon: string }> = {
silver: { bg: 'bg-slate-100', border: 'border-slate-300', icon: 'text-slate-500' },
gold: { bg: 'bg-amber-100', border: 'border-amber-300', icon: 'text-amber-500' },
platinum: { bg: 'bg-purple-100', border: 'border-purple-300', icon: 'text-purple-500' },
diamond: { bg: 'bg-blue-100', border: 'border-blue-300', icon: 'text-blue-500' },
};
const handleCreate = () => {
if (!newInvestment.planName || !newInvestment.totalInvestment) {
toast.error('Please fill all required fields');
return;
}
if (newInvestment.paymentType === 'partial' && newInvestment.initialPayment < newInvestment.totalInvestment * 0.5) {
toast.error('Initial payment must be at least 50% of investment amount');
return;
}
toast.success(newInvestment.paymentType === 'partial'
? `Investment created! Initial: ৳${newInvestment.initialPayment.toLocaleString()}, Balance: ৳${(newInvestment.totalInvestment - newInvestment.initialPayment).toLocaleString()}`
: 'Investment created successfully!'
);
setShowCreateModal(false);
setSelectedTemplate(null);
setNewInvestment({ planName: '', planType: 'gold', assetType: 'bike', totalInvestment: 0, initialPayment: 0, paymentType: 'full', startDate: '', endDate: '', paymentMethod: 'bank', transactionReference: '', notes: '' });
};
return (
<div className="min-h-screen lg:pt-6 pt-0">
<InvestorNotification isMobile />
<div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
{/* Header */}
<div className="mb-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-xl sm:text-2xl font-bold text-slate-800 flex items-center gap-2">
<Target className="w-5 h-5 sm:w-6 sm:h-6 text-investor" /> My Investments
</h1>
<p className="text-sm text-slate-500 mt-1">Manage your active portfolios and track your earnings.</p>
</div>
{/* <button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2.5 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-2 shadow-sm"
>
<Plus className="w-4 h-4" /> Create Investment
</button> */}
</div>
</div>
{/* EV Investment Plans Cards */}
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-slate-800">EV Investment Plans</h3>
<p className="text-sm text-slate-500">Manage investment portfolios for this investor</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{investor.investments?.map((inv) => {
const style = planConfig[inv.planType] || planConfig.gold;
return (
<div key={inv.id} className={`bg-white rounded-xl border ${style.border} overflow-hidden`}>
<div className={`${style.bg} p-4 flex items-center justify-between`}>
<div>
<h4 className="font-bold text-slate-800 flex items-center gap-1.5">
{inv.planName.toLowerCase().includes('battery') ? (
<Battery className="w-4 h-4 text-emerald-600 animate-pulse flex-shrink-0" />
) : (
<Zap className="w-4 h-4 text-investor flex-shrink-0" />
)}
{inv.planName}
</h4>
<p className="text-sm text-slate-500 capitalize">{inv.planType} Plan</p>
</div>
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${inv.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-slate-200 text-slate-600'}`}>
{inv.status}
</span>
</div>
<div className="p-4 space-y-4">
<div className="grid grid-cols-2 gap-3">
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-xs text-slate-500">Investment</p>
<p className="font-bold text-slate-800">{inv.totalInvestment.toLocaleString()}</p>
</div>
<div className="bg-purple-50 rounded-lg p-3">
<p className="text-xs text-purple-600">Total Return</p>
<p className="font-bold text-purple-700">{inv.actualEarnings.toLocaleString()}</p>
</div>
</div>
<div className="bg-slate-50 rounded-lg p-3 space-y-1.5">
<div className="flex justify-between text-xs">
<span className="text-slate-400">Assigned Assets</span>
<span className="font-semibold text-slate-800">
{inv.planName.toLowerCase().includes('battery') || inv.assetType === 'battery' ? (
`${inv.batteryIds?.length || 3} Batteries`
) : (
`${inv.bikeIds?.length || 1} Bike${(inv.bikeIds?.length || 1) > 1 ? 's' : ''}`
)}
</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-slate-400">Duration</span>
<span className="font-medium">{inv.durationMonths || 12} months</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-slate-400">Lock-in Period</span>
<span className="font-medium">{inv.lockInMonths || 3} months</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-slate-400">Early Exit Penalty</span>
<span className="font-medium text-red-500">{inv.exitPenaltyPercent || 10}%</span>
</div>
<div className="flex justify-between text-xs border-t border-slate-200/60 pt-1.5">
<span className="text-slate-400">Profit Sharing</span>
<span className="font-semibold text-emerald-600">
{inv.planName.toLowerCase().includes('battery') || inv.assetType === 'battery' ? (
'40%'
) : (
'40% / 50% / 55%'
)}
</span>
</div>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-slate-400">{inv.startDate} - {inv.endDate || 'Ongoing'}</span>
<span className="capitalize text-slate-500">{inv.paymentMethod}</span>
</div>
<div className="pt-3 border-t border-slate-100">
<p className="text-xs text-slate-400 mb-2">ID: #{inv.id?.slice(-6) || 'N/A'}</p>
<div className="flex gap-2">
<Link href={`/investor/investments/${inv.id}`} className="flex-1 py-2 text-sm font-medium text-slate-600 hover:bg-slate-50 rounded-lg border border-slate-200 text-center">
<Eye className="w-4 h-4 inline mr-1" /> View
</Link>
</div>
</div>
</div>
</div>
);
})}
</div>
{(!investor.investments || investor.investments.length === 0) && (
<div className="text-center py-16 bg-slate-50 rounded-xl border-2 border-dashed border-slate-200">
<TrendingUp className="w-12 h-12 mx-auto mb-3 text-slate-300" />
<h4 className="font-semibold text-slate-600 mb-2">No Investments Yet</h4>
<p className="text-slate-400 mb-4">Create your first investment plan</p>
<button onClick={() => setShowCreateModal(true)} className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark">
<Plus className="w-4 h-4 inline mr-1" /> Create Investment
</button>
</div>
)}
</div>
{/* Create Investment Modal */}
{showCreateModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<div>
<h2 className="text-lg font-bold text-slate-800 flex items-center gap-2">
<Plus className="w-5 h-5 text-investor" /> Create New Investment
</h2>
<p className="text-sm text-slate-500">Set up investment for {investor.name}</p>
</div>
<button onClick={() => setShowCreateModal(false)} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-5 overflow-y-auto flex-1 space-y-5">
{!selectedTemplate ? (
<>
{/* Category Selector */}
<div>
<label className="text-sm font-semibold text-slate-700 mb-2 block">Choose Investment Asset Type *</label>
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => {
setNewInvestment({ ...newInvestment, assetType: 'bike' });
}}
className={`py-3 px-4 rounded-xl border-2 flex items-center justify-center gap-2 font-bold text-sm transition-all ${
newInvestment.assetType === 'bike'
? 'bg-investor/10 border-investor text-investor shadow-sm'
: 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'
}`}
>
<Zap className="w-4 h-4 text-investor animate-pulse" /> Bike Investment Plans
</button>
<button
onClick={() => {
setNewInvestment({ ...newInvestment, assetType: 'battery' });
}}
className={`py-3 px-4 rounded-xl border-2 flex items-center justify-center gap-2 font-bold text-sm transition-all ${
newInvestment.assetType === 'battery'
? 'bg-emerald-100 border-emerald-500 text-emerald-800 shadow-sm'
: 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'
}`}
>
<Battery className="w-4 h-4 text-emerald-600 animate-pulse" /> Battery Investment Plans
</button>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-2 block">Select Plan Template</label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{(newInvestment.assetType === 'battery' ? BATTERY_TEMPLATES : PLAN_TEMPLATES).map(plan => (
<button
key={plan.id}
onClick={() => {
setSelectedTemplate(plan);
setNewInvestment({
...newInvestment,
planName: plan.name,
planType: plan.tier.toLowerCase() as any,
totalInvestment: plan.evBasePrice * plan.minQuantity,
startDate: new Date().toISOString().split('T')[0],
});
}}
className={`p-4 rounded-lg border-2 text-left transition-all hover:bg-slate-50 ${newInvestment.assetType === 'battery' ? 'border-slate-200 hover:border-emerald-500' : 'border-slate-200 hover:border-investor/50'}`}
>
<p className="font-semibold text-slate-800">{plan.name}</p>
<p className="text-xs text-slate-500 mt-1">{plan.evBasePrice.toLocaleString()} × {plan.minQuantity} {newInvestment.assetType}(s)</p>
<p className="text-sm text-slate-600 mt-1">Duration: {plan.duration} months</p>
</button>
))}
</div>
</div>
</>
) : (
<>
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<button onClick={() => setSelectedTemplate(null)} className="text-xs text-investor hover:underline"> Change Template</button>
<span className="text-slate-300">|</span>
<span className="text-sm font-medium text-slate-700">{selectedTemplate.name}</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Plan Name *</label>
<input
type="text"
value={newInvestment.planName}
onChange={(e) => setNewInvestment({ ...newInvestment, planName: e.target.value })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
placeholder="Plan name"
/>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Plan Type</label>
<select
value={newInvestment.planType}
onChange={(e) => setNewInvestment({ ...newInvestment, planType: e.target.value as any })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
>
<option value="silver">Silver</option>
<option value="gold">Gold</option>
<option value="platinum">Platinum</option>
<option value="diamond">Diamond</option>
</select>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">{selectedTemplate.type === 'battery' ? 'Battery Pack Cost (৳)' : 'EV Base Price (৳)'}</label>
<input type="number" value={selectedTemplate.evBasePrice} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-50 cursor-not-allowed" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Minimum Quantity ({selectedTemplate.type === 'battery' ? 'Packs' : 'Bikes'})</label>
<input type="number" value={selectedTemplate.minQuantity} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-50 cursor-not-allowed" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Min Investment ()</label>
<input type="number" value={selectedTemplate.evBasePrice * selectedTemplate.minQuantity} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100 cursor-not-allowed" />
<p className="text-xs text-slate-400 mt-1">= Qty × Base Price</p>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Investment Amount () *</label>
<input
type="number"
value={newInvestment.totalInvestment}
onChange={(e) => setNewInvestment({ ...newInvestment, totalInvestment: Number(e.target.value) })}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-yellow-50"
/>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Max Investment ()</label>
<input type="number" value={selectedTemplate.maxInvestment} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100 cursor-not-allowed" />
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 border border-slate-200">
<h4 className="text-sm font-semibold text-slate-800 mb-3 flex items-center gap-2">
<CreditCard className="w-4 h-4 text-investor" /> Payment Options
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
<label className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${newInvestment.paymentType === 'full' ? 'border-investor bg-investor/5' : 'border-slate-200 hover:border-slate-300'}`}>
<input
type="radio"
name="paymentType"
value="full"
checked={newInvestment.paymentType === 'full'}
onChange={() => setNewInvestment({ ...newInvestment, paymentType: 'full', initialPayment: newInvestment.totalInvestment })}
className="w-4 h-4 text-investor"
/>
<div className="flex-1">
<p className="font-medium text-slate-800 text-sm">Full Payment</p>
<p className="text-xs text-slate-500">Pay total amount at once</p>
</div>
<span className="text-lg font-bold text-green-600">{newInvestment.totalInvestment.toLocaleString()}</span>
</label>
<label className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${newInvestment.paymentType === 'partial' ? 'border-investor bg-investor/5' : 'border-slate-200 hover:border-slate-300'}`}>
<input
type="radio"
name="paymentType"
value="partial"
checked={newInvestment.paymentType === 'partial'}
onChange={() => setNewInvestment({ ...newInvestment, paymentType: 'partial', initialPayment: Math.floor(newInvestment.totalInvestment * 0.5) })}
className="w-4 h-4 text-investor"
/>
<div className="flex-1">
<p className="font-medium text-slate-800 text-sm">Partial Payment</p>
<p className="text-xs text-slate-500">Pay initial amount (50% min)</p>
</div>
<span className="text-sm font-bold text-amber-600">Min {Math.floor(newInvestment.totalInvestment * 0.5).toLocaleString()}</span>
</label>
</div>
{newInvestment.paymentType === 'partial' && (
<div className="border-t border-slate-200 pt-4 mt-4">
<label className="text-sm font-medium text-slate-600 mb-2 block">Initial Payment () *</label>
<input
type="number"
value={newInvestment.initialPayment}
onChange={(e) => {
const val = Number(e.target.value);
if (val >= newInvestment.totalInvestment * 0.5 && val <= newInvestment.totalInvestment) {
setNewInvestment({ ...newInvestment, initialPayment: val });
}
}}
className="w-full px-3 py-2 border border-investor rounded-lg text-sm bg-white"
/>
<p className="text-xs text-slate-500 mt-2">Balance: {(newInvestment.totalInvestment - newInvestment.initialPayment).toLocaleString()}</p>
</div>
)}
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Duration (Months)</label>
<input type="number" value={selectedTemplate.duration} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100 cursor-not-allowed" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Lock-in Period (Months)</label>
<input type="number" value={3} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100 cursor-not-allowed" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Exit Penalty (%)</label>
<input type="number" value={10} disabled className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-slate-100 cursor-not-allowed" />
</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
<h4 className="text-sm font-semibold text-amber-800 mb-3 flex items-center gap-2">
<TrendingUp className="w-4 h-4" /> FICO Share - Profit per Ride
</h4>
<p className="text-xs text-amber-600 mb-3">Profit sharing when bikes are rented to end customers</p>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-xs text-slate-500 mb-1 block">Single Rent (%)</label>
<input type="number" value={45} disabled className="w-full px-3 py-2 border border-amber-200 rounded-lg text-sm bg-white cursor-not-allowed" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Rent to Own (%)</label>
<input type="number" value={55} disabled className="w-full px-3 py-2 border border-amber-200 rounded-lg text-sm bg-white cursor-not-allowed" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Share EV (%)</label>
<input type="number" value={60} disabled className="w-full px-3 py-2 border border-amber-200 rounded-lg text-sm bg-white cursor-not-allowed" />
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Start Date *</label>
<input type="date" value={newInvestment.startDate} onChange={(e) => setNewInvestment({ ...newInvestment, startDate: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">End Date</label>
<input type="date" value={newInvestment.endDate} onChange={(e) => setNewInvestment({ ...newInvestment, endDate: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Payment Method</label>
<select value={newInvestment.paymentMethod} onChange={(e) => setNewInvestment({ ...newInvestment, paymentMethod: e.target.value as any })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white">
<option value="bank">Bank Transfer</option>
<option value="mobile">Mobile Banking</option>
<option value="cash">Cash</option>
<option value="cheque">Cheque</option>
</select>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Transaction Reference</label>
<input type="text" value={newInvestment.transactionReference} onChange={(e) => setNewInvestment({ ...newInvestment, transactionReference: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Auto-generated if empty" />
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-600 mb-1 block">Description</label>
<textarea value={newInvestment.notes} onChange={(e) => setNewInvestment({ ...newInvestment, notes: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" rows={2} placeholder="Add notes..." />
</div>
<div className="bg-green-50 border border-green-200 rounded-xl p-4">
<h4 className="font-semibold text-green-800 mb-3 flex items-center gap-2">
<TrendingUp className="w-4 h-4" /> Auto-Journal Entry
</h4>
<div className="bg-white rounded-lg p-4 border border-green-100">
<div className="flex items-center justify-between text-sm mb-2">
<span className="text-slate-500">Date:</span>
<span className="font-medium">{newInvestment.startDate || 'Not set'}</span>
</div>
<div className="flex items-center justify-between text-sm mb-3">
<span className="text-slate-500">Reference:</span>
<span className="font-medium">{newInvestment.transactionReference || `INV/${new Date().getFullYear()}/auto`}</span>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-200">
<div>
<p className="text-xs text-green-600 font-medium uppercase">Debit (Dr)</p>
<p className="font-medium text-slate-800">{newInvestment.paymentMethod === 'bank' ? 'Bank - City Bank' : newInvestment.paymentMethod === 'cash' ? 'Cash in Hand' : 'bKash Business'} ({newInvestment.paymentMethod === 'bank' ? '1200' : newInvestment.paymentMethod === 'cash' ? '1100' : '1300'})</p>
</div>
<p className="font-bold text-green-700">{newInvestment.paymentType === 'partial' ? newInvestment.initialPayment.toLocaleString() : newInvestment.totalInvestment.toLocaleString()}</p>
</div>
<div className="flex justify-center">
<div className="w-8 h-8 rounded-full bg-green-200 flex items-center justify-center">
<span className="text-green-600"></span>
</div>
</div>
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg border border-blue-200">
<div>
<p className="text-xs text-blue-600 font-medium uppercase">Credit (Cr)</p>
<p className="font-medium text-slate-800">Investor Liabilities (2200)</p>
</div>
<p className="font-bold text-blue-700">{newInvestment.totalInvestment.toLocaleString()}</p>
</div>
</div>
</div>
</div>
</>
)}
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3">
<button onClick={() => setShowCreateModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">Cancel</button>
{selectedTemplate && (
<button onClick={handleCreate} disabled={!newInvestment.planName || !newInvestment.totalInvestment} className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark disabled:opacity-50 flex items-center gap-2">
<Plus className="w-4 h-4" /> Create Investment
</button>
)}
</div>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,98 +0,0 @@
import { Wallet, TrendingUp, Bike, DollarSign, Calendar, Download, ArrowUpRight } from 'lucide-react';
import { investors, bikes } from '@/data/mockData';
const investor = investors[0];
const investorBikes = bikes.filter(b => b.investorId === investor?.id);
export default function InvestorPortfolioPage() {
return (
<div className="p-4 lg:p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">My Portfolio</h1>
<p className="text-sm text-slate-500 mt-1">Track your investments</p>
</div>
<button className="py-2 px-4 bg-accent text-white rounded-lg text-sm font-semibold hover:bg-accent-dark flex items-center gap-2">
<Download className="w-4 h-4" /> Export Statement
</button>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3 mb-2">
<Wallet className="w-5 h-5 text-purple-600" />
<span className="text-sm text-slate-500">Invested</span>
</div>
<p className="text-2xl font-extrabold text-slate-800">{investor.totalInvested.toLocaleString()}</p>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3 mb-2">
<TrendingUp className="w-5 h-5 text-green-600" />
<span className="text-sm text-slate-500">Earnings</span>
</div>
<p className="text-2xl font-extrabold text-green-600">{investor.totalEarnings.toLocaleString()}</p>
<span className="text-xs text-green-600 flex items-center gap-1 mt-1">
<ArrowUpRight className="w-3 h-3" /> +2.3%
</span>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3 mb-2">
<Bike className="w-5 h-5 text-blue-600" />
<span className="text-sm text-slate-500">Active Bikes</span>
</div>
<p className="text-2xl font-extrabold text-slate-800">{investor.activeBikes}</p>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
<div className="flex items-center gap-3 mb-2">
<DollarSign className="w-5 h-5 text-amber-600" />
<span className="text-sm text-slate-500">ROI</span>
</div>
<p className="text-2xl font-extrabold text-slate-800">{investor.roi}%</p>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
<div className="px-5 py-4 border-b border-slate-100">
<h2 className="font-bold text-slate-800">My Bikes</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Bike</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Plate</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Location</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Status</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Earnings</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{investorBikes.map(bike => (
<tr key={bike.id} className="hover:bg-slate-50">
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<Bike className="w-5 h-5 text-slate-400" />
<span className="text-sm font-medium text-slate-700">{bike.model}</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-slate-600">{bike.plateNumber}</td>
<td className="px-4 py-3 text-sm text-slate-600">{bike.location}</td>
<td className="px-4 py-3">
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${
bike.status === 'rented' ? 'bg-green-100 text-green-700' :
bike.status === 'available' ? 'bg-blue-100 text-blue-700' :
'bg-amber-100 text-amber-700'
}`}>
{bike.status}
</span>
</td>
<td className="px-4 py-3 text-sm font-semibold text-green-600">2,500</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,379 @@
'use client';
import { useState } from 'react';
import {
History, Bike, DollarSign, Clock, User, Download, Search,
ChevronLeft, ChevronRight, CheckCircle, XCircle, AlertCircle, Calendar
} from 'lucide-react';
import { investors, bikes, rentalPayments } from '@/data/mockData';
import InvestorNotification from '@/components/InvestorNotification';
export default function RentalHistoryPage() {
const investor = investors.find(i => i.id === 'inv1') || investors[0];
const investorBikes = bikes.filter(b => b.investorId === investor.id);
const investorPayments = rentalPayments.filter(p => p.investorId === investor.id);
const [bikeFilter, setBikeFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [sortBy, setSortBy] = useState<'date' | 'amount'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [page, setPage] = useState(1);
const [searchQuery, setSearchQuery] = useState('');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const pageSize = 10;
const filteredPayments = investorPayments.filter(p => {
if (bikeFilter !== 'all' && p.bikeId !== bikeFilter) return false;
if (statusFilter !== 'all' && p.status !== statusFilter) return false;
if (searchQuery && !p.bikerName.toLowerCase().includes(searchQuery.toLowerCase()) &&
!p.bikeModel.toLowerCase().includes(searchQuery.toLowerCase()) &&
!p.plateNumber.toLowerCase().includes(searchQuery.toLowerCase())) return false;
if (dateFrom && new Date(p.date) < new Date(dateFrom)) return false;
if (dateTo && new Date(p.date) > new Date(dateTo)) return false;
return true;
});
const sortedPayments = [...filteredPayments].sort((a, b) => {
if (sortBy === 'date') {
return sortOrder === 'asc'
? new Date(a.date).getTime() - new Date(b.date).getTime()
: new Date(b.date).getTime() - new Date(a.date).getTime();
} else {
return sortOrder === 'asc' ? a.amount - b.amount : b.amount - a.amount;
}
});
const totalPages = Math.ceil(sortedPayments.length / pageSize);
const paginatedPayments = sortedPayments.slice((page - 1) * pageSize, page * pageSize);
const totalCollected = investorPayments.filter(p => p.status === 'paid').reduce((sum, p) => sum + p.amount, 0);
const totalPending = investorPayments.filter(p => p.status === 'pending').reduce((sum, p) => sum + p.amount, 0);
const totalFailed = investorPayments.filter(p => p.status === 'failed').reduce((sum, p) => sum + p.amount, 0);
const activeRentals = new Set(investorPayments.filter(p => p.status === 'paid').map(p => p.bikeId)).size;
const statusConfig: Record<string, { label: string; bg: string; color: string; icon: any }> = {
paid: { label: 'Paid', bg: 'bg-green-100', color: 'text-green-700', icon: CheckCircle },
pending: { label: 'Pending', bg: 'bg-amber-100', color: 'text-amber-700', icon: Clock },
failed: { label: 'Failed', bg: 'bg-red-100', color: 'text-red-700', icon: XCircle },
};
const planConfig: Record<string, { label: string; bg: string; color: string }> = {
single: { label: 'Single Rent', bg: 'bg-green-100', color: 'text-green-700' },
'rent-to-own': { label: 'Rent to Own', bg: 'bg-blue-100', color: 'text-blue-700' },
share_ev: { label: 'Share EV', bg: 'bg-purple-100', color: 'text-purple-700' },
};
return (
<div className="min-h-screen lg:pt-6 pt-0">
<InvestorNotification isMobile />
<div className="pt-18 lg:pt-0 p-4 sm:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
{/* Header */}
<div className="mb-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-xl sm:text-2xl font-bold text-slate-800 flex items-center gap-2">
<History className="w-5 h-5 sm:w-6 sm:h-6 text-investor" /> Rental History
</h1>
<p className="text-sm text-slate-500 mt-1">Track daily rental payments from your bikes</p>
</div>
<button className="px-4 py-2.5 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-2 shadow-sm w-fit">
<Download className="w-4 h-4" /> Export
</button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center shrink-0">
<DollarSign className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xs text-slate-500">Total Collected</p>
<p className="text-lg font-bold text-green-600">{totalCollected.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center shrink-0">
<Bike className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-xs text-slate-500">Active Rentals</p>
<p className="text-lg font-bold text-slate-800">{activeRentals}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-100 rounded-lg flex items-center justify-center shrink-0">
<Clock className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="text-xs text-slate-500">Pending</p>
<p className="text-lg font-bold text-amber-600">{totalPending.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center shrink-0">
<AlertCircle className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-xs text-slate-500">Failed</p>
<p className="text-lg font-bold text-red-600">{totalFailed.toLocaleString()}</p>
</div>
</div>
</div>
</div>
{/* Main Table Card */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
{/* Filters */}
<div className="p-4 border-b border-slate-100">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
type="text"
placeholder="Search biker, bike..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm w-48 sm:w-64"
/>
</div>
<select
value={bikeFilter}
onChange={(e) => { setBikeFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
>
<option value="all">All Bikes</option>
{investorBikes.map(bike => (
<option key={bike.id} value={bike.id}>{bike.model}</option>
))}
</select>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
>
<option value="all">All Status</option>
<option value="paid">Paid</option>
<option value="pending">Pending</option>
<option value="failed">Failed</option>
</select>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 text-xs text-slate-500 font-medium">
<Calendar className="w-4 h-4" /> Date Range
</div>
<div className="flex items-center gap-1">
<input
type="date"
value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
<span className="text-slate-400">to</span>
<input
type="date"
value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white"
/>
</div>
{(dateFrom || dateTo) && (
<button
onClick={() => { setDateFrom(''); setDateTo(''); setPage(1); }}
className="px-2 py-1 text-xs text-red-500 hover:bg-red-50 rounded"
>
Clear
</button>
)}
</div>
</div>
</div>
{/* Card View - Mobile/Tablet */}
<div className="lg:hidden divide-y divide-slate-100">
{paginatedPayments.length > 0 ? paginatedPayments.map((payment) => {
const status = statusConfig[payment.status] || statusConfig.pending;
const plan = planConfig[payment.planType] || planConfig.single;
const StatusIcon = status.icon;
return (
<div key={payment.id} className="p-4 hover:bg-slate-50">
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<Bike className="w-4 h-4 text-slate-400 shrink-0" />
<p className="text-sm font-semibold text-slate-800 truncate">{payment.bikeModel}</p>
</div>
<p className="text-xs text-slate-400 ml-6">{payment.plateNumber}</p>
<div className="flex items-center gap-2 mt-1 ml-6">
<User className="w-3 h-3 text-slate-400" />
<p className="text-xs text-slate-600">{payment.bikerName}</p>
</div>
</div>
<div className="text-right shrink-0">
<p className="text-base font-bold text-slate-800">{payment.amount.toLocaleString()}</p>
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium mt-1 ${status.bg} ${status.color}`}>
<StatusIcon className="w-3 h-3" /> {status.label}
</span>
</div>
</div>
<div className="flex items-center justify-between ml-6 text-xs text-slate-400">
<span>{payment.date}</span>
<span className={`px-2 py-0.5 rounded ${plan.bg} ${plan.color}`}>{plan.label}</span>
</div>
</div>
);
}) : (
<div className="p-8 text-center text-slate-500">
<History className="w-10 h-10 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</div>
)}
</div>
{/* Table View - Desktop */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th
onClick={() => {
if (sortBy === 'date') setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
else { setSortBy('date'); setSortOrder('desc'); }
}}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Date {sortBy === 'date' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Bike</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Biker</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Plan</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Duration</th>
<th
onClick={() => {
if (sortBy === 'amount') setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
else { setSortBy('amount'); setSortOrder('desc'); }
}}
className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase cursor-pointer hover:bg-slate-100"
>
<div className="flex items-center gap-1">
Amount {sortBy === 'amount' && <span className="text-investor">{sortOrder === 'asc' ? '↑' : '↓'}</span>}
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Method</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedPayments.length > 0 ? paginatedPayments.map((payment) => {
const status = statusConfig[payment.status] || statusConfig.pending;
const plan = planConfig[payment.planType] || planConfig.single;
const StatusIcon = status.icon;
return (
<tr key={payment.id} className="hover:bg-slate-50">
<td className="px-4 py-3">
<div>
<p className="text-sm font-medium text-slate-800">{payment.date}</p>
<p className="text-xs text-slate-400">{payment.transactionId || payment.id}</p>
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Bike className="w-4 h-4 text-slate-400" />
<div>
<p className="text-sm font-medium text-slate-800">{payment.bikeModel}</p>
<p className="text-xs text-slate-400">{payment.plateNumber}</p>
</div>
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-slate-400" />
<span className="text-sm text-slate-700">{payment.bikerName}</span>
</div>
</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${plan.bg} ${plan.color}`}>
{plan.label}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600">{payment.duration}</span>
</td>
<td className="px-4 py-3">
<p className="text-sm font-bold text-slate-800">{payment.amount.toLocaleString()}</p>
</td>
<td className="px-4 py-3">
<span className="text-sm text-slate-600 capitalize">{payment.paymentMethod}</span>
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${status.bg} ${status.color}`}>
<StatusIcon className="w-3 h-3" /> {status.label}
</span>
</td>
</tr>
);
}) : (
<tr>
<td colSpan={8} className="px-4 py-12 text-center text-slate-500">
<History className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p>No rental payments found</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
{sortedPayments.length > pageSize && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3">
<p className="text-xs sm:text-sm text-slate-500">
Showing {((page - 1) * pageSize) + 1} to {Math.min(page * pageSize, sortedPayments.length)} of {sortedPayments.length}
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronLeft className="w-4 h-4" />
</button>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageNum = totalPages <= 5 ? i + 1 : page <= 3 ? i + 1 : page >= totalPages - 2 ? totalPages - 4 + i : page - 2 + i;
return (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`w-8 h-8 rounded-lg text-sm font-medium ${page === pageNum ? 'bg-investor text-white' : 'border border-slate-200 hover:bg-slate-50'}`}
>
{pageNum}
</button>
);
})}
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-2 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,123 +1,590 @@
import { DollarSign, Banknote, Clock, Check, X, AlertCircle } from 'lucide-react'; 'use client';
import { investors } from '@/data/mockData';
const investor = investors[0]; import { useState } from 'react';
import { CreditCard, Wallet, History, CheckCircle, Clock, Building2, Smartphone, AlertCircle, Settings, X, Bike, ChevronDown, Search, Filter, ChevronLeft, ChevronRight } from 'lucide-react';
import { investors, transactions, bikes } from '@/data/mockData';
import toast from 'react-hot-toast';
import InvestorNotification from '@/components/InvestorNotification';
const withdrawalHistory = [ export default function InvestorWithdrawPage() {
{ id: 'w1', amount: 3000, method: 'bKash', status: 'pending', date: '2024-03-20' }, const investor = investors.find(i => i.id === 'inv1') || investors[0];
{ id: 'w2', amount: 5000, method: 'Bank Transfer', status: 'completed', date: '2024-03-15' }, const investorBikes = bikes.filter(b => b.investorId === investor.id);
{ id: 'w3', amount: 2500, method: 'bKash', status: 'completed', date: '2024-02-28' }, const availableBalance = investor.totalEarnings - investor.totalWithdrawn - investor.pendingEarnings;
];
const allWithdrawals = transactions.filter(t => t.investorId === investor.id && t.type === 'withdrawal').sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
const [selectAll, setSelectAll] = useState(false);
const [selectedPlans, setSelectedPlans] = useState<string[]>([]);
const [selectedBikes, setSelectedBikes] = useState<string[]>([]);
const [paymentMethod, setPaymentMethod] = useState<'bank' | 'mobile'>('bank');
const [selectedAccount, setSelectedAccount] = useState('');
const [showAutoWithdrawModal, setShowAutoWithdrawModal] = useState(false);
const [autoWithdrawEnabled, setAutoWithdrawEnabled] = useState(false);
const [autoWithdrawFreq, setAutoWithdrawFreq] = useState('as_per_request');
const [autoWithdrawMin, setAutoWithdrawMin] = useState('1000');
const [autoWithdrawAccount, setAutoWithdrawAccount] = useState('');
const [withdrawPage, setWithdrawPage] = useState(1);
const [withdrawPageSize, setWithdrawPageSize] = useState(5);
const [withdrawFilter, setWithdrawFilter] = useState('all');
const planColors: Record<string, string> = {
silver: 'bg-slate-100 text-slate-700',
gold: 'bg-amber-100 text-amber-700',
platinum: 'bg-purple-100 text-purple-700',
diamond: 'bg-blue-100 text-blue-700',
};
const bikeStatusColors: Record<string, { bg: string; text: string }> = {
available: { bg: 'bg-blue-50', text: 'text-blue-600' },
rented: { bg: 'bg-green-50', text: 'text-green-600' },
maintenance: { bg: 'bg-amber-50', text: 'text-amber-600' },
};
const assignedBikes = investorBikes;
const calculatedAmount = selectAll
? assignedBikes.reduce((sum: number, b: any) => sum + (b.totalEarnings || 0), 0)
: selectedBikes.reduce((sum: number, bikeId: string) => {
const bike = assignedBikes.find((b: any) => b.id === bikeId);
return sum + (bike?.totalEarnings || 0);
}, 0);
const filteredWithdrawals = allWithdrawals.filter(w => withdrawFilter === 'all' || w.status === withdrawFilter);
const totalWithdrawPages = Math.ceil(filteredWithdrawals.length / withdrawPageSize);
const paginatedWithdrawals = filteredWithdrawals.slice((withdrawPage - 1) * withdrawPageSize, withdrawPage * withdrawPageSize);
const totalCompleted = allWithdrawals.filter(w => w.status === 'completed').reduce((sum, w) => sum + w.amount, 0);
const totalPending = allWithdrawals.filter(w => w.status === 'pending').reduce((sum, w) => sum + w.amount, 0);
const toggleSelectAll = (checked: boolean) => {
setSelectAll(checked);
if (checked) {
setSelectedPlans(investor.investments?.map((inv: any) => inv.id) || []);
setSelectedBikes(assignedBikes.map(b => b.id));
} else {
setSelectedPlans([]);
setSelectedBikes([]);
}
};
const togglePlan = (planId: string, invBikes: any[]) => {
const isSelected = selectedPlans.includes(planId);
if (isSelected) {
setSelectedPlans(selectedPlans.filter(p => p !== planId));
setSelectedBikes(selectedBikes.filter(b => !invBikes.find((ib: any) => ib.id === b)));
} else {
setSelectedPlans([...selectedPlans, planId]);
setSelectedBikes([...new Set([...selectedBikes, ...invBikes.map((b: any) => b.id)])]);
}
setSelectAll(false);
};
const toggleBike = (bikeId: string) => {
setSelectedBikes(prev => prev.includes(bikeId) ? prev.filter(b => b !== bikeId) : [...prev, bikeId]);
setSelectAll(false);
};
const handleSubmitWithdraw = () => {
if (!selectedAccount) {
toast.error('Please select a payment method');
return;
}
if (selectedBikes.length === 0 && !selectAll) {
toast.error('Please select at least one bike or select all');
return;
}
toast.success(`Withdrawal request for ৳${calculatedAmount.toLocaleString()} submitted successfully.`);
setSelectedPlans([]);
setSelectedBikes([]);
setSelectAll(false);
setShowWithdrawModal(false);
};
const handleSaveAutoWithdraw = () => {
toast.success('Auto-withdraw settings saved!');
setShowAutoWithdrawModal(false);
};
export default function WithdrawPage() {
return ( return (
<div className="p-4 lg:p-6"> <div className="min-h-screen lg:pt-6 pt-0 ">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6"> <InvestorNotification isMobile />
<div> <div className="pt-18 lg:pt-0 p-4 lg:p-6 max-w-8xl mx-auto mb-12 lg:mb-0">
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Withdraw Earnings</h1> {/* Header */}
<p className="text-sm text-slate-500 mt-1">Request withdrawals to your bank or bKash</p> <div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
</div> <div>
</div> <h1 className="text-xl lg:text-2xl font-bold text-slate-800 flex items-center gap-2">
<CreditCard className="w-5 h-5 lg:w-6 lg:h-6 text-investor" /> Withdraw Funds
<div className="grid lg:grid-cols-3 gap-6 mb-8"> </h1>
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-100 p-6"> <p className="text-sm text-slate-500 mt-1">Request withdrawals to your bank or mobile banking accounts</p>
<h2 className="font-bold text-slate-800 mb-4">Request Withdrawal</h2>
<div className="mb-4">
<label className="text-sm font-medium text-slate-600 mb-2 block">Available Balance</label>
<p className="text-3xl font-extrabold text-green-600">{(investor.totalEarnings - investor.withdrawalPending).toLocaleString()}</p>
</div> </div>
<div className="mb-4"> <button
<label className="text-sm font-medium text-slate-600 mb-2 block">Amount</label> onClick={() => setShowAutoWithdrawModal(true)}
<input className="px-4 py-2.5 border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-50 flex items-center gap-2 shadow-sm bg-white w-fit"
type="number" >
placeholder="Enter amount" <Settings className="w-4 h-4" /> Configure Auto-Withdraw
className="w-full px-4 py-3 border border-slate-200 rounded-lg text-lg focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
/>
</div>
<div className="mb-4">
<label className="text-sm font-medium text-slate-600 mb-2 block">Withdrawal Method</label>
<div className="grid grid-cols-2 gap-3">
<button className="py-3 px-4 border-2 border-accent bg-accent-light rounded-lg font-medium text-accent">
bKash
</button>
<button className="py-3 px-4 border border-slate-200 rounded-lg font-medium text-slate-600 hover:bg-slate-50">
Bank Transfer
</button>
</div>
</div>
<div className="mb-4">
<label className="text-sm font-medium text-slate-600 mb-2 block">Account Number</label>
<input
type="text"
placeholder="01XXXXXXXXX"
className="w-full px-4 py-3 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
/>
</div>
<button className="w-full py-3 bg-accent text-white rounded-lg font-semibold hover:bg-accent-dark">
Request Withdrawal
</button> </button>
</div> </div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-5"> {/* Balance Cards - Mobile Responsive Grid */}
<h2 className="font-bold text-slate-800 mb-4">Quick Withdraw</h2> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
<div className="space-y-2 mb-4"> <div className="sm:col-span-2 bg-investor to-investor-dark rounded-xl p-5 text-white shadow-lg relative overflow-hidden order-first lg:order-first">
<button className="w-full py-2 px-4 border border-slate-200 rounded-lg text-sm text-left hover:bg-slate-50"> <div className="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/3 blur-2xl"></div>
1,000 <div className="relative z-10">
</button> <div className="flex items-center gap-2 text-investor-light mb-2">
<button className="w-full py-2 px-4 border border-slate-200 rounded-lg text-sm text-left hover:bg-slate-50"> <Wallet className="w-4 h-4" />
2,500 <span className="font-medium text-xs">Available Balance</span>
</button> </div>
<button className="w-full py-2 px-4 border border-slate-200 rounded-lg text-sm text-left hover:bg-slate-50"> <p className="text-3xl font-bold mb-1">{availableBalance.toLocaleString()}</p>
5,000 <p className="text-xs text-white/80">Ready to withdraw</p>
</button> </div>
<button className="w-full py-2 px-4 border border-slate-200 rounded-lg text-sm text-left hover:bg-slate-50">
All Available
</button>
</div> </div>
<div className="p-3 bg-blue-50 rounded-lg"> <div className="bg-white rounded-xl p-4 border border-slate-200 shadow-sm">
<p className="text-sm text-blue-700 flex items-center gap-2"> <p className="text-xs text-amber-600 font-medium mb-1">Pending Request</p>
<AlertCircle className="w-4 h-4" /> <p className="text-xl font-bold text-amber-600">{investor.pendingEarnings.toLocaleString()}</p>
Processing time: 1-3 business days </div>
</p> <div className="bg-white rounded-xl p-4 border border-slate-200 shadow-sm">
<p className="text-xs text-slate-600 font-medium mb-1">Total Withdrawn</p>
<p className="text-xl font-bold text-slate-700">{investor.totalWithdrawn.toLocaleString()}</p>
</div> </div>
</div> </div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden"> {/* Alert + Action Button */}
<div className="px-5 py-4 border-b border-slate-100"> <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 mb-6">
<h2 className="font-bold text-slate-800">Withdrawal History</h2> <div className="flex-1 bg-amber-50 border border-amber-200 rounded-xl p-4 flex gap-3 order-last sm:order-first">
<AlertCircle className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<div>
<h4 className="text-sm font-bold text-amber-800 mb-1">Pending Requests</h4>
<p className="text-xs text-amber-700">You currently have <b>{investor.pendingEarnings.toLocaleString()}</b> in pending withdrawals. Processing takes 1-3 business days.</p>
</div>
</div>
<button
onClick={() => setShowWithdrawModal(true)}
className="px-5 py-3 bg-investor text-white rounded-xl font-semibold text-sm shadow-md hover:bg-investor-dark transition-all flex items-center justify-center gap-2 whitespace-nowrap sm:w-auto"
>
<CreditCard className="w-4 h-4" /> Withdrawal Request
</button>
</div> </div>
<div className="overflow-x-auto">
<table className="w-full"> {/* Recent Withdrawals Card */}
<thead className="bg-slate-50"> <div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<tr> <div className="p-4 border-b border-slate-100 bg-slate-50">
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Date</th> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Method</th> <h3 className="font-semibold text-slate-800 flex items-center gap-2">
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Amount</th> <History className="w-5 h-5 text-slate-400" /> Recent Withdrawals
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Status</th> </h3>
</tr> <div className="flex items-center gap-2">
</thead> <select
<tbody className="divide-y divide-slate-50"> value={withdrawFilter}
{withdrawalHistory.map(w => ( onChange={(e) => { setWithdrawFilter(e.target.value); setWithdrawPage(1); }}
<tr key={w.id} className="hover:bg-slate-50"> className="px-3 py-1.5 border border-slate-200 rounded-lg text-xs bg-white"
<td className="px-4 py-3 text-sm text-slate-600">{w.date}</td> >
<td className="px-4 py-3 text-sm text-slate-600">{w.method}</td> <option value="all">All</option>
<td className="px-4 py-3 text-sm font-semibold text-slate-700">{w.amount}</td> <option value="completed">Completed</option>
<td className="px-4 py-3"> <option value="pending">Pending</option>
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full ${ </select>
w.status === 'completed' ? 'bg-green-100 text-green-700' : </div>
'bg-amber-100 text-amber-700' </div>
}`}> </div>
{w.status === 'completed' ? <Check className="w-3 h-3" /> : <Clock className="w-3 h-3" />}
{w.status}
{/* Card View - Mobile/Tablet */}
<div className="lg:hidden divide-y divide-slate-100">
{paginatedWithdrawals.length > 0 ? paginatedWithdrawals.map((t) => (
<div key={t.id} className="p-4 sm:p-5 hover:bg-slate-50 transition-colors">
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-800 mb-1">{t.description || 'Withdrawal'}</p>
<div className="flex items-center gap-2 text-xs text-slate-400">
<span>{t.createdAt}</span>
{t.referenceNumber && (
<>
<span>&bull;</span>
<span className="hidden sm:inline">{t.referenceNumber}</span>
</>
)}
</div>
</div>
<div className="text-right shrink-0">
<p className="text-base font-bold text-slate-800">{t.amount.toLocaleString()}</p>
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-semibold mt-1 ${t.status === 'completed' ? 'bg-green-100 text-green-700' :
t.status === 'pending' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700'
}`}>
{t.status === 'completed' && <CheckCircle className="w-3 h-3" />}
{t.status === 'pending' && <Clock className="w-3 h-3" />}
{t.status}
</span> </span>
</td> </div>
</div>
</div>
)) : (
<div className="px-4 py-12 text-center text-sm text-slate-500">
<History className="w-10 h-10 mx-auto mb-2 text-slate-300" />
No withdrawals found.
</div>
)}
</div>
{/* Table View - Desktop */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-white border-b border-slate-100">
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Date</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider hidden sm:table-cell">Ref</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Description</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Amount</th>
<th className="px-4 py-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
</tr> </tr>
))} </thead>
</tbody> <tbody className="divide-y divide-slate-100">
</table> {paginatedWithdrawals.length > 0 ? paginatedWithdrawals.map((t) => (
<tr key={t.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3.5 text-xs text-slate-600 whitespace-nowrap">{t.createdAt}</td>
<td className="px-4 py-3.5 text-xs text-slate-500 hidden sm:table-cell">{t.referenceNumber || '-'}</td>
<td className="px-4 py-3.5">
<p className="text-sm font-medium text-slate-800">{t.description || 'Withdrawal'}</p>
</td>
<td className="px-4 py-3.5 text-sm font-bold text-slate-800">{t.amount.toLocaleString()}</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-semibold ${t.status === 'completed' ? 'bg-green-100 text-green-700' :
t.status === 'pending' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700'
}`}>
{t.status === 'completed' && <CheckCircle className="w-3 h-3" />}
{t.status === 'pending' && <Clock className="w-3 h-3" />}
{t.status}
</span>
</td>
</tr>
)) : (
<tr>
<td colSpan={5} className="px-4 py-12 text-center text-sm text-slate-500">
<History className="w-10 h-10 mx-auto mb-2 text-slate-300" />
No withdrawals found.
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
{totalWithdrawPages > 1 && (
<div className="p-4 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-3">
<p className="text-xs text-slate-500">
Showing {((withdrawPage - 1) * withdrawPageSize) + 1} to {Math.min(withdrawPage * withdrawPageSize, filteredWithdrawals.length)} of {filteredWithdrawals.length}
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setWithdrawPage(p => Math.max(1, p - 1))}
disabled={withdrawPage === 1}
className="p-1.5 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronLeft className="w-4 h-4 text-slate-500" />
</button>
{Array.from({ length: Math.min(5, totalWithdrawPages) }, (_, i) => {
const page = totalWithdrawPages <= 5 ? i + 1 : withdrawPage <= 3 ? i + 1 : withdrawPage >= totalWithdrawPages - 2 ? totalWithdrawPages - 4 + i : withdrawPage - 2 + i;
return (
<button
key={page}
onClick={() => setWithdrawPage(page)}
className={`w-8 h-8 rounded-lg text-xs font-medium ${withdrawPage === page ? 'bg-investor text-white' : 'border border-slate-200 hover:bg-slate-50'}`}
>
{page}
</button>
);
})}
<button
onClick={() => setWithdrawPage(p => Math.min(totalWithdrawPages, p + 1))}
disabled={withdrawPage === totalWithdrawPages}
className="p-1.5 border border-slate-200 rounded-lg disabled:opacity-50 hover:bg-slate-50"
>
<ChevronRight className="w-4 h-4 text-slate-500" />
</button>
</div>
</div>
)}
</div> </div>
{/* Withdrawal Request Modal */}
{
showWithdrawModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="p-4 sm:p-5 border-b border-slate-100 flex items-center justify-between bg-gradient-to-r from-slate-50 to-white">
<h3 className="text-base sm:text-lg font-bold text-slate-800">Create Withdrawal Request</h3>
<button onClick={() => setShowWithdrawModal(false)} className="p-1.5 hover:bg-slate-100 rounded-lg transition-colors">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-4 sm:p-5 overflow-y-auto flex-1 space-y-4 sm:space-y-5">
{/* Balance Cards */}
<div className="grid grid-cols-3 gap-2 sm:gap-3">
<div className="bg-green-50 rounded-lg p-3 border border-green-200">
<p className="text-xs text-green-600 font-medium">Available</p>
<p className="text-sm font-bold text-green-700">{availableBalance.toLocaleString()}</p>
</div>
<div className="bg-amber-50 rounded-lg p-3 border border-amber-200">
<p className="text-xs text-amber-600 font-medium">Pending</p>
<p className="text-sm font-bold text-amber-700">{investor.pendingEarnings.toLocaleString()}</p>
</div>
<div className="bg-slate-50 rounded-lg p-3 border border-slate-200">
<p className="text-xs text-slate-600 font-medium">Withdrawn</p>
<p className="text-sm font-bold text-slate-700">{investor.totalWithdrawn.toLocaleString()}</p>
</div>
</div>
{/* Select EV Investment Plans & Bikes */}
<div>
<h4 className="font-semibold text-slate-800 mb-2">Select EV Investment Plans & Bikes</h4>
<div className="space-y-3">
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-200">
<input
type="checkbox"
id="selectAll"
checked={selectAll}
onChange={(e) => toggleSelectAll(e.target.checked)}
className="w-5 h-5 text-investor rounded border-slate-300"
/>
<label htmlFor="selectAll" className="flex-1 cursor-pointer">
<span className="font-semibold text-slate-800">Select All</span>
<p className="text-xs text-slate-500">Include all investments and bikes</p>
</label>
</div>
<div className="border border-slate-200 rounded-lg overflow-hidden">
<div className="bg-slate-100 px-3 py-2 border-b border-slate-200">
<p className="text-sm font-semibold text-slate-700">EV Investment Plans</p>
</div>
<div className="divide-y divide-slate-100">
{investor.investments?.map((inv: any) => {
const invBikes = assignedBikes.filter((b: any) => b.investmentId === inv.id);
const invEarnings = invBikes.reduce((sum: number, b: any) => sum + (b.totalEarnings || 0), 0);
return (
<div key={inv.id} className="p-3 sm:p-4">
<div className="flex items-center gap-3 mb-2">
<input
type="checkbox"
id={`plan-${inv.id}`}
checked={selectAll || selectedPlans.includes(inv.id)}
disabled={selectAll}
onChange={() => togglePlan(inv.id, invBikes)}
className="w-4 h-4 text-investor rounded border-slate-300"
/>
<label htmlFor={`plan-${inv.id}`} className="flex-1 flex items-center justify-between cursor-pointer">
<div className="flex items-center gap-2">
<span className="font-medium text-slate-800 text-sm">{inv.planName}</span>
<span className={`px-2 py-0.5 rounded text-xs capitalize font-medium ${planColors[inv.planType]}`}>{inv.planType}</span>
</div>
<span className="text-sm font-bold text-green-600">{invEarnings.toLocaleString()}</span>
</label>
</div>
{!selectAll && invBikes.length > 0 && (
<div className="ml-6 sm:ml-8 pl-3 sm:pl-4 border-l-2 border-slate-200 space-y-2">
{invBikes.map((bike: any) => (
<div key={bike.id} className="flex items-center gap-2 p-2 bg-white rounded-lg border border-slate-100">
<input
type="checkbox"
id={`bike-${bike.id}`}
checked={selectAll || selectedBikes.includes(bike.id)}
onChange={() => toggleBike(bike.id)}
className="w-4 h-4 text-investor rounded border-slate-300"
/>
<label htmlFor={`bike-${bike.id}`} className="flex-1 flex items-center gap-2 cursor-pointer">
<Bike className="w-4 h-4 text-slate-400" />
<div>
<span className="text-sm font-medium text-slate-700">{bike.model}</span>
<span className="text-xs text-slate-400 ml-2">{bike.plateNumber}</span>
</div>
</label>
<span className="text-xs text-green-600 font-semibold">{bike.totalEarnings?.toLocaleString() || 0}</span>
</div>
))}
</div>
)}
</div>
);
})}
</div>
</div>
</div>
</div>
{/* Withdrawal Amount */}
<div className="bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-xl p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-slate-800 text-sm">Withdrawal Amount</h4>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${selectAll ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
{selectAll ? 'All Selected' : `${selectedBikes.length} bikes`}
</span>
</div>
<p className="text-2xl sm:text-3xl font-bold text-green-700 mb-1">{calculatedAmount.toLocaleString()}</p>
<p className="text-xs text-slate-500">Based on {selectAll ? 'all' : selectedBikes.length} selected bike(s) earnings</p>
</div>
{/* Payment Method */}
<div>
<h4 className="font-semibold text-slate-800 mb-2">Payment Method</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{(investor as any).bankAccounts?.map((account: any) => (
<div
key={account.id}
onClick={() => { setPaymentMethod('bank'); setSelectedAccount(account.id); }}
className={`p-3 sm:p-4 rounded-lg border cursor-pointer transition-all ${selectedAccount === account.id ? 'border-investor bg-investor/5 ring-2 ring-investor/20' : 'border-slate-200 hover:border-slate-300'}`}
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center bg-slate-100">
<Building2 className="w-4 h-4 sm:w-5 sm:h-5 text-slate-500" />
</div>
<div>
<p className="text-sm font-semibold text-slate-800">{account.bankName}</p>
<p className="text-xs text-slate-500 font-mono">{account.accountNumber}</p>
</div>
</div>
</div>
))}
</div>
{(investor as any).mobileBanking && (
<div
onClick={() => { setPaymentMethod('mobile'); setSelectedAccount('mobile'); }}
className={`mt-2 p-3 sm:p-4 rounded-lg border cursor-pointer transition-all ${selectedAccount === 'mobile' ? 'border-investor bg-investor/5 ring-2 ring-investor/20' : 'border-slate-200 hover:border-slate-300'}`}
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-pink-100 flex items-center justify-center">
<Smartphone className="w-4 h-4 sm:w-5 sm:h-5 text-pink-600" />
</div>
<div>
<p className="text-sm font-semibold text-slate-800">{(investor as any).mobileBanking}</p>
<p className="text-xs text-slate-500 font-mono">{(investor as any).mobileBankingNumber}</p>
</div>
</div>
</div>
)}
</div>
</div>
<div className="p-4 sm:p-5 border-t border-slate-200 flex justify-end gap-2 sm:gap-3 bg-slate-50">
<button onClick={() => setShowWithdrawModal(false)} className="px-4 py-2 sm:py-2.5 border border-slate-300 text-slate-600 rounded-lg text-sm font-medium hover:bg-white hover:shadow-sm transition-all">
Cancel
</button>
<button
onClick={handleSubmitWithdraw}
disabled={!selectedAccount || (selectedBikes.length === 0 && !selectAll)}
className="px-5 py-2 sm:py-2.5 bg-investor text-white rounded-lg text-sm font-semibold hover:bg-investor-dark disabled:opacity-50 disabled:cursor-not-allowed shadow-sm transition-all flex items-center gap-2"
>
<CreditCard className="w-4 h-4" /> Submit Request
</button>
</div>
</div>
</div>
)
}
{/* Auto-Withdraw Modal */}
{
showAutoWithdrawModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[60] p-4">
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
<h3 className="text-lg font-bold text-slate-800">Auto-Withdraw Settings</h3>
<button onClick={() => setShowAutoWithdrawModal(false)} className="p-1.5 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-5 space-y-4">
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">Enable Auto-Withdraw</label>
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
<button
onClick={() => setAutoWithdrawEnabled(!autoWithdrawEnabled)}
className={`w-12 h-6 rounded-full transition-colors ${autoWithdrawEnabled ? 'bg-green-500' : 'bg-slate-200'}`}
>
<span className={`block w-5 h-5 bg-white rounded-full shadow transition-transform ${autoWithdrawEnabled ? 'translate-x-6' : 'translate-x-0.5'}`} />
</button>
<span className="text-sm text-slate-600">Automatically withdraw earnings</span>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">Withdrawal Frequency</label>
<select
value={autoWithdrawFreq}
onChange={(e) => setAutoWithdrawFreq(e.target.value)}
className="w-full px-3 py-2.5 border border-slate-200 rounded-lg text-sm bg-white"
>
<option value="as_per_request">As Requested</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">Minimum Amount</label>
<div className="flex items-center gap-2">
<span className="px-3 py-2.5 bg-slate-100 rounded-lg text-slate-500 font-bold"></span>
<input
type="number"
value={autoWithdrawMin}
onChange={(e) => setAutoWithdrawMin(e.target.value)}
className="flex-1 px-3 py-2.5 border border-slate-200 rounded-lg text-sm"
placeholder="1000"
/>
</div>
<p className="text-xs text-slate-500 mt-1.5">Minimum balance required for auto-withdrawal</p>
</div>
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">Destination Account</label>
<div className="space-y-2">
{(investor as any).bankAccounts?.map((account: any) => (
<div key={account.id} className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-100">
<input
type="radio"
name="destAccount"
checked={autoWithdrawAccount === account.id}
onChange={() => setAutoWithdrawAccount(account.id)}
className="w-4 h-4 text-investor"
/>
<div className="flex-1">
<p className="font-medium text-slate-700">{account.bankName}</p>
<p className="text-xs text-slate-500 font-mono">{account.accountNumber}</p>
</div>
</div>
))}
{(investor as any).mobileBanking && (
<div className="flex items-center gap-3 p-3 bg-purple-50 rounded-lg border border-purple-100">
<input
type="radio"
name="destAccount"
checked={autoWithdrawAccount === 'mobile'}
onChange={() => setAutoWithdrawAccount('mobile')}
className="w-4 h-4 text-investor"
/>
<div className="flex-1">
<p className="font-medium text-slate-700">{(investor as any).mobileBanking}</p>
<p className="text-xs text-slate-500 font-mono">{(investor as any).mobileBankingNumber}</p>
</div>
</div>
)}
</div>
</div>
</div>
<div className="p-5 border-t border-slate-100 flex justify-end gap-3 bg-slate-50">
<button onClick={() => setShowAutoWithdrawModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-white">Cancel</button>
<button onClick={handleSaveAutoWithdraw} className="px-5 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark shadow-sm">Save Settings</button>
</div>
</div>
</div>
)
}
</div> </div>
</div> </div >
); );
} }

View File

@@ -1,8 +1,8 @@
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Sidebar from "@/components/Sidebar";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import LayoutContent from "@/components/LayoutContent";
const inter = Inter({ const inter = Inter({
variable: "--font-inter", variable: "--font-inter",
@@ -11,7 +11,7 @@ const inter = Inter({
export const metadata: Metadata = { export const metadata: Metadata = {
title: "JAIBEN Mobility - EV Rental Platform", title: "JAIBEN Mobility - EV Rental Platform",
description: "JAIBEN Mobility Ltd - EV Rental, Rent-to-Own, Share EV, FOCO Investor", description: "JAIBEN Mobility Ltd - EV Rental, Rent-to-Own, Share EV, FICO Investor",
manifest: "/manifest.json", manifest: "/manifest.json",
appleWebApp: { appleWebApp: {
capable: true, capable: true,
@@ -36,10 +36,7 @@ export default function RootLayout({
return ( return (
<html lang="en" className={inter.variable} suppressHydrationWarning> <html lang="en" className={inter.variable} suppressHydrationWarning>
<body className="min-h-screen bg-slate-50 antialiased" suppressHydrationWarning> <body className="min-h-screen bg-slate-50 antialiased" suppressHydrationWarning>
<Sidebar /> <LayoutContent>{children}</LayoutContent>
<main className="lg:ml-64 min-h-screen pb-20 lg:pb-0">
{children}
</main>
<Toaster position="top-right" /> <Toaster position="top-right" />
</body> </body>
</html> </html>

29
src/app/login/layout.tsx Normal file
View File

@@ -0,0 +1,29 @@
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "JAIBEN Mobility - Login",
description: "Login to JAIBEN Mobility EV Rental Platform",
};
export const viewport: Viewport = {
themeColor: "#ffffff",
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
};
export default function LoginLayout({ children }: { children: React.ReactNode }) {
return (
<section className="min-h-screen bg-slate-50 antialiased flex items-center justify-center">
{children}
</section>
);
}

252
src/app/login/page.tsx Normal file
View File

@@ -0,0 +1,252 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { users } from '@/data/mockData';
import { Zap, ArrowRight, Bike, Wallet, Shield, Users, Calculator } from 'lucide-react';
const demoUsers = [
{ email: 'superadmin@jaiben.com', role: 'super_admin', label: 'Super Admin', icon: Shield, color: 'bg-accent' },
{ email: 'adminmanager@jaiben.com', role: 'admin_manager', label: 'Admin Manager', icon: Users, color: 'bg-blue-500' },
{ email: 'staff@jaiben.com', role: 'staff', label: 'Front Desk', icon: Users, color: 'bg-purple-500' },
{ email: 'accountant@jaiben.com', role: 'accountant', label: 'Accountant', icon: Calculator, color: 'bg-green-500' },
{ email: 'investor@email.com', role: 'investor', label: 'Investor', icon: Wallet, color: 'bg-amber-500' },
// { email: 'swap@jaiben.com', role: 'swapstation', label: 'Swap Station', icon: Zap, color: 'bg-purple-500' },
];
export default function LoginPage() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
await new Promise(resolve => setTimeout(resolve, 800));
const user = users.find(u => u.email === email);
if (user && password === 'demo123') {
sessionStorage.setItem('authToken', 'demo-token');
sessionStorage.setItem('userRole', user.role);
sessionStorage.setItem('userName', user.name);
const rolePerms: Record<string, string[]> = {
super_admin: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'kyc.doc_approve', 'kyc.doc_reject', 'kyc.make_valid_user', 'dashboard.view'],
admin_manager: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'kyc.doc_approve', 'kyc.doc_reject', 'kyc.make_valid_user', 'dashboard.view'],
staff: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'dashboard.view'],
accountant: ['dashboard.view', 'accounting.view', 'accounting.create', 'accounting.edit', 'accounting.delete'],
investor: ['dashboard.view', 'kyc.request', 'kyc.view'],
biker: ['dashboard.view', 'kyc.request', 'kyc.view', 'rentals.view', 'rentals.create'],
'swapstation': ['dashboard.view', 'kyc.request', 'kyc.view'],
merchant: ['dashboard.view', 'kyc.request', 'kyc.view', 'merchants.view'],
};
sessionStorage.setItem('userPermissions', JSON.stringify(rolePerms[user.role] || []));
switch (user.role) {
case 'super_admin':
case 'admin_manager':
case 'staff':
router.push('/admin');
break;
case 'accountant':
router.push('/admin/accounting');
break;
case 'investor':
router.push('/investor/dashboard');
break;
case 'biker':
router.push('/biker');
break;
case 'swapstation':
router.push('/swapstation');
break;
case 'merchant':
router.push('/merchant');
break;
default:
router.push('/login');
}
} else {
setError('Invalid email or password. Try demo123');
}
setLoading(false);
};
const handleQuickLogin = async (userEmail: string) => {
setLoading(true);
await new Promise(resolve => setTimeout(resolve, 500));
const user = users.find(u => u.email === userEmail);
if (user) {
sessionStorage.setItem('authToken', 'demo-token');
sessionStorage.setItem('userRole', user.role);
sessionStorage.setItem('userName', user.name);
const rolePerms: Record<string, string[]> = {
super_admin: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'kyc.doc_approve', 'kyc.doc_reject', 'kyc.make_valid_user', 'dashboard.view'],
admin_manager: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'kyc.doc_approve', 'kyc.doc_reject', 'kyc.make_valid_user', 'dashboard.view'],
staff: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'dashboard.view'],
accountant: ['dashboard.view', 'accounting.view', 'accounting.create', 'accounting.edit', 'accounting.delete'],
investor: ['dashboard.view', 'kyc.request', 'kyc.view'],
biker: ['dashboard.view', 'kyc.request', 'kyc.view', 'rentals.view', 'rentals.create'],
'swapstation': ['dashboard.view', 'kyc.request', 'kyc.view'],
merchant: ['dashboard.view', 'kyc.request', 'kyc.view', 'merchants.view'],
};
sessionStorage.setItem('userPermissions', JSON.stringify(rolePerms[user.role] || []));
switch (user.role) {
case 'super_admin':
case 'admin_manager':
case 'staff':
router.push('/admin');
break;
case 'accountant':
router.push('/admin/accounting');
break;
case 'investor':
router.push('/investor/dashboard');
break;
case 'biker':
router.push('/biker');
break;
case 'swapstation':
router.push('/swapstation');
break;
case 'merchant':
router.push('/merchant');
break;
default:
router.push('/login');
}
}
setLoading(false);
};
return (
<div className="w-full min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<button
onClick={() => router.push('/')}
className="inline-block"
>
<h1 className="text-3xl font-extrabold text-white">JAIBEN</h1>
<p className="text-xs text-slate-400">Mobility Ltd</p>
</button>
</div>
<div className="bg-slate-800/50 border border-slate-700 rounded-2xl p-6 lg:p-8 backdrop-blur">
<div className="text-center mb-6">
<h2 className="text-xl font-bold text-white mb-1">Welcome Back</h2>
<p className="text-slate-400 text-sm">Sign in to continue to your dashboard</p>
</div>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-300 mb-2">
Email Address
</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-4 py-3 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-accent focus:border-accent transition-all"
placeholder="Enter your email"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-300 mb-2">
Password <span className="text-slate-500 text-xs">(demo: demo123)</span>
</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full px-4 py-3 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-accent focus:border-accent transition-all"
placeholder="Enter your password"
/>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500/30 text-red-400 px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full py-3 bg-accent text-white rounded-lg font-semibold hover:bg-accent-dark transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{loading ? (
<>
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
</svg>
Signing in...
</>
) : (
<>
Sign In <ArrowRight className="w-5 h-5" />
</>
)}
</button>
</form>
<div className="mt-6 pt-6 border-t border-slate-700">
<p className="text-slate-400 text-sm mb-4 text-center">Quick Login as Demo User:</p>
<div className="grid grid-cols-1 gap-2">
{demoUsers.map((user) => {
const Icon = user.icon;
return (
<button
key={user.email}
onClick={() => handleQuickLogin(user.email)}
disabled={loading}
className="flex items-center gap-3 px-4 py-2.5 bg-slate-700/30 border border-slate-600 rounded-lg hover:border-accent hover:bg-slate-700/50 transition-all text-left"
>
<div className={`w-8 h-8 ${user.color} rounded-lg flex items-center justify-center`}>
<Icon className="w-4 h-4 text-white" />
</div>
<div className="flex-1">
<p className="text-white font-medium text-sm">{user.label}</p>
<p className="text-slate-400 text-xs">{user.email}</p>
</div>
<ArrowRight className="w-4 h-4 text-slate-500" />
</button>
);
})}
</div>
</div>
</div>
<div className="mt-6 text-center">
<button
onClick={() => router.push('/')}
className="text-slate-400 hover:text-white text-sm transition-colors flex items-center justify-center gap-2"
>
<ArrowRight className="w-4 h-4 rotate-180" /> Back to Home
</button>
</div>
<div className="mt-6 text-center">
<p className="text-slate-500 text-xs">
&copy; 2026 JAIBEN Mobility Ltd. All rights reserved.
</p>
</div>
</div>
</div>
);
}

View File

@@ -1,80 +1,182 @@
import Image from 'next/image'; 'use client';
import StatCard from '@/components/StatCard';
import BikeCard from '@/components/BikeCard';
import TransactionList from '@/components/TransactionList';
import { bikes, rentals, transactions, stats, users } from '@/data/mockData';
import { Bike, Wallet, Footprints, Clock, Zap, RotateCcw, CreditCard, FileText, Share2 } from 'lucide-react';
export default function Home() { import { useState } from 'react';
const activeRental = rentals.find(r => r.status === 'active'); import { useRouter } from 'next/navigation';
const currentBike = bikes.find(b => b.id === activeRental?.bikeId); import { users } from '@/data/mockData';
const currentUser = users[0]; import { Zap, ArrowRight, Bike, Wallet } from 'lucide-react';
export default function LandingPage() {
const router = useRouter();
const [loading, setLoading] = useState(false);
const handleLogin = async (email: string) => {
setLoading(true);
await new Promise(resolve => setTimeout(resolve, 500));
const user = users.find(u => u.email === email);
if (user) {
sessionStorage.setItem('authToken', 'demo-token');
sessionStorage.setItem('userRole', user.role);
sessionStorage.setItem('userName', user.name);
switch (user.role) {
case 'super_admin':
case 'admin_manager':
case 'staff':
router.push('/admin');
break;
case 'accountant':
router.push('/admin');
break;
case 'investor':
router.push('/investor');
break;
case 'biker':
router.push('/biker');
break;
case 'swapstation':
router.push('/swapstation');
break;
case 'merchant':
router.push('/merchant');
break;
default:
router.push('/login');
}
}
setLoading(false);
};
return ( return (
<div className="p-4 lg:p-6"> <div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
<div className="mb-6"> <header className="p-4 lg:p-6">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-full bg-biker-light flex items-center justify-center text-2xl font-bold text-biker">
{currentUser.name.charAt(0)}
</div>
<div>
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">Welcome back, {currentUser.name.split(' ')[0]}!</h1>
<p className="text-sm text-slate-500">Here's your rental status</p>
</div>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<StatCard label="Active Rental" value={stats.biker.activeRental} icon={Bike} color="text-biker" />
<StatCard label="Wallet Balance" value={`৳${stats.biker.walletBalance}`} icon={Wallet} color="text-green-600" />
<StatCard label="Total Rides" value={stats.biker.totalRides} icon={Footprints} color="text-purple-600" />
<StatCard label="Days Remaining" value={stats.biker.daysRemaining} icon={Clock} color="text-amber-600" />
</div>
{currentBike && (
<div className="mb-6">
<h2 className="text-lg font-bold text-slate-800 mb-3">Current Bike</h2>
<BikeCard bike={currentBike} />
</div>
)}
<div className="grid lg:grid-cols-2 gap-6 mb-6">
<div>
<h2 className="text-lg font-bold text-slate-800 mb-3">Quick Actions</h2>
<div className="grid grid-cols-2 gap-3">
<button className="py-3 bg-accent text-white rounded-lg font-semibold hover:bg-accent-dark transition-colors flex items-center justify-center gap-2">
<Zap className="w-4 h-4" /> Rent Bike
</button>
<button className="py-3 bg-biker text-white rounded-lg font-semibold hover:bg-biker-dark transition-colors flex items-center justify-center gap-2">
<RotateCcw className="w-4 h-4" /> Extend Rental
</button>
<button className="py-3 bg-green-600 text-white rounded-lg font-semibold hover:bg-green-700 transition-colors flex items-center justify-center gap-2">
<CreditCard className="w-4 h-4" /> Top Up
</button>
<button className="py-3 border-2 border-slate-200 text-slate-600 rounded-lg font-semibold hover:bg-slate-50 transition-colors flex items-center justify-center gap-2">
<FileText className="w-4 h-4" /> End Rental
</button>
</div>
</div>
<div>
<h2 className="text-lg font-bold text-slate-800 mb-3">Recent Transactions</h2>
<TransactionList transactions={transactions.filter(t => t.userId === 'u1')} compact />
</div>
</div>
<div className="bg-gradient-to-r from-accent to-accent-dark rounded-xl p-6 text-white">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm opacity-80">Refer a friend</p> <h1 className="text-2xl lg:text-3xl font-extrabold text-white">JAIBEN</h1>
<h3 className="text-xl font-bold mt-1">Earn 500 Credit</h3> <p className="text-xs text-slate-400">Mobility Ltd</p>
<p className="text-xs opacity-80 mt-2">Share your referral link and earn free rides</p>
</div> </div>
<button className="px-4 py-2 bg-white text-accent rounded-lg font-semibold text-sm flex items-center gap-2"> <button
<Share2 className="w-4 h-4" /> Share Link onClick={() => router.push('/login')}
className="px-4 py-2 bg-accent text-white rounded-lg font-semibold hover:bg-accent-dark transition-colors flex items-center gap-2"
>
Sign In <ArrowRight className="w-4 h-4" />
</button> </button>
</div> </div>
</div> </header>
<main className="p-4 lg:p-6">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-8 lg:mb-12">
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-accent/20 rounded-full text-accent text-sm font-medium mb-4">
<Zap className="w-4 h-4" /> Bangladesh&apos;s Leading EV Platform
</div>
<h2 className="text-3xl lg:text-5xl font-extrabold text-white mb-4 leading-tight">
Electric Vehicle
<br className="lg:hidden" /> Rental
<span className="text-accent"> Made Simple</span>
</h2>
<p className="text-slate-400 text-lg lg:text-xl max-w-2xl mx-auto">
Rent, Rent-to-Own, or Invest in EVs. Join Bangladesh&apos;s fastest growing
electric mobility ecosystem with FICO model for investors.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-8">
<button
onClick={() => handleLogin('rahim@email.com')}
disabled={loading}
className="p-6 bg-slate-800/50 border border-slate-700 rounded-2xl hover:border-biker hover:bg-biker/10 transition-all group text-left"
>
<div className="w-12 h-12 bg-biker/20 rounded-xl flex items-center justify-center mb-4">
<Bike className="w-6 h-6 text-biker" />
</div>
<h3 className="text-xl font-bold text-white mb-1">Biker</h3>
<p className="text-slate-400 text-sm mb-3">Rent electric bikes for daily commute</p>
<span className="text-biker text-sm font-medium flex items-center gap-1">
Login as Biker <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</span>
</button>
<button
onClick={() => handleLogin('investor@email.com')}
disabled={loading}
className="p-6 bg-slate-800/50 border border-slate-700 rounded-2xl hover:border-green-500 hover:bg-green-500/10 transition-all group text-left"
>
<div className="w-12 h-12 bg-green-500/20 rounded-xl flex items-center justify-center mb-4">
<Wallet className="w-6 h-6 text-green-500" />
</div>
<h3 className="text-xl font-bold text-white mb-1">Investor</h3>
<p className="text-slate-400 text-sm mb-3">FICO model with guaranteed returns</p>
<span className="text-green-500 text-sm font-medium flex items-center gap-1">
Login as Investor <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</span>
</button>
<button
onClick={() => handleLogin('swap@jaiben.com')}
disabled={loading}
className="p-6 bg-slate-800/50 border border-slate-700 rounded-2xl hover:border-purple-500 hover:bg-purple-500/10 transition-all group text-left"
>
<div className="w-12 h-12 bg-purple-500/20 rounded-xl flex items-center justify-center mb-4">
<Zap className="w-6 h-6 text-purple-500" />
</div>
<h3 className="text-xl font-bold text-white mb-1">Swap Station</h3>
<p className="text-slate-400 text-sm mb-3">Battery swap and charging services</p>
<span className="text-purple-500 text-sm font-medium flex items-center gap-1">
Login as Swap Station <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</span>
</button>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div className="text-center p-4">
<div className="text-2xl lg:text-3xl font-bold text-accent mb-1">500+</div>
<div className="text-slate-400 text-sm">Active Bikers</div>
</div>
<div className="text-center p-4">
<div className="text-2xl lg:text-3xl font-bold text-accent mb-1">200+</div>
<div className="text-slate-400 text-sm">EV Fleet</div>
</div>
<div className="text-center p-4">
<div className="text-2xl lg:text-3xl font-bold text-accent mb-1">50+</div>
<div className="text-slate-400 text-sm">Investors</div>
</div>
<div className="text-center p-4">
<div className="text-2xl lg:text-3xl font-bold text-accent mb-1">18%</div>
<div className="text-slate-400 text-sm">Avg. ROI</div>
</div>
</div>
<div className="bg-gradient-to-r from-accent to-accent-dark rounded-2xl p-6 lg:p-8 text-white">
<div className="grid lg:grid-cols-2 gap-6 items-center">
<div>
<h3 className="text-xl lg:text-2xl font-bold mb-2">Ready to join the EV revolution?</h3>
<p className="text-white/80 mb-4">Start renting or investing today and be part of Bangladesh&apos;s sustainable future.</p>
</div>
<div className="flex flex-col gap-3">
<button
onClick={() => router.push('/login')}
className="w-full py-3 bg-white text-accent rounded-lg font-semibold hover:bg-slate-100 transition-colors"
>
Get Started
</button>
<button
onClick={() => router.push('/login')}
className="w-full py-3 border-2 border-white text-white rounded-lg font-semibold hover:bg-white/10 transition-colors"
>
Learn More
</button>
</div>
</div>
</div>
</div>
</main>
<footer className="p-4 lg:p-6 text-center">
<p className="text-slate-500 text-sm">
&copy; 2024 JAIBEN Mobility Ltd. All rights reserved.
</p>
</footer>
</div> </div>
); );
} }

View File

@@ -0,0 +1,52 @@
'use client';
import Link from 'next/link';
import { Bell, Package, DollarSign, Bike, AlertCircle, CheckCircle } from 'lucide-react';
const mockNotifications = [
{ id: '1', type: 'rental', title: 'New Rental Started', message: 'Your bike AB-1234 has been rented by rider MR-456', time: '5 min ago', read: false },
{ id: '2', type: 'earning', title: 'Earning Credited', message: '৳450 has been added to your wallet from bike CD-5678', time: '2 hours ago', read: false },
{ id: '3', type: 'success', title: 'Withdrawal Complete', message: 'Your withdrawal of ৳5,000 has been processed successfully', time: '1 day ago', read: true },
{ id: '4', type: 'alert', title: 'Maintenance Alert', message: 'Bike XY-9012 requires maintenance attention', time: '2 days ago', read: true },
];
const iconMap: Record<string, any> = {
rental: Bike, earning: DollarSign, success: CheckCircle, alert: AlertCircle, default: Package,
};
export default function InvestorNotification({ isMobile = false }: { isMobile?: boolean }) {
const unreadCount = mockNotifications.filter(n => !n.read).length;
if (isMobile) {
return (
<div className="lg:hidden fixed top-0 left-0 right-0 h-14 bg-white border-b border-slate-200 flex items-center justify-between px-4 z-40">
<div className="flex items-center gap-2">
<h1 className="text-lg font-extrabold text-accent">JAIBEN</h1>
<span className="text-[10px] text-accent font-medium">Investor</span>
</div>
<Link href="/investor/notifications" className="relative p-2 hover:bg-slate-100 rounded-lg transition-colors">
<Bell className="w-5 h-5 text-slate-600" />
{unreadCount > 0 && (
<span className="absolute top-1 right-1 w-4 h-4 bg-red-500 text-white text-[10px] font-bold rounded-full flex items-center justify-center">
{unreadCount}
</span>
)}
</Link>
</div>
);
}
return (
<div className="hidden lg:block">
<Link href="/investor/notifications" className="flex items-center justify-between p-3 rounded-lg hover:bg-slate-50 transition-colors">
<div className="flex items-center gap-3">
<Bell className="w-5 h-5 text-slate-600" />
<span className="text-sm font-medium text-slate-700">Notifications</span>
</div>
{unreadCount > 0 && (
<span className="px-2 py-0.5 bg-accent text-white text-xs font-bold rounded-full">{unreadCount}</span>
)}
</Link>
</div>
);
}

View File

@@ -0,0 +1,21 @@
"use client";
import { usePathname } from "next/navigation";
import Sidebar from "@/components/Sidebar";
interface LayoutContentProps {
children: React.ReactNode;
}
export default function LayoutContent({ children }: LayoutContentProps) {
const pathname = usePathname();
const showSidebar = pathname !== "/" && pathname !== "/login";
return (
<>
{showSidebar && <Sidebar />}
<main className={showSidebar ? "lg:ml-64 min-h-screen pb-4 lg:pb-0" : "min-h-screen pb-4 lg:pb-0"}>
{children}
</main>
</>
);
}

View File

@@ -2,7 +2,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { import {
Bike, Bike,
Settings, Settings,
@@ -22,42 +22,63 @@ import {
ChevronDown, ChevronDown,
LogOut, LogOut,
Calculator, Calculator,
Wrench Wrench,
Target, User, History, Bell,
Building2
} from 'lucide-react'; } from 'lucide-react';
import { getUserName, getUserRole, logout } from '@/lib/auth';
const adminNavItems = [ const ROLE_LABELS: Record<string, string> = {
super_admin: 'Super Admin',
admin_manager: 'Admin Manager',
staff: 'Front Desk',
accountant: 'Accountant',
investor: 'Investor',
biker: 'Biker',
'swap-station': 'Swap Station',
merchant: 'Merchant',
};
type NavItem = { label: string; href: string; icon: any; isNotification?: boolean };
const adminNavItems: NavItem[] = [
{ label: 'Dashboard', href: '/admin', icon: BarChart3 }, { label: 'Dashboard', href: '/admin', icon: BarChart3 },
{ label: 'Notifications', href: '/admin/notifications', icon: Bell, isNotification: true },
{ label: 'KYC Requests', href: '/admin/kyc', icon: Shield }, { label: 'KYC Requests', href: '/admin/kyc', icon: Shield },
{ label: 'Rentals', href: '/admin/rentals', icon: FileText }, { label: 'Rentals', href: '/admin/rentals', icon: FileText },
{ label: 'Bikers', href: '/admin/bikers', icon: Users }, { label: 'Bikers', href: '/admin/bikers', icon: Users },
{ label: 'Investors', href: '/admin/investors', icon: Wallet }, { label: 'Investors', href: '/admin/investors', icon: Wallet },
{ label: 'Fleet Management', href: '/admin/fleet', icon: Bike }, { label: 'Fleet Management', href: '/admin/fleet', icon: Bike },
{ label: 'Battery Management', href: '/admin/batteries', icon: Battery },
{ label: 'Merchants (P2)', href: '/admin/merchants', icon: Store }, { label: 'Merchants (P2)', href: '/admin/merchants', icon: Store },
{ label: 'Swap Stations (P3)', href: '/admin/swap-stations', icon: Zap }, { label: 'Swap Stations (P3)', href: '/admin/swap-stations', icon: Zap },
{ label: 'Damage & Maintenance', href: '/admin/maintenance', icon: Wrench }, { label: 'Damage & Maintenance', href: '/admin/maintenance', icon: Wrench },
{ label: 'Service Centers', href: '/admin/service-centers', icon: Building2 },
{ label: 'Accounting', href: '/admin/accounting', icon: Calculator }, { label: 'Accounting', href: '/admin/accounting', icon: Calculator },
{ label: 'Hubs', href: '/admin/hub', icon: MapPin }, { label: 'Hubs', href: '/admin/hub', icon: MapPin },
{ label: 'Reports', href: '/admin/reports', icon: BarChart3 }, { label: 'Reports', href: '/admin/reports', icon: BarChart3 },
{ label: 'Users Management', href: '/admin/users', icon: Users }, { label: 'Users Management', href: '/admin/users', icon: Users },
{ label: 'Roles & Permissions', href: '/admin/roles', icon: Shield }, { label: 'Roles & Permissions', href: '/admin/roles', icon: Shield },
{ label: 'Settings', href: '/admin/settings', icon: Settings }, { label: 'Settings', href: '/admin/settings', icon: Settings },
]; ];
const bikerNavItems = [ const bikerNavItems: NavItem[] = [
{ label: 'Biker Dashboard', href: '/', icon: Bike }, { label: 'Biker Dashboard', href: '/', icon: Bike },
{ label: 'Rent Bike', href: '/rent', icon: Zap }, { label: 'Rent Bike', href: '/rent', icon: Zap },
{ label: 'Browse EVs', href: '/bikes', icon: Battery }, { label: 'Browse EVs', href: '/bikes', icon: Battery },
]; ];
const investorNavItems = [ const investorNavItems: NavItem[] = [
{ label: 'Dashboard', href: '/investor', icon: Wallet }, { label: 'Dashboard', href: '/investor/dashboard', icon: Bike },
{ label: 'Portfolio', href: '/investor/portfolio', icon: BarChart3 }, { label: 'My Investments', href: '/investor/plans', icon: Target },
{ label: 'Rental History', href: '/investor/rental-history', icon: History },
{ label: 'Withdraw', href: '/investor/withdraw', icon: CreditCard }, { label: 'Withdraw', href: '/investor/withdraw', icon: CreditCard },
{ label: 'Notifications', href: '/investor/notifications', icon: Bell },
{ label: 'My Profile', href: '/investor/profile', icon: User },
]; ];
const shopNavItems = [ const shopNavItems: NavItem[] = [
{ label: 'Dashboard', href: '/shop', icon: Store }, { label: 'Dashboard', href: '/shop', icon: Store },
{ label: 'Deliveries', href: '/shop/deliveries', icon: Truck }, { label: 'Deliveries', href: '/shop/deliveries', icon: Truck },
{ label: 'Fleet', href: '/shop/fleet', icon: Bike }, { label: 'Fleet', href: '/shop/fleet', icon: Bike },
@@ -67,11 +88,50 @@ export default function Sidebar() {
const pathname = usePathname(); const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
const [expandedMenu, setExpandedMenu] = useState<string | null>(null); const [expandedMenu, setExpandedMenu] = useState<string | null>(null);
const [userName, setUserName] = useState('User');
const [userRole, setUserRole] = useState('admin');
const [adminUnreadCount, setAdminUnreadCount] = useState(5);
const [investorUnreadCount, setInvestorUnreadCount] = useState(2);
useEffect(() => {
setUserName(getUserName() || 'User');
setUserRole(getUserRole() || 'staff');
const updateCounts = () => {
const adminNotifs = localStorage.getItem('jaiben_admin_notifications');
if (adminNotifs) {
try {
const parsed = JSON.parse(adminNotifs);
setAdminUnreadCount(parsed.filter((n: any) => !n.read).length);
} catch (e) { }
}
const invNotifs = localStorage.getItem('jaiben_investor_notifications');
if (invNotifs) {
try {
const parsed = JSON.parse(invNotifs);
setInvestorUnreadCount(parsed.filter((n: any) => !n.read).length);
} catch (e) { }
}
};
updateCounts();
window.addEventListener('storage', updateCounts);
window.addEventListener('jaiben_notifications_update', updateCounts);
return () => {
window.removeEventListener('storage', updateCounts);
window.removeEventListener('jaiben_notifications_update', updateCounts);
};
}, []);
const isAdmin = pathname.startsWith('/admin'); const isAdmin = pathname.startsWith('/admin');
const isInvestor = pathname.startsWith('/investor'); const isInvestor = pathname.startsWith('/investor');
const isShop = pathname.startsWith('/shop'); const isShop = pathname.startsWith('/shop');
const roleLabel = ROLE_LABELS[userRole] || userRole;
const navItems = isAdmin ? adminNavItems : const navItems = isAdmin ? adminNavItems :
isInvestor ? investorNavItems : isInvestor ? investorNavItems :
isShop ? shopNavItems : bikerNavItems; isShop ? shopNavItems : bikerNavItems;
@@ -81,9 +141,11 @@ export default function Sidebar() {
{ label: 'Fleet', href: '/admin/fleet', icon: Bike }, { label: 'Fleet', href: '/admin/fleet', icon: Bike },
{ label: 'Users', href: '/admin/users', icon: Users }, { label: 'Users', href: '/admin/users', icon: Users },
] : isInvestor ? [ ] : isInvestor ? [
{ label: 'Home', href: '/investor', icon: Wallet }, { label: 'Home', href: '/investor/dashboard', icon: Bike },
{ label: 'Portfolio', href: '/investor/portfolio', icon: BarChart3 }, { label: 'Investments', href: '/investor/plans', icon: Target },
{ label: 'History', href: '/investor/rental-history', icon: History },
{ label: 'Withdraw', href: '/investor/withdraw', icon: CreditCard }, { label: 'Withdraw', href: '/investor/withdraw', icon: CreditCard },
{ label: 'Profile', href: '/investor/profile', icon: User },
] : isShop ? [ ] : isShop ? [
{ label: 'Home', href: '/shop', icon: Store }, { label: 'Home', href: '/shop', icon: Store },
{ label: 'Deliveries', href: '/shop/deliveries', icon: Truck }, { label: 'Deliveries', href: '/shop/deliveries', icon: Truck },
@@ -109,7 +171,7 @@ export default function Sidebar() {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="px-2 py-1 bg-accent-light rounded text-xs font-semibold text-accent"> <div className="px-2 py-1 bg-accent-light rounded text-xs font-semibold text-accent">
{isAdmin ? 'Admin' : isInvestor ? 'Investor' : isShop ? 'Shop' : 'Biker'} {roleLabel}
</div> </div>
<button <button
onClick={() => setMobileOpen(false)} onClick={() => setMobileOpen(false)}
@@ -123,8 +185,12 @@ export default function Sidebar() {
<nav className="p-3 space-y-1 overflow-y-auto h-[calc(100vh-140px)] pb-24 lg:pb-3"> <nav className="p-3 space-y-1 overflow-y-auto h-[calc(100vh-140px)] pb-24 lg:pb-3">
{navItems.map((item) => { {navItems.map((item) => {
const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href)); const isExact = pathname === item.href;
const isChild = item.href !== '/' && pathname.startsWith(item.href + '/');
const isActive = isExact || isChild;
const Icon = item.icon; const Icon = item.icon;
const isNotification = item.label === 'Notifications';
return ( return (
<Link <Link
key={item.href} key={item.href}
@@ -140,42 +206,55 @@ export default function Sidebar() {
> >
<Icon className={`w-5 h-5 ${isActive ? 'text-white' : ''}`} /> <Icon className={`w-5 h-5 ${isActive ? 'text-white' : ''}`} />
<span>{item.label}</span> <span>{item.label}</span>
{isNotification && (
<span className="ml-auto px-2 py-0.5 bg-red-500 text-white text-xs font-bold rounded-full">
{isAdmin ? adminUnreadCount : investorUnreadCount}
</span>
)}
</Link> </Link>
); );
})} })}
</nav> </nav>
<div className="absolute bottom-0 left-0 right-0 p-3 border-t border-slate-100 bg-white"> <div className="absolute bottom-0 left-0 right-0 p-3 border-t border-slate-100 bg-white">
<Link href="/admin/users/USR-001" className="flex items-center gap-3 px-3 py-2 hover:bg-slate-50 rounded-lg -mx-1"> <Link href={isInvestor ? '/investor/profile' : '/admin/users/USR-001'} className="flex items-center gap-3 px-3 py-2 hover:bg-slate-50 rounded-lg -mx-1">
<div className="w-8 h-8 rounded-full bg-accent-light flex items-center justify-center"> <div className="w-8 h-8 rounded-full bg-accent-light flex items-center justify-center">
<span className="text-sm font-bold text-accent">A</span> <span className="text-sm font-bold text-accent">{userName.charAt(0).toUpperCase()}</span>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-700 truncate">Admin User</p> <p className="text-sm font-medium text-slate-700 truncate">{userName}</p>
<p className="text-xs text-slate-400">admin@jaiben.com</p> <p className="text-xs text-slate-400">{roleLabel}</p>
</div> </div>
<button className="p-1.5 hover:bg-slate-100 rounded-lg"> <button
onClick={() => {
logout();
window.location.href = '/login';
}}
className="p-1.5 hover:bg-slate-100 rounded-lg"
>
<LogOut className="w-4 h-4 text-slate-400" /> <LogOut className="w-4 h-4 text-slate-400" />
</button> </button>
</Link> </Link>
<div className="mt-2 text-xs text-slate-400 text-center"> {/* <div className="mt-2 text-xs text-slate-400 text-center">
<p>Phase 1 - Core EV Rental</p> <p>Phase 1 - Core EV Rental</p>
<p className="mt-1">v1.0.0</p> <p className="mt-1">v1.0.0</p>
</div> </div> */}
</div> </div>
</aside> </aside>
{/* Bottom Navigation for Mobile */} {/* Bottom Navigation for Mobile */}
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-slate-200 flex justify-around items-center h-16 z-30 pb-safe shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]"> <nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-slate-200 flex items-center h-16 z-30 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
{bottomNavItems.map((item) => { {bottomNavItems.map((item) => {
const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href)); const isExact = pathname === item.href;
const isChild = item.href !== '/' && pathname.startsWith(item.href + '/');
const isActive = isExact || isChild;
const Icon = item.icon; const Icon = item.icon;
return ( return (
<Link <Link
key={item.href} key={item.href}
href={item.href} href={item.href}
className={` className={`
flex flex-col items-center justify-center w-full h-full gap-1 transition-colors flex flex-col items-center justify-center flex-1 h-full gap-1 transition-colors
${isActive ? 'text-accent' : 'text-slate-500 hover:text-slate-900'} ${isActive ? 'text-accent' : 'text-slate-500 hover:text-slate-900'}
`} `}
> >
@@ -184,13 +263,15 @@ export default function Sidebar() {
</Link> </Link>
); );
})} })}
<button {isAdmin && (
onClick={() => setMobileOpen(true)} <button
className="flex flex-col items-center justify-center w-full h-full gap-1 text-slate-500 hover:text-slate-900 transition-colors" onClick={() => setMobileOpen(true)}
> className="flex flex-col items-center justify-center flex-1 h-full gap-1 text-slate-500 hover:text-slate-900 transition-colors"
<Menu className="w-5 h-5" /> >
<span className="text-[10px] font-medium">Menu</span> <Menu className="w-5 h-5" />
</button> <span className="text-[10px] font-medium">Menu</span>
</button>
)}
</nav> </nav>
{mobileOpen && ( {mobileOpen && (

View File

@@ -3,7 +3,7 @@ export interface User {
name: string; name: string;
email: string; email: string;
phone: string; phone: string;
role: 'biker' | 'admin' | 'manager' | 'staff' | 'accountant' | 'investor' | 'shop' | 'merchant'; role: 'biker' | 'super_admin' | 'admin_manager' | 'staff' | 'accountant' | 'investor' | 'swapstation' | 'merchant';
avatar?: string; avatar?: string;
status: 'active' | 'pending' | 'inactive'; status: 'active' | 'pending' | 'inactive';
createdAt: string; createdAt: string;
@@ -20,6 +20,8 @@ export interface Bike {
location: string; location: string;
assignedTo?: string; assignedTo?: string;
investorId?: string; investorId?: string;
investmentId?: string;
rentalType?: 'single_rent' | 'rent_to_own' | 'share_ev';
purchasePrice?: number; purchasePrice?: number;
purchaseDate?: string; purchaseDate?: string;
currentRent?: number; currentRent?: number;
@@ -44,7 +46,9 @@ export interface BikeAssignment {
export interface Rental { export interface Rental {
id: string; id: string;
bikeId: string; bikeId: string;
investorId: string;
userId: string; userId: string;
bikerName: string;
type: 'single' | 'shared' | 'rent-to-own'; type: 'single' | 'shared' | 'rent-to-own';
status: 'active' | 'pending' | 'completed' | 'disputed'; status: 'active' | 'pending' | 'completed' | 'disputed';
startDate: string; startDate: string;
@@ -54,6 +58,24 @@ export interface Rental {
totalPaid: number; totalPaid: number;
} }
export interface RentalPayment {
id: string;
rentalId: string;
bikeId: string;
investorId: string;
bikeModel: string;
plateNumber: string;
bikerId: string;
bikerName: string;
date: string;
amount: number;
duration: string;
planType: string;
status: 'paid' | 'pending' | 'failed';
paymentMethod: 'cash' | 'mobile' | 'bank';
transactionId?: string;
}
export interface Transaction { export interface Transaction {
id: string; id: string;
userId?: string; userId?: string;
@@ -74,7 +96,9 @@ export interface InvestmentPlan {
investorId: string; investorId: string;
planName: string; planName: string;
planType: 'silver' | 'gold' | 'platinum' | 'diamond'; planType: 'silver' | 'gold' | 'platinum' | 'diamond';
bikeIds: string[]; bikeIds?: string[];
batteryIds?: string[];
assetType?: 'bike' | 'battery';
totalInvestment: number; totalInvestment: number;
monthlyReturn: number; monthlyReturn: number;
expectedRoi: number; expectedRoi: number;
@@ -85,6 +109,9 @@ export interface InvestmentPlan {
paymentMethod: 'bank' | 'mobile' | 'cash' | 'cheque'; paymentMethod: 'bank' | 'mobile' | 'cash' | 'cheque';
transactionId?: string; transactionId?: string;
notes?: string; notes?: string;
durationMonths?: number;
lockInMonths?: number;
exitPenaltyPercent?: number;
createdAt: string; createdAt: string;
} }
@@ -109,8 +136,10 @@ export interface Investor {
bankAccountNumber?: string; bankAccountNumber?: string;
bankBranch?: string; bankBranch?: string;
bankRouting?: string; bankRouting?: string;
bankAccounts?: { id: string; bankName: string; accountName: string; accountNumber: string; branch?: string; routing?: string; isPrimary: boolean; verified?: boolean }[];
mobileBanking?: string; mobileBanking?: string;
mobileBankingNumber?: string; mobileBankingNumber?: string;
mobileBankingVerified?: boolean;
additionalMobileBanking?: { provider: string; number: string; verified: boolean }[]; additionalMobileBanking?: { provider: string; number: string; verified: boolean }[];
emergencyContactName?: string; emergencyContactName?: string;
emergencyContactRelation?: string; emergencyContactRelation?: string;
@@ -157,27 +186,28 @@ export interface Delivery {
} }
export const users: User[] = [ export const users: User[] = [
{ id: 'u0', name: 'Super Admin', email: 'superadmin@jaiben.com', phone: '01710000000', role: 'super_admin', status: 'active', createdAt: '2023-01-01' },
{ id: 'u1', name: 'Rahim Ahmed', email: 'rahim@email.com', phone: '01712345678', role: 'biker', status: 'active', createdAt: '2024-01-15' }, { id: 'u1', name: 'Rahim Ahmed', email: 'rahim@email.com', phone: '01712345678', role: 'biker', status: 'active', createdAt: '2024-01-15' },
{ id: 'u2', name: 'Karim Hasan', email: 'karim@email.com', phone: '01712345679', role: 'biker', status: 'active', createdAt: '2024-02-20' }, { id: 'u2', name: 'Karim Hasan', email: 'karim@email.com', phone: '01712345679', role: 'biker', status: 'active', createdAt: '2024-02-20' },
{ id: 'u3', name: 'Admin User', email: 'admin@jaiben.com', phone: '01710000001', role: 'admin', status: 'active', createdAt: '2023-06-01' }, { id: 'u3', name: 'Admin Manager', email: 'adminmanager@jaiben.com', phone: '01710000001', role: 'admin_manager', status: 'active', createdAt: '2023-06-01' },
{ id: 'u4', name: 'Manager User', email: 'manager@jaiben.com', phone: '01710000002', role: 'manager', status: 'active', createdAt: '2023-06-01' }, { id: 'u4', name: 'Manager User', email: 'manager@jaiben.com', phone: '01710000002', role: 'admin_manager', status: 'active', createdAt: '2023-06-01' },
{ id: 'u5', name: 'Staff User', email: 'staff@jaiben.com', phone: '01710000003', role: 'staff', status: 'active', createdAt: '2023-07-01' }, { id: 'u5', name: 'Staff User', email: 'staff@jaiben.com', phone: '01710000003', role: 'staff', status: 'active', createdAt: '2023-07-01' },
{ id: 'u6', name: 'Accountant User', email: 'accountant@jaiben.com', phone: '01710000004', role: 'accountant', status: 'active', createdAt: '2023-07-01' }, { id: 'u6', name: 'Accountant User', email: 'accountant@jaiben.com', phone: '01710000004', role: 'accountant', status: 'active', createdAt: '2023-07-01' },
{ id: 'u7', name: 'Investor User', email: 'investor@email.com', phone: '01720000001', role: 'investor', status: 'active', createdAt: '2023-08-01' }, { id: 'u7', name: 'Investor User', email: 'investor@email.com', phone: '01720000001', role: 'investor', status: 'active', createdAt: '2023-08-01' },
{ id: 'u8', name: 'Shop Owner', email: 'shop@email.com', phone: '01730000001', role: 'shop', status: 'active', createdAt: '2023-09-01' }, { id: 'u8', name: 'Swap Station Owner', email: 'swap@jaiben.com', phone: '01730000001', role: 'swapstation', status: 'active', createdAt: '2023-09-01' },
{ id: 'u9', name: 'Merchant User', email: 'merchant@email.com', phone: '01740000001', role: 'merchant', status: 'active', createdAt: '2023-10-01' }, { id: 'u9', name: 'Merchant User', email: 'merchant@email.com', phone: '01740000001', role: 'merchant', status: 'active', createdAt: '2023-10-01' },
]; ];
export const bikes: Bike[] = [ export const bikes: Bike[] = [
{ {
id: 'EV001', model: 'Etron ET50', brand: 'Etron', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-1234', status: 'rented', batteryLevel: 78, location: 'Gulshan 1', assignedTo: 'u1', investorId: 'inv1', purchasePrice: 85000, purchaseDate: '2024-01-15', currentRent: 350, totalEarnings: 14250, id: 'EV001', model: 'Etron ET50', brand: 'Etron', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-1234', status: 'rented', batteryLevel: 78, location: 'Gulshan 1', assignedTo: 'u1', investorId: 'inv1', investmentId: 'ip1', rentalType: 'single_rent', purchasePrice: 85000, purchaseDate: '2024-01-15', currentRent: 350, totalEarnings: 114250,
assignmentHistory: [ assignmentHistory: [
{ id: 'ash1', bikeId: 'EV001', bikerId: 'u3', bikerName: 'Rahim Khan', assignedAt: '2024-01-20 10:30:00', assignedBy: 'admin1', unassignedAt: '2024-02-15 14:20:00', unassignedBy: 'admin1', reason: 'Bike transfer to another biker', status: 'completed', notes: 'Initial assignment' }, { id: 'ash1', bikeId: 'EV001', bikerId: 'u3', bikerName: 'Rahim Khan', assignedAt: '2024-01-20 10:30:00', assignedBy: 'admin1', unassignedAt: '2024-02-15 14:20:00', unassignedBy: 'admin1', reason: 'Bike transfer to another biker', status: 'completed', notes: 'Initial assignment' },
{ id: 'ash2', bikeId: 'EV001', bikerId: 'u1', bikerName: 'Karim Ahmed', assignedAt: '2024-02-15 15:00:00', assignedBy: 'admin1', status: 'active', notes: 'Reassigned after maintenance' } { id: 'ash2', bikeId: 'EV001', bikerId: 'u1', bikerName: 'Karim Ahmed', assignedAt: '2024-02-15 15:00:00', assignedBy: 'admin1', status: 'active', notes: 'Reassigned after maintenance' }
] ]
}, },
{ {
id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-5678', status: 'available', batteryLevel: 95, location: 'Banani', investorId: 'inv1', purchasePrice: 65000, purchaseDate: '2024-01-20', currentRent: 0, totalEarnings: 12750, id: 'EV002', model: 'Yadea DT3', brand: 'Yadea', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400', plateNumber: 'Dhaka Metro Cha-5678', status: 'available', batteryLevel: 95, location: 'Banani', investorId: 'inv1', investmentId: 'ip2', rentalType: 'rent_to_own', purchasePrice: 65000, purchaseDate: '2024-01-20', currentRent: 0, totalEarnings: 12750,
assignmentHistory: [ assignmentHistory: [
{ id: 'ash3', bikeId: 'EV002', bikerId: 'u1', bikerName: 'Karim Ahmed', assignedAt: '2024-01-25 09:00:00', assignedBy: 'admin1', unassignedAt: '2024-03-01 11:30:00', unassignedBy: 'admin1', reason: 'Rental completed', status: 'completed', notes: 'First rental period' } { id: 'ash3', bikeId: 'EV002', bikerId: 'u1', bikerName: 'Karim Ahmed', assignedAt: '2024-01-25 09:00:00', assignedBy: 'admin1', unassignedAt: '2024-03-01 11:30:00', unassignedBy: 'admin1', reason: 'Rental completed', status: 'completed', notes: 'First rental period' }
] ]
@@ -228,9 +258,26 @@ export const bikes: Bike[] = [
]; ];
export const rentals: Rental[] = [ export const rentals: Rental[] = [
{ id: 'r1', bikeId: 'b1', userId: 'u1', type: 'single', status: 'active', startDate: '2024-03-01', deposit: 5000, dailyRate: 350, totalPaid: 10500 }, { id: 'r1', bikeId: 'EV001', investorId: 'inv1', userId: 'u1', bikerName: 'Karim Ahmed', type: 'single', status: 'active', startDate: '2024-03-01', deposit: 5000, dailyRate: 350, totalPaid: 10500 },
{ id: 'r2', bikeId: 'b3', userId: 'u2', type: 'rent-to-own', status: 'active', startDate: '2024-02-15', deposit: 8000, dailyRate: 450, totalPaid: 18000 }, { id: 'r2', bikeId: 'EV003', investorId: 'inv2', userId: 'u2', bikerName: 'Sofiq Rahman', type: 'rent-to-own', status: 'active', startDate: '2024-02-15', deposit: 8000, dailyRate: 450, totalPaid: 18000 },
{ id: 'r3', bikeId: 'b2', userId: 'u1', type: 'single', status: 'completed', startDate: '2024-01-10', endDate: '2024-01-25', deposit: 5000, dailyRate: 350, totalPaid: 5250 }, { id: 'r3', bikeId: 'EV002', investorId: 'inv1', userId: 'u1', bikerName: 'Karim Ahmed', type: 'single', status: 'completed', startDate: '2024-01-10', endDate: '2024-01-25', deposit: 5000, dailyRate: 350, totalPaid: 5250 },
];
export const rentalPayments: RentalPayment[] = [
{ id: 'rp1', rentalId: 'r1', bikeId: 'EV001', investorId: 'inv1', bikeModel: 'Etron ET50', plateNumber: 'Dhaka Metro Cha-1234', bikerId: 'u1', bikerName: 'Karim Ahmed', date: '2024-03-25', amount: 350, duration: '1 day', planType: 'single', status: 'paid', paymentMethod: 'mobile' },
{ id: 'rp2', rentalId: 'r1', bikeId: 'EV001', investorId: 'inv1', bikeModel: 'Etron ET50', plateNumber: 'Dhaka Metro Cha-1234', bikerId: 'u1', bikerName: 'Karim Ahmed', date: '2024-03-24', amount: 350, duration: '1 day', planType: 'single', status: 'paid', paymentMethod: 'mobile' },
{ id: 'rp3', rentalId: 'r1', bikeId: 'EV001', investorId: 'inv1', bikeModel: 'Etron ET50', plateNumber: 'Dhaka Metro Cha-1234', bikerId: 'u1', bikerName: 'Karim Ahmed', date: '2024-03-23', amount: 350, duration: '1 day', planType: 'single', status: 'paid', paymentMethod: 'cash' },
{ id: 'rp4', rentalId: 'r1', bikeId: 'EV001', investorId: 'inv1', bikeModel: 'Etron ET50', plateNumber: 'Dhaka Metro Cha-1234', bikerId: 'u1', bikerName: 'Karim Ahmed', date: '2024-03-22', amount: 350, duration: '1 day', planType: 'single', status: 'paid', paymentMethod: 'mobile' },
{ id: 'rp5', rentalId: 'r1', bikeId: 'EV001', investorId: 'inv1', bikeModel: 'Etron ET50', plateNumber: 'Dhaka Metro Cha-1234', bikerId: 'u1', bikerName: 'Karim Ahmed', date: '2024-03-21', amount: 350, duration: '1 day', planType: 'single', status: 'paid', paymentMethod: 'mobile' },
{ id: 'rp6', rentalId: 'r2', bikeId: 'EV002', investorId: 'inv1', bikeModel: 'Yadea DT3', plateNumber: 'Dhaka Metro Cha-5678', bikerId: 'u1', bikerName: 'Karim Ahmed', date: '2024-03-25', amount: 300, duration: '1 day', planType: 'single', status: 'pending', paymentMethod: 'mobile' },
{ id: 'rp7', rentalId: 'r2', bikeId: 'EV002', investorId: 'inv1', bikeModel: 'Yadea DT3', plateNumber: 'Dhaka Metro Cha-5678', bikerId: 'u1', bikerName: 'Karim Ahmed', date: '2024-03-24', amount: 300, duration: '1 day', planType: 'single', status: 'paid', paymentMethod: 'cash' },
{ id: 'rp8', rentalId: 'r2', bikeId: 'EV002', investorId: 'inv1', bikeModel: 'Yadea DT3', plateNumber: 'Dhaka Metro Cha-5678', bikerId: 'u1', bikerName: 'Karim Ahmed', date: '2024-03-23', amount: 300, duration: '1 day', planType: 'single', status: 'paid', paymentMethod: 'mobile' },
{ id: 'rp9', rentalId: 'r3', bikeId: 'EV003', investorId: 'inv2', bikeModel: 'AIMA Lightning', plateNumber: 'Dhaka Metro Cha-9012', bikerId: 'u2', bikerName: 'Sofiq Rahman', date: '2024-03-25', amount: 450, duration: '1 day', planType: 'rent-to-own', status: 'paid', paymentMethod: 'bank' },
{ id: 'rp10', rentalId: 'r3', bikeId: 'EV003', investorId: 'inv2', bikeModel: 'AIMA Lightning', plateNumber: 'Dhaka Metro Cha-9012', bikerId: 'u2', bikerName: 'Sofiq Rahman', date: '2024-03-24', amount: 450, duration: '1 day', planType: 'rent-to-own', status: 'paid', paymentMethod: 'mobile' },
{ id: 'rp11', rentalId: 'r3', bikeId: 'EV003', investorId: 'inv2', bikeModel: 'AIMA Lightning', plateNumber: 'Dhaka Metro Cha-9012', bikerId: 'u2', bikerName: 'Sofiq Rahman', date: '2024-03-23', amount: 450, duration: '1 day', planType: 'rent-to-own', status: 'paid', paymentMethod: 'bank' },
{ id: 'rp12', rentalId: 'r1', bikeId: 'EV001', investorId: 'inv1', bikeModel: 'Etron ET50', plateNumber: 'Dhaka Metro Cha-1234', bikerId: 'u1', bikerName: 'Karim Ahmed', date: '2024-03-20', amount: 350, duration: '1 day', planType: 'single', status: 'paid', paymentMethod: 'cash' },
{ id: 'rp13', rentalId: 'r1', bikeId: 'EV001', investorId: 'inv1', bikeModel: 'Etron ET50', plateNumber: 'Dhaka Metro Cha-1234', bikerId: 'u1', bikerName: 'Karim Ahmed', date: '2024-03-19', amount: 350, duration: '1 day', planType: 'single', status: 'paid', paymentMethod: 'mobile' },
{ id: 'rp14', rentalId: 'r2', bikeId: 'EV002', investorId: 'inv1', bikeModel: 'Yadea DT3', plateNumber: 'Dhaka Metro Cha-5678', bikerId: 'u1', bikerName: 'Karim Ahmed', date: '2024-03-22', amount: 300, duration: '1 day', planType: 'single', status: 'failed', paymentMethod: 'mobile' },
]; ];
export const transactions: Transaction[] = [ export const transactions: Transaction[] = [
@@ -304,12 +351,17 @@ export const investors: Investor[] = [
gender: 'male', gender: 'male',
occupation: 'Business', occupation: 'Business',
nidNumber: '1234567890', nidNumber: '1234567890',
tinNumber: '9876543210',
bankName: 'Islami Bank Bangladesh Ltd', bankName: 'Islami Bank Bangladesh Ltd',
bankAccountName: 'Hasan Mahmud', bankAccountName: 'Hasan Mahmud',
bankAccountNumber: '2050 1500 2345', bankAccountNumber: '205015002345',
bankBranch: 'Dhanmondi Branch', bankBranch: 'Dhanmondi Branch',
bankRouting: '140', bankRouting: '140',
mobileBanking: 'Bkash', bankAccounts: [
{ id: 'ba1', bankName: 'Islami Bank Bangladesh Ltd', accountName: 'Hasan Mahmud', accountNumber: '205015002345', branch: 'Dhanmondi Branch', routing: '140', isPrimary: true },
{ id: 'ba2', bankName: 'Dutch-Bangla Bank', accountName: 'Hasan Mahmud', accountNumber: '1203456789012', branch: 'Gulshan Branch', routing: '090', isPrimary: false },
],
mobileBanking: 'bKash',
mobileBankingNumber: '01712345678', mobileBankingNumber: '01712345678',
additionalMobileBanking: [ additionalMobileBanking: [
{ provider: 'Nagad', number: '01712345679', verified: true } { provider: 'Nagad', number: '01712345679', verified: true }
@@ -317,8 +369,8 @@ export const investors: Investor[] = [
emergencyContactName: 'Fatema Begum', emergencyContactName: 'Fatema Begum',
emergencyContactRelation: 'Wife', emergencyContactRelation: 'Wife',
emergencyContactPhone: '01712345679', emergencyContactPhone: '01712345679',
totalInvested: 150000, totalInvested: 300000,
totalEarnings: 14250, totalEarnings: 114250,
activeBikes: 2, activeBikes: 2,
withdrawalPending: 3000, withdrawalPending: 3000,
totalWithdrawn: 45000, totalWithdrawn: 45000,
@@ -341,7 +393,8 @@ export const investors: Investor[] = [
referralEarnings: 2500, referralEarnings: 2500,
investments: [ investments: [
{ id: 'ip1', investorId: 'inv1', planName: 'Gold EV Fleet 2024', planType: 'gold', bikeIds: ['b1'], totalInvestment: 85000, monthlyReturn: 2500, expectedRoi: 18, actualEarnings: 10000, startDate: '2024-01-15', endDate: '2025-01-14', status: 'active', paymentMethod: 'bank', transactionId: 'invt1', createdAt: '2024-01-15' }, { id: 'ip1', investorId: 'inv1', planName: 'Gold EV Fleet 2024', planType: 'gold', bikeIds: ['b1'], totalInvestment: 85000, monthlyReturn: 2500, expectedRoi: 18, actualEarnings: 10000, startDate: '2024-01-15', endDate: '2025-01-14', status: 'active', paymentMethod: 'bank', transactionId: 'invt1', createdAt: '2024-01-15' },
{ id: 'ip2', investorId: 'inv1', planName: 'Gold City Commuter', planType: 'gold', bikeIds: ['b2'], totalInvestment: 65000, monthlyReturn: 2125, expectedRoi: 18, actualEarnings: 4250, startDate: '2024-01-20', endDate: '2025-01-19', status: 'active', paymentMethod: 'mobile', transactionId: 'invt2', createdAt: '2024-01-20' } { id: 'ip2', investorId: 'inv1', planName: 'Gold City Commuter', planType: 'gold', bikeIds: ['b2'], totalInvestment: 65000, monthlyReturn: 2125, expectedRoi: 18, actualEarnings: 4250, startDate: '2024-01-20', endDate: '2025-01-19', status: 'active', paymentMethod: 'mobile', transactionId: 'invt2', createdAt: '2024-01-20' },
{ id: 'ip3', investorId: 'inv1', planName: 'Standard Battery Plan', planType: 'silver', assetType: 'battery', batteryIds: ['BAT-001', 'BAT-002'], totalInvestment: 150000, monthlyReturn: 4500, expectedRoi: 16, actualEarnings: 9000, startDate: '2024-02-01', endDate: '2025-02-01', status: 'active', paymentMethod: 'bank', transactionId: 'invt3', createdAt: '2024-02-01' }
] ]
}, },
{ {

109
src/lib/auth.ts Normal file
View File

@@ -0,0 +1,109 @@
const ALL_PERMISSIONS = [
'kyc.request', 'kyc.view', 'kyc.doc_upload', 'kyc.doc_approve', 'kyc.doc_reject', 'kyc.make_valid_user',
'settings.kyc_documents_view', 'settings.kyc_documents_config',
'settings.plan_selection_with_condition_view', 'settings.plan_selection_with_condition_config',
'settings.investment_plan_view', 'settings.investment_plan_config',
'settings.battery_investment_plan_view', 'settings.battery_investment_plan_config',
'settings.swap_station_plan_view', 'settings.swap_station_plan_config',
'settings.rider_request_plan_for_merchant_view', 'settings.rider_request_plan_for_merchant_config',
'settings.company_policy_view', 'settings.company_policy_config',
'settings.es_templates_view', 'settings.es_templates_config',
'settings.ev_parts_view', 'settings.ev_parts_config',
'dashboard.view',
'rental.requset', 'rental.accept', 'rental.reject', 'rental.view', 'rental.cancel', 'rental.edit', 'rental.image_approve', 'rental.lock', 'rental.unlock', 'rental.create',
'biker.view', 'biker.create', 'biker.edit', 'biker.delete', 'biker.status_change', 'biker.membership_change', 'biker.kyc_view', 'biker.kyc_update', 'biker.activity_view', 'biker.document_view', 'biker.document_upload', 'biker.document_delete', 'biker.rental_history_view', 'biker.payment_history_view', 'biker.wallet_view', 'biker.note_add', 'biker.note_view', 'biker.export', 'biker.make_valid_user', 'biker.lock', 'biker.unlock',
'investor.view', 'investor.create', 'investor.edit', 'investor.delete', 'investor.plan_assign', 'investor.bank_edit', 'investor.withdraw_request', 'investor.document_upload', 'investor.document_approve', 'investor.notification_view',
'battery.view', 'battery.create', 'battery.edit', 'battery.delete', 'battery.export',
'fleet.view', 'fleet.create', 'fleet.edit', 'fleet.delete', 'fleet.gps_config', 'fleet.export',
'service_center.view', 'service_center.create', 'service_center.edit', 'service_center.delete',
'maintenance.view', 'maintenance.create', 'maintenance.edit', 'maintenance.delete',
'accounting.view', 'accounting.create', 'accounting.edit', 'accounting.delete', 'accounting.withdraw_process',
'hub.view', 'hub.create', 'hub.edit', 'hub.delete',
'reports.view', 'reports.export',
'users.view', 'users.create', 'users.edit', 'users.delete',
'roles.view', 'roles.config',
'notifications.view', 'messaging.compose', 'messaging.broadcast', 'messaging.schedule'
];
const ROLE_PERMISSIONS: Record<string, string[]> = {
super_admin: ALL_PERMISSIONS,
admin_manager: ALL_PERMISSIONS.filter(p => !p.includes('delete') || p === 'biker.document_delete' || p === 'fleet.delete' || p === 'battery.delete'),
staff: [
'kyc.request', 'kyc.view', 'kyc.doc_upload',
'settings.kyc_documents_view', 'settings.plan_selection_with_condition_view', 'settings.investment_plan_view', 'settings.battery_investment_plan_view', 'settings.swap_station_plan_view', 'settings.rider_request_plan_for_merchant_view', 'settings.company_policy_view', 'settings.es_templates_view', 'settings.ev_parts_view',
'dashboard.view',
'rental.view', 'rental.create', 'rental.image_approve',
'biker.view', 'biker.edit', 'biker.kyc_view', 'biker.kyc_update', 'biker.activity_view', 'biker.document_view', 'biker.document_upload', 'biker.rental_history_view', 'biker.payment_history_view', 'biker.wallet_view', 'biker.note_add', 'biker.note_view',
'investor.view', 'investor.document_upload',
'battery.view', 'fleet.view', 'service_center.view', 'maintenance.view', 'maintenance.create', 'accounting.view', 'hub.view', 'reports.view', 'notifications.view'
],
accountant: [
'dashboard.view', 'accounting.view', 'accounting.create', 'accounting.edit', 'accounting.delete', 'accounting.withdraw_process', 'reports.view', 'reports.export'
],
investor: [
'dashboard.view', 'kyc.request', 'kyc.view', 'investor.view', 'investor.bank_edit', 'investor.withdraw_request', 'investor.document_upload', 'notifications.view'
],
biker: [
'dashboard.view', 'kyc.request', 'kyc.view', 'rental.requset', 'rental.accept', 'rental.reject', 'rental.view', 'biker.view', 'maintenance.create', 'maintenance.view', 'notifications.view'
],
'swap-station': [
'dashboard.view', 'kyc.request', 'kyc.view', 'notifications.view'
],
merchant: [
'dashboard.view', 'kyc.request', 'kyc.view', 'settings.rider_request_plan_for_merchant_view', 'notifications.view'
],
};
export const canRentalAccept = () => hasPermission('rental.accept');
export const canRentalReject = () => hasPermission('rental.reject');
export const canRentalCancel = () => hasPermission('rental.cancel');
export const canRentalEdit = () => hasPermission('rental.edit');
export const canRentalImageApprove = () => hasPermission('rental.image_approve');
export const canRentalLock = () => hasPermission('rental.lock');
export const canRentalUnlock = () => hasPermission('rental.unlock');
export const canRentalCreate = () => hasPermission('rental.create');
export const isAuthenticated = (): boolean => {
return typeof window !== 'undefined' && !!sessionStorage.getItem('authToken');
};
export const getUserRole = (): string | null => {
return typeof window !== 'undefined' ? sessionStorage.getItem('userRole') : null;
};
export const getUserName = (): string | null => {
return typeof window !== 'undefined' ? sessionStorage.getItem('userName') : null;
};
export const getUserPermissions = (): string[] => {
if (typeof window === 'undefined') return [];
const role = getUserRole();
if (role) return ROLE_PERMISSIONS[role] || [];
return [];
};
export const hasPermission = (permission: string): boolean => {
const permissions = getUserPermissions();
return permissions.includes(permission);
};
export const canApproveKycDocument = (): boolean => {
return hasPermission('kyc.doc_approve');
};
export const canRejectKycDocument = (): boolean => {
return hasPermission('kyc.doc_reject');
};
export const canMakeValidUser = (): boolean => {
return hasPermission('kyc.make_valid_user');
};
export const logout = () => {
if (typeof window !== 'undefined') {
sessionStorage.removeItem('authToken');
sessionStorage.removeItem('userRole');
sessionStorage.removeItem('userName');
sessionStorage.removeItem('userPermissions');
}
};

115
src/middleware.ts Normal file
View File

@@ -0,0 +1,115 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { isAuthenticated, getUserRole } from '@/lib/auth';
// Define protected routes and their corresponding roles
const protectedRoutes: { [key: string]: string[] } = {
'/admin': ['admin', 'manager', 'staff', 'accountant'],
'/investor': ['investor'],
'/swapstation': ['swapstation'],
'/merchant': ['merchant'],
'/biker': ['biker'],
};
// Define public routes that don't require authentication
const publicRoutes = ['/login', '/', '/api/', '/_next/'];
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Check if the route is public
const isPublicRoute = publicRoutes.some(route =>
pathname.startsWith(route)
);
if (isPublicRoute) {
return NextResponse.next();
}
// Check if the user is authenticated
const isAuth = isAuthenticated();
// If trying to access login page while authenticated, redirect to appropriate dashboard
if (pathname === '/login' && isAuth) {
const role = getUserRole();
if (!role) {
// If no role found, logout and redirect to login
// In a real app, you'd clear the session and redirect
// For demo, we'll just redirect to login (but this would cause a loop, so we'll go to home)
return NextResponse.redirect(new URL('/', request.url));
}
// Redirect based on role
let redirectUrl = '/'; // default
switch (role) {
case 'admin':
case 'manager':
case 'staff':
redirectUrl = '/admin';
break;
case 'accountant':
redirectUrl = '/admin/accounting';
break;
case 'investor':
redirectUrl = '/investor';
break;
case 'biker':
redirectUrl = '/';
break;
case 'swapstation':
redirectUrl = '/swapstation';
break;
case 'merchant':
redirectUrl = '/merchant';
break;
default:
redirectUrl = '/';
}
return NextResponse.redirect(new URL(redirectUrl, request.url));
}
// If not authenticated and trying to access a protected route, redirect to login
if (!isAuth) {
// Check if the route is protected
const isProtectedRoute = Object.keys(protectedRoutes).some(prefix =>
pathname.startsWith(prefix)
);
if (isProtectedRoute) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
// If authenticated, check if the user has access to the specific route
if (isAuth) {
const role = getUserRole();
if (role) {
// Check if the route is protected and if the user's role is allowed
for (const [prefix, allowedRoles] of Object.entries(protectedRoutes)) {
if (pathname.startsWith(prefix) && !allowedRoles.includes(role)) {
// User is authenticated but doesn't have permission for this route
// Redirect to home or show an error - for demo, we'll redirect to home
return NextResponse.redirect(new URL('/', request.url));
}
}
}
}
return NextResponse.next();
}
// Configure middleware to run on specific paths
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public folder
*/
'/((?!_next/static|_next/image|favicon.ico|public).*)',
],
};

29
tailwind.config.js Normal file
View File

@@ -0,0 +1,29 @@
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./app/**/*.{js,ts,jsx,tsx}"
],
darkMode: "class", // enable class-based dark mode
theme: {
extend: {
colors: {
// Brand-specific colors used in UI components
biker: "#1e3a8a", // deep blue for Biker theme
swapstation: "#7c3aed", // purple for Shop/Merchant theme
accent: "#6366f1", // primary accent (indigo)
"accent-dark": "#4f46e5", // darker accent for hover states
// Additional subtle shades for UI elements
"bg-dark": "#1f2937",
"bg-darker": "#111827",
"text-muted": "#9ca3af"
},
backgroundImage: {
// optional gradient for hero background
"hero-gradient": "radial-gradient(circle at top left, #1e293b, #111827)"
}
}
},
plugins: []
};