From f4165e2c68184f3a011d2b045a3e9b700ac262b8 Mon Sep 17 00:00:00 2001 From: Akira Date: Sun, 15 Feb 2026 15:26:03 +0900 Subject: [PATCH] =?UTF-8?q?Day=2014=20=E5=AE=8C=E4=BA=86=20=E4=BD=9C?= =?UTF-8?q?=E4=BB=98=E3=81=91=E8=A8=88=E7=94=BB=E7=94=BB=E9=9D=A2=E3=81=AB?= =?UTF-8?q?=E9=9B=86=E8=A8=88=E3=82=B5=E3=82=A4=E3=83=89=E3=83=90=E3=83=BC?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F?= =?UTF-8?q?=EF=BC=9A=20=E6=A9=9F=E8=83=BD:=20-=20PC:=20=E5=B7=A6=E5=81=B4?= =?UTF-8?q?=E3=81=AB=E9=9B=86=E8=A8=88=E3=82=B5=E3=82=A4=E3=83=89=E3=83=90?= =?UTF-8?q?=E3=83=BC=EF=BC=88=E9=96=8B/=E9=96=89=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=EF=BC=89=20-=20=E3=82=B9=E3=83=9E=E3=83=9B:=20=E3=80=8C?= =?UTF-8?q?=F0=9F=93=8A=20=E9=9B=86=E8=A8=88=E3=82=92=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=80=8D=E3=83=9C=E3=82=BF=E3=83=B3=20=E2=86=92=20=E3=83=A2?= =?UTF-8?q?=E3=83=BC=E3=83=80=E3=83=AB=E8=A1=A8=E7=A4=BA=20-=20=E3=83=AA?= =?UTF-8?q?=E3=82=A2=E3=83=AB=E3=82=BF=E3=82=A4=E3=83=A0=E6=9B=B4=E6=96=B0?= =?UTF-8?q?:=20=E4=BD=9C=E7=89=A9=E3=83=BB=E5=93=81=E7=A7=8D=E9=81=B8?= =?UTF-8?q?=E6=8A=9E=E6=99=82=E3=81=AB=E8=87=AA=E5=8B=95=E5=86=8D=E8=A8=88?= =?UTF-8?q?=E7=AE=97=20-=20=E6=9C=AA=E8=A8=AD=E5=AE=9A=E5=9C=83=E5=A0=B4?= =?UTF-8?q?=E3=81=AE=E8=AD=A6=E5=91=8A=E8=A1=A8=E7=A4=BA=EF=BC=88=E9=BB=84?= =?UTF-8?q?=E8=89=B2=EF=BC=89=20=E5=AE=9F=E8=A3=85:=20-=20useMemo=20?= =?UTF-8?q?=E3=81=A7=E9=9B=86=E8=A8=88=E8=A8=88=E7=AE=97=E3=82=92=E6=9C=80?= =?UTF-8?q?=E9=81=A9=E5=8C=96=20-=20=E4=BD=9C=E7=89=A9=E5=88=A5=E3=83=BB?= =?UTF-8?q?=E5=93=81=E7=A7=8D=E5=88=A5=E3=81=AE=E9=9D=A2=E7=A9=8D=E9=9B=86?= =?UTF-8?q?=E8=A8=88=20-=20=E5=B1=95=E9=96=8B=E5=8F=AF=E8=83=BD=E3=81=AA?= =?UTF-8?q?=E3=83=84=E3=83=AA=E3=83=BC=E8=A1=A8=E7=A4=BA=20http://localhos?= =?UTF-8?q?t:3000/allocation=20=E3=81=A7=E7=A2=BA=E8=AA=8D=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=81=BE=E3=81=99=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/allocation/page.tsx | 518 ++++++++++++++++++++------- 1 file changed, 384 insertions(+), 134 deletions(-) diff --git a/frontend/src/app/allocation/page.tsx b/frontend/src/app/allocation/page.tsx index e944f97..91c4c3a 100644 --- a/frontend/src/app/allocation/page.tsx +++ b/frontend/src/app/allocation/page.tsx @@ -1,9 +1,21 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { api } from '@/lib/api'; import { Field, Crop, Plan } from '@/types'; import Navbar from '@/components/Navbar'; +import { Menu, X, BarChart3, ChevronRight, ChevronDown } from 'lucide-react'; + +interface SummaryItem { + cropId: number; + cropName: string; + areaTan: number; + varieties: { + varietyId: number | null; + varietyName: string; + areaTan: number; + }[]; +} export default function AllocationPage() { const [fields, setFields] = useState([]); @@ -12,6 +24,9 @@ export default function AllocationPage() { const [year, setYear] = useState(2025); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(null); + const [showSidebar, setShowSidebar] = useState(true); + const [showMobileSummary, setShowMobileSummary] = useState(false); + const [expandedCrops, setExpandedCrops] = useState>(new Set()); useEffect(() => { fetchData(); @@ -35,15 +50,60 @@ export default function AllocationPage() { } }; + const summary = useMemo(() => { + const cropSummary = new Map(); + let totalAreaTan = 0; + let unassignedAreaTan = 0; + + fields.forEach((field) => { + const area = parseFloat(field.area_tan) || 0; + totalAreaTan += area; + + const plan = plans.find((p) => p.field === field.id); + + if (!plan?.crop) { + unassignedAreaTan += area; + return; + } + + if (!cropSummary.has(plan.crop)) { + const crop = crops.find((c) => c.id === plan.crop); + cropSummary.set(plan.crop, { + cropId: plan.crop, + cropName: crop?.name || '不明', + areaTan: 0, + varieties: [], + }); + } + + const item = cropSummary.get(plan.crop)!; + item.areaTan += area; + + const varietyId = plan.variety; + const varietyName = plan.variety_name || '(品種未選択)'; + + let varietyItem = item.varieties.find((v) => v.varietyId === varietyId); + if (!varietyItem) { + varietyItem = { varietyId, varietyName, areaTan: 0 }; + item.varieties.push(varietyItem); + } + varietyItem.areaTan += area; + }); + + return { + totalAreaTan, + unassignedAreaTan, + items: Array.from(cropSummary.values()), + }; + }, [fields, plans, crops]); + const getPlanForField = (fieldId: number): Plan | undefined => { return plans.find((p) => p.field === fieldId); }; const handleCropChange = async (fieldId: number, cropId: string) => { const crop = parseInt(cropId); - if (!crop) { - return; - } + if (!crop) return; const existingPlan = getPlanForField(fieldId); setSaving(fieldId); @@ -76,9 +136,7 @@ export default function AllocationPage() { const variety = parseInt(varietyId) || null; const existingPlan = getPlanForField(fieldId); - if (!existingPlan || !existingPlan.crop) { - return; - } + if (!existingPlan || !existingPlan.crop) return; setSaving(fieldId); @@ -97,17 +155,12 @@ export default function AllocationPage() { const handleNotesChange = async (fieldId: number, notes: string) => { const existingPlan = getPlanForField(fieldId); - - if (!existingPlan) { - return; - } + if (!existingPlan) return; setSaving(fieldId); try { - await api.patch(`/plans/${existingPlan.id}/`, { - notes, - }); + await api.patch(`/plans/${existingPlan.id}/`, { notes }); await fetchData(true); } catch (error) { console.error('Failed to save notes:', error); @@ -116,11 +169,99 @@ export default function AllocationPage() { } }; - const getVarietiesForCrop = (cropId: number): typeof crops[0]['varieties'] => { + const getVarietiesForCrop = (cropId: number) => { const crop = crops.find((c) => c.id === cropId); return crop?.varieties || []; }; + const toggleCropExpand = (cropId: number) => { + setExpandedCrops((prev) => { + const next = new Set(prev); + if (next.has(cropId)) { + next.delete(cropId); + } else { + next.add(cropId); + } + return next; + }); + }; + + const renderSidebar = () => ( + <> +
+ {showSidebar &&

集計

} + +
+ + {showSidebar && ( +
+
+
合計面積
+
+ {summary.totalAreaTan.toFixed(2)} 反 +
+
+ ({Math.round(summary.totalAreaTan * 1000)} m²) +
+
+ + {summary.unassignedAreaTan > 0 && ( +
+
未設定
+
+ {summary.unassignedAreaTan.toFixed(2)} 反 +
+
+ )} + +
+ {summary.items.map((item) => ( +
+ + + {expandedCrops.has(item.cropId) && ( +
+ {item.varieties.map((v, idx) => ( +
+ {v.varietyName} + + {v.areaTan.toFixed(2)} 反 + +
+ ))} +
+ )} +
+ ))} +
+
+ )} + + ); + if (loading) { return (
@@ -135,131 +276,240 @@ export default function AllocationPage() { return (
-
-
-

作付け計画

-
- - -
+ +
+ {/* PC用サイドバー */} +
+ {renderSidebar()}
- {fields.length === 0 ? ( -
-

- 圃場データがありません。インポートを実行してください。 -

-
- ) : ( -
-
- - - - - - - - - - - - {fields.map((field) => { - const plan = getPlanForField(field.id); - const selectedCropId = plan?.crop || 0; - const selectedVarietyId = plan?.variety || 0; + {/* メインコンテンツ */} +
+
+
+

作付け計画

- return ( -
- - - - - + {/* スマホ用集計ボタン */} + + +
+ + +
+ + + {fields.length === 0 ? ( +
+

+ 圃場データがありません。インポートを実行してください。 +

+
+ ) : ( +
+
+
- 圃場名 - - 面積(反) - - 作物 - - 品種 - - 備考 -
-
- {field.name} -
-
- {field.address} -
-
- {field.area_tan} - - - - - - - handleNotesChange(field.id, e.target.value) - } - disabled={saving === field.id || !plan} - placeholder="備考を入力" - className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 text-sm w-full disabled:opacity-50 disabled:bg-gray-100" - /> -
+ + + + + + + - ); - })} - -
+ 圃場名 + + 面積(反) + + 作物 + + 品種 + + 備考 +
+ + + {fields.map((field) => { + const plan = getPlanForField(field.id); + const selectedCropId = plan?.crop || 0; + const selectedVarietyId = plan?.variety || 0; + + return ( + + +
+ {field.name} +
+
+ {field.address} +
+ + + {field.area_tan} + + + + + + + + + + handleNotesChange(field.id, e.target.value) + } + disabled={saving === field.id || !plan} + placeholder="備考を入力" + className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 text-sm w-full disabled:opacity-50 disabled:bg-gray-100" + /> + + + ); + })} + + +
+
+ )} +
+
+
+ + {/* スマホ用モーダル */} + {showMobileSummary && ( +
+
setShowMobileSummary(false)} + /> +
+
+
+

集計

+ +
+
+
+
合計面積
+
+ {summary.totalAreaTan.toFixed(2)} 反 +
+
+ ({Math.round(summary.totalAreaTan * 1000)} m²) +
+
+ + {summary.unassignedAreaTan > 0 && ( +
+
未設定
+
+ {summary.unassignedAreaTan.toFixed(2)} 反 +
+
+ )} + +
+ {summary.items.map((item) => ( +
+ + + {expandedCrops.has(item.cropId) && ( +
+ {item.varieties.map((v, idx) => ( +
+ {v.varietyName} + + {v.areaTan.toFixed(2)} 反 + +
+ ))} +
+ )} +
+ ))} +
+
- )} -
+
+ )}
); }