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'],
},
{
text: 'Daily Checklist',
text: 'Formulir',
link: '/daily-checklist/daily-checklist',
icon: 'lucide:clipboard-check',
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 DropFileInput from '@/components/input/DropFileInput';
import Link from 'next/link';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { Icon } from '@iconify/react';
// Static categories
@@ -51,7 +52,7 @@ const CATEGORIES = [
{ 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 } = {
Umum: 'Umum',
Pagi: 'Pagi',
@@ -67,7 +68,23 @@ interface Phase {
}
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 } =
useSelect(KandangApi.basePath, 'id', 'name', 'search', {
@@ -104,12 +121,6 @@ export function DailyChecklistContent() {
? 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 [selectedEmployees, setSelectedEmployees] = useState<
@@ -118,7 +129,7 @@ export function DailyChecklistContent() {
const [dailyChecklistId, setDailyChecklistId] = useState<string | null>(null);
const [checklistStatus, setChecklistStatus] = useState<string>('DRAFT');
const [isEditMode, setIsEditMode] = useState(false);
// const [isEditMode, setIsEditMode] = useState(false);
// Activities grouped by phase
const [activitiesByPhase, setActivitiesByPhase] = useState<{
@@ -148,13 +159,57 @@ export function DailyChecklistContent() {
const [searchAbk, setSearchAbk] = 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 [existingDocuments, setExistingDocuments] = useState<Document[]>([]);
const [documents, setDocuments] = useState<File[]>([]);
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
const formatDateForDisplay = (dateStr: string) => {
if (!dateStr) return 'Pilih tanggal';
@@ -179,7 +234,7 @@ export function DailyChecklistContent() {
if (!date || !kandangId || !selectedCategory) {
setDailyChecklistId(null);
setChecklistStatus('DRAFT');
setIsEditMode(false);
// setIsEditMode(false);
setSelectedPhaseIds([]);
setActivitiesByPhase({});
setTaskIdsByPhaseActivityId({});
@@ -216,7 +271,7 @@ export function DailyChecklistContent() {
existingPhases.data.phases.length > 0
) {
// Existing checklist - EDIT MODE
setIsEditMode(true);
// setIsEditMode(true);
const phaseIds = existingPhases.data.phases.map((p) =>
String(p.phase_id)
);
@@ -234,7 +289,7 @@ export function DailyChecklistContent() {
}
} else {
// New checklist - CREATE MODE
setIsEditMode(false);
// setIsEditMode(false);
setSelectedPhaseIds([]);
}
} catch (error) {
@@ -608,7 +663,7 @@ export function DailyChecklistContent() {
// taskId,
// hasTaskId: !!taskId,
// checklistStatus,
// isEditable,
// isChecklistStatusDraft,
// });
if (!taskId) {
@@ -618,7 +673,7 @@ export function DailyChecklistContent() {
return;
}
if (!isEditable) {
if (!isChecklistStatusDraft) {
console.warn(
'[CHECKBOX] Checklist is not editable, status:',
checklistStatus
@@ -736,7 +791,7 @@ export function DailyChecklistContent() {
return;
}
setLoading(true);
setIsLoadingSubmit(true);
try {
const submitRes = await DailyChecklistApi.submit(
@@ -757,13 +812,15 @@ export function DailyChecklistContent() {
console.error('Error submitting:', error);
toast.error('Terjadi kesalahan');
} finally {
setLoading(false);
setIsLoadingSubmit(false);
}
};
const handleSaveDraft = async () => {
if (!dailyChecklistId) return;
setIsLoadingDraft(true);
const uploadImageRes = await DailyChecklistApi.uploadImage(
Number(dailyChecklistId),
'DRAFT',
@@ -774,10 +831,12 @@ export function DailyChecklistContent() {
if (isResponseError(uploadImageRes)) {
console.error('Error saving draft:', uploadImageRes.message);
toast.error('Gagal menyimpan draft');
setIsLoadingDraft(false);
return;
}
toast.success('Draft tersimpan otomatis');
setIsLoadingDraft(false);
toast.success('Draft tersimpan!');
};
// Filter functions
@@ -825,7 +884,7 @@ export function DailyChecklistContent() {
// Group activities by time_type within this phase
phaseActivities.forEach((activity) => {
const timeType = activity.time_type || 'umum';
const timeType = activity.time_type || 'Umum';
if (!grouped[phase.id].timeGroups[timeType]) {
grouped[phase.id].timeGroups[timeType] = [];
@@ -838,7 +897,7 @@ export function DailyChecklistContent() {
return grouped;
};
const isEditable = checklistStatus === 'DRAFT';
const isChecklistStatusDraft = checklistStatus === 'DRAFT';
if (initialLoading) {
return (
@@ -871,7 +930,7 @@ export function DailyChecklistContent() {
<h1 className='text-2xl font-semibold text-gray-900'>
Daily Checklist
</h1>
{isEditMode && (
{isChecklistStatusDraft && (
<Badge
variant='outline'
className='border-amber-300 text-amber-700 bg-white'
@@ -907,7 +966,7 @@ export function DailyChecklistContent() {
<DatePicker
date={date}
onDateChange={setDate}
disabled={!isEditable}
disabled={!isChecklistStatusDraft}
placeholder='Pilih tanggal'
formatDisplay={formatDateForDisplay}
/>
@@ -921,7 +980,7 @@ export function DailyChecklistContent() {
<Select
value={kandangId}
onValueChange={setKandangId}
disabled={!isEditable}
disabled={!isChecklistStatusDraft}
>
<SelectTrigger
id='kandang'
@@ -949,7 +1008,7 @@ export function DailyChecklistContent() {
<Select
value={selectedCategory}
onValueChange={setSelectedCategory}
disabled={!isEditable}
disabled={!isChecklistStatusDraft}
>
<SelectTrigger
id='category'
@@ -971,6 +1030,7 @@ export function DailyChecklistContent() {
{/* Phase Selection Section */}
{dailyChecklistId && (
<div className='mb-6 pb-6 border-b border-gray-200'>
{isChecklistStatusDraft && (
<div className='flex items-center justify-between mb-3'>
<Label>Fase / Tahap</Label>
<Button
@@ -978,12 +1038,13 @@ export function DailyChecklistContent() {
size='sm'
variant='outline'
className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50'
disabled={!selectedCategory || !isEditable}
disabled={!selectedCategory || !isChecklistStatusDraft}
>
<Plus className='w-4 h-4 mr-1' />
Pilih Fase
</Button>
</div>
)}
{selectedPhaseIds.length > 0 ? (
<div className='flex flex-wrap gap-2'>
@@ -1010,6 +1071,7 @@ export function DailyChecklistContent() {
{/* ABK Assignment Section */}
{dailyChecklistId && selectedPhaseIds.length > 0 && (
<div className='mb-6 pb-6 border-b border-gray-200'>
{isChecklistStatusDraft && (
<div className='flex items-center justify-between mb-3'>
<Label>ABK Assignment</Label>
<Button
@@ -1017,12 +1079,13 @@ export function DailyChecklistContent() {
size='sm'
variant='outline'
className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50'
disabled={!kandangId || !isEditable}
disabled={!kandangId || !isChecklistStatusDraft}
>
<Plus className='w-4 h-4 mr-1' />
Tambah ABK
</Button>
</div>
)}
{selectedEmployees.length > 0 ? (
<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'
>
{emp.name}
{isEditable && (
{isChecklistStatusDraft && (
<button
onClick={() => handleRemoveAbk(String(emp.id))}
className='ml-2 hover:text-gray-900'
@@ -1084,6 +1147,7 @@ export function DailyChecklistContent() {
(phaseId) => {
const phaseData = groupActivitiesByPhase()[phaseId];
const { phase, timeGroups } = phaseData;
const timeTypes = Object.keys(timeGroups).sort(
(a, b) =>
TIME_TYPE_ORDER.indexOf(a) -
@@ -1197,7 +1261,7 @@ export function DailyChecklistContent() {
e.target.checked
)
}
disabled={!isEditable}
disabled={!isChecklistStatusDraft}
className='checkbox-clean'
/>
</td>
@@ -1224,7 +1288,7 @@ export function DailyChecklistContent() {
);
}
}}
disabled={!isEditable}
disabled={!isChecklistStatusDraft}
/>
</td>
</tr>
@@ -1320,6 +1384,7 @@ export function DailyChecklistContent() {
/>
</Link>
{isChecklistStatusDraft && (
<Button
type='button'
variant='ghost'
@@ -1330,7 +1395,8 @@ export function DailyChecklistContent() {
existingDocument.id,
]);
setExistingDocuments((prevExistingDocument) => {
setExistingDocuments(
(prevExistingDocument) => {
const newExistingDocuments = [
...prevExistingDocument,
];
@@ -1339,7 +1405,8 @@ export function DailyChecklistContent() {
1
);
return newExistingDocuments;
});
}
);
}}
className='p-1 rounded-full text-error focus-visible:text-error-content hover:text-error-content'
>
@@ -1349,12 +1416,14 @@ export function DailyChecklistContent() {
height={20}
/>
</Button>
)}
</div>
)
)}
</div>
)}
{isChecklistStatusDraft && (
<DropFileInput
name='Dokumen'
label='Dokumen'
@@ -1369,12 +1438,14 @@ export function DailyChecklistContent() {
setDocuments(newRequestDocuments);
}}
disabled={!isChecklistStatusDraft}
className={{
wrapper: 'mt-6',
inputWrapper: 'flex items-center',
label: 'font-semibold text-gray-900',
}}
/>
)}
</>
)}
@@ -1382,24 +1453,30 @@ export function DailyChecklistContent() {
{dailyChecklistId &&
selectedPhaseIds.length > 0 &&
selectedEmployees.length > 0 &&
isEditable && (
isChecklistStatusDraft && (
<div className='flex justify-end gap-3 mt-6 pt-6 border-t border-gray-200'>
<Button
onClick={handleSaveDraft}
variant='outline'
disabled={loading}
disabled={isLoadingDraft}
className='border-gray-200'
>
<Save className='w-4 h-4 mr-2' />
Simpan Draft
{isLoadingDraft ? (
<span className='loading loading-spinner loading-sm' />
) : (
'Simpan Draft'
)}
</Button>
<Button
onClick={handleSubmit}
disabled={loading}
disabled={isLoadingSubmit}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
<Send className='w-4 h-4 mr-2' />
Submit Checklist
{isLoadingSubmit
? 'Mengirim Checklist...'
: 'Submit Checklist'}
</Button>
</div>
)}
@@ -1440,7 +1517,9 @@ export function DailyChecklistContent() {
if (isAllPhasesSelected) {
setTempSelectedPhaseIds([]);
} else {
setTempSelectedPhaseIds(availablePhases.map((p) => p.id));
setTempSelectedPhaseIds(
availablePhases.map((p) => String(p.id))
);
}
}}
className='checkbox-clean'
@@ -1,7 +1,7 @@
'use client';
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 { Button } from '@/figma-make/components/base/button';
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) => {
setSelectedItem(item);
setShowApproveModal(true);
@@ -377,6 +387,19 @@ export function ListDailyChecklistContent() {
<Eye className='w-4 h-4 mr-1' />
Detail
</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' && (
<>
<Button
@@ -398,6 +421,8 @@ export function ListDailyChecklistContent() {
</Button>
</>
)}
{row.original.status === 'DRAFT' && (
<Button
size='sm'
variant='destructive'
@@ -407,6 +432,7 @@ export function ListDailyChecklistContent() {
<Trash2 className='w-4 h-4 mr-1' />
Hapus
</Button>
)}
</div>
),
},
@@ -389,7 +389,7 @@ export function DetailDailyChecklistContent() {
} = {};
phaseData.activities.forEach((activityData) => {
const timeType = activityData.time_type || 'umum';
const timeType = activityData.time_type || 'Umum';
if (!timeGroups[timeType]) {
timeGroups[timeType] = { activities: [] };