feat: add employee management module with roster, search, and filtering to hub details page

This commit is contained in:
sazzadulalambd
2026-05-21 20:16:42 +06:00
parent 916eec0f72
commit c85b609797
2 changed files with 450 additions and 3 deletions

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, Battery Navigation, User, Wallet, DollarSign, CheckCircle, AlertTriangle, Battery,
Mail, Calendar, Briefcase, Users, Search, UserPlus
} from 'lucide-react'; } from 'lucide-react';
interface Hub { interface Hub {
@@ -90,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();
@@ -101,7 +166,23 @@ export default function HubDetailPage() {
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' | 'batteries' | '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 [assignModal, setAssignModal] = useState<BatteryInfo | null>(null);
const [selectedBike, setSelectedBike] = useState(''); const [selectedBike, setSelectedBike] = useState('');
const [addBikeModal, setAddBikeModal] = useState(false); const [addBikeModal, setAddBikeModal] = useState(false);
@@ -191,6 +272,15 @@ export default function HubDetailPage() {
> >
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'
@@ -376,6 +466,194 @@ 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">
@@ -745,6 +1023,175 @@ export default function HubDetailPage() {
</div> </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>
); );
} }

View File

@@ -1960,7 +1960,7 @@ export default function InvestorDetailPage() {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="font-semibold text-slate-800">EV Investment Plans</h3> <h3 className="font-semibold text-slate-800">Investment Plans</h3>
<p className="text-sm text-slate-500">Manage investment portfolios for this investor</p> <p className="text-sm text-slate-500">Manage investment portfolios for this investor</p>
</div> </div>
<button onClick={() => setShowCreateInvestmentModal(true)} className="py-2 px-4 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-2"> <button onClick={() => setShowCreateInvestmentModal(true)} className="py-2 px-4 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark flex items-center gap-2">