見直し前の最終

This commit is contained in:
Akira
2026-02-16 13:45:16 +09:00
parent 4486722949
commit 9c21caa017
8 changed files with 953 additions and 160 deletions

View File

@@ -39,7 +39,7 @@ export default function AllocationPage() {
if (!background) setLoading(true);
try {
const [fieldsRes, cropsRes, plansRes] = await Promise.all([
api.get('/fields/'),
api.get('/fields/?ordering=group_name,display_order,id'),
api.get('/plans/crops/'),
api.get(`/plans/?year=${year}`),
]);
@@ -215,7 +215,6 @@ export default function AllocationPage() {
};
const handleGroupChange = async (fieldId: number, groupName: string) => {
// ローカル状態を先に更新(並び替え防止)
setFields(prev => prev.map(f =>
f.id === fieldId ? { ...f, group_name: groupName || null } : f
));
@@ -226,7 +225,6 @@ export default function AllocationPage() {
});
} catch (error) {
console.error('Failed to save group:', error);
// エラー時は再取得
await fetchData(true);
}
};
@@ -448,155 +446,155 @@ export default function AllocationPage() {
</p>
</div>
) : (
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-sm text-blue-800">
💡
</p>
</div>
)}
<>
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-sm text-blue-800">
💡
</p>
</div>
{sortedFields.length > 0 && (
<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>
{sortType === 'custom' && (
<th className="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-20">
<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>
{sortType === 'custom' && (
<th className="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-20">
</th>
)}
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
)}
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
()
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{sortedFields.map((field, index) => {
const plan = getPlanForField(field.id);
const selectedCropId = plan?.crop || 0;
const selectedVarietyId = plan?.variety || 0;
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
()
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{sortedFields.map((field, index) => {
const plan = getPlanForField(field.id);
const selectedCropId = plan?.crop || 0;
const selectedVarietyId = plan?.variety || 0;
return (
<tr key={field.id} className="hover:bg-gray-50">
{sortType === 'custom' && (
<td className="px-2 py-4 whitespace-nowrap">
<div className="flex items-center gap-1">
<button
onClick={() => moveUp(index)}
disabled={index === 0 || saving === field.id}
className="p-1 hover:bg-gray-100 rounded disabled:opacity-30"
title="上へ移動"
>
<ArrowUp className="h-4 w-4" />
</button>
<button
onClick={() => moveDown(index)}
disabled={index === sortedFields.length - 1 || saving === field.id}
className="p-1 hover:bg-gray-100 rounded disabled:opacity-30"
title="下へ移動"
>
<ArrowDown className="h-4 w-4" />
</button>
return (
<tr key={field.id} className="hover:bg-gray-50">
{sortType === 'custom' && (
<td className="px-2 py-4 whitespace-nowrap">
<div className="flex items-center gap-1">
<button
onClick={() => moveUp(index)}
disabled={index === 0 || saving === field.id}
className="p-1 hover:bg-gray-100 rounded disabled:opacity-30"
title="上へ移動"
>
<ArrowUp className="h-4 w-4" />
</button>
<button
onClick={() => moveDown(index)}
disabled={index === sortedFields.length - 1 || saving === field.id}
className="p-1 hover:bg-gray-100 rounded disabled:opacity-30"
title="下へ移動"
>
<ArrowDown className="h-4 w-4" />
</button>
</div>
</td>
)}
<td className="px-4 py-4 whitespace-nowrap">
<input
list="group-options"
value={field.group_name || ''}
onChange={(e) => handleGroupChange(field.id, e.target.value)}
disabled={saving === field.id}
placeholder="選択または入力"
className="w-36 px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500 disabled:opacity-50"
/>
<datalist id="group-options">
{groupOptions.map((g) => (
<option key={g} value={g} />
))}
</datalist>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{field.name}
</div>
<div className="text-xs text-gray-500">
{field.address}
</div>
</td>
)}
<td className="px-4 py-4 whitespace-nowrap">
<input
list="group-options"
value={field.group_name || ''}
onChange={(e) => handleGroupChange(field.id, e.target.value)}
disabled={saving === field.id}
placeholder="選択または入力"
className="w-36 px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500 disabled:opacity-50"
/>
<datalist id="group-options">
{groupOptions.map((g) => (
<option key={g} value={g} />
))}
</datalist>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{field.name}
</div>
<div className="text-xs text-gray-500">
{field.address}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{field.area_tan}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<select
value={selectedCropId || ''}
onChange={(e) =>
handleCropChange(field.id, e.target.value)
}
disabled={saving === field.id}
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 text-sm disabled:opacity-50"
>
<option value=""></option>
{crops.map((crop) => (
<option key={crop.id} value={crop.id}>
{crop.name}
</option>
))}
</select>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<select
value={selectedVarietyId || ''}
onChange={(e) =>
handleVarietyChange(field.id, e.target.value)
}
disabled={
saving === field.id || !selectedCropId
}
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 text-sm disabled:opacity-50 disabled:bg-gray-100"
>
<option value=""></option>
{getVarietiesForCrop(selectedCropId).map((variety) => (
<option key={variety.id} value={variety.id}>
{variety.name}
</option>
))}
</select>
</td>
<td className="px-6 py-4">
<input
type="text"
value={plan?.notes || ''}
onChange={(e) =>
handleNotesChange(field.id, e.target.value)
}
disabled={saving === field.id || !plan}
placeholder="備考を入力"
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 text-sm w-full disabled:opacity-50 disabled:bg-gray-100"
/>
</td>
</tr>
);
})}
</tbody>
</table>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{field.area_tan}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<select
value={selectedCropId || ''}
onChange={(e) =>
handleCropChange(field.id, e.target.value)
}
disabled={saving === field.id}
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 text-sm disabled:opacity-50"
>
<option value=""></option>
{crops.map((crop) => (
<option key={crop.id} value={crop.id}>
{crop.name}
</option>
))}
</select>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<select
value={selectedVarietyId || ''}
onChange={(e) =>
handleVarietyChange(field.id, e.target.value)
}
disabled={
saving === field.id || !selectedCropId
}
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 text-sm disabled:opacity-50 disabled:bg-gray-100"
>
<option value=""></option>
{getVarietiesForCrop(selectedCropId).map((variety) => (
<option key={variety.id} value={variety.id}>
{variety.name}
</option>
))}
</select>
</td>
<td className="px-6 py-4">
<input
type="text"
value={plan?.notes || ''}
onChange={(e) =>
handleNotesChange(field.id, e.target.value)
}
disabled={saving === field.id || !plan}
placeholder="備考を入力"
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 text-sm w-full disabled:opacity-50 disabled:bg-gray-100"
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
</>
)}
</div>
</div>

View File

@@ -23,6 +23,7 @@ export default function EditFieldPage() {
area_tan: '',
area_m2: '',
owner_name: '',
group_name: '',
});
useEffect(() => {
@@ -39,6 +40,7 @@ export default function EditFieldPage() {
area_tan: field.area_tan?.toString() || '',
area_m2: field.area_m2?.toString() || '',
owner_name: field.owner_name || '',
group_name: field.group_name || '',
});
} catch (err: unknown) {
console.error('Failed to fetch field:', err);
@@ -68,6 +70,7 @@ export default function EditFieldPage() {
area_tan: formData.area_tan ? parseFloat(formData.area_tan) : null,
area_m2: formData.area_m2 ? parseInt(formData.area_m2) : null,
owner_name: formData.owner_name || null,
group_name: formData.group_name || null,
};
await api.patch(`/fields/${fieldId}/`, data);
@@ -213,6 +216,26 @@ export default function EditFieldPage() {
/>
</div>
<div>
<label htmlFor="group_name" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="text"
id="group_name"
name="group_name"
value={formData.group_name}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
placeholder="例Aエリア"
/>
</div>
<div className="pt-4">
placeholder="例:山田太郎"
/>
</div>
<div className="pt-4">
<button
type="submit"

View File

@@ -5,23 +5,27 @@ import { useRouter } from 'next/navigation';
import { api } from '@/lib/api';
import { Field } from '@/types';
import Navbar from '@/components/Navbar';
import { Plus, Pencil, Trash2 } from 'lucide-react';
import { Plus, Pencil, Trash2, ArrowUp, ArrowDown } from 'lucide-react';
export default function FieldsPage() {
const router = useRouter();
const [fields, setFields] = useState<Field[]>([]);
const [loading, setLoading] = useState(true);
const [deleting, setDeleting] = useState<number | null>(null);
const [uniqueGroups, setUniqueGroups] = useState<string[]>([]);
const [sortOrder, setSortOrder] = useState('group_name,display_order,id');
useEffect(() => {
fetchFields();
}, []);
}, [sortOrder]);
const fetchFields = async () => {
setLoading(true);
try {
const response = await api.get('/fields/');
const response = await api.get(`/fields/?ordering=${sortOrder}`);
setFields(response.data);
const groups = [...new Set(response.data.map((f: Field) => f.group_name).filter(Boolean))] as string[];
setUniqueGroups(groups.sort());
} catch (error) {
console.error('Failed to fetch fields:', error);
} finally {
@@ -46,6 +50,52 @@ export default function FieldsPage() {
}
};
const handleGroupChange = async (fieldId: number, newGroup: string) => {
try {
await api.patch(`/fields/${fieldId}/`, {
group_name: newGroup || null
});
if (newGroup && !uniqueGroups.includes(newGroup)) {
setUniqueGroups([...uniqueGroups, newGroup].sort());
}
if (sortOrder !== 'id') {
await fetchFields();
}
} catch (error) {
console.error('Failed to update group:', error);
alert('グループの更新に失敗しました');
}
};
const handleMoveOrder = async (index: number, direction: 'up' | 'down') => {
const newIndex = direction === 'up' ? index - 1 : index + 1;
if (newIndex < 0 || newIndex >= fields.length) return;
if (sortOrder !== 'display_order,group_name,id') {
setSortOrder('display_order,group_name,id');
return;
}
const currentField = fields[index];
const targetField = fields[newIndex];
const currentOrder = currentField.display_order ?? 0;
const targetOrder = targetField.display_order ?? 0;
try {
await api.patch(`/fields/${currentField.id}/`, { display_order: targetOrder });
await api.patch(`/fields/${targetField.id}/`, { display_order: currentOrder });
await fetchFields();
} catch (error) {
console.error('Failed to reorder:', error);
alert('順序の変更に失敗しました');
}
};
const isDisplayOrderMode = sortOrder === 'display_order,group_name,id';
if (loading) {
return (
<div className="min-h-screen bg-gray-50">
@@ -63,13 +113,27 @@ export default function FieldsPage() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-6 flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900"></h1>
<button
onClick={() => router.push('/fields/new')}
className="flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
>
<Plus className="h-4 w-4 mr-2" />
</button>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<label className="text-sm text-gray-600">:</label>
<select
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value)}
className="border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-green-500"
>
<option value="group_name,display_order,id"></option>
<option value="display_order,group_name,id"></option>
<option value="id"></option>
</select>
</div>
<button
onClick={() => router.push('/fields/new')}
className="flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
>
<Plus className="h-4 w-4 mr-2" />
</button>
</div>
</div>
{fields.length === 0 ? (
@@ -78,13 +142,24 @@ export default function FieldsPage() {
</div>
) : (
<div className="bg-white rounded-lg shadow overflow-hidden">
<datalist id="groups">
{uniqueGroups.map((group) => (
<option key={group} value={group} />
))}
</datalist>
<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 text-center text-xs font-medium text-gray-500 uppercase tracking-wider w-16">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
@@ -103,13 +178,42 @@ export default function FieldsPage() {
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{fields.map((field) => (
{fields.map((field, index) => (
<tr key={field.id} className="hover:bg-gray-50">
<td className="px-2 py-4 whitespace-nowrap text-center">
<div className="flex items-center justify-center space-x-1">
<button
onClick={() => handleMoveOrder(index, 'up')}
disabled={!isDisplayOrderMode || index === 0}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
title={!isDisplayOrderMode ? "表示順優先モードで操作してください" : "上へ移動"}
>
<ArrowUp className="h-4 w-4" />
</button>
<button
onClick={() => handleMoveOrder(index, 'down')}
disabled={!isDisplayOrderMode || index === fields.length - 1}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
title={!isDisplayOrderMode ? "表示順優先モードで操作してください" : "下へ移動"}
>
<ArrowDown className="h-4 w-4" />
</button>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{field.name}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<input
list="groups"
defaultValue={field.group_name || ''}
onBlur={(e) => handleGroupChange(field.id, e.target.value)}
placeholder="グループ名"
className="w-32 text-sm border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-green-500"
/>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-500">
{field.address || '-'}