From cce119b1a889e2e4b4a87393e30d5e340201211b Mon Sep 17 00:00:00 2001 From: Akira Date: Thu, 19 Feb 2026 12:29:54 +0900 Subject: [PATCH] =?UTF-8?q?A-2=20=E5=AE=9F=E8=A3=85=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E3=81=BE=E3=81=A8=E3=82=81:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit バックエンド: POST /api/plans/bulk_update/ API(field_ids, year, crop, variety を受けて一括設定) フロントエンド: チェックボックス列、全選択/個別選択、一括操作バー(作物・品種セレクタ + 確認ダイアログ) --- CLAUDE.md | 5 +- backend/apps/plans/views.py | 38 ++++++ .../06_ドキュメントvs実装_差異レポート.md | 15 ++- frontend/src/app/allocation/page.tsx | 109 +++++++++++++++++- 4 files changed, 154 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 11b4b7d..379256c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -215,9 +215,8 @@ Variety (品種マスタ) 4. **パフォーマンス**: N+1問題が一部存在(現状は問題ないが、データ増加時に対応必要) ### 🔜 次の実装タスク(優先順) -1. **A-2**: チェックボックス・一括操作 -2. **A-1**: ダッシュボード画面 -3. **A-7**: 検索・フィルタ +1. **A-1**: ダッシュボード画面 +2. **A-7**: 検索・フィルタ 詳細は `document/06_ドキュメントvs実装_差異レポート.md` を参照 diff --git a/backend/apps/plans/views.py b/backend/apps/plans/views.py index 01dca04..c1ec375 100644 --- a/backend/apps/plans/views.py +++ b/backend/apps/plans/views.py @@ -79,6 +79,44 @@ class PlanViewSet(viewsets.ModelViewSet): return Response({'message': f'Copied {len(new_plans)} plans from {from_year} to {to_year}'}) + @action(detail=False, methods=['post']) + def bulk_update(self, request): + """複数圃場の作付け計画を一括更新""" + field_ids = request.data.get('field_ids', []) + year = request.data.get('year') + crop_id = request.data.get('crop') + variety_id = request.data.get('variety') + + if not field_ids or not year or not crop_id: + return Response({'error': 'field_ids, year, crop are required'}, status=status.HTTP_400_BAD_REQUEST) + + try: + crop = Crop.objects.get(id=crop_id) + except Crop.DoesNotExist: + return Response({'error': 'Crop not found'}, status=status.HTTP_400_BAD_REQUEST) + + variety = None + if variety_id: + try: + variety = Variety.objects.get(id=variety_id) + except Variety.DoesNotExist: + pass + + updated = 0 + created = 0 + for field_id in field_ids: + plan, was_created = Plan.objects.update_or_create( + field_id=field_id, + year=year, + defaults={'crop': crop, 'variety': variety} + ) + if was_created: + created += 1 + else: + updated += 1 + + return Response({'created': created, 'updated': updated, 'total': created + updated}) + @action(detail=False, methods=['get']) def get_crops_with_varieties(self, request): crops = Crop.objects.prefetch_related('varieties').all() diff --git a/document/06_ドキュメントvs実装_差異レポート.md b/document/06_ドキュメントvs実装_差異レポート.md index 7504f01..200264e 100644 --- a/document/06_ドキュメントvs実装_差異レポート.md +++ b/document/06_ドキュメントvs実装_差異レポート.md @@ -19,14 +19,13 @@ --- -### A-2: チェックボックスによる一括操作 +### ~~A-2: チェックボックスによる一括操作~~ ✅ 対応済み -- **ドキュメント**: 画面設計書 画面3 - 各行にチェックボックス、複数選択→一括割当 -- **実装**: チェックボックスなし、一括操作UI なし -- **補足**: Backend には `POST /api/plans/bulk_update/` APIが既に存在する -- **状態**: 🔜 未着手 - -**対応方針**: 利便性向上の為必要です。 +- **対応内容**: + - バックエンド: `POST /api/plans/bulk_update/` API追加(field_ids, year, crop, variety を受けて一括 update_or_create) + - フロントエンド: 作付け計画画面(/allocation)にチェックボックス列追加、全選択/個別選択 + - 一括操作バー: 選択件数表示、作物・品種セレクタ、「一括設定」ボタン、確認ダイアログ付き +- **対応日**: 2026-02-19 --- @@ -226,7 +225,7 @@ | カテゴリ | 項目 | 状態 | |---------|------|------| | A-1 | ダッシュボード画面 | 🔜 未着手 | -| A-2 | チェックボックス一括操作 | 🔜 未着手 | +| A-2 | チェックボックス一括操作 | ✅ 完了 | | A-3 | 前年度コピーボタン | ✅ 完了 | | A-4 | 品種インライン追加・削除 | ✅ 完了 | | A-5 | PDFプレビュー | ✅ 完了 | diff --git a/frontend/src/app/allocation/page.tsx b/frontend/src/app/allocation/page.tsx index 7fbbaec..f4eefb2 100644 --- a/frontend/src/app/allocation/page.tsx +++ b/frontend/src/app/allocation/page.tsx @@ -4,7 +4,7 @@ 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, ArrowUp, ArrowDown, Copy, Settings, Trash2 } from 'lucide-react'; +import { Menu, X, BarChart3, ChevronRight, ChevronDown, ArrowUp, ArrowDown, Copy, Settings, Trash2, CheckSquare } from 'lucide-react'; interface SummaryItem { cropId: number; @@ -35,6 +35,10 @@ export default function AllocationPage() { const [newVarietyName, setNewVarietyName] = useState(''); const [showVarietyManager, setShowVarietyManager] = useState(false); const [managerCropId, setManagerCropId] = useState(null); + const [selectedFields, setSelectedFields] = useState>(new Set()); + const [bulkCropId, setBulkCropId] = useState(0); + const [bulkVarietyId, setBulkVarietyId] = useState(0); + const [bulkUpdating, setBulkUpdating] = useState(false); useEffect(() => { fetchData(); @@ -321,6 +325,47 @@ export default function AllocationPage() { } }; + const toggleFieldSelection = (fieldId: number) => { + setSelectedFields((prev) => { + const next = new Set(prev); + if (next.has(fieldId)) next.delete(fieldId); + else next.add(fieldId); + return next; + }); + }; + + const toggleAllFields = () => { + if (selectedFields.size === sortedFields.length) { + setSelectedFields(new Set()); + } else { + setSelectedFields(new Set(sortedFields.map((f) => f.id))); + } + }; + + const handleBulkUpdate = async () => { + if (selectedFields.size === 0 || !bulkCropId) return; + if (!confirm(`選択した${selectedFields.size}件の圃場に一括で作物・品種を設定します。\n既存の設定は上書きされます。実行しますか?`)) return; + + setBulkUpdating(true); + try { + await api.post('/plans/bulk_update/', { + field_ids: Array.from(selectedFields), + year, + crop: bulkCropId, + variety: bulkVarietyId || null, + }); + setSelectedFields(new Set()); + setBulkCropId(0); + setBulkVarietyId(0); + await fetchData(); + } catch (error) { + console.error('Bulk update failed:', error); + alert('一括更新に失敗しました'); + } finally { + setBulkUpdating(false); + } + }; + const handleCopyFromPreviousYear = async () => { const fromYear = year - 1; if (!confirm(`${fromYear}年度の作付け計画を${year}年度にコピーします。\n既に設定済みの圃場はスキップされます。\n実行しますか?`)) return; @@ -534,11 +579,63 @@ export default function AllocationPage() {

+ {selectedFields.size > 0 && ( +
+ + + {selectedFields.size}件選択中 + + + {bulkCropId > 0 && ( + + )} + + +
+ )} +
+ {sortType === 'custom' && ( + + {sortType === 'custom' && (
+ 0} + onChange={toggleAllFields} + className="rounded border-gray-300 text-green-600 focus:ring-green-500" + /> + 順序 @@ -571,7 +668,15 @@ export default function AllocationPage() { const selectedVarietyId = plan?.variety || 0; return ( -
+ toggleFieldSelection(field.id)} + className="rounded border-gray-300 text-green-600 focus:ring-green-500" + /> +