feat: add employee management module with roster, search, and filtering to hub details page
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user