A-7(検索・フィルタ)の実装が完了しました。
実装内容: テキスト検索: 圃場名・住所で部分一致検索(リアルタイムフィルタリング、検索アイコン付き) 作物フィルタ: ドロップダウンで特定作物に絞り込み 未割当トグル: チェックボックスで未割当の圃場のみ表示 件数表示: フィルタ適用中は 5/39件 のように表示 チェックボックス全選択もフィルタ結果に連動 http://localhost:3000/allocation で確認できます。
This commit is contained in:
@@ -216,7 +216,8 @@ Variety (品種マスタ)
|
|||||||
4. **パフォーマンス**: N+1問題が一部存在(現状は問題ないが、データ増加時に対応必要)
|
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` を参照
|
詳細は `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-4 | 品種インライン追加・削除 | ✅ 完了 |
|
||||||
| A-5 | PDFプレビュー | ✅ 完了 |
|
| A-5 | PDFプレビュー | ✅ 完了 |
|
||||||
| A-6 | エクスポート機能 | ✅ 完了 |
|
| A-6 | エクスポート機能 | ✅ 完了 |
|
||||||
| A-7 | 検索・フィルタ | 🔜 未着手 |
|
| A-7 | 検索・フィルタ | ✅ 完了 |
|
||||||
| A-8 | 圃場詳細 共済/中山間表示 | ✅ 完了 |
|
| A-8 | 圃場詳細 共済/中山間表示 | ✅ 完了 |
|
||||||
| B-1〜B-5 | ドキュメント追記 | ✅ 完了 |
|
| B-1〜B-5 | ドキュメント追記 | ✅ 完了 |
|
||||||
| C-1〜C-8 | ドキュメント/実装の食い違い修正 | ✅ 全件完了 |
|
| C-1〜C-8 | ドキュメント/実装の食い違い修正 | ✅ 全件完了 |
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState, useEffect, useMemo } from 'react';
|
|||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { Field, Crop, Plan } from '@/types';
|
import { Field, Crop, Plan } from '@/types';
|
||||||
import Navbar from '@/components/Navbar';
|
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 {
|
interface SummaryItem {
|
||||||
cropId: number;
|
cropId: number;
|
||||||
@@ -39,6 +39,9 @@ export default function AllocationPage() {
|
|||||||
const [bulkCropId, setBulkCropId] = useState<number | 0>(0);
|
const [bulkCropId, setBulkCropId] = useState<number | 0>(0);
|
||||||
const [bulkVarietyId, setBulkVarietyId] = useState<number | 0>(0);
|
const [bulkVarietyId, setBulkVarietyId] = useState<number | 0>(0);
|
||||||
const [bulkUpdating, setBulkUpdating] = useState(false);
|
const [bulkUpdating, setBulkUpdating] = useState(false);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [filterCropId, setFilterCropId] = useState<number | 0>(0);
|
||||||
|
const [filterUnassigned, setFilterUnassigned] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -143,6 +146,35 @@ export default function AllocationPage() {
|
|||||||
return sorted;
|
return sorted;
|
||||||
}, [fields, sortType, plans]);
|
}, [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 groupOptions = useMemo(() => {
|
||||||
const groups = new Set<string>();
|
const groups = new Set<string>();
|
||||||
fields.forEach(f => {
|
fields.forEach(f => {
|
||||||
@@ -335,10 +367,10 @@ export default function AllocationPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleAllFields = () => {
|
const toggleAllFields = () => {
|
||||||
if (selectedFields.size === sortedFields.length) {
|
if (selectedFields.size === filteredFields.length) {
|
||||||
setSelectedFields(new Set());
|
setSelectedFields(new Set());
|
||||||
} else {
|
} 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() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sortedFields.length === 0 ? (
|
{/* 検索・フィルタバー */}
|
||||||
|
<div className="mb-4 flex flex-wrap items-center gap-2">
|
||||||
|
<div className="relative flex-1 min-w-[200px] max-w-sm">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={filterCropId || ''}
|
||||||
|
onChange={(e) => { setFilterCropId(parseInt(e.target.value) || 0); setFilterUnassigned(false); }}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
<option value="">全作物</option>
|
||||||
|
{crops.map((crop) => (
|
||||||
|
<option key={crop.id} value={crop.id}>{crop.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<label className="flex items-center gap-1.5 text-sm text-gray-700 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={filterUnassigned}
|
||||||
|
onChange={(e) => { setFilterUnassigned(e.target.checked); if (e.target.checked) setFilterCropId(0); }}
|
||||||
|
className="rounded border-gray-300 text-green-600 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
未割当のみ
|
||||||
|
</label>
|
||||||
|
{(searchText || filterCropId || filterUnassigned) && (
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
{filteredFields.length}/{fields.length}件
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredFields.length === 0 && fields.length > 0 ? (
|
||||||
|
<div className="bg-white rounded-lg shadow p-8 text-center">
|
||||||
|
<p className="text-gray-500">
|
||||||
|
条件に一致する圃場がありません。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : sortedFields.length === 0 ? (
|
||||||
<div className="bg-white rounded-lg shadow p-8 text-center">
|
<div className="bg-white rounded-lg shadow p-8 text-center">
|
||||||
<p className="text-gray-500">
|
<p className="text-gray-500">
|
||||||
圃場データがありません。インポートを実行してください。
|
圃場データがありません。インポートを実行してください。
|
||||||
@@ -631,7 +707,7 @@ export default function AllocationPage() {
|
|||||||
<th className="px-2 py-3 w-10">
|
<th className="px-2 py-3 w-10">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedFields.size === sortedFields.length && sortedFields.length > 0}
|
checked={selectedFields.size === filteredFields.length && filteredFields.length > 0}
|
||||||
onChange={toggleAllFields}
|
onChange={toggleAllFields}
|
||||||
className="rounded border-gray-300 text-green-600 focus:ring-green-500"
|
className="rounded border-gray-300 text-green-600 focus:ring-green-500"
|
||||||
/>
|
/>
|
||||||
@@ -662,7 +738,7 @@ export default function AllocationPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{sortedFields.map((field, index) => {
|
{filteredFields.map((field, index) => {
|
||||||
const plan = getPlanForField(field.id);
|
const plan = getPlanForField(field.id);
|
||||||
const selectedCropId = plan?.crop || 0;
|
const selectedCropId = plan?.crop || 0;
|
||||||
const selectedVarietyId = plan?.variety || 0;
|
const selectedVarietyId = plan?.variety || 0;
|
||||||
@@ -690,7 +766,7 @@ export default function AllocationPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => moveDown(index)}
|
onClick={() => moveDown(index)}
|
||||||
disabled={index === sortedFields.length - 1 || saving === field.id}
|
disabled={index === filteredFields.length - 1 || saving === field.id}
|
||||||
className="p-1 hover:bg-gray-100 rounded disabled:opacity-30"
|
className="p-1 hover:bg-gray-100 rounded disabled:opacity-30"
|
||||||
title="下へ移動"
|
title="下へ移動"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user