'use client'; import { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { ChevronLeft, Save } from 'lucide-react'; import Navbar from '@/components/Navbar'; import { api } from '@/lib/api'; import { Crop, Field, RiceTransplantPlan, StockSummary, Variety } from '@/types'; type BoxMap = Record; const currentYear = new Date().getFullYear(); export default function RiceTransplantEditPage({ planId }: { planId?: number }) { const router = useRouter(); const isNew = !planId; const [name, setName] = useState(''); const [year, setYear] = useState(currentYear); const [seedMaterialId, setSeedMaterialId] = useState(''); const [seedlingBoxesPerTan, setSeedlingBoxesPerTan] = useState(''); const [defaultSeedGramsPerBox, setDefaultSeedGramsPerBox] = useState('200'); const [notes, setNotes] = useState(''); const [crops, setCrops] = useState([]); const [allFields, setAllFields] = useState([]); const [candidateFields, setCandidateFields] = useState([]); const [selectedFields, setSelectedFields] = useState([]); const [seedStocks, setSeedStocks] = useState([]); const [calcBoxes, setCalcBoxes] = useState({}); const [adjustedBoxes, setAdjustedBoxes] = useState({}); const [boxesRounded, setBoxesRounded] = useState(false); const [loading, setLoading] = useState(!isNew); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const years = Array.from({ length: 5 }, (_, i) => currentYear + 1 - i); const allVarieties = crops.flatMap((crop: Crop) => crop.varieties); const getVarietyBySeedMaterial = (id: number) => allVarieties.find((variety: Variety) => variety.seed_material === id) ?? null; const calculateDefaultBoxes = (field: Field, perTan: string) => { const areaTan = parseFloat(field.area_tan || '0'); const boxesPerTan = parseFloat(perTan || '0'); return Number.isNaN(areaTan * boxesPerTan) ? '' : (areaTan * boxesPerTan).toFixed(1); }; useEffect(() => { const init = async () => { setError(null); try { const [cropsRes, fieldsRes, seedStockRes] = await Promise.all([ api.get('/plans/crops/'), api.get('/fields/?ordering=display_order,id'), api.get('/materials/stock-summary/?material_type=seed'), ]); setCrops(cropsRes.data); setAllFields(fieldsRes.data); setSeedStocks(seedStockRes.data); if (!isNew && planId) { const planRes = await api.get(`/plans/rice-transplant-plans/${planId}/`); const plan: RiceTransplantPlan = planRes.data; const fetchedVarieties = cropsRes.data.flatMap((crop: Crop) => crop.varieties); const linkedVariety = fetchedVarieties.find((variety: Variety) => variety.id === plan.variety) ?? null; setName(plan.name); setYear(plan.year); setSeedMaterialId(linkedVariety?.seed_material ?? ''); setSeedlingBoxesPerTan(plan.seedling_boxes_per_tan); setDefaultSeedGramsPerBox(plan.default_seed_grams_per_box); setNotes(plan.notes); const fieldIds = new Set(plan.entries.map((entry) => entry.field)); const planFields = fieldsRes.data.filter((field: Field) => fieldIds.has(field.id)); setSelectedFields(planFields); setCandidateFields(planFields); const nextAdjusted: BoxMap = {}; const nextCalc: BoxMap = {}; plan.entries.forEach((entry) => { nextAdjusted[entry.field] = Number(entry.installed_seedling_boxes).toFixed(1); nextCalc[entry.field] = Number(entry.default_seedling_boxes).toFixed(1); }); setAdjustedBoxes(nextAdjusted); setCalcBoxes(nextCalc); } } catch (e) { console.error(e); setError('データの読み込みに失敗しました。'); } finally { setLoading(false); } }; init(); }, [isNew, planId]); useEffect(() => { const fetchCandidates = async () => { const selectedVariety = seedMaterialId ? getVarietyBySeedMaterial(seedMaterialId) : null; if (!selectedVariety || !year || (!isNew && loading)) return; try { const res = await api.get( `/plans/rice-transplant-plans/candidate_fields/?year=${year}&variety_id=${selectedVariety.id}` ); const nextCandidates: Field[] = res.data; setCandidateFields(nextCandidates); if (isNew) { setSelectedFields(nextCandidates); } } catch (e) { console.error(e); setError('候補圃場の取得に失敗しました。'); } }; fetchCandidates(); }, [seedMaterialId, year, isNew, loading]); useEffect(() => { if (!seedMaterialId) return; const variety = getVarietyBySeedMaterial(seedMaterialId); if (!variety) return; if (seedlingBoxesPerTan === '') { setSeedlingBoxesPerTan(variety.default_seedling_boxes_per_tan); } }, [seedMaterialId, crops, seedlingBoxesPerTan]); useEffect(() => { const nextCalc: BoxMap = {}; selectedFields.forEach((field) => { nextCalc[field.id] = calculateDefaultBoxes(field, seedlingBoxesPerTan); }); setCalcBoxes(nextCalc); setBoxesRounded(false); }, [selectedFields, seedlingBoxesPerTan]); const addField = (field: Field) => { if (selectedFields.some((selected) => selected.id === field.id)) return; setSelectedFields((prev) => [...prev, field]); }; const removeField = (fieldId: number) => { setSelectedFields((prev) => prev.filter((field) => field.id !== fieldId)); setCalcBoxes((prev) => { const next = { ...prev }; delete next[fieldId]; return next; }); setAdjustedBoxes((prev) => { const next = { ...prev }; delete next[fieldId]; return next; }); }; const updateBoxCount = (fieldId: number, value: string) => { setAdjustedBoxes((prev) => ({ ...prev, [fieldId]: value, })); }; const applyColumnDefaults = () => { setAdjustedBoxes((prev) => { const next = { ...prev }; selectedFields.forEach((field) => { next[field.id] = calcBoxes[field.id] ?? ''; }); return next; }); setBoxesRounded(false); }; const toggleRoundColumn = () => { if (boxesRounded) { setAdjustedBoxes((prev) => { const next = { ...prev }; selectedFields.forEach((field) => { delete next[field.id]; }); return next; }); setBoxesRounded(false); return; } setAdjustedBoxes((prev) => { const next = { ...prev }; selectedFields.forEach((field) => { const raw = calcBoxes[field.id] ?? prev[field.id]; if (!raw) return; const value = parseFloat(raw); if (Number.isNaN(value)) return; next[field.id] = String(Math.round(value)); }); return next; }); setBoxesRounded(true); }; const effectiveBoxes = (fieldId: number) => { const raw = adjustedBoxes[fieldId] !== undefined && adjustedBoxes[fieldId] !== '' ? adjustedBoxes[fieldId] : calcBoxes[fieldId]; const value = parseFloat(raw ?? '0'); return Number.isNaN(value) ? 0 : value; }; const selectedSeedStock = seedMaterialId ? seedStocks.find((item) => item.material_id === seedMaterialId) ?? null : null; const selectedVariety = seedMaterialId ? getVarietyBySeedMaterial(seedMaterialId) : null; const totalBoxes = selectedFields.reduce((sum, field) => sum + effectiveBoxes(field.id), 0); const seedGrams = parseFloat(defaultSeedGramsPerBox || '0'); const totalSeedKg = seedGrams > 0 ? (totalBoxes * seedGrams) / 1000 : 0; const seedInventoryKg = parseFloat(selectedSeedStock?.current_stock ?? '0'); const remainingSeedKg = seedInventoryKg - totalSeedKg; const handleSave = async () => { setError(null); if (!name.trim()) { setError('計画名を入力してください。'); return; } if (!seedMaterialId) { setError('種子資材を選択してください。'); return; } if (!selectedVariety) { setError('選択した種子資材に対応する品種が未設定です。資材マスタで紐付けてください。'); return; } if (selectedFields.length === 0) { setError('圃場を1つ以上選択してください。'); return; } const entries = selectedFields.map((field) => ({ field_id: field.id, installed_seedling_boxes: effectiveBoxes(field.id).toFixed(2), })); const payload = { name, year, variety: selectedVariety.id, seedling_boxes_per_tan: seedlingBoxesPerTan || '0', default_seed_grams_per_box: defaultSeedGramsPerBox || '0', notes, entries, }; setSaving(true); try { if (isNew) { await api.post('/plans/rice-transplant-plans/', payload); } else { await api.put(`/plans/rice-transplant-plans/${planId}/`, payload); } router.push('/rice-transplant'); } catch (e) { console.error(e); setError('保存に失敗しました。'); } finally { setSaving(false); } }; const unselectedFields = (candidateFields.length > 0 ? candidateFields : allFields).filter( (field) => !selectedFields.some((selected) => selected.id === field.id) ); const fieldRows = useMemo( () => selectedFields.map((field) => ({ field, defaultBoxes: calcBoxes[field.id] ?? '', boxCount: adjustedBoxes[field.id] !== undefined && adjustedBoxes[field.id] !== '' ? adjustedBoxes[field.id] : calcBoxes[field.id] ?? '', })), [selectedFields, calcBoxes, adjustedBoxes] ); if (loading) { return (
読み込み中...
); } return (

{isNew ? '田植え計画 新規作成' : '田植え計画 編集'}

{error && (
{error}
)}
setName(e.target.value)} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500" placeholder="例: 2026年度 にこまる 第1回" />

同じ年度・同じ品種でも、第1回や播種日ごとに複数計画を作れます。

setDefaultSeedGramsPerBox(e.target.value)} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:outline-none focus:ring-2 focus:ring-green-500" inputMode="decimal" />

対象圃場

{selectedFields.map((field) => ( ))} {selectedFields.length === 0 && (

圃場が選択されていません。

)}
{unselectedFields.length > 0 && (

追加可能

{unselectedFields.map((field) => ( ))}
)}