feat: add figma make components

This commit is contained in:
ValdiANS
2026-01-07 10:59:12 +07:00
parent 88c6c863e7
commit 770f363c60
56 changed files with 11887 additions and 0 deletions
@@ -0,0 +1,589 @@
'use client';
import { useState, useEffect } from 'react';
import { Eye, Download, Search } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Badge } from '@/figma-make/components/base/badge';
import { 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,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/figma-make/components/base/select';
import { toast } from 'sonner';
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
import { useRouter } from 'next/navigation';
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 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<SubmissionReportItem[]>([]);
const [filteredReportList, setFilteredReportList] = useState<
SubmissionReportItem[]
>([]);
// Master data
const [kandangList, setKandangList] = useState<Kandang[]>([]);
// 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 fetchReports = async () => {
if (!isSupabaseConfigured()) {
console.warn('Supabase not configured');
setLoading(false);
return;
}
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<string>();
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<string>();
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 (
<Badge
variant='outline'
className='border-gray-300 text-gray-700 bg-white'
>
Draft
</Badge>
);
case 'SUBMITTED':
return (
<Badge
variant='outline'
className='border-orange-300 text-orange-700 bg-white'
>
Submitted
</Badge>
);
case 'APPROVED':
return (
<Badge
variant='outline'
className='border-green-300 text-green-700 bg-white'
>
Approved
</Badge>
);
case 'REJECTED':
return (
<Badge
variant='outline'
className='border-red-300 text-red-700 bg-white'
>
Rejected
</Badge>
);
default:
return (
<Badge
variant='outline'
className='border-gray-300 text-gray-700 bg-white'
>
{status}
</Badge>
);
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
});
};
const formatDateTime = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
const handleViewDetail = (checklistId: string) => {
// Navigate to detail page (same as List Daily Checklist)
router.push(`/list-daily-checklist/detail?checklistId=${checklistId}`);
};
const exportToCSV = () => {
toast.info('Export CSV akan segera tersedia');
};
return (
<div className='min-h-screen'>
<div className='p-6'>
{/* Page Title */}
<div className='mb-6 flex items-center justify-between'>
<div>
<h1 className='text-2xl font-semibold text-gray-900'>Reports</h1>
<p className='text-sm text-gray-600 mt-1'>
Laporan lengkap checklist harian
</p>
</div>
<Button
onClick={exportToCSV}
className='bg-[#0069e0] hover:bg-[#0058c0] text-white'
>
<Download className='w-4 h-4 mr-2' />
Export CSV
</Button>
</div>
{/* Main Card */}
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-6'>
{/* Filters Section */}
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6 pb-6 border-b border-gray-200'>
<div>
<Label>Periode Tanggal</Label>
<div className='mt-1.5'>
<DateRangePicker
dateFrom={dateFrom}
dateTo={dateTo}
onDateChange={(from, to) => {
setDateFrom(from);
setDateTo(to);
}}
/>
</div>
</div>
<div>
<Label htmlFor='kandang-filter-report'>Kandang</Label>
<Select value={kandangFilter} onValueChange={setKandangFilter}>
<SelectTrigger
id='kandang-filter-report'
className='mt-1.5 border-gray-200'
>
<SelectValue placeholder='Semua Kandang' />
</SelectTrigger>
<SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangList.map((kandang) => (
<SelectItem key={kandang.id} value={kandang.id}>
{kandang.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor='status-filter-report'>Status</Label>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger
id='status-filter-report'
className='mt-1.5 border-gray-200'
>
<SelectValue placeholder='Semua Status' />
</SelectTrigger>
<SelectContent>
{STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor='search-text-report'>Cari</Label>
<div className='relative mt-1.5'>
<Input
id='search-text-report'
type='text'
placeholder='Kandang / Kategori...'
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className='border-gray-200 pl-9'
/>
<Search className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400' />
</div>
</div>
</div>
{/* Reports Table */}
{loading ? (
<div className='text-center py-12 text-gray-500'>
Memuat data...
</div>
) : filteredReportList.length > 0 ? (
<div className='overflow-x-auto'>
<table className='w-full border border-gray-200 rounded-lg'>
<thead>
<tr className='bg-gray-50 border-b border-gray-200'>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Tanggal
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Kandang
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Kategori
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Status
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Phase
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Aktivitas
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
ABK
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Progress
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Updated At
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Aksi
</th>
</tr>
</thead>
<tbody>
{filteredReportList.map((item, index) => (
<tr
key={`${item.checklist_id}-${item.date}-${index}`}
className={
index % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'
}
>
<td className='py-3 px-4 text-sm text-gray-900'>
{formatDate(item.date)}
</td>
<td className='py-3 px-4 text-sm text-gray-900'>
{item.kandang_name}
</td>
<td className='py-3 px-4 text-sm text-gray-900'>
{CATEGORY_LABELS[item.category] || item.category}
</td>
<td className='py-3 px-4'>
{getStatusBadge(item.status)}
</td>
<td className='py-3 px-4 text-center text-sm text-gray-900'>
{item.total_phases}
</td>
<td className='py-3 px-4 text-center text-sm text-gray-900'>
{item.total_activities}
</td>
<td className='py-3 px-4 text-center text-sm text-gray-900'>
{item.total_employees}
</td>
<td className='py-3 px-4 text-center'>
<div className='flex items-center justify-center gap-2'>
<div className='w-20 bg-gray-200 rounded-full h-2'>
<div
className='bg-[#0069e0] h-2 rounded-full transition-all'
style={{ width: `${item.progress_percent}%` }}
/>
</div>
<span className='text-sm text-gray-700 font-medium'>
{item.progress_percent}%
</span>
</div>
</td>
<td className='py-3 px-4 text-sm text-gray-600'>
{formatDateTime(item.updated_at)}
</td>
<td className='py-3 px-4'>
<div className='flex items-center justify-center'>
<Button
size='sm'
variant='outline'
onClick={() =>
handleViewDetail(item.checklist_id)
}
className='border-gray-200 text-gray-700 hover:bg-gray-50'
>
<Eye className='w-4 h-4 mr-1' />
Detail
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className='text-center py-12 text-gray-500'>
{searchText ||
dateFrom ||
dateTo ||
statusFilter !== 'ALL' ||
kandangFilter !== 'ALL'
? 'Tidak ada data yang sesuai dengan filter'
: 'Belum ada data checklist'}
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}