281 lines
9.5 KiB
TypeScript
281 lines
9.5 KiB
TypeScript
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;
|
|
feed_out_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,
|
|
feed_out_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,
|
|
feed_out_manual: row.feed_out_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,
|
|
feed_out_manual: form.feed_out_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, FCR, dan EEF dihitung otomatis setelah data sumber lengkap.'}
|
|
</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">
|
|
Karung keluar
|
|
<input
|
|
type="number"
|
|
className={`${inputClass} mt-1`}
|
|
value={form.feed_out_manual}
|
|
disabled={readOnly}
|
|
onChange={(e) => setForm((f) => ({ ...f, feed_out_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)} · karung keluar{' '}
|
|
{formatNumber(saved.feed_out_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;
|