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 && (
+
+ )}
+
+
+
+ )}
+