first commit
This commit is contained in:
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;
|
||||
Reference in new issue
Block a user