見直し前の最終

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

@@ -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 || '-'}