mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-23 23:05:46 +00:00
811 lines
26 KiB
TypeScript
811 lines
26 KiB
TypeScript
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import {
|
||
Eye,
|
||
CheckCircle,
|
||
XCircle,
|
||
Search,
|
||
Trash2,
|
||
Edit,
|
||
Loader2,
|
||
} from 'lucide-react';
|
||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||
import { Button } from '@/figma-make/components/base/button';
|
||
import { Badge } from '@/figma-make/components/base/badge';
|
||
import { Label } from '@/figma-make/components/base/label';
|
||
import { Textarea } from '@/figma-make/components/base/textarea';
|
||
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
|
||
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 { toast } from 'sonner';
|
||
import { useRouter } from 'next/navigation';
|
||
import useSWR from 'swr';
|
||
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||
import Table from '@/components/Table';
|
||
import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
|
||
import { cn } from '@/lib/helper';
|
||
import { ColumnDef } from '@tanstack/react-table';
|
||
import { useSelect } from '@/components/input/SelectInput';
|
||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||
import RequirePermission from '@/components/helper/RequirePermission';
|
||
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||
|
||
const STATUS_OPTIONS = [
|
||
{ value: 'ALL', label: 'Semua Status' },
|
||
{ value: 'DRAFT', label: 'Draft' },
|
||
{ value: 'SUBMITTED', label: 'Submitted' },
|
||
{ value: 'APPROVED', label: 'Approved' },
|
||
{ value: 'REJECTED', label: 'Rejected' },
|
||
];
|
||
|
||
const CATEGORY_LABELS: { [key: string]: string } = {
|
||
pullet_open: 'Pullet Open',
|
||
pullet_close: 'Pullet Close',
|
||
produksi_open: 'Produksi Open',
|
||
produksi_close: 'Produksi Close',
|
||
};
|
||
|
||
export function ListDailyChecklistContent() {
|
||
const router = useRouter();
|
||
|
||
const {
|
||
state: tableFilterState,
|
||
updateFilter,
|
||
setPage,
|
||
setPageSize,
|
||
toQueryString: getTableFilterQueryString,
|
||
} = useTableFilter({
|
||
initial: {
|
||
date_from: '',
|
||
date_to: '',
|
||
search: '',
|
||
kandang_id: '',
|
||
status: '',
|
||
},
|
||
paramMap: {
|
||
page: 'page',
|
||
pageSize: 'limit',
|
||
search: 'search',
|
||
kandang_id: 'kandang_id',
|
||
status: 'status',
|
||
date_from: 'date_from',
|
||
date_to: 'date_to',
|
||
},
|
||
});
|
||
|
||
const {
|
||
data: checklistListRes,
|
||
isLoading: isLoadingChecklistList,
|
||
mutate: refreshChecklistList,
|
||
} = useSWR(
|
||
`${DailyChecklistApi.basePath}${getTableFilterQueryString()}`,
|
||
DailyChecklistApi.getAllFetcher,
|
||
{
|
||
keepPreviousData: true,
|
||
}
|
||
);
|
||
|
||
const {
|
||
options: kandangOptions,
|
||
isLoadingMore: isLoadingMoreKandang,
|
||
loadMore: loadMoreKandang,
|
||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||
|
||
const checklistList = isResponseSuccess(checklistListRes)
|
||
? checklistListRes.data || []
|
||
: [];
|
||
|
||
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||
const target = e.target as HTMLDivElement;
|
||
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||
if (!isLoadingMoreKandang) {
|
||
loadMoreKandang();
|
||
}
|
||
}
|
||
};
|
||
|
||
// Modals
|
||
const [showApproveModal, setShowApproveModal] = useState(false);
|
||
const [showRejectModal, setShowRejectModal] = useState(false);
|
||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||
const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
|
||
const [rejectReason, setRejectReason] = useState('');
|
||
const [actionLoading, setActionLoading] = useState(false);
|
||
|
||
const handleDetail = (item: DailyChecklist) => {
|
||
router.push(
|
||
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
|
||
);
|
||
};
|
||
|
||
const handleEdit = (item: DailyChecklist) => {
|
||
const formattedDate = new Date(item.date).toISOString().split('T')[0];
|
||
const kandangId = item.kandang.id;
|
||
const category = item.category;
|
||
|
||
router.push(
|
||
`/daily-checklist/daily-checklist?date=${formattedDate}&kandang_id=${kandangId}&category=${category}`
|
||
);
|
||
};
|
||
|
||
const handleApprove = (item: DailyChecklist) => {
|
||
setSelectedItem(item);
|
||
setShowApproveModal(true);
|
||
};
|
||
|
||
const handleReject = (item: DailyChecklist) => {
|
||
setSelectedItem(item);
|
||
setRejectReason('');
|
||
setShowRejectModal(true);
|
||
};
|
||
|
||
const handleDelete = (item: DailyChecklist) => {
|
||
// ✅ VALIDATION: Only DRAFT can be deleted
|
||
if (item.status !== 'DRAFT') {
|
||
toast.error('Hanya checklist dengan status DRAFT yang bisa dihapus', {
|
||
description: `Status saat ini: ${item.status}`,
|
||
});
|
||
return;
|
||
}
|
||
|
||
setSelectedItem(item);
|
||
setShowDeleteModal(true);
|
||
};
|
||
|
||
const confirmApprove = async () => {
|
||
if (!selectedItem) return;
|
||
|
||
try {
|
||
setActionLoading(true);
|
||
|
||
const approveRes = await DailyChecklistApi.approve(
|
||
String(selectedItem.id)
|
||
);
|
||
|
||
if (isResponseError(approveRes)) {
|
||
toast.error('Gagal approve checklist: ' + approveRes.message);
|
||
return;
|
||
}
|
||
|
||
refreshChecklistList();
|
||
toast.success('Checklist berhasil di-approve');
|
||
setShowApproveModal(false);
|
||
setSelectedItem(null);
|
||
} catch (error) {
|
||
console.error('Error approving checklist:', error);
|
||
toast.error('Terjadi kesalahan');
|
||
} finally {
|
||
setActionLoading(false);
|
||
}
|
||
};
|
||
|
||
const confirmReject = async () => {
|
||
if (!selectedItem) return;
|
||
|
||
if (!rejectReason.trim()) {
|
||
toast.error('Alasan reject harus diisi');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setActionLoading(true);
|
||
|
||
const rejectRes = await DailyChecklistApi.reject(
|
||
String(selectedItem.id),
|
||
rejectReason
|
||
);
|
||
|
||
if (isResponseError(rejectRes)) {
|
||
toast.error('Gagal reject checklist: ' + rejectRes.message);
|
||
return;
|
||
}
|
||
|
||
refreshChecklistList();
|
||
toast.success('Checklist berhasil di-reject');
|
||
setShowRejectModal(false);
|
||
setSelectedItem(null);
|
||
setRejectReason('');
|
||
} catch (error) {
|
||
console.error('Error rejecting checklist:', error);
|
||
toast.error('Terjadi kesalahan');
|
||
} finally {
|
||
setActionLoading(false);
|
||
}
|
||
};
|
||
|
||
const confirmDelete = async () => {
|
||
if (!selectedItem) return;
|
||
|
||
try {
|
||
setActionLoading(true);
|
||
|
||
const deleteRes = await DailyChecklistApi.delete(selectedItem.id);
|
||
|
||
if (isResponseError(deleteRes)) {
|
||
toast.error('Gagal hapus checklist: ' + deleteRes.message);
|
||
return;
|
||
}
|
||
|
||
refreshChecklistList();
|
||
toast.success('Checklist berhasil dihapus');
|
||
setShowDeleteModal(false);
|
||
setSelectedItem(null);
|
||
} catch (error) {
|
||
console.error('Error deleting checklist:', error);
|
||
toast.error('Terjadi kesalahan');
|
||
} finally {
|
||
setActionLoading(false);
|
||
}
|
||
};
|
||
|
||
const getStatusBadge = (status: string) => {
|
||
switch (status) {
|
||
case 'DRAFT':
|
||
return (
|
||
<Badge
|
||
variant='outline'
|
||
className='border-gray-300 text-gray-700 bg-white'
|
||
>
|
||
Draft
|
||
</Badge>
|
||
);
|
||
case 'SUBMITTED':
|
||
return (
|
||
<Badge
|
||
variant='outline'
|
||
className='border-orange-300 text-orange-700 bg-white'
|
||
>
|
||
Submitted
|
||
</Badge>
|
||
);
|
||
case 'APPROVED':
|
||
return (
|
||
<Badge
|
||
variant='outline'
|
||
className='border-green-300 text-green-700 bg-white'
|
||
>
|
||
Approved
|
||
</Badge>
|
||
);
|
||
case 'REJECTED':
|
||
return (
|
||
<Badge
|
||
variant='outline'
|
||
className='border-red-300 text-red-700 bg-white'
|
||
>
|
||
Rejected
|
||
</Badge>
|
||
);
|
||
default:
|
||
return (
|
||
<Badge
|
||
variant='outline'
|
||
className='border-gray-300 text-gray-700 bg-white'
|
||
>
|
||
{status}
|
||
</Badge>
|
||
);
|
||
}
|
||
};
|
||
|
||
const formatDate = (dateString: string) => {
|
||
const date = new Date(dateString);
|
||
return date.toLocaleDateString('id-ID', {
|
||
day: '2-digit',
|
||
month: 'short',
|
||
year: 'numeric',
|
||
});
|
||
};
|
||
|
||
const formatDateTime = (dateString: string) => {
|
||
const date = new Date(dateString);
|
||
return date.toLocaleString('id-ID', {
|
||
day: '2-digit',
|
||
month: 'short',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
});
|
||
};
|
||
|
||
const checklistListColumns: ColumnDef<DailyChecklist>[] = [
|
||
{
|
||
accessorKey: 'date',
|
||
header: 'Tanggal',
|
||
enableSorting: false,
|
||
cell: ({ row }) => formatDate(row.original.date),
|
||
},
|
||
{
|
||
accessorKey: 'kandang',
|
||
header: 'Kandang',
|
||
enableSorting: false,
|
||
cell: ({ row }) => row.original.kandang.name,
|
||
},
|
||
{
|
||
accessorKey: 'category',
|
||
header: 'Kategori',
|
||
enableSorting: false,
|
||
cell: ({ row }) =>
|
||
CATEGORY_LABELS[row.original.category] || row.original.category,
|
||
},
|
||
{
|
||
accessorKey: 'status',
|
||
header: 'Status',
|
||
enableSorting: false,
|
||
cell: ({ row }) => getStatusBadge(row.original.status),
|
||
},
|
||
{
|
||
accessorKey: 'total_phase',
|
||
header: 'Total Phase',
|
||
enableSorting: false,
|
||
},
|
||
{
|
||
accessorKey: 'total_activity',
|
||
header: 'Total Aktivitas',
|
||
enableSorting: false,
|
||
},
|
||
{
|
||
accessorKey: 'progress',
|
||
header: 'Progress',
|
||
enableSorting: false,
|
||
cell: ({ row }) => (
|
||
<div className='flex items-center justify-center gap-2'>
|
||
<div className='w-24 bg-gray-200 rounded-full h-2'>
|
||
<div
|
||
className='bg-[#0069e0] h-2 rounded-full transition-all'
|
||
style={{ width: `${row.original.progress}%` }}
|
||
/>
|
||
</div>
|
||
<span className='text-sm text-gray-700 font-medium'>
|
||
{row.original.progress}%
|
||
</span>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
accessorKey: 'updated_at',
|
||
header: 'Diperbarui',
|
||
enableSorting: false,
|
||
cell: ({ row }) => formatDateTime(row.original.updated_at),
|
||
},
|
||
{
|
||
id: 'action',
|
||
header: 'Aksi',
|
||
accessorKey: 'action',
|
||
enableSorting: false,
|
||
cell: ({ row }) => (
|
||
<div className='flex items-center justify-center gap-2'>
|
||
<Button
|
||
size='sm'
|
||
variant='outline'
|
||
onClick={() => handleDetail(row.original)}
|
||
className='border-gray-200 text-gray-700 hover:bg-gray-50'
|
||
>
|
||
<Eye className='w-4 h-4 mr-1' />
|
||
Detail
|
||
</Button>
|
||
|
||
{row.original.status === 'DRAFT' && (
|
||
<RequirePermission permissions='lti.daily_checklist.create'>
|
||
<Button
|
||
size='sm'
|
||
variant='outline'
|
||
onClick={() => handleEdit(row.original)}
|
||
className='border-gray-200 text-gray-700 hover:bg-gray-50'
|
||
>
|
||
<Edit className='w-4 h-4 mr-1' />
|
||
Edit
|
||
</Button>
|
||
</RequirePermission>
|
||
)}
|
||
|
||
{row.original.status === 'SUBMITTED' && (
|
||
<RequirePermission permissions='lti.daily_checklist.create'>
|
||
<Button
|
||
size='sm'
|
||
onClick={() => handleApprove(row.original)}
|
||
className='bg-green-600 hover:bg-green-700 text-white'
|
||
>
|
||
<CheckCircle className='w-4 h-4 mr-1' />
|
||
Approve
|
||
</Button>
|
||
<Button
|
||
size='sm'
|
||
variant='destructive'
|
||
onClick={() => handleReject(row.original)}
|
||
className='bg-red-600 hover:bg-red-700 text-white'
|
||
>
|
||
<XCircle className='w-4 h-4 mr-1' />
|
||
Reject
|
||
</Button>
|
||
</RequirePermission>
|
||
)}
|
||
|
||
{row.original.status === 'DRAFT' && (
|
||
<RequirePermission permissions='lti.daily_checklist.create'>
|
||
<Button
|
||
size='sm'
|
||
variant='destructive'
|
||
onClick={() => handleDelete(row.original)}
|
||
className='bg-red-600 hover:bg-red-700 text-white'
|
||
>
|
||
<Trash2 className='w-4 h-4 mr-1' />
|
||
Hapus
|
||
</Button>
|
||
</RequirePermission>
|
||
)}
|
||
</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'>
|
||
List Daily Checklist
|
||
</h1>
|
||
<p className='text-sm text-gray-600 mt-1'>
|
||
Daftar semua checklist harian
|
||
</p>
|
||
</div>
|
||
|
||
{/* Main Card */}
|
||
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
||
<CardContent className='p-6'>
|
||
{/* Filters Section */}
|
||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6 pb-6 border-b border-gray-200'>
|
||
<div>
|
||
<Label>Periode Tanggal</Label>
|
||
<div className='mt-1.5'>
|
||
<DateRangePicker
|
||
dateFrom={tableFilterState.date_from}
|
||
dateTo={tableFilterState.date_to}
|
||
onDateChange={(from, to) => {
|
||
updateFilter('date_from', from);
|
||
updateFilter('date_to', to);
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor='kandang-filter'>Kandang</Label>
|
||
<div className='mt-1.5'>
|
||
<Select
|
||
value={tableFilterState.kandang_id}
|
||
onValueChange={(value) => {
|
||
updateFilter('kandang_id', value === 'ALL' ? '' : value);
|
||
}}
|
||
>
|
||
<SelectTrigger
|
||
id='kandang-filter'
|
||
className='border-gray-200'
|
||
>
|
||
<SelectValue placeholder='Semua Kandang' />
|
||
</SelectTrigger>
|
||
<SelectContent onScroll={handleKandangScroll}>
|
||
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
||
{kandangOptions.map((kandang) => (
|
||
<SelectItem
|
||
key={kandang.value}
|
||
value={String(kandang.value)}
|
||
>
|
||
{kandang.label}
|
||
</SelectItem>
|
||
))}
|
||
{isLoadingMoreKandang && (
|
||
<div className='flex justify-center p-2'>
|
||
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||
</div>
|
||
)}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor='status-filter'>Status</Label>
|
||
<div className='mt-1.5'>
|
||
<Select
|
||
value={tableFilterState.status}
|
||
onValueChange={(value) => {
|
||
updateFilter('status', value === 'ALL' ? '' : value);
|
||
}}
|
||
>
|
||
<SelectTrigger
|
||
id='status-filter'
|
||
className='border-gray-200'
|
||
>
|
||
<SelectValue placeholder='Semua Status' />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{STATUS_OPTIONS.map((option) => (
|
||
<SelectItem key={option.value} value={option.value}>
|
||
{option.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor='search-text'>Cari</Label>
|
||
<div className='relative mt-1.5'>
|
||
<DebouncedTextInput
|
||
name='search'
|
||
placeholder='Kandang / Kategori'
|
||
value={tableFilterState.search}
|
||
onChange={(e) => updateFilter('search', e.target.value)}
|
||
className={{
|
||
wrapper: 'w-full 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>
|
||
</div>
|
||
|
||
{/* Table Section */}
|
||
<Table<DailyChecklist>
|
||
data={checklistList}
|
||
columns={checklistListColumns}
|
||
pageSize={tableFilterState.pageSize}
|
||
onPageSizeChange={setPageSize}
|
||
rowOptions={[10, 20, 50, 100]}
|
||
page={
|
||
isResponseSuccess(checklistListRes)
|
||
? checklistListRes?.meta?.page
|
||
: 0
|
||
}
|
||
totalItems={
|
||
isResponseSuccess(checklistListRes)
|
||
? checklistListRes?.meta?.total_results
|
||
: 0
|
||
}
|
||
onPageChange={setPage}
|
||
isLoading={isLoadingChecklistList}
|
||
className={{
|
||
containerClassName: cn({
|
||
'w-full mb-20':
|
||
isResponseSuccess(checklistListRes) &&
|
||
checklistListRes?.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>
|
||
|
||
{/* Approve Modal */}
|
||
<Dialog open={showApproveModal} onOpenChange={setShowApproveModal}>
|
||
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||
<DialogHeader>
|
||
<DialogTitle>Approve Checklist</DialogTitle>
|
||
<DialogDescription>
|
||
Apakah Anda yakin ingin approve checklist ini?
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
{selectedItem && (
|
||
<div className='bg-gray-50 rounded-lg p-4 space-y-2'>
|
||
<div className='flex justify-between text-sm'>
|
||
<span className='text-gray-600'>Tanggal:</span>
|
||
<span className='font-medium text-gray-900'>
|
||
{formatDate(selectedItem.date)}
|
||
</span>
|
||
</div>
|
||
<div className='flex justify-between text-sm'>
|
||
<span className='text-gray-600'>Kandang:</span>
|
||
<span className='font-medium text-gray-900'>
|
||
{selectedItem.kandang.name}
|
||
</span>
|
||
</div>
|
||
<div className='flex justify-between text-sm'>
|
||
<span className='text-gray-600'>Kategori:</span>
|
||
<span className='font-medium text-gray-900'>
|
||
{CATEGORY_LABELS[selectedItem.category] ||
|
||
selectedItem.category}
|
||
</span>
|
||
</div>
|
||
<div className='flex justify-between text-sm'>
|
||
<span className='text-gray-600'>Progress:</span>
|
||
<span className='font-medium text-gray-900'>
|
||
{selectedItem.progress}%
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<DialogFooter className='flex gap-2'>
|
||
<Button
|
||
variant='outline'
|
||
onClick={() => setShowApproveModal(false)}
|
||
disabled={actionLoading}
|
||
className='border-gray-200'
|
||
>
|
||
Batal
|
||
</Button>
|
||
<Button
|
||
onClick={confirmApprove}
|
||
disabled={actionLoading}
|
||
className='bg-green-600 hover:bg-green-700 text-white'
|
||
>
|
||
{actionLoading ? 'Memproses...' : 'Ya, Approve'}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Reject Modal */}
|
||
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
|
||
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||
<DialogHeader>
|
||
<DialogTitle>Reject Checklist</DialogTitle>
|
||
<DialogDescription>
|
||
Berikan alasan reject untuk checklist ini
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
{selectedItem && (
|
||
<div className='bg-gray-50 rounded-lg p-4 space-y-2 mb-4'>
|
||
<div className='flex justify-between text-sm'>
|
||
<span className='text-gray-600'>Tanggal:</span>
|
||
<span className='font-medium text-gray-900'>
|
||
{formatDate(selectedItem.date)}
|
||
</span>
|
||
</div>
|
||
<div className='flex justify-between text-sm'>
|
||
<span className='text-gray-600'>Kandang:</span>
|
||
<span className='font-medium text-gray-900'>
|
||
{selectedItem.kandang.name}
|
||
</span>
|
||
</div>
|
||
<div className='flex justify-between text-sm'>
|
||
<span className='text-gray-600'>Kategori:</span>
|
||
<span className='font-medium text-gray-900'>
|
||
{CATEGORY_LABELS[selectedItem.category] ||
|
||
selectedItem.category}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<Label htmlFor='reject-reason'>
|
||
Alasan Reject <span className='text-red-500'>*</span>
|
||
</Label>
|
||
<Textarea
|
||
id='reject-reason'
|
||
value={rejectReason}
|
||
onChange={(e) => setRejectReason(e.target.value)}
|
||
placeholder='Tuliskan alasan reject...'
|
||
className='mt-1.5 border-gray-200 min-h-[100px]'
|
||
disabled={actionLoading}
|
||
/>
|
||
</div>
|
||
|
||
<DialogFooter className='flex gap-2'>
|
||
<Button
|
||
variant='outline'
|
||
onClick={() => setShowRejectModal(false)}
|
||
disabled={actionLoading}
|
||
className='border-gray-200'
|
||
>
|
||
Batal
|
||
</Button>
|
||
<Button
|
||
onClick={confirmReject}
|
||
disabled={actionLoading}
|
||
variant='destructive'
|
||
className='bg-red-600 hover:bg-red-700 text-white'
|
||
>
|
||
{actionLoading ? 'Memproses...' : 'Ya, Reject'}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Delete Modal */}
|
||
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
||
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||
<DialogHeader>
|
||
<DialogTitle className='text-red-600'>
|
||
⚠️ Hapus Checklist
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
Apakah Anda yakin ingin menghapus checklist ini? Data yang dihapus
|
||
tidak dapat dikembalikan.
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
{selectedItem && (
|
||
<>
|
||
<div className='bg-red-50 border border-red-200 rounded-lg p-4 space-y-2 mb-2'>
|
||
<div className='flex justify-between text-sm'>
|
||
<span className='text-gray-600'>Tanggal:</span>
|
||
<span className='font-medium text-gray-900'>
|
||
{formatDate(selectedItem.date)}
|
||
</span>
|
||
</div>
|
||
<div className='flex justify-between text-sm'>
|
||
<span className='text-gray-600'>Kandang:</span>
|
||
<span className='font-medium text-gray-900'>
|
||
{selectedItem.kandang.name}
|
||
</span>
|
||
</div>
|
||
<div className='flex justify-between text-sm'>
|
||
<span className='text-gray-600'>Kategori:</span>
|
||
<span className='font-medium text-gray-900'>
|
||
{CATEGORY_LABELS[selectedItem.category] ||
|
||
selectedItem.category}
|
||
</span>
|
||
</div>
|
||
<div className='flex justify-between text-sm'>
|
||
<span className='text-gray-600'>Status:</span>
|
||
{getStatusBadge(selectedItem.status)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className='bg-yellow-50 border border-yellow-200 rounded-lg p-3'>
|
||
<p className='text-xs text-yellow-800'>
|
||
<strong>Peringatan:</strong> Semua data terkait (phases,
|
||
activities, assignments) akan ikut terhapus secara permanen.
|
||
</p>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
<DialogFooter className='flex gap-2'>
|
||
<Button
|
||
variant='outline'
|
||
onClick={() => setShowDeleteModal(false)}
|
||
disabled={actionLoading}
|
||
className='border-gray-200'
|
||
>
|
||
Batal
|
||
</Button>
|
||
<Button
|
||
onClick={confirmDelete}
|
||
disabled={actionLoading}
|
||
variant='destructive'
|
||
className='bg-red-600 hover:bg-red-700 text-white'
|
||
>
|
||
{actionLoading ? 'Memproses...' : 'Ya, Hapus'}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
);
|
||
}
|