mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat: add figma make components
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user