init: SR System — Helpdesk & ITAM Portal

This commit is contained in:
pray4skylark
2026-06-02 13:50:39 +09:00
commit 832c9f096f
262 changed files with 30470 additions and 0 deletions
+841
View File
@@ -0,0 +1,841 @@
'use client';
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Search, Plus, MoreHorizontal, X, Pencil, Trash2, AlertTriangle } from 'lucide-react';
import employeeService from '@/services/employeeService';
import { useCommonCodes } from '@/hooks/useCommonCodes';
import { useAuthStore } from '@/stores';
import type { Employee, RoleCode, EmpStatus, DispatchStatus } from '@/types';
const statusColor: Record<string, string> = {
ACTIVE: 'bg-status-resolved-bg text-status-resolved-text',
ON_LEAVE: 'bg-status-pending-bg text-status-pending-text',
RESIGNED: 'bg-status-critical-bg text-status-critical-text',
};
const initialColors = ['bg-indigo-500', 'bg-purple-500', 'bg-blue-500', 'bg-green-500', 'bg-orange-500', 'bg-pink-500'];
function getInitialColor(name: string, index: number) {
return initialColors[index % initialColors.length];
}
export default function EmployeesPage() {
const [filter, setFilter] = useState('all');
const [employees, setEmployees] = useState<Employee[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const { user } = useAuthStore();
const { codes: empStatusCodes, loading: statusCodesLoading } = useCommonCodes('EMP_STATUS');
const { codes: empRoleCodes, loading: roleCodesLoading } = useCommonCodes('EMP_ROLE');
const roleLabel = roleCodesLoading ? {} : Object.fromEntries(empRoleCodes.map((c) => [c.codeValue, c.codeName]));
// Add Employee Modal State
const [isModalOpen, setIsModalOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [modalError, setModalError] = useState<string | null>(null);
const [formData, setFormData] = useState({
loginId: '',
name: '',
email: '',
roleCode: 'EMPLOYEE' as RoleCode,
empStatus: 'ACTIVE' as EmpStatus,
dispatchStatus: 'WAITING' as DispatchStatus,
});
// Edit Modal State
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [editModalError, setEditModalError] = useState<string | null>(null);
const [editData, setEditData] = useState({
name: '',
email: '',
roleCode: 'EMPLOYEE' as RoleCode,
empStatus: 'ACTIVE' as EmpStatus,
dispatchStatus: 'WAITING' as DispatchStatus,
});
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null);
// Delete Confirmation State
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [deletingEmployee, setDeletingEmployee] = useState<Employee | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const fetchEmployees = async () => {
try {
setLoading(true);
const res = await employeeService.getAll({ size: 100 });
setEmployees(res.data.content);
setError(null);
} catch (err) {
setError('Failed to load employees. Please try again.');
console.error(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (!user) return;
if (user.roleCode === 'EMPLOYEE') {
setError('Only managers and admins can view employee list.');
setLoading(false);
return;
}
fetchEmployees();
}, [user]);
const handleOpenModal = () => {
setIsModalOpen(true);
setFormData({
loginId: '',
name: '',
email: '',
roleCode: 'EMPLOYEE',
empStatus: 'ACTIVE',
dispatchStatus: 'WAITING',
});
setModalError(null);
};
const handleCloseModal = () => {
setIsModalOpen(false);
setFormData({
loginId: '',
name: '',
email: '',
roleCode: 'EMPLOYEE',
empStatus: 'ACTIVE',
dispatchStatus: 'WAITING',
});
setModalError(null);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.loginId.trim() || !formData.name.trim()) {
setModalError('Login ID and Name are required.');
return;
}
setIsSubmitting(true);
setModalError(null);
try {
await employeeService.create(formData);
alert('Employee added successfully!');
handleCloseModal();
await fetchEmployees();
} catch (err: any) {
setModalError(err.response?.data?.message || 'Failed to add employee. Please try again.');
} finally {
setIsSubmitting(false);
}
};
// Edit handlers
const handleEdit = (emp: Employee) => {
setEditingEmployee(emp);
setEditData({
name: emp.name,
email: emp.email || '',
roleCode: emp.roleCode,
empStatus: emp.empStatus,
dispatchStatus: emp.dispatchStatus,
});
setEditModalError(null);
setIsEditModalOpen(true);
};
const handleCloseEditModal = () => {
setIsEditModalOpen(false);
setEditingEmployee(null);
setEditData({
name: '',
email: '',
roleCode: 'EMPLOYEE',
empStatus: 'ACTIVE',
dispatchStatus: 'WAITING',
});
setEditModalError(null);
};
const handleEditInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setEditData((prev) => ({ ...prev, [name]: value }));
};
const handleUpdateSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!editData.name.trim()) {
setEditModalError('Name is required.');
return;
}
if (!editingEmployee) return;
setIsUpdating(true);
setEditModalError(null);
try {
await employeeService.update(editingEmployee.empId, editData);
alert('Employee updated successfully!');
handleCloseEditModal();
await fetchEmployees();
} catch (err: any) {
setEditModalError(err.response?.data?.message || 'Failed to update employee. Please try again.');
} finally {
setIsUpdating(false);
}
};
// Delete handlers
const handleDelete = (emp: Employee) => {
setDeletingEmployee(emp);
setIsDeleteDialogOpen(true);
};
const handleCloseDeleteDialog = () => {
setIsDeleteDialogOpen(false);
setDeletingEmployee(null);
};
const handleDeleteConfirm = async () => {
if (!deletingEmployee) return;
setIsDeleting(true);
try {
await employeeService.delete(deletingEmployee.empId);
alert('Employee deleted successfully!');
handleCloseDeleteDialog();
await fetchEmployees();
} catch (err: any) {
alert(err.response?.data?.message || 'Failed to delete employee. Please try again.');
handleCloseDeleteDialog();
} finally {
setIsDeleting(false);
}
};
const [skillKeyword, setSkillKeyword] = useState('');
const [skillResults, setSkillResults] = useState<Employee[]>([]);
useEffect(() => {
if (!skillKeyword.trim()) { setSkillResults([]); return; }
const timer = setTimeout(() => {
employeeService.search(skillKeyword.trim()).then((res) => {
const empIds = new Set(res.data.map((r: any) => r.empId));
setSkillResults(employees.filter((e) => empIds.has(e.empId)));
}).catch(() => {});
}, 300);
return () => clearTimeout(timer);
}, [skillKeyword, employees]);
const displayList = skillKeyword.trim() ? skillResults : employees;
const filtered = filter === 'all' ? displayList : displayList.filter((e) => e.empStatus === filter);
const stats = [
{ label: '전체', value: employees.length, color: 'text-text-main' },
{ label: '재직', value: employees.filter((e) => e.empStatus === 'ACTIVE').length, color: 'text-status-resolved-text' },
{ label: '파견중', value: employees.filter((e) => e.dispatchStatus === 'DISPATCHED').length, color: 'text-status-pending-text' },
{ label: '휴직', value: employees.filter((e) => e.empStatus === 'ON_LEAVE').length, color: 'text-status-critical-text' },
];
// Add Employee Modal
if (isModalOpen) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-surface rounded-2xl shadow-xl max-w-md w-full mx-4 p-6 relative animate-in fade-in zoom-in duration-200">
<button
onClick={handleCloseModal}
className="absolute top-4 right-4 p-1.5 text-text-muted hover:text-text-main hover:bg-gray-100 rounded-lg transition-all"
>
<X className="w-5 h-5" />
</button>
<h3 className="font-outfit text-xl font-semibold text-text-main mb-1">Add Employee</h3>
<p className="text-text-muted text-sm mb-6">Create a new employee account</p>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-text-main mb-1.5">
Login ID <span className="text-status-critical-text">*</span>
</label>
<input
type="text"
name="loginId"
value={formData.loginId}
onChange={handleInputChange}
required
className="w-full px-3 py-2.5 rounded-xl bg-gray-50 border border-border text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:bg-white transition-all"
placeholder="Enter login ID"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-main mb-1.5">
Name <span className="text-status-critical-text">*</span>
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
required
className="w-full px-3 py-2.5 rounded-xl bg-gray-50 border border-border text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:bg-white transition-all"
placeholder="Enter full name"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-main mb-1.5">Email</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className="w-full px-3 py-2.5 rounded-xl bg-gray-50 border border-border text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:bg-white transition-all"
placeholder="email@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-main mb-1.5">Role</label>
<select
name="roleCode"
value={formData.roleCode}
onChange={handleInputChange}
className="w-full px-3 py-2.5 rounded-xl bg-gray-50 border border-border text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:bg-white transition-all"
>
{roleCodesLoading ? (
<option value="">Loading roles...</option>
) : (
empRoleCodes.map((code) => (
<option key={code.codeValue} value={code.codeValue}>
{code.codeName} ({code.codeValue})
</option>
))
)}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-main mb-1.5">Employee Status</label>
<select
name="empStatus"
value={formData.empStatus}
onChange={handleInputChange}
className="w-full px-3 py-2.5 rounded-xl bg-gray-50 border border-border text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:bg-white transition-all"
>
{statusCodesLoading ? (
<option value="">Loading status...</option>
) : (
empStatusCodes.map((code) => (
<option key={code.codeValue} value={code.codeValue}>
{code.codeName} ({code.codeValue})
</option>
))
)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-main mb-1.5">Dispatch Status</label>
<select
name="dispatchStatus"
value={formData.dispatchStatus}
onChange={handleInputChange}
className="w-full px-3 py-2.5 rounded-xl bg-gray-50 border border-border text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:bg-white transition-all"
>
<option value="WAITING">대기 (WAITING)</option>
<option value="DISPATCHED">파견중 (DISPATCHED)</option>
</select>
</div>
</div>
{modalError && (
<div className="bg-status-critical-bg border border-status-critical text-status-critical-text rounded-xl px-4 py-3 text-sm">
{modalError}
</div>
)}
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={handleCloseModal}
disabled={isSubmitting}
className="flex-1 px-4 py-2.5 rounded-xl border border-border text-text-main text-sm font-medium hover:bg-gray-100 transition-all disabled:opacity-50"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className="flex-1 px-4 py-2.5 rounded-xl bg-primary hover:bg-primary-hover text-white text-sm font-semibold shadow-soft transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Adding...' : 'Add Employee'}
</button>
</div>
</form>
</div>
</div>
);
}
// Edit Employee Modal
if (isEditModalOpen) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-surface rounded-2xl shadow-xl max-w-md w-full mx-4 p-6 relative animate-in fade-in zoom-in duration-200">
<button
onClick={handleCloseEditModal}
className="absolute top-4 right-4 p-1.5 text-text-muted hover:text-text-main hover:bg-gray-100 rounded-lg transition-all"
>
<X className="w-5 h-5" />
</button>
<h3 className="font-outfit text-xl font-semibold text-text-main mb-1">Edit Employee</h3>
<p className="text-text-muted text-sm mb-6">Update employee information</p>
<form onSubmit={handleUpdateSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-text-main mb-1.5">
Name <span className="text-status-critical-text">*</span>
</label>
<input
type="text"
name="name"
value={editData.name}
onChange={handleEditInputChange}
required
className="w-full px-3 py-2.5 rounded-xl bg-gray-50 border border-border text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:bg-white transition-all"
placeholder="Enter full name"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-main mb-1.5">Email</label>
<input
type="email"
name="email"
value={editData.email}
onChange={handleEditInputChange}
className="w-full px-3 py-2.5 rounded-xl bg-gray-50 border border-border text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:bg-white transition-all"
placeholder="email@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-main mb-1.5">Role</label>
<select
name="roleCode"
value={editData.roleCode}
onChange={handleEditInputChange}
className="w-full px-3 py-2.5 rounded-xl bg-gray-50 border border-border text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:bg-white transition-all"
>
{roleCodesLoading ? (
<option value="">Loading roles...</option>
) : (
empRoleCodes.map((code) => (
<option key={code.codeValue} value={code.codeValue}>
{code.codeName} ({code.codeValue})
</option>
))
)}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-main mb-1.5">Employee Status</label>
<select
name="empStatus"
value={editData.empStatus}
onChange={handleEditInputChange}
className="w-full px-3 py-2.5 rounded-xl bg-gray-50 border border-border text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:bg-white transition-all"
>
{statusCodesLoading ? (
<option value="">Loading status...</option>
) : (
empStatusCodes.map((code) => (
<option key={code.codeValue} value={code.codeValue}>
{code.codeName} ({code.codeValue})
</option>
))
)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-main mb-1.5">Dispatch Status</label>
<select
name="dispatchStatus"
value={editData.dispatchStatus}
onChange={handleEditInputChange}
className="w-full px-3 py-2.5 rounded-xl bg-gray-50 border border-border text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:bg-white transition-all"
>
<option value="WAITING">대기 (WAITING)</option>
<option value="DISPATCHED">파견중 (DISPATCHED)</option>
</select>
</div>
</div>
{editModalError && (
<div className="bg-status-critical-bg border border-status-critical text-status-critical-text rounded-xl px-4 py-3 text-sm">
{editModalError}
</div>
)}
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={handleCloseEditModal}
disabled={isUpdating}
className="flex-1 px-4 py-2.5 rounded-xl border border-border text-text-main text-sm font-medium hover:bg-gray-100 transition-all disabled:opacity-50"
>
Cancel
</button>
<button
type="submit"
disabled={isUpdating}
className="flex-1 px-4 py-2.5 rounded-xl bg-primary hover:bg-primary-hover text-white text-sm font-semibold shadow-soft transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isUpdating ? 'Updating...' : 'Save Changes'}
</button>
</div>
</form>
</div>
</div>
);
}
// Delete Confirmation Dialog
if (isDeleteDialogOpen) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-surface rounded-2xl shadow-xl max-w-md w-full mx-4 p-6 relative animate-in fade-in zoom-in duration-200">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-full bg-status-critical-bg flex items-center justify-center flex-shrink-0">
<AlertTriangle className="w-6 h-6 text-status-critical-text" />
</div>
<div>
<h3 className="font-outfit text-xl font-semibold text-text-main">Delete Employee</h3>
<p className="text-text-muted text-sm">This action cannot be undone</p>
</div>
</div>
<div className="bg-gray-50 rounded-xl px-4 py-3 mb-6">
<p className="text-sm text-text-main">
Are you sure you want to delete <span className="font-semibold">{deletingEmployee?.name}</span>?
</p>
<p className="text-xs text-text-muted mt-1">
This will soft-delete the employee record and remove their access to the system.
</p>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={handleCloseDeleteDialog}
disabled={isDeleting}
className="flex-1 px-4 py-2.5 rounded-xl border border-border text-text-main text-sm font-medium hover:bg-gray-100 transition-all disabled:opacity-50"
>
Cancel
</button>
<button
type="button"
onClick={handleDeleteConfirm}
disabled={isDeleting}
className="flex-1 px-4 py-2.5 rounded-xl bg-status-critical hover:bg-status-critical-hover text-white text-sm font-semibold shadow-soft transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isDeleting ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
</div>
);
}
if (loading) {
return (
<div className="p-4 sm:p-6 lg:p-8">
<div className="max-w-7xl mx-auto">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4 mb-6 sm:mb-8">
<div>
<h2 className="font-outfit text-2xl sm:text-3xl font-semibold text-text-main tracking-tight">Employees</h2>
<p className="text-text-muted mt-1 text-sm">Manage employee records, dispatch status, and role assignments.</p>
</div>
<div className="h-10 w-32 bg-gray-200 rounded-xl animate-pulse" />
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="bg-surface rounded-2xl p-5 border border-border">
<div className="h-3 w-12 bg-gray-200 rounded mb-2 animate-pulse" />
<div className="h-7 w-16 bg-gray-200 rounded animate-pulse" />
</div>
))}
</div>
<div className="bg-surface rounded-2xl border border-border shadow-card overflow-hidden">
<div className="px-6 py-4 border-b border-border flex items-center justify-between">
<div className="flex space-x-2">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-8 w-16 bg-gray-200 rounded-lg animate-pulse" />
))}
</div>
<div className="h-9 w-48 bg-gray-200 rounded-lg animate-pulse" />
</div>
<div className="p-6">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="flex items-center py-4 border-b border-gray-100 last:border-0">
<div className="w-8 h-8 rounded-full bg-gray-200 mr-3 animate-pulse" />
<div className="flex-1 h-4 bg-gray-200 rounded animate-pulse" />
</div>
))}
</div>
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="p-4 sm:p-6 lg:p-8">
<div className="max-w-7xl mx-auto">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4 mb-6 sm:mb-8">
<div>
<h2 className="font-outfit text-2xl sm:text-3xl font-semibold text-text-main tracking-tight">Employees</h2>
<p className="text-text-muted mt-1 text-sm">Manage employee records, dispatch status, and role assignments.</p>
</div>
</div>
<div className="bg-status-critical-bg border border-status-critical text-status-critical-text rounded-2xl p-6 sm:p-8 text-center">
<p className="font-medium">{error}</p>
</div>
</div>
</div>
);
}
if (employees.length === 0) {
return (
<div className="p-4 sm:p-6 lg:p-8">
<div className="max-w-7xl mx-auto">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4 mb-6 sm:mb-8">
<div>
<h2 className="font-outfit text-2xl sm:text-3xl font-semibold text-text-main tracking-tight">Employees</h2>
<p className="text-text-muted mt-1 text-sm">Manage employee records, dispatch status, and role assignments.</p>
</div>
<button onClick={handleOpenModal} className="w-full sm:w-auto bg-primary hover:bg-primary-hover text-white px-5 py-2.5 rounded-xl text-sm font-semibold shadow-soft transition-all flex items-center justify-center">
<Plus className="w-4 h-4 mr-2" />
Add Employee
</button>
</div>
<div className="bg-surface rounded-2xl border border-border shadow-card p-12 text-center">
<p className="text-text-muted text-lg">No employees found</p>
</div>
</div>
</div>
);
}
return (
<div className="p-4 sm:p-6 lg:p-8">
<div className="max-w-7xl mx-auto">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4 mb-6 sm:mb-8">
<div>
<h2 className="font-outfit text-2xl sm:text-3xl font-semibold text-text-main tracking-tight">Employees</h2>
<p className="text-text-muted mt-1 text-sm">Manage employee records, dispatch status, and role assignments.</p>
</div>
<button onClick={handleOpenModal} className="w-full sm:w-auto bg-primary hover:bg-primary-hover text-white px-5 py-2.5 rounded-xl text-sm font-semibold shadow-soft transition-all flex items-center justify-center">
<Plus className="w-4 h-4 mr-2" />
Add Employee
</button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
{stats.map((s) => (
<div key={s.label} className="bg-surface rounded-2xl p-5 border border-border">
<p className="text-xs text-text-muted font-medium mb-1">{s.label}</p>
<p className={`font-outfit text-2xl font-semibold ${s.color}`}>{s.value}</p>
</div>
))}
</div>
<div className="bg-surface rounded-2xl border border-border shadow-card overflow-hidden">
<div className="px-4 sm:px-6 py-4 border-b border-border flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<button
key="all"
onClick={() => setFilter('all')}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
filter === 'all' ? 'bg-primary text-white' : 'text-text-muted hover:bg-gray-100'
}`}
>
전체
</button>
{statusCodesLoading ? (
<span className="text-xs text-text-muted">Loading...</span>
) : (
empStatusCodes.map((code) => (
<button
key={code.codeValue}
onClick={() => setFilter(code.codeValue)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
filter === code.codeValue ? 'bg-primary text-white' : 'text-text-muted hover:bg-gray-100'
}`}
>
{code.codeName}
</button>
))
)}
</div>
<div className="relative w-full sm:w-56">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
value={skillKeyword}
onChange={(e) => setSkillKeyword(e.target.value)}
className="w-full pl-9 pr-3 py-1.5 rounded-lg bg-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="Search by name or skill..."
/>
</div>
</div>
<div className="overflow-x-auto">
{/* Desktop table */}
<div className="hidden md:block">
<table className="w-full text-left">
<thead>
<tr className="bg-gray-50/50">
<th className="py-3 px-4 sm:px-6 text-xs font-semibold text-text-muted uppercase whitespace-nowrap">이름</th>
<th className="py-3 px-4 sm:px-6 text-xs font-semibold text-text-muted uppercase whitespace-nowrap">권한</th>
<th className="py-3 px-4 sm:px-6 text-xs font-semibold text-text-muted uppercase whitespace-nowrap">상태</th>
<th className="py-3 px-4 sm:px-6 text-xs font-semibold text-text-muted uppercase whitespace-nowrap">이메일</th>
<th className="py-3 px-4 sm:px-6 text-xs font-semibold text-text-muted uppercase whitespace-nowrap">액션</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 text-sm">
{filtered.map((emp, idx) => (
<tr key={emp.empId} className="hover:bg-gray-50 transition-colors">
<td className="py-4 px-4 sm:px-6">
<div className="flex items-center">
<div className={`w-8 h-8 rounded-full ${getInitialColor(emp.name, idx)} flex items-center justify-center text-xs font-medium text-white mr-3 flex-shrink-0`}>
{emp.name[0]}
</div>
<div className="min-w-0">
<p className="font-medium text-text-main truncate">{emp.name}</p>
<p className="text-xs text-text-muted truncate">{emp.email}</p>
</div>
</div>
</td>
<td className="py-4 px-4 sm:px-6">
<span className="text-xs font-medium text-text-muted">{roleLabel[emp.roleCode] || emp.roleCode}</span>
</td>
<td className="py-4 px-4 sm:px-6">
<span className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${statusColor[emp.empStatus]}`}>
{emp.empStatus === 'ACTIVE' ? '재직' : emp.empStatus === 'ON_LEAVE' ? '휴직' : '퇴사'}
{emp.dispatchStatus === 'DISPATCHED' && (
<span className="ml-1.5 px-1.5 py-0.5 rounded bg-blue-100 text-blue-600 text-[10px]">파견중</span>
)}
</span>
</td>
<td className="py-4 px-4 sm:px-6 text-text-muted truncate max-w-[150px] sm:max-w-none">{emp.email}</td>
<td className="py-4 px-4 sm:px-6">
{user?.roleCode === 'ADMIN' && (
<div className="flex items-center gap-1">
<button
onClick={() => handleEdit(emp)}
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
title="Edit"
>
<Pencil className="w-3.5 h-3.5 text-text-muted hover:text-primary" />
</button>
<button
onClick={() => handleDelete(emp)}
className="p-1.5 hover:bg-red-50 rounded-lg transition-colors"
title="Delete"
>
<Trash2 className="w-3.5 h-3.5 text-text-muted hover:text-status-critical-text" />
</button>
</div>
)}
{user?.roleCode !== 'ADMIN' && (
<button
onClick={() => router.push(`/profile/${emp.empId}`)}
className="p-1.5 text-text-muted hover:text-primary hover:bg-gray-100 rounded-lg transition-all"
title="View Profile"
>
<MoreHorizontal className="w-4 h-4" />
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile cards */}
<div className="md:hidden space-y-3">
{filtered.map((emp, idx) => (
<div key={emp.empId} className="bg-surface rounded-2xl border border-border p-4 space-y-2">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3 flex-1">
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-white text-sm font-semibold ${getInitialColor(emp.name, idx)}`}>
{emp.name.charAt(0)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-text-main truncate">{emp.name}</p>
<p className="text-xs text-text-muted truncate">{roleLabel[emp.roleCode] || emp.roleCode}</p>
</div>
</div>
<span className={`text-[10px] font-medium px-2.5 py-1 rounded-full ${statusColor[emp.empStatus] || 'bg-gray-100 text-gray-600'}`}>
{emp.empStatus === 'ACTIVE' ? '재직' : emp.empStatus === 'ON_LEAVE' ? '휴직' : '퇴사'}
</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs text-text-muted">
<div className="truncate">Email: {emp.email}</div>
<div>상태: {emp.dispatchStatus === 'DISPATCHED' ? '파견중' : '대기'}</div>
</div>
{user?.roleCode === 'ADMIN' && (
<div className="flex items-center gap-2 pt-2 border-t border-gray-100">
<button
onClick={() => handleEdit(emp)}
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 rounded-lg text-xs font-medium text-text-muted hover:bg-gray-100 transition-all"
>
<Pencil className="w-3.5 h-3.5" />
Edit
</button>
<button
onClick={() => handleDelete(emp)}
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 rounded-lg text-xs font-medium text-status-critical-text hover:bg-red-50 transition-all"
>
<Trash2 className="w-3.5 h-3.5" />
Delete
</button>
</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}