'use client'; import { useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { ChevronLeft, Plus, X, Calculator, Save, FileDown } from 'lucide-react'; import Navbar from '@/components/Navbar'; import { api } from '@/lib/api'; import { Fertilizer, FertilizationPlan, Crop, Field } from '@/types'; type CalcMethod = 'per_tan' | 'even' | 'nitrogen'; interface CalcSetting { fertilizer_id: number; method: CalcMethod; param: string; } // field_id → fertilizer_id → bags (string) type Matrix = Record>; const METHOD_LABELS: Record = { per_tan: '反当袋数', even: '均等配分', nitrogen: '反当チッソ', }; const METHOD_UNIT: Record = { per_tan: '袋/反', even: '袋(総数)', nitrogen: 'kg/反 (N)', }; const currentYear = new Date().getFullYear(); export default function FertilizerEditPage({ planId }: { planId?: number }) { const router = useRouter(); const isNew = !planId; // ─── ヘッダー情報 const [name, setName] = useState(''); const [year, setYear] = useState(currentYear); const [varietyId, setVarietyId] = useState(''); // ─── マスタデータ const [crops, setCrops] = useState([]); const [allFertilizers, setAllFertilizers] = useState([]); // ─── 圃場 const [selectedFields, setSelectedFields] = useState([]); const [candidateFields, setCandidateFields] = useState([]); const [showFieldPicker, setShowFieldPicker] = useState(false); const [allFields, setAllFields] = useState([]); // ─── 肥料(計画に使う肥料) const [planFertilizers, setPlanFertilizers] = useState([]); const [calcSettings, setCalcSettings] = useState([]); const [showFertPicker, setShowFertPicker] = useState(false); // ─── マトリクス // calcMatrix: 自動計算の結果(参照用・変更不可の表示値) // adjusted: ユーザーが最終確定した値(保存対象) // roundedColumns: 四捨五入済みの肥料列ID(↩ トグル用) const [calcMatrix, setCalcMatrix] = useState({}); const [adjusted, setAdjusted] = useState({}); const [roundedColumns, setRoundedColumns] = useState>(new Set()); const [loading, setLoading] = useState(!isNew); const [saving, setSaving] = useState(false); // ─── 初期データ取得 useEffect(() => { const init = async () => { try { const [cropsRes, fertsRes, fieldsRes] = await Promise.all([ api.get('/plans/crops/'), api.get('/fertilizer/fertilizers/'), api.get('/fields/?ordering=display_order,id'), ]); setCrops(cropsRes.data); setAllFertilizers(fertsRes.data); setAllFields(fieldsRes.data); if (!isNew && planId) { const planRes = await api.get(`/fertilizer/plans/${planId}/`); const plan: FertilizationPlan = planRes.data; setName(plan.name); setYear(plan.year); setVarietyId(plan.variety); const fertIds = Array.from(new Set(plan.entries.map((e) => e.fertilizer))); const ferts = fertsRes.data.filter((f: Fertilizer) => fertIds.includes(f.id)); setPlanFertilizers(ferts); setCalcSettings(ferts.map((f: Fertilizer) => ({ fertilizer_id: f.id, method: 'per_tan' as CalcMethod, param: '' }))); const fieldIds = Array.from(new Set(plan.entries.map((e) => e.field))); const fields = fieldsRes.data.filter((f: Field) => fieldIds.includes(f.id)); setSelectedFields(fields); // 保存済みの値は adjusted に復元(calc値はなし) const newAdjusted: Matrix = {}; plan.entries.forEach((e) => { if (!newAdjusted[e.field]) newAdjusted[e.field] = {}; newAdjusted[e.field][e.fertilizer] = String(e.bags); }); setAdjusted(newAdjusted); } } catch (e: unknown) { const err = e as { response?: { status?: number; data?: unknown } }; console.error('初期データ取得エラー:', err); alert(`データの読み込みに失敗しました (${err.response?.status ?? 'network error'})\n${JSON.stringify(err.response?.data ?? '')}`); } finally { setLoading(false); } }; init(); }, [planId, isNew]); // ─── 品種変更時: 候補圃場を取得して selectedFields をリセット const fetchCandidates = useCallback(async (y: number, vId: number) => { try { const res = await api.get(`/fertilizer/candidate_fields/?year=${y}&variety_id=${vId}`); const candidates: Field[] = res.data; setCandidateFields(candidates); if (isNew) setSelectedFields(candidates); } catch (e) { console.error(e); } }, [isNew]); useEffect(() => { if (varietyId && year) { fetchCandidates(year, varietyId as number); } }, [varietyId, year, fetchCandidates]); // ─── 肥料追加・削除 const addFertilizer = (fert: Fertilizer) => { if (planFertilizers.find((f) => f.id === fert.id)) return; setPlanFertilizers((prev) => [...prev, fert]); setCalcSettings((prev) => [...prev, { fertilizer_id: fert.id, method: 'per_tan', param: '' }]); setShowFertPicker(false); }; const removeFertilizer = (id: number) => { if (!confirm('この肥料を計画から削除しますか?')) return; setPlanFertilizers((prev) => prev.filter((f) => f.id !== id)); setCalcSettings((prev) => prev.filter((s) => s.fertilizer_id !== id)); const dropCol = (m: Matrix): Matrix => { const next = { ...m }; Object.keys(next).forEach((fid) => { const row = { ...next[Number(fid)] }; delete row[id]; next[Number(fid)] = row; }); return next; }; setCalcMatrix(dropCol); setAdjusted(dropCol); setRoundedColumns((prev) => { const next = new Set(prev); next.delete(id); return next; }); }; // ─── 圃場追加・削除 const addField = (field: Field) => { if (selectedFields.find((f) => f.id === field.id)) return; setSelectedFields((prev) => [...prev, field]); setShowFieldPicker(false); }; const removeField = (id: number) => { setSelectedFields((prev) => prev.filter((f) => f.id !== id)); setCalcMatrix((prev) => { const next = { ...prev }; delete next[id]; return next; }); setAdjusted((prev) => { const next = { ...prev }; delete next[id]; return next; }); }; // ─── 自動計算 const runCalc = async (setting: CalcSetting) => { if (!setting.param) return alert('パラメータを入力してください'); if (selectedFields.length === 0) return alert('対象圃場を選択してください'); try { const res = await api.post('/fertilizer/calculate/', { method: setting.method, param: parseFloat(setting.param), fertilizer_id: setting.fertilizer_id, field_ids: selectedFields.map((f) => f.id), }); const results: { field_id: number; bags: number }[] = res.data; // calc値を更新 setCalcMatrix((prev) => { const next = { ...prev }; results.forEach(({ field_id, bags }) => { if (!next[field_id]) next[field_id] = {}; next[field_id][setting.fertilizer_id] = String(bags); }); return next; }); // adjusted と丸め状態をリセット(新しい計算結果を再丸めさせる) setAdjusted((prev) => { const next = { ...prev }; results.forEach(({ field_id }) => { if (next[field_id]) { const row = { ...next[field_id] }; delete row[setting.fertilizer_id]; next[field_id] = row; } }); return next; }); setRoundedColumns((prev) => { const next = new Set(prev); next.delete(setting.fertilizer_id); return next; }); } catch (e: unknown) { const err = e as { response?: { data?: { error?: string } } }; alert(err.response?.data?.error ?? '計算に失敗しました'); } }; const updateCalcSetting = (fertId: number, key: keyof CalcSetting, value: string) => { setCalcSettings((prev) => prev.map((s) => (s.fertilizer_id === fertId ? { ...s, [key]: value } : s)) ); }; // ─── セル更新(adjusted を更新) const updateCell = (fieldId: number, fertId: number, value: string) => { setAdjusted((prev) => { const next = { ...prev }; if (!next[fieldId]) next[fieldId] = {}; next[fieldId][fertId] = value; return next; }); }; // ─── 列単位で四捨五入 / 元に戻す(トグル) const roundColumn = (fertId: number) => { if (roundedColumns.has(fertId)) { // 元に戻す: adjusted からこの列を削除 → calc値が再び表示される setAdjusted((prev) => { const next = { ...prev }; selectedFields.forEach((field) => { if (next[field.id]) { const row = { ...next[field.id] }; delete row[fertId]; next[field.id] = row; } }); return next; }); setRoundedColumns((prev) => { const next = new Set(prev); next.delete(fertId); return next; }); } else { // 四捨五入: calc値を整数に丸めて adjusted に書き込む setAdjusted((prev) => { const next = { ...prev }; selectedFields.forEach((field) => { const calc = calcMatrix[field.id]?.[fertId]; if (calc !== undefined && calc !== '') { const v = parseFloat(calc); if (!isNaN(v)) { if (!next[field.id]) next[field.id] = {}; next[field.id][fertId] = String(Math.round(v)); } } }); return next; }); setRoundedColumns((prev) => { const next = new Set(prev); next.add(fertId); return next; }); } }; // ─── 集計(adjusted 優先、なければ calc 値) const effectiveValue = (fieldId: number, fertId: number): number => { const adj = adjusted[fieldId]?.[fertId]; const calc = calcMatrix[fieldId]?.[fertId]; const raw = adj !== undefined && adj !== '' ? adj : calc; const v = parseFloat(raw ?? '0'); return isNaN(v) ? 0 : v; }; const rowTotal = (fieldId: number) => planFertilizers.reduce((sum, f) => sum + effectiveValue(fieldId, f.id), 0); const colTotal = (fertId: number) => selectedFields.reduce((sum, f) => sum + effectiveValue(f.id, fertId), 0); const grandTotal = planFertilizers.reduce((sum, f) => sum + colTotal(f.id), 0); // ─── 保存(adjusted 優先、なければ calc 値を使用) const handleSave = async () => { if (!name.trim()) return alert('計画名を入力してください'); if (!varietyId) return alert('品種を選択してください'); if (selectedFields.length === 0) return alert('圃場を1つ以上選択してください'); const entries: { field_id: number; fertilizer_id: number; bags: number }[] = []; selectedFields.forEach((field) => { planFertilizers.forEach((fert) => { const adj = adjusted[field.id]?.[fert.id]; const calc = calcMatrix[field.id]?.[fert.id]; const raw = adj !== undefined && adj !== '' ? adj : calc; if (raw) { const v = parseFloat(raw); if (v > 0) entries.push({ field_id: field.id, fertilizer_id: fert.id, bags: v }); } }); }); if (entries.length === 0) { if (!confirm('袋数が1件も入力されていません。このまま保存しますか?\n(後から編集画面で袋数を入力できます)')) return; } setSaving(true); try { const payload = { name, year, variety: varietyId, entries }; if (isNew) { await api.post('/fertilizer/plans/', payload); } else { await api.put(`/fertilizer/plans/${planId}/`, payload); } alert('保存しました'); router.push('/fertilizer'); } catch (e: unknown) { const err = e as { response?: { data?: unknown } }; console.error(err); alert('保存に失敗しました: ' + JSON.stringify(err.response?.data)); } finally { setSaving(false); } }; // ─── PDF出力 const handlePdf = async () => { if (!planId) return; try { const res = await api.get(`/fertilizer/plans/${planId}/pdf/`, { responseType: 'blob' }); const url = URL.createObjectURL(new Blob([res.data], { type: 'application/pdf' })); const a = document.createElement('a'); a.href = url; a.download = `施肥計画_${year}_${name}.pdf`; a.click(); URL.revokeObjectURL(url); } catch (e) { alert('PDF出力に失敗しました'); } }; const years = Array.from({ length: 5 }, (_, i) => currentYear + 1 - i); const availableFerts = allFertilizers.filter((f) => !planFertilizers.find((pf) => pf.id === f.id)); const unselectedFields = allFields.filter((f) => !selectedFields.find((sf) => sf.id === f.id)); if (loading) { return (
読み込み中...
); } return (
{/* ヘッダー */}

{isNew ? '施肥計画 新規作成' : '施肥計画 編集'}

{!isNew && ( )}
{/* 基本情報 */}
setName(e.target.value)} placeholder="例: 2025年度 コシヒカリ 元肥" />
{/* 対象圃場 */}

対象圃場 {selectedFields.length}筆 / {selectedFields.reduce((s, f) => s + parseFloat(f.area_tan), 0).toFixed(2)}反

{selectedFields.length === 0 && (

品種を選択すると作付け計画から圃場が自動抽出されます

)} {selectedFields.map((f) => ( {f.name}({f.area_tan}反) ))}
{/* 自動計算設定パネル */}

自動計算設定

{planFertilizers.length === 0 ? (

肥料を追加してください

) : (
{planFertilizers.map((fert) => { const setting = calcSettings.find((s) => s.fertilizer_id === fert.id); if (!setting) return null; return (
{fert.name} updateCalcSetting(fert.id, 'param', e.target.value)} placeholder="値" /> {METHOD_UNIT[setting.method]}
); })}
)}
{/* マトリクス表 */} {selectedFields.length > 0 && planFertilizers.length > 0 && (
{planFertilizers.map((f) => { const isRounded = roundedColumns.has(f.id); return ( ); })} {selectedFields.map((field) => ( {planFertilizers.map((fert) => { const calcVal = calcMatrix[field.id]?.[fert.id]; const adjVal = adjusted[field.id]?.[fert.id]; // adjusted が設定されているときだけ灰色参照を表示(丸め後) const showRef = adjVal !== undefined && calcVal !== undefined; // 入力欄: adjusted → calc値 → 空 const inputValue = adjVal !== undefined ? adjVal : (calcVal ?? ''); return ( ); })} ))} {planFertilizers.map((f) => ( ))}
圃場名 面積(反) {f.name} (袋) 合計袋数
{field.name} {field.area_tan}
{showRef && ( {calcVal} )} updateCell(field.id, fert.id, e.target.value)} placeholder="-" />
{rowTotal(field.id) > 0 ? rowTotal(field.id).toFixed(2) : '-'}
合計 {selectedFields.reduce((s, f) => s + parseFloat(f.area_tan), 0).toFixed(2)} {colTotal(f.id) > 0 ? colTotal(f.id).toFixed(2) : '-'} {grandTotal > 0 ? grandTotal.toFixed(2) : '-'}
)}
{/* 圃場選択ピッカー */} {showFieldPicker && (

圃場を追加

{candidateFields.length > 0 && ( <>

作付け計画から({year}年度 / 選択品種)

{candidateFields.filter((f) => !selectedFields.find((sf) => sf.id === f.id)).map((f) => ( ))}

その他の圃場

)} {unselectedFields.filter((f) => !candidateFields.find((cf) => cf.id === f.id)).map((f) => ( ))}
)} {/* 肥料選択ピッカー */} {showFertPicker && (

肥料を追加

{availableFerts.length === 0 ? (

追加できる肥料がありません

) : ( availableFerts.map((f) => ( )) )}
)}
); }