ソートできるようにしました。page.tsx
畔塗画面の対象圃場一覧で、圃場 / 面積 / グループ / 品種 の各ヘッダーを押すと昇順・降順を切り替えられます。初期状態は 圃場名昇順 です。選択状態はそのまま維持されるので、並べ替えてもチェックが外れることはありません。 必要なら次に、ソートだけでなく検索欄も足せます。圃場数が多いなら検索もかなり効きます。
This commit is contained in:
@@ -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' : ''}>
|
||||
|
||||
Reference in New Issue
Block a user