feat: add figma make components

This commit is contained in:
ValdiANS
2026-01-07 10:59:12 +07:00
parent 88c6c863e7
commit 770f363c60
56 changed files with 11887 additions and 0 deletions
@@ -0,0 +1,633 @@
'use client';
import { useState, useEffect } from 'react';
import {
Plus,
Download,
ChevronDown,
MoreVertical,
Pencil,
Trash2,
Search,
} 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 { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
import useSWR from 'swr';
import { EmployeeApi } from '@/services/api/daily-checklist/employee';
interface Employee {
id: string;
name: string;
kandang_id: string;
is_active: boolean;
kandang?: {
id: string;
name: string;
};
}
interface Kandang {
id: string;
name: string;
}
export function MasterEmployeeContent() {
const { data: employeesTest, isLoading: isLoadingEmployees } = useSWR(
EmployeeApi.basePath,
EmployeeApi.getAllFetcher,
{
keepPreviousData: true,
}
);
const [employees, setEmployees] = useState<Employee[]>([]);
const [kandangList, setKandangList] = useState<Kandang[]>([]);
const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [employeeToDelete, setEmployeeToDelete] = useState<string | null>(null);
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 [employeeForm, setEmployeeForm] = useState({
id: '',
name: '',
kandang_ids: [] as string[],
status: 'Active' as 'Active' | 'Non Active',
});
const fetchEmployees = async () => {
if (!isSupabaseConfigured()) {
console.warn(
'Supabase not configured. Please add environment variables.'
);
setInitialLoading(false);
return;
}
try {
const { data, error } = await supabase
.from('employees')
.select(
`
id,
name,
kandang_id,
is_active,
kandang:kandang_id (
id,
name
)
`
)
.order('name', { ascending: true });
if (error) {
console.error('Error fetching employees:', error);
toast.error('Gagal memuat data ABK');
return;
}
setEmployees((data as unknown as Employee[]) || []);
} catch (error) {
console.error('Error fetching employees:', error);
toast.error('Gagal memuat data ABK');
} finally {
setInitialLoading(false);
}
};
const fetchKandang = async () => {
if (!isSupabaseConfigured()) {
return;
}
try {
const { data, error } = await supabase
.from('kandang')
.select('id, name')
.order('name', { ascending: true });
if (error) {
console.error('Error fetching kandang:', error);
return;
}
setKandangList(data || []);
} catch (error) {
console.error('Error fetching kandang:', error);
}
};
useEffect(() => {
fetchEmployees();
fetchKandang();
}, []);
const handleAdd = () => {
setModalMode('create');
setEmployeeForm({ id: '', name: '', kandang_ids: [], status: 'Active' });
setShowModal(true);
};
const handleEdit = (employee: Employee) => {
setModalMode('edit');
setEmployeeForm({
id: employee.id,
name: employee.name,
kandang_ids: employee.kandang_id ? [employee.kandang_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;
}
if (!isSupabaseConfigured()) {
toast.error(
'Supabase belum dikonfigurasi. Tambahkan environment variables.'
);
return;
}
setLoading(true);
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') {
const { error } = await supabase.from('employees').insert([
{
name: employeeForm.name.trim(),
kandang_id: kandangIdToSave,
is_active: employeeForm.status === 'Active',
},
]);
if (error) {
console.error('Error creating employee:', error);
toast.error('Gagal menambahkan ABK');
return;
}
toast.success('ABK berhasil ditambahkan');
} else {
const { error } = await supabase
.from('employees')
.update({
name: employeeForm.name.trim(),
kandang_id: kandangIdToSave,
is_active: employeeForm.status === 'Active',
})
.eq('id', employeeForm.id);
if (error) {
console.error('Error updating employee:', error);
toast.error('Gagal mengubah ABK');
return;
}
toast.success('ABK berhasil diubah');
}
setShowModal(false);
setEmployeeForm({ id: '', name: '', kandang_ids: [], status: 'Active' });
await fetchEmployees();
} catch (error) {
console.error('Error saving employee:', error);
toast.error('Terjadi kesalahan saat menyimpan ABK');
} finally {
setLoading(false);
}
};
const handleDeleteClick = (employeeId: string) => {
setEmployeeToDelete(employeeId);
setShowDeleteConfirm(true);
};
const handleConfirmDelete = async () => {
if (!employeeToDelete) return;
setLoading(true);
try {
const { error } = await supabase
.from('employees')
.delete()
.eq('id', employeeToDelete);
if (error) {
console.error('Error deleting employee:', error);
toast.error('Gagal menghapus ABK');
return;
}
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);
}
};
const handleExport = (format: string) => {
toast.success(`Data berhasil diekspor ke ${format}`);
};
const filteredEmployees = employees.filter((emp) => {
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 (
<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 items-center justify-between gap-4 p-6 border-b border-gray-200/60'>
{/* LEFT: Search + Filters */}
<div className='flex items-center gap-3'>
<div className='relative'>
<Search className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4' />
<Input
type='text'
placeholder='Cari nama ABK atau kandang...'
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className='pl-10 w-[280px] border-gray-200'
/>
</div>
<Select value={kandangFilter} onValueChange={setKandangFilter}>
<SelectTrigger className='w-[180px] border-gray-200'>
<SelectValue placeholder='Semua Kandang' />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>Semua Kandang</SelectItem>
{kandangList.map((kandang) => (
<SelectItem key={kandang.id} value={kandang.id}>
{kandang.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className='w-[160px] border-gray-200'>
<SelectValue placeholder='Semua Status' />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>Semua Status</SelectItem>
<SelectItem value='active'>Active</SelectItem>
<SelectItem value='non_active'>Non Active</SelectItem>
</SelectContent>
</Select>
</div>
{/* RIGHT: Export + Add */}
<div className='flex items-center gap-2'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='outline'
className='border-gray-200 text-gray-700'
>
<Download className='w-4 h-4 mr-2' />
Export
<ChevronDown className='w-4 h-4 ml-2' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => handleExport('CSV')}>
Export CSV
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport('Excel')}>
Export Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<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 */}
<div className='overflow-x-auto'>
<table className='w-full'>
<thead>
<tr className='border-b border-gray-200/60 bg-gray-50/50'>
<th className='text-left py-3.5 px-6 text-sm font-semibold text-gray-700'>
Nama ABK
</th>
<th className='text-left py-3.5 px-6 text-sm font-semibold text-gray-700'>
Kandang
</th>
<th className='text-left py-3.5 px-6 text-sm font-semibold text-gray-700'>
Status
</th>
<th className='text-center py-3.5 px-6 text-sm font-semibold text-gray-700 w-[80px]'>
Aksi
</th>
</tr>
</thead>
<tbody className='divide-y divide-gray-200/60'>
{filteredEmployees.length === 0 ? (
<tr>
<td
colSpan={4}
className='text-center py-12 text-gray-500'
>
{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>
</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={kandangList.map((k) => ({
value: k.id,
label: k.name,
}))}
selected={employeeForm.kandang_ids}
onChange={(selected) =>
setEmployeeForm({ ...employeeForm, kandang_ids: selected })
}
// onSearchChange={(val) =>
// console.log({
// test: val,
// })
// }
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>
);
}