mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
580 lines
19 KiB
TypeScript
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>
|
|
);
|
|
}
|