'use client'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { Plus, X, ChevronUp, ChevronDown, Pencil, Check } from 'lucide-react'; import Navbar from '@/components/Navbar'; import { DistributionPlan, FertilizationPlan } from '@/types'; import { api } from '@/lib/api'; const CURRENT_YEAR = new Date().getFullYear(); // ローカル管理用のグループ型(ID未採番の新規グループも持てる) interface LocalGroup { tempId: string; name: string; order: number; fieldIds: number[]; isRenamingName?: string; // 名前変更中の一時値 } interface FieldInfo { id: number; name: string; area_tan: string; } interface Props { planId?: number; // 編集時のみ } export default function DistributionEditPage({ planId }: Props) { const router = useRouter(); const isEdit = planId !== undefined; // 基本情報 const [name, setName] = useState(''); const [fertilizationPlanId, setFertilizationPlanId] = useState(''); const [year] = useState(() => { if (typeof window !== 'undefined') { return parseInt(localStorage.getItem('distributionYear') || String(CURRENT_YEAR), 10); } return CURRENT_YEAR; }); // 施肥計画一覧(セレクタ用) const [fertilizationPlans, setFertilizationPlans] = useState([]); // 選択中の施肥計画の詳細(肥料・entries) const [fertPlanDetail, setFertPlanDetail] = useState(null); // ローカルグループ状態 const [groups, setGroups] = useState([]); const [newGroupName, setNewGroupName] = useState(''); // UI状態 const [saveError, setSaveError] = useState(null); const [saving, setSaving] = useState(false); const [loading, setLoading] = useState(true); // ── 初期データ読み込み ────────────────────────────────── useEffect(() => { const init = async () => { try { // 施肥計画一覧を全年度取得(分配計画のベースになる) const res = await api.get('/fertilizer/plans/'); setFertilizationPlans(res.data); } catch (e) { console.error(e); } if (isEdit && planId) { try { // 既存の分配計画を読み込む const detailRes = await api.get(`/fertilizer/distribution/${planId}/`); const detail: DistributionPlan = detailRes.data; setName(detail.name); setFertilizationPlanId(detail.fertilization_plan.id); setFertPlanDetail(detail.fertilization_plan); // グループを LocalGroup 形式に変換 setGroups( detail.groups.map((g, i) => ({ tempId: String(g.id), name: g.name, order: g.order ?? i, fieldIds: g.fields.map(f => f.id), })) ); } catch (e) { console.error(e); } } setLoading(false); }; init(); }, [planId]); // 施肥計画が変わったら詳細を取得 useEffect(() => { if (!fertilizationPlanId) { setFertPlanDetail(null); if (!isEdit) setGroups([]); return; } if (isEdit && fertPlanDetail?.id === fertilizationPlanId) return; const fetchDetail = async () => { try { const res = await api.get(`/fertilizer/plans/${fertilizationPlanId}/`); const data: FertilizationPlan = res.data; // FertilizationPlanForDistributionSerializer と同じ構造に合わせる const ferts = Array.from( new Map( data.entries.map(e => [e.fertilizer, { id: e.fertilizer, name: e.fertilizer_name || '' }]) ).values() ).sort((a, b) => a.name.localeCompare(b.name)); setFertPlanDetail({ id: data.id, name: data.name, year: data.year, variety_name: data.variety_name, crop_name: data.crop_name, fertilizers: ferts, entries: data.entries.map(e => ({ field: e.field, fertilizer: e.fertilizer, bags: String(e.bags), })), }); if (!isEdit) setGroups([]); } catch (e) { console.error(e); } }; fetchDetail(); }, [fertilizationPlanId]); // ── 計算ヘルパー ────────────────────────────────────── // 全圃場一覧(施肥計画のentries に含まれる圃場) const allPlanFields: FieldInfo[] = (() => { if (!fertPlanDetail) return []; const seen = new Map(); for (const e of fertPlanDetail.entries) { if (!seen.has(e.field)) { // field名は後述の fertilizationPlans から取る seen.set(e.field, { id: e.field, name: String(e.field), area_tan: '0' }); } } return Array.from(seen.values()); })(); // fertilizationPlans から field情報を取得(FertilizationPlanSerializer の entries に field_name が含まれる) const fieldInfoMap = (() => { const map = new Map(); if (!fertPlanDetail) return map; const plan = fertilizationPlans.find(p => p.id === fertPlanDetail.id); if (plan) { for (const e of plan.entries) { if (e.field && !map.has(e.field)) { map.set(e.field, { id: e.field, name: e.field_name || String(e.field), area_tan: e.field_area_tan || '0', }); } } } return map; })(); const getFieldInfo = (fieldId: number): FieldInfo => fieldInfoMap.get(fieldId) ?? { id: fieldId, name: `圃場#${fieldId}`, area_tan: '0' }; // 割り当て済みフィールドIDセット const assignedFieldIds = new Set(groups.flatMap(g => g.fieldIds)); // 未割り当て圃場 const unassignedFields = fertPlanDetail ? Array.from( new Map( fertPlanDetail.entries .map(e => e.field) .filter(id => !assignedFieldIds.has(id)) .map(id => [id, getFieldInfo(id)]) ).values() ) : []; // bags取得 const getBags = (fieldId: number, fertilizerId: number): number => { if (!fertPlanDetail) return 0; const entry = fertPlanDetail.entries.find( e => e.field === fieldId && e.fertilizer === fertilizerId ); return entry ? parseFloat(entry.bags) : 0; }; // グループごとの集計 const groupSummaries = groups.map(g => { const fertTotals = (fertPlanDetail?.fertilizers || []).map(fert => ({ fertilizerId: fert.id, fertilizerName: fert.name, total: g.fieldIds.reduce((sum, fId) => sum + getBags(fId, fert.id), 0), })); const rowTotal = fertTotals.reduce((s, f) => s + f.total, 0); return { ...g, fertTotals, rowTotal }; }); // 未割り当てグループの集計 const unassignedSummary = { fertTotals: (fertPlanDetail?.fertilizers || []).map(fert => ({ fertilizerId: fert.id, fertilizerName: fert.name, total: unassignedFields.reduce((sum, f) => sum + getBags(f.id, fert.id), 0), })), rowTotal: 0 as number, }; unassignedSummary.rowTotal = unassignedSummary.fertTotals.reduce((s, f) => s + f.total, 0); // 肥料合計行 const fertColumnTotals = (fertPlanDetail?.fertilizers || []).map(fert => { const groupTotal = groupSummaries.reduce( (sum, g) => sum + (g.fertTotals.find(f => f.fertilizerId === fert.id)?.total || 0), 0 ); const unassignedTotal = unassignedSummary.fertTotals.find(f => f.fertilizerId === fert.id)?.total || 0; return { id: fert.id, total: groupTotal + unassignedTotal }; }); const grandTotal = fertColumnTotals.reduce((s, f) => s + f.total, 0); // ── グループ操作 ────────────────────────────────────── const addGroup = () => { const n = newGroupName.trim(); if (!n) return; if (groups.some(g => g.name === n)) { setSaveError(`グループ名「${n}」はすでに存在します`); return; } setSaveError(null); setGroups(prev => [ ...prev, { tempId: crypto.randomUUID(), name: n, order: prev.length, fieldIds: [] }, ]); setNewGroupName(''); }; const removeGroup = (tempId: string) => { setGroups(prev => prev.filter(g => g.tempId !== tempId)); }; const moveGroup = (tempId: string, dir: -1 | 1) => { setGroups(prev => { const idx = prev.findIndex(g => g.tempId === tempId); if (idx < 0 || idx + dir < 0 || idx + dir >= prev.length) return prev; const next = [...prev]; [next[idx], next[idx + dir]] = [next[idx + dir], next[idx]]; return next.map((g, i) => ({ ...g, order: i })); }); }; const startRename = (tempId: string) => { setGroups(prev => prev.map(g => (g.tempId === tempId ? { ...g, isRenamingName: g.name } : g)) ); }; const commitRename = (tempId: string) => { setGroups(prev => prev.map(g => { if (g.tempId !== tempId) return g; const newName = (g.isRenamingName || '').trim(); if (!newName || newName === g.name) return { ...g, isRenamingName: undefined }; if (prev.some(other => other.tempId !== tempId && other.name === newName)) { setSaveError(`グループ名「${newName}」はすでに存在します`); return { ...g, isRenamingName: undefined }; } return { ...g, name: newName, isRenamingName: undefined }; }) ); }; const assignFieldToGroup = (fieldId: number, groupTempId: string) => { setGroups(prev => prev.map(g => { if (g.tempId === groupTempId) { return { ...g, fieldIds: [...g.fieldIds, fieldId] }; } return { ...g, fieldIds: g.fieldIds.filter(id => id !== fieldId) }; }) ); }; const removeFieldFromGroup = (fieldId: number, groupTempId: string) => { setGroups(prev => prev.map(g => g.tempId === groupTempId ? { ...g, fieldIds: g.fieldIds.filter(id => id !== fieldId) } : g ) ); }; // ── 保存 ────────────────────────────────────────────── const handleSave = async () => { setSaveError(null); if (!name.trim()) { setSaveError('計画名を入力してください'); return; } if (!fertilizationPlanId) { setSaveError('施肥計画を選択してください'); return; } setSaving(true); const payload = { name: name.trim(), fertilization_plan_id: fertilizationPlanId, groups: groups.map((g, i) => ({ name: g.name, order: i, field_ids: g.fieldIds, })), }; try { if (isEdit) { await api.put(`/fertilizer/distribution/${planId}/`, payload); } else { await api.post('/fertilizer/distribution/', payload); } setSaving(false); router.push('/distribution'); } catch (e: unknown) { setSaving(false); const axiosErr = e as { response?: { data?: unknown } }; const errData = axiosErr?.response?.data; setSaveError(errData ? JSON.stringify(errData) : '保存に失敗しました'); } }; // ── レンダリング ────────────────────────────────────── if (loading) { return (
読み込み中...
); } const fertilizers = fertPlanDetail?.fertilizers || []; return (
{/* ヘッダー */}

{isEdit ? '分配計画を編集' : '分配計画を新規作成'}

{saveError && (
{saveError}
)} {/* 基本情報 */}
setName(e.target.value)} placeholder="例: 2025年コシヒカリ 分配計画" className="flex-1 border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-green-500" />
{!fertPlanDetail ? (
施肥計画を選択するとグループ割り当て画面が表示されます
) : ( <> {/* グループ割り当て */}

グループ割り当て

{/* 新規グループ追加 */}
setNewGroupName(e.target.value)} onKeyDown={e => e.key === 'Enter' && addGroup()} placeholder="新規グループ名" className="border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-green-500 w-48" />
{/* グループ一覧 */}
{groups.map((group, idx) => (
{/* グループヘッダー */}
{group.isRenamingName !== undefined ? ( <> setGroups(prev => prev.map(g => g.tempId === group.tempId ? { ...g, isRenamingName: e.target.value } : g ) ) } onKeyDown={e => e.key === 'Enter' && commitRename(group.tempId)} className="flex-1 border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-green-500" autoFocus /> ) : ( <> {group.name} )}
{/* グループ内圃場 */}
{group.fieldIds.length === 0 ? (

圃場が割り当てられていません

) : ( group.fieldIds.map(fId => { const fi = getFieldInfo(fId); const bags = fertilizers.map(fert => getBags(fId, fert.id)); return (
{fi.name} {fi.area_tan}反 {fertilizers.map((fert, i) => ( {i > 0 && ' / '} {fert.name}: {bags[i].toFixed(2)}袋 ))}
); }) )}
))}
{/* 未割り当て圃場 */} {unassignedFields.length > 0 && (

未割り当て圃場

{unassignedFields.map((fi, idx) => (
{fi.name} {fi.area_tan}反
))}
)}
{/* 集計プレビュー */} {(groups.length > 0 || unassignedFields.length > 0) && fertilizers.length > 0 && (

集計プレビュー

{fertilizers.map(fert => ( ))} {groupSummaries.map(g => ( {fertilizers.map(fert => { const t = g.fertTotals.find(f => f.fertilizerId === fert.id)?.total || 0; return ( ); })} ))} {unassignedSummary.rowTotal > 0 && ( {fertilizers.map(fert => { const t = unassignedSummary.fertTotals.find(f => f.fertilizerId === fert.id)?.total || 0; return ( ); })} )} {fertColumnTotals.map(f => ( ))}
グループ {fert.name} 合計(袋)
{g.name} {t > 0 ? t.toFixed(2) : -} {g.rowTotal.toFixed(2)}
未割り当て {t > 0 ? t.toFixed(2) : -} {unassignedSummary.rowTotal.toFixed(2)}
合計 {f.total.toFixed(2)} {grandTotal.toFixed(2)}
)} )} {/* フッターボタン */}
); }