ソートできるようにしました。page.tsx
畔塗画面の対象圃場一覧で、圃場 / 面積 / グループ / 品種 の各ヘッダーを押すと昇順・降順を切り替えられます。初期状態は 圃場名昇順 です。選択状態はそのまま維持されるので、並べ替えてもチェックが外れることはありません。 必要なら次に、ソートだけでなく検索欄も足せます。圃場数が多いなら検索もかなり効きます。
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Suspense, useEffect, useMemo, useState } from 'react';
|
import { Suspense, useEffect, useMemo, useState } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
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 Navbar from '@/components/Navbar';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
@@ -18,6 +18,9 @@ type FormState = {
|
|||||||
selectedFieldIds: Set<number>;
|
selectedFieldIds: Set<number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SortKey = 'field_name' | 'field_area_tan' | 'group_name' | 'variety_name';
|
||||||
|
type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
const extractErrorMessage = (error: any) => {
|
const extractErrorMessage = (error: any) => {
|
||||||
const data = error?.response?.data;
|
const data = error?.response?.data;
|
||||||
if (!data) return '保存に失敗しました。';
|
if (!data) return '保存に失敗しました。';
|
||||||
@@ -64,6 +67,8 @@ function LeveeWorkPageContent() {
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [openedFromQuery, setOpenedFromQuery] = useState(false);
|
const [openedFromQuery, setOpenedFromQuery] = useState(false);
|
||||||
|
const [sortKey, setSortKey] = useState<SortKey>('field_name');
|
||||||
|
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(YEAR_KEY, String(year));
|
localStorage.setItem(YEAR_KEY, String(year));
|
||||||
@@ -173,10 +178,48 @@ function LeveeWorkPageContent() {
|
|||||||
|
|
||||||
const selectedCount = form?.selectedFieldIds.size ?? 0;
|
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(() => {
|
const selectedCandidates = useMemo(() => {
|
||||||
if (!form) return [];
|
if (!form) return [];
|
||||||
return candidates.filter((candidate) => form.selectedFieldIds.has(candidate.field_id));
|
return sortedCandidates.filter((candidate) => form.selectedFieldIds.has(candidate.field_id));
|
||||||
}, [candidates, form]);
|
}, [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 () => {
|
const handleSave = async () => {
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
@@ -377,14 +420,50 @@ function LeveeWorkPageContent() {
|
|||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<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 className="px-4 py-3 text-left font-medium text-gray-700">面積</th>
|
<button
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-700">グループ</th>
|
type="button"
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-700">品種</th>
|
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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100">
|
<tbody className="divide-y divide-gray-100">
|
||||||
{candidates.map((candidate) => {
|
{sortedCandidates.map((candidate) => {
|
||||||
const checked = form.selectedFieldIds.has(candidate.field_id);
|
const checked = form.selectedFieldIds.has(candidate.field_id);
|
||||||
return (
|
return (
|
||||||
<tr key={candidate.field_id} className={checked ? 'bg-amber-50/40' : ''}>
|
<tr key={candidate.field_id} className={checked ? 'bg-amber-50/40' : ''}>
|
||||||
|
|||||||
Reference in New Issue
Block a user