feat: integrate Daily Checklist Dashboard to API integration

This commit is contained in:
ValdiANS
2026-01-12 10:46:19 +07:00
parent ebd3e14f0e
commit 334bd08e60
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { import {
Card, Card,
CardContent, CardContent,
@@ -15,7 +15,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/figma-make/components/base/select'; } from '@/figma-make/components/base/select';
import { Input } from '@/figma-make/components/base/input';
import { Badge } from '@/figma-make/components/base/badge'; import { Badge } from '@/figma-make/components/base/badge';
import { import {
Calendar as CalendarIcon, Calendar as CalendarIcon,
@@ -35,53 +34,17 @@ import {
ResponsiveContainer, ResponsiveContainer,
Cell, Cell,
} from 'recharts'; } from 'recharts';
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
import { toast } from 'sonner'; import { toast } from 'sonner';
import useSWR from 'swr';
interface EmployeePerformance { import { BaseApiResponse } from '@/types/api/api-general';
employee_id: string; import { DailyChecklistSummary } from '@/types/api/daily-checklist/daily-checklist';
employee_name: string; import { AxiosError } from 'axios';
kandang_id: string; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
kandang_name: string; import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
total_activities_in_category: number; // Total aktivitas di kategori import { KandangApi } from '@/services/api/master-data';
completed_activities: number; // Aktivitas yang sudah di-check import { useSelect } from '@/components/input/SelectInput';
completion_rate: number; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
last_activity_date: string | null; import { formatDate } from '@/lib/helper';
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;
}
const KANDANG_COLORS = [ const KANDANG_COLORS = [
'#0069e0', // Blue (primary) '#0069e0', // Blue (primary)
@@ -102,312 +65,65 @@ const CATEGORY_LABELS: { [key: string]: string } = {
}; };
export function Dashboard() { export function Dashboard() {
const [loading, setLoading] = useState(false);
const [employeePerformance, setEmployeePerformance] = useState<
EmployeePerformance[]
>([]);
// Master data
const [kandangList, setKandangList] = useState<Kandang[]>([]);
const [categoryList, setCategoryList] = useState<Category[]>([]);
// Filters // Filters
const [dateFrom, setDateFrom] = useState(''); const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState(''); const [dateTo, setDateTo] = useState('');
const [kandangFilter, setKandangFilter] = useState('ALL'); const [kandangFilter, setKandangFilter] = useState('ALL');
const [categoryFilter, setCategoryFilter] = useState('ALL'); const [categoryFilter, setCategoryFilter] = useState('ALL');
// Color mapping for kandang const {
const [kandangColorMap, setKandangColorMap] = useState<{ data: summaryResponse,
[key: string]: string; isLoading: isLoadingSummary,
}>({}); mutate: refreshSummary,
} = useSWR<
useEffect(() => { BaseApiResponse<DailyChecklistSummary | undefined>,
fetchMasterData(); AxiosError<BaseApiResponse>,
}, []); SWRHttpKey
>(
useEffect(() => { dateFrom && dateTo
// Only fetch when date filters are set ? `${DailyChecklistApi.basePath}/summary?date_from=${dateFrom}&date_to=${dateTo}&kandang_id=${kandangFilter === 'ALL' ? '' : kandangFilter}&category=${categoryFilter === 'ALL' ? '' : categoryFilter}`
if (dateFrom && dateTo) { : '',
fetchEmployeePerformance(); httpClientFetcher,
} else { {
setEmployeePerformance([]); keepPreviousData: true,
} }
}, [dateFrom, dateTo, kandangFilter, categoryFilter]); );
const fetchMasterData = async () => { const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
if (!isSupabaseConfigured()) return; useSelect(KandangApi.basePath, 'id', 'name', 'search', {
page: '1',
try { limit: '100',
// 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 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 employeePerformance = isResponseSuccess(summaryResponse)
const chartData = employeePerformance.map((emp) => ({ ? summaryResponse.data?.tracking_abk.map((abk) => {
return {
...abk,
color: kandangColorMap[abk.kandang_id] || '#0069e0',
};
})
: [];
const chartData = employeePerformance?.map((emp) => ({
name: emp.employee_name, name: emp.employee_name,
completed: emp.completed_activities, completed: emp.activity_done,
remaining: emp.total_activities_in_category - emp.completed_activities, remaining: emp.activity_left,
total: emp.total_activities_in_category, total: emp.total_activity,
color: emp.color, color: emp.color,
kandang: emp.kandang_name, kandang: emp.kandang_name,
})); }));
const hasFilters = dateFrom && dateTo;
if (summaryResponse && isResponseError(summaryResponse)) {
toast.error('Gagal memuat data: ' + summaryResponse.message);
}
return ( return (
<div className='min-h-screen'> <div className='min-h-screen'>
<div className='p-6'> <div className='p-6'>
@@ -457,9 +173,12 @@ export function Dashboard() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem> <SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangList.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem key={kandang.id} value={kandang.id}> <SelectItem
{kandang.name} key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -482,9 +201,9 @@ export function Dashboard() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value='ALL'>Semua Kategori</SelectItem> <SelectItem value='ALL'>Semua Kategori</SelectItem>
{categoryList.map((category) => ( {Object.keys(CATEGORY_LABELS).map((category) => (
<SelectItem key={category.id} value={category.id}> <SelectItem key={category} value={category}>
{category.name} {CATEGORY_LABELS[category]}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -523,11 +242,11 @@ export function Dashboard() {
melihat performance ABK. melihat performance ABK.
</p> </p>
</div> </div>
) : loading ? ( ) : isLoadingSummary ? (
<div className='text-center py-16 text-gray-500'> <div className='text-center py-16 text-gray-500'>
Memuat data... Memuat data...
</div> </div>
) : employeePerformance.length === 0 ? ( ) : employeePerformance && employeePerformance.length === 0 ? (
<div className='flex flex-col items-center justify-center py-16 text-center'> <div className='flex flex-col items-center justify-center py-16 text-center'>
<Users className='w-16 h-16 text-gray-300 mb-4' /> <Users className='w-16 h-16 text-gray-300 mb-4' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'> <h3 className='text-lg font-semibold text-gray-700 mb-2'>
@@ -582,7 +301,7 @@ export function Dashboard() {
fill='#10B981' fill='#10B981'
radius={[0, 0, 0, 0]} radius={[0, 0, 0, 0]}
> >
{chartData.map((entry, index) => ( {chartData?.map((entry, index) => (
<Cell <Cell
key={`cell-completed-${index}`} key={`cell-completed-${index}`}
fill={entry.color} fill={entry.color}
@@ -595,7 +314,7 @@ export function Dashboard() {
fill='#E5E7EB' fill='#E5E7EB'
radius={[4, 4, 0, 0]} radius={[4, 4, 0, 0]}
> >
{chartData.map((entry, index) => ( {chartData?.map((entry, index) => (
<Cell <Cell
key={`cell-remaining-${index}`} key={`cell-remaining-${index}`}
fill={`${entry.color}33`} fill={`${entry.color}33`}
@@ -610,102 +329,103 @@ export function Dashboard() {
</Card> </Card>
{/* Employee Tracking Table */} {/* Employee Tracking Table */}
{hasFilters && employeePerformance.length > 0 && ( {hasFilters &&
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'> employeePerformance &&
<CardHeader> employeePerformance.length > 0 && (
<CardTitle className='text-lg'>Tracking ABK</CardTitle> <Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<p className='text-sm text-gray-500 mt-1'> <CardHeader>
Detail performance masing-masing ABK <CardTitle className='text-lg'>Tracking ABK</CardTitle>
</p> <p className='text-sm text-gray-500 mt-1'>
</CardHeader> Detail performance masing-masing ABK
<CardContent> </p>
<div className='overflow-x-auto'> </CardHeader>
<table className='w-full border border-gray-200 rounded-lg'> <CardContent>
<thead> <div className='overflow-x-auto'>
<tr className='bg-gray-50 border-b border-gray-200'> <table className='w-full border border-gray-200 rounded-lg'>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'> <thead>
Nama ABK <tr className='bg-gray-50 border-b border-gray-200'>
</th> <th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'> Nama ABK
Kandang </th>
</th> <th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'> Kandang
Total Aktivitas </th>
</th> <th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'> Total Aktivitas
Aktivitas Selesai </th>
</th> <th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'> Aktivitas Selesai
Aktivitas Tersisa </th>
</th> <th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'> Aktivitas Tersisa
Completion Rate </th>
</th> <th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'> Completion Rate
Last Activity </th>
</th> <th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
</tr> Last Activity
</thead> </th>
<tbody>
{employeePerformance.map((emp, index) => (
<tr
key={emp.employee_id}
className={
index % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'
}
>
<td className='py-3 px-4 text-sm text-gray-900 font-medium'>
{emp.employee_name}
</td>
<td className='py-3 px-4'>
<Badge
style={{
backgroundColor: `${emp.color}15`,
color: emp.color,
borderColor: `${emp.color}30`,
}}
className='border'
>
{emp.kandang_name}
</Badge>
</td>
<td className='py-3 px-4 text-center text-sm text-gray-900'>
{emp.total_activities_in_category}
</td>
<td className='py-3 px-4 text-center text-sm font-semibold text-green-700'>
{emp.completed_activities}
</td>
<td className='py-3 px-4 text-center text-sm text-gray-600'>
{emp.total_activities_in_category -
emp.completed_activities}
</td>
<td className='py-3 px-4 text-center'>
<div className='flex items-center justify-center gap-2'>
<div className='w-24 bg-gray-200 rounded-full h-2'>
<div
className='h-2 rounded-full transition-all'
style={{
width: `${emp.completion_rate}%`,
backgroundColor: emp.color,
}}
/>
</div>
<span className='text-sm text-gray-700 font-medium'>
{emp.completion_rate}%
</span>
</div>
</td>
<td className='py-3 px-4 text-sm text-gray-600'>
{formatDate(emp.last_activity_date)}
</td>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {employeePerformance?.map((emp, index) => (
</div> <tr
</CardContent> key={emp.employee_id}
</Card> className={
)} index % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'
}
>
<td className='py-3 px-4 text-sm text-gray-900 font-medium'>
{emp.employee_name}
</td>
<td className='py-3 px-4'>
<Badge
style={{
backgroundColor: `${emp.color}15`,
color: emp.color,
borderColor: `${emp.color}30`,
}}
className='border'
>
{emp.kandang_name}
</Badge>
</td>
<td className='py-3 px-4 text-center text-sm text-gray-900'>
{emp.total_activity}
</td>
<td className='py-3 px-4 text-center text-sm font-semibold text-green-700'>
{emp.activity_done}
</td>
<td className='py-3 px-4 text-center text-sm text-gray-600'>
{emp.activity_left}
</td>
<td className='py-3 px-4 text-center'>
<div className='flex items-center justify-center gap-2'>
<div className='w-24 bg-gray-200 rounded-full h-2'>
<div
className='h-2 rounded-full transition-all'
style={{
width: `${emp.completion_rate}%`,
backgroundColor: emp.color,
}}
/>
</div>
<span className='text-sm text-gray-700 font-medium'>
{emp.completion_rate}%
</span>
</div>
</td>
<td className='py-3 px-4 text-sm text-gray-600'>
{formatDate(emp.last_activity, 'DD MMM YYYY')}
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)}
</div> </div>
</div> </div>
); );