Merge branch 'fix/daily-checklist' into 'development'

[FIX/FE] Daily Checklist

See merge request mbugroup/lti-web-client!224
This commit is contained in:
Rivaldi A N S
2026-01-21 05:40:07 +00:00
4 changed files with 221 additions and 116 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
permission: ['lti.daily_checklist.dashboard.list'], permission: ['lti.daily_checklist.dashboard.list'],
}, },
{ {
text: 'Daily Checklist', text: 'Formulir',
link: '/daily-checklist/daily-checklist', link: '/daily-checklist/daily-checklist',
icon: 'lucide:clipboard-check', icon: 'lucide:clipboard-check',
permission: ['lti.daily_checklist.create'], permission: ['lti.daily_checklist.create'],
@@ -41,6 +41,7 @@ import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity';
import DebouncedTextArea from '@/components/input/DebouncedTextArea'; import DebouncedTextArea from '@/components/input/DebouncedTextArea';
import DropFileInput from '@/components/input/DropFileInput'; import DropFileInput from '@/components/input/DropFileInput';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
// Static categories // Static categories
@@ -51,7 +52,7 @@ const CATEGORIES = [
{ value: 'produksi_close', label: 'Produksi Close' }, { value: 'produksi_close', label: 'Produksi Close' },
]; ];
const TIME_TYPE_ORDER = ['umum', 'pagi', 'siang', 'sore', 'malam']; const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
const TIME_TYPE_LABELS: { [key: string]: string } = { const TIME_TYPE_LABELS: { [key: string]: string } = {
Umum: 'Umum', Umum: 'Umum',
Pagi: 'Pagi', Pagi: 'Pagi',
@@ -67,7 +68,23 @@ interface Phase {
} }
export function DailyChecklistContent() { export function DailyChecklistContent() {
const [kandangId, setKandangId] = useState(''); const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [kandangId, setKandangId] = useState(
searchParams.get('kandang_id') || ''
);
const [date, setDate] = useState(() => {
const paramDate = searchParams.get('date');
if (paramDate) return paramDate;
const today = new Date();
return today.toISOString().split('T')[0];
});
const [selectedCategory, setSelectedCategory] = useState(
searchParams.get('category') || ''
);
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
useSelect(KandangApi.basePath, 'id', 'name', 'search', { useSelect(KandangApi.basePath, 'id', 'name', 'search', {
@@ -104,12 +121,6 @@ export function DailyChecklistContent() {
? employeesRes.data || [] ? employeesRes.data || []
: []; : [];
const [date, setDate] = useState(() => {
const today = new Date();
return today.toISOString().split('T')[0];
});
const [selectedCategory, setSelectedCategory] = useState('');
const [selectedPhaseIds, setSelectedPhaseIds] = useState<string[]>([]); const [selectedPhaseIds, setSelectedPhaseIds] = useState<string[]>([]);
const [selectedEmployees, setSelectedEmployees] = useState< const [selectedEmployees, setSelectedEmployees] = useState<
@@ -118,7 +129,7 @@ export function DailyChecklistContent() {
const [dailyChecklistId, setDailyChecklistId] = useState<string | null>(null); const [dailyChecklistId, setDailyChecklistId] = useState<string | null>(null);
const [checklistStatus, setChecklistStatus] = useState<string>('DRAFT'); const [checklistStatus, setChecklistStatus] = useState<string>('DRAFT');
const [isEditMode, setIsEditMode] = useState(false); // const [isEditMode, setIsEditMode] = useState(false);
// Activities grouped by phase // Activities grouped by phase
const [activitiesByPhase, setActivitiesByPhase] = useState<{ const [activitiesByPhase, setActivitiesByPhase] = useState<{
@@ -148,13 +159,57 @@ export function DailyChecklistContent() {
const [searchAbk, setSearchAbk] = useState(''); const [searchAbk, setSearchAbk] = useState('');
const [searchPhase, setSearchPhase] = useState(''); const [searchPhase, setSearchPhase] = useState('');
const [loading, setLoading] = useState(false); const [isLoadingSubmit, setIsLoadingSubmit] = useState(false);
const [isLoadingDraft, setIsLoadingDraft] = useState(false);
const [initialLoading, setInitialLoading] = useState(true); const [initialLoading, setInitialLoading] = useState(true);
const [existingDocuments, setExistingDocuments] = useState<Document[]>([]); const [existingDocuments, setExistingDocuments] = useState<Document[]>([]);
const [documents, setDocuments] = useState<File[]>([]); const [documents, setDocuments] = useState<File[]>([]);
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]); const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
// Sync state to URL query params
useEffect(() => {
const params = new URLSearchParams(searchParams.toString());
let pendingUpdate = false;
// Sync date
if (date) {
if (params.get('date') !== date) {
params.set('date', date);
pendingUpdate = true;
}
} else if (params.has('date')) {
params.delete('date');
pendingUpdate = true;
}
// Sync kandang_id
if (kandangId) {
if (params.get('kandang_id') !== kandangId) {
params.set('kandang_id', kandangId);
pendingUpdate = true;
}
} else if (params.has('kandang_id')) {
params.delete('kandang_id');
pendingUpdate = true;
}
// Sync category
if (selectedCategory) {
if (params.get('category') !== selectedCategory) {
params.set('category', selectedCategory);
pendingUpdate = true;
}
} else if (params.has('category')) {
params.delete('category');
pendingUpdate = true;
}
if (pendingUpdate) {
router.replace(`${pathname}?${params.toString()}`);
}
}, [date, kandangId, selectedCategory, pathname, router, searchParams]);
// Format date for display // Format date for display
const formatDateForDisplay = (dateStr: string) => { const formatDateForDisplay = (dateStr: string) => {
if (!dateStr) return 'Pilih tanggal'; if (!dateStr) return 'Pilih tanggal';
@@ -179,7 +234,7 @@ export function DailyChecklistContent() {
if (!date || !kandangId || !selectedCategory) { if (!date || !kandangId || !selectedCategory) {
setDailyChecklistId(null); setDailyChecklistId(null);
setChecklistStatus('DRAFT'); setChecklistStatus('DRAFT');
setIsEditMode(false); // setIsEditMode(false);
setSelectedPhaseIds([]); setSelectedPhaseIds([]);
setActivitiesByPhase({}); setActivitiesByPhase({});
setTaskIdsByPhaseActivityId({}); setTaskIdsByPhaseActivityId({});
@@ -216,7 +271,7 @@ export function DailyChecklistContent() {
existingPhases.data.phases.length > 0 existingPhases.data.phases.length > 0
) { ) {
// Existing checklist - EDIT MODE // Existing checklist - EDIT MODE
setIsEditMode(true); // setIsEditMode(true);
const phaseIds = existingPhases.data.phases.map((p) => const phaseIds = existingPhases.data.phases.map((p) =>
String(p.phase_id) String(p.phase_id)
); );
@@ -234,7 +289,7 @@ export function DailyChecklistContent() {
} }
} else { } else {
// New checklist - CREATE MODE // New checklist - CREATE MODE
setIsEditMode(false); // setIsEditMode(false);
setSelectedPhaseIds([]); setSelectedPhaseIds([]);
} }
} catch (error) { } catch (error) {
@@ -608,7 +663,7 @@ export function DailyChecklistContent() {
// taskId, // taskId,
// hasTaskId: !!taskId, // hasTaskId: !!taskId,
// checklistStatus, // checklistStatus,
// isEditable, // isChecklistStatusDraft,
// }); // });
if (!taskId) { if (!taskId) {
@@ -618,7 +673,7 @@ export function DailyChecklistContent() {
return; return;
} }
if (!isEditable) { if (!isChecklistStatusDraft) {
console.warn( console.warn(
'[CHECKBOX] Checklist is not editable, status:', '[CHECKBOX] Checklist is not editable, status:',
checklistStatus checklistStatus
@@ -736,7 +791,7 @@ export function DailyChecklistContent() {
return; return;
} }
setLoading(true); setIsLoadingSubmit(true);
try { try {
const submitRes = await DailyChecklistApi.submit( const submitRes = await DailyChecklistApi.submit(
@@ -757,13 +812,15 @@ export function DailyChecklistContent() {
console.error('Error submitting:', error); console.error('Error submitting:', error);
toast.error('Terjadi kesalahan'); toast.error('Terjadi kesalahan');
} finally { } finally {
setLoading(false); setIsLoadingSubmit(false);
} }
}; };
const handleSaveDraft = async () => { const handleSaveDraft = async () => {
if (!dailyChecklistId) return; if (!dailyChecklistId) return;
setIsLoadingDraft(true);
const uploadImageRes = await DailyChecklistApi.uploadImage( const uploadImageRes = await DailyChecklistApi.uploadImage(
Number(dailyChecklistId), Number(dailyChecklistId),
'DRAFT', 'DRAFT',
@@ -774,10 +831,12 @@ export function DailyChecklistContent() {
if (isResponseError(uploadImageRes)) { if (isResponseError(uploadImageRes)) {
console.error('Error saving draft:', uploadImageRes.message); console.error('Error saving draft:', uploadImageRes.message);
toast.error('Gagal menyimpan draft'); toast.error('Gagal menyimpan draft');
setIsLoadingDraft(false);
return; return;
} }
toast.success('Draft tersimpan otomatis'); setIsLoadingDraft(false);
toast.success('Draft tersimpan!');
}; };
// Filter functions // Filter functions
@@ -825,7 +884,7 @@ export function DailyChecklistContent() {
// Group activities by time_type within this phase // Group activities by time_type within this phase
phaseActivities.forEach((activity) => { phaseActivities.forEach((activity) => {
const timeType = activity.time_type || 'umum'; const timeType = activity.time_type || 'Umum';
if (!grouped[phase.id].timeGroups[timeType]) { if (!grouped[phase.id].timeGroups[timeType]) {
grouped[phase.id].timeGroups[timeType] = []; grouped[phase.id].timeGroups[timeType] = [];
@@ -838,7 +897,7 @@ export function DailyChecklistContent() {
return grouped; return grouped;
}; };
const isEditable = checklistStatus === 'DRAFT'; const isChecklistStatusDraft = checklistStatus === 'DRAFT';
if (initialLoading) { if (initialLoading) {
return ( return (
@@ -871,7 +930,7 @@ export function DailyChecklistContent() {
<h1 className='text-2xl font-semibold text-gray-900'> <h1 className='text-2xl font-semibold text-gray-900'>
Daily Checklist Daily Checklist
</h1> </h1>
{isEditMode && ( {isChecklistStatusDraft && (
<Badge <Badge
variant='outline' variant='outline'
className='border-amber-300 text-amber-700 bg-white' className='border-amber-300 text-amber-700 bg-white'
@@ -907,7 +966,7 @@ export function DailyChecklistContent() {
<DatePicker <DatePicker
date={date} date={date}
onDateChange={setDate} onDateChange={setDate}
disabled={!isEditable} disabled={!isChecklistStatusDraft}
placeholder='Pilih tanggal' placeholder='Pilih tanggal'
formatDisplay={formatDateForDisplay} formatDisplay={formatDateForDisplay}
/> />
@@ -921,7 +980,7 @@ export function DailyChecklistContent() {
<Select <Select
value={kandangId} value={kandangId}
onValueChange={setKandangId} onValueChange={setKandangId}
disabled={!isEditable} disabled={!isChecklistStatusDraft}
> >
<SelectTrigger <SelectTrigger
id='kandang' id='kandang'
@@ -949,7 +1008,7 @@ export function DailyChecklistContent() {
<Select <Select
value={selectedCategory} value={selectedCategory}
onValueChange={setSelectedCategory} onValueChange={setSelectedCategory}
disabled={!isEditable} disabled={!isChecklistStatusDraft}
> >
<SelectTrigger <SelectTrigger
id='category' id='category'
@@ -971,19 +1030,21 @@ export function DailyChecklistContent() {
{/* Phase Selection Section */} {/* Phase Selection Section */}
{dailyChecklistId && ( {dailyChecklistId && (
<div className='mb-6 pb-6 border-b border-gray-200'> <div className='mb-6 pb-6 border-b border-gray-200'>
<div className='flex items-center justify-between mb-3'> {isChecklistStatusDraft && (
<Label>Fase / Tahap</Label> <div className='flex items-center justify-between mb-3'>
<Button <Label>Fase / Tahap</Label>
onClick={handleAddPhase} <Button
size='sm' onClick={handleAddPhase}
variant='outline' size='sm'
className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50' variant='outline'
disabled={!selectedCategory || !isEditable} className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50'
> disabled={!selectedCategory || !isChecklistStatusDraft}
<Plus className='w-4 h-4 mr-1' /> >
Pilih Fase <Plus className='w-4 h-4 mr-1' />
</Button> Pilih Fase
</div> </Button>
</div>
)}
{selectedPhaseIds.length > 0 ? ( {selectedPhaseIds.length > 0 ? (
<div className='flex flex-wrap gap-2'> <div className='flex flex-wrap gap-2'>
@@ -1010,19 +1071,21 @@ export function DailyChecklistContent() {
{/* ABK Assignment Section */} {/* ABK Assignment Section */}
{dailyChecklistId && selectedPhaseIds.length > 0 && ( {dailyChecklistId && selectedPhaseIds.length > 0 && (
<div className='mb-6 pb-6 border-b border-gray-200'> <div className='mb-6 pb-6 border-b border-gray-200'>
<div className='flex items-center justify-between mb-3'> {isChecklistStatusDraft && (
<Label>ABK Assignment</Label> <div className='flex items-center justify-between mb-3'>
<Button <Label>ABK Assignment</Label>
onClick={handleAddAbk} <Button
size='sm' onClick={handleAddAbk}
variant='outline' size='sm'
className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50' variant='outline'
disabled={!kandangId || !isEditable} className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50'
> disabled={!kandangId || !isChecklistStatusDraft}
<Plus className='w-4 h-4 mr-1' /> >
Tambah ABK <Plus className='w-4 h-4 mr-1' />
</Button> Tambah ABK
</div> </Button>
</div>
)}
{selectedEmployees.length > 0 ? ( {selectedEmployees.length > 0 ? (
<div className='flex flex-wrap gap-2'> <div className='flex flex-wrap gap-2'>
@@ -1033,7 +1096,7 @@ export function DailyChecklistContent() {
className='px-3 py-1.5 bg-gray-100 text-gray-700 border border-gray-200 rounded-lg' className='px-3 py-1.5 bg-gray-100 text-gray-700 border border-gray-200 rounded-lg'
> >
{emp.name} {emp.name}
{isEditable && ( {isChecklistStatusDraft && (
<button <button
onClick={() => handleRemoveAbk(String(emp.id))} onClick={() => handleRemoveAbk(String(emp.id))}
className='ml-2 hover:text-gray-900' className='ml-2 hover:text-gray-900'
@@ -1084,6 +1147,7 @@ export function DailyChecklistContent() {
(phaseId) => { (phaseId) => {
const phaseData = groupActivitiesByPhase()[phaseId]; const phaseData = groupActivitiesByPhase()[phaseId];
const { phase, timeGroups } = phaseData; const { phase, timeGroups } = phaseData;
const timeTypes = Object.keys(timeGroups).sort( const timeTypes = Object.keys(timeGroups).sort(
(a, b) => (a, b) =>
TIME_TYPE_ORDER.indexOf(a) - TIME_TYPE_ORDER.indexOf(a) -
@@ -1197,7 +1261,7 @@ export function DailyChecklistContent() {
e.target.checked e.target.checked
) )
} }
disabled={!isEditable} disabled={!isChecklistStatusDraft}
className='checkbox-clean' className='checkbox-clean'
/> />
</td> </td>
@@ -1224,7 +1288,7 @@ export function DailyChecklistContent() {
); );
} }
}} }}
disabled={!isEditable} disabled={!isChecklistStatusDraft}
/> />
</td> </td>
</tr> </tr>
@@ -1320,61 +1384,68 @@ export function DailyChecklistContent() {
/> />
</Link> </Link>
<Button {isChecklistStatusDraft && (
type='button' <Button
variant='ghost' type='button'
color='error' variant='ghost'
onClick={() => { color='error'
setDeletedDocumentIds((prevIds) => [ onClick={() => {
...prevIds, setDeletedDocumentIds((prevIds) => [
existingDocument.id, ...prevIds,
]); existingDocument.id,
]);
setExistingDocuments((prevExistingDocument) => { setExistingDocuments(
const newExistingDocuments = [ (prevExistingDocument) => {
...prevExistingDocument, const newExistingDocuments = [
]; ...prevExistingDocument,
newExistingDocuments.splice( ];
existingDocumentIdx, newExistingDocuments.splice(
1 existingDocumentIdx,
1
);
return newExistingDocuments;
}
); );
return newExistingDocuments; }}
}); className='p-1 rounded-full text-error focus-visible:text-error-content hover:text-error-content'
}} >
className='p-1 rounded-full text-error focus-visible:text-error-content hover:text-error-content' <Icon
> icon='fluent:delete-12-regular'
<Icon width={20}
icon='fluent:delete-12-regular' height={20}
width={20} />
height={20} </Button>
/> )}
</Button>
</div> </div>
) )
)} )}
</div> </div>
)} )}
<DropFileInput {isChecklistStatusDraft && (
name='Dokumen' <DropFileInput
label='Dokumen' name='Dokumen'
values={documents} label='Dokumen'
onChange={(files) => { values={documents}
setDocuments(files); onChange={(files) => {
}} setDocuments(files);
onDelete={(deletedFileIdx: number) => { }}
const newRequestDocuments = [...documents]; onDelete={(deletedFileIdx: number) => {
const newRequestDocuments = [...documents];
newRequestDocuments?.splice(deletedFileIdx, 1); newRequestDocuments?.splice(deletedFileIdx, 1);
setDocuments(newRequestDocuments); setDocuments(newRequestDocuments);
}} }}
className={{ disabled={!isChecklistStatusDraft}
wrapper: 'mt-6', className={{
inputWrapper: 'flex items-center', wrapper: 'mt-6',
label: 'font-semibold text-gray-900', inputWrapper: 'flex items-center',
}} label: 'font-semibold text-gray-900',
/> }}
/>
)}
</> </>
)} )}
@@ -1382,24 +1453,30 @@ export function DailyChecklistContent() {
{dailyChecklistId && {dailyChecklistId &&
selectedPhaseIds.length > 0 && selectedPhaseIds.length > 0 &&
selectedEmployees.length > 0 && selectedEmployees.length > 0 &&
isEditable && ( isChecklistStatusDraft && (
<div className='flex justify-end gap-3 mt-6 pt-6 border-t border-gray-200'> <div className='flex justify-end gap-3 mt-6 pt-6 border-t border-gray-200'>
<Button <Button
onClick={handleSaveDraft} onClick={handleSaveDraft}
variant='outline' variant='outline'
disabled={loading} disabled={isLoadingDraft}
className='border-gray-200' className='border-gray-200'
> >
<Save className='w-4 h-4 mr-2' /> <Save className='w-4 h-4 mr-2' />
Simpan Draft {isLoadingDraft ? (
<span className='loading loading-spinner loading-sm' />
) : (
'Simpan Draft'
)}
</Button> </Button>
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
disabled={loading} disabled={isLoadingSubmit}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white' className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
> >
<Send className='w-4 h-4 mr-2' /> <Send className='w-4 h-4 mr-2' />
Submit Checklist {isLoadingSubmit
? 'Mengirim Checklist...'
: 'Submit Checklist'}
</Button> </Button>
</div> </div>
)} )}
@@ -1440,7 +1517,9 @@ export function DailyChecklistContent() {
if (isAllPhasesSelected) { if (isAllPhasesSelected) {
setTempSelectedPhaseIds([]); setTempSelectedPhaseIds([]);
} else { } else {
setTempSelectedPhaseIds(availablePhases.map((p) => p.id)); setTempSelectedPhaseIds(
availablePhases.map((p) => String(p.id))
);
} }
}} }}
className='checkbox-clean' className='checkbox-clean'
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Eye, CheckCircle, XCircle, Search, Trash2 } from 'lucide-react'; import { Eye, CheckCircle, XCircle, Search, Trash2, Edit } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card'; import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Badge } from '@/figma-make/components/base/badge'; import { Badge } from '@/figma-make/components/base/badge';
@@ -121,6 +121,16 @@ export function ListDailyChecklistContent() {
); );
}; };
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) => { const handleApprove = (item: DailyChecklist) => {
setSelectedItem(item); setSelectedItem(item);
setShowApproveModal(true); setShowApproveModal(true);
@@ -377,6 +387,19 @@ export function ListDailyChecklistContent() {
<Eye className='w-4 h-4 mr-1' /> <Eye className='w-4 h-4 mr-1' />
Detail Detail
</Button> </Button>
{row.original.status === 'DRAFT' && (
<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>
)}
{row.original.status === 'SUBMITTED' && ( {row.original.status === 'SUBMITTED' && (
<> <>
<Button <Button
@@ -398,15 +421,18 @@ export function ListDailyChecklistContent() {
</Button> </Button>
</> </>
)} )}
<Button
size='sm' {row.original.status === 'DRAFT' && (
variant='destructive' <Button
onClick={() => handleDelete(row.original)} size='sm'
className='bg-red-600 hover:bg-red-700 text-white' variant='destructive'
> onClick={() => handleDelete(row.original)}
<Trash2 className='w-4 h-4 mr-1' /> className='bg-red-600 hover:bg-red-700 text-white'
Hapus >
</Button> <Trash2 className='w-4 h-4 mr-1' />
Hapus
</Button>
)}
</div> </div>
), ),
}, },
@@ -389,7 +389,7 @@ export function DetailDailyChecklistContent() {
} = {}; } = {};
phaseData.activities.forEach((activityData) => { phaseData.activities.forEach((activityData) => {
const timeType = activityData.time_type || 'umum'; const timeType = activityData.time_type || 'Umum';
if (!timeGroups[timeType]) { if (!timeGroups[timeType]) {
timeGroups[timeType] = { activities: [] }; timeGroups[timeType] = { activities: [] };