A-2 実装内容まとめ:
バックエンド: POST /api/plans/bulk_update/ API(field_ids, year, crop, variety を受けて一括設定) フロントエンド: チェックボックス列、全選択/個別選択、一括操作バー(作物・品種セレクタ + 確認ダイアログ)
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user