diff --git a/backend/apps/plans/migrations/0007_ricetransplantplan_seedling_boxes_per_tan.py b/backend/apps/plans/migrations/0007_ricetransplantplan_seedling_boxes_per_tan.py new file mode 100644 index 0000000..8958faf --- /dev/null +++ b/backend/apps/plans/migrations/0007_ricetransplantplan_seedling_boxes_per_tan.py @@ -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='反当苗箱枚数'), + ), + ] diff --git a/backend/apps/plans/models.py b/backend/apps/plans/models.py index 4cfacc9..00708aa 100644 --- a/backend/apps/plans/models.py +++ b/backend/apps/plans/models.py @@ -71,6 +71,12 @@ class RiceTransplantPlan(models.Model): default=0, 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='備考') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/backend/apps/plans/serializers.py b/backend/apps/plans/serializers.py index 7e64d30..5771aa9 100644 --- a/backend/apps/plans/serializers.py +++ b/backend/apps/plans/serializers.py @@ -49,8 +49,8 @@ class RiceTransplantEntrySerializer(serializers.ModelSerializer): read_only=True, ) planned_boxes = serializers.SerializerMethodField() - planned_seed_kg = serializers.SerializerMethodField() default_seedling_boxes = serializers.SerializerMethodField() + planned_seed_kg = serializers.SerializerMethodField() class Meta: model = RiceTransplantEntry @@ -60,7 +60,6 @@ class RiceTransplantEntrySerializer(serializers.ModelSerializer): 'field_name', 'field_area_tan', 'installed_seedling_boxes', - 'seed_grams_per_box', 'default_seedling_boxes', 'planned_boxes', 'planned_seed_kg', @@ -68,7 +67,7 @@ class RiceTransplantEntrySerializer(serializers.ModelSerializer): def get_default_seedling_boxes(self, obj): 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'))) def get_planned_boxes(self, obj): @@ -76,7 +75,7 @@ class RiceTransplantEntrySerializer(serializers.ModelSerializer): def get_planned_seed_kg(self, obj): 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')) return str(seed_kg) @@ -101,6 +100,7 @@ class RiceTransplantPlanSerializer(serializers.ModelSerializer): 'variety_name', 'crop_name', 'default_seed_grams_per_box', + 'seedling_boxes_per_tan', 'notes', 'entries', 'field_count', @@ -126,7 +126,7 @@ class RiceTransplantPlanSerializer(serializers.ModelSerializer): total = sum( ( entry.installed_seedling_boxes - * entry.seed_grams_per_box + * obj.default_seed_grams_per_box / Decimal('1000') ) for entry in obj.entries.all() @@ -152,6 +152,7 @@ class RiceTransplantPlanWriteSerializer(serializers.ModelSerializer): 'year', 'variety', 'default_seed_grams_per_box', + 'seedling_boxes_per_tan', 'notes', 'entries', ] @@ -192,5 +193,5 @@ class RiceTransplantPlanWriteSerializer(serializers.ModelSerializer): plan=plan, field_id=entry['field_id'], 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, ) diff --git a/document/16_マスタードキュメント_田植え計画編.md b/document/16_マスタードキュメント_田植え計画編.md index 9b189c5..1edf0b7 100644 --- a/document/16_マスタードキュメント_田植え計画編.md +++ b/document/16_マスタードキュメント_田植え計画編.md @@ -10,7 +10,7 @@ ## 概要 農業生産者が「年度 × 品種」を軸に、田植え前の播種・育苗準備量を見積もる機能。 -各圃場について「実際に設置する苗箱枚数」と「苗箱1枚あたり種もみを何g使うか」を記録し、圃場別・計画全体の苗箱枚数と種もみ使用量を自動集計する。 +各圃場について「実際に使う苗箱数」を記録し、計画全体で必要な種もみ量を自動集計する。 圃場候補は既存の作付け計画から自動取得し、種もみ在庫は作物単位、反当苗箱枚数の初期値は品種単位で管理する。 同じ年度・同じ品種でも、播種時期や育苗ロットを分けるために複数の田植え計画を作成できる。 @@ -21,8 +21,8 @@ |---|---| | 田植え計画の作成・編集・削除 | 育苗日程のカレンダー管理 | | 作付け計画からの候補圃場自動取得 | 実播種実績の記録 | -| 圃場ごとの設置苗箱枚数の個別調整 | 種もみロット管理 | -| 圃場ごとの種もみg/箱の個別調整 | 在庫の自動引当 | +| 圃場ごとの苗箱数の個別調整 | 種もみロット管理 | +| 列単位のデフォルト反映・四捨五入 | 在庫の自動引当 | | 苗箱合計・種もみkg合計の自動集計 | PDF出力 | | 作物ごとの種もみ在庫kg管理 | 品種ごとの播種日管理 | | 品種ごとの反当苗箱枚数デフォルト管理 | | @@ -35,24 +35,25 @@ 2. 対象圃場は、その年度・品種の作付け計画が登録されている圃場から取得する 3. 種もみ在庫は作物単位で管理する 4. 反当苗箱枚数の初期値は品種単位で管理する -5. 画面上では `反当苗箱枚数 × 面積(反)` を設置苗箱枚数のデフォルト候補として表示する -6. 実際に保存するのは圃場ごとの `設置苗箱枚数` であり、手動調整後の値をそのまま保持する -7. 種もみg/箱は計画全体のデフォルト値を持ちつつ、圃場単位で上書きできる -8. 在庫不足はエラーで保存停止せず、一覧・編集画面で残在庫見込みとして可視化する -9. 同じ年度・同じ品種で複数の計画を作成してよい -10. 複数回に分ける場合は、`計画名` で「第1回」「第2回」「4/10播種分」などを区別する +5. 計画ヘッダ側に `反当苗箱枚数` を持ち、施肥計画の `反当袋数` と同じ役割で使う +6. 画面上では `反当苗箱枚数 × 面積(反)` を各圃場のデフォルト苗箱数として表示する +7. 実際に保存するのは圃場ごとの `苗箱数` であり、手動調整後の値をそのまま保持する +8. `種もみg/箱` は計画全体の共通パラメータとして扱い、圃場ごとには持たない +9. 在庫不足はエラーで保存停止せず、一覧・編集画面で残在庫見込みとして可視化する +10. 同じ年度・同じ品種で複数の計画を作成してよい +11. 複数回に分ける場合は、`計画名` で「第1回」「第2回」「4/10播種分」などを区別する --- ## 計算式 -### 圃場ごとの設置苗箱枚数デフォルト候補 +### 圃場ごとのデフォルト苗箱数 -`設置苗箱枚数デフォルト = 圃場面積(反) × 反当苗箱枚数` +`デフォルト苗箱数 = 圃場面積(反) × 反当苗箱枚数` ### 圃場ごとの種もみ使用量 -`種もみkg = 設置苗箱枚数 × 苗箱1枚あたり種もみ(g) ÷ 1000` +`種もみkg = 苗箱数合計 × 苗箱1枚あたり種もみ(g) ÷ 1000` ### 計画全体の残在庫見込み @@ -86,6 +87,7 @@ | name | varchar(200) | required | 計画名 | | year | int | required | 年度 | | 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)の初期値 | | notes | text | blank | 備考 | | created_at | datetime | auto | | @@ -99,7 +101,7 @@ | 項目 | 型 | 説明 | |---|---|---| | field_count | int | 対象圃場数 | -| total_seedling_boxes | decimal | 設置苗箱枚数合計 | +| total_seedling_boxes | decimal | 苗箱数合計 | | total_seed_kg | decimal | 種もみ使用量合計(kg) | | crop_seed_inventory_kg | decimal | 作物在庫(kg) | | remaining_seed_kg | decimal | 残在庫見込み(kg) | @@ -111,8 +113,7 @@ | id | int | PK | | | plan | FK(RiceTransplantPlan) | CASCADE | | | field | FK(fields.Field) | CASCADE | | -| installed_seedling_boxes | decimal(8,2) | required | 設置苗箱枚数 | -| seed_grams_per_box | decimal(8,2) | required | 苗箱1枚あたり種もみ(g) | +| installed_seedling_boxes | decimal(8,2) | required | その圃場の苗箱数 | - `unique_together = ['plan', 'field']` - 順序: `field__display_order, field__id` @@ -124,8 +125,8 @@ | field_name | string | 圃場名 | | field_area_tan | decimal | 圃場面積(反) | | default_seedling_boxes | decimal | `反当苗箱枚数 × 面積(反)` で求めたデフォルト候補 | -| planned_boxes | decimal | 圃場ごとの設置苗箱枚数 | -| planned_seed_kg | decimal | 圃場ごとの種もみkg | +| planned_boxes | decimal | 圃場ごとの苗箱数 | +| planned_seed_kg | decimal | 圃場ごとの必要種もみkg | --- @@ -154,6 +155,7 @@ "variety": 3, "variety_name": "コシヒカリ", "crop_name": "水稲", + "seedling_boxes_per_tan": "12.00", "default_seed_grams_per_box": "200.00", "notes": "", "field_count": 8, @@ -168,7 +170,6 @@ "field_name": "田中上", "field_area_tan": "1.2000", "installed_seedling_boxes": "14.40", - "seed_grams_per_box": "200.00", "default_seedling_boxes": "14.40", "planned_boxes": "14.40", "planned_seed_kg": "2.880" @@ -184,18 +185,17 @@ POST/PUT リクエスト例: "name": "2026年度 コシヒカリ 田植え計画", "year": 2026, "variety": 3, + "seedling_boxes_per_tan": "12.00", "default_seed_grams_per_box": "200.00", "notes": "", "entries": [ { "field_id": 5, - "installed_seedling_boxes": "14.40", - "seed_grams_per_box": "200.00" + "installed_seedling_boxes": "14.40" }, { "field_id": 6, - "installed_seedling_boxes": "13.80", - "seed_grams_per_box": "190.00" + "installed_seedling_boxes": "13.80" } ] } @@ -247,26 +247,25 @@ POST/PUT リクエスト例: - 新規作成時は候補圃場を初期選択 - 圃場の追加・除外が可能 - 初期値: - - `反当苗箱枚数` は品種マスタの `default_seedling_boxes_per_tan` - - `設置苗箱枚数デフォルト` は `反当苗箱枚数 × 面積(反)` で求める - - `種もみg/箱` は計画ヘッダの `default_seed_grams_per_box` + - `反当苗箱枚数` は品種マスタの `default_seedling_boxes_per_tan` を初期値として表示 + - `反当苗箱枚数 × 面積(反)` で各圃場のデフォルト苗箱数を求める + - `種もみg/箱` は計画全体の共通値 - 圃場テーブル: - 圃場 - 面積(反) - - 設置苗箱枚数デフォルト(ラベル表示) - - 設置苗箱枚数(入力欄) - - 種もみg/箱 - - デフォルト反映ボタン - - 苗箱合計 - - 種もみkg + - 苗箱数入力欄 + - 左側にデフォルト苗箱数ラベルを表示 + - 必要種もみkg +- 列操作: + - `反当苗箱枚数` の入力欄 + - デフォルトを列単位で一括反映するボタン + - 列単位の四捨五入ボタン - サマリー: - 対象圃場数 - 苗箱合計 - 種もみ計画kg - 作物在庫kg - 残在庫見込みkg -- 補助操作: - - 初期値を一括反映 ### 3. 品種管理モーダル `/allocation` @@ -284,7 +283,7 @@ POST/PUT リクエスト例: 1. 計画名は必須 2. 品種は必須 3. 圃場は1件以上必要 -4. `installed_seedling_boxes` と `seed_grams_per_box` は 0 以上の数値を想定 +4. `installed_seedling_boxes` と `seedling_boxes_per_tan` は 0 以上の数値を想定 5. 在庫不足でも保存は許可し、UIで不足を可視化する 6. 候補圃場の抽出元は既存 `Plan`(作付け計画)であるため、先に作付け計画が必要 @@ -307,7 +306,7 @@ POST/PUT リクエスト例: | シリアライザ | `backend/apps/plans/serializers.py` | | ViewSet | `backend/apps/plans/views.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/_components/RiceTransplantEditPage.tsx` | | ナビゲーション | `frontend/src/components/Navbar.tsx` | diff --git a/frontend/src/app/rice-transplant/_components/RiceTransplantEditPage.tsx b/frontend/src/app/rice-transplant/_components/RiceTransplantEditPage.tsx index 2561c59..738974a 100644 --- a/frontend/src/app/rice-transplant/_components/RiceTransplantEditPage.tsx +++ b/frontend/src/app/rice-transplant/_components/RiceTransplantEditPage.tsx @@ -8,12 +8,7 @@ import Navbar from '@/components/Navbar'; import { api } from '@/lib/api'; import { Crop, Field, RiceTransplantPlan } from '@/types'; -type EntryInput = { - installed_seedling_boxes: string; - seed_grams_per_box: string; -}; - -type EntryMap = Record; +type BoxMap = Record; const currentYear = new Date().getFullYear(); @@ -24,6 +19,7 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number }) const [name, setName] = useState(''); const [year, setYear] = useState(currentYear); const [varietyId, setVarietyId] = useState(''); + const [seedlingBoxesPerTan, setSeedlingBoxesPerTan] = useState(''); const [defaultSeedGramsPerBox, setDefaultSeedGramsPerBox] = useState('200'); const [notes, setNotes] = useState(''); @@ -31,7 +27,10 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number }) const [allFields, setAllFields] = useState([]); const [candidateFields, setCandidateFields] = useState([]); const [selectedFields, setSelectedFields] = useState([]); - const [entries, setEntries] = useState({}); + + const [calcBoxes, setCalcBoxes] = useState({}); + const [adjustedBoxes, setAdjustedBoxes] = useState({}); + const [applyToEmptyOnly, setApplyToEmptyOnly] = useState(true); const [loading, setLoading] = useState(!isNew); 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; }; - const calculateDefaultBoxes = (fieldId: number) => { - const field = allFields.find((item) => item.id === fieldId); - const variety = varietyId ? getVariety(varietyId) : null; - const areaTan = parseFloat(field?.area_tan ?? '0'); - 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, - }; + const calculateDefaultBoxes = (field: Field, perTan: string) => { + const areaTan = parseFloat(field.area_tan || '0'); + const boxesPerTan = parseFloat(perTan || '0'); + return isNaN(areaTan * boxesPerTan) ? '' : (areaTan * boxesPerTan).toFixed(2); }; useEffect(() => { @@ -79,6 +69,7 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number }) setName(plan.name); setYear(plan.year); setVarietyId(plan.variety); + setSeedlingBoxesPerTan(plan.seedling_boxes_per_tan); setDefaultSeedGramsPerBox(plan.default_seed_grams_per_box); 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)); setSelectedFields(planFields); setCandidateFields(planFields); - setEntries( - plan.entries.reduce((acc: EntryMap, entry) => { - acc[entry.field] = { - installed_seedling_boxes: String(entry.installed_seedling_boxes), - seed_grams_per_box: String(entry.seed_grams_per_box), - }; - return acc; - }, {}) - ); + + const nextAdjusted: BoxMap = {}; + const nextCalc: BoxMap = {}; + plan.entries.forEach((entry) => { + nextAdjusted[entry.field] = String(entry.installed_seedling_boxes); + nextCalc[entry.field] = String(entry.default_seedling_boxes); + }); + setAdjustedBoxes(nextAdjusted); + setCalcBoxes(nextCalc); } } catch (e) { console.error(e); @@ -115,15 +106,6 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number }) setCandidateFields(nextCandidates); if (isNew) { setSelectedFields(nextCandidates); - setEntries((prev) => { - const next = { ...prev }; - nextCandidates.forEach((field) => { - if (!next[field.id]) { - next[field.id] = initializeEntry(field.id); - } - }); - return next; - }); } } catch (e) { console.error(e); @@ -133,62 +115,84 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number }) fetchCandidates(); }, [varietyId, year, isNew, loading]); - const updateEntry = (fieldId: number, key: keyof EntryInput, value: string) => { - setEntries((prev) => ({ + useEffect(() => { + 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, - [fieldId]: { - ...(prev[fieldId] ?? initializeEntry(fieldId)), - [key]: value, - }, + [fieldId]: value, })); }; - const applyDefaultsToSelected = () => { - setEntries((prev) => { + const applyColumnDefaults = () => { + setAdjustedBoxes((prev) => { const next = { ...prev }; 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; }); }; - const addField = (field: Field) => { - if (selectedFields.some((selected) => selected.id === field.id)) return; - setSelectedFields((prev) => [...prev, field]); - setEntries((prev) => ({ - ...prev, - [field.id]: prev[field.id] ?? initializeEntry(field.id), - })); + const roundColumn = () => { + setAdjustedBoxes((prev) => { + const next = { ...prev }; + selectedFields.forEach((field) => { + const raw = prev[field.id] !== undefined && prev[field.id] !== '' ? prev[field.id] : calcBoxes[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) => { - setSelectedFields((prev) => prev.filter((field) => field.id !== fieldId)); + const effectiveBoxes = (fieldId: number) => { + const raw = adjustedBoxes[fieldId] !== undefined && adjustedBoxes[fieldId] !== '' ? adjustedBoxes[fieldId] : calcBoxes[fieldId]; + const value = parseFloat(raw ?? '0'); + return isNaN(value) ? 0 : value; }; - const fieldRows = useMemo( - () => - selectedFields.map((field) => ({ - 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 totalBoxes = selectedFields.reduce((sum, field) => sum + effectiveBoxes(field.id), 0); + const seedGrams = parseFloat(defaultSeedGramsPerBox || '0'); + const totalSeedKg = seedGrams > 0 ? (totalBoxes * seedGrams) / 1000 : 0; const cropSeedInventoryKg = parseFloat(getSelectedCrop()?.seed_inventory_kg ?? '0'); const remainingSeedKg = cropSeedInventoryKg - totalSeedKg; @@ -207,17 +211,19 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number }) return; } + const entries = selectedFields.map((field) => ({ + field_id: field.id, + installed_seedling_boxes: effectiveBoxes(field.id).toFixed(2), + })); + const payload = { name, year, variety: varietyId, - default_seed_grams_per_box: defaultSeedGramsPerBox, + seedling_boxes_per_tan: seedlingBoxesPerTan || '0', + default_seed_grams_per_box: defaultSeedGramsPerBox || '0', notes, - entries: selectedFields.map((field) => ({ - 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, - })), + entries, }; setSaving(true); @@ -240,6 +246,16 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number }) (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) { return (
@@ -278,8 +294,8 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
)} -
-
+
+
- + 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" />
+
+
+

対象圃場

+
+ +
+
+ +
+ {selectedFields.map((field) => ( + + ))} + {selectedFields.length === 0 &&

圃場が選択されていません。

} +
+ + {unselectedFields.length > 0 && ( +
+

追加可能

+
+ {unselectedFields.map((field) => ( + + ))} +
+
+ )} +
+ +
+
+
+ + 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" + /> +
+ + +
+

各圃場のデフォルト苗箱数は `反当苗箱枚数 × 面積(反)` で計算されます。

+
+
-
-

対象圃場

- -
-
- {selectedFields.map((field) => ( - - ))} - {selectedFields.length === 0 &&

圃場が選択されていません。

} -
- {unselectedFields.length > 0 && ( -
-

追加可能

-
- {unselectedFields.map((field) => ( - - ))} -
-
- )} + +