Files
lti-web-client/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx
T
2026-04-22 16:04:39 +07:00

580 lines
19 KiB
TypeScript

'use client';
import { useState } from 'react';
import {
Plus,
MoreVertical,
Pencil,
Trash2,
Search,
Loader2,
} from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label';
import { Input } from '@/figma-make/components/base/input';
import { Badge } from '@/figma-make/components/base/badge';
import { MultiSelect } from '@/figma-make/components/base/multi-select';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/figma-make/components/base/select';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/figma-make/components/base/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/figma-make/components/base/alert-dialog';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/figma-make/components/base/dropdown-menu';
import { toast } from 'sonner';
import useSWR from 'swr';
import { EmployeeApi } from '@/services/api/daily-checklist/employee';
import Table from '@/components/Table';
import { Employee } from '@/types/api/daily-checklist/employee';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ColumnDef } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
export function MasterEmployeeContent() {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
kandang_id: '',
status: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
kandang_id: 'kandang_id',
status: 'is_active',
},
});
const {
data: employees,
isLoading: isLoadingEmployees,
mutate: refreshEmployees,
} = useSWR(
`${EmployeeApi.basePath}${getTableFilterQueryString()}`,
EmployeeApi.getAllFetcher,
{
keepPreviousData: true,
}
);
const {
options: kandangOptions,
loadMore: loadMoreKandang,
isLoadingMore: isLoadingMoreKandang,
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}
};
const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [employeeToDelete, setEmployeeToDelete] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
const [employeeForm, setEmployeeForm] = useState({
id: 0,
name: '',
kandang_ids: [] as number[],
status: 'Active' as 'Active' | 'Non Active',
});
const employeeColumns: ColumnDef<Employee>[] = [
{
id: 'name',
header: 'Nama ABK',
accessorKey: 'name',
enableSorting: false,
},
{
id: 'kandang',
header: 'Kandang',
accessorKey: 'kandangs',
enableSorting: false,
cell: ({ row }) =>
row.original.kandangs.map((kandang) => kandang.name).join(', '),
},
{
id: 'status',
header: 'Status',
accessorKey: 'is_active',
enableSorting: false,
cell: ({ row }) => (
<Badge variant={row.original.is_active ? 'success' : 'secondary'}>
{row.original.is_active ? 'Active' : 'Non Active'}
</Badge>
),
},
{
id: 'action',
header: 'Aksi',
accessorKey: 'action',
enableSorting: false,
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
size='sm'
className='h-8 w-8 p-0 hover:bg-gray-100'
>
<MoreVertical className='h-4 w-4 text-gray-600' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
<Pencil className='mr-2 h-4 w-4' />
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteClick(row.original.id)}
className='text-red-600'
>
<Trash2 className='mr-2 h-4 w-4' />
Hapus
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
const handleAdd = () => {
setModalMode('create');
setEmployeeForm({ id: 0, name: '', kandang_ids: [], status: 'Active' });
setShowModal(true);
};
const handleEdit = (employee: Employee) => {
setModalMode('edit');
setEmployeeForm({
id: employee.id,
name: employee.name,
kandang_ids: employee.kandangs ? employee.kandangs.map((k) => k.id) : [],
status: employee.is_active ? 'Active' : 'Non Active',
});
setShowModal(true);
};
const handleSave = async () => {
if (!employeeForm.name.trim() || employeeForm.kandang_ids.length === 0) {
toast.error('Nama ABK dan minimal satu Kandang harus diisi');
return;
}
setLoading(true);
try {
if (modalMode === 'create') {
const createEmployeeResponse = await EmployeeApi.create({
is_active: employeeForm.status === 'Active',
kandang_ids: employeeForm.kandang_ids,
name: employeeForm.name.trim(),
});
if (isResponseError(createEmployeeResponse)) {
console.error(
'Error creating employee:',
createEmployeeResponse.message
);
toast.error(
'Gagal menambahkan ABK: ' + createEmployeeResponse.message
);
return;
}
refreshEmployees();
toast.success('ABK berhasil ditambahkan');
} else {
const updateEmployeeResponse = await EmployeeApi.update(
employeeForm.id,
{
is_active: employeeForm.status === 'Active',
kandang_ids: employeeForm.kandang_ids,
name: employeeForm.name.trim(),
}
);
if (isResponseError(updateEmployeeResponse)) {
console.error(
'Error updating employee:',
updateEmployeeResponse.message
);
toast.error(
'Gagal memperbarui ABK: ' + updateEmployeeResponse.message
);
return;
}
refreshEmployees();
toast.success('ABK berhasil diubah');
}
setShowModal(false);
setEmployeeForm({ id: 0, name: '', kandang_ids: [], status: 'Active' });
} catch (error) {
console.error('Error saving employee:', error);
toast.error('Terjadi kesalahan saat menyimpan ABK');
} finally {
setLoading(false);
}
};
const handleDeleteClick = (employeeId: number) => {
setEmployeeToDelete(employeeId);
setShowDeleteConfirm(true);
};
const handleConfirmDelete = async () => {
if (!employeeToDelete) return;
setLoading(true);
try {
const deleteEmployeeResponse = await EmployeeApi.delete(employeeToDelete);
if (isResponseError(deleteEmployeeResponse)) {
console.error(
'Error deleting employee:',
deleteEmployeeResponse.message
);
toast.error('Gagal menghapus ABK');
return;
}
refreshEmployees();
toast.success('ABK berhasil dihapus');
setShowDeleteConfirm(false);
setEmployeeToDelete(null);
// await fetchEmployees();
} catch (error) {
console.error('Error deleting employee:', error);
toast.error('Terjadi kesalahan saat menghapus ABK');
} finally {
setLoading(false);
}
};
if (isLoadingEmployees && !employees) {
return (
<div className='min-h-screen'>
<div className='p-6'>
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Master Employee (ABK)
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data {' '}
<span className='text-[#0069e0]'>Employee (ABK)</span>
</p>
</div>
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-12 text-center text-gray-500'>
Memuat data...
</CardContent>
</Card>
</div>
</div>
);
}
return (
<div className='min-h-screen'>
<div className='p-6'>
{/* Page Title */}
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Master Employee (ABK)
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data <span className='text-[#0069e0]'>Employee (ABK)</span>
</p>
</div>
{/* Main Card */}
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-0'>
{/* Single Toolbar Row */}
<div className='flex flex-wrap items-center justify-between gap-4 p-6 border-b border-gray-200/60'>
{/* LEFT: Search + Filters */}
<div className='flex items-center gap-3 flex-wrap'>
<div className='relative'>
<Search className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4' />
<DebouncedTextInput
name='search'
placeholder='Cari nama ABK atau kandang...'
value={tableFilterState.search}
onChange={(e) => updateFilter('search', e.target.value)}
className={{
wrapper: 'w-full sm:w-[280px] border-gray-200',
inputWrapper: 'px-3 py-2 h-fit rounded-md',
input: 'text-sm',
}}
startAdornment={
<Search className='text-gray-400 w-4 h-4' />
}
/>
</div>
<Select
value={tableFilterState.kandang_id}
onValueChange={(value) =>
updateFilter('kandang_id', value === 'all' ? '' : value)
}
>
<SelectTrigger className='w-[180px] border-gray-200'>
<SelectValue placeholder='Semua Kandang' />
</SelectTrigger>
<SelectContent onScroll={handleKandangScroll}>
<SelectItem value='all'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => (
<SelectItem
key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem>
))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent>
</Select>
<Select
value={tableFilterState.status}
onValueChange={(value) => {
updateFilter('status', value === 'all' ? '' : value);
}}
>
<SelectTrigger className='w-40 border-gray-200'>
<SelectValue placeholder='Semua Status' />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>Semua Status</SelectItem>
<SelectItem value='true'>Active</SelectItem>
<SelectItem value='false'>Non Active</SelectItem>
</SelectContent>
</Select>
</div>
{/* RIGHT: Export + Add */}
<div className='flex items-center gap-2 flex-wrap'>
<Button
onClick={handleAdd}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
<Plus className='w-4 h-4 mr-2' />
Tambah ABK
</Button>
</div>
</div>
{/* Table */}
<Table<Employee>
data={isResponseSuccess(employees) ? employees?.data : []}
columns={employeeColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={isResponseSuccess(employees) ? employees?.meta?.page : 0}
totalItems={
isResponseSuccess(employees)
? employees?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingEmployees}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(employees) &&
employees?.data?.length === 0,
}),
tableWrapperClassName:
'overflow-x-auto border border-solid border-base-content/10 rounded-none',
headerRowClassName: 'bg-gray-50/50',
headerColumnClassName:
'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
paginationClassName: 'px-4',
}}
/>
</CardContent>
</Card>
</div>
{/* Add/Edit Modal */}
<Dialog open={showModal} onOpenChange={setShowModal}>
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
<DialogHeader>
<DialogTitle>
{modalMode === 'create' ? 'Tambah ABK' : 'Edit ABK'}
</DialogTitle>
<DialogDescription>
{modalMode === 'create'
? 'Masukkan detail ABK baru'
: 'Ubah detail ABK'}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
<div>
<Label htmlFor='nama-abk'>
Nama ABK <span className='text-red-500'>*</span>
</Label>
<Input
id='nama-abk'
value={employeeForm.name}
onChange={(e) =>
setEmployeeForm({ ...employeeForm, name: e.target.value })
}
placeholder='Masukkan nama ABK'
className='mt-1.5'
disabled={loading}
/>
</div>
<div>
<Label htmlFor='kandang'>
Kandang <span className='text-red-500'>*</span>
</Label>
<MultiSelect
options={kandangOptions.map((k) => ({
value: String(k.value),
label: k.label,
}))}
selected={employeeForm.kandang_ids.map((id) => String(id))}
onChange={(selected) =>
setEmployeeForm({
...employeeForm,
kandang_ids: selected.map((id) => Number(id)),
})
}
onLoadMore={() => {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}}
isLoadingMore={isLoadingMoreKandang}
placeholder='Pilih kandang'
className='mt-1.5'
/>
</div>
<div>
<Label htmlFor='status'>
Status <span className='text-red-500'>*</span>
</Label>
<Select
value={employeeForm.status}
onValueChange={(value: 'Active' | 'Non Active') =>
setEmployeeForm({ ...employeeForm, status: value })
}
disabled={loading}
>
<SelectTrigger id='status' className='mt-1.5'>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value='Active'>
<div className='flex items-center'>
<Badge variant='success' className='mr-2'>
Active
</Badge>
</div>
</SelectItem>
<SelectItem value='Non Active'>
<div className='flex items-center'>
<Badge variant='secondary' className='mr-2'>
Non Active
</Badge>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button
variant='outline'
onClick={() => setShowModal(false)}
disabled={loading}
>
Batal
</Button>
<Button
onClick={handleSave}
disabled={loading}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
{loading ? 'Menyimpan...' : 'Simpan'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation */}
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogContent className='bg-white rounded-xl shadow-lg sm:max-w-md'>
<AlertDialogHeader>
<AlertDialogTitle>Hapus ABK?</AlertDialogTitle>
<AlertDialogDescription>
Data ABK akan dihapus secara permanen.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>Batal</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmDelete}
disabled={loading}
className='bg-red-600 hover:bg-red-700 text-white'
>
{loading ? 'Menghapus...' : 'Hapus'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}