Compare commits

...

2 Commits

11 changed files with 3496 additions and 252 deletions

8
.gitignore vendored
View File

@@ -39,3 +39,11 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# PWA files
**/public/sw.js
**/public/workbox-*.js
**/public/worker-*.js
**/public/sw.js.map
**/public/workbox-*.js.map
**/public/worker-*.js.map

View File

@@ -0,0 +1,136 @@
# Settings Module — Permission Documentation
## Module: Settings
Controls access to all system configuration areas including KYC documents, plans, and company policies.
---
## 1. KYC Documents
**Permission Keys:**
- `settings.kyc_documents_config` — Admin can configure document lists
- `settings.kyc_documents_view` — Staff/Users can view document lists
**Description:**
- **Admin** will set KYC Documents list for:
- Investor
- Merchant
- Swap Station
- Rental Types:
- Rental (Single)
- Rental (2 Person Shared)
- Rent-to-Own
- **Staffs and Users** will show the list of documents based on what Admin configured.
---
## 2. Plan Selection + EV Condition
**Permission Keys:**
- `settings.plan_selection_with_condition_config` — Admin can configure plan details
- `settings.plan_selection_with_condition_view` — Staff/Users can view plan details
**Description:**
- **Admin** will set Rental Types plan selection with EV condition including:
- Rental (Single)
- Rental (2 Person Shared)
- Rent-to-Own
- Configurable fields: Price, Deposit, EV Condition, etc.
- **Staffs and Users** will show the list of Plan Selection + EV Condition with price and deposit etc.
---
## 3. Investment Plan
**Permission Keys:**
- `settings.investment_plan_config` — Admin can configure investment plans
- `settings.investment_plan_view` — Staff/Users can view investment plans
**Description:**
- Admin can create/edit investment plans with pricing, duration, features.
- Staffs and Users can only view the available investment plans.
---
## 4. Swap Station Plan
**Permission Keys:**
- `settings.swap_station_plan_config` — Admin can configure swap station plans
- `settings.swap_station_plan_view` — Staff/Users can view swap station plans
**Description:**
- Admin can create/edit swap station plans with pricing, requirements, features.
- Staffs and Users can only view the available swap station plans.
---
## 5. Rider Request Plan for Merchant
**Permission Keys:**
- `settings.rider_request_plan_for_merchant_config` — Admin can configure merchant rider request plans
- `settings.rider_request_plan_for_merchant_view` — Staff/Users can view merchant rider request plans
**Description:**
- Admin can create/edit rider request plans specifically for merchants.
- Staffs and Users can only view the available rider request plans for merchants.
---
## 6. Company Policy
**Permission Keys:**
- `settings.company_policy_config` — Admin can configure company policies
- `settings.company_policy_view` — Staff/Users can view company policies
**Description:**
- Admin can create/edit company policies for all sections.
- Staffs and Users can only view the company policies.
---
## Summary Table
| Section | Config Permission | View Permission |
|---------|------------------|-----------------|
| KYC Documents | `settings.kyc_documents_config` | `settings.kyc_documents_view` |
| Plan Selection + EV Condition | `settings.plan_selection_with_condition_config` | `settings.plan_selection_with_condition_view` |
| Investment Plan | `settings.investment_plan_config` | `settings.investment_plan_view` |
| Swap Station Plan | `settings.swap_station_plan_config` | `settings.swap_station_plan_view` |
| Rider Request Plan for Merchant | `settings.rider_request_plan_for_merchant_config` | `settings.rider_request_plan_for_merchant_view` |
| Company Policy | `settings.company_policy_config` | `settings.company_policy_view` |
---
## Role Examples
### Admin
- Has **Config** + **View** for all settings sections
### Front Desk Officer
- Has **View** only (can see documents, plans, policies)
- Cannot edit any settings
### Biker / Rider
- Has **View** only for visible settings (plans, policies)
- Cannot access KYC document configuration
### Investor / Merchant / Shop
- Has **View** only for their relevant sections
- Cannot configure any settings
Next.js will automatically detect the changes to your next.config.ts and restart the server, but if it doesn't, just restart your npm run dev command manually. You should now see the message (pwa) PWA support is enabled in your terminal!
WARNING
A quick heads-up about developing with PWA enabled: Because the Service Worker aggressively caches files to make your app work offline, you might occasionally notice that your code changes don't immediately show up in the browser.
If that happens, you can easily bypass the cache by:
Opening Chrome DevTools (F12)
Going to the Application tab -> Service Workers
Clicking "Update on reload" or "Unregister"
Alternatively, keeping DevTools open and checking "Disable cache" in the Network tab usually prevents stale code while developing!

View File

@@ -1,4 +1,11 @@
import type { NextConfig } from "next";
import withPWAInit from "@ducanh2912/next-pwa";
const withPWA = withPWAInit({
dest: "public",
disable: false,
register: true,
} as any);
const nextConfig: NextConfig = {
images: {
@@ -10,6 +17,7 @@ const nextConfig: NextConfig = {
},
],
},
turbopack: {},
};
export default nextConfig;
export default withPWA(nextConfig);

3323
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,13 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev --webpack",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@ducanh2912/next-pwa": "^10.2.9",
"date-fns": "^4.1.0",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.7",

1
public/icon-192x192.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="192" height="192" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="#2563eb"/><text x="50%" y="50%" fill="white" font-size="100" font-family="sans-serif" text-anchor="middle" dominant-baseline="middle">J</text></svg>

After

Width:  |  Height:  |  Size: 252 B

1
public/icon-512x512.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="#2563eb"/><text x="50%" y="50%" fill="white" font-size="100" font-family="sans-serif" text-anchor="middle" dominant-baseline="middle">J</text></svg>

After

Width:  |  Height:  |  Size: 252 B

21
public/manifest.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "JAIBEN Mobility",
"short_name": "JAIBEN",
"description": "EV Rental Platform",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2563eb",
"icons": [
{
"src": "/icon-192x192.svg",
"sizes": "192x192",
"type": "image/svg+xml"
},
{
"src": "/icon-512x512.svg",
"sizes": "512x512",
"type": "image/svg+xml"
}
]
}

View File

@@ -254,6 +254,7 @@ export default function KYCDetailPage() {
const [editForm, setEditForm] = useState<Partial<Request>>({});
const [showMessageModal, setShowMessageModal] = useState(false);
const [showAddNoteModal, setShowAddNoteModal] = useState(false);
const [showApproveModal, setShowApproveModal] = useState(false);
const [newNoteText, setNewNoteText] = useState('');
const [newMessageText, setNewMessageText] = useState('');
const [showAddDocModal, setShowAddDocModal] = useState(false);
@@ -409,82 +410,86 @@ export default function KYCDetailPage() {
</div>
<p className="text-slate-500 mt-1">{request.name} {request.submittedAt}</p>
</div>
<div className="flex gap-2">
<div className="flex flex-col sm:flex-row sm:flex-wrap gap-2">
{editMode ? (
<>
<button onClick={handleSaveEdit} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2">
<Save className="w-4 h-4" /> Save
<button onClick={handleSaveEdit} className="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center justify-center gap-2">
<Save className="w-4 h-4" /> <span>Save</span>
</button>
<button onClick={() => { setEditForm(request); setEditMode(false); }} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
<button onClick={() => { setEditForm(request); setEditMode(false); }} className="flex-1 sm:flex-none px-3 sm:px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50">
Cancel
</button>
</>
) : (
<>
{request.type === 'biker' && request.status !== 'approved' && (
<button
onClick={() => {
if (confirm('Approve this request and create biker profile?')) {
setRequest(prev => prev ? { ...prev, status: 'approved', verificationStage: 'active' } : null);
alert('Biker created successfully!');
}
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2"
>
<Bike className="w-4 h-4" /> Make Biker
{/* Top Row on Mobile: Make [Type] Button */}
<div className="flex gap-2 w-full sm:w-auto">
{request.type === 'biker' && request.status !== 'approved' && (
<button
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"
>
<Bike className="w-4 h-4" /> <span>Make Biker</span>
</button>
)}
{request.type === 'investor' && request.status !== 'approved' && (
<button
onClick={() => {
if (confirm('Approve this request and create investor profile?')) {
setRequest(prev => prev ? { ...prev, status: 'approved' } : null);
alert('Investor created successfully!');
}
}}
className="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center justify-center gap-2"
>
<DollarSign className="w-4 h-4" /> <span>Make Investor</span>
</button>
)}
{request.type === 'swapstation' && request.status !== 'approved' && (
<button
onClick={() => {
if (confirm('Approve this request and create shop profile?')) {
setRequest(prev => prev ? { ...prev, status: 'approved' } : null);
alert('Shop created successfully!');
}
}}
className="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center justify-center gap-2"
>
<Store className="w-4 h-4" /> <span>Make Shop</span>
</button>
)}
{request.type === 'merchant' && request.status !== 'approved' && (
<button
onClick={() => {
if (confirm('Approve this request and create merchant profile?')) {
setRequest(prev => prev ? { ...prev, status: 'approved' } : null);
alert('Merchant created successfully!');
}
}}
className="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-orange-600 text-white rounded-lg text-sm hover:bg-orange-700 flex items-center justify-center gap-2"
>
<User className="w-4 h-4" /> <span>Make Merchant</span>
</button>
)}
</div>
{/* Bottom Row on Mobile: Edit, Note, SMS */}
<div className="flex gap-2 w-full sm:w-auto">
<button onClick={() => setEditMode(true)} className="flex-1 sm:flex-none px-3 sm:px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center justify-center gap-2">
<Edit className="w-4 h-4" /> <span>Edit</span>
</button>
)}
{request.type === 'investor' && request.status !== 'approved' && (
<button
onClick={() => {
if (confirm('Approve this request and create investor profile?')) {
setRequest(prev => prev ? { ...prev, status: 'approved' } : null);
alert('Investor created successfully!');
}
}}
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 flex items-center gap-2"
>
<DollarSign className="w-4 h-4" /> Make Investor
<button onClick={() => setShowAddNoteModal(true)} className="flex-1 sm:flex-none px-3 sm:px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center justify-center gap-2">
<MessageSquare className="w-4 h-4" /> <span>Note</span>
</button>
)}
{request.type === 'swapstation' && request.status !== 'approved' && (
<button
onClick={() => {
if (confirm('Approve this request and create shop profile?')) {
setRequest(prev => prev ? { ...prev, status: 'approved' } : null);
alert('Shop created successfully!');
}
}}
className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 flex items-center gap-2"
>
<Store className="w-4 h-4" /> Make Shop
<button onClick={() => setShowMessageModal(true)} className="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent/90 flex items-center justify-center gap-2">
<Send className="w-4 h-4" /> <span>SMS</span>
</button>
)}
{request.type === 'merchant' && request.status !== 'approved' && (
<button
onClick={() => {
if (confirm('Approve this request and create merchant profile?')) {
setRequest(prev => prev ? { ...prev, status: 'approved' } : null);
alert('Merchant created successfully!');
}
}}
className="px-4 py-2 bg-orange-600 text-white rounded-lg text-sm hover:bg-orange-700 flex items-center gap-2"
>
<User className="w-4 h-4" /> Make Merchant
</button>
)}
<button onClick={() => setEditMode(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
<Edit className="w-4 h-4" /> Edit
</button>
<button onClick={() => setShowAddNoteModal(true)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2">
<MessageSquare className="w-4 h-4" /> Note
</button>
<button onClick={() => setShowMessageModal(true)} className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent/90 flex items-center gap-2">
<Send className="w-4 h-4" /> SMS
</button>
</div>
</>
)}
</div>
</div>
</div>
@@ -771,6 +776,27 @@ export default function KYCDetailPage() {
</div>
)}
{showApproveModal && (
<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">Approve Request</h3>
<button onClick={() => setShowApproveModal(false)} className="text-slate-400 hover:text-slate-600">×</button>
</div>
<div className="p-4">
<p className="text-sm text-slate-700 mb-4">Approve this request and create biker profile?</p>
<div className="flex justify-end gap-2">
<button onClick={() => setShowApproveModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg">Cancel</button>
<button onClick={() => {
setRequest(prev => prev ? { ...prev, status: 'approved', verificationStage: 'active' } : null);
setShowApproveModal(false);
toast.success('Biker created successfully!');
}} className="px-4 py-2 bg-blue-600 text-white rounded-lg">Confirm</button>
</div>
</div>
</div>
</div>
)}
{showAddNoteModal && (
<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">

View File

@@ -1,4 +1,4 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Sidebar from "@/components/Sidebar";
@@ -12,6 +12,20 @@ const inter = Inter({
export const metadata: Metadata = {
title: "JAIBEN Mobility - EV Rental Platform",
description: "JAIBEN Mobility Ltd - EV Rental, Rent-to-Own, Share EV, FOCO Investor",
manifest: "/manifest.json",
appleWebApp: {
capable: true,
statusBarStyle: "default",
title: "JAIBEN",
},
};
export const viewport: Viewport = {
themeColor: "#ffffff",
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
};
export default function RootLayout({
@@ -20,10 +34,10 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en" className={inter.variable}>
<body className="min-h-screen bg-slate-50 antialiased">
<html lang="en" className={inter.variable} suppressHydrationWarning>
<body className="min-h-screen bg-slate-50 antialiased" suppressHydrationWarning>
<Sidebar />
<main className="lg:ml-64 min-h-screen">
<main className="lg:ml-64 min-h-screen pb-20 lg:pb-0">
{children}
</main>
<Toaster position="top-right" />

View File

@@ -76,21 +76,28 @@ export default function Sidebar() {
isInvestor ? investorNavItems :
isShop ? shopNavItems : bikerNavItems;
const toggleMenu = (label: string) => {
setExpandedMenu(expandedMenu === label ? null : label);
};
const bottomNavItems = isAdmin ? [
{ label: 'Home', href: '/admin', icon: BarChart3 },
{ label: 'Fleet', href: '/admin/fleet', icon: Bike },
{ label: 'Users', href: '/admin/users', icon: Users },
] : isInvestor ? [
{ label: 'Home', href: '/investor', icon: Wallet },
{ label: 'Portfolio', href: '/investor/portfolio', icon: BarChart3 },
{ label: 'Withdraw', href: '/investor/withdraw', icon: CreditCard },
] : isShop ? [
{ label: 'Home', href: '/shop', icon: Store },
{ label: 'Deliveries', href: '/shop/deliveries', icon: Truck },
{ label: 'Fleet', href: '/shop/fleet', icon: Bike },
] : [
{ label: 'Home', href: '/', icon: Bike },
{ label: 'Rent', href: '/rent', icon: Zap },
{ label: 'EVs', href: '/bikes', icon: Battery },
];
return (
<>
<button
onClick={() => setMobileOpen(!mobileOpen)}
className="lg:hidden fixed top-4 left-4 z-50 p-2 bg-white rounded-lg shadow-md"
>
{mobileOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
<aside className={`
fixed left-0 top-0 h-screen w-64 bg-white border-r border-slate-200 shadow-sm z-40
fixed left-0 top-0 h-screen w-64 bg-white border-r border-slate-200 shadow-sm z-50
transform transition-transform duration-300 ease-in-out
${mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}>
@@ -100,13 +107,21 @@ export default function Sidebar() {
<h1 className="text-xl font-extrabold text-accent">JAIBEN</h1>
<p className="text-xs text-slate-500">Mobility Ltd</p>
</div>
<div className="px-2 py-1 bg-accent-light rounded text-xs font-semibold text-accent">
{isAdmin ? 'Admin' : isInvestor ? 'Investor' : isShop ? 'Shop' : 'Biker'}
<div className="flex items-center gap-2">
<div className="px-2 py-1 bg-accent-light rounded text-xs font-semibold text-accent">
{isAdmin ? 'Admin' : isInvestor ? 'Investor' : isShop ? 'Shop' : 'Biker'}
</div>
<button
onClick={() => setMobileOpen(false)}
className="lg:hidden p-1 text-slate-400 hover:text-slate-600 rounded-lg hover:bg-slate-100"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
</div>
<nav className="p-3 space-y-1 overflow-y-auto h-[calc(100vh-140px)]">
<nav className="p-3 space-y-1 overflow-y-auto h-[calc(100vh-140px)] pb-24 lg:pb-3">
{navItems.map((item) => {
const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href));
const Icon = item.icon;
@@ -150,9 +165,37 @@ export default function Sidebar() {
</div>
</aside>
{/* 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)]">
{bottomNavItems.map((item) => {
const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href));
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className={`
flex flex-col items-center justify-center w-full h-full gap-1 transition-colors
${isActive ? 'text-accent' : 'text-slate-500 hover:text-slate-900'}
`}
>
<Icon className={`w-5 h-5 ${isActive ? 'text-accent' : ''}`} />
<span className="text-[10px] font-medium">{item.label}</span>
</Link>
);
})}
<button
onClick={() => setMobileOpen(true)}
className="flex flex-col items-center justify-center w-full 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>
</button>
</nav>
{mobileOpen && (
<div
className="lg:hidden fixed inset-0 bg-black/30 z-30"
className="lg:hidden fixed inset-0 bg-black/40 backdrop-blur-sm z-40 transition-opacity"
onClick={() => setMobileOpen(false)}
/>
)}