From 6eb19f75b78afcd3a7b38a076c28b39fe1806021 Mon Sep 17 00:00:00 2001 From: Akira Date: Thu, 19 Feb 2026 13:11:13 +0900 Subject: [PATCH] =?UTF-8?q?A-7=EF=BC=88=E6=A4=9C=E7=B4=A2=E3=83=BB?= =?UTF-8?q?=E3=83=95=E3=82=A3=E3=83=AB=E3=82=BF=EF=BC=89=E3=81=AE=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=E3=81=8C=E5=AE=8C=E4=BA=86=E3=81=97=E3=81=BE=E3=81=97?= =?UTF-8?q?=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 実装内容: テキスト検索: 圃場名・住所で部分一致検索(リアルタイムフィルタリング、検索アイコン付き) 作物フィルタ: ドロップダウンで特定作物に絞り込み 未割当トグル: チェックボックスで未割当の圃場のみ表示 件数表示: フィルタ適用中は 5/39件 のように表示 チェックボックス全選択もフィルタ結果に連動 http://localhost:3000/allocation で確認できます。 --- CLAUDE.md | 3 +- .../06_ドキュメントvs実装_差異レポート.md | 17 ++-- frontend/src/app/allocation/page.tsx | 90 +++++++++++++++++-- 3 files changed, 94 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c690d37..9d07e29 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -216,7 +216,8 @@ Variety (品種マスタ) 4. **パフォーマンス**: N+1問題が一部存在(現状は問題ないが、データ増加時に対応必要) ### 🔜 次の実装タスク(優先順) -1. **A-7**: 検索・フィルタ +差異レポートの全タスク(A-1〜A-8, B-1〜B-5, C-1〜C-8, D-1〜D-4, E-1〜E-2)は全件完了。 +Phase 2 のタスクに進む段階。 詳細は `document/06_ドキュメントvs実装_差異レポート.md` を参照 diff --git a/document/06_ドキュメントvs実装_差異レポート.md b/document/06_ドキュメントvs実装_差異レポート.md index c1cab29..6f3c8e1 100644 --- a/document/06_ドキュメントvs実装_差異レポート.md +++ b/document/06_ドキュメントvs実装_差異レポート.md @@ -63,14 +63,15 @@ --- -### A-7: 作付け計画画面の検索・フィルタ +### ~~A-7: 作付け計画画面の検索・フィルタ~~ ✅ 対応済み -- **ドキュメント**: 画面設計書 画面3 - 圃場名・住所で部分一致検索、作物で絞り込み、未割当のみトグル -- **実装**: 並び替え(カスタム順/グループ順/作付け順)のみ。テキスト検索なし -- **影響**: 39筆なので目視でも探せるが、検索があると便利 -- **状態**: 🔜 未着手 - -**対応方針**: 優先度は低いですが必要です。 +- **対応内容**: + - テキスト検索: 圃場名・住所で部分一致検索(リアルタイムフィルタリング) + - 作物フィルタ: ドロップダウンで特定の作物に絞り込み + - 未割当トグル: チェックボックスで未割当の圃場のみ表示 + - フィルタ結果件数表示(例: 5/39件) + - クライアントサイドフィルタ(39筆のためAPI不要) +- **対応日**: 2026-02-19 --- @@ -233,7 +234,7 @@ | A-4 | 品種インライン追加・削除 | ✅ 完了 | | A-5 | PDFプレビュー | ✅ 完了 | | A-6 | エクスポート機能 | ✅ 完了 | -| A-7 | 検索・フィルタ | 🔜 未着手 | +| A-7 | 検索・フィルタ | ✅ 完了 | | A-8 | 圃場詳細 共済/中山間表示 | ✅ 完了 | | B-1〜B-5 | ドキュメント追記 | ✅ 完了 | | C-1〜C-8 | ドキュメント/実装の食い違い修正 | ✅ 全件完了 | diff --git a/frontend/src/app/allocation/page.tsx b/frontend/src/app/allocation/page.tsx index f4eefb2..d8d0767 100644 --- a/frontend/src/app/allocation/page.tsx +++ b/frontend/src/app/allocation/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, useMemo } from 'react'; import { api } from '@/lib/api'; import { Field, Crop, Plan } from '@/types'; import Navbar from '@/components/Navbar'; -import { Menu, X, BarChart3, ChevronRight, ChevronDown, ArrowUp, ArrowDown, Copy, Settings, Trash2, CheckSquare } from 'lucide-react'; +import { Menu, X, BarChart3, ChevronRight, ChevronDown, ArrowUp, ArrowDown, Copy, Settings, Trash2, CheckSquare, Search } from 'lucide-react'; interface SummaryItem { cropId: number; @@ -39,6 +39,9 @@ export default function AllocationPage() { const [bulkCropId, setBulkCropId] = useState(0); const [bulkVarietyId, setBulkVarietyId] = useState(0); const [bulkUpdating, setBulkUpdating] = useState(false); + const [searchText, setSearchText] = useState(''); + const [filterCropId, setFilterCropId] = useState(0); + const [filterUnassigned, setFilterUnassigned] = useState(false); useEffect(() => { fetchData(); @@ -143,6 +146,35 @@ export default function AllocationPage() { return sorted; }, [fields, sortType, plans]); + const filteredFields = useMemo(() => { + let result = sortedFields; + + if (searchText) { + const query = searchText.toLowerCase(); + result = result.filter( + (f) => + f.name.toLowerCase().includes(query) || + (f.address && f.address.toLowerCase().includes(query)) + ); + } + + if (filterCropId) { + result = result.filter((f) => { + const plan = plans.find((p) => p.field === f.id); + return plan?.crop === filterCropId; + }); + } + + if (filterUnassigned) { + result = result.filter((f) => { + const plan = plans.find((p) => p.field === f.id); + return !plan?.crop; + }); + } + + return result; + }, [sortedFields, searchText, filterCropId, filterUnassigned, plans]); + const groupOptions = useMemo(() => { const groups = new Set(); fields.forEach(f => { @@ -335,10 +367,10 @@ export default function AllocationPage() { }; const toggleAllFields = () => { - if (selectedFields.size === sortedFields.length) { + if (selectedFields.size === filteredFields.length) { setSelectedFields(new Set()); } else { - setSelectedFields(new Set(sortedFields.map((f) => f.id))); + setSelectedFields(new Set(filteredFields.map((f) => f.id))); } }; @@ -565,7 +597,51 @@ export default function AllocationPage() { - {sortedFields.length === 0 ? ( + {/* 検索・フィルタバー */} +
+
+ + setSearchText(e.target.value)} + placeholder="圃場名・住所で検索..." + className="w-full pl-9 pr-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500" + /> +
+ + + {(searchText || filterCropId || filterUnassigned) && ( + + {filteredFields.length}/{fields.length}件 + + )} +
+ + {filteredFields.length === 0 && fields.length > 0 ? ( +
+

+ 条件に一致する圃場がありません。 +

+
+ ) : sortedFields.length === 0 ? (

圃場データがありません。インポートを実行してください。 @@ -631,7 +707,7 @@ export default function AllocationPage() { 0} + checked={selectedFields.size === filteredFields.length && filteredFields.length > 0} onChange={toggleAllFields} className="rounded border-gray-300 text-green-600 focus:ring-green-500" /> @@ -662,7 +738,7 @@ export default function AllocationPage() { - {sortedFields.map((field, index) => { + {filteredFields.map((field, index) => { const plan = getPlanForField(field.id); const selectedCropId = plan?.crop || 0; const selectedVarietyId = plan?.variety || 0; @@ -690,7 +766,7 @@ export default function AllocationPage() {