feat: create MasterConfigurationContent component

This commit is contained in:
ValdiANS
2026-01-12 14:42:14 +07:00
parent c099314bdb
commit dfac7f84ff
@@ -0,0 +1,564 @@
'use client';
import { useState } from 'react';
import { Plus, MoreVertical, Pencil, Trash2 } 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 {
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 Table from '@/components/Table';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { cn, formatDate } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ColumnDef } from '@tanstack/react-table';
import { DailyChecklistConfiguration } from '@/types/api/daily-checklist/configuration';
import { DailyChecklistConfigurationApi } from '@/services/api/daily-checklist/configuration';
import { DatePicker } from '@/figma-make/components/base/date-picker';
export function MasterConfigurationContent() {
const {
state: tableFilterState,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
},
});
const {
data: dailyChecklistConfigurations,
isLoading: isLoadingDailyChecklistConfigurations,
mutate: refreshDailyChecklistConfigurations,
} = useSWR(
`${DailyChecklistConfigurationApi.basePath}${getTableFilterQueryString()}`,
DailyChecklistConfigurationApi.getAllFetcher,
{
keepPreviousData: true,
}
);
const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [configurationToDelete, setConfigurationToDelete] = useState<
number | null
>(null);
const [loading, setLoading] = useState(false);
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
const [configurationForm, setConfigurationForm] = useState({
id: 0,
date: '',
percentage_threshold_bad: '',
percentage_threshold_enough: '',
});
const configurationColumns: ColumnDef<DailyChecklistConfiguration>[] = [
{
id: 'date',
header: 'Tanggal',
accessorKey: 'date',
enableSorting: false,
cell: ({ row }) => formatDate(row.original.date, 'DD MMM YYYY'),
},
{
id: 'percentage_threshold_bad',
header: 'Threshold Bad',
accessorKey: 'percentage_threshold_bad',
enableSorting: false,
cell: ({ row }) => `${row.original.percentage_threshold_bad}%`,
},
{
id: 'percentage_threshold_enough',
header: 'Threshold Enough',
accessorKey: 'percentage_threshold_enough',
enableSorting: false,
cell: ({ row }) => `${row.original.percentage_threshold_enough}%`,
},
{
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');
setConfigurationForm({
id: 0,
date: '',
percentage_threshold_bad: '',
percentage_threshold_enough: '',
});
setShowModal(true);
};
const handleEdit = (configuration: DailyChecklistConfiguration) => {
setModalMode('edit');
setConfigurationForm({
id: configuration.id,
date: configuration.date,
percentage_threshold_bad: String(configuration.percentage_threshold_bad),
percentage_threshold_enough: String(
configuration.percentage_threshold_enough
),
});
setShowModal(true);
};
const handleSave = async () => {
if (
!configurationForm.date.trim() ||
Number(configurationForm.percentage_threshold_bad) === 0 ||
Number(configurationForm.percentage_threshold_enough) === 0
) {
toast.error('Tanggal dan persentase harus diisi');
return;
}
setLoading(true);
try {
if (modalMode === 'create') {
const createConfigurationResponse =
await DailyChecklistConfigurationApi.create({
date: formatDate(configurationForm.date, 'YYYY-MM-DD'),
percentage_threshold_bad: Number(
configurationForm.percentage_threshold_bad
),
percentage_threshold_enough: Number(
configurationForm.percentage_threshold_enough
),
});
if (isResponseError(createConfigurationResponse)) {
console.error(
'Error creating configuration:',
createConfigurationResponse.message
);
toast.error('Gagal menambahkan konfigurasi');
return;
}
refreshDailyChecklistConfigurations();
toast.success('Konfigurasi berhasil ditambahkan');
} else {
const updateConfigurationResponse =
await DailyChecklistConfigurationApi.update(configurationForm.id, {
date: formatDate(configurationForm.date, 'YYYY-MM-DD'),
percentage_threshold_bad: Number(
configurationForm.percentage_threshold_bad
),
percentage_threshold_enough: Number(
configurationForm.percentage_threshold_enough
),
});
if (isResponseError(updateConfigurationResponse)) {
console.error(
'Error updating configuration:',
updateConfigurationResponse.message
);
toast.error('Gagal mengubah konfigurasi');
return;
}
refreshDailyChecklistConfigurations();
toast.success('Konfigurasi berhasil diubah');
}
setShowModal(false);
setConfigurationForm({
id: 0,
date: '',
percentage_threshold_bad: '',
percentage_threshold_enough: '',
});
} catch (error) {
console.error('Error saving configuration:', error);
toast.error('Terjadi kesalahan saat menyimpan konfigurasi');
} finally {
setLoading(false);
}
};
const handleDeleteClick = (configurationId: number) => {
setConfigurationToDelete(configurationId);
setShowDeleteConfirm(true);
};
const handleConfirmDelete = async () => {
if (!configurationToDelete) return;
setLoading(true);
try {
const deleteConfigurationResponse =
await DailyChecklistConfigurationApi.delete(configurationToDelete);
if (isResponseError(deleteConfigurationResponse)) {
console.error(
'Error deleting configuration:',
deleteConfigurationResponse.message
);
toast.error('Gagal menghapus konfigurasi');
return;
}
refreshDailyChecklistConfigurations();
toast.success('Konfigurasi berhasil dihapus');
setShowDeleteConfirm(false);
setConfigurationToDelete(null);
} catch (error) {
console.error('Error deleting employee:', error);
toast.error('Terjadi kesalahan saat menghapus konfigurasi');
} finally {
setLoading(false);
}
};
const handleExport = (format: string) => {
toast.success(`Data berhasil diekspor ke ${format}`);
};
if (isLoadingDailyChecklistConfigurations && !dailyChecklistConfigurations) {
return (
<div className='min-h-screen'>
<div className='p-6'>
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Master Konfigurasi
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data <span className='text-[#0069e0]'>Konfigurasi</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>
);
}
const formatDateForDisplay = (dateStr: string) => {
if (!dateStr) return 'Pilih tanggal';
const [year, month, day] = dateStr.split('-');
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
return date.toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
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 Konfigurasi
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data <span className='text-[#0069e0]'>Konfigurasi</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'>
<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 Konfigurasi
</Button>
</div>
</div>
{/* Table */}
<Table<DailyChecklistConfiguration>
data={
isResponseSuccess(dailyChecklistConfigurations)
? dailyChecklistConfigurations?.data
: []
}
columns={configurationColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(dailyChecklistConfigurations)
? dailyChecklistConfigurations?.meta?.page
: 0
}
totalItems={
isResponseSuccess(dailyChecklistConfigurations)
? dailyChecklistConfigurations?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingDailyChecklistConfigurations}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(dailyChecklistConfigurations) &&
dailyChecklistConfigurations?.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 Konfigurasi'
: 'Edit Konfigurasi'}
</DialogTitle>
<DialogDescription>
{modalMode === 'create'
? 'Masukkan detail konfigurasi baru'
: 'Ubah detail konfigurasi'}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
<div>
<Label htmlFor='date'>
Tanggal Efektif <span className='text-red-500'>*</span>
</Label>
<div className='mt-1.5'>
<DatePicker
date={configurationForm.date}
onDateChange={(e) =>
setConfigurationForm({
...configurationForm,
date: e,
})
}
disabled={loading}
placeholder='Pilih tanggal'
formatDisplay={formatDateForDisplay}
/>
</div>
</div>
<div>
<Label>
Threshold <span className='text-red-500'>*</span>
</Label>
</div>
<div className='flex flex-row items-center justify-between gap-2 max-w-64'>
<Label htmlFor='thresholdBad'>
Kurang <span className='text-red-500'>*</span>
</Label>
<div className='flex flex-row items-center gap-1'>
<Input
id='thresholdBadGround'
value={0}
disabled
className='w-16'
/>
<span>{'<='}</span>
<Input
type='number'
id='percentageThresholdBad'
value={configurationForm.percentage_threshold_bad}
onChange={(e) =>
setConfigurationForm({
...configurationForm,
percentage_threshold_bad: e.target.value,
})
}
placeholder='Kurang'
className='w-20'
disabled={loading}
max={100}
/>
</div>
</div>
<div className='flex flex-row items-center justify-between gap-2 max-w-64'>
<Label htmlFor='thresholdEnough'>
Cukup <span className='text-red-500'>*</span>
</Label>
<div className='flex flex-row items-center gap-1'>
<Input
id='thresholdEnoughGround'
value={Number(configurationForm.percentage_threshold_bad) + 1}
disabled
className='w-16'
/>
<span>{'<='}</span>
<Input
type='number'
id='percentageThresholdEnough'
value={configurationForm.percentage_threshold_enough}
onChange={(e) =>
setConfigurationForm({
...configurationForm,
percentage_threshold_enough: e.target.value,
})
}
placeholder='Cukup'
className='w-20'
disabled={loading}
min={Number(configurationForm.percentage_threshold_bad) + 1}
max={100}
/>
</div>
</div>
<div className='flex flex-row items-center justify-between gap-2 max-w-64'>
<Label htmlFor='thresholdGood'>
Baik <span className='text-red-500'>*</span>
</Label>
<div className='flex flex-row items-center gap-1'>
<Input
id='thresholdGoodGround'
value={
Number(configurationForm.percentage_threshold_enough) + 1
}
disabled
className='w-16'
/>
<span>{'<='}</span>
<Input
type='number'
id='percentageThresholdGood'
value={100}
placeholder='Good'
className='w-20'
disabled
/>
</div>
</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 konfigurasi?</AlertDialogTitle>
<AlertDialogDescription>
Data konfigurasi 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>
);
}