feat: integrate MasterEmployeeContent component to API

This commit is contained in:
ValdiANS
2026-01-08 09:40:02 +07:00
parent 06dd9a3609
commit db4d9ad38c
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { import {
Plus, Plus,
Download, Download,
@@ -48,124 +48,132 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/figma-make/components/base/dropdown-menu'; } from '@/figma-make/components/base/dropdown-menu';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
import useSWR from 'swr'; import useSWR from 'swr';
import { EmployeeApi } from '@/services/api/daily-checklist/employee'; import { EmployeeApi } from '@/services/api/daily-checklist/employee';
import Table from '@/components/Table';
interface Employee { import { Employee } from '@/types/api/daily-checklist/employee';
id: string; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
name: string; import { cn } from '@/lib/helper';
kandang_id: string; import { useTableFilter } from '@/services/hooks/useTableFilter';
is_active: boolean; import { ColumnDef } from '@tanstack/react-table';
kandang?: { import { useSelect } from '@/components/input/SelectInput';
id: string; import { KandangApi } from '@/services/api/master-data';
name: string; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
};
}
interface Kandang {
id: string;
name: string;
}
export function MasterEmployeeContent() { export function MasterEmployeeContent() {
const { data: employeesTest, isLoading: isLoadingEmployees } = useSWR( const {
EmployeeApi.basePath, 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, EmployeeApi.getAllFetcher,
{ {
keepPreviousData: true, keepPreviousData: true,
} }
); );
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
useSelect(KandangApi.basePath, 'id', 'name', 'search', {
page: '1',
limit: '100',
});
const [employees, setEmployees] = useState<Employee[]>([]);
const [kandangList, setKandangList] = useState<Kandang[]>([]);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [employeeToDelete, setEmployeeToDelete] = useState<string | null>(null); const [employeeToDelete, setEmployeeToDelete] = useState<number | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [initialLoading, setInitialLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [kandangFilter, setKandangFilter] = useState<string>('all');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create'); const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
const [employeeForm, setEmployeeForm] = useState({ const [employeeForm, setEmployeeForm] = useState({
id: '', id: 0,
name: '', name: '',
kandang_ids: [] as string[], kandang_ids: [] as number[],
status: 'Active' as 'Active' | 'Non Active', status: 'Active' as 'Active' | 'Non Active',
}); });
const fetchEmployees = async () => { const employeeColumns: ColumnDef<Employee>[] = [
if (!isSupabaseConfigured()) { {
console.warn( id: 'name',
'Supabase not configured. Please add environment variables.' header: 'Nama ABK',
); accessorKey: 'name',
setInitialLoading(false); enableSorting: false,
return; },
} {
id: 'kandang',
try { header: 'Kandang',
const { data, error } = await supabase accessorKey: 'kandangs',
.from('employees') enableSorting: false,
.select( cell: ({ row }) =>
` row.original.kandangs.map((kandang) => kandang.name).join(', '),
id, },
name, {
kandang_id, id: 'status',
is_active, header: 'Status',
kandang:kandang_id ( accessorKey: 'is_active',
id, enableSorting: false,
name cell: ({ row }) => (
) <Badge variant={row.original.is_active ? 'success' : 'secondary'}>
` {row.original.is_active ? 'Active' : 'Non Active'}
) </Badge>
.order('name', { ascending: true }); ),
},
if (error) { {
console.error('Error fetching employees:', error); id: 'action',
toast.error('Gagal memuat data ABK'); header: 'Aksi',
return; accessorKey: 'action',
} enableSorting: false,
cell: ({ row }) => (
setEmployees((data as unknown as Employee[]) || []); <DropdownMenu>
} catch (error) { <DropdownMenuTrigger asChild>
console.error('Error fetching employees:', error); <Button
toast.error('Gagal memuat data ABK'); variant='ghost'
} finally { size='sm'
setInitialLoading(false); className='h-8 w-8 p-0 hover:bg-gray-100'
} >
}; <MoreVertical className='h-4 w-4 text-gray-600' />
</Button>
const fetchKandang = async () => { </DropdownMenuTrigger>
if (!isSupabaseConfigured()) { <DropdownMenuContent align='end'>
return; <DropdownMenuItem onClick={() => handleEdit(row.original)}>
} <Pencil className='mr-2 h-4 w-4' />
Edit
try { </DropdownMenuItem>
const { data, error } = await supabase <DropdownMenuItem
.from('kandang') onClick={() => handleDeleteClick(row.original.id)}
.select('id, name') className='text-red-600'
.order('name', { ascending: true }); >
<Trash2 className='mr-2 h-4 w-4' />
if (error) { Hapus
console.error('Error fetching kandang:', error); </DropdownMenuItem>
return; </DropdownMenuContent>
} </DropdownMenu>
),
setKandangList(data || []); },
} catch (error) { ];
console.error('Error fetching kandang:', error);
}
};
useEffect(() => {
fetchEmployees();
fetchKandang();
}, []);
const handleAdd = () => { const handleAdd = () => {
setModalMode('create'); setModalMode('create');
setEmployeeForm({ id: '', name: '', kandang_ids: [], status: 'Active' }); setEmployeeForm({ id: 0, name: '', kandang_ids: [], status: 'Active' });
setShowModal(true); setShowModal(true);
}; };
@@ -174,7 +182,7 @@ export function MasterEmployeeContent() {
setEmployeeForm({ setEmployeeForm({
id: employee.id, id: employee.id,
name: employee.name, name: employee.name,
kandang_ids: employee.kandang_id ? [employee.kandang_id] : [], kandang_ids: employee.kandangs ? employee.kandangs.map((k) => k.id) : [],
status: employee.is_active ? 'Active' : 'Non Active', status: employee.is_active ? 'Active' : 'Non Active',
}); });
setShowModal(true); setShowModal(true);
@@ -186,58 +194,52 @@ export function MasterEmployeeContent() {
return; return;
} }
if (!isSupabaseConfigured()) {
toast.error(
'Supabase belum dikonfigurasi. Tambahkan environment variables.'
);
return;
}
setLoading(true); setLoading(true);
try { try {
// Create payload - taking the first selected kandang for now as schema supports single FK
// TODO: Support multiple kandangs in backend
const kandangIdToSave = employeeForm.kandang_ids[0];
if (modalMode === 'create') { if (modalMode === 'create') {
const { error } = await supabase.from('employees').insert([ const createEmployeeResponse = await EmployeeApi.create({
{ is_active: employeeForm.status === 'Active',
name: employeeForm.name.trim(), kandang_ids: employeeForm.kandang_ids,
kandang_id: kandangIdToSave, name: employeeForm.name.trim(),
is_active: employeeForm.status === 'Active', });
},
]);
if (error) { if (isResponseError(createEmployeeResponse)) {
console.error('Error creating employee:', error); console.error(
'Error creating employee:',
createEmployeeResponse.message
);
toast.error('Gagal menambahkan ABK'); toast.error('Gagal menambahkan ABK');
return; return;
} }
refreshEmployees();
toast.success('ABK berhasil ditambahkan'); toast.success('ABK berhasil ditambahkan');
} else { } else {
const { error } = await supabase const updateEmployeeResponse = await EmployeeApi.update(
.from('employees') employeeForm.id,
.update({ {
name: employeeForm.name.trim(),
kandang_id: kandangIdToSave,
is_active: employeeForm.status === 'Active', is_active: employeeForm.status === 'Active',
}) kandang_ids: employeeForm.kandang_ids,
.eq('id', employeeForm.id); name: employeeForm.name.trim(),
}
);
if (error) { if (isResponseError(updateEmployeeResponse)) {
console.error('Error updating employee:', error); console.error(
toast.error('Gagal mengubah ABK'); 'Error updating employee:',
updateEmployeeResponse.message
);
toast.error('Gagal menambahkan ABK');
return; return;
} }
refreshEmployees();
toast.success('ABK berhasil diubah'); toast.success('ABK berhasil diubah');
} }
setShowModal(false); setShowModal(false);
setEmployeeForm({ id: '', name: '', kandang_ids: [], status: 'Active' }); setEmployeeForm({ id: 0, name: '', kandang_ids: [], status: 'Active' });
await fetchEmployees();
} catch (error) { } catch (error) {
console.error('Error saving employee:', error); console.error('Error saving employee:', error);
toast.error('Terjadi kesalahan saat menyimpan ABK'); toast.error('Terjadi kesalahan saat menyimpan ABK');
@@ -246,7 +248,7 @@ export function MasterEmployeeContent() {
} }
}; };
const handleDeleteClick = (employeeId: string) => { const handleDeleteClick = (employeeId: number) => {
setEmployeeToDelete(employeeId); setEmployeeToDelete(employeeId);
setShowDeleteConfirm(true); setShowDeleteConfirm(true);
}; };
@@ -257,21 +259,22 @@ export function MasterEmployeeContent() {
setLoading(true); setLoading(true);
try { try {
const { error } = await supabase const deleteEmployeeResponse = await EmployeeApi.delete(employeeToDelete);
.from('employees')
.delete()
.eq('id', employeeToDelete);
if (error) { if (isResponseError(deleteEmployeeResponse)) {
console.error('Error deleting employee:', error); console.error(
'Error deleting employee:',
deleteEmployeeResponse.message
);
toast.error('Gagal menghapus ABK'); toast.error('Gagal menghapus ABK');
return; return;
} }
refreshEmployees();
toast.success('ABK berhasil dihapus'); toast.success('ABK berhasil dihapus');
setShowDeleteConfirm(false); setShowDeleteConfirm(false);
setEmployeeToDelete(null); setEmployeeToDelete(null);
await fetchEmployees(); // await fetchEmployees();
} catch (error) { } catch (error) {
console.error('Error deleting employee:', error); console.error('Error deleting employee:', error);
toast.error('Terjadi kesalahan saat menghapus ABK'); toast.error('Terjadi kesalahan saat menghapus ABK');
@@ -284,22 +287,7 @@ export function MasterEmployeeContent() {
toast.success(`Data berhasil diekspor ke ${format}`); toast.success(`Data berhasil diekspor ke ${format}`);
}; };
const filteredEmployees = employees.filter((emp) => { if (isLoadingEmployees && !employees) {
const kandangName = emp.kandang?.name || '';
const matchesSearch =
emp.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
kandangName.toLowerCase().includes(searchQuery.toLowerCase());
const matchesKandang =
kandangFilter === 'all' || emp.kandang_id === kandangFilter;
const matchesStatus =
statusFilter === 'all' || emp.is_active === (statusFilter === 'active');
return matchesSearch && matchesKandang && matchesStatus;
});
if (initialLoading) {
return ( return (
<div className='min-h-screen'> <div className='min-h-screen'>
<div className='p-6'> <div className='p-6'>
@@ -339,48 +327,75 @@ export function MasterEmployeeContent() {
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'> <Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-0'> <CardContent className='p-0'>
{/* Single Toolbar Row */} {/* Single Toolbar Row */}
<div className='flex items-center justify-between gap-4 p-6 border-b border-gray-200/60'> <div className='flex flex-wrap items-center justify-between gap-4 p-6 border-b border-gray-200/60'>
{/* LEFT: Search + Filters */} {/* LEFT: Search + Filters */}
<div className='flex items-center gap-3'> <div className='flex items-center gap-3 flex-wrap'>
<div className='relative'> <div className='relative'>
<Search className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4' /> <Search className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4' />
<Input {/* <Input
type='text' type='text'
placeholder='Cari nama ABK atau kandang...' placeholder='Cari nama ABK atau kandang...'
value={searchQuery} value={tableFilterState.search}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => updateFilter('search', e.target.value)}
className='pl-10 w-[280px] border-gray-200' className='pl-10 w-[280px] border-gray-200'
/> */}
<DebouncedTextInput
name='search'
placeholder='Cari nama ABK atau kandang...'
value={tableFilterState.search}
onChange={(e) => updateFilter('search', e.target.value)}
className={{
wrapper: '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> </div>
<Select value={kandangFilter} onValueChange={setKandangFilter}> <Select
value={tableFilterState.kandang_id}
onValueChange={(value) =>
updateFilter('kandang_id', value === 'all' ? '' : value)
}
>
<SelectTrigger className='w-[180px] border-gray-200'> <SelectTrigger className='w-[180px] border-gray-200'>
<SelectValue placeholder='Semua Kandang' /> <SelectValue placeholder='Semua Kandang' />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value='all'>Semua Kandang</SelectItem> <SelectItem value='all'>Semua Kandang</SelectItem>
{kandangList.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem key={kandang.id} value={kandang.id}> <SelectItem
{kandang.name} key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={statusFilter} onValueChange={setStatusFilter}> <Select
value={tableFilterState.status}
onValueChange={(value) => {
updateFilter('status', value === 'all' ? '' : value);
}}
>
<SelectTrigger className='w-[160px] border-gray-200'> <SelectTrigger className='w-[160px] border-gray-200'>
<SelectValue placeholder='Semua Status' /> <SelectValue placeholder='Semua Status' />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value='all'>Semua Status</SelectItem> <SelectItem value='all'>Semua Status</SelectItem>
<SelectItem value='active'>Active</SelectItem> <SelectItem value='true'>Active</SelectItem>
<SelectItem value='non_active'>Non Active</SelectItem> <SelectItem value='false'>Non Active</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* RIGHT: Export + Add */} {/* RIGHT: Export + Add */}
<div className='flex items-center gap-2'> <div className='flex items-center gap-2 flex-wrap'>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
@@ -413,93 +428,33 @@ export function MasterEmployeeContent() {
</div> </div>
{/* Table */} {/* Table */}
<div className='overflow-x-auto'> <Table<Employee>
<table className='w-full'> data={isResponseSuccess(employees) ? employees?.data : []}
<thead> columns={employeeColumns}
<tr className='border-b border-gray-200/60 bg-gray-50/50'> pageSize={tableFilterState.pageSize}
<th className='text-left py-3.5 px-6 text-sm font-semibold text-gray-700'> onPageSizeChange={setPageSize}
Nama ABK rowOptions={[10, 20, 50, 100]}
</th> page={isResponseSuccess(employees) ? employees?.meta?.page : 0}
<th className='text-left py-3.5 px-6 text-sm font-semibold text-gray-700'> totalItems={
Kandang isResponseSuccess(employees)
</th> ? employees?.meta?.total_results
<th className='text-left py-3.5 px-6 text-sm font-semibold text-gray-700'> : 0
Status }
</th> onPageChange={setPage}
<th className='text-center py-3.5 px-6 text-sm font-semibold text-gray-700 w-[80px]'> isLoading={isLoadingEmployees}
Aksi className={{
</th> containerClassName: cn({
</tr> 'w-full mb-20':
</thead> isResponseSuccess(employees) &&
<tbody className='divide-y divide-gray-200/60'> employees?.data?.length === 0,
{filteredEmployees.length === 0 ? ( }),
<tr> tableWrapperClassName: 'rounded-none',
<td headerRowClassName: 'bg-gray-50/50',
colSpan={4} headerColumnClassName:
className='text-center py-12 text-gray-500' 'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
> paginationClassName: 'px-4',
{searchQuery || }}
kandangFilter !== 'all' || />
statusFilter !== 'all'
? 'Tidak ada ABK yang ditemukan'
: 'Belum ada data ABK'}
</td>
</tr>
) : (
filteredEmployees.map((employee) => (
<tr
key={employee.id}
className='hover:bg-blue-50/30 transition-colors'
>
<td className='py-3.5 px-6 text-sm text-gray-900'>
{employee.name}
</td>
<td className='py-3.5 px-6 text-sm text-gray-700'>
{employee.kandang?.name || '-'}
</td>
<td className='py-3.5 px-6 text-sm'>
<Badge
variant={
employee.is_active ? 'success' : 'secondary'
}
>
{employee.is_active ? 'Active' : 'Non Active'}
</Badge>
</td>
<td className='py-3.5 px-6 text-center'>
<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(employee)}
>
<Pencil className='mr-2 h-4 w-4' />
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteClick(employee.id)}
className='text-red-600'
>
<Trash2 className='mr-2 h-4 w-4' />
Hapus
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -538,19 +493,17 @@ export function MasterEmployeeContent() {
Kandang <span className='text-red-500'>*</span> Kandang <span className='text-red-500'>*</span>
</Label> </Label>
<MultiSelect <MultiSelect
options={kandangList.map((k) => ({ options={kandangOptions.map((k) => ({
value: k.id, value: String(k.value),
label: k.name, label: k.label,
}))} }))}
selected={employeeForm.kandang_ids} selected={employeeForm.kandang_ids.map((id) => String(id))}
onChange={(selected) => onChange={(selected) =>
setEmployeeForm({ ...employeeForm, kandang_ids: selected }) setEmployeeForm({
...employeeForm,
kandang_ids: selected.map((id) => Number(id)),
})
} }
// onSearchChange={(val) =>
// console.log({
// test: val,
// })
// }
placeholder='Pilih kandang' placeholder='Pilih kandang'
className='mt-1.5' className='mt-1.5'
/> />