first commit

This commit is contained in:
Alberto-Audrix committed 2026-08-26 13:46:05 +07:00
commit 367533c02b
171 files changed
+101175

No files matched your search

@@ -0,0 +1,265 @@
import React, { useEffect, useState } from 'react';
import { api, errorMessage } from '../../services/apiClient.ts';
import type { ManualInput, ManualInputWrite } from '../../types/api.ts';
import { Banner } from '../ui/Feedback.tsx';
import { formatNumber, todayIso } from '../../utils/format.ts';
const inputClass =
'border border-gray-300 rounded-md px-3 py-2 text-sm w-full focus:ring-red-500 focus:border-red-500';
type Props = {
cycleId: number;
currentDay: number;
readOnly?: boolean;
};
type FormState = {
date: string;
age_manual: number;
mortality_manual: number;
feed_in_manual: number;
feed_use_manual: number;
harvest_manual: number;
harvest_weight_manual: number;
manual_weight: number;
};
const emptyForm = (currentDay: number): FormState => ({
date: todayIso(),
age_manual: currentDay,
mortality_manual: 0,
feed_in_manual: 0,
feed_use_manual: 0,
harvest_manual: 0,
harvest_weight_manual: 0,
manual_weight: 0,
});
function toForm(row: ManualInput): FormState {
return {
date: row.date,
age_manual: row.age_manual,
mortality_manual: row.mortality_manual,
feed_in_manual: row.feed_in_manual,
feed_use_manual: row.feed_use_manual,
harvest_manual: row.harvest_manual,
// Database dalam gram, form selalu dalam kg.
harvest_weight_manual: row.harvest_weight_manual / 1000,
manual_weight: row.manual_weight,
};
}
const DashboardManualInputForm: React.FC<Props> = ({ cycleId, currentDay, readOnly = false }) => {
const [form, setForm] = useState<FormState>(() => emptyForm(currentDay));
const [saved, setSaved] = useState<ManualInput | null>(null);
const [rows, setRows] = useState<ManualInput[]>([]);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const load = async (date: string) => {
const list = await api.manualInputs.list({ cycle_id: cycleId });
setRows(list);
const match = list.find((row) => row.date === date) ?? null;
setSaved(match);
if (match) setForm(toForm(match));
else {
setForm((prev) => ({
...emptyForm(currentDay),
date,
age_manual: prev.date === date ? prev.age_manual : currentDay,
}));
}
};
useEffect(() => {
let cancelled = false;
(async () => {
try {
const list = await api.manualInputs.list({ cycle_id: cycleId });
if (cancelled) return;
setRows(list);
const today = todayIso();
const match =
list.find((row) => row.date === today) ?? (readOnly ? (list[0] ?? null) : null);
setSaved(match);
setForm(match ? toForm(match) : emptyForm(currentDay));
} catch (err) {
if (!cancelled) setError(errorMessage(err));
}
})();
return () => {
cancelled = true;
};
}, [cycleId, currentDay, readOnly]);
const onDateChange = async (date: string) => {
setError(null);
setNotice(null);
try {
await load(date);
} catch (err) {
setError(errorMessage(err));
}
};
return (
<section className="mt-8">
<form
onSubmit={(e) => {
e.preventDefault();
if (readOnly) return;
void (async () => {
setSaving(true);
setError(null);
setNotice(null);
try {
const body: ManualInputWrite = {
cycle: cycleId,
date: form.date,
age_manual: form.age_manual,
mortality_manual: form.mortality_manual,
feed_in_manual: form.feed_in_manual,
feed_use_manual: form.feed_use_manual,
harvest_manual: form.harvest_manual,
// Form dalam kg, disimpan ke database dalam gram.
harvest_weight_manual: form.harvest_weight_manual * 1000,
manual_weight: form.manual_weight,
};
const row = await api.manualInputs.create(body);
setSaved(row);
setForm(toForm(row));
setRows((prev) => {
const others = prev.filter((item) => item.date !== row.date);
return [row, ...others];
});
setNotice('Input manual tersimpan.');
} catch (err) {
setError(errorMessage(err));
} finally {
setSaving(false);
}
})();
}}
className="bg-white rounded-xl shadow-sm p-5 space-y-4"
>
<h2 className="text-lg font-bold text-gray-800">Input manual</h2>
<p className="text-sm text-gray-500">
{readOnly
? 'Siklus ini sudah ditutup. Data di bawah hanya untuk ditinjau.'
: 'Isi data harian di sini. Total dihitung otomatis. FCR, EEF, dan rata-rata panen belum dihitung.'}
</p>
{error && <Banner kind="error">{error}</Banner>}
{notice && <Banner kind="success">{notice}</Banner>}
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-3">
<label className="block text-sm">
Tanggal
<input
type="date"
className={`${inputClass} mt-1`}
value={form.date}
onChange={(e) => void onDateChange(e.target.value)}
/>
</label>
<label className="block text-sm">
Umur (hari)
<input
type="number"
className={`${inputClass} mt-1`}
value={form.age_manual}
disabled={readOnly}
onChange={(e) => setForm((f) => ({ ...f, age_manual: Number(e.target.value) }))}
/>
</label>
<label className="block text-sm">
Mortalitas (ekor)
<input
type="number"
className={`${inputClass} mt-1`}
value={form.mortality_manual}
disabled={readOnly}
onChange={(e) => setForm((f) => ({ ...f, mortality_manual: Number(e.target.value) }))}
/>
</label>
<label className="block text-sm">
Karung masuk
<input
type="number"
className={`${inputClass} mt-1`}
value={form.feed_in_manual}
disabled={readOnly}
onChange={(e) => setForm((f) => ({ ...f, feed_in_manual: Number(e.target.value) }))}
/>
</label>
<label className="block text-sm">
Karung Tuang
<input
type="number"
className={`${inputClass} mt-1`}
value={form.feed_use_manual}
disabled={readOnly}
onChange={(e) => setForm((f) => ({ ...f, feed_use_manual: Number(e.target.value) }))}
/>
</label>
<label className="block text-sm">
Panen (ekor)
<input
type="number"
className={`${inputClass} mt-1`}
value={form.harvest_manual}
disabled={readOnly}
onChange={(e) => setForm((f) => ({ ...f, harvest_manual: Number(e.target.value) }))}
/>
</label>
<label className="block text-sm">
Berat panen (kg)
<input
type="number"
step="0.01"
className={`${inputClass} mt-1`}
value={form.harvest_weight_manual}
disabled={readOnly}
onChange={(e) =>
setForm((f) => ({ ...f, harvest_weight_manual: Number(e.target.value) }))
}
/>
</label>
<label className="block text-sm">
Berat AVG
<input
type="number"
step="0.01"
className={`${inputClass} mt-1`}
value={form.manual_weight}
disabled={readOnly}
onChange={(e) => setForm((f) => ({ ...f, manual_weight: Number(e.target.value) }))}
/>
</label>
</div>
{saved && (
<p className="text-xs text-gray-500">
Total mortalitas {formatNumber(saved.mortality_manual_total, 0)} · karung masuk{' '}
{formatNumber(saved.feed_in_manual_total, 0)} · karung tuang{' '}
{formatNumber(saved.feed_use_manual_total, 0)} · panen{' '}
{formatNumber(saved.harvest_manual_total, 0)} · berat panen{' '}
{formatNumber(saved.harvest_weight_manual_total / 1000, 2)} kg
</p>
)}
{!saved && rows.length > 0 && (
<p className="text-xs text-gray-400">Belum ada input untuk tanggal ini.</p>
)}
{!readOnly && (
<button
type="submit"
disabled={saving}
className="bg-red-600 text-white px-4 py-2 rounded-lg text-sm font-semibold disabled:opacity-60"
>
{saving ? 'Menyimpan...' : 'Simpan'}
</button>
)}
</form>
</section>
);
};
export default DashboardManualInputForm;