first commit
CI / lint-and-test (push) Canceled after 0s

This commit is contained in:
Alberto-Audrix committed 2026-07-28 08:55:05 +07:00
commit 32a36cceff
444 files changed
+67186

No files matched your search

@@ -0,0 +1,69 @@
import React from 'react';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
import { ConsumptionPatternPoint } from '../../types';
interface ConsumptionPatternChartProps {
data: ConsumptionPatternPoint[];
}
const ConsumptionPatternChart: React.FC<ConsumptionPatternChartProps> = ({ data }) => {
return (
<div className="bg-white p-6 rounded-xl shadow-sm">
<h2 className="text-xl font-bold mb-4">Pola Konsumsi Harian</h2>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 5, right: 20, left: -10, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e0e0e0" />
<XAxis
dataKey="hour"
fontSize={12}
tick={{ fill: '#6b7280' }}
unit=":00"
interval={3}
/>
<YAxis fontSize={12} tick={{ fill: '#6b7280' }} unit="%" width={40} />
<Tooltip
contentStyle={{
backgroundColor: 'rgba(255, 255, 255, 0.9)',
border: '1px solid #ccc',
borderRadius: '8px',
}}
formatter={(value: number) => [`${value.toFixed(1)}%`, '']}
/>
<Legend wrapperStyle={{ fontSize: '14px' }} verticalAlign="top" align="right" />
<Line
type="monotone"
dataKey="actual"
stroke="#ef4444"
strokeWidth={2.5}
name="Aktual"
dot={{ r: 3 }}
activeDot={{ r: 5 }}
connectNulls
/>
<Line
type="monotone"
dataKey="predicted"
stroke="#8884d8"
strokeWidth={2}
name="Prediksi AI"
strokeDasharray="5 5"
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
);
};
export default ConsumptionPatternChart;
+87
View File
@@ -0,0 +1,87 @@
import React, { useMemo } from 'react';
import { FeedingEvent } from '../../types';
interface DailyEventsListProps {
selectedDate: Date;
events: FeedingEvent[];
onEditEvent: (event: FeedingEvent) => void;
}
const eventConfig = {
completed: { icon: 'fa-check-circle', color: 'text-green-500' },
upcoming: { icon: 'fa-clock', color: 'text-blue-500' },
skipped: { icon: 'fa-ban', color: 'text-gray-500' },
};
const typeConfig = {
'AI Recommended': { icon: 'fa-robot', color: 'text-purple-600' },
Manual: { icon: 'fa-user', color: 'text-gray-600' },
Skipped: { icon: 'fa-forward', color: 'text-gray-500' },
};
const DailyEventsList: React.FC<DailyEventsListProps> = ({ selectedDate, events, onEditEvent }) => {
const dailyEvents = useMemo(() => {
return events
.filter(
(event) =>
event.time.getFullYear() === selectedDate.getFullYear() &&
event.time.getMonth() === selectedDate.getMonth() &&
event.time.getDate() === selectedDate.getDate()
)
.sort((a, b) => a.time.getTime() - b.time.getTime());
}, [selectedDate, events]);
return (
<div className="bg-white p-6 rounded-xl shadow-sm h-full flex flex-col">
<h2 className="text-xl font-bold mb-4">
Jadwal untuk{' '}
<span className="text-red-600">
{selectedDate.toLocaleDateString('id-ID', { day: 'numeric', month: 'long' })}
</span>
</h2>
{dailyEvents.length > 0 ? (
<div className="space-y-4 overflow-y-auto pr-2 flex-grow">
{dailyEvents.map((event) => {
const statusInfo = eventConfig[event.status];
const typeInfo = typeConfig[event.type];
return (
<div
key={event.id}
className="p-3 bg-gray-50 border border-gray-200 rounded-lg flex items-center space-x-4"
>
<div className={`text-2xl w-8 text-center ${statusInfo.color}`}>
<i className={`fa-solid ${statusInfo.icon}`}></i>
</div>
<div className="flex-grow">
<p className="font-bold text-gray-800">
Pukul{' '}
{event.time.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' })}
</p>
<p className="text-sm text-gray-600 flex items-center space-x-2">
<span className={typeInfo.color}>
<i className={`fa-solid ${typeInfo.icon}`}></i> {event.type}
</span>
</p>
</div>
<button
onClick={() => onEditEvent(event)}
className="text-gray-500 hover:text-blue-600 text-lg"
>
<i className="fa-solid fa-pencil"></i>
</button>
</div>
);
})}
</div>
) : (
<div className="flex-grow flex flex-col items-center justify-center text-center text-gray-500">
<i className="fa-solid fa-calendar-check text-4xl text-gray-300"></i>
<p className="mt-3 font-semibold">Tidak ada jadwal pakan</p>
<p className="text-sm">Tidak ada jadwal pemberian pakan untuk tanggal ini.</p>
</div>
)}
</div>
);
};
export default DailyEventsList;
@@ -0,0 +1,78 @@
import React from 'react';
import { FeedingEvent } from '../../types';
interface DailyScheduleTimelineProps {
events: FeedingEvent[];
}
const eventConfig = {
completed: {
icon: 'fa-check-circle',
color: 'text-green-500',
bg: 'bg-green-100',
line: 'bg-green-500',
},
upcoming: { icon: 'fa-clock', color: 'text-blue-500', bg: 'bg-blue-100', line: 'bg-gray-300' },
skipped: { icon: 'fa-ban', color: 'text-gray-500', bg: 'bg-gray-100', line: 'bg-gray-300' },
};
const typeConfig = {
'AI Recommended': { icon: 'fa-robot', color: 'text-purple-600' },
Manual: { icon: 'fa-user', color: 'text-gray-600' },
Skipped: { icon: 'fa-forward', color: 'text-gray-500' },
};
const DailyScheduleTimeline: React.FC<DailyScheduleTimelineProps> = ({ events }) => {
return (
<div className="bg-white p-6 rounded-xl shadow-sm">
<h2 className="text-xl font-bold mb-6">Linimasa Pemberian Pakan Harian</h2>
<div className="relative pl-5">
{/* Vertical Line */}
<div className="absolute left-11 top-0 bottom-0 w-0.5 bg-gray-200"></div>
{events.map((event) => {
const config = eventConfig[event.status];
const typeInfo = typeConfig[event.type];
return (
<div key={event.id} className="relative flex items-start mb-6">
{/* Time and Icon */}
<div className="flex-shrink-0 flex flex-col items-center mr-6 z-10">
<div
className={`w-12 h-12 rounded-full flex items-center justify-center ${config.bg}`}
>
<i className={`fa-solid ${config.icon} text-2xl ${config.color}`}></i>
</div>
<div className="mt-1 font-bold text-gray-700">
{event.time.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
{/* Card Details */}
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 w-full">
<div className="flex justify-between items-center">
<p className={`font-semibold text-lg ${config.color}`}>
{event.status.charAt(0).toUpperCase() + event.status.slice(1)}
</p>
<div
className={`flex items-center text-sm font-medium ${typeInfo.color}`}
title={event.type}
>
<i className={`fa-solid ${typeInfo.icon} mr-2`}></i>
<span>{event.type}</span>
</div>
</div>
<p className="text-sm text-gray-600 mt-1">
Status jadwal untuk pukul{' '}
{event.time.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' })}.
</p>
</div>
</div>
);
})}
</div>
</div>
);
};
export default DailyScheduleTimeline;
@@ -0,0 +1,226 @@
import React, { useState, useEffect, useMemo } from 'react';
import { FeedingEvent } from '../../types';
import { useAppContext } from '../../context/AppContext.tsx';
interface FeedScheduleFormModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (eventData: Omit<FeedingEvent, 'id' | 'status'>, id?: string) => void;
eventToEdit: FeedingEvent | null;
initialDate: Date;
}
const FeedScheduleFormModal: React.FC<FeedScheduleFormModalProps> = ({
isOpen,
onClose,
onSave,
eventToEdit,
initialDate,
}) => {
const { users, roles } = useAppContext();
const operators = useMemo(() => {
const operatorRoleId = roles.find((r) => r.name === 'Operator')?.id;
if (!operatorRoleId) return [];
return users.filter((u) => u.roleId === operatorRoleId);
}, [users, roles]);
const [time, setTime] = useState('08:00');
const [type, setType] = useState<'AI Recommended' | 'Manual'>('Manual');
const [operatorId, setOperatorId] = useState(operators[0]?.id || '');
const [feedType, setFeedType] = useState('CP 511 Bravo');
const [quantityKg, setQuantityKg] = useState('250');
useEffect(() => {
if (eventToEdit) {
const eventTime = eventToEdit.time;
setTime(
`${String(eventTime.getHours()).padStart(2, '0')}:${String(eventTime.getMinutes()).padStart(2, '0')}`
);
if (eventToEdit.type === 'AI Recommended' || eventToEdit.type === 'Manual') {
setType(eventToEdit.type);
}
setOperatorId(eventToEdit.operatorId);
setFeedType(eventToEdit.feedType);
setQuantityKg(String(eventToEdit.quantityKg));
} else {
setTime('08:00');
setType('Manual');
setOperatorId(operators[0]?.id || '');
setFeedType('CP 511 Bravo');
setQuantityKg('250');
}
}, [eventToEdit, operators]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const [hours, minutes] = time.split(':').map(Number);
const eventDate = eventToEdit ? new Date(eventToEdit.time) : new Date(initialDate);
eventDate.setHours(hours, minutes, 0, 0);
const operator = operators.find((o) => o.id === operatorId);
if (!operator) return;
const kg = parseInt(quantityKg, 10);
onSave(
{
time: eventDate,
type,
operatorId,
operatorName: operator.name,
feedType,
quantityKg: kg,
quantitySacks: Math.round(kg / 50),
},
eventToEdit?.id
);
};
if (!isOpen) return null;
return (
<div
className="fixed inset-0 bg-black bg-opacity-60 flex justify-center items-center z-50 p-4"
onClick={onClose}
>
<div
className="bg-white rounded-xl shadow-2xl w-full max-w-lg"
onClick={(e) => e.stopPropagation()}
>
<form onSubmit={handleSubmit}>
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-800">
{eventToEdit ? 'Edit Jadwal Pakan' : 'Tambah Jadwal Pakan'}
</h2>
<button
type="button"
onClick={onClose}
className="text-gray-400 hover:text-gray-600 text-2xl"
>
&times;
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Tanggal</label>
<input
type="text"
readOnly
value={(eventToEdit ? eventToEdit.time : initialDate).toLocaleDateString(
'id-ID',
{ weekday: 'long', day: 'numeric', month: 'long' }
)}
className="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-100"
/>
</div>
<div>
<label
htmlFor="operatorId"
className="block text-sm font-medium text-gray-700 mb-1"
>
Petugas
</label>
<select
id="operatorId"
value={operatorId}
onChange={(e) => setOperatorId(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md bg-white"
required
>
{operators.map((o) => (
<option key={o.id} value={o.id}>
{o.name}
</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="time" className="block text-sm font-medium text-gray-700 mb-1">
Waktu
</label>
<input
type="time"
id="time"
value={time}
onChange={(e) => setTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
required
/>
</div>
<div>
<label htmlFor="type" className="block text-sm font-medium text-gray-700 mb-1">
Tipe
</label>
<select
id="type"
value={type}
onChange={(e) => setType(e.target.value as 'AI Recommended' | 'Manual')}
className="w-full px-3 py-2 border border-gray-300 rounded-md bg-white"
>
<option value="Manual">Manual</option>
<option value="AI Recommended">AI Recommended</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label
htmlFor="feedType"
className="block text-sm font-medium text-gray-700 mb-1"
>
Jenis Pakan
</label>
<input
type="text"
id="feedType"
value={feedType}
onChange={(e) => setFeedType(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
required
/>
</div>
<div>
<label
htmlFor="quantityKg"
className="block text-sm font-medium text-gray-700 mb-1"
>
Jumlah (Kg)
</label>
<input
type="number"
id="quantityKg"
value={quantityKg}
onChange={(e) => setQuantityKg(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
required
min="1"
/>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-6 py-4 rounded-b-xl flex justify-end space-x-3">
<button
type="button"
onClick={onClose}
className="bg-gray-200 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-300 font-medium"
>
Batal
</button>
<button
type="submit"
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 font-medium shadow-sm"
>
Simpan Jadwal
</button>
</div>
</form>
</div>
</div>
);
};
export default FeedScheduleFormModal;
@@ -0,0 +1,66 @@
import React from 'react';
import { ScheduleInfluencingFactors } from '../../types';
import { formatNumber } from '../../utils/formatters.ts';
const statusStyles = {
ok: 'text-green-600',
warning: 'text-yellow-600',
critical: 'text-red-600',
};
const FactorCard: React.FC<{
icon: string;
label: string;
value: number;
unit: string;
status: 'ok' | 'warning' | 'critical';
}> = ({ icon, label, value, unit, status }) => (
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<div className={`text-2xl w-8 text-center ${statusStyles[status]}`}>
<i className={`fa-solid ${icon}`}></i>
</div>
<div>
<p className="text-xs text-gray-500 font-medium">{label}</p>
<p className="font-bold text-gray-800">
{formatNumber(value)} {unit}
</p>
</div>
</div>
);
interface InfluencingFactorsProps {
factors: ScheduleInfluencingFactors;
}
const InfluencingFactors: React.FC<InfluencingFactorsProps> = ({ factors }) => {
return (
<div className="bg-white p-6 rounded-xl shadow-sm">
<h2 className="text-xl font-bold mb-4">Faktor-faktor Pengaruh</h2>
<div className="space-y-3">
<FactorCard
icon="fa-temperature-high"
label="Suhu Lingkungan"
value={factors.temperature.value}
unit={factors.temperature.unit}
status={factors.temperature.status}
/>
<FactorCard
icon="fa-droplet"
label="Kelembapan"
value={factors.humidity.value}
unit={factors.humidity.unit}
status={factors.humidity.status}
/>
<FactorCard
icon="fa-person-running"
label="Tingkat Aktivitas"
value={factors.activityLevel.value}
unit={factors.activityLevel.unit}
status={factors.activityLevel.status}
/>
</div>
</div>
);
};
export default InfluencingFactors;
@@ -0,0 +1,58 @@
import React from 'react';
import { FeedScheduleRecommendation } from '../../types';
interface ScheduleOverviewProps {
recommendation: FeedScheduleRecommendation;
}
const ScheduleOverview: React.FC<ScheduleOverviewProps> = ({ recommendation }) => {
const { nextFeedingTime, lastFeedingTime, reason } = recommendation;
const formatTime = (date: Date) =>
date.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
const formatDate = (date: Date) =>
date.toLocaleDateString('id-ID', { weekday: 'long', day: 'numeric', month: 'long' });
return (
<div className="bg-white p-6 rounded-xl shadow-sm border border-red-200 bg-red-50/50">
<h2 className="text-xl font-bold text-gray-800 mb-4">Rekomendasi Jadwal AI</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<i className="fa-solid fa-clock text-4xl text-red-500"></i>
</div>
<div>
<p className="text-sm font-semibold text-gray-600">Jadwal Pakan Berikutnya</p>
<p className="text-3xl font-bold text-red-600">{formatTime(nextFeedingTime)}</p>
<p className="text-sm text-gray-500">{formatDate(nextFeedingTime)}</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<i className="fa-solid fa-check-circle text-4xl text-green-500"></i>
</div>
<div>
<p className="text-sm font-semibold text-gray-600">Pemberian Pakan Terakhir</p>
<p className="text-2xl font-bold text-gray-700">{formatTime(lastFeedingTime)}</p>
</div>
</div>
</div>
<div className="mt-6 pt-4 border-t border-red-200">
<p className="text-sm font-semibold text-gray-600 flex items-center">
<i className="fa-solid fa-lightbulb text-yellow-500 mr-2"></i> Alasan Rekomendasi AI:
</p>
<p className="mt-1 text-sm text-gray-700 italic">"{reason}"</p>
</div>
<div className="mt-6 pt-4 border-t border-red-200 flex items-center justify-end space-x-3">
<button className="px-4 py-2 bg-white border border-gray-300 text-gray-800 text-sm font-semibold rounded-lg hover:bg-gray-100 transition shadow-sm">
<i className="fa-solid fa-clock-rotate-left mr-2"></i> Tunda 30 Menit
</button>
<button className="px-4 py-2 bg-green-600 text-white text-sm font-semibold rounded-lg hover:bg-green-700 transition shadow-sm">
<i className="fa-solid fa-play-circle mr-2"></i> Mulai Sekarang
</button>
</div>
</div>
);
};
export default ScheduleOverview;