種子資材ベースへの切り替えを反映しました。

frontend/src/app/rice-transplant/_components/RiceTransplantEditPage.tsx では、田植え計画の主選択を 品種 から 種子資材 に変更し、画面上は種子資材名だけで進めるようにしました。保存時だけ内部的に、その種子資材に紐づく Variety を解決して既存 API に送っています。候補圃場取得もその内部参照で動く形です。frontend/src/app/rice-transplant/page.tsx の一覧表示も 作物 / 品種 ではなく 種子資材 列に変更しました。

仕様書 document/16_マスタードキュメント_田植え計画編.md も 年度 × 種子資材 を軸にした説明へ更新済みです。確認できたのは Python 側の py_compile までで、フロントのビルド確認はまだしていません。Issue #2 にも今回の変更内容をコメント済みです。
This commit is contained in:
akira
2026-04-05 12:04:36 +09:00
parent 491f05eee8
commit 0131982c34
3 changed files with 75 additions and 59 deletions

View File

@@ -18,7 +18,7 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
const [name, setName] = useState('');
const [year, setYear] = useState(currentYear);
const [varietyId, setVarietyId] = useState<number | ''>('');
const [seedMaterialId, setSeedMaterialId] = useState<number | ''>('');
const [seedlingBoxesPerTan, setSeedlingBoxesPerTan] = useState('');
const [defaultSeedGramsPerBox, setDefaultSeedGramsPerBox] = useState('200');
const [notes, setNotes] = useState('');
@@ -39,8 +39,9 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
const years = Array.from({ length: 5 }, (_, i) => currentYear + 1 - i);
const getVariety = (id: number) =>
crops.flatMap((crop) => crop.varieties).find((variety) => variety.id === id);
const allVarieties = crops.flatMap((crop) => crop.varieties);
const getVarietyBySeedMaterial = (id: number) =>
allVarieties.find((variety) => variety.seed_material === id) ?? null;
const calculateDefaultBoxes = (field: Field, perTan: string) => {
const areaTan = parseFloat(field.area_tan || '0');
@@ -64,9 +65,12 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
if (!isNew && planId) {
const planRes = await api.get(`/plans/rice-transplant-plans/${planId}/`);
const plan: RiceTransplantPlan = planRes.data;
const fetchedVarieties = cropsRes.data.flatMap((crop: Crop) => crop.varieties);
const linkedVariety =
fetchedVarieties.find((variety) => variety.id === plan.variety) ?? null;
setName(plan.name);
setYear(plan.year);
setVarietyId(plan.variety);
setSeedMaterialId(linkedVariety?.seed_material ?? '');
setSeedlingBoxesPerTan(plan.seedling_boxes_per_tan);
setDefaultSeedGramsPerBox(plan.default_seed_grams_per_box);
setNotes(plan.notes);
@@ -97,10 +101,11 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
useEffect(() => {
const fetchCandidates = async () => {
if (!varietyId || !year || (!isNew && loading)) return;
const selectedVariety = seedMaterialId ? getVarietyBySeedMaterial(seedMaterialId) : null;
if (!selectedVariety || !year || (!isNew && loading)) return;
try {
const res = await api.get(
`/plans/rice-transplant-plans/candidate_fields/?year=${year}&variety_id=${varietyId}`
`/plans/rice-transplant-plans/candidate_fields/?year=${year}&variety_id=${selectedVariety.id}`
);
const nextCandidates: Field[] = res.data;
setCandidateFields(nextCandidates);
@@ -113,16 +118,16 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
}
};
fetchCandidates();
}, [varietyId, year, isNew, loading]);
}, [seedMaterialId, year, isNew, loading]);
useEffect(() => {
if (!varietyId) return;
const variety = getVariety(varietyId);
if (!seedMaterialId) return;
const variety = getVarietyBySeedMaterial(seedMaterialId);
if (!variety) return;
if (isNew || seedlingBoxesPerTan === '') {
setSeedlingBoxesPerTan(variety.default_seedling_boxes_per_tan);
}
}, [varietyId, crops, isNew, seedlingBoxesPerTan]);
}, [seedMaterialId, crops, isNew, seedlingBoxesPerTan]);
useEffect(() => {
const nextCalc: BoxMap = {};
@@ -206,15 +211,15 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
return Number.isNaN(value) ? 0 : value;
};
const selectedVariety = varietyId ? getVariety(varietyId) : null;
const seedStock = selectedVariety?.seed_material
? seedStocks.find((item) => item.material_id === selectedVariety.seed_material) ?? null
const selectedSeedStock = seedMaterialId
? seedStocks.find((item) => item.material_id === seedMaterialId) ?? null
: null;
const selectedVariety = seedMaterialId ? getVarietyBySeedMaterial(seedMaterialId) : null;
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 seedInventoryKg = parseFloat(seedStock?.current_stock ?? '0');
const seedInventoryKg = parseFloat(selectedSeedStock?.current_stock ?? '0');
const remainingSeedKg = seedInventoryKg - totalSeedKg;
const handleSave = async () => {
@@ -223,8 +228,12 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
setError('計画名を入力してください。');
return;
}
if (!varietyId) {
setError('種を選択してください。');
if (!seedMaterialId) {
setError('種子資材を選択してください。');
return;
}
if (!selectedVariety) {
setError('選択した種子資材に対応する品種が未設定です。資材マスタで紐付けてください。');
return;
}
if (selectedFields.length === 0) {
@@ -240,7 +249,7 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
const payload = {
name,
year,
variety: varietyId,
variety: selectedVariety.id,
seedling_boxes_per_tan: seedlingBoxesPerTan || '0',
default_seed_grams_per_box: defaultSeedGramsPerBox || '0',
notes,
@@ -349,21 +358,19 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
</select>
</div>
<div>
<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>
<select
value={varietyId}
onChange={(e) => setVarietyId(e.target.value ? parseInt(e.target.value, 10) : '')}
value={seedMaterialId}
onChange={(e) =>
setSeedMaterialId(e.target.value ? parseInt(e.target.value, 10) : '')
}
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"
>
<option value=""></option>
{crops.map((crop) => (
<optgroup key={crop.id} label={crop.name}>
{crop.varieties.map((variety) => (
<option key={variety.id} value={variety.id}>
{variety.name}
</option>
))}
</optgroup>
{seedStocks.map((stock) => (
<option key={stock.material_id} value={stock.material_id}>
{stock.name}
</option>
))}
</select>
</div>
@@ -445,7 +452,7 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
<span>{totalSeedKg.toFixed(3)}kg</span>
</div>
<div className="flex justify-between text-gray-600">
<span>{seedStock?.name || selectedVariety?.seed_material_name || '種子在庫未設定'}</span>
<span>{selectedSeedStock?.name || '種子在庫未設定'}</span>
<span>{seedInventoryKg.toFixed(3)}kg</span>
</div>
<div