Issue #2 に最新の理解を記録し、その内容で仕様書と実装を修正しました。
document/16_マスタードキュメント_田植え計画編.md は、「行ごとに保持するのは圃場の苗箱数」「列側に反当苗箱枚数を持つ」「種もみg/箱 は全体共通値」という前提に更新しています。コード側は backend/apps/plans/models.py と backend/apps/plans/serializers.py で計画ヘッダに seedling_boxes_per_tan を追加し、backend/apps/plans/migrations/0007_ricetransplantplan_seedling_boxes_per_tan.py も作成しました。画面は frontend/src/app/rice-transplant/_components/RiceTransplantEditPage.tsx を施肥計画寄りに組み直し、列単位のデフォルト反映と四捨五入、行ごとの苗箱数入力に寄せています。frontend/src/types/index.ts も合わせて更新済みです。 確認できたのはバックエンドの構文チェックまでで、python3 -m py_compile backend/apps/plans/models.py backend/apps/plans/serializers.py backend/apps/plans/views.py は通過しています。フロントのビルド確認まではこの環境では回していません。Issue #2 にも今回の反映内容をコメント済みです。
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('plans', '0006_rename_seedling_boxes_per_tan_to_installed_seedling_boxes'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ricetransplantplan',
|
||||||
|
name='seedling_boxes_per_tan',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=6, verbose_name='反当苗箱枚数'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -71,6 +71,12 @@ class RiceTransplantPlan(models.Model):
|
|||||||
default=0,
|
default=0,
|
||||||
verbose_name='苗箱1枚あたり種もみ(g)デフォルト',
|
verbose_name='苗箱1枚あたり種もみ(g)デフォルト',
|
||||||
)
|
)
|
||||||
|
seedling_boxes_per_tan = models.DecimalField(
|
||||||
|
max_digits=6,
|
||||||
|
decimal_places=2,
|
||||||
|
default=0,
|
||||||
|
verbose_name='反当苗箱枚数',
|
||||||
|
)
|
||||||
notes = models.TextField(blank=True, default='', verbose_name='備考')
|
notes = models.TextField(blank=True, default='', verbose_name='備考')
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ class RiceTransplantEntrySerializer(serializers.ModelSerializer):
|
|||||||
read_only=True,
|
read_only=True,
|
||||||
)
|
)
|
||||||
planned_boxes = serializers.SerializerMethodField()
|
planned_boxes = serializers.SerializerMethodField()
|
||||||
planned_seed_kg = serializers.SerializerMethodField()
|
|
||||||
default_seedling_boxes = serializers.SerializerMethodField()
|
default_seedling_boxes = serializers.SerializerMethodField()
|
||||||
|
planned_seed_kg = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RiceTransplantEntry
|
model = RiceTransplantEntry
|
||||||
@@ -60,7 +60,6 @@ class RiceTransplantEntrySerializer(serializers.ModelSerializer):
|
|||||||
'field_name',
|
'field_name',
|
||||||
'field_area_tan',
|
'field_area_tan',
|
||||||
'installed_seedling_boxes',
|
'installed_seedling_boxes',
|
||||||
'seed_grams_per_box',
|
|
||||||
'default_seedling_boxes',
|
'default_seedling_boxes',
|
||||||
'planned_boxes',
|
'planned_boxes',
|
||||||
'planned_seed_kg',
|
'planned_seed_kg',
|
||||||
@@ -68,7 +67,7 @@ class RiceTransplantEntrySerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def get_default_seedling_boxes(self, obj):
|
def get_default_seedling_boxes(self, obj):
|
||||||
area = Decimal(str(obj.field.area_tan))
|
area = Decimal(str(obj.field.area_tan))
|
||||||
default_boxes_per_tan = obj.plan.variety.default_seedling_boxes_per_tan
|
default_boxes_per_tan = obj.plan.seedling_boxes_per_tan
|
||||||
return str((area * default_boxes_per_tan).quantize(Decimal('0.01')))
|
return str((area * default_boxes_per_tan).quantize(Decimal('0.01')))
|
||||||
|
|
||||||
def get_planned_boxes(self, obj):
|
def get_planned_boxes(self, obj):
|
||||||
@@ -76,7 +75,7 @@ class RiceTransplantEntrySerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def get_planned_seed_kg(self, obj):
|
def get_planned_seed_kg(self, obj):
|
||||||
seed_kg = (
|
seed_kg = (
|
||||||
obj.installed_seedling_boxes * obj.seed_grams_per_box / Decimal('1000')
|
obj.installed_seedling_boxes * obj.plan.default_seed_grams_per_box / Decimal('1000')
|
||||||
).quantize(Decimal('0.001'))
|
).quantize(Decimal('0.001'))
|
||||||
return str(seed_kg)
|
return str(seed_kg)
|
||||||
|
|
||||||
@@ -101,6 +100,7 @@ class RiceTransplantPlanSerializer(serializers.ModelSerializer):
|
|||||||
'variety_name',
|
'variety_name',
|
||||||
'crop_name',
|
'crop_name',
|
||||||
'default_seed_grams_per_box',
|
'default_seed_grams_per_box',
|
||||||
|
'seedling_boxes_per_tan',
|
||||||
'notes',
|
'notes',
|
||||||
'entries',
|
'entries',
|
||||||
'field_count',
|
'field_count',
|
||||||
@@ -126,7 +126,7 @@ class RiceTransplantPlanSerializer(serializers.ModelSerializer):
|
|||||||
total = sum(
|
total = sum(
|
||||||
(
|
(
|
||||||
entry.installed_seedling_boxes
|
entry.installed_seedling_boxes
|
||||||
* entry.seed_grams_per_box
|
* obj.default_seed_grams_per_box
|
||||||
/ Decimal('1000')
|
/ Decimal('1000')
|
||||||
)
|
)
|
||||||
for entry in obj.entries.all()
|
for entry in obj.entries.all()
|
||||||
@@ -152,6 +152,7 @@ class RiceTransplantPlanWriteSerializer(serializers.ModelSerializer):
|
|||||||
'year',
|
'year',
|
||||||
'variety',
|
'variety',
|
||||||
'default_seed_grams_per_box',
|
'default_seed_grams_per_box',
|
||||||
|
'seedling_boxes_per_tan',
|
||||||
'notes',
|
'notes',
|
||||||
'entries',
|
'entries',
|
||||||
]
|
]
|
||||||
@@ -192,5 +193,5 @@ class RiceTransplantPlanWriteSerializer(serializers.ModelSerializer):
|
|||||||
plan=plan,
|
plan=plan,
|
||||||
field_id=entry['field_id'],
|
field_id=entry['field_id'],
|
||||||
installed_seedling_boxes=entry['installed_seedling_boxes'],
|
installed_seedling_boxes=entry['installed_seedling_boxes'],
|
||||||
seed_grams_per_box=entry['seed_grams_per_box'],
|
seed_grams_per_box=plan.default_seed_grams_per_box,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
## 概要
|
## 概要
|
||||||
|
|
||||||
農業生産者が「年度 × 品種」を軸に、田植え前の播種・育苗準備量を見積もる機能。
|
農業生産者が「年度 × 品種」を軸に、田植え前の播種・育苗準備量を見積もる機能。
|
||||||
各圃場について「実際に設置する苗箱枚数」と「苗箱1枚あたり種もみを何g使うか」を記録し、圃場別・計画全体の苗箱枚数と種もみ使用量を自動集計する。
|
各圃場について「実際に使う苗箱数」を記録し、計画全体で必要な種もみ量を自動集計する。
|
||||||
|
|
||||||
圃場候補は既存の作付け計画から自動取得し、種もみ在庫は作物単位、反当苗箱枚数の初期値は品種単位で管理する。
|
圃場候補は既存の作付け計画から自動取得し、種もみ在庫は作物単位、反当苗箱枚数の初期値は品種単位で管理する。
|
||||||
同じ年度・同じ品種でも、播種時期や育苗ロットを分けるために複数の田植え計画を作成できる。
|
同じ年度・同じ品種でも、播種時期や育苗ロットを分けるために複数の田植え計画を作成できる。
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 田植え計画の作成・編集・削除 | 育苗日程のカレンダー管理 |
|
| 田植え計画の作成・編集・削除 | 育苗日程のカレンダー管理 |
|
||||||
| 作付け計画からの候補圃場自動取得 | 実播種実績の記録 |
|
| 作付け計画からの候補圃場自動取得 | 実播種実績の記録 |
|
||||||
| 圃場ごとの設置苗箱枚数の個別調整 | 種もみロット管理 |
|
| 圃場ごとの苗箱数の個別調整 | 種もみロット管理 |
|
||||||
| 圃場ごとの種もみg/箱の個別調整 | 在庫の自動引当 |
|
| 列単位のデフォルト反映・四捨五入 | 在庫の自動引当 |
|
||||||
| 苗箱合計・種もみkg合計の自動集計 | PDF出力 |
|
| 苗箱合計・種もみkg合計の自動集計 | PDF出力 |
|
||||||
| 作物ごとの種もみ在庫kg管理 | 品種ごとの播種日管理 |
|
| 作物ごとの種もみ在庫kg管理 | 品種ごとの播種日管理 |
|
||||||
| 品種ごとの反当苗箱枚数デフォルト管理 | |
|
| 品種ごとの反当苗箱枚数デフォルト管理 | |
|
||||||
@@ -35,24 +35,25 @@
|
|||||||
2. 対象圃場は、その年度・品種の作付け計画が登録されている圃場から取得する
|
2. 対象圃場は、その年度・品種の作付け計画が登録されている圃場から取得する
|
||||||
3. 種もみ在庫は作物単位で管理する
|
3. 種もみ在庫は作物単位で管理する
|
||||||
4. 反当苗箱枚数の初期値は品種単位で管理する
|
4. 反当苗箱枚数の初期値は品種単位で管理する
|
||||||
5. 画面上では `反当苗箱枚数 × 面積(反)` を設置苗箱枚数のデフォルト候補として表示する
|
5. 計画ヘッダ側に `反当苗箱枚数` を持ち、施肥計画の `反当袋数` と同じ役割で使う
|
||||||
6. 実際に保存するのは圃場ごとの `設置苗箱枚数` であり、手動調整後の値をそのまま保持する
|
6. 画面上では `反当苗箱枚数 × 面積(反)` を各圃場のデフォルト苗箱数として表示する
|
||||||
7. 種もみg/箱は計画全体のデフォルト値を持ちつつ、圃場単位で上書きできる
|
7. 実際に保存するのは圃場ごとの `苗箱数` であり、手動調整後の値をそのまま保持する
|
||||||
8. 在庫不足はエラーで保存停止せず、一覧・編集画面で残在庫見込みとして可視化する
|
8. `種もみg/箱` は計画全体の共通パラメータとして扱い、圃場ごとには持たない
|
||||||
9. 同じ年度・同じ品種で複数の計画を作成してよい
|
9. 在庫不足はエラーで保存停止せず、一覧・編集画面で残在庫見込みとして可視化する
|
||||||
10. 複数回に分ける場合は、`計画名` で「第1回」「第2回」「4/10播種分」などを区別する
|
10. 同じ年度・同じ品種で複数の計画を作成してよい
|
||||||
|
11. 複数回に分ける場合は、`計画名` で「第1回」「第2回」「4/10播種分」などを区別する
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 計算式
|
## 計算式
|
||||||
|
|
||||||
### 圃場ごとの設置苗箱枚数デフォルト候補
|
### 圃場ごとのデフォルト苗箱数
|
||||||
|
|
||||||
`設置苗箱枚数デフォルト = 圃場面積(反) × 反当苗箱枚数`
|
`デフォルト苗箱数 = 圃場面積(反) × 反当苗箱枚数`
|
||||||
|
|
||||||
### 圃場ごとの種もみ使用量
|
### 圃場ごとの種もみ使用量
|
||||||
|
|
||||||
`種もみkg = 設置苗箱枚数 × 苗箱1枚あたり種もみ(g) ÷ 1000`
|
`種もみkg = 苗箱数合計 × 苗箱1枚あたり種もみ(g) ÷ 1000`
|
||||||
|
|
||||||
### 計画全体の残在庫見込み
|
### 計画全体の残在庫見込み
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@
|
|||||||
| name | varchar(200) | required | 計画名 |
|
| name | varchar(200) | required | 計画名 |
|
||||||
| year | int | required | 年度 |
|
| year | int | required | 年度 |
|
||||||
| variety | FK(plans.Variety) | PROTECT | 品種 |
|
| variety | FK(plans.Variety) | PROTECT | 品種 |
|
||||||
|
| seedling_boxes_per_tan | decimal(6,2) | default=0 | 計画で使う反当苗箱枚数 |
|
||||||
| default_seed_grams_per_box | decimal(8,2) | default=0 | 苗箱1枚あたり種もみ(g)の初期値 |
|
| default_seed_grams_per_box | decimal(8,2) | default=0 | 苗箱1枚あたり種もみ(g)の初期値 |
|
||||||
| notes | text | blank | 備考 |
|
| notes | text | blank | 備考 |
|
||||||
| created_at | datetime | auto | |
|
| created_at | datetime | auto | |
|
||||||
@@ -99,7 +101,7 @@
|
|||||||
| 項目 | 型 | 説明 |
|
| 項目 | 型 | 説明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| field_count | int | 対象圃場数 |
|
| field_count | int | 対象圃場数 |
|
||||||
| total_seedling_boxes | decimal | 設置苗箱枚数合計 |
|
| total_seedling_boxes | decimal | 苗箱数合計 |
|
||||||
| total_seed_kg | decimal | 種もみ使用量合計(kg) |
|
| total_seed_kg | decimal | 種もみ使用量合計(kg) |
|
||||||
| crop_seed_inventory_kg | decimal | 作物在庫(kg) |
|
| crop_seed_inventory_kg | decimal | 作物在庫(kg) |
|
||||||
| remaining_seed_kg | decimal | 残在庫見込み(kg) |
|
| remaining_seed_kg | decimal | 残在庫見込み(kg) |
|
||||||
@@ -111,8 +113,7 @@
|
|||||||
| id | int | PK | |
|
| id | int | PK | |
|
||||||
| plan | FK(RiceTransplantPlan) | CASCADE | |
|
| plan | FK(RiceTransplantPlan) | CASCADE | |
|
||||||
| field | FK(fields.Field) | CASCADE | |
|
| field | FK(fields.Field) | CASCADE | |
|
||||||
| installed_seedling_boxes | decimal(8,2) | required | 設置苗箱枚数 |
|
| installed_seedling_boxes | decimal(8,2) | required | その圃場の苗箱数 |
|
||||||
| seed_grams_per_box | decimal(8,2) | required | 苗箱1枚あたり種もみ(g) |
|
|
||||||
|
|
||||||
- `unique_together = ['plan', 'field']`
|
- `unique_together = ['plan', 'field']`
|
||||||
- 順序: `field__display_order, field__id`
|
- 順序: `field__display_order, field__id`
|
||||||
@@ -124,8 +125,8 @@
|
|||||||
| field_name | string | 圃場名 |
|
| field_name | string | 圃場名 |
|
||||||
| field_area_tan | decimal | 圃場面積(反) |
|
| field_area_tan | decimal | 圃場面積(反) |
|
||||||
| default_seedling_boxes | decimal | `反当苗箱枚数 × 面積(反)` で求めたデフォルト候補 |
|
| default_seedling_boxes | decimal | `反当苗箱枚数 × 面積(反)` で求めたデフォルト候補 |
|
||||||
| planned_boxes | decimal | 圃場ごとの設置苗箱枚数 |
|
| planned_boxes | decimal | 圃場ごとの苗箱数 |
|
||||||
| planned_seed_kg | decimal | 圃場ごとの種もみkg |
|
| planned_seed_kg | decimal | 圃場ごとの必要種もみkg |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -154,6 +155,7 @@
|
|||||||
"variety": 3,
|
"variety": 3,
|
||||||
"variety_name": "コシヒカリ",
|
"variety_name": "コシヒカリ",
|
||||||
"crop_name": "水稲",
|
"crop_name": "水稲",
|
||||||
|
"seedling_boxes_per_tan": "12.00",
|
||||||
"default_seed_grams_per_box": "200.00",
|
"default_seed_grams_per_box": "200.00",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"field_count": 8,
|
"field_count": 8,
|
||||||
@@ -168,7 +170,6 @@
|
|||||||
"field_name": "田中上",
|
"field_name": "田中上",
|
||||||
"field_area_tan": "1.2000",
|
"field_area_tan": "1.2000",
|
||||||
"installed_seedling_boxes": "14.40",
|
"installed_seedling_boxes": "14.40",
|
||||||
"seed_grams_per_box": "200.00",
|
|
||||||
"default_seedling_boxes": "14.40",
|
"default_seedling_boxes": "14.40",
|
||||||
"planned_boxes": "14.40",
|
"planned_boxes": "14.40",
|
||||||
"planned_seed_kg": "2.880"
|
"planned_seed_kg": "2.880"
|
||||||
@@ -184,18 +185,17 @@ POST/PUT リクエスト例:
|
|||||||
"name": "2026年度 コシヒカリ 田植え計画",
|
"name": "2026年度 コシヒカリ 田植え計画",
|
||||||
"year": 2026,
|
"year": 2026,
|
||||||
"variety": 3,
|
"variety": 3,
|
||||||
|
"seedling_boxes_per_tan": "12.00",
|
||||||
"default_seed_grams_per_box": "200.00",
|
"default_seed_grams_per_box": "200.00",
|
||||||
"notes": "",
|
"notes": "",
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"field_id": 5,
|
"field_id": 5,
|
||||||
"installed_seedling_boxes": "14.40",
|
"installed_seedling_boxes": "14.40"
|
||||||
"seed_grams_per_box": "200.00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field_id": 6,
|
"field_id": 6,
|
||||||
"installed_seedling_boxes": "13.80",
|
"installed_seedling_boxes": "13.80"
|
||||||
"seed_grams_per_box": "190.00"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -247,26 +247,25 @@ POST/PUT リクエスト例:
|
|||||||
- 新規作成時は候補圃場を初期選択
|
- 新規作成時は候補圃場を初期選択
|
||||||
- 圃場の追加・除外が可能
|
- 圃場の追加・除外が可能
|
||||||
- 初期値:
|
- 初期値:
|
||||||
- `反当苗箱枚数` は品種マスタの `default_seedling_boxes_per_tan`
|
- `反当苗箱枚数` は品種マスタの `default_seedling_boxes_per_tan` を初期値として表示
|
||||||
- `設置苗箱枚数デフォルト` は `反当苗箱枚数 × 面積(反)` で求める
|
- `反当苗箱枚数 × 面積(反)` で各圃場のデフォルト苗箱数を求める
|
||||||
- `種もみg/箱` は計画ヘッダの `default_seed_grams_per_box`
|
- `種もみg/箱` は計画全体の共通値
|
||||||
- 圃場テーブル:
|
- 圃場テーブル:
|
||||||
- 圃場
|
- 圃場
|
||||||
- 面積(反)
|
- 面積(反)
|
||||||
- 設置苗箱枚数デフォルト(ラベル表示)
|
- 苗箱数入力欄
|
||||||
- 設置苗箱枚数(入力欄)
|
- 左側にデフォルト苗箱数ラベルを表示
|
||||||
- 種もみg/箱
|
- 必要種もみkg
|
||||||
- デフォルト反映ボタン
|
- 列操作:
|
||||||
- 苗箱合計
|
- `反当苗箱枚数` の入力欄
|
||||||
- 種もみkg
|
- デフォルトを列単位で一括反映するボタン
|
||||||
|
- 列単位の四捨五入ボタン
|
||||||
- サマリー:
|
- サマリー:
|
||||||
- 対象圃場数
|
- 対象圃場数
|
||||||
- 苗箱合計
|
- 苗箱合計
|
||||||
- 種もみ計画kg
|
- 種もみ計画kg
|
||||||
- 作物在庫kg
|
- 作物在庫kg
|
||||||
- 残在庫見込みkg
|
- 残在庫見込みkg
|
||||||
- 補助操作:
|
|
||||||
- 初期値を一括反映
|
|
||||||
|
|
||||||
### 3. 品種管理モーダル `/allocation`
|
### 3. 品種管理モーダル `/allocation`
|
||||||
|
|
||||||
@@ -284,7 +283,7 @@ POST/PUT リクエスト例:
|
|||||||
1. 計画名は必須
|
1. 計画名は必須
|
||||||
2. 品種は必須
|
2. 品種は必須
|
||||||
3. 圃場は1件以上必要
|
3. 圃場は1件以上必要
|
||||||
4. `installed_seedling_boxes` と `seed_grams_per_box` は 0 以上の数値を想定
|
4. `installed_seedling_boxes` と `seedling_boxes_per_tan` は 0 以上の数値を想定
|
||||||
5. 在庫不足でも保存は許可し、UIで不足を可視化する
|
5. 在庫不足でも保存は許可し、UIで不足を可視化する
|
||||||
6. 候補圃場の抽出元は既存 `Plan`(作付け計画)であるため、先に作付け計画が必要
|
6. 候補圃場の抽出元は既存 `Plan`(作付け計画)であるため、先に作付け計画が必要
|
||||||
|
|
||||||
@@ -307,7 +306,7 @@ POST/PUT リクエスト例:
|
|||||||
| シリアライザ | `backend/apps/plans/serializers.py` |
|
| シリアライザ | `backend/apps/plans/serializers.py` |
|
||||||
| ViewSet | `backend/apps/plans/views.py` |
|
| ViewSet | `backend/apps/plans/views.py` |
|
||||||
| URL | `backend/apps/plans/urls.py` |
|
| URL | `backend/apps/plans/urls.py` |
|
||||||
| マイグレーション | `backend/apps/plans/migrations/0005_crop_seed_inventory_variety_seedling_boxes_and_rice_transplant.py` |
|
| マイグレーション | `backend/apps/plans/migrations/0005_crop_seed_inventory_variety_seedling_boxes_and_rice_transplant.py`, `backend/apps/plans/migrations/0006_rename_seedling_boxes_per_tan_to_installed_seedling_boxes.py`, `backend/apps/plans/migrations/0007_ricetransplantplan_seedling_boxes_per_tan.py` |
|
||||||
| 一覧画面 | `frontend/src/app/rice-transplant/page.tsx` |
|
| 一覧画面 | `frontend/src/app/rice-transplant/page.tsx` |
|
||||||
| 編集画面 | `frontend/src/app/rice-transplant/_components/RiceTransplantEditPage.tsx` |
|
| 編集画面 | `frontend/src/app/rice-transplant/_components/RiceTransplantEditPage.tsx` |
|
||||||
| ナビゲーション | `frontend/src/components/Navbar.tsx` |
|
| ナビゲーション | `frontend/src/components/Navbar.tsx` |
|
||||||
|
|||||||
@@ -8,12 +8,7 @@ import Navbar from '@/components/Navbar';
|
|||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { Crop, Field, RiceTransplantPlan } from '@/types';
|
import { Crop, Field, RiceTransplantPlan } from '@/types';
|
||||||
|
|
||||||
type EntryInput = {
|
type BoxMap = Record<number, string>;
|
||||||
installed_seedling_boxes: string;
|
|
||||||
seed_grams_per_box: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EntryMap = Record<number, EntryInput>;
|
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
@@ -24,6 +19,7 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [year, setYear] = useState(currentYear);
|
const [year, setYear] = useState(currentYear);
|
||||||
const [varietyId, setVarietyId] = useState<number | ''>('');
|
const [varietyId, setVarietyId] = useState<number | ''>('');
|
||||||
|
const [seedlingBoxesPerTan, setSeedlingBoxesPerTan] = useState('');
|
||||||
const [defaultSeedGramsPerBox, setDefaultSeedGramsPerBox] = useState('200');
|
const [defaultSeedGramsPerBox, setDefaultSeedGramsPerBox] = useState('200');
|
||||||
const [notes, setNotes] = useState('');
|
const [notes, setNotes] = useState('');
|
||||||
|
|
||||||
@@ -31,7 +27,10 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
const [allFields, setAllFields] = useState<Field[]>([]);
|
const [allFields, setAllFields] = useState<Field[]>([]);
|
||||||
const [candidateFields, setCandidateFields] = useState<Field[]>([]);
|
const [candidateFields, setCandidateFields] = useState<Field[]>([]);
|
||||||
const [selectedFields, setSelectedFields] = useState<Field[]>([]);
|
const [selectedFields, setSelectedFields] = useState<Field[]>([]);
|
||||||
const [entries, setEntries] = useState<EntryMap>({});
|
|
||||||
|
const [calcBoxes, setCalcBoxes] = useState<BoxMap>({});
|
||||||
|
const [adjustedBoxes, setAdjustedBoxes] = useState<BoxMap>({});
|
||||||
|
const [applyToEmptyOnly, setApplyToEmptyOnly] = useState(true);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(!isNew);
|
const [loading, setLoading] = useState(!isNew);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -47,19 +46,10 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
return crops.find((crop) => crop.varieties.some((variety) => variety.id === varietyId)) ?? null;
|
return crops.find((crop) => crop.varieties.some((variety) => variety.id === varietyId)) ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateDefaultBoxes = (fieldId: number) => {
|
const calculateDefaultBoxes = (field: Field, perTan: string) => {
|
||||||
const field = allFields.find((item) => item.id === fieldId);
|
const areaTan = parseFloat(field.area_tan || '0');
|
||||||
const variety = varietyId ? getVariety(varietyId) : null;
|
const boxesPerTan = parseFloat(perTan || '0');
|
||||||
const areaTan = parseFloat(field?.area_tan ?? '0');
|
return isNaN(areaTan * boxesPerTan) ? '' : (areaTan * boxesPerTan).toFixed(2);
|
||||||
const defaultBoxesPerTan = parseFloat(variety?.default_seedling_boxes_per_tan ?? '0');
|
|
||||||
return (areaTan * defaultBoxesPerTan).toFixed(2);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initializeEntry = (fieldId: number, nextDefaultSeedGramsPerBox = defaultSeedGramsPerBox) => {
|
|
||||||
return {
|
|
||||||
installed_seedling_boxes: calculateDefaultBoxes(fieldId),
|
|
||||||
seed_grams_per_box: nextDefaultSeedGramsPerBox,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -79,6 +69,7 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
setName(plan.name);
|
setName(plan.name);
|
||||||
setYear(plan.year);
|
setYear(plan.year);
|
||||||
setVarietyId(plan.variety);
|
setVarietyId(plan.variety);
|
||||||
|
setSeedlingBoxesPerTan(plan.seedling_boxes_per_tan);
|
||||||
setDefaultSeedGramsPerBox(plan.default_seed_grams_per_box);
|
setDefaultSeedGramsPerBox(plan.default_seed_grams_per_box);
|
||||||
setNotes(plan.notes);
|
setNotes(plan.notes);
|
||||||
|
|
||||||
@@ -86,15 +77,15 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
const planFields = fieldsRes.data.filter((field: Field) => fieldIds.has(field.id));
|
const planFields = fieldsRes.data.filter((field: Field) => fieldIds.has(field.id));
|
||||||
setSelectedFields(planFields);
|
setSelectedFields(planFields);
|
||||||
setCandidateFields(planFields);
|
setCandidateFields(planFields);
|
||||||
setEntries(
|
|
||||||
plan.entries.reduce((acc: EntryMap, entry) => {
|
const nextAdjusted: BoxMap = {};
|
||||||
acc[entry.field] = {
|
const nextCalc: BoxMap = {};
|
||||||
installed_seedling_boxes: String(entry.installed_seedling_boxes),
|
plan.entries.forEach((entry) => {
|
||||||
seed_grams_per_box: String(entry.seed_grams_per_box),
|
nextAdjusted[entry.field] = String(entry.installed_seedling_boxes);
|
||||||
};
|
nextCalc[entry.field] = String(entry.default_seedling_boxes);
|
||||||
return acc;
|
});
|
||||||
}, {})
|
setAdjustedBoxes(nextAdjusted);
|
||||||
);
|
setCalcBoxes(nextCalc);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -115,15 +106,6 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
setCandidateFields(nextCandidates);
|
setCandidateFields(nextCandidates);
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
setSelectedFields(nextCandidates);
|
setSelectedFields(nextCandidates);
|
||||||
setEntries((prev) => {
|
|
||||||
const next = { ...prev };
|
|
||||||
nextCandidates.forEach((field) => {
|
|
||||||
if (!next[field.id]) {
|
|
||||||
next[field.id] = initializeEntry(field.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -133,62 +115,84 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
fetchCandidates();
|
fetchCandidates();
|
||||||
}, [varietyId, year, isNew, loading]);
|
}, [varietyId, year, isNew, loading]);
|
||||||
|
|
||||||
const updateEntry = (fieldId: number, key: keyof EntryInput, value: string) => {
|
useEffect(() => {
|
||||||
setEntries((prev) => ({
|
if (!varietyId) return;
|
||||||
|
const variety = getVariety(varietyId);
|
||||||
|
if (!variety) return;
|
||||||
|
if (isNew || seedlingBoxesPerTan === '') {
|
||||||
|
setSeedlingBoxesPerTan(variety.default_seedling_boxes_per_tan);
|
||||||
|
}
|
||||||
|
}, [varietyId, crops, isNew]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nextCalc: BoxMap = {};
|
||||||
|
selectedFields.forEach((field) => {
|
||||||
|
nextCalc[field.id] = calculateDefaultBoxes(field, seedlingBoxesPerTan);
|
||||||
|
});
|
||||||
|
setCalcBoxes(nextCalc);
|
||||||
|
}, [selectedFields, seedlingBoxesPerTan]);
|
||||||
|
|
||||||
|
const addField = (field: Field) => {
|
||||||
|
if (selectedFields.some((selected) => selected.id === field.id)) return;
|
||||||
|
setSelectedFields((prev) => [...prev, field]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeField = (fieldId: number) => {
|
||||||
|
setSelectedFields((prev) => prev.filter((field) => field.id !== fieldId));
|
||||||
|
setCalcBoxes((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[fieldId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setAdjustedBoxes((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[fieldId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBoxCount = (fieldId: number, value: string) => {
|
||||||
|
setAdjustedBoxes((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[fieldId]: {
|
[fieldId]: value,
|
||||||
...(prev[fieldId] ?? initializeEntry(fieldId)),
|
|
||||||
[key]: value,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyDefaultsToSelected = () => {
|
const applyColumnDefaults = () => {
|
||||||
setEntries((prev) => {
|
setAdjustedBoxes((prev) => {
|
||||||
const next = { ...prev };
|
const next = { ...prev };
|
||||||
selectedFields.forEach((field) => {
|
selectedFields.forEach((field) => {
|
||||||
next[field.id] = initializeEntry(field.id, defaultSeedGramsPerBox);
|
const currentValue = prev[field.id] ?? '';
|
||||||
|
if (applyToEmptyOnly && currentValue !== '') return;
|
||||||
|
next[field.id] = calcBoxes[field.id] ?? '';
|
||||||
});
|
});
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addField = (field: Field) => {
|
const roundColumn = () => {
|
||||||
if (selectedFields.some((selected) => selected.id === field.id)) return;
|
setAdjustedBoxes((prev) => {
|
||||||
setSelectedFields((prev) => [...prev, field]);
|
const next = { ...prev };
|
||||||
setEntries((prev) => ({
|
selectedFields.forEach((field) => {
|
||||||
...prev,
|
const raw = prev[field.id] !== undefined && prev[field.id] !== '' ? prev[field.id] : calcBoxes[field.id];
|
||||||
[field.id]: prev[field.id] ?? initializeEntry(field.id),
|
if (!raw) return;
|
||||||
}));
|
const value = parseFloat(raw);
|
||||||
|
if (isNaN(value)) return;
|
||||||
|
next[field.id] = String(Math.round(value));
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeField = (fieldId: number) => {
|
const effectiveBoxes = (fieldId: number) => {
|
||||||
setSelectedFields((prev) => prev.filter((field) => field.id !== fieldId));
|
const raw = adjustedBoxes[fieldId] !== undefined && adjustedBoxes[fieldId] !== '' ? adjustedBoxes[fieldId] : calcBoxes[fieldId];
|
||||||
|
const value = parseFloat(raw ?? '0');
|
||||||
|
return isNaN(value) ? 0 : value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldRows = useMemo(
|
const totalBoxes = selectedFields.reduce((sum, field) => sum + effectiveBoxes(field.id), 0);
|
||||||
() =>
|
const seedGrams = parseFloat(defaultSeedGramsPerBox || '0');
|
||||||
selectedFields.map((field) => ({
|
const totalSeedKg = seedGrams > 0 ? (totalBoxes * seedGrams) / 1000 : 0;
|
||||||
field,
|
|
||||||
entry: entries[field.id] ?? initializeEntry(field.id),
|
|
||||||
defaultBoxes: calculateDefaultBoxes(field.id),
|
|
||||||
})),
|
|
||||||
[selectedFields, entries, varietyId, defaultSeedGramsPerBox, allFields]
|
|
||||||
);
|
|
||||||
|
|
||||||
const calculateBoxes = (_field: Field, entry: EntryInput) => {
|
|
||||||
const installedBoxes = parseFloat(entry.installed_seedling_boxes || '0');
|
|
||||||
return installedBoxes;
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateSeedKg = (field: Field, entry: EntryInput) => {
|
|
||||||
const boxes = calculateBoxes(field, entry);
|
|
||||||
const gramsPerBox = parseFloat(entry.seed_grams_per_box || '0');
|
|
||||||
return (boxes * gramsPerBox) / 1000;
|
|
||||||
};
|
|
||||||
|
|
||||||
const totalBoxes = fieldRows.reduce((sum, row) => sum + calculateBoxes(row.field, row.entry), 0);
|
|
||||||
const totalSeedKg = fieldRows.reduce((sum, row) => sum + calculateSeedKg(row.field, row.entry), 0);
|
|
||||||
const cropSeedInventoryKg = parseFloat(getSelectedCrop()?.seed_inventory_kg ?? '0');
|
const cropSeedInventoryKg = parseFloat(getSelectedCrop()?.seed_inventory_kg ?? '0');
|
||||||
const remainingSeedKg = cropSeedInventoryKg - totalSeedKg;
|
const remainingSeedKg = cropSeedInventoryKg - totalSeedKg;
|
||||||
|
|
||||||
@@ -207,17 +211,19 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const entries = selectedFields.map((field) => ({
|
||||||
|
field_id: field.id,
|
||||||
|
installed_seedling_boxes: effectiveBoxes(field.id).toFixed(2),
|
||||||
|
}));
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name,
|
name,
|
||||||
year,
|
year,
|
||||||
variety: varietyId,
|
variety: varietyId,
|
||||||
default_seed_grams_per_box: defaultSeedGramsPerBox,
|
seedling_boxes_per_tan: seedlingBoxesPerTan || '0',
|
||||||
|
default_seed_grams_per_box: defaultSeedGramsPerBox || '0',
|
||||||
notes,
|
notes,
|
||||||
entries: selectedFields.map((field) => ({
|
entries,
|
||||||
field_id: field.id,
|
|
||||||
installed_seedling_boxes: entries[field.id]?.installed_seedling_boxes ?? initializeEntry(field.id).installed_seedling_boxes,
|
|
||||||
seed_grams_per_box: entries[field.id]?.seed_grams_per_box ?? defaultSeedGramsPerBox,
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@@ -240,6 +246,16 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
(field) => !selectedFields.some((selected) => selected.id === field.id)
|
(field) => !selectedFields.some((selected) => selected.id === field.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fieldRows = useMemo(
|
||||||
|
() =>
|
||||||
|
selectedFields.map((field) => ({
|
||||||
|
field,
|
||||||
|
defaultBoxes: calcBoxes[field.id] ?? '',
|
||||||
|
boxCount: adjustedBoxes[field.id] !== undefined && adjustedBoxes[field.id] !== '' ? adjustedBoxes[field.id] : calcBoxes[field.id] ?? '',
|
||||||
|
})),
|
||||||
|
[selectedFields, calcBoxes, adjustedBoxes]
|
||||||
|
);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
@@ -278,8 +294,8 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mb-4 grid gap-4 rounded-lg bg-white p-4 shadow md:grid-cols-2 xl:grid-cols-4">
|
<div className="mb-4 grid gap-4 rounded-lg bg-white p-4 shadow md:grid-cols-2 xl:grid-cols-5">
|
||||||
<div>
|
<div className="xl:col-span-2">
|
||||||
<label className="mb-1 block text-xs font-medium text-gray-600">計画名</label>
|
<label className="mb-1 block text-xs font-medium text-gray-600">計画名</label>
|
||||||
<input
|
<input
|
||||||
value={name}
|
value={name}
|
||||||
@@ -323,27 +339,32 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs font-medium text-gray-600">苗箱1枚あたり種もみ(g) デフォルト</label>
|
<label className="mb-1 block text-xs font-medium text-gray-600">苗箱1枚あたり種もみ(g)</label>
|
||||||
<input
|
<input
|
||||||
value={defaultSeedGramsPerBox}
|
value={defaultSeedGramsPerBox}
|
||||||
onChange={(e) => setDefaultSeedGramsPerBox(e.target.value)}
|
onChange={(e) => setDefaultSeedGramsPerBox(e.target.value)}
|
||||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4 grid gap-4 md:grid-cols-[2fr,1fr]">
|
<div className="mb-4 rounded-lg bg-white p-4 shadow">
|
||||||
<div className="rounded-lg bg-white p-4 shadow">
|
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<div className="mb-3 flex items-center justify-between">
|
||||||
<h2 className="text-sm font-semibold text-gray-800">対象圃場</h2>
|
<h2 className="text-sm font-semibold text-gray-800">対象圃場</h2>
|
||||||
<button
|
<div className="flex items-center gap-3">
|
||||||
onClick={applyDefaultsToSelected}
|
<label className="flex items-center gap-1.5 text-xs text-gray-600">
|
||||||
className="rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-100"
|
<input
|
||||||
>
|
type="checkbox"
|
||||||
初期値を一括反映
|
checked={applyToEmptyOnly}
|
||||||
</button>
|
onChange={(e) => setApplyToEmptyOnly(e.target.checked)}
|
||||||
|
className="rounded border-gray-300 text-green-600 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
未入力圃場のみ
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mb-3 flex flex-wrap gap-2">
|
<div className="mb-3 flex flex-wrap gap-2">
|
||||||
{selectedFields.map((field) => (
|
{selectedFields.map((field) => (
|
||||||
<button
|
<button
|
||||||
@@ -356,6 +377,7 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
))}
|
))}
|
||||||
{selectedFields.length === 0 && <p className="text-sm text-gray-500">圃場が選択されていません。</p>}
|
{selectedFields.length === 0 && <p className="text-sm text-gray-500">圃場が選択されていません。</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{unselectedFields.length > 0 && (
|
{unselectedFields.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-xs font-medium text-gray-500">追加可能</p>
|
<p className="mb-2 text-xs font-medium text-gray-500">追加可能</p>
|
||||||
@@ -374,6 +396,46 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4 rounded-lg bg-white p-4 shadow">
|
||||||
|
<div className="mb-3 flex flex-wrap items-end gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-gray-600">反当苗箱枚数</label>
|
||||||
|
<input
|
||||||
|
value={seedlingBoxesPerTan}
|
||||||
|
onChange={(e) => setSeedlingBoxesPerTan(e.target.value)}
|
||||||
|
className="w-28 rounded-lg border border-gray-300 px-3 py-2 text-sm text-right focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
inputMode="decimal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={applyColumnDefaults}
|
||||||
|
className="rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
デフォルトを反映
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={roundColumn}
|
||||||
|
className="rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
四捨五入
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500">各圃場のデフォルト苗箱数は `反当苗箱枚数 × 面積(反)` で計算されます。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4 grid gap-4 md:grid-cols-[2fr,1fr]">
|
||||||
|
<div className="rounded-lg bg-white p-4 shadow">
|
||||||
|
<label className="mb-1 block text-xs font-medium text-gray-600">備考</label>
|
||||||
|
<textarea
|
||||||
|
value={notes}
|
||||||
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg bg-white p-4 shadow">
|
<div className="rounded-lg bg-white p-4 shadow">
|
||||||
<h2 className="mb-3 text-sm font-semibold text-gray-800">集計</h2>
|
<h2 className="mb-3 text-sm font-semibold text-gray-800">集計</h2>
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
@@ -386,7 +448,7 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
<span>{totalBoxes.toFixed(2)}枚</span>
|
<span>{totalBoxes.toFixed(2)}枚</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-gray-600">
|
<div className="flex justify-between text-gray-600">
|
||||||
<span>種もみ計画</span>
|
<span>必要種もみ量</span>
|
||||||
<span>{totalSeedKg.toFixed(3)}kg</span>
|
<span>{totalSeedKg.toFixed(3)}kg</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-gray-600">
|
<div className="flex justify-between text-gray-600">
|
||||||
@@ -401,66 +463,38 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4 rounded-lg bg-white p-4 shadow">
|
|
||||||
<label className="mb-1 block text-xs font-medium text-gray-600">備考</label>
|
|
||||||
<textarea
|
|
||||||
value={notes}
|
|
||||||
onChange={(e) => setNotes(e.target.value)}
|
|
||||||
rows={3}
|
|
||||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="overflow-hidden rounded-lg bg-white shadow">
|
<div className="overflow-hidden rounded-lg bg-white shadow">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="border-b bg-gray-50">
|
<thead className="border-b bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-700">圃場</th>
|
<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>
|
||||||
<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 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">種もみg/箱</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">種もみkg</th>
|
<th className="px-4 py-3 text-right font-medium text-gray-700">種もみkg</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100">
|
<tbody className="divide-y divide-gray-100">
|
||||||
{fieldRows.map(({ field, entry, defaultBoxes }) => (
|
{fieldRows.map(({ field, defaultBoxes, boxCount }) => {
|
||||||
|
const seedKg = (effectiveBoxes(field.id) * seedGrams) / 1000;
|
||||||
|
return (
|
||||||
<tr key={field.id} className="hover:bg-gray-50">
|
<tr key={field.id} className="hover:bg-gray-50">
|
||||||
<td className="px-4 py-3 font-medium text-gray-900">{field.name}</td>
|
<td className="px-4 py-3 font-medium text-gray-900">{field.name}</td>
|
||||||
<td className="px-4 py-3 text-right tabular-nums text-gray-600">{field.area_tan}</td>
|
<td className="px-4 py-3 text-right tabular-nums text-gray-600">{field.area_tan}</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-3">
|
||||||
<span className="text-xs tabular-nums text-gray-500">{defaultBoxes}枚</span>
|
<span className="text-xs tabular-nums text-gray-500">既定 {defaultBoxes || '0.00'}枚</span>
|
||||||
<button
|
<input
|
||||||
type="button"
|
value={boxCount}
|
||||||
onClick={() => updateEntry(field.id, 'installed_seedling_boxes', defaultBoxes)}
|
onChange={(e) => updateBoxCount(field.id, e.target.value)}
|
||||||
className="rounded border border-gray-300 px-2 py-1 text-xs text-gray-700 hover:bg-gray-100"
|
className="w-24 rounded-md border border-gray-300 px-2 py-1 text-right focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
>
|
inputMode="decimal"
|
||||||
反映
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3 text-right tabular-nums text-gray-700">{seedKg.toFixed(3)}</td>
|
||||||
<input
|
|
||||||
value={entry.installed_seedling_boxes}
|
|
||||||
onChange={(e) => updateEntry(field.id, 'installed_seedling_boxes', e.target.value)}
|
|
||||||
className="w-24 rounded-md border border-gray-300 px-2 py-1 text-right focus:outline-none focus:ring-2 focus:ring-green-500"
|
|
||||||
inputMode="decimal"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<input
|
|
||||||
value={entry.seed_grams_per_box}
|
|
||||||
onChange={(e) => updateEntry(field.id, 'seed_grams_per_box', e.target.value)}
|
|
||||||
className="w-24 rounded-md border border-gray-300 px-2 py-1 text-right focus:outline-none focus:ring-2 focus:ring-green-500"
|
|
||||||
inputMode="decimal"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-right tabular-nums text-gray-700">{calculateBoxes(field, entry).toFixed(2)}</td>
|
|
||||||
<td className="px-4 py-3 text-right tabular-nums text-gray-700">{calculateSeedKg(field, entry).toFixed(3)}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -173,7 +173,6 @@ export interface RiceTransplantEntry {
|
|||||||
field_name?: string;
|
field_name?: string;
|
||||||
field_area_tan?: string;
|
field_area_tan?: string;
|
||||||
installed_seedling_boxes: string;
|
installed_seedling_boxes: string;
|
||||||
seed_grams_per_box: string;
|
|
||||||
default_seedling_boxes: string;
|
default_seedling_boxes: string;
|
||||||
planned_boxes: string;
|
planned_boxes: string;
|
||||||
planned_seed_kg: string;
|
planned_seed_kg: string;
|
||||||
@@ -187,6 +186,7 @@ export interface RiceTransplantPlan {
|
|||||||
variety_name: string;
|
variety_name: string;
|
||||||
crop_name: string;
|
crop_name: string;
|
||||||
default_seed_grams_per_box: string;
|
default_seed_grams_per_box: string;
|
||||||
|
seedling_boxes_per_tan: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
entries: RiceTransplantEntry[];
|
entries: RiceTransplantEntry[];
|
||||||
field_count: number;
|
field_count: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user