Files
keinasystem/frontend/src/app/fertilizer/_components/FertilizerEditPage.tsx
Akira 048b17ef43 施肥計画保存時のフィードバックを改善
- 袋数が0件のまま保存しようとした場合に確認ダイアログを表示
- 保存成功後に「保存しました」アラートを表示してから一覧に遷移

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:56:02 +09:00

702 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { ChevronLeft, Plus, X, Calculator, Save, FileDown } from 'lucide-react';
import Navbar from '@/components/Navbar';
import { api } from '@/lib/api';
import { Fertilizer, FertilizationPlan, Crop, Field } from '@/types';
type CalcMethod = 'per_tan' | 'even' | 'nitrogen';
interface CalcSetting {
fertilizer_id: number;
method: CalcMethod;
param: string;
}
// field_id → fertilizer_id → bags (string)
type Matrix = Record<number, Record<number, string>>;
const METHOD_LABELS: Record<CalcMethod, string> = {
per_tan: '反当袋数',
even: '均等配分',
nitrogen: '反当チッソ',
};
const METHOD_UNIT: Record<CalcMethod, string> = {
per_tan: '袋/反',
even: '袋(総数)',
nitrogen: 'kg/反 (N)',
};
const currentYear = new Date().getFullYear();
export default function FertilizerEditPage({ planId }: { planId?: number }) {
const router = useRouter();
const isNew = !planId;
// ─── ヘッダー情報
const [name, setName] = useState('');
const [year, setYear] = useState(currentYear);
const [varietyId, setVarietyId] = useState<number | ''>('');
// ─── マスタデータ
const [crops, setCrops] = useState<Crop[]>([]);
const [allFertilizers, setAllFertilizers] = useState<Fertilizer[]>([]);
// ─── 圃場
const [selectedFields, setSelectedFields] = useState<Field[]>([]);
const [candidateFields, setCandidateFields] = useState<Field[]>([]);
const [showFieldPicker, setShowFieldPicker] = useState(false);
const [allFields, setAllFields] = useState<Field[]>([]);
// ─── 肥料(計画に使う肥料)
const [planFertilizers, setPlanFertilizers] = useState<Fertilizer[]>([]);
const [calcSettings, setCalcSettings] = useState<CalcSetting[]>([]);
const [showFertPicker, setShowFertPicker] = useState(false);
// ─── マトリクス
// calcMatrix: 自動計算の結果(参照用・変更不可の表示値)
// adjusted: ユーザーが最終確定した値(保存対象)
// roundedColumns: 四捨五入済みの肥料列ID↩ トグル用)
const [calcMatrix, setCalcMatrix] = useState<Matrix>({});
const [adjusted, setAdjusted] = useState<Matrix>({});
const [roundedColumns, setRoundedColumns] = useState<Set<number>>(new Set());
const [loading, setLoading] = useState(!isNew);
const [saving, setSaving] = useState(false);
// ─── 初期データ取得
useEffect(() => {
const init = async () => {
try {
const [cropsRes, fertsRes, fieldsRes] = await Promise.all([
api.get('/plans/crops/'),
api.get('/fertilizer/fertilizers/'),
api.get('/fields/?ordering=display_order,id'),
]);
setCrops(cropsRes.data);
setAllFertilizers(fertsRes.data);
setAllFields(fieldsRes.data);
if (!isNew && planId) {
const planRes = await api.get(`/fertilizer/plans/${planId}/`);
const plan: FertilizationPlan = planRes.data;
setName(plan.name);
setYear(plan.year);
setVarietyId(plan.variety);
const fertIds = Array.from(new Set(plan.entries.map((e) => e.fertilizer)));
const ferts = fertsRes.data.filter((f: Fertilizer) => fertIds.includes(f.id));
setPlanFertilizers(ferts);
setCalcSettings(ferts.map((f: Fertilizer) => ({ fertilizer_id: f.id, method: 'per_tan' as CalcMethod, param: '' })));
const fieldIds = Array.from(new Set(plan.entries.map((e) => e.field)));
const fields = fieldsRes.data.filter((f: Field) => fieldIds.includes(f.id));
setSelectedFields(fields);
// 保存済みの値は adjusted に復元calc値はなし
const newAdjusted: Matrix = {};
plan.entries.forEach((e) => {
if (!newAdjusted[e.field]) newAdjusted[e.field] = {};
newAdjusted[e.field][e.fertilizer] = String(e.bags);
});
setAdjusted(newAdjusted);
}
} catch (e: unknown) {
const err = e as { response?: { status?: number; data?: unknown } };
console.error('初期データ取得エラー:', err);
alert(`データの読み込みに失敗しました (${err.response?.status ?? 'network error'})\n${JSON.stringify(err.response?.data ?? '')}`);
} finally {
setLoading(false);
}
};
init();
}, [planId, isNew]);
// ─── 品種変更時: 候補圃場を取得して selectedFields をリセット
const fetchCandidates = useCallback(async (y: number, vId: number) => {
try {
const res = await api.get(`/fertilizer/candidate_fields/?year=${y}&variety_id=${vId}`);
const candidates: Field[] = res.data;
setCandidateFields(candidates);
if (isNew) setSelectedFields(candidates);
} catch (e) {
console.error(e);
}
}, [isNew]);
useEffect(() => {
if (varietyId && year) {
fetchCandidates(year, varietyId as number);
}
}, [varietyId, year, fetchCandidates]);
// ─── 肥料追加・削除
const addFertilizer = (fert: Fertilizer) => {
if (planFertilizers.find((f) => f.id === fert.id)) return;
setPlanFertilizers((prev) => [...prev, fert]);
setCalcSettings((prev) => [...prev, { fertilizer_id: fert.id, method: 'per_tan', param: '' }]);
setShowFertPicker(false);
};
const removeFertilizer = (id: number) => {
if (!confirm('この肥料を計画から削除しますか?')) return;
setPlanFertilizers((prev) => prev.filter((f) => f.id !== id));
setCalcSettings((prev) => prev.filter((s) => s.fertilizer_id !== id));
const dropCol = (m: Matrix): Matrix => {
const next = { ...m };
Object.keys(next).forEach((fid) => {
const row = { ...next[Number(fid)] };
delete row[id];
next[Number(fid)] = row;
});
return next;
};
setCalcMatrix(dropCol);
setAdjusted(dropCol);
setRoundedColumns((prev) => { const next = new Set(prev); next.delete(id); return next; });
};
// ─── 圃場追加・削除
const addField = (field: Field) => {
if (selectedFields.find((f) => f.id === field.id)) return;
setSelectedFields((prev) => [...prev, field]);
setShowFieldPicker(false);
};
const removeField = (id: number) => {
setSelectedFields((prev) => prev.filter((f) => f.id !== id));
setCalcMatrix((prev) => { const next = { ...prev }; delete next[id]; return next; });
setAdjusted((prev) => { const next = { ...prev }; delete next[id]; return next; });
};
// ─── 自動計算
const runCalc = async (setting: CalcSetting) => {
if (!setting.param) return alert('パラメータを入力してください');
if (selectedFields.length === 0) return alert('対象圃場を選択してください');
try {
const res = await api.post('/fertilizer/calculate/', {
method: setting.method,
param: parseFloat(setting.param),
fertilizer_id: setting.fertilizer_id,
field_ids: selectedFields.map((f) => f.id),
});
const results: { field_id: number; bags: number }[] = res.data;
// calc値を更新
setCalcMatrix((prev) => {
const next = { ...prev };
results.forEach(({ field_id, bags }) => {
if (!next[field_id]) next[field_id] = {};
next[field_id][setting.fertilizer_id] = String(bags);
});
return next;
});
// adjusted と丸め状態をリセット(新しい計算結果を再丸めさせる)
setAdjusted((prev) => {
const next = { ...prev };
results.forEach(({ field_id }) => {
if (next[field_id]) {
const row = { ...next[field_id] };
delete row[setting.fertilizer_id];
next[field_id] = row;
}
});
return next;
});
setRoundedColumns((prev) => { const next = new Set(prev); next.delete(setting.fertilizer_id); return next; });
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
alert(err.response?.data?.error ?? '計算に失敗しました');
}
};
const updateCalcSetting = (fertId: number, key: keyof CalcSetting, value: string) => {
setCalcSettings((prev) =>
prev.map((s) => (s.fertilizer_id === fertId ? { ...s, [key]: value } : s))
);
};
// ─── セル更新adjusted を更新)
const updateCell = (fieldId: number, fertId: number, value: string) => {
setAdjusted((prev) => {
const next = { ...prev };
if (!next[fieldId]) next[fieldId] = {};
next[fieldId][fertId] = value;
return next;
});
};
// ─── 列単位で四捨五入 / 元に戻す(トグル)
const roundColumn = (fertId: number) => {
if (roundedColumns.has(fertId)) {
// 元に戻す: adjusted からこの列を削除 → calc値が再び表示される
setAdjusted((prev) => {
const next = { ...prev };
selectedFields.forEach((field) => {
if (next[field.id]) {
const row = { ...next[field.id] };
delete row[fertId];
next[field.id] = row;
}
});
return next;
});
setRoundedColumns((prev) => { const next = new Set(prev); next.delete(fertId); return next; });
} else {
// 四捨五入: calc値を整数に丸めて adjusted に書き込む
setAdjusted((prev) => {
const next = { ...prev };
selectedFields.forEach((field) => {
const calc = calcMatrix[field.id]?.[fertId];
if (calc !== undefined && calc !== '') {
const v = parseFloat(calc);
if (!isNaN(v)) {
if (!next[field.id]) next[field.id] = {};
next[field.id][fertId] = String(Math.round(v));
}
}
});
return next;
});
setRoundedColumns((prev) => { const next = new Set(prev); next.add(fertId); return next; });
}
};
// ─── 集計adjusted 優先、なければ calc 値)
const effectiveValue = (fieldId: number, fertId: number): number => {
const adj = adjusted[fieldId]?.[fertId];
const calc = calcMatrix[fieldId]?.[fertId];
const raw = adj !== undefined && adj !== '' ? adj : calc;
const v = parseFloat(raw ?? '0');
return isNaN(v) ? 0 : v;
};
const rowTotal = (fieldId: number) =>
planFertilizers.reduce((sum, f) => sum + effectiveValue(fieldId, f.id), 0);
const colTotal = (fertId: number) =>
selectedFields.reduce((sum, f) => sum + effectiveValue(f.id, fertId), 0);
const grandTotal = planFertilizers.reduce((sum, f) => sum + colTotal(f.id), 0);
// ─── 保存adjusted 優先、なければ calc 値を使用)
const handleSave = async () => {
if (!name.trim()) return alert('計画名を入力してください');
if (!varietyId) return alert('品種を選択してください');
if (selectedFields.length === 0) return alert('圃場を1つ以上選択してください');
const entries: { field_id: number; fertilizer_id: number; bags: number }[] = [];
selectedFields.forEach((field) => {
planFertilizers.forEach((fert) => {
const adj = adjusted[field.id]?.[fert.id];
const calc = calcMatrix[field.id]?.[fert.id];
const raw = adj !== undefined && adj !== '' ? adj : calc;
if (raw) {
const v = parseFloat(raw);
if (v > 0) entries.push({ field_id: field.id, fertilizer_id: fert.id, bags: v });
}
});
});
if (entries.length === 0) {
if (!confirm('袋数が1件も入力されていません。このまま保存しますか\n後から編集画面で袋数を入力できます')) return;
}
setSaving(true);
try {
const payload = { name, year, variety: varietyId, entries };
if (isNew) {
await api.post('/fertilizer/plans/', payload);
} else {
await api.put(`/fertilizer/plans/${planId}/`, payload);
}
alert('保存しました');
router.push('/fertilizer');
} catch (e: unknown) {
const err = e as { response?: { data?: unknown } };
console.error(err);
alert('保存に失敗しました: ' + JSON.stringify(err.response?.data));
} finally {
setSaving(false);
}
};
// ─── PDF出力
const handlePdf = async () => {
if (!planId) return;
try {
const res = await api.get(`/fertilizer/plans/${planId}/pdf/`, { responseType: 'blob' });
const url = URL.createObjectURL(new Blob([res.data], { type: 'application/pdf' }));
const a = document.createElement('a');
a.href = url;
a.download = `施肥計画_${year}_${name}.pdf`;
a.click();
URL.revokeObjectURL(url);
} catch (e) {
alert('PDF出力に失敗しました');
}
};
const years = Array.from({ length: 5 }, (_, i) => currentYear + 1 - i);
const availableFerts = allFertilizers.filter((f) => !planFertilizers.find((pf) => pf.id === f.id));
const unselectedFields = allFields.filter((f) => !selectedFields.find((sf) => sf.id === f.id));
if (loading) {
return (
<div className="min-h-screen bg-gray-50">
<Navbar />
<div className="max-w-7xl mx-auto px-4 py-8 text-gray-500">...</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<Navbar />
<div className="max-w-7xl mx-auto px-4 py-8">
{/* ヘッダー */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<button onClick={() => router.push('/fertilizer')} className="text-gray-500 hover:text-gray-700">
<ChevronLeft className="h-5 w-5" />
</button>
<h1 className="text-xl font-bold text-gray-800">
{isNew ? '施肥計画 新規作成' : '施肥計画 編集'}
</h1>
</div>
<div className="flex items-center gap-2">
{!isNew && (
<button
onClick={handlePdf}
className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-700 hover:bg-gray-100"
>
<FileDown className="h-4 w-4" />
PDF出力
</button>
)}
<button
onClick={handleSave}
disabled={saving}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
>
<Save className="h-4 w-4" />
{saving ? '保存中...' : '保存'}
</button>
</div>
</div>
{/* 基本情報 */}
<div className="bg-white rounded-lg shadow p-4 mb-4 flex flex-wrap gap-4 items-end">
<div className="flex-1 min-w-48">
<label className="block text-xs font-medium text-gray-600 mb-1"></label>
<input
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="例: 2025年度 コシヒカリ 元肥"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1"></label>
<select
className="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
value={year}
onChange={(e) => setYear(parseInt(e.target.value))}
>
{years.map((y) => <option key={y} value={y}>{y}</option>)}
</select>
</div>
<div className="min-w-48">
<label className="block text-xs font-medium text-gray-600 mb-1"></label>
<select
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
value={varietyId}
onChange={(e) => setVarietyId(e.target.value ? parseInt(e.target.value) : '')}
>
<option value=""></option>
{crops.map((crop) => (
<optgroup key={crop.id} label={crop.name}>
{crop.varieties.map((v) => (
<option key={v.id} value={v.id}>{v.name}</option>
))}
</optgroup>
))}
</select>
</div>
</div>
{/* 対象圃場 */}
<div className="bg-white rounded-lg shadow p-4 mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-gray-700">
<span className="ml-2 text-gray-400 font-normal">
{selectedFields.length} /
{selectedFields.reduce((s, f) => s + parseFloat(f.area_tan), 0).toFixed(2)}
</span>
</h2>
<button
onClick={() => setShowFieldPicker(true)}
className="flex items-center gap-1 text-xs text-green-600 hover:text-green-800 border border-green-300 rounded px-2 py-1"
>
<Plus className="h-3 w-3" />
</button>
</div>
<div className="flex flex-wrap gap-2">
{selectedFields.length === 0 && (
<p className="text-sm text-gray-400">
</p>
)}
{selectedFields.map((f) => (
<span
key={f.id}
className="flex items-center gap-1 bg-green-50 border border-green-200 rounded-full px-3 py-1 text-xs text-green-800"
>
{f.name}{f.area_tan}
<button onClick={() => removeField(f.id)} className="text-green-400 hover:text-red-500">
<X className="h-3 w-3" />
</button>
</span>
))}
</div>
</div>
{/* 自動計算設定パネル */}
<div className="bg-white rounded-lg shadow p-4 mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-gray-700"></h2>
<button
onClick={() => setShowFertPicker(true)}
className="flex items-center gap-1 text-xs text-green-600 hover:text-green-800 border border-green-300 rounded px-2 py-1"
>
<Plus className="h-3 w-3" />
</button>
</div>
{planFertilizers.length === 0 ? (
<p className="text-sm text-gray-400"></p>
) : (
<div className="space-y-2">
{planFertilizers.map((fert) => {
const setting = calcSettings.find((s) => s.fertilizer_id === fert.id);
if (!setting) return null;
return (
<div key={fert.id} className="flex items-center gap-3 py-2 border-b last:border-b-0">
<span className="font-medium text-sm w-40 truncate" title={fert.name}>
{fert.name}
</span>
<select
className="border border-gray-300 rounded px-2 py-1 text-xs"
value={setting.method}
onChange={(e) => updateCalcSetting(fert.id, 'method', e.target.value)}
>
{Object.entries(METHOD_LABELS).map(([k, v]) => (
<option key={k} value={k}>{v}</option>
))}
</select>
<input
type="number"
step="0.01"
className="border border-gray-300 rounded px-2 py-1 text-xs w-24 text-right"
value={setting.param}
onChange={(e) => updateCalcSetting(fert.id, 'param', e.target.value)}
placeholder="値"
/>
<span className="text-xs text-gray-500 w-24">{METHOD_UNIT[setting.method]}</span>
<button
onClick={() => runCalc(setting)}
className="flex items-center gap-1 text-xs bg-blue-50 border border-blue-300 text-blue-700 rounded px-3 py-1 hover:bg-blue-100"
>
<Calculator className="h-3 w-3" />
</button>
<button
onClick={() => removeFertilizer(fert.id)}
className="ml-auto text-gray-300 hover:text-red-500"
>
<X className="h-4 w-4" />
</button>
</div>
);
})}
</div>
)}
</div>
{/* マトリクス表 */}
{selectedFields.length > 0 && planFertilizers.length > 0 && (
<div className="bg-white rounded-lg shadow overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead className="bg-gray-50">
<tr>
<th className="text-left px-4 py-3 border border-gray-200 font-medium text-gray-700 whitespace-nowrap"></th>
<th className="text-right px-3 py-3 border border-gray-200 font-medium text-gray-700 whitespace-nowrap">()</th>
{planFertilizers.map((f) => {
const isRounded = roundedColumns.has(f.id);
return (
<th key={f.id} className="text-center px-3 py-2 border border-gray-200 font-medium text-gray-700 whitespace-nowrap">
{f.name}
<span className="flex items-center justify-center gap-1.5 text-xs font-normal text-gray-400 mt-0.5">
<button
onClick={() => roundColumn(f.id)}
className={`inline-flex items-center justify-center w-5 h-5 rounded font-bold leading-none ${
isRounded
? 'bg-amber-100 text-amber-600 hover:bg-amber-200'
: 'bg-blue-100 text-blue-500 hover:bg-blue-200'
}`}
title={isRounded ? '元の計算値に戻す' : '四捨五入して整数に丸める'}
>
{isRounded ? '↩' : '≈'}
</button>
</span>
</th>
);
})}
<th className="text-right px-3 py-3 border border-gray-200 font-medium text-gray-700 whitespace-nowrap"></th>
</tr>
</thead>
<tbody>
{selectedFields.map((field) => (
<tr key={field.id} className="hover:bg-gray-50">
<td className="px-4 py-2 border border-gray-200 whitespace-nowrap">{field.name}</td>
<td className="px-3 py-2 border border-gray-200 text-right text-gray-600">{field.area_tan}</td>
{planFertilizers.map((fert) => {
const calcVal = calcMatrix[field.id]?.[fert.id];
const adjVal = adjusted[field.id]?.[fert.id];
// adjusted が設定されているときだけ灰色参照を表示(丸め後)
const showRef = adjVal !== undefined && calcVal !== undefined;
// 入力欄: adjusted → calc値 → 空
const inputValue = adjVal !== undefined ? adjVal : (calcVal ?? '');
return (
<td key={fert.id} className="px-2 py-1 border border-gray-200">
<div className="flex items-center justify-end gap-1.5">
{showRef && (
<span className="text-gray-300 text-xs tabular-nums">{calcVal}</span>
)}
<input
type="number"
step="0.1"
className="w-14 text-right border border-gray-200 rounded bg-white focus:outline-none focus:ring-1 focus:ring-green-400 px-1 py-0.5 text-sm"
value={inputValue}
onChange={(e) => updateCell(field.id, fert.id, e.target.value)}
placeholder="-"
/>
</div>
</td>
);
})}
<td className="px-3 py-2 border border-gray-200 text-right font-medium">
{rowTotal(field.id) > 0 ? rowTotal(field.id).toFixed(2) : '-'}
</td>
</tr>
))}
</tbody>
<tfoot className="bg-gray-50 font-semibold">
<tr>
<td className="px-4 py-2 border border-gray-200"></td>
<td className="px-3 py-2 border border-gray-200 text-right text-gray-500">
{selectedFields.reduce((s, f) => s + parseFloat(f.area_tan), 0).toFixed(2)}
</td>
{planFertilizers.map((f) => (
<td key={f.id} className="px-3 py-2 border border-gray-200 text-right">
{colTotal(f.id) > 0 ? colTotal(f.id).toFixed(2) : '-'}
</td>
))}
<td className="px-3 py-2 border border-gray-200 text-right text-green-700">
{grandTotal > 0 ? grandTotal.toFixed(2) : '-'}
</td>
</tr>
</tfoot>
</table>
</div>
)}
</div>
{/* 圃場選択ピッカー */}
{showFieldPicker && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[80vh] flex flex-col">
<div className="flex items-center justify-between p-4 border-b">
<h3 className="font-bold"></h3>
<button onClick={() => setShowFieldPicker(false)}><X className="h-5 w-5 text-gray-400" /></button>
</div>
<div className="overflow-y-auto flex-1 p-2">
{candidateFields.length > 0 && (
<>
<p className="text-xs text-gray-500 px-2 py-1">{year} / </p>
{candidateFields.filter((f) => !selectedFields.find((sf) => sf.id === f.id)).map((f) => (
<button
key={f.id}
onClick={() => addField(f)}
className="w-full text-left px-3 py-2 hover:bg-green-50 rounded text-sm flex justify-between"
>
<span>{f.name}</span>
<span className="text-gray-400">{f.area_tan}</span>
</button>
))}
<hr className="my-2" />
<p className="text-xs text-gray-500 px-2 py-1"></p>
</>
)}
{unselectedFields.filter((f) => !candidateFields.find((cf) => cf.id === f.id)).map((f) => (
<button
key={f.id}
onClick={() => addField(f)}
className="w-full text-left px-3 py-2 hover:bg-gray-50 rounded text-sm flex justify-between"
>
<span>{f.name}</span>
<span className="text-gray-400">{f.area_tan}</span>
</button>
))}
</div>
</div>
</div>
)}
{/* 肥料選択ピッカー */}
{showFertPicker && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[80vh] flex flex-col">
<div className="flex items-center justify-between p-4 border-b">
<h3 className="font-bold"></h3>
<button onClick={() => setShowFertPicker(false)}><X className="h-5 w-5 text-gray-400" /></button>
</div>
<div className="overflow-y-auto flex-1 p-2">
{availableFerts.length === 0 ? (
<p className="text-sm text-gray-400 px-3 py-4"></p>
) : (
availableFerts.map((f) => (
<button
key={f.id}
onClick={() => addFertilizer(f)}
className="w-full text-left px-3 py-2 hover:bg-green-50 rounded text-sm"
>
<span className="font-medium">{f.name}</span>
{f.maker && <span className="ml-2 text-gray-400 text-xs">{f.maker}</span>}
{f.nitrogen_pct && (
<span className="ml-2 text-blue-500 text-xs">N:{f.nitrogen_pct}%</span>
)}
</button>
))
)}
<div className="border-t mt-2 pt-2">
<button
onClick={() => { setShowFertPicker(false); router.push('/fertilizer/masters'); }}
className="w-full text-left px-3 py-2 text-xs text-green-600 hover:bg-green-50 rounded"
>
+
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}