ConfirmSpreadingModal の改善点:
groupedEntries(肥料別リスト表示)→ layout(圃場×肥料のマトリクス表)に変更 ✅ 施肥計画編集画面と同じ「圃場名 / 面積(反) / 肥料列... / 合計」のテーブル構造に統一 ✅ 各セルに計画値ラベル + 実績入力欄を縦並び ✅ 列合計(肥料別)・行合計(圃場別)・総合計を追加 ✅ 計画情報サマリーカード(年度・品種・圃場数・肥料数)を追加 ✅ 操作ガイド(sky色バナー)を追加 ✅ モーダル幅を max-w-4xl → max-w-[95vw] に拡大(マトリクス表に合わせて) ✅ ドキュメント更新: document/13_マスタードキュメント_施肥計画編.md — 在庫引当・散布確定・確定取消 API を追記 ✅ 改善案/在庫管理機能実装案.md — 微修正 ✅
This commit is contained in:
@@ -16,6 +16,7 @@ interface ConfirmSpreadingModalProps {
|
||||
type ActualMap = Record<string, string>;
|
||||
|
||||
const entryKey = (fieldId: number, fertilizerId: number) => `${fieldId}-${fertilizerId}`;
|
||||
type EntryMatrix = Record<number, Record<number, string>>;
|
||||
|
||||
export default function ConfirmSpreadingModal({
|
||||
plan,
|
||||
@@ -40,32 +41,44 @@ export default function ConfirmSpreadingModal({
|
||||
setError(null);
|
||||
}, [isOpen, plan]);
|
||||
|
||||
const groupedEntries = useMemo(() => {
|
||||
const layout = useMemo(() => {
|
||||
if (!plan) {
|
||||
return [];
|
||||
return {
|
||||
fields: [] as { id: number; name: string; areaTan: string | undefined }[],
|
||||
fertilizers: [] as { id: number; name: string }[],
|
||||
planned: {} as EntryMatrix,
|
||||
};
|
||||
}
|
||||
|
||||
const groups = new Map<
|
||||
number,
|
||||
{ fertilizerId: number; fertilizerName: string; entries: FertilizationPlan['entries'] }
|
||||
>();
|
||||
const fieldMap = new Map<number, { id: number; name: string; areaTan: string | undefined }>();
|
||||
const fertilizerMap = new Map<number, { id: number; name: string }>();
|
||||
const planned: EntryMatrix = {};
|
||||
|
||||
plan.entries.forEach((entry) => {
|
||||
const existing = groups.get(entry.fertilizer);
|
||||
if (existing) {
|
||||
existing.entries.push(entry);
|
||||
return;
|
||||
if (!fieldMap.has(entry.field)) {
|
||||
fieldMap.set(entry.field, {
|
||||
id: entry.field,
|
||||
name: entry.field_name ?? `圃場ID:${entry.field}`,
|
||||
areaTan: entry.field_area_tan,
|
||||
});
|
||||
}
|
||||
groups.set(entry.fertilizer, {
|
||||
fertilizerId: entry.fertilizer,
|
||||
fertilizerName: entry.fertilizer_name ?? `肥料ID:${entry.fertilizer}`,
|
||||
entries: [entry],
|
||||
});
|
||||
if (!fertilizerMap.has(entry.fertilizer)) {
|
||||
fertilizerMap.set(entry.fertilizer, {
|
||||
id: entry.fertilizer,
|
||||
name: entry.fertilizer_name ?? `肥料ID:${entry.fertilizer}`,
|
||||
});
|
||||
}
|
||||
if (!planned[entry.field]) {
|
||||
planned[entry.field] = {};
|
||||
}
|
||||
planned[entry.field][entry.fertilizer] = String(entry.bags);
|
||||
});
|
||||
|
||||
return Array.from(groups.values()).sort((a, b) =>
|
||||
a.fertilizerName.localeCompare(b.fertilizerName, 'ja')
|
||||
);
|
||||
return {
|
||||
fields: Array.from(fieldMap.values()),
|
||||
fertilizers: Array.from(fertilizerMap.values()),
|
||||
planned,
|
||||
};
|
||||
}, [plan]);
|
||||
|
||||
if (!isOpen || !plan) {
|
||||
@@ -103,16 +116,33 @@ export default function ConfirmSpreadingModal({
|
||||
}
|
||||
};
|
||||
|
||||
const numericValue = (value: string | undefined) => {
|
||||
const parsed = parseFloat(value ?? '0');
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
};
|
||||
|
||||
const actualTotalByField = (fieldId: number) =>
|
||||
layout.fertilizers.reduce(
|
||||
(sum, fertilizer) => sum + numericValue(actuals[entryKey(fieldId, fertilizer.id)]),
|
||||
0
|
||||
);
|
||||
|
||||
const actualTotalByFertilizer = (fertilizerId: number) =>
|
||||
layout.fields.reduce(
|
||||
(sum, field) => sum + numericValue(actuals[entryKey(field.id, fertilizerId)]),
|
||||
0
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/40 px-4">
|
||||
<div className="max-h-[90vh] w-full max-w-4xl overflow-hidden rounded-2xl bg-white shadow-2xl">
|
||||
<div className="max-h-[92vh] w-full max-w-[95vw] overflow-hidden rounded-2xl bg-white shadow-2xl">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
散布確定: 「{plan.name}」
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
実績数量を確認して、一括で reserve から use へ変換します。
|
||||
施肥計画編集と同じ並びで、各セルの計画値を確認しながら実績数量を入力します。
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -123,40 +153,86 @@ export default function ConfirmSpreadingModal({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[calc(90vh-144px)] overflow-y-auto px-6 py-5">
|
||||
<div className="max-h-[calc(92vh-144px)] overflow-y-auto bg-gray-50 px-6 py-5">
|
||||
{error && (
|
||||
<div className="mb-4 rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
{groupedEntries.map((group) => (
|
||||
<section key={group.fertilizerId}>
|
||||
<h3 className="mb-3 text-base font-semibold text-gray-800">
|
||||
肥料: {group.fertilizerName}
|
||||
</h3>
|
||||
<div className="overflow-hidden rounded-xl border border-gray-200">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50">
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="px-4 py-3 text-left font-medium text-gray-700">圃場</th>
|
||||
<th className="px-4 py-3 text-right font-medium text-gray-700">計画</th>
|
||||
<th className="px-4 py-3 text-right font-medium text-gray-700">実績</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{group.entries.map((entry) => {
|
||||
const key = entryKey(entry.field, entry.fertilizer);
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td className="px-4 py-3 text-gray-800">
|
||||
{entry.field_name ?? `圃場ID:${entry.field}`}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-gray-600">
|
||||
{entry.bags}袋
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="mb-4 rounded-lg bg-white p-4 shadow">
|
||||
<div className="grid gap-3 text-sm text-gray-700 sm:grid-cols-4">
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">年度</div>
|
||||
<div className="font-medium">{plan.year}年度</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">作物 / 品種</div>
|
||||
<div className="font-medium">
|
||||
{plan.crop_name} / {plan.variety_name}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">対象圃場</div>
|
||||
<div className="font-medium">{plan.field_count}筆</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">肥料数</div>
|
||||
<div className="font-medium">{plan.fertilizer_count}種</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3 rounded-lg border border-sky-200 bg-sky-50 px-4 py-3 text-xs text-sky-800">
|
||||
各セルの灰色表示が計画値、入力欄が散布実績です。「0」を入力したセルは未散布として引当解除されます。
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-lg bg-white shadow">
|
||||
<table className="min-w-full text-sm border-collapse">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="border border-gray-200 px-4 py-3 text-left font-medium text-gray-700 whitespace-nowrap">
|
||||
圃場名
|
||||
</th>
|
||||
<th className="border border-gray-200 px-3 py-3 text-right font-medium text-gray-700 whitespace-nowrap">
|
||||
面積(反)
|
||||
</th>
|
||||
{layout.fertilizers.map((fertilizer) => (
|
||||
<th
|
||||
key={fertilizer.id}
|
||||
className="border border-gray-200 px-3 py-2 text-center font-medium text-gray-700 whitespace-nowrap"
|
||||
>
|
||||
<div>{fertilizer.name}</div>
|
||||
<div className="mt-0.5 text-[11px] font-normal text-gray-400">
|
||||
計画 / 実績
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
<th className="border border-gray-200 px-3 py-3 text-right font-medium text-gray-700 whitespace-nowrap">
|
||||
実績合計
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{layout.fields.map((field) => (
|
||||
<tr key={field.id} className="hover:bg-gray-50">
|
||||
<td className="border border-gray-200 px-4 py-2 whitespace-nowrap text-gray-800">
|
||||
{field.name}
|
||||
</td>
|
||||
<td className="border border-gray-200 px-3 py-2 text-right text-gray-600 whitespace-nowrap">
|
||||
{field.areaTan ?? '-'}
|
||||
</td>
|
||||
{layout.fertilizers.map((fertilizer) => {
|
||||
const key = entryKey(field.id, fertilizer.id);
|
||||
const planned = layout.planned[field.id]?.[fertilizer.id];
|
||||
const hasEntry = planned !== undefined;
|
||||
return (
|
||||
<td key={key} className="border border-gray-200 px-2 py-2">
|
||||
{hasEntry ? (
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<span className="text-[11px] text-gray-400">
|
||||
計画 {planned}
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -168,17 +244,45 @@ export default function ConfirmSpreadingModal({
|
||||
[key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="w-24 rounded-lg border border-gray-300 px-3 py-2 text-right text-sm focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-200"
|
||||
className="w-20 rounded-md border border-gray-300 px-2 py-1 text-right text-sm focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-200"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-gray-300">-</div>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td className="border border-gray-200 px-3 py-2 text-right font-medium text-gray-700">
|
||||
{actualTotalByField(field.id).toFixed(2)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot className="bg-gray-50 font-semibold">
|
||||
<tr>
|
||||
<td className="border border-gray-200 px-4 py-2">合計</td>
|
||||
<td className="border border-gray-200 px-3 py-2 text-right text-gray-500">
|
||||
{layout.fields
|
||||
.reduce((sum, field) => sum + (parseFloat(field.areaTan ?? '0') || 0), 0)
|
||||
.toFixed(2)}
|
||||
</td>
|
||||
{layout.fertilizers.map((fertilizer) => (
|
||||
<td
|
||||
key={fertilizer.id}
|
||||
className="border border-gray-200 px-3 py-2 text-right text-gray-700"
|
||||
>
|
||||
{actualTotalByFertilizer(fertilizer.id).toFixed(2)}
|
||||
</td>
|
||||
))}
|
||||
<td className="border border-gray-200 px-3 py-2 text-right text-green-700">
|
||||
{layout.fields
|
||||
.reduce((sum, field) => sum + actualTotalByField(field.id), 0)
|
||||
.toFixed(2)}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user