ConfirmSpreadingModal の改善点:
groupedEntries(肥料別リスト表示)→ layout(圃場×肥料のマトリクス表)に変更 ✅ 施肥計画編集画面と同じ「圃場名 / 面積(反) / 肥料列... / 合計」のテーブル構造に統一 ✅ 各セルに計画値ラベル + 実績入力欄を縦並び ✅ 列合計(肥料別)・行合計(圃場別)・総合計を追加 ✅ 計画情報サマリーカード(年度・品種・圃場数・肥料数)を追加 ✅ 操作ガイド(sky色バナー)を追加 ✅ モーダル幅を max-w-4xl → max-w-[95vw] に拡大(マトリクス表に合わせて) ✅ ドキュメント更新: document/13_マスタードキュメント_施肥計画編.md — 在庫引当・散布確定・確定取消 API を追記 ✅ 改善案/在庫管理機能実装案.md — 微修正 ✅
This commit is contained in:
@@ -63,7 +63,8 @@
|
|||||||
"Bash(python -m json.tool)",
|
"Bash(python -m json.tool)",
|
||||||
"Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzczNTY4MTAxLCJpYXQiOjE3NzM0ODE3MDEsImp0aSI6ImM1N2EyNzVjMzkyNTRmMjFiZmUxMzA4ZmU4ZjQ3ZWExIiwidXNlcl9pZCI6Mn0.LOZIpPOHN48YZuf7UBaDrPJfxb12hIm15QRnUPc9sYM\" __NEW_LINE_0b7fd53b80bd968a__ echo \"=== Stock summary \\(should show reserved\\) ===\" curl -s http://localhost:8000/api/materials/stock-summary/?material_type=fertilizer -H \"Authorization: Bearer $TOKEN\")",
|
"Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzczNTY4MTAxLCJpYXQiOjE3NzM0ODE3MDEsImp0aSI6ImM1N2EyNzVjMzkyNTRmMjFiZmUxMzA4ZmU4ZjQ3ZWExIiwidXNlcl9pZCI6Mn0.LOZIpPOHN48YZuf7UBaDrPJfxb12hIm15QRnUPc9sYM\" __NEW_LINE_0b7fd53b80bd968a__ echo \"=== Stock summary \\(should show reserved\\) ===\" curl -s http://localhost:8000/api/materials/stock-summary/?material_type=fertilizer -H \"Authorization: Bearer $TOKEN\")",
|
||||||
"Read(//c/Users/akira/Develop/keinasystem_t02/**)",
|
"Read(//c/Users/akira/Develop/keinasystem_t02/**)",
|
||||||
"Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzczNTY4MTAxLCJpYXQiOjE3NzM0ODE3MDEsImp0aSI6ImM1N2EyNzVjMzkyNTRmMjFiZmUxMzA4ZmU4ZjQ3ZWExIiwidXNlcl9pZCI6Mn0.LOZIpPOHN48YZuf7UBaDrPJfxb12hIm15QRnUPc9sYM\" __NEW_LINE_74a785697e4cd919__ echo \"=== After confirm: stock summary ===\" curl -s http://localhost:8000/api/materials/stock-summary/?material_type=fertilizer -H \"Authorization: Bearer $TOKEN\")"
|
"Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzczNTY4MTAxLCJpYXQiOjE3NzM0ODE3MDEsImp0aSI6ImM1N2EyNzVjMzkyNTRmMjFiZmUxMzA4ZmU4ZjQ3ZWExIiwidXNlcl9pZCI6Mn0.LOZIpPOHN48YZuf7UBaDrPJfxb12hIm15QRnUPc9sYM\" __NEW_LINE_74a785697e4cd919__ echo \"=== After confirm: stock summary ===\" curl -s http://localhost:8000/api/materials/stock-summary/?material_type=fertilizer -H \"Authorization: Bearer $TOKEN\")",
|
||||||
|
"Bash(git diff:*)"
|
||||||
],
|
],
|
||||||
"additionalDirectories": [
|
"additionalDirectories": [
|
||||||
"C:\\Users\\akira\\AppData\\Local\\Temp"
|
"C:\\Users\\akira\\AppData\\Local\\Temp"
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# マスタードキュメント:施肥計画機能
|
# マスタードキュメント:施肥計画機能
|
||||||
|
|
||||||
> **作成**: 2026-03-01
|
> **作成**: 2026-03-01
|
||||||
> **最終更新**: 2026-03-01
|
> **最終更新**: 2026-03-15
|
||||||
> **対象機能**: 施肥計画(年度×品種単位のマトリクス管理)
|
> **対象機能**: 施肥計画(年度×品種単位のマトリクス管理・在庫引当・散布確定)
|
||||||
> **実装状況**: 実装完了・本番稼働中(最終 commit deb03ef)
|
> **実装状況**: 実装完了・本番稼働中
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 概要
|
## 概要
|
||||||
|
|
||||||
農業生産者が「年度 × 品種」単位で施肥計画を立てる機能。
|
農業生産者が「年度 × 品種」単位で施肥計画を立てる機能。
|
||||||
複数圃場 × 複数肥料 × 袋数をマトリクス形式で管理し、PDF出力する。
|
複数圃場 × 複数肥料 × 袋数をマトリクス形式で管理し、PDF出力に加えて在庫引当と散布確定まで一連で扱う。
|
||||||
|
|
||||||
### 機能スコープ(IN / OUT)
|
### 機能スコープ(IN / OUT)
|
||||||
|
|
||||||
@@ -18,9 +18,11 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 肥料マスタ管理 | 肥料購入管理 |
|
| 肥料マスタ管理 | 肥料購入管理 |
|
||||||
| 施肥計画の作成・編集・削除 | 圃場への配置計画(置き場所割り当て) |
|
| 施肥計画の作成・編集・削除 | 圃場への配置計画(置き場所割り当て) |
|
||||||
| 3方式の自動計算 | 施肥作業の実績記録 |
|
| 3方式の自動計算 | 個別作業日報の詳細管理 |
|
||||||
| 作付け計画からの圃場自動取得 | |
|
| 作付け計画からの圃場自動取得 | |
|
||||||
| PDF出力(圃場×肥料マトリクス表) | |
|
| PDF出力(圃場×肥料マトリクス表) | |
|
||||||
|
| 在庫引当・引当解除 | |
|
||||||
|
| 散布確定(計画値確認 + 実績入力) | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -47,6 +49,8 @@
|
|||||||
| name | varchar(200) | required | 計画名(ユーザーが自由入力) |
|
| name | varchar(200) | required | 計画名(ユーザーが自由入力) |
|
||||||
| year | int | required | 年度 |
|
| year | int | required | 年度 |
|
||||||
| variety | FK(plans.Variety) | PROTECT | 品種(≠NULL) |
|
| variety | FK(plans.Variety) | PROTECT | 品種(≠NULL) |
|
||||||
|
| is_confirmed | bool | default=False | 散布確定済みフラグ |
|
||||||
|
| confirmed_at | datetime | nullable | 散布確定日時 |
|
||||||
| created_at | datetime | auto | |
|
| created_at | datetime | auto | |
|
||||||
| updated_at | datetime | auto | |
|
| updated_at | datetime | auto | |
|
||||||
|
|
||||||
@@ -102,6 +106,8 @@
|
|||||||
| GET | `/api/fertilizer/plans/{id}/` | 詳細取得(entries 含む) |
|
| GET | `/api/fertilizer/plans/{id}/` | 詳細取得(entries 含む) |
|
||||||
| PUT | `/api/fertilizer/plans/{id}/` | 更新(entries 全置換) |
|
| PUT | `/api/fertilizer/plans/{id}/` | 更新(entries 全置換) |
|
||||||
| DELETE | `/api/fertilizer/plans/{id}/` | 削除 |
|
| DELETE | `/api/fertilizer/plans/{id}/` | 削除 |
|
||||||
|
| POST | `/api/fertilizer/plans/{id}/confirm_spreading/` | 散布確定(引当 → 使用へ変換) |
|
||||||
|
| POST | `/api/fertilizer/plans/{id}/unconfirm/` | 散布確定取消(使用 → 引当に戻す) |
|
||||||
| GET | `/api/fertilizer/plans/{id}/pdf/` | PDF出力(application/pdf) |
|
| GET | `/api/fertilizer/plans/{id}/pdf/` | PDF出力(application/pdf) |
|
||||||
|
|
||||||
一覧レスポンス例(FertilizationPlan):
|
一覧レスポンス例(FertilizationPlan):
|
||||||
@@ -113,6 +119,8 @@
|
|||||||
"variety": 3,
|
"variety": 3,
|
||||||
"variety_name": "コシヒカリ",
|
"variety_name": "コシヒカリ",
|
||||||
"crop_name": "米",
|
"crop_name": "米",
|
||||||
|
"is_confirmed": false,
|
||||||
|
"confirmed_at": null,
|
||||||
"field_count": 12,
|
"field_count": 12,
|
||||||
"fertilizer_count": 2,
|
"fertilizer_count": 2,
|
||||||
"entries": [
|
"entries": [
|
||||||
@@ -146,6 +154,19 @@ POST/PUT リクエスト例:
|
|||||||
|
|
||||||
PUT 時は entries が全置換(削除→再作成)。entries を省略した場合は既存を維持。
|
PUT 時は entries が全置換(削除→再作成)。entries を省略した場合は既存を維持。
|
||||||
|
|
||||||
|
散布確定 API リクエスト例:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"entries": [
|
||||||
|
{"field_id": 5, "fertilizer_id": 1, "actual_bags": 2.4},
|
||||||
|
{"field_id": 6, "fertilizer_id": 1, "actual_bags": 0}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `actual_bags > 0`: 対応する引当を使用実績へ変換
|
||||||
|
- `actual_bags = 0`: 未散布として引当解除
|
||||||
|
|
||||||
### 圃場候補取得
|
### 圃場候補取得
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -269,8 +290,8 @@ GET /api/plans/crops/
|
|||||||
### 施肥計画一覧(`/fertilizer`)
|
### 施肥計画一覧(`/fertilizer`)
|
||||||
|
|
||||||
- 年度セレクタ(localStorage `fertilizerYear` で保持)
|
- 年度セレクタ(localStorage `fertilizerYear` で保持)
|
||||||
- 計画カード一覧: 計画名・作物/品種・圃場数・肥料数
|
- 計画カード一覧: 計画名・作物/品種・圃場数・肥料数・散布確定状態
|
||||||
- 操作ボタン: PDF出力・編集・削除
|
- 操作ボタン: PDF出力・編集・削除・散布確定
|
||||||
- ヘッダー: 「肥料マスタ」「新規作成」ボタン
|
- ヘッダー: 「肥料マスタ」「新規作成」ボタン
|
||||||
|
|
||||||
### 肥料マスタ(`/fertilizer/masters`)
|
### 肥料マスタ(`/fertilizer/masters`)
|
||||||
@@ -295,6 +316,12 @@ GET /api/plans/crops/
|
|||||||
6. **手動調整**: マトリクス表のセルを直接編集
|
6. **手動調整**: マトリクス表のセルを直接編集
|
||||||
7. **保存**: 「保存」ボタンで entries を一括送信
|
7. **保存**: 「保存」ボタンで entries を一括送信
|
||||||
|
|
||||||
|
#### 在庫連携・確定状態
|
||||||
|
|
||||||
|
- 肥料列ヘッダーに在庫 / 利用可能在庫 / 計画計 / 不足数を表示
|
||||||
|
- 散布確定済みの計画は情報バナーを表示し、編集操作をロック
|
||||||
|
- 「確定取消」で使用実績を引当に戻し、再編集できる
|
||||||
|
|
||||||
#### マトリクスの表示仕様
|
#### マトリクスの表示仕様
|
||||||
|
|
||||||
- 自動計算直後: セルに計算値(小数)がそのまま表示される(編集可)
|
- 自動計算直後: セルに計算値(小数)がそのまま表示される(編集可)
|
||||||
@@ -302,6 +329,15 @@ GET /api/plans/crops/
|
|||||||
- `↩` ボタン押下: 整数値を破棄し、元の計算値に戻る(参照グレー表示も消える)
|
- `↩` ボタン押下: 整数値を破棄し、元の計算値に戻る(参照グレー表示も消える)
|
||||||
- 編集中に計算を再実行すると、その肥料列の `adjusted` と `roundedColumns` がリセットされる
|
- 編集中に計算を再実行すると、その肥料列の `adjusted` と `roundedColumns` がリセットされる
|
||||||
|
|
||||||
|
### 散布確定モーダル(`/fertilizer` 一覧から起動)
|
||||||
|
|
||||||
|
- 全画面遷移ではなくモーダル表示
|
||||||
|
- 施肥計画編集と同じ視線移動になるよう、`圃場 = 行`、`肥料 = 列` のマトリクス表を採用
|
||||||
|
- 画面上部に計画名・年度・作物/品種・対象圃場数・肥料数を表示
|
||||||
|
- 各セルは「薄いグレーの計画値」+「実績入力欄」の2段表示
|
||||||
|
- 行末に圃場ごとの実績合計、表フッターに肥料別合計と総合計を表示
|
||||||
|
- `0` を入力したセルは未散布として扱い、対応する引当を解除する
|
||||||
|
|
||||||
#### State 構成
|
#### State 構成
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface ConfirmSpreadingModalProps {
|
|||||||
type ActualMap = Record<string, string>;
|
type ActualMap = Record<string, string>;
|
||||||
|
|
||||||
const entryKey = (fieldId: number, fertilizerId: number) => `${fieldId}-${fertilizerId}`;
|
const entryKey = (fieldId: number, fertilizerId: number) => `${fieldId}-${fertilizerId}`;
|
||||||
|
type EntryMatrix = Record<number, Record<number, string>>;
|
||||||
|
|
||||||
export default function ConfirmSpreadingModal({
|
export default function ConfirmSpreadingModal({
|
||||||
plan,
|
plan,
|
||||||
@@ -40,32 +41,44 @@ export default function ConfirmSpreadingModal({
|
|||||||
setError(null);
|
setError(null);
|
||||||
}, [isOpen, plan]);
|
}, [isOpen, plan]);
|
||||||
|
|
||||||
const groupedEntries = useMemo(() => {
|
const layout = useMemo(() => {
|
||||||
if (!plan) {
|
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<
|
const fieldMap = new Map<number, { id: number; name: string; areaTan: string | undefined }>();
|
||||||
number,
|
const fertilizerMap = new Map<number, { id: number; name: string }>();
|
||||||
{ fertilizerId: number; fertilizerName: string; entries: FertilizationPlan['entries'] }
|
const planned: EntryMatrix = {};
|
||||||
>();
|
|
||||||
|
|
||||||
plan.entries.forEach((entry) => {
|
plan.entries.forEach((entry) => {
|
||||||
const existing = groups.get(entry.fertilizer);
|
if (!fieldMap.has(entry.field)) {
|
||||||
if (existing) {
|
fieldMap.set(entry.field, {
|
||||||
existing.entries.push(entry);
|
id: entry.field,
|
||||||
return;
|
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) =>
|
return {
|
||||||
a.fertilizerName.localeCompare(b.fertilizerName, 'ja')
|
fields: Array.from(fieldMap.values()),
|
||||||
);
|
fertilizers: Array.from(fertilizerMap.values()),
|
||||||
|
planned,
|
||||||
|
};
|
||||||
}, [plan]);
|
}, [plan]);
|
||||||
|
|
||||||
if (!isOpen || !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 (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/40 px-4">
|
<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 className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-900">
|
<h2 className="text-lg font-semibold text-gray-900">
|
||||||
散布確定: 「{plan.name}」
|
散布確定: 「{plan.name}」
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
実績数量を確認して、一括で reserve から use へ変換します。
|
施肥計画編集と同じ並びで、各セルの計画値を確認しながら実績数量を入力します。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -123,40 +153,86 @@ export default function ConfirmSpreadingModal({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 && (
|
{error && (
|
||||||
<div className="mb-4 rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700">
|
<div className="mb-4 rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="mb-4 rounded-lg bg-white p-4 shadow">
|
||||||
{groupedEntries.map((group) => (
|
<div className="grid gap-3 text-sm text-gray-700 sm:grid-cols-4">
|
||||||
<section key={group.fertilizerId}>
|
<div>
|
||||||
<h3 className="mb-3 text-base font-semibold text-gray-800">
|
<div className="text-xs text-gray-500">年度</div>
|
||||||
肥料: {group.fertilizerName}
|
<div className="font-medium">{plan.year}年度</div>
|
||||||
</h3>
|
</div>
|
||||||
<div className="overflow-hidden rounded-xl border border-gray-200">
|
<div>
|
||||||
<table className="w-full text-sm">
|
<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">
|
<thead className="bg-gray-50">
|
||||||
<tr className="border-b border-gray-200">
|
<tr>
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-700">圃場</th>
|
<th className="border border-gray-200 px-4 py-3 text-left font-medium text-gray-700 whitespace-nowrap">
|
||||||
<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>
|
</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100">
|
<tbody>
|
||||||
{group.entries.map((entry) => {
|
{layout.fields.map((field) => (
|
||||||
const key = entryKey(entry.field, entry.fertilizer);
|
<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 (
|
return (
|
||||||
<tr key={key}>
|
<td key={key} className="border border-gray-200 px-2 py-2">
|
||||||
<td className="px-4 py-3 text-gray-800">
|
{hasEntry ? (
|
||||||
{entry.field_name ?? `圃場ID:${entry.field}`}
|
<div className="flex flex-col items-end gap-1">
|
||||||
</td>
|
<span className="text-[11px] text-gray-400">
|
||||||
<td className="px-4 py-3 text-right text-gray-600">
|
計画 {planned}
|
||||||
{entry.bags}袋
|
</span>
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-right">
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -168,17 +244,45 @@ export default function ConfirmSpreadingModal({
|
|||||||
[key]: e.target.value,
|
[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"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-gray-300">-</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
<td className="border border-gray-200 px-3 py-2 text-right font-medium text-gray-700">
|
||||||
</table>
|
{actualTotalByField(field.id).toFixed(2)}
|
||||||
</div>
|
</td>
|
||||||
</section>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1867,8 +1867,9 @@ migrations.AddField(
|
|||||||
#### 散布確定画面(新規)
|
#### 散布確定画面(新規)
|
||||||
|
|
||||||
- 施肥計画一覧から「散布確定」ボタンで遷移 or モーダル
|
- 施肥計画一覧から「散布確定」ボタンで遷移 or モーダル
|
||||||
- 圃場×肥料のマトリクスに「計画」と「実績」列を並べて表示
|
- 施肥計画編集画面と同じく「圃場 = 行」「肥料 = 列」のマトリクス表で表示
|
||||||
- 計画値がプリセットされ、修正が必要な行だけ編集する
|
- 各セルは薄いグレーの計画値 + 実績入力欄の2段表示にし、編集時の視線移動を揃える
|
||||||
|
- 行末に圃場ごとの実績合計、フッターに肥料別合計と総合計を表示する
|
||||||
- 「一括確定」ボタンで POST
|
- 「一括確定」ボタンで POST
|
||||||
|
|
||||||
#### 在庫一覧 (`/materials`)
|
#### 在庫一覧 (`/materials`)
|
||||||
|
|||||||
Reference in New Issue
Block a user