Compare commits

..

2 Commits

6 changed files with 132 additions and 23 deletions

3
.gitignore vendored
View File

@@ -47,3 +47,6 @@ 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

View File

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

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' && (
<>
{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> <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' && (
<> <>
{permApprove ? (
<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> <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> <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>}

View File

@@ -35,6 +35,18 @@ export default function LoginPage() {
sessionStorage.setItem('userRole', user.role); sessionStorage.setItem('userRole', user.role);
sessionStorage.setItem('userName', user.name); 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'],
'swap-station': ['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) { switch (user.role) {
case 'super_admin': case 'super_admin':
case 'admin_manager': case 'admin_manager':
@@ -76,6 +88,18 @@ export default function LoginPage() {
sessionStorage.setItem('userRole', user.role); sessionStorage.setItem('userRole', user.role);
sessionStorage.setItem('userName', user.name); 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'],
'swap-station': ['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) { switch (user.role) {
case 'super_admin': case 'super_admin':
case 'admin_manager': case 'admin_manager':

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,
@@ -24,6 +24,18 @@ import {
Calculator, Calculator,
Wrench Wrench
} from 'lucide-react'; } from 'lucide-react';
import { getUserName, getUserRole, logout } from '@/lib/auth';
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',
};
const adminNavItems = [ const adminNavItems = [
{ label: 'Dashboard', href: '/admin', icon: BarChart3 }, { label: 'Dashboard', href: '/admin', icon: BarChart3 },
@@ -67,11 +79,20 @@ 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');
useEffect(() => {
setUserName(getUserName() || 'User');
setUserRole(getUserRole() || 'staff');
}, []);
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;
@@ -109,7 +130,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)}
@@ -148,23 +169,16 @@ export default function Sidebar() {
<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="/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 <button
onClick={() => { onClick={() => {
// Import and call logout function logout();
// Note: We can't use client-only imports in server components directly
// For now, we'll just clear sessionStorage and redirect
if (typeof window !== 'undefined') {
window.sessionStorage.removeItem('authToken');
window.sessionStorage.removeItem('userRole');
window.sessionStorage.removeItem('userName');
window.location.href = '/login'; window.location.href = '/login';
}
}} }}
className="p-1.5 hover:bg-slate-100 rounded-lg" className="p-1.5 hover:bg-slate-100 rounded-lg"
> >

View File

@@ -1,3 +1,14 @@
const ROLE_PERMISSIONS: 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'],
'swap-station': ['dashboard.view', 'kyc.request', 'kyc.view'],
merchant: ['dashboard.view', 'kyc.request', 'kyc.view', 'merchants.view'],
};
export const isAuthenticated = (): boolean => { export const isAuthenticated = (): boolean => {
return typeof window !== 'undefined' && !!sessionStorage.getItem('authToken'); return typeof window !== 'undefined' && !!sessionStorage.getItem('authToken');
}; };
@@ -10,10 +21,36 @@ export const getUserName = (): string | null => {
return typeof window !== 'undefined' ? sessionStorage.getItem('userName') : null; return typeof window !== 'undefined' ? sessionStorage.getItem('userName') : null;
}; };
export const getUserPermissions = (): string[] => {
if (typeof window === 'undefined') return [];
const stored = sessionStorage.getItem('userPermissions');
if (stored) return JSON.parse(stored);
const role = getUserRole();
return role ? (ROLE_PERMISSIONS[role] || []) : [];
};
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 = () => { export const logout = () => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
sessionStorage.removeItem('authToken'); sessionStorage.removeItem('authToken');
sessionStorage.removeItem('userRole'); sessionStorage.removeItem('userRole');
sessionStorage.removeItem('userName'); sessionStorage.removeItem('userName');
sessionStorage.removeItem('userPermissions');
} }
}; };