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,712 @@
'use client';
import { useState, useEffect } from 'react';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/figma-make/components/base/card';
import { Label } from '@/figma-make/components/base/label';
import {
Select,
SelectContent,
SelectItem,
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,
Users,
AlertCircle,
Info,
} from 'lucide-react';
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
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;
}
const KANDANG_COLORS = [
'#0069e0', // Blue (primary)
'#10B981', // Green
'#F59E0B', // Amber
'#EF4444', // Red
'#8B5CF6', // Violet
'#EC4899', // Pink
'#14B8A6', // Teal
'#F97316', // Orange
];
const CATEGORY_LABELS: { [key: string]: string } = {
pullet_open: 'Pullet Open',
pullet_close: 'Pullet Close',
produksi_open: 'Produksi Open',
produksi_close: 'Produksi Close',
};
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
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([]);
}
}, [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 hasFilters = dateFrom && dateTo;
// Prepare chart data
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,
color: emp.color,
kandang: emp.kandang_name,
}));
return (
<div className='min-h-screen'>
<div className='p-6'>
{/* Page Title */}
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>Dashboard</h1>
<p className='text-sm text-gray-600 mt-1'>
Performance tracking per ABK
</p>
</div>
{/* Filters Card */}
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white mb-6'>
<CardHeader>
<CardTitle className='text-lg'>Filter</CardTitle>
</CardHeader>
<CardContent>
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'>
<div>
<Label>
Periode Tanggal <span className='text-red-500'>*</span>
</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-dashboard'>Kandang</Label>
<div className='mt-1.5'>
<Select
value={kandangFilter}
onValueChange={setKandangFilter}
>
<SelectTrigger
id='kandang-filter-dashboard'
className='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>
<div>
<Label htmlFor='category-filter-dashboard'>Kategori</Label>
<div className='mt-1.5'>
<Select
value={categoryFilter}
onValueChange={setCategoryFilter}
>
<SelectTrigger
id='category-filter-dashboard'
className='border-gray-200'
>
<SelectValue placeholder='Semua Kategori' />
</SelectTrigger>
<SelectContent>
<SelectItem value='ALL'>Semua Kategori</SelectItem>
{categoryList.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Performance Chart Card */}
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white mb-6'>
<CardHeader>
<div className='flex items-center gap-2'>
<CardTitle className='text-lg'>
Performance Overview - Aktivitas per ABK
</CardTitle>
<Info className='w-4 h-4 text-gray-400' />
</div>
<p className='text-sm text-gray-500 mt-1'>
Aktivitas yang telah diselesaikan vs total aktivitas di kategori
</p>
</CardHeader>
<CardContent>
{!hasFilters ? (
// Empty state - no filters
<div className='flex flex-col items-center justify-center py-16 text-center'>
<AlertCircle className='w-16 h-16 text-gray-300 mb-4' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
Pilih Filter Terlebih Dahulu
</h3>
<p className='text-sm text-gray-500 max-w-md'>
Silakan pilih{' '}
<span className='font-semibold'>Tanggal Dari</span> dan{' '}
<span className='font-semibold'>Tanggal Sampai</span> untuk
melihat performance ABK.
</p>
</div>
) : loading ? (
<div className='text-center py-16 text-gray-500'>
Memuat data...
</div>
) : employeePerformance.length === 0 ? (
<div className='flex flex-col items-center justify-center py-16 text-center'>
<Users className='w-16 h-16 text-gray-300 mb-4' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
Tidak Ada Data
</h3>
<p className='text-sm text-gray-500 max-w-md'>
Tidak ada data aktivitas ABK pada periode yang dipilih.
</p>
</div>
) : (
<ResponsiveContainer width='100%' height={400}>
<BarChart
data={chartData}
margin={{ top: 20, right: 30, left: 20, bottom: 80 }}
>
<CartesianGrid strokeDasharray='3 3' stroke='#E5E7EB' />
<XAxis
dataKey='name'
angle={-45}
textAnchor='end'
height={100}
tick={{ fill: '#6B7280', fontSize: 12 }}
/>
<YAxis tick={{ fill: '#6B7280', fontSize: 12 }} />
<Tooltip
contentStyle={{
backgroundColor: '#fff',
border: '1px solid #E5E7EB',
borderRadius: '8px',
padding: '12px',
}}
labelStyle={{ fontWeight: 600, marginBottom: '8px' }}
formatter={(value, name) => {
if (name === 'completed')
return [value, 'Aktivitas Selesai'];
if (name === 'remaining')
return [value, 'Aktivitas Tersisa'];
return [value, name];
}}
/>
<Legend
wrapperStyle={{ paddingTop: '20px' }}
formatter={(value: string) => {
if (value === 'completed') return 'Aktivitas Selesai';
if (value === 'remaining') return 'Aktivitas Tersisa';
return value;
}}
/>
<Bar
dataKey='completed'
stackId='a'
fill='#10B981'
radius={[0, 0, 0, 0]}
>
{chartData.map((entry, index) => (
<Cell
key={`cell-completed-${index}`}
fill={entry.color}
/>
))}
</Bar>
<Bar
dataKey='remaining'
stackId='a'
fill='#E5E7EB'
radius={[4, 4, 0, 0]}
>
{chartData.map((entry, index) => (
<Cell
key={`cell-remaining-${index}`}
fill={`${entry.color}33`}
opacity={0.3}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
)}
</CardContent>
</Card>
{/* Employee Tracking Table */}
{hasFilters && employeePerformance.length > 0 && (
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardHeader>
<CardTitle className='text-lg'>Tracking ABK</CardTitle>
<p className='text-sm text-gray-500 mt-1'>
Detail performance masing-masing ABK
</p>
</CardHeader>
<CardContent>
<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'>
Nama ABK
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Kandang
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Total Aktivitas
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Aktivitas Selesai
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Aktivitas Tersisa
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Completion Rate
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Last Activity
</th>
</tr>
</thead>
<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>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)}
</div>
</div>
);
}