first commit
This commit is contained in:
commit
b54624be96
226 files changed
+108840
No files matched your search
@@ -0,0 +1,172 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useFarm } from '../context/FarmContext.tsx';
|
||||
import { formatDateId, formatDateRange, resolveCycleStatus } from '../utils/format.ts';
|
||||
import type { Cycle } from '../types/api.ts';
|
||||
|
||||
type Phase = { name: string; startDay: number; endDay: number; color: string };
|
||||
|
||||
function phasesFor(totalDays: number): Phase[] {
|
||||
const starterEnd = Math.min(10, totalDays);
|
||||
const growerEnd = Math.min(24, totalDays);
|
||||
const phases: Phase[] = [
|
||||
{ name: 'Starter', startDay: 1, endDay: starterEnd, color: 'bg-red-400' },
|
||||
];
|
||||
if (growerEnd > starterEnd) {
|
||||
phases.push({
|
||||
name: 'Grower',
|
||||
startDay: starterEnd + 1,
|
||||
endDay: growerEnd,
|
||||
color: 'bg-orange-400',
|
||||
});
|
||||
}
|
||||
if (totalDays > growerEnd) {
|
||||
phases.push({
|
||||
name: 'Finisher',
|
||||
startDay: growerEnd + 1,
|
||||
endDay: totalDays,
|
||||
color: 'bg-yellow-500',
|
||||
});
|
||||
}
|
||||
return phases;
|
||||
}
|
||||
|
||||
const statusConfig = {
|
||||
active: { label: 'Active', bg: 'bg-red-100', text: 'text-red-800', icon: 'fa-running' },
|
||||
closed: {
|
||||
label: 'Completed',
|
||||
bg: 'bg-green-100',
|
||||
text: 'text-green-800',
|
||||
icon: 'fa-check-circle',
|
||||
},
|
||||
};
|
||||
|
||||
const SingleCycleView: React.FC<{ cycle: Cycle }> = ({ cycle }) => {
|
||||
const phases = phasesFor(cycle.total_days);
|
||||
const currentDay = Math.min(Math.max(cycle.current_day, 0), cycle.total_days);
|
||||
const cycleStatus = resolveCycleStatus(cycle);
|
||||
const progress = cycle.total_days > 0 ? (currentDay / cycle.total_days) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<div className="flex flex-col sm:flex-row justify-between sm:items-center mb-4 gap-4">
|
||||
<p className="text-sm text-gray-500">
|
||||
Siklus:{' '}
|
||||
<span className="font-semibold">{formatDateRange(cycle.start_date, cycle.end_date)}</span>
|
||||
</p>
|
||||
<div className="text-sm sm:text-right">
|
||||
<p className="font-semibold text-gray-700">Estimasi Panen</p>
|
||||
<p className="text-red-600 font-bold">{formatDateId(cycle.end_date)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative pt-8">
|
||||
<div className="relative h-8 bg-gray-200 rounded-lg flex items-center shadow-inner overflow-hidden">
|
||||
{phases.map((phase) => {
|
||||
const width = ((phase.endDay - phase.startDay + 1) / cycle.total_days) * 100;
|
||||
return (
|
||||
<div
|
||||
key={phase.name}
|
||||
className={`h-full ${phase.color} flex items-center justify-center text-white text-sm font-bold`}
|
||||
style={{ width: `${width}%` }}
|
||||
title={`${phase.name} (Hari ${phase.startDay}-${phase.endDay})`}
|
||||
>
|
||||
<span className="hidden sm:inline opacity-90">{phase.name}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{cycleStatus === 'active' && (
|
||||
<div
|
||||
className="absolute top-0 transition-all duration-500 ease-out"
|
||||
style={{ left: `calc(${progress}% - 2.5rem)` }}
|
||||
>
|
||||
<div className="relative w-24 text-center">
|
||||
<div className="font-bold text-red-600 bg-red-100 px-3 py-1.5 rounded-md shadow-sm border border-red-200 text-base">
|
||||
Hari ke-{currentDay}
|
||||
</div>
|
||||
<div className="absolute left-1/2 -translate-x-1/2 w-0 h-0 border-l-8 border-l-transparent border-r-8 border-r-transparent border-t-8 border-t-red-100" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center text-sm font-semibold text-gray-500 mt-2 px-1">
|
||||
<div className="flex flex-col items-start">
|
||||
<span>Hari 1</span>
|
||||
<span className="text-xs font-normal text-gray-400">
|
||||
{formatDateId(cycle.start_date)}
|
||||
</span>
|
||||
<span className="text-xs font-normal text-gray-400">
|
||||
Berat DOC: {cycle.chick_in_weight} gram
|
||||
</span>
|
||||
<span className="text-xs font-normal text-gray-400">
|
||||
DOC In: {cycle.doc_in_count.toLocaleString('id-ID')} ekor
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end">
|
||||
<span>Hari {cycle.total_days}</span>
|
||||
<span className="text-xs font-normal text-gray-400">
|
||||
{formatDateId(cycle.end_date)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CycleTimeline: React.FC = () => {
|
||||
const { cycles, selectedCycleId, setSelectedCycleId } = useFarm();
|
||||
const [localId, setLocalId] = useState<number | null>(selectedCycleId);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalId(selectedCycleId);
|
||||
}, [selectedCycleId]);
|
||||
|
||||
const selected = cycles.find((c) => c.id === localId) ?? null;
|
||||
|
||||
return (
|
||||
<div className="bg-white p-6 sm:p-8 rounded-xl shadow-sm mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-4">Linimasa Siklus Produksi</h2>
|
||||
{cycles.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
Belum ada siklus untuk kandang ini. Data siklus datang dari sinkron site kota.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="border-b border-gray-200 overflow-x-auto">
|
||||
<nav className="-mb-px flex space-x-6 min-w-max" aria-label="Tabs">
|
||||
{cycles.map((cycle) => {
|
||||
const config = statusConfig[resolveCycleStatus(cycle)];
|
||||
const isActive = cycle.id === selected?.id;
|
||||
return (
|
||||
<button
|
||||
key={cycle.id}
|
||||
onClick={() => {
|
||||
setLocalId(cycle.id);
|
||||
setSelectedCycleId(cycle.id);
|
||||
}}
|
||||
className={`flex items-center gap-2 px-3 py-2 text-sm font-semibold rounded-t-lg transition-colors border-b-2 whitespace-nowrap ${
|
||||
isActive
|
||||
? 'border-red-500 text-red-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<i className={`fa-solid ${config.icon}`} />
|
||||
<span>
|
||||
{formatDateId(cycle.start_date)} ({config.label})
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
{selected ? (
|
||||
<SingleCycleView cycle={selected} />
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 mt-4">Pilih tab siklus di atas.</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CycleTimeline;
|
||||
Reference in new issue
Block a user