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
| + 0} + onChange={toggleAllFields} + className="rounded border-gray-300 text-green-600 focus:ring-green-500" + /> + | {sortType === 'custom' && (順序 @@ -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" + /> + | {sortType === 'custom' && (
|