From 334bd08e60a0c97e660615555a086b6232a2aaf6 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 10:46:19 +0700 Subject: [PATCH 01/29] feat: integrate Daily Checklist Dashboard to API integration --- .../components/pages/dashboard/Dashboard.tsx | 606 +++++------------- 1 file changed, 163 insertions(+), 443 deletions(-) diff --git a/src/figma-make/components/pages/dashboard/Dashboard.tsx b/src/figma-make/components/pages/dashboard/Dashboard.tsx index 5d866ffc..f6d12d79 100644 --- a/src/figma-make/components/pages/dashboard/Dashboard.tsx +++ b/src/figma-make/components/pages/dashboard/Dashboard.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { Card, CardContent, @@ -15,7 +15,6 @@ import { SelectTrigger, SelectValue, } from '@/figma-make/components/base/select'; -import { Input } from '@/figma-make/components/base/input'; import { Badge } from '@/figma-make/components/base/badge'; import { Calendar as CalendarIcon, @@ -35,53 +34,17 @@ import { ResponsiveContainer, Cell, } from 'recharts'; -import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase'; import { toast } from 'sonner'; - -interface EmployeePerformance { - employee_id: string; - employee_name: string; - kandang_id: string; - kandang_name: string; - total_activities_in_category: number; // Total aktivitas di kategori - completed_activities: number; // Aktivitas yang sudah di-check - completion_rate: number; - last_activity_date: string | null; - color: string; // Color based on kandang -} - -interface Kandang { - id: string; - name: string; -} - -interface Category { - id: string; - name: string; -} - -interface ChecklistKandang { - id: string; - date: string; - kandang_id: string; - category: string; - kandang: { - id: string; - name: string; - } | null; -} - -interface AssignmentEmployee { - id: string; - task_id: string; - employee_id: string; - checked: boolean; - updated_at: string; - employee: { - id: string; - name: string; - } | null; -} +import useSWR from 'swr'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { DailyChecklistSummary } from '@/types/api/daily-checklist/daily-checklist'; +import { AxiosError } from 'axios'; +import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; +import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist'; +import { KandangApi } from '@/services/api/master-data'; +import { useSelect } from '@/components/input/SelectInput'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { formatDate } from '@/lib/helper'; const KANDANG_COLORS = [ '#0069e0', // Blue (primary) @@ -102,312 +65,65 @@ const CATEGORY_LABELS: { [key: string]: string } = { }; export function Dashboard() { - const [loading, setLoading] = useState(false); - const [employeePerformance, setEmployeePerformance] = useState< - EmployeePerformance[] - >([]); - - // Master data - const [kandangList, setKandangList] = useState([]); - const [categoryList, setCategoryList] = useState([]); - // Filters const [dateFrom, setDateFrom] = useState(''); const [dateTo, setDateTo] = useState(''); const [kandangFilter, setKandangFilter] = useState('ALL'); const [categoryFilter, setCategoryFilter] = useState('ALL'); - // Color mapping for kandang - const [kandangColorMap, setKandangColorMap] = useState<{ - [key: string]: string; - }>({}); - - useEffect(() => { - fetchMasterData(); - }, []); - - useEffect(() => { - // Only fetch when date filters are set - if (dateFrom && dateTo) { - fetchEmployeePerformance(); - } else { - setEmployeePerformance([]); + const { + data: summaryResponse, + isLoading: isLoadingSummary, + mutate: refreshSummary, + } = useSWR< + BaseApiResponse, + AxiosError, + SWRHttpKey + >( + dateFrom && dateTo + ? `${DailyChecklistApi.basePath}/summary?date_from=${dateFrom}&date_to=${dateTo}&kandang_id=${kandangFilter === 'ALL' ? '' : kandangFilter}&category=${categoryFilter === 'ALL' ? '' : categoryFilter}` + : '', + httpClientFetcher, + { + keepPreviousData: true, } - }, [dateFrom, dateTo, kandangFilter, categoryFilter]); + ); - const fetchMasterData = async () => { - if (!isSupabaseConfigured()) return; - - try { - // Fetch kandang - const { data: kandangData, error: kandangError } = await supabase - .from('kandang') - .select('id, name') - .order('name', { ascending: true }); - - if (kandangError) { - console.error('Error fetching kandang:', kandangError); - } else { - setKandangList(kandangData || []); - - // Create color mapping - const colorMap: { [key: string]: string } = {}; - (kandangData || []).forEach((k, index) => { - colorMap[k.id] = KANDANG_COLORS[index % KANDANG_COLORS.length]; - }); - setKandangColorMap(colorMap); - } - - // Set categories from CATEGORY_LABELS (hardcoded list) - const categories: Category[] = Object.keys(CATEGORY_LABELS).map((id) => ({ - id, - name: CATEGORY_LABELS[id], - })); - setCategoryList(categories); - } catch (error) { - console.error('Error fetching master data:', error); - } - }; - - const fetchEmployeePerformance = async () => { - if (!isSupabaseConfigured() || !dateFrom || !dateTo) { - return; - } - - try { - setLoading(true); - - // Step 1: Get all checklists in date range + filters - let checklistQuery = supabase - .from('daily_checklists') - .select( - ` - id, - date, - kandang_id, - category, - kandang:kandang_id ( - id, - name - ) - ` - ) - .gte('date', dateFrom) - .lte('date', dateTo); - - if (kandangFilter !== 'ALL') { - checklistQuery = checklistQuery.eq('kandang_id', kandangFilter); - } - - if (categoryFilter !== 'ALL') { - checklistQuery = checklistQuery.eq('category', categoryFilter); - } - - const { data: checklists, error: checklistError } = await checklistQuery; - - if (checklistError) { - console.error('Error fetching checklists:', checklistError); - toast.error('Gagal memuat data checklist'); - return; - } - - if (!checklists || checklists.length === 0) { - setEmployeePerformance([]); - return; - } - - const checklistsData = checklists as unknown as ChecklistKandang[]; - - // Step 2: Get all tasks from these checklists - const checklistIds = checklistsData.map((c) => c.id); - const { data: tasks, error: tasksError } = await supabase - .from('daily_checklist_activity_tasks') - .select('id, checklist_id') - .in('checklist_id', checklistIds); - - if (tasksError) { - console.error('Error fetching tasks:', tasksError); - return; - } - - if (!tasks || tasks.length === 0) { - setEmployeePerformance([]); - return; - } - - const taskIds = tasks.map((t) => t.id); - - // Step 3: Get all assignments for these tasks - const { data: assignments, error: assignmentsError } = await supabase - .from('daily_checklist_activity_task_assignments') - .select( - ` - id, - task_id, - employee_id, - checked, - updated_at, - employee:employee_id ( - id, - name - ) - ` - ) - .in('task_id', taskIds); - - if (assignmentsError) { - console.error('Error fetching assignments:', assignmentsError); - return; - } - - if (!assignments || assignments.length === 0) { - setEmployeePerformance([]); - return; - } - - const assignmentsData = assignments as unknown as AssignmentEmployee[]; - - // Step 4: Calculate total activities in selected category (if filtered) - let totalActivitiesInCategory = 0; - - if (categoryFilter !== 'ALL') { - // Get total activities from master data for this category - const { data: phases } = await supabase - .from('phases') - .select('id') - .eq('category_id', categoryFilter); - - if (phases && phases.length > 0) { - const phaseIds = phases.map((p) => p.id); - const { count } = await supabase - .from('activities') - .select('*', { count: 'exact', head: true }) - .in('phase_id', phaseIds); - - totalActivitiesInCategory = count || 0; - } - } - - // Step 5: Group by employee and calculate performance - const employeeMap = new Map< - string, - { - employee_id: string; - employee_name: string; - kandang_id: string; - kandang_name: string; - completed_count: number; - total_count: number; - last_activity_date: string | null; - } - >(); - - assignmentsData.forEach((assignment) => { - const task = tasks.find((t) => t.id === assignment.task_id); - if (!task) return; - - const checklist = checklistsData.find( - (c) => c.id === task.checklist_id - ); - if (!checklist) return; - - const employeeId = assignment.employee_id; - const employeeName = assignment.employee?.name || 'Unknown'; - const kandangId = checklist.kandang_id; - const kandangName = checklist.kandang?.name || 'Unknown'; - - if (!employeeMap.has(employeeId)) { - employeeMap.set(employeeId, { - employee_id: employeeId, - employee_name: employeeName, - kandang_id: kandangId, - kandang_name: kandangName, - completed_count: 0, - total_count: 0, - last_activity_date: null, - }); - } - - const empData = employeeMap.get(employeeId)!; - empData.total_count += 1; - - if (assignment.checked) { - empData.completed_count += 1; - } - - // Update last activity date - if (assignment.updated_at) { - if ( - !empData.last_activity_date || - assignment.updated_at > empData.last_activity_date - ) { - empData.last_activity_date = assignment.updated_at; - } - } - }); - - // Step 6: Convert to array and add calculated fields - const performanceData: EmployeePerformance[] = Array.from( - employeeMap.values() - ).map((emp) => { - // Use total activities in category if category is selected, otherwise use employee's assigned count - const totalActivities = - categoryFilter !== 'ALL' && totalActivitiesInCategory > 0 - ? totalActivitiesInCategory - : emp.total_count; - - return { - employee_id: emp.employee_id, - employee_name: emp.employee_name, - kandang_id: emp.kandang_id, - kandang_name: emp.kandang_name, - total_activities_in_category: totalActivities, - completed_activities: emp.completed_count, - completion_rate: - totalActivities > 0 - ? Math.round((emp.completed_count / totalActivities) * 100) - : 0, - last_activity_date: emp.last_activity_date, - color: kandangColorMap[emp.kandang_id] || '#0069e0', - }; - }); - - // Sort by employee name - performanceData.sort((a, b) => - a.employee_name.localeCompare(b.employee_name) - ); - - setEmployeePerformance(performanceData); - } catch (error) { - console.error('Error fetching employee performance:', error); - toast.error('Terjadi kesalahan saat memuat data'); - } finally { - setLoading(false); - } - }; - - const formatDate = (dateString: string | null) => { - if (!dateString) return '-'; - const date = new Date(dateString); - return date.toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', + const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = + useSelect(KandangApi.basePath, 'id', 'name', 'search', { + page: '1', + limit: '100', }); - }; - const hasFilters = dateFrom && dateTo; + const kandangColorMap: { [key: string]: string } = {}; + (kandangOptions || []).forEach((k, index) => { + kandangColorMap[k.value] = KANDANG_COLORS[index % KANDANG_COLORS.length]; + }); - // Prepare chart data - const chartData = employeePerformance.map((emp) => ({ + const employeePerformance = isResponseSuccess(summaryResponse) + ? summaryResponse.data?.tracking_abk.map((abk) => { + return { + ...abk, + color: kandangColorMap[abk.kandang_id] || '#0069e0', + }; + }) + : []; + + const chartData = employeePerformance?.map((emp) => ({ name: emp.employee_name, - completed: emp.completed_activities, - remaining: emp.total_activities_in_category - emp.completed_activities, - total: emp.total_activities_in_category, + completed: emp.activity_done, + remaining: emp.activity_left, + total: emp.total_activity, color: emp.color, kandang: emp.kandang_name, })); + const hasFilters = dateFrom && dateTo; + + if (summaryResponse && isResponseError(summaryResponse)) { + toast.error('Gagal memuat data: ' + summaryResponse.message); + } + return (
@@ -457,9 +173,12 @@ export function Dashboard() { Semua Kandang - {kandangList.map((kandang) => ( - - {kandang.name} + {kandangOptions.map((kandang) => ( + + {kandang.label} ))} @@ -482,9 +201,9 @@ export function Dashboard() { Semua Kategori - {categoryList.map((category) => ( - - {category.name} + {Object.keys(CATEGORY_LABELS).map((category) => ( + + {CATEGORY_LABELS[category]} ))} @@ -523,11 +242,11 @@ export function Dashboard() { melihat performance ABK.

- ) : loading ? ( + ) : isLoadingSummary ? (
Memuat data...
- ) : employeePerformance.length === 0 ? ( + ) : employeePerformance && employeePerformance.length === 0 ? (

@@ -582,7 +301,7 @@ export function Dashboard() { fill='#10B981' radius={[0, 0, 0, 0]} > - {chartData.map((entry, index) => ( + {chartData?.map((entry, index) => ( - {chartData.map((entry, index) => ( + {chartData?.map((entry, index) => ( {/* Employee Tracking Table */} - {hasFilters && employeePerformance.length > 0 && ( - - - Tracking ABK -

- Detail performance masing-masing ABK -

-
- -
- - - - - - - - - - - - - - {employeePerformance.map((emp, index) => ( - - - - - - - - + {hasFilters && + employeePerformance && + employeePerformance.length > 0 && ( + + + Tracking ABK +

+ Detail performance masing-masing ABK +

+
+ +
+
- Nama ABK - - Kandang - - Total Aktivitas - - Aktivitas Selesai - - Aktivitas Tersisa - - Completion Rate - - Last Activity -
- {emp.employee_name} - - - {emp.kandang_name} - - - {emp.total_activities_in_category} - - {emp.completed_activities} - - {emp.total_activities_in_category - - emp.completed_activities} - -
-
-
-
- - {emp.completion_rate}% - -
-
- {formatDate(emp.last_activity_date)} -
+ + + + + + + + + - ))} - -
+ Nama ABK + + Kandang + + Total Aktivitas + + Aktivitas Selesai + + Aktivitas Tersisa + + Completion Rate + + Last Activity +
-
-
-
- )} + + + {employeePerformance?.map((emp, index) => ( + + + {emp.employee_name} + + + + {emp.kandang_name} + + + + {emp.total_activity} + + + {emp.activity_done} + + + {emp.activity_left} + + +
+
+
+
+ + {emp.completion_rate}% + +
+ + + {formatDate(emp.last_activity, 'DD MMM YYYY')} + + + ))} + + +
+ + + )}

); From a8c12d0c929842a65e33fd1ae9ad55e0ae88aac5 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 10:47:00 +0700 Subject: [PATCH 02/29] feat(FE): create type for daily checklist dashboard --- .../api/daily-checklist/daily-checklist.d.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/types/api/daily-checklist/daily-checklist.d.ts b/src/types/api/daily-checklist/daily-checklist.d.ts index 9f01ae1f..b88a34aa 100644 --- a/src/types/api/daily-checklist/daily-checklist.d.ts +++ b/src/types/api/daily-checklist/daily-checklist.d.ts @@ -54,3 +54,29 @@ export type CreateDailyChecklistPayload = { category: string; status: string; }; + +export type PerformanceOverviewItem = { + employee_id: number; + employee_name: string; + total_activity: number; + activity_done: number; + activity_left: number; + kandang: Pick; +}; + +export type TrackingAbkItem = { + employee_id: number; + employee_name: string; + kandang_id: number; + kandang_name: string; + total_activity: number; + activity_done: number; + activity_left: number; + completion_rate: number; + last_activity: string; +}; + +export type DailyChecklistSummary = { + performance_overview: PerformanceOverviewItem[]; + tracking_abk: TrackingAbkItem[]; +}; From f46a0610f57477bb681e41f95931e42d087e05ea Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 12:55:31 +0700 Subject: [PATCH 03/29] feat: integrate Daily Checklist Report to API --- .../reports/DailyChecklistReportsContent.tsx | 923 ++++++++---------- 1 file changed, 432 insertions(+), 491 deletions(-) diff --git a/src/figma-make/components/pages/reports/DailyChecklistReportsContent.tsx b/src/figma-make/components/pages/reports/DailyChecklistReportsContent.tsx index 71156f15..7d6383a0 100644 --- a/src/figma-make/components/pages/reports/DailyChecklistReportsContent.tsx +++ b/src/figma-make/components/pages/reports/DailyChecklistReportsContent.tsx @@ -1,13 +1,9 @@ 'use client'; -import { useState, useEffect } from 'react'; -import { Eye, Download, Search } from 'lucide-react'; +import { useMemo } from '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 { Input } from '@/figma-make/components/base/input'; import { Label } from '@/figma-make/components/base/label'; -import { DateRangePicker } from '@/figma-make/components/base/date-range-picker'; import { Select, SelectContent, @@ -16,357 +12,287 @@ import { SelectValue, } from '@/figma-make/components/base/select'; import { toast } from 'sonner'; -import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase'; import { useRouter } from 'next/navigation'; +import { useSelect } from '@/components/input/SelectInput'; +import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data'; +import useSWR from 'swr'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { DailyChecklistReport } from '@/types/api/daily-checklist/daily-checklist'; +import { AxiosError } from 'axios'; +import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; +import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist'; +import Table from '@/components/Table'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { cn } from '@/lib/helper'; +import { ColumnDef } from '@tanstack/react-table'; +import { report } from 'process'; +import { PhaseApi } from '@/services/api/daily-checklist/phase'; +import { EmployeeApi } from '@/services/api/daily-checklist/employee'; -interface SubmissionReportItem { - checklist_id: string; - date: string; - kandang_id: string; - kandang_name: string; - category: string; - status: string; - progress_percent: number; - total_phases: number; - total_activities: number; - total_employees: number; - updated_at: string; -} - -interface Kandang { - id: string; - name: string; -} - -interface ReportQueryResult { - id: string; - date: string; - kandang_id: string; - category: string; - status: string; - updated_at: string; - kandang: { - id: string; - name: string; - } | null; -} - -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 MONTH_OPTIONS = [ + { value: '1', label: 'Januari' }, + { value: '2', label: 'Februari' }, + { value: '3', label: 'Maret' }, + { value: '4', label: 'April' }, + { value: '5', label: 'Mei' }, + { value: '6', label: 'Juni' }, + { value: '7', label: 'Juli' }, + { value: '8', label: 'Agustus' }, + { value: '9', label: 'September' }, + { value: '10', label: 'Oktober' }, + { value: '11', label: 'November' }, + { value: '12', label: 'Desember' }, ]; -const CATEGORY_LABELS: { [key: string]: string } = { - pullet_open: 'Pullet Open', - pullet_close: 'Pullet Close', - produksi_open: 'Produksi Open', - produksi_close: 'Produksi Close', -}; +const YEAR_OPTIONS = [ + { value: '2027', label: '2027' }, + { value: '2026', label: '2026' }, + { value: '2025', label: '2025' }, + { value: '2024', label: '2024' }, + { value: '2023', label: '2023' }, + { value: '2022', label: '2022' }, + { value: '2021', label: '2021' }, + { value: '2020', label: '2020' }, +]; + +// const CATEGORY_LABELS: { [key: string]: string } = { +// pullet_open: 'Pullet Open', +// pullet_close: 'Pullet Close', +// produksi_open: 'Produksi Open', +// produksi_close: 'Produksi Close', +// }; export function DailyChecklistReportsContent() { const router = useRouter(); - const [loading, setLoading] = useState(true); - // Report State - const [reportList, setReportList] = useState([]); - const [filteredReportList, setFilteredReportList] = useState< - SubmissionReportItem[] - >([]); + const currentMonth = useMemo(() => new Date().getMonth() + 1, []); + const currentYear = useMemo(() => new Date().getFullYear(), []); - // Master data - const [kandangList, setKandangList] = useState([]); + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + bulan: currentMonth.toString(), + tahun: currentYear.toString(), + area_id: '', + location_id: '', + kandang_id: '', + employee_id: '', + phase_id: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + bulan: 'bulan', + tahun: 'tahun', + area_id: 'area_id', + location_id: 'location_id', + kandang_id: 'kandang_id', + employee_id: 'employee_id', + phase_id: 'phase_id', + }, + }); - // Filters - const [statusFilter, setStatusFilter] = useState('ALL'); - const [kandangFilter, setKandangFilter] = useState('ALL'); - const [searchText, setSearchText] = useState(''); - const [dateFrom, setDateFrom] = useState(''); - const [dateTo, setDateTo] = useState(''); - - useEffect(() => { - fetchKandangList(); - fetchReports(); - }, []); - - useEffect(() => { - applyFilters(); - }, [reportList, statusFilter, kandangFilter, searchText, dateFrom, dateTo]); - - const fetchKandangList = async () => { - if (!isSupabaseConfigured()) return; - - try { - const { data, error } = await supabase - .from('kandang') - .select('id, name') - .order('name', { ascending: true }); - - if (error) { - console.error('Error fetching kandang:', error); - return; - } - - setKandangList(data || []); - } catch (error) { - console.error('Error fetching kandang:', error); + const { + data: reportResponse, + isLoading: isLoadingReport, + mutate: refreshReport, + } = useSWR< + BaseApiResponse, + AxiosError, + SWRHttpKey + >( + `${DailyChecklistApi.basePath}/report${getTableFilterQueryString()}`, + httpClientFetcher, + { + keepPreviousData: true, } - }; + ); - const fetchReports = async () => { - if (!isSupabaseConfigured()) { - console.warn('Supabase not configured'); - setLoading(false); - return; + const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( + AreaApi.basePath, + 'id', + 'name', + 'search', + { + page: '1', + limit: '100', } + ); - try { - setLoading(true); - - // Fetch checklists directly from daily_checklists table - const { data: checklists, error } = await supabase - .from('daily_checklists') - .select( - ` - id, - date, - kandang_id, - category, - status, - updated_at, - kandang:kandang_id ( - id, - name - ) - ` - ) - .order('date', { ascending: false }) - .order('updated_at', { ascending: false }); - - if (error) { - console.error('Error fetching reports:', error); - toast.error('Gagal memuat data reports'); - return; - } - - // Enrich data with calculations - const enrichedData = await Promise.all( - ((checklists as unknown as ReportQueryResult[]) || []) - .filter((checklist) => checklist.id) - .map(async (checklist) => { - // Count phases - const { count: phaseCount } = await supabase - .from('daily_checklist_phases') - .select('*', { count: 'exact', head: true }) - .eq('checklist_id', checklist.id); - - // Count activities (tasks) - const { count: activityCount } = await supabase - .from('daily_checklist_activity_tasks') - .select('*', { count: 'exact', head: true }) - .eq('checklist_id', checklist.id); - - // Count unique employees - const { data: tasks } = await supabase - .from('daily_checklist_activity_tasks') - .select('id') - .eq('checklist_id', checklist.id); - - const taskIds = (tasks || []).map((t) => t.id); - let uniqueEmployees = new Set(); - - if (taskIds.length > 0) { - const { data: assignments } = await supabase - .from('daily_checklist_activity_task_assignments') - .select('employee_id') - .in('task_id', taskIds); - - uniqueEmployees = new Set( - (assignments || []).map((a) => a.employee_id) - ); - } - - // ✅ Calculate progress based on phase coverage - const { count: totalPhasesInMaster } = await supabase - .from('phases') - .select('*', { count: 'exact', head: true }) - .eq('category_id', checklist.category); - - const { data: checklistTasks } = await supabase - .from('daily_checklist_activity_tasks') - .select('id, phase_id') - .eq('checklist_id', checklist.id); - - const checklistTaskIds = (checklistTasks || []).map((t) => t.id); - const uniquePhasesWithChecked = new Set(); - - if (checklistTaskIds.length > 0) { - const { data: checkedAssignments } = await supabase - .from('daily_checklist_activity_task_assignments') - .select('task_id') - .in('task_id', checklistTaskIds) - .eq('checked', true); - - if (checkedAssignments && checkedAssignments.length > 0) { - const checkedTaskIds = new Set( - checkedAssignments.map((a) => a.task_id) - ); - checklistTasks?.forEach((task) => { - if (checkedTaskIds.has(task.id)) { - uniquePhasesWithChecked.add(task.phase_id); - } - }); - } - } - - const phasesWithCheckedCount = uniquePhasesWithChecked.size; - const progressPercent = - totalPhasesInMaster && totalPhasesInMaster > 0 - ? Math.round( - (phasesWithCheckedCount / totalPhasesInMaster) * 100 - ) - : 0; - - return { - checklist_id: checklist.id, - date: checklist.date, - kandang_id: checklist.kandang_id, - kandang_name: checklist.kandang?.name || '-', - category: checklist.category, - status: checklist.status, - progress_percent: progressPercent, - total_phases: phaseCount || 0, - total_activities: activityCount || 0, - total_employees: uniqueEmployees.size, - updated_at: checklist.updated_at, - }; - }) - ); - - setReportList(enrichedData); - } catch (error) { - console.error('Error fetching reports:', error); - toast.error('Terjadi kesalahan'); - } finally { - setLoading(false); - } - }; - - const applyFilters = () => { - let filtered = [...reportList]; - - if (statusFilter && statusFilter !== 'ALL') { - filtered = filtered.filter((item) => item.status === statusFilter); - } - - if (kandangFilter && kandangFilter !== 'ALL') { - filtered = filtered.filter((item) => item.kandang_id === kandangFilter); - } - - if (searchText) { - filtered = filtered.filter( - (item) => - item.kandang_name.toLowerCase().includes(searchText.toLowerCase()) || - item.category.toLowerCase().includes(searchText.toLowerCase()) || - (CATEGORY_LABELS[item.category] || '') - .toLowerCase() - .includes(searchText.toLowerCase()) - ); - } - - if (dateFrom) { - filtered = filtered.filter( - (item) => new Date(item.date) >= new Date(dateFrom) - ); - } - if (dateTo) { - filtered = filtered.filter( - (item) => new Date(item.date) <= new Date(dateTo) - ); - } - - setFilteredReportList(filtered); - }; - - const getStatusBadge = (status: string) => { - switch (status) { - case 'DRAFT': - return ( - - Draft - - ); - case 'SUBMITTED': - return ( - - Submitted - - ); - case 'APPROVED': - return ( - - Approved - - ); - case 'REJECTED': - return ( - - Rejected - - ); - default: - return ( - - {status} - - ); - } - }; - - const formatDate = (dateString: string) => { - const date = new Date(dateString); - return date.toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', + const { options: locationOptions, isLoadingOptions: isLoadingLocations } = + useSelect(LocationApi.basePath, 'id', 'name', 'search', { + page: '1', + limit: '100', + area_id: tableFilterState.area_id, }); - }; - 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 { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = + useSelect(KandangApi.basePath, 'id', 'name', 'search', { + page: '1', + limit: '100', + area_id: tableFilterState.area_id, + location_id: tableFilterState.location_id, }); + + const { options: phaseOptions, isLoadingOptions: isLoadingPhases } = + useSelect(PhaseApi.basePath, 'id', 'name', 'search', { + page: '1', + limit: '100', + }); + + const { options: employeeOptions, isLoadingOptions: isLoadingEmployees } = + useSelect(EmployeeApi.basePath, 'id', 'name', 'search', { + page: '1', + limit: '500', + kandang_id: tableFilterState.kandang_id, + }); + + const currentMonthMaxDay = new Date( + Number(tableFilterState.tahun), + Number(tableFilterState.bulan), + 0 + ).getDate(); + + const reportDateColumns: ColumnDef[] = []; + + if (isResponseSuccess(reportResponse) && reportResponse.data) { + for (let dateNumber = 1; dateNumber <= currentMonthMaxDay; dateNumber++) { + reportDateColumns.push({ + accessorKey: `daily_activities[${dateNumber}]`, + header: `${dateNumber}`, + enableSorting: false, + cell: ({ row }) => row.original.daily_activities[dateNumber], + }); + } + } + + const reportColumns: ColumnDef[] = [ + { + accessorKey: 'area', + header: 'Area', + enableSorting: false, + cell: ({ row }) => row.original.area.name, + }, + { + accessorKey: 'farm', + header: 'Farm', + enableSorting: false, + cell: ({ row }) => row.original.farm.name, + }, + { + accessorKey: 'kandang', + header: 'Kandang', + enableSorting: false, + cell: ({ row }) => row.original.kandang.name, + }, + { + accessorKey: 'abk', + header: 'ABK', + enableSorting: false, + cell: ({ row }) => ( + {row.original.abk.name} + ), + }, + { + accessorKey: 'phase', + header: 'Phase', + enableSorting: false, + cell: ({ row }) => row.original.phase, + }, + { + header: `Tanggal - ${MONTH_OPTIONS[Number(tableFilterState.bulan) - 1].label} - ${tableFilterState.tahun}`, + columns: reportDateColumns, + }, + { + accessorKey: 'summary.total_checklist', + header: 'Total Checklist', + enableSorting: false, + }, + { + accessorKey: 'summary.jumlah_hari_efektif', + header: 'Jumlah Hari Efektif', + enableSorting: false, + }, + { + accessorKey: 'summary.abk_percentage', + header: 'ABK (%)', + enableSorting: false, + }, + { + accessorKey: 'summary.kandang_percentage', + header: 'Kandang (%)', + enableSorting: false, + }, + { + header: 'Kategori', + columns: [ + { + accessorKey: 'summary.kategori.kurang', + header: 'Kurang', + enableSorting: false, + cell: ({ row }) => ( + + {row.original.summary.kategori.kurang} + + ), + }, + { + accessorKey: 'summary.kategori.cukup', + header: 'Cukup', + enableSorting: false, + cell: ({ row }) => row.original.summary.kategori.cukup, + }, + { + accessorKey: 'summary.kategori.baik', + header: 'Baik', + enableSorting: false, + cell: ({ row }) => ( + + {row.original.summary.kategori.baik} + + ), + }, + ], + }, + ]; + + // const exportToCSV = () => { + // toast.info('Export CSV akan segera tersedia'); + // }; + + const monthChangeHandler = (value: string) => updateFilter('bulan', value); + const yearChangeHandler = (value: string) => updateFilter('tahun', value); + + const areaChangeHandler = (value: string) => { + updateFilter('area_id', value === 'ALL' ? '' : value); + updateFilter('location_id', ''); + updateFilter('kandang_id', ''); + updateFilter('employee_id', ''); }; - const handleViewDetail = (checklistId: string) => { - // Navigate to detail page (same as List Daily Checklist) - router.push(`/list-daily-checklist/detail?checklistId=${checklistId}`); + const locationChangeHandler = (value: string) => { + updateFilter('location_id', value === 'ALL' ? '' : value); + updateFilter('kandang_id', ''); + updateFilter('employee_id', ''); }; - const exportToCSV = () => { - toast.info('Export CSV akan segera tersedia'); + const kandangChangeHandler = (value: string) => { + updateFilter('kandang_id', value === 'ALL' ? '' : value); + updateFilter('employee_id', ''); + }; + + const phaseChangeHandler = (value: string) => { + updateFilter('phase_id', value); + }; + + const employeeChangeHandler = (value: string) => { + updateFilter('employee_id', value === 'ALL' ? '' : value); }; return ( @@ -380,13 +306,13 @@ export function DailyChecklistReportsContent() { Laporan lengkap checklist harian

- + */} {/* Main Card */} @@ -395,22 +321,100 @@ export function DailyChecklistReportsContent() { {/* Filters Section */}
- -
- { - setDateFrom(from); - setDateTo(to); - }} - /> -
+ + +
+
+ + +
+
+ + +
+
+ +
-
- Semua Kandang - {kandangList.map((kandang) => ( - - {kandang.name} + {kandangOptions.map((kandang) => ( + + {kandang.label} ))}
-
- - - + - {STATUS_OPTIONS.map((option) => ( - - {option.label} + Semua Phase + {phaseOptions.map((phase) => ( + + {phase.label} ))}
-
- -
- setSearchText(e.target.value)} - className='border-gray-200 pl-9' - /> - -
+ +
{/* Reports Table */} - {loading ? ( -
- Memuat data... -
- ) : filteredReportList.length > 0 ? ( -
- - - - - - - - - - - - - - - - - {filteredReportList.map((item, index) => ( - - - - - - - - - - - - - ))} - -
- Tanggal - - Kandang - - Kategori - - Status - - Phase - - Aktivitas - - ABK - - Progress - - Updated At - - Aksi -
- {formatDate(item.date)} - - {item.kandang_name} - - {CATEGORY_LABELS[item.category] || item.category} - - {getStatusBadge(item.status)} - - {item.total_phases} - - {item.total_activities} - - {item.total_employees} - -
-
-
-
- - {item.progress_percent}% - -
-
- {formatDateTime(item.updated_at)} - -
- -
-
-
- ) : ( -
- {searchText || - dateFrom || - dateTo || - statusFilter !== 'ALL' || - kandangFilter !== 'ALL' - ? 'Tidak ada data yang sesuai dengan filter' - : 'Belum ada data checklist'} -
- )} + + data={ + isResponseSuccess(reportResponse) + ? reportResponse.data || [] + : [] + } + columns={reportColumns} + isLoading={isLoadingReport} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(reportResponse) + ? reportResponse?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(reportResponse) + ? reportResponse?.meta?.total_results + : 0 + } + onPageChange={setPage} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(reportResponse) && + reportResponse?.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 border-x border-base-content/10', + bodyColumnClassName: + 'px-4 py-3 text-base-content border-x border-base-content/10', + paginationClassName: 'px-4', + }} + /> From aec5c8997988a1503ece029dcb1c90d1810560c1 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 12:56:42 +0700 Subject: [PATCH 04/29] feat: create Daily Checklist Report type --- .../api/daily-checklist/daily-checklist.d.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/types/api/daily-checklist/daily-checklist.d.ts b/src/types/api/daily-checklist/daily-checklist.d.ts index b88a34aa..017b16b6 100644 --- a/src/types/api/daily-checklist/daily-checklist.d.ts +++ b/src/types/api/daily-checklist/daily-checklist.d.ts @@ -2,6 +2,9 @@ import { BaseMetadata } from '@/types/api/api-general'; import { BaseKandang } from '@/types/api/master-data/kandang'; import { Phase } from '@/types/api/daily-checklist/phase'; import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity'; +import { BaseArea } from '@/types/api/master-data/area'; +import { BaseLocation } from '@/types/api/master-data/location'; +import { BaseEmployee } from '@/types/api/master-data/employee'; export type BaseDailyChecklist = { id: number; @@ -80,3 +83,23 @@ export type DailyChecklistSummary = { performance_overview: PerformanceOverviewItem[]; tracking_abk: TrackingAbkItem[]; }; + +export type DailyChecklistReport = { + area: Pick; + farm: Pick; + kandang: Pick; + abk: Pick; + phase: string; + daily_activities: Record; + summary: { + total_checklist: number; + jumlah_hari_efektif: number; + abk_percentage: number; + kandang_percentage: number; + kategori: { + kurang: number; + cukup: number; + baik: number; + }; + }; +}; From f84fcb78b8698aec0cc08a0e740bce9eaeaf17ae Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 13:04:46 +0700 Subject: [PATCH 05/29] chore: delete supabase related code --- src/figma-make/lib/info.tsx | 6 - src/figma-make/lib/supabase.ts | 339 --------------------------------- 2 files changed, 345 deletions(-) delete mode 100644 src/figma-make/lib/info.tsx delete mode 100644 src/figma-make/lib/supabase.ts diff --git a/src/figma-make/lib/info.tsx b/src/figma-make/lib/info.tsx deleted file mode 100644 index 3127f912..00000000 --- a/src/figma-make/lib/info.tsx +++ /dev/null @@ -1,6 +0,0 @@ -// TODO: delete this file later - -/* AUTOGENERATED FILE - DO NOT EDIT CONTENTS */ - -export const projectId = 'xxx'; -export const publicAnonKey = 'xxx'; diff --git a/src/figma-make/lib/supabase.ts b/src/figma-make/lib/supabase.ts deleted file mode 100644 index 0b693389..00000000 --- a/src/figma-make/lib/supabase.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { projectId, publicAnonKey } from '@/figma-make/lib/info'; - -// ============================================ -// 🔍 SUPABASE ENVIRONMENT DEBUG CHECK -// ============================================ - -/** - * Get environment variable from multiple sources - * Checks in order: __ENV__, window.__ENV__, process.env, import.meta.env - */ -function getEnv(key: string): string | undefined { - let value: string | undefined; - let source: string | undefined; - - // Check globalThis.__ENV__ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((globalThis as any).__ENV__?.[key]) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value = (globalThis as any).__ENV__[key]; - source = 'globalThis.__ENV__'; - } - // Check window.__ENV__ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - else if (typeof window !== 'undefined' && (window as any).__ENV__?.[key]) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value = (window as any).__ENV__[key]; - source = 'window.__ENV__'; - } - // Check process.env - // eslint-disable-next-line @typescript-eslint/no-explicit-any - else if ((globalThis as any).process?.env?.[key]) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value = (globalThis as any).process.env[key]; - source = 'process.env'; - } - // Check import.meta.env (if available) - else if ( - typeof import.meta !== 'undefined' && - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (import.meta as any)?.env?.[key] - ) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value = (import.meta as any).env[key]; - source = 'import.meta.env'; - } - - if (value && source) { - console.log(`✅ ${key} loaded from: ${source}`); - } - - return value; -} - -// Try to read from environment variables first -let supabaseUrl = getEnv('VITE_SUPABASE_URL'); -let supabaseAnonKey = getEnv('VITE_SUPABASE_ANON_KEY'); - -// Fallback to Figma Make autogenerated credentials -if (!supabaseUrl || !supabaseAnonKey) { - console.log( - '📋 Using Figma Make autogenerated Supabase credentials from /utils/supabase/info.tsx' - ); - supabaseUrl = `https://${projectId}.supabase.co`; - supabaseAnonKey = publicAnonKey; -} - -// Helper function to mask sensitive data -const maskString = (str: string | undefined): string => { - if (!str) return 'undefined'; - if (str.length <= 20) return str.substring(0, 10) + '...'; - return str.substring(0, 20) + '...' + `(${str.length - 20} chars masked)`; -}; - -// Debug logging -console.group('🔍 Supabase Environment Check'); -console.log('projectId (from info.tsx):', projectId); -console.log('SUPABASE_URL present?', !!supabaseUrl); -console.log('SUPABASE_KEY present?', !!supabaseAnonKey); -console.log('SUPABASE_URL value:', maskString(supabaseUrl)); -console.log('SUPABASE_KEY value:', maskString(supabaseAnonKey)); -console.groupEnd(); - -// Check if Supabase is configured -export const isSupabaseConfigured = () => { - return !!(supabaseUrl && supabaseAnonKey); -}; - -// Create Supabase client or throw error -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let supabase: SupabaseClient; - -if (isSupabaseConfigured()) { - console.log('✅ Creating real Supabase client...'); - supabase = createClient(supabaseUrl!, supabaseAnonKey!); - console.log('✅ Supabase client created successfully!'); -} else { - const errorMessage = ` -❌ SUPABASE CONFIGURATION ERROR ❌ - -Missing required environment variables: -- VITE_SUPABASE_URL: ${!!supabaseUrl ? '✅ Present' : '❌ Missing'} -- VITE_SUPABASE_ANON_KEY: ${!!supabaseAnonKey ? '✅ Present' : '❌ Missing'} - -Please set Supabase environment variables in: -→ Figma Make Supabase integration settings -→ Deployment settings/environment configuration - -The app checked the following sources: -- globalThis.__ENV__ -- window.__ENV__ -- process.env -- import.meta.env - -None of these sources contained the required variables. - `.trim(); - - console.error(errorMessage); - throw new Error(errorMessage); -} - -export { supabase }; - -// Database types -export interface Database { - public: { - Tables: { - kandang: { - Row: { - id: string; - name: string; - created_at?: string; - }; - Insert: { - id?: string; - name: string; - created_at?: string; - }; - Update: { - id?: string; - name?: string; - created_at?: string; - }; - }; - employees: { - Row: { - id: string; - name: string; - kandang_id: string; - is_active: boolean; - created_at?: string; - }; - Insert: { - id?: string; - name: string; - kandang_id: string; - is_active?: boolean; - created_at?: string; - }; - Update: { - id?: string; - name?: string; - kandang_id?: string; - is_active?: boolean; - created_at?: string; - }; - }; - phases: { - Row: { - id: string; - name: string; - created_at?: string; - updated_at?: string; - }; - Insert: { - id?: string; - name: string; - created_at?: string; - updated_at?: string; - }; - Update: { - id?: string; - name?: string; - created_at?: string; - updated_at?: string; - }; - }; - phase_activities: { - Row: { - id: string; - phase_id: string; - name: string; - description?: string; - created_at?: string; - updated_at?: string; - }; - Insert: { - id?: string; - phase_id: string; - name: string; - description?: string; - created_at?: string; - updated_at?: string; - }; - Update: { - id?: string; - phase_id?: string; - name?: string; - description?: string; - created_at?: string; - updated_at?: string; - }; - }; - checklists: { - Row: { - id: string; - name: string; - description?: string; - phase_id: string; - created_at?: string; - updated_at?: string; - }; - Insert: { - id?: string; - name: string; - description?: string; - phase_id: string; - created_at?: string; - updated_at?: string; - }; - Update: { - id?: string; - name?: string; - description?: string; - phase_id?: string; - created_at?: string; - updated_at?: string; - }; - }; - daily_checklists: { - Row: { - id: string; - date: string; - kandang_id: string; - checklist_id: string; - category: string; - status: string; - name?: string; - total_score?: number; - document_path?: string; - reject_reason?: string; - created_by: string; - created_at?: string; - updated_at?: string; - }; - Insert: { - id?: string; - date: string; - kandang_id: string; - checklist_id: string; - category: string; - status?: string; - name?: string; - total_score?: number; - document_path?: string; - reject_reason?: string; - created_at?: string; - updated_at?: string; - }; - Update: { - id?: string; - date?: string; - kandang_id?: string; - checklist_id?: string; - category?: string; - status?: string; - name?: string; - total_score?: number; - document_path?: string; - reject_reason?: string; - created_at?: string; - updated_at?: string; - }; - }; - daily_checklist_tasks: { - Row: { - id: string; - checklist_id: string; - activity_id: string; - notes?: string; - created_at?: string; - updated_at?: string; - }; - Insert: { - id?: string; - checklist_id: string; - activity_id: string; - notes?: string; - created_at?: string; - updated_at?: string; - }; - Update: { - id?: string; - checklist_id?: string; - activity_id?: string; - notes?: string; - created_at?: string; - updated_at?: string; - }; - }; - task_assignees: { - Row: { - id: string; - task_id: string; - employee_id: string; - is_completed: boolean; - created_at?: string; - updated_at?: string; - }; - Insert: { - id?: string; - task_id: string; - employee_id: string; - is_completed?: boolean; - created_at?: string; - updated_at?: string; - }; - Update: { - id?: string; - task_id?: string; - employee_id?: string; - is_completed?: boolean; - created_at?: string; - updated_at?: string; - }; - }; - }; - }; -} From 536b1c5b01a00ea6cab54c9cdc6e9e091a3c9620 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 13:05:16 +0700 Subject: [PATCH 06/29] chore: uninstall supabase --- package-lock.json | 152 ++-------------------------------------------- package.json | 1 - 2 files changed, 6 insertions(+), 147 deletions(-) diff --git a/package-lock.json b/package-lock.json index a20c8c4d..38844543 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.1.0", "dependencies": { "@react-pdf/renderer": "^4.3.1", - "@supabase/supabase-js": "^2.89.0", "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-table": "^8.21.3", "axios": "^1.12.2", @@ -3951,86 +3950,6 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, - "node_modules/@supabase/auth-js": { - "version": "2.89.0", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.89.0.tgz", - "integrity": "sha512-wiWZdz8WMad8LQdJMWYDZ2SJtZP5MwMqzQq3ehtW2ngiI3UTgbKiFrvMUUS3KADiVlk4LiGfODB2mrYx7w2f8w==", - "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/functions-js": { - "version": "2.89.0", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.89.0.tgz", - "integrity": "sha512-XEueaC5gMe5NufNYfBh9kPwJlP5M2f+Ogr8rvhmRDAZNHgY6mI35RCkYDijd92pMcNM7g8pUUJov93UGUnqfyw==", - "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/postgrest-js": { - "version": "2.89.0", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.89.0.tgz", - "integrity": "sha512-/b0fKrxV9i7RNOEXMno/I1862RsYhuUo+Q6m6z3ar1f4ulTMXnDfv0y4YYxK2POcgrOXQOgKYQx1eArybyNvtg==", - "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/realtime-js": { - "version": "2.89.0", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.89.0.tgz", - "integrity": "sha512-aMOvfDb2a52u6PX6jrrjvACHXGV3zsOlWRzZsTIOAJa0hOVvRp01AwC1+nLTGUzxzezejrYeCX+KnnM1xHdl+w==", - "license": "MIT", - "dependencies": { - "@types/phoenix": "^1.6.6", - "@types/ws": "^8.18.1", - "tslib": "2.8.1", - "ws": "^8.18.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/storage-js": { - "version": "2.89.0", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.89.0.tgz", - "integrity": "sha512-6zKcXofk/M/4Eato7iqpRh+B+vnxeiTumCIP+Tz26xEqIiywzD9JxHq+udRrDuv6hXE+pmetvJd8n5wcf4MFRQ==", - "license": "MIT", - "dependencies": { - "iceberg-js": "^0.8.1", - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/supabase-js": { - "version": "2.89.0", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.89.0.tgz", - "integrity": "sha512-KlaRwSfFA0fD73PYVMHj5/iXFtQGCcX7PSx0FdQwYEEw9b2wqM7GxadY+5YwcmuEhalmjFB/YvqaoNVF+sWUlg==", - "license": "MIT", - "dependencies": { - "@supabase/auth-js": "2.89.0", - "@supabase/functions-js": "2.89.0", - "@supabase/postgrest-js": "2.89.0", - "@supabase/realtime-js": "2.89.0", - "@supabase/storage-js": "2.89.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -4471,6 +4390,7 @@ "version": "20.19.23", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz", "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4488,12 +4408,6 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, - "node_modules/@types/phoenix": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", - "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", - "license": "MIT" - }, "node_modules/@types/raf": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", @@ -4506,7 +4420,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4517,7 +4430,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4544,15 +4456,6 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", @@ -4599,7 +4502,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -5123,7 +5025,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5829,8 +5730,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", @@ -6206,8 +6106,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -6468,7 +6367,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6642,7 +6540,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7519,15 +7416,6 @@ "integrity": "sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==", "license": "ISC" }, - "node_modules/iceberg-js": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", - "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", - "license": "MIT", - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8160,7 +8048,6 @@ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz", "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", @@ -9380,7 +9267,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9411,7 +9297,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9479,8 +9364,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-number-format": { "version": "5.4.4", @@ -9497,7 +9381,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -9666,8 +9549,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -10533,7 +10415,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10701,7 +10582,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10733,6 +10613,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, "license": "MIT" }, "node_modules/unicode-properties": { @@ -11064,27 +10945,6 @@ "node": ">=0.10.0" } }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xlsx": { "version": "0.20.3", "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", diff --git a/package.json b/package.json index aa90e44a..3a775db2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ }, "dependencies": { "@react-pdf/renderer": "^4.3.1", - "@supabase/supabase-js": "^2.89.0", "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-table": "^8.21.3", "axios": "^1.12.2", From 8afd8c63826289c67df79e3e52ac333a74d0c1bd Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 14:41:34 +0700 Subject: [PATCH 07/29] feat: create master daily checklist configuration page --- .../master-data/configuration/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/daily-checklist/master-data/configuration/page.tsx diff --git a/src/app/daily-checklist/master-data/configuration/page.tsx b/src/app/daily-checklist/master-data/configuration/page.tsx new file mode 100644 index 00000000..7b55c2ea --- /dev/null +++ b/src/app/daily-checklist/master-data/configuration/page.tsx @@ -0,0 +1,11 @@ +import { MasterConfigurationContent } from '@/figma-make/components/pages/master-data/configuration/MasterConfigurationContent'; + +const MasterConfigurationPage = () => { + return ( +
+ +
+ ); +}; + +export default MasterConfigurationPage; From 404019f181eac1307ce7b45c82756336147babdf Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 14:41:46 +0700 Subject: [PATCH 08/29] feat: add Konfigurasi menu --- src/config/constant.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index f7f2255e..e4409748 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -60,6 +60,12 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ // TODO: add permission // permission: ['lti.daily_checklist.list'], }, + { + text: 'Konfigurasi', + link: '/daily-checklist/master-data/configuration', + // TODO: add permission + // permission: ['lti.daily_checklist.list'], + }, ], }, ], From c099314bdb65f4596a9e0f62ea25e3e02980fa38 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 14:42:02 +0700 Subject: [PATCH 09/29] feat: add route permission for daily checklist master configuration --- src/config/route-permission.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index ca720f28..9ebc566b 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -20,6 +20,7 @@ export const ROUTE_PERMISSIONS: Record = { '/daily-checklist/reports/': ['lti.dashboard.list'], '/daily-checklist/master-data/employee/': ['lti.dashboard.list'], '/daily-checklist/master-data/activity/': ['lti.dashboard.list'], + '/daily-checklist/master-data/configuration/': ['lti.dashboard.list'], // Production // Production - Project Flock From dfac7f84ff7af96b5a5276600c7e77ade50ad037 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 14:42:14 +0700 Subject: [PATCH 10/29] feat: create MasterConfigurationContent component --- .../MasterConfigurationContent.tsx | 564 ++++++++++++++++++ 1 file changed, 564 insertions(+) create mode 100644 src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx diff --git a/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx b/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx new file mode 100644 index 00000000..1358d6ba --- /dev/null +++ b/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx @@ -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[] = [ + { + 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 }) => ( + + + + + + handleEdit(row.original)}> + + Edit + + handleDeleteClick(row.original.id)} + className='text-red-600' + > + + Hapus + + + + ), + }, + ]; + + 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 ( +
+
+
+

+ Master Konfigurasi +

+

+ Master Data • Konfigurasi +

+
+ + + Memuat data... + + +
+
+ ); + } + + 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 ( +
+
+ {/* Page Title */} +
+

+ Master Konfigurasi +

+

+ Master Data • Konfigurasi +

+
+ + {/* Main Card */} + + + {/* Single Toolbar Row */} +
+
+ +
+
+ + {/* Table */} + + 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', + }} + /> +
+
+
+ + {/* Add/Edit Modal */} + + + + + {modalMode === 'create' + ? 'Tambah Konfigurasi' + : 'Edit Konfigurasi'} + + + {modalMode === 'create' + ? 'Masukkan detail konfigurasi baru' + : 'Ubah detail konfigurasi'} + + +
+
+ +
+ + setConfigurationForm({ + ...configurationForm, + date: e, + }) + } + disabled={loading} + placeholder='Pilih tanggal' + formatDisplay={formatDateForDisplay} + /> +
+
+ +
+ +
+ +
+ + +
+ + + {'<='} + + + setConfigurationForm({ + ...configurationForm, + percentage_threshold_bad: e.target.value, + }) + } + placeholder='Kurang' + className='w-20' + disabled={loading} + max={100} + /> +
+
+ +
+ + +
+ + + {'<='} + + + setConfigurationForm({ + ...configurationForm, + percentage_threshold_enough: e.target.value, + }) + } + placeholder='Cukup' + className='w-20' + disabled={loading} + min={Number(configurationForm.percentage_threshold_bad) + 1} + max={100} + /> +
+
+ +
+ + +
+ + + {'<='} + + +
+
+
+ + + + +
+
+ + {/* Delete Confirmation */} + + + + Hapus konfigurasi? + + Data konfigurasi akan dihapus secara permanen. + + + + Batal + + {loading ? 'Menghapus...' : 'Hapus'} + + + + +
+ ); +} From 25352659f33eac32f86050ece001531256cfb3dd Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 14:42:33 +0700 Subject: [PATCH 11/29] feat: create Daily Checklist Configuration Api Service --- .../api/daily-checklist/configuration.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/services/api/daily-checklist/configuration.ts diff --git a/src/services/api/daily-checklist/configuration.ts b/src/services/api/daily-checklist/configuration.ts new file mode 100644 index 00000000..27331aee --- /dev/null +++ b/src/services/api/daily-checklist/configuration.ts @@ -0,0 +1,19 @@ +import { BaseApiService } from '@/services/api/base'; +import { + CreateDailyChecklistConfigurationPayload, + DailyChecklistConfiguration, + UpdateDailyChecklistConfigurationPayload, +} from '@/types/api/daily-checklist/configuration'; + +export class DailyChecklistConfigurationApiService extends BaseApiService< + DailyChecklistConfiguration, + CreateDailyChecklistConfigurationPayload, + UpdateDailyChecklistConfigurationPayload +> { + constructor(basePath: string = '/master-data/config-checklists') { + super(basePath); + } +} + +export const DailyChecklistConfigurationApi = + new DailyChecklistConfigurationApiService('/master-data/config-checklists'); From 1002c6c437f4a9f9c40b0fbfd74c7382aa8143e4 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 14:42:41 +0700 Subject: [PATCH 12/29] feat: create Daily Checklist Configuration type --- .../api/daily-checklist/configuration.d.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/types/api/daily-checklist/configuration.d.ts diff --git a/src/types/api/daily-checklist/configuration.d.ts b/src/types/api/daily-checklist/configuration.d.ts new file mode 100644 index 00000000..7e94c69b --- /dev/null +++ b/src/types/api/daily-checklist/configuration.d.ts @@ -0,0 +1,22 @@ +import { BaseMetadata } from '@/types/api/api-general'; + +export type BaseConfiguration = { + id: number; + date: string; + percentage_threshold_bad: number; + percentage_threshold_enough: number; +}; + +export type DailyChecklistConfiguration = BaseMetadata & BaseConfiguration; + +export type CreateDailyChecklistConfigurationPayload = { + date: string; + percentage_threshold_bad: number; + percentage_threshold_enough: number; +}; + +export type UpdateDailyChecklistConfigurationPayload = { + date: string; + percentage_threshold_bad: number; + percentage_threshold_enough: number; +}; From 9245285fe211acab85c0cafcd64780b828f32408 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 17:30:30 +0700 Subject: [PATCH 13/29] feat: add upload document in daily checklist --- .../daily-checklist/DailyChecklistContent.tsx | 119 +++++++++++++++++- 1 file changed, 117 insertions(+), 2 deletions(-) diff --git a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx index ae704e92..7bd0be83 100644 --- a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx +++ b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx @@ -30,7 +30,7 @@ import { KandangApi } from '@/services/api/master-data'; import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import useSWR from 'swr'; -import { BaseApiResponse } from '@/types/api/api-general'; +import { BaseApiResponse, Document } from '@/types/api/api-general'; import { AxiosError } from 'axios'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; import { PhaseApi } from '@/services/api/daily-checklist/phase'; @@ -39,6 +39,9 @@ import { Employee } from '@/types/api/daily-checklist/employee'; import { PhaseActivityApi } from '@/services/api/daily-checklist/phase-activity'; 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 { Icon } from '@iconify/react'; // Static categories const CATEGORIES = [ @@ -148,6 +151,10 @@ export function DailyChecklistContent() { const [loading, setLoading] = useState(false); const [initialLoading, setInitialLoading] = useState(true); + const [existingDocuments, setExistingDocuments] = useState([]); + const [documents, setDocuments] = useState([]); + const [deletedDocumentIds, setDeletedDocumentIds] = useState([]); + // Format date for display const formatDateForDisplay = (dateStr: string) => { if (!dateStr) return 'Pilih tanggal'; @@ -340,6 +347,9 @@ export function DailyChecklistContent() { return; } + // set existing document + setExistingDocuments(existingDailyChecklist?.data.document_urls || []); + // Build assignments map const assignmentMap: { [taskId: string]: { @@ -729,7 +739,11 @@ export function DailyChecklistContent() { setLoading(true); try { - const submitRes = await DailyChecklistApi.submit(dailyChecklistId); + const submitRes = await DailyChecklistApi.submit( + dailyChecklistId, + documents, + deletedDocumentIds + ); if (isResponseError(submitRes)) { console.error('Error submitting:', submitRes.message); @@ -750,6 +764,19 @@ export function DailyChecklistContent() { const handleSaveDraft = async () => { if (!dailyChecklistId) return; + const uploadImageRes = await DailyChecklistApi.uploadImage( + Number(dailyChecklistId), + 'DRAFT', + documents, + deletedDocumentIds + ); + + if (isResponseError(uploadImageRes)) { + console.error('Error saving draft:', uploadImageRes.message); + toast.error('Gagal menyimpan draft'); + return; + } + toast.success('Draft tersimpan otomatis'); }; @@ -1263,6 +1290,94 @@ export function DailyChecklistContent() { )} + {dailyChecklistId && + selectedPhaseIds.length > 0 && + selectedEmployees.length > 0 && ( + <> + {existingDocuments.length > 0 && ( +
+

+ Dokumen yang telah diupload +

+ {existingDocuments.map( + (existingDocument, existingDocumentIdx) => ( +
+ + {existingDocument.name}{' '} + + + + +
+ ) + )} +
+ )} + + { + setDocuments(files); + }} + onDelete={(deletedFileIdx: number) => { + const newRequestDocuments = [...documents]; + + newRequestDocuments?.splice(deletedFileIdx, 1); + + setDocuments(newRequestDocuments); + }} + className={{ + wrapper: 'mt-6', + inputWrapper: 'flex items-center', + label: 'font-semibold text-gray-900', + }} + /> + + )} + {/* Action Buttons */} {dailyChecklistId && selectedPhaseIds.length > 0 && From a24f51dad35a82e5e590008e99d3cea5df8887d4 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 17:30:44 +0700 Subject: [PATCH 14/29] feat: show existing document --- .../detail/DetailDailyChecklistContent.tsx | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx index 54e4c93f..a867c29d 100644 --- a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx @@ -20,6 +20,9 @@ import { toast } from 'sonner'; import { useRouter, useSearchParams } from 'next/navigation'; import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist'; import { isResponseError } from '@/lib/api-helper'; +import Link from 'next/link'; +import { Icon } from '@iconify/react'; +import { Document } from '@/types/api/api-general'; interface ChecklistDetailRow { checklist_id: string; @@ -125,6 +128,7 @@ export function DetailDailyChecklistContent() { const [employees, setEmployees] = useState<{ id: string; name: string }[]>( [] ); + const [documents, setDocuments] = useState([]); // Modals const [showApproveModal, setShowApproveModal] = useState(false); @@ -160,6 +164,8 @@ export function DetailDailyChecklistContent() { const rawDetailChecklist = checklistDataRes?.data; + setDocuments(rawDetailChecklist?.document_urls || []); + const checklistData = { id: rawDetailChecklist?.id, date: rawDetailChecklist?.date, @@ -842,6 +848,37 @@ export function DetailDailyChecklistContent() { Tidak ada data aktivitas )} + + {documents.length > 0 && ( +
+

+ Dokumen yang telah diupload +

+ +
    + {documents.map((existingDocument, existingDocumentIdx) => ( +
  • +
    + + {existingDocument.name}{' '} + + +
    +
  • + ))} +
+
+ )} From 10e843aebf780c6b486a7d26d614b4aa7172125a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 17:31:00 +0700 Subject: [PATCH 15/29] chore: refresh phase after modifying activity --- .../pages/master-data/activity/MasterAktivitasContent.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/figma-make/components/pages/master-data/activity/MasterAktivitasContent.tsx b/src/figma-make/components/pages/master-data/activity/MasterAktivitasContent.tsx index a5b8ac3d..36774ce2 100644 --- a/src/figma-make/components/pages/master-data/activity/MasterAktivitasContent.tsx +++ b/src/figma-make/components/pages/master-data/activity/MasterAktivitasContent.tsx @@ -328,6 +328,7 @@ export function MasterAktivitasContent() { return; } + refreshPhases(); refreshPhaseActivities(); toast.success('Aktivitas berhasil ditambahkan'); } else { @@ -349,6 +350,7 @@ export function MasterAktivitasContent() { return; } + refreshPhases(); refreshPhaseActivities(); toast.success('Aktivitas berhasil diubah'); } @@ -387,6 +389,7 @@ export function MasterAktivitasContent() { return; } + refreshPhases(); refreshPhaseActivities(); toast.success('Aktivitas berhasil dihapus'); setShowActivityDeleteConfirm(false); From 70eac011f37fc4d8942ded42393081c54c860933 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 17:31:07 +0700 Subject: [PATCH 16/29] chore: remove export button --- .../employee/MasterEmployeeContent.tsx | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx b/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx index c9562971..f8b67e7a 100644 --- a/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx +++ b/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx @@ -283,10 +283,6 @@ export function MasterEmployeeContent() { } }; - const handleExport = (format: string) => { - toast.success(`Data berhasil diekspor ke ${format}`); - }; - if (isLoadingEmployees && !employees) { return (
@@ -390,27 +386,6 @@ export function MasterEmployeeContent() { {/* RIGHT: Export + Add */}
- - - - - - handleExport('CSV')}> - Export CSV - - handleExport('Excel')}> - Export Excel - - - -
- {/* */} +
{/* Main Card */} From a891608d13efe188aab52a34a65573a20bca5d89 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 17:31:55 +0700 Subject: [PATCH 18/29] feat: add uploadImage and exportDailyChecklistReportToExcel method --- .../api/daily-checklist/daily-checklist.ts | 155 ++++++++++++++++-- 1 file changed, 141 insertions(+), 14 deletions(-) diff --git a/src/services/api/daily-checklist/daily-checklist.ts b/src/services/api/daily-checklist/daily-checklist.ts index 48b789a8..b8f72201 100644 --- a/src/services/api/daily-checklist/daily-checklist.ts +++ b/src/services/api/daily-checklist/daily-checklist.ts @@ -1,13 +1,18 @@ import axios from 'axios'; +import * as XLSX from 'xlsx'; import { BaseApiService } from '@/services/api/base'; -import { httpClient } from '@/services/http/client'; +import { httpClient, httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; import { CreateDailyChecklistPayload, DailyChecklist, + DailyChecklistReport, DetailDailyChecklist, } from '@/types/api/daily-checklist/daily-checklist'; +import { isResponseError } from '@/lib/api-helper'; +import { toast } from 'sonner'; +import { formatDate } from '@/lib/helper'; export class DailyChecklistApiService extends BaseApiService< DailyChecklist, @@ -134,15 +139,26 @@ export class DailyChecklistApiService extends BaseApiService< } } - async submit(id: string) { + async submit( + id: string, + files: File[] = [], + deletedDocumentIds: number[] = [] + ) { try { + const formData = new FormData(); + + formData.append('status', 'SUBMITTED'); + + formData.append('reject_reason', ''); + + files.forEach((file) => formData.append(`documents`, file)); + + formData.append('deleted_document_ids', deletedDocumentIds.join(',')); + const submitPath = `${this.basePath}/${id}`; const submitRes = await httpClient(submitPath, { method: 'PATCH', - body: { - status: 'SUBMITTED', - reject_reason: '', - }, + body: formData, }); return submitRes; @@ -156,13 +172,16 @@ export class DailyChecklistApiService extends BaseApiService< async approve(id: string) { try { + const formData = new FormData(); + + formData.append('status', 'APPROVED'); + + formData.append('reject_reason', ''); + const approvePath = `${this.basePath}/${id}`; const approveRes = await httpClient(approvePath, { method: 'PATCH', - body: { - status: 'APPROVED', - reject_reason: '', - }, + body: formData, }); return approveRes; @@ -176,13 +195,16 @@ export class DailyChecklistApiService extends BaseApiService< async reject(id: string, rejectReason: string) { try { + const formData = new FormData(); + + formData.append('status', 'REJECTED'); + + formData.append('reject_reason', rejectReason); + const rejectPath = `${this.basePath}/${id}`; const rejectRes = await httpClient(rejectPath, { method: 'PATCH', - body: { - status: 'REJECTED', - reject_reason: rejectReason, - }, + body: formData, }); return rejectRes; @@ -193,6 +215,111 @@ export class DailyChecklistApiService extends BaseApiService< return undefined; } } + + async uploadImage( + id: number, + status: string, + files: File[], + deletedDocumentIds: number[] = [] + ) { + try { + const formData = new FormData(); + + formData.append('status', status); + + files.forEach((file) => formData.append(`documents`, file)); + + formData.append('deleted_document_ids', deletedDocumentIds.join(',')); + + const uploadImagePath = `${this.basePath}/${id}`; + const uploadImageRes = await httpClient( + uploadImagePath, + { + method: 'PATCH', + body: formData, + } + ); + + return uploadImageRes; + } catch (error) { + if (axios.isAxiosError(error)) { + return error.response?.data; + } + return undefined; + } + } + + async exportDailyChecklistReportToExcel(initialQueryString: string) { + const params = new URLSearchParams(initialQueryString); + + params.set('limit', '2000'); + + const queryString = `?${params.toString()}`; + + try { + const dailyMarketingsReport = await httpClientFetcher< + BaseApiResponse + >(`${this.basePath}/report${queryString}`); + + if (isResponseError(dailyMarketingsReport)) { + toast.error('Gagal melakukan export daily checklist! Coba lagi.'); + return; + } + + const currentMonthMaxDay = new Date( + Number(params.get('tahun')), + Number(params.get('bulan')), + 0 + ).getDate(); + + const rows = dailyMarketingsReport.data; + + const formattedRows = []; + + for (let i = 0; i < rows.length; i++) { + const formattedData: Record = { + Area: rows[i].area.name, + Farm: rows[i].farm.name, + Kandang: rows[i].kandang.name, + ABK: rows[i].abk.name, + Phase: rows[i].phase, + }; + + // Add day + for (let j = 1; j <= currentMonthMaxDay; j++) { + formattedData[`Day ${j}`] = rows[i].daily_activities[`${j}`]; + } + + // add summary + formattedData['Total Checklist'] = rows[i].summary.total_checklist; + formattedData['Jumlah Hari Efektif'] = + rows[i].summary.jumlah_hari_efektif; + formattedData['ABK %'] = rows[i].summary.abk_percentage; + formattedData['Kandang %'] = rows[i].summary.kandang_percentage; + formattedData['Kategori Kurang'] = rows[i].summary.kategori.kurang; + formattedData['Kategori Cukup'] = rows[i].summary.kategori.cukup; + formattedData['Kategori Baik'] = rows[i].summary.kategori.baik; + + formattedRows.push(formattedData); + } + + const ws = XLSX.utils.json_to_sheet(formattedRows); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet( + wb, + ws, + `Daily Checklist ${params.get('tahun')}-${params.get('bulan')?.slice(0, 3)}` + ); + + // triggers download in browser + XLSX.writeFile( + wb, + `laporan-daily-checklist-${params.get('tahun')}-${params.get('bulan')}.xlsx` + ); + } catch (error) { + toast.error('Gagal melakukan export daily checklist! Coba lagi.'); + } + } } export const DailyChecklistApi = new DailyChecklistApiService( From b39202111e0bad96f1222aacfa466ef938f8dbf8 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 17:32:07 +0700 Subject: [PATCH 19/29] feat: create Document type --- src/types/api/api-general.d.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/types/api/api-general.d.ts b/src/types/api/api-general.d.ts index 5b9f04e3..d3deb616 100644 --- a/src/types/api/api-general.d.ts +++ b/src/types/api/api-general.d.ts @@ -116,3 +116,10 @@ export type BaseGroupedApproval = { export type Approvals = BaseApiResponse; export type GroupedApprovals = BaseApiResponse; + +export type Document = { + id: number; + name: string; + size: number; + url: string; +}; From 4cbd83a6fabaeae9c36db53a488f2a07b6646db9 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 12 Jan 2026 17:32:34 +0700 Subject: [PATCH 20/29] feat: add document_urls property in DetailDailyChecklist type --- src/types/api/daily-checklist/daily-checklist.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/api/daily-checklist/daily-checklist.d.ts b/src/types/api/daily-checklist/daily-checklist.d.ts index 017b16b6..5e5a3fe8 100644 --- a/src/types/api/daily-checklist/daily-checklist.d.ts +++ b/src/types/api/daily-checklist/daily-checklist.d.ts @@ -1,4 +1,4 @@ -import { BaseMetadata } from '@/types/api/api-general'; +import { BaseMetadata, Document } from '@/types/api/api-general'; import { BaseKandang } from '@/types/api/master-data/kandang'; import { Phase } from '@/types/api/daily-checklist/phase'; import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity'; @@ -49,6 +49,7 @@ export type DetailDailyChecklist = BaseDailyChecklist & { id: number; name: string; }[]; + document_urls: Document[]; }; export type CreateDailyChecklistPayload = { From 7add41ea5a8e982f3845908bfb5e693f8e5b713a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 12 Jan 2026 17:45:15 +0700 Subject: [PATCH 21/29] refactor(FE): Map approval 6 to green and export FinanceApi --- src/components/pages/expense/ExpenseStatusBadge.tsx | 4 ++++ src/services/api/report/finance-report.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/components/pages/expense/ExpenseStatusBadge.tsx b/src/components/pages/expense/ExpenseStatusBadge.tsx index a70b6454..a7fcb3e9 100644 --- a/src/components/pages/expense/ExpenseStatusBadge.tsx +++ b/src/components/pages/expense/ExpenseStatusBadge.tsx @@ -39,6 +39,10 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => { case 5: expenseStatusPillBadgeColor = 'green'; break; + + case 6: + expenseStatusPillBadgeColor = 'green'; + break; } if (isLatestApprovalRejected) { diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts index 014b9fec..61480fb1 100644 --- a/src/services/api/report/finance-report.ts +++ b/src/services/api/report/finance-report.ts @@ -64,3 +64,7 @@ export class FinanceApiService extends BaseApiService< } export const FinanceApi = new FinanceApiService('reports'); + +// export const FinanceApi = new FinanceApiService( +// 'http://localhost:4010/api/reports/finance' +// ); From e1c0701629d8d34a2371596612d08514e71e5630 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 12 Jan 2026 21:39:22 +0700 Subject: [PATCH 22/29] refactor(FE): Adapt customer payment report types and exports --- .../finance/export/CustomerPaymentExportPDF.tsx | 9 ++++----- .../finance/export/CustomerPaymentExportXLSX.tsx | 6 ++++-- .../pages/report/finance/tab/CustomerPaymentTab.tsx | 12 ++++++++---- src/services/api/report/finance-report.ts | 8 ++++---- src/types/api/report/customer-payment.d.ts | 13 ++++--------- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index e224d0f0..88c556de 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -161,10 +161,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { {customerReport.customer.name} - {customerReport.customer_address || ''} - - - NPWP: {customerReport.customer_npwp || '-'} + {customerReport.customer.address || ''} {customerReport.summary && ( @@ -266,7 +263,9 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { - {formatNumber(item.aging)} hari + + {item.aging_day ? formatNumber(item.aging_day) : '-'} hari + {item.reference || '-'} diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx index 3cc4d67a..d51aa3b7 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx @@ -30,9 +30,11 @@ export const generateCustomerPaymentExcel = ( 'Tanggal Realisasi': item.realization_date ? formatDate(item.realization_date, 'DD MMM YYYY') : '', - Aging: formatNumber(item.aging || 0), + Aging: formatNumber(item.aging_day || 0), Referensi: item.reference || '', - 'Nomor Polisi': item.vehicle_plate || '', + 'Nomor Polisi': Array.isArray(item.vehicle_plate) + ? item.vehicle_plate.join(', ') + : '', 'Ekor/Qty': formatNumber(item.qty || 0), 'Berat (Kg)': formatNumber(item.weight || 0), AVG: formatNumber(item.average_weight || 0), diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index f57f7335..1d8d1993 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -279,10 +279,14 @@ const CustomerPaymentTab = () => { { id: 'aging', header: 'Aging', - accessorKey: 'aging', + accessorKey: 'aging_day', cell: (props) => { - const value = props.row.original.aging; - return
{formatNumber(value)} hari
; + const value = props.row.original.aging_day; + return ( +
+ {value ? formatNumber(value) : '-'} hari +
+ ); }, }, { @@ -662,7 +666,7 @@ const CustomerPaymentTab = () => { Date: Tue, 13 Jan 2026 08:42:07 +0700 Subject: [PATCH 23/29] feat(FE): Add 5MB file size validation for document uploads --- src/components/pages/expense/ExpenseRealizationContent.tsx | 7 +++++++ src/components/pages/expense/ExpenseRequestContent.tsx | 7 +++++++ .../pages/expense/form/ExpenseRealizationForm.tsx | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/src/components/pages/expense/ExpenseRealizationContent.tsx b/src/components/pages/expense/ExpenseRealizationContent.tsx index ccd57ec3..ea4a0e8d 100644 --- a/src/components/pages/expense/ExpenseRealizationContent.tsx +++ b/src/components/pages/expense/ExpenseRealizationContent.tsx @@ -48,6 +48,13 @@ const ExpenseRealizationContent = ({ const realizationDocumentsChangeHandler = (val: File[]) => { formik.setFieldTouched('documents', true); + + const invalidFiles = val.filter((file) => file.size > 5 * 1024 * 1024); + if (invalidFiles.length > 0) { + toast.error('Ukuran dokumen maksimal 5 MB!'); + return; + } + formik.setFieldValue('documents', val); }; diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index 82c58341..a1ad4643 100644 --- a/src/components/pages/expense/ExpenseRequestContent.tsx +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -251,6 +251,13 @@ const ExpenseRequestContent = ({ const requestDocumentsChangeHandler = (val: File[]) => { formik.setFieldTouched('documents', true); + + const invalidFiles = val.filter((file) => file.size > 5 * 1024 * 1024); + if (invalidFiles.length > 0) { + toast.error('Ukuran dokumen maksimal 5 MB!'); + return; + } + formik.setFieldValue('documents', val); }; diff --git a/src/components/pages/expense/form/ExpenseRealizationForm.tsx b/src/components/pages/expense/form/ExpenseRealizationForm.tsx index 6526b1c1..e214e56f 100644 --- a/src/components/pages/expense/form/ExpenseRealizationForm.tsx +++ b/src/components/pages/expense/form/ExpenseRealizationForm.tsx @@ -223,6 +223,13 @@ const ExpenseRealizationForm = ({ const realizationDocumentsChangeHandler = (val: File[]) => { formik.setFieldTouched('documents', true); + + const invalidFiles = val.filter((file) => file.size > 5 * 1024 * 1024); + if (invalidFiles.length > 0) { + toast.error('Ukuran dokumen maksimal 5 MB!'); + return; + } + formik.setFieldValue('documents', val); }; From d306fad40c1b198f7a0f71d081219e1e07792b52 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 13 Jan 2026 10:19:22 +0700 Subject: [PATCH 24/29] chore: use DailyMarketingReportResponse type --- .../report/DailyMarketingReportContent.tsx | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/pages/report/DailyMarketingReportContent.tsx b/src/components/pages/report/DailyMarketingReportContent.tsx index 1eba4ea3..3ddbd6cf 100644 --- a/src/components/pages/report/DailyMarketingReportContent.tsx +++ b/src/components/pages/report/DailyMarketingReportContent.tsx @@ -31,7 +31,10 @@ import { MarketingReportApi } from '@/services/api/report/marketing-report'; import { MARKETING_TYPE_OPTIONS } from '@/config/constant'; import { httpClient } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; -import { DailyMarketingReport } from '@/types/api/report/marketing'; +import { + DailyMarketingReport, + DailyMarketingReportResponse, +} from '@/types/api/report/marketing'; import { isResponseError } from '@/lib/api-helper'; const DailyMarketingReportContent = () => { @@ -191,9 +194,10 @@ const DailyMarketingReportContent = () => { const queryString = `?${params.toString()}`; try { - const dailyMarketingsReport = await httpClient< - BaseApiResponse - >(`${MarketingReportApi.basePath}${queryString}`); + const dailyMarketingsReport = + await httpClient( + `${MarketingReportApi.basePath}${queryString}` + ); if (isResponseError(dailyMarketingsReport)) { toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); @@ -202,7 +206,10 @@ const DailyMarketingReportContent = () => { const openPdf = async () => { const dailyMarketingReportPdfBlob = await pdf( - + ).toBlob(); const dailyMarketingReportPdfUrl = URL.createObjectURL( @@ -213,7 +220,10 @@ const DailyMarketingReportContent = () => { const downloadPdf = async () => { const blob = await pdf( - + ).toBlob(); const url = URL.createObjectURL(blob); From 021df116004396d37ae8266680aab199ca4f4ec5 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 13 Jan 2026 10:20:23 +0700 Subject: [PATCH 25/29] chore: add total props in DailyMarketingReportPDFProps and adjust data type --- .../pages/report/DailyMarketingReportPDF.tsx | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/components/pages/report/DailyMarketingReportPDF.tsx b/src/components/pages/report/DailyMarketingReportPDF.tsx index 337892b3..86ee29bc 100644 --- a/src/components/pages/report/DailyMarketingReportPDF.tsx +++ b/src/components/pages/report/DailyMarketingReportPDF.tsx @@ -9,11 +9,15 @@ import { View, } from '@react-pdf/renderer'; -import { DailyMarketingReport } from '@/types/api/report/marketing'; +import { + DailyMarketingReport, + SalesSummary, +} from '@/types/api/report/marketing'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; interface DailyMarketingReportPDFProps { data?: DailyMarketingReport; + total?: SalesSummary; } const DailyMarketingReportPDFStyle = StyleSheet.create({ @@ -267,9 +271,12 @@ const DailyMarketingReportPDFStyle = StyleSheet.create({ }, }); -const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => { - const rows = data?.rows || []; - const summary = data?.summary; +const DailyMarketingReportPDF = ({ + data, + total, +}: DailyMarketingReportPDFProps) => { + const rows = data || []; + const summary = total; return ( @@ -409,7 +416,7 @@ const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => {
- {formatDate(row.do_date, 'DD/MM/YYYY')} + {formatDate(row.realization_date, 'DD/MM/YYYY')} @@ -429,7 +436,7 @@ const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => { - {row.sales} + {row.sales.name} @@ -518,6 +525,19 @@ const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => { {formatCurrency(summary?.total_sales_amount ?? 0)} + + + Total HPP Per KG: + + + {formatCurrency(summary?.total_hpp_price_per_kg ?? 0)} + + Date: Tue, 13 Jan 2026 10:21:22 +0700 Subject: [PATCH 26/29] chore: add total hpp price per kg footer --- .../pages/report/DailyMarketingsTable.tsx | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/components/pages/report/DailyMarketingsTable.tsx b/src/components/pages/report/DailyMarketingsTable.tsx index d6914cf1..6b46a001 100644 --- a/src/components/pages/report/DailyMarketingsTable.tsx +++ b/src/components/pages/report/DailyMarketingsTable.tsx @@ -60,9 +60,10 @@ const DailyMarketingsTable = ({ footer: 'Total', }, { - accessorKey: 'do_date', - header: 'Tanggal DO', - cell: (props) => formatDate(props.row.original.do_date, 'DD-MMM-YYYY'), + accessorKey: 'realization_date', + header: 'Tanggal Realisasi', + cell: (props) => + formatDate(props.row.original.realization_date, 'DD-MMM-YYYY'), }, { accessorKey: 'aging_days', @@ -106,10 +107,10 @@ const DailyMarketingsTable = ({ cell: (props) => formatNumber(props.row.original.qty), footer: () => { const totalQty = isResponseSuccess(dailyMarketings) - ? dailyMarketings.data.summary.total_qty + ? dailyMarketings?.total?.total_qty : 0; - return formatNumber(totalQty); + return totalQty ? formatNumber(totalQty) : '-'; }, }, { @@ -123,10 +124,10 @@ const DailyMarketingsTable = ({ cell: (props) => formatNumber(props.row.original.total_weight_kg), footer: () => { const totalWeightKg = isResponseSuccess(dailyMarketings) - ? dailyMarketings.data.summary.total_weight_kg + ? dailyMarketings?.total?.total_weight_kg : 0; - return formatNumber(totalWeightKg); + return totalWeightKg ? formatNumber(totalWeightKg) : '-'; }, }, { @@ -138,6 +139,13 @@ const DailyMarketingsTable = ({ accessorKey: 'hpp_price_per_kg', header: 'HPP (Rp)', cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg), + footer: () => { + const totalHppPricePerKg = isResponseSuccess(dailyMarketings) + ? dailyMarketings?.total?.total_hpp_price_per_kg + : 0; + + return totalHppPricePerKg ? formatCurrency(totalHppPricePerKg) : '-'; + }, }, { accessorKey: 'sales_amount', @@ -145,10 +153,10 @@ const DailyMarketingsTable = ({ cell: (props) => formatCurrency(props.row.original.sales_amount), footer: () => { const totalSalesAmount = isResponseSuccess(dailyMarketings) - ? dailyMarketings.data.summary.total_sales_amount + ? dailyMarketings?.total?.total_sales_amount : 0; - return formatCurrency(totalSalesAmount); + return totalSalesAmount ? formatCurrency(totalSalesAmount) : '-'; }, }, ]; @@ -167,7 +175,7 @@ const DailyMarketingsTable = ({ if (!open) { setOpen( isResponseSuccess(dailyMarketings) - ? dailyMarketings.data.rows.length > 0 + ? dailyMarketings.data.length > 0 : false ); } @@ -215,9 +223,7 @@ const DailyMarketingsTable = ({ data={ - isResponseSuccess(dailyMarketings) - ? dailyMarketings?.data.rows - : [] + isResponseSuccess(dailyMarketings) ? dailyMarketings?.data : [] } columns={dailyMarketingColumns} pageSize={pageSize} @@ -242,7 +248,7 @@ const DailyMarketingsTable = ({ containerClassName: cn({ 'w-full mb-20': isResponseSuccess(dailyMarketings) && - dailyMarketings?.data?.rows.length === 0, + dailyMarketings?.data?.length === 0, }), }} /> From f32024d19aed21a9622f4d61d8455625f576e40f Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 13 Jan 2026 10:21:31 +0700 Subject: [PATCH 27/29] chore: use DailyMarketingReportResponse --- src/services/api/report/marketing-report.ts | 26 ++++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/services/api/report/marketing-report.ts b/src/services/api/report/marketing-report.ts index 5d81605e..f55336ac 100644 --- a/src/services/api/report/marketing-report.ts +++ b/src/services/api/report/marketing-report.ts @@ -2,11 +2,14 @@ import * as XLSX from 'xlsx'; import toast from 'react-hot-toast'; import { BaseApiService } from '@/services/api/base'; -import { httpClient, httpClientFetcher } from '@/services/http/client'; +import { httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; -import { DailyMarketingReport } from '@/types/api/report/marketing'; +import { + DailyMarketingReport, + DailyMarketingReportResponse, +} from '@/types/api/report/marketing'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { formatDate, sleep } from '@/lib/helper'; +import { formatDate } from '@/lib/helper'; export class MarketingReportApiService extends BaseApiService< DailyMarketingReport, @@ -19,10 +22,8 @@ export class MarketingReportApiService extends BaseApiService< async getAllDailyMarketingFetcher( endpoint: string - ): Promise> { - return await httpClientFetcher>( - endpoint - ); + ): Promise { + return await httpClientFetcher(endpoint); } async exportDailyMarketingToExcel(initialQueryString: string) { @@ -42,16 +43,19 @@ export class MarketingReportApiService extends BaseApiService< return; } - const rows = dailyMarketingsReport.data.rows; + const rows = dailyMarketingsReport.data; const formattedRows = []; for (let i = 0; i < rows.length; i++) { formattedRows.push({ ...rows[i], - created_user: rows[i].created_user.name, - created_at: formatDate(rows[i].created_at, 'YYYY-MM-DD'), - updated_at: formatDate(rows[i].updated_at, 'YYYY-MM-DD'), + // created_user: rows[i].created_user.name, + // created_at: formatDate(rows[i].created_at, 'YYYY-MM-DD'), + // updated_at: formatDate(rows[i].updated_at, 'YYYY-MM-DD'), + so_date: formatDate(rows[i].so_date, 'YYYY-MM-DD'), + realization_date: formatDate(rows[i].realization_date, 'YYYY-MM-DD'), + sales: rows[i].sales.name, warehouse: rows[i].warehouse.name, customer: rows[i].customer.name, product: rows[i].product.name, From 4f4787e08836c84debb2d9559cf5ed989c040123 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 13 Jan 2026 10:22:19 +0700 Subject: [PATCH 28/29] chore: create DailyMarketingReportResponse --- src/types/api/report/marketing.d.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/types/api/report/marketing.d.ts b/src/types/api/report/marketing.d.ts index d1e81f77..4a0ab306 100644 --- a/src/types/api/report/marketing.d.ts +++ b/src/types/api/report/marketing.d.ts @@ -1,4 +1,4 @@ -import { BaseMetadata } from '@/types/api/api-general'; +import { BaseApiResponse, BaseMetadata } from '@/types/api/api-general'; import { BaseCustomer, Customer } from '@/types/api/master-data/customer'; import { BaseWarehouseArea, @@ -9,16 +9,17 @@ import { import { Location } from '@/types/api/master-data/location'; import { Area } from '@/types/api/master-data/area'; import { BaseProduct } from '@/types/api/master-data/product'; +import { BaseUser } from '@/types/api/user'; export type BaseDailyMarketingRow = { - no: number; - so_date: string; // e.g. "01-Dec-2025" - do_date: string; // e.g. "08-Dec-2025" + id: number; + so_date: string; + realization_date: string; aging_days: number; warehouse: BaseWarehouseArea | BaseWarehouseLocation | BaseWarehouseKandang; customer: BaseCustomer; - sales: string; + sales: BaseUser; product: BaseProduct; do_number: string; @@ -43,12 +44,13 @@ export interface SalesSummary { total_weight_kg: number; total_sales_amount: number; total_hpp_amount: number; + total_hpp_price_per_kg: number; } -export type DailyMarketingReport = { - rows: DailyMarketingRow[]; - summary: SalesSummary; -}; +export type DailyMarketingReport = DailyMarketingRow[]; + +export type DailyMarketingReportResponse = + BaseApiResponse & { total: SalesSummary }; export type MarketingReportFilters = { area_id?: number; From b486d25a8be3fb960170dc63928e4946c8061ac8 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 13 Jan 2026 10:34:54 +0700 Subject: [PATCH 29/29] chore: access sales.name --- src/components/pages/report/DailyMarketingsTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/pages/report/DailyMarketingsTable.tsx b/src/components/pages/report/DailyMarketingsTable.tsx index 6b46a001..94da96f8 100644 --- a/src/components/pages/report/DailyMarketingsTable.tsx +++ b/src/components/pages/report/DailyMarketingsTable.tsx @@ -85,6 +85,7 @@ const DailyMarketingsTable = ({ { accessorKey: 'sales', header: 'Sales/Marketing', + cell: (props) => props.row.original.sales.name, }, { accessorKey: 'vehicle_number',