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