A-2 実装内容まとめ:

バックエンド: POST /api/plans/bulk_update/ API(field_ids, year, crop, variety を受けて一括設定)
フロントエンド: チェックボックス列、全選択/個別選択、一括操作バー(作物・品種セレクタ + 確認ダイアログ)
This commit is contained in:
Akira
2026-02-19 12:29:54 +09:00
parent 8b5e0fc66e
commit cce119b1a8
4 changed files with 154 additions and 13 deletions

View File

@@ -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<number | null>(null);
const [selectedFields, setSelectedFields] = useState<Set<number>>(new Set());
const [bulkCropId, setBulkCropId] = useState<number | 0>(0);
const [bulkVarietyId, setBulkVarietyId] = useState<number | 0>(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() {
</p>
</div>
{selectedFields.size > 0 && (
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-md flex items-center gap-3 flex-wrap">
<span className="text-sm font-medium text-green-800">
<CheckSquare className="h-4 w-4 inline mr-1" />
{selectedFields.size}
</span>
<select
value={bulkCropId || ''}
onChange={(e) => { setBulkCropId(parseInt(e.target.value) || 0); setBulkVarietyId(0); }}
className="px-2 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
>
<option value=""></option>
{crops.map((crop) => (
<option key={crop.id} value={crop.id}>{crop.name}</option>
))}
</select>
{bulkCropId > 0 && (
<select
value={bulkVarietyId || ''}
onChange={(e) => setBulkVarietyId(parseInt(e.target.value) || 0)}
className="px-2 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
>
<option value=""></option>
{getVarietiesForCrop(bulkCropId).map((v) => (
<option key={v.id} value={v.id}>{v.name}</option>
))}
</select>
)}
<button
onClick={handleBulkUpdate}
disabled={!bulkCropId || bulkUpdating}
className="px-3 py-1.5 bg-green-600 text-white rounded-md text-sm hover:bg-green-700 disabled:opacity-50"
>
{bulkUpdating ? '更新中...' : '一括設定'}
</button>
<button
onClick={() => setSelectedFields(new Set())}
className="px-3 py-1.5 text-gray-600 hover:text-gray-800 text-sm"
>
</button>
</div>
)}
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-2 py-3 w-10">
<input
type="checkbox"
checked={selectedFields.size === sortedFields.length && sortedFields.length > 0}
onChange={toggleAllFields}
className="rounded border-gray-300 text-green-600 focus:ring-green-500"
/>
</th>
{sortType === 'custom' && (
<th className="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-20">
@@ -571,7 +668,15 @@ export default function AllocationPage() {
const selectedVarietyId = plan?.variety || 0;
return (
<tr key={field.id} className="hover:bg-gray-50">
<tr key={field.id} className={`hover:bg-gray-50 ${selectedFields.has(field.id) ? 'bg-green-50' : ''}`}>
<td className="px-2 py-4 w-10">
<input
type="checkbox"
checked={selectedFields.has(field.id)}
onChange={() => toggleFieldSelection(field.id)}
className="rounded border-gray-300 text-green-600 focus:ring-green-500"
/>
</td>
{sortType === 'custom' && (
<td className="px-2 py-4 whitespace-nowrap">
<div className="flex items-center gap-1">