ソートできるようにしました。page.tsx

畔塗画面の対象圃場一覧で、圃場 / 面積 / グループ / 品種 の各ヘッダーを押すと昇順・降順を切り替えられます。初期状態は 圃場名昇順 です。選択状態はそのまま維持されるので、並べ替えてもチェックが外れることはありません。

必要なら次に、ソートだけでなく検索欄も足せます。圃場数が多いなら検索もかなり効きます。
This commit is contained in:
akira
2026-04-04 12:07:41 +09:00
parent b7b9818855
commit f236fe2f90

View File

@@ -2,7 +2,7 @@
import { Suspense, useEffect, useMemo, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { ChevronLeft, PencilLine, Plus, Save, Trash2 } from 'lucide-react';
import { ArrowDown, ArrowUp, ChevronLeft, PencilLine, Plus, Save, Trash2 } from 'lucide-react';
import Navbar from '@/components/Navbar';
import { api } from '@/lib/api';
@@ -18,6 +18,9 @@ type FormState = {
selectedFieldIds: Set<number>;
};
type SortKey = 'field_name' | 'field_area_tan' | 'group_name' | 'variety_name';
type SortDirection = 'asc' | 'desc';
const extractErrorMessage = (error: any) => {
const data = error?.response?.data;
if (!data) return '保存に失敗しました。';
@@ -64,6 +67,8 @@ function LeveeWorkPageContent() {
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [openedFromQuery, setOpenedFromQuery] = useState(false);
const [sortKey, setSortKey] = useState<SortKey>('field_name');
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
useEffect(() => {
localStorage.setItem(YEAR_KEY, String(year));
@@ -173,10 +178,48 @@ function LeveeWorkPageContent() {
const selectedCount = form?.selectedFieldIds.size ?? 0;
const sortedCandidates = useMemo(() => {
const rows = [...candidates];
rows.sort((a, b) => {
let result = 0;
if (sortKey === 'field_area_tan') {
result = Number(a.field_area_tan) - Number(b.field_area_tan);
} else {
result = (a[sortKey] || '').toString().localeCompare((b[sortKey] || '').toString(), 'ja');
}
if (result === 0) {
result = a.field_name.localeCompare(b.field_name, 'ja');
}
return sortDirection === 'asc' ? result : -result;
});
return rows;
}, [candidates, sortDirection, sortKey]);
const selectedCandidates = useMemo(() => {
if (!form) return [];
return candidates.filter((candidate) => form.selectedFieldIds.has(candidate.field_id));
}, [candidates, form]);
return sortedCandidates.filter((candidate) => form.selectedFieldIds.has(candidate.field_id));
}, [form, sortedCandidates]);
const handleSort = (nextKey: SortKey) => {
if (sortKey === nextKey) {
setSortDirection((current) => (current === 'asc' ? 'desc' : 'asc'));
return;
}
setSortKey(nextKey);
setSortDirection('asc');
};
const renderSortIcon = (key: SortKey) => {
if (sortKey !== key) return null;
return sortDirection === 'asc' ? (
<ArrowUp className="h-3.5 w-3.5" />
) : (
<ArrowDown className="h-3.5 w-3.5" />
);
};
const handleSave = async () => {
if (!form) return;
@@ -377,14 +420,50 @@ function LeveeWorkPageContent() {
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left font-medium text-gray-700"></th>
<th className="px-4 py-3 text-left font-medium text-gray-700"></th>
<th className="px-4 py-3 text-left font-medium text-gray-700"></th>
<th className="px-4 py-3 text-left font-medium text-gray-700"></th>
<th className="px-4 py-3 text-left font-medium text-gray-700"></th>
<th className="px-4 py-3 text-left font-medium text-gray-700">
<button
type="button"
onClick={() => handleSort('field_name')}
className="inline-flex items-center gap-1 hover:text-gray-900"
>
{renderSortIcon('field_name')}
</button>
</th>
<th className="px-4 py-3 text-left font-medium text-gray-700">
<button
type="button"
onClick={() => handleSort('field_area_tan')}
className="inline-flex items-center gap-1 hover:text-gray-900"
>
{renderSortIcon('field_area_tan')}
</button>
</th>
<th className="px-4 py-3 text-left font-medium text-gray-700">
<button
type="button"
onClick={() => handleSort('group_name')}
className="inline-flex items-center gap-1 hover:text-gray-900"
>
{renderSortIcon('group_name')}
</button>
</th>
<th className="px-4 py-3 text-left font-medium text-gray-700">
<button
type="button"
onClick={() => handleSort('variety_name')}
className="inline-flex items-center gap-1 hover:text-gray-900"
>
{renderSortIcon('variety_name')}
</button>
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{candidates.map((candidate) => {
{sortedCandidates.map((candidate) => {
const checked = form.selectedFieldIds.has(candidate.field_id);
return (
<tr key={candidate.field_id} className={checked ? 'bg-amber-50/40' : ''}>