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:
@@ -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<number, EntryInput>;
|
||||
type BoxMap = Record<number, string>;
|
||||
|
||||
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<number | ''>('');
|
||||
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<Field[]>([]);
|
||||
const [candidateFields, setCandidateFields] = 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 [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 (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
@@ -278,8 +294,8 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4 grid gap-4 rounded-lg bg-white p-4 shadow md:grid-cols-2 xl:grid-cols-4">
|
||||
<div>
|
||||
<div className="mb-4 grid gap-4 rounded-lg bg-white p-4 shadow md:grid-cols-2 xl:grid-cols-5">
|
||||
<div className="xl:col-span-2">
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600">計画名</label>
|
||||
<input
|
||||
value={name}
|
||||
@@ -323,55 +339,101 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
||||
</select>
|
||||
</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
|
||||
value={defaultSeedGramsPerBox}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 rounded-lg bg-white p-4 shadow">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-gray-800">対象圃場</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex items-center gap-1.5 text-xs text-gray-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={applyToEmptyOnly}
|
||||
onChange={(e) => setApplyToEmptyOnly(e.target.checked)}
|
||||
className="rounded border-gray-300 text-green-600 focus:ring-green-500"
|
||||
/>
|
||||
未入力圃場のみ
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3 flex flex-wrap gap-2">
|
||||
{selectedFields.map((field) => (
|
||||
<button
|
||||
key={field.id}
|
||||
onClick={() => removeField(field.id)}
|
||||
className="rounded-full border border-green-200 bg-green-50 px-3 py-1 text-xs text-green-800"
|
||||
>
|
||||
{field.name} ×
|
||||
</button>
|
||||
))}
|
||||
{selectedFields.length === 0 && <p className="text-sm text-gray-500">圃場が選択されていません。</p>}
|
||||
</div>
|
||||
|
||||
{unselectedFields.length > 0 && (
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-medium text-gray-500">追加可能</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{unselectedFields.map((field) => (
|
||||
<button
|
||||
key={field.id}
|
||||
onClick={() => addField(field)}
|
||||
className="rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
{field.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</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">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-gray-800">対象圃場</h2>
|
||||
<button
|
||||
onClick={applyDefaultsToSelected}
|
||||
className="rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
初期値を一括反映
|
||||
</button>
|
||||
</div>
|
||||
<div className="mb-3 flex flex-wrap gap-2">
|
||||
{selectedFields.map((field) => (
|
||||
<button
|
||||
key={field.id}
|
||||
onClick={() => removeField(field.id)}
|
||||
className="rounded-full border border-green-200 bg-green-50 px-3 py-1 text-xs text-green-800"
|
||||
>
|
||||
{field.name} ×
|
||||
</button>
|
||||
))}
|
||||
{selectedFields.length === 0 && <p className="text-sm text-gray-500">圃場が選択されていません。</p>}
|
||||
</div>
|
||||
{unselectedFields.length > 0 && (
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-medium text-gray-500">追加可能</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{unselectedFields.map((field) => (
|
||||
<button
|
||||
key={field.id}
|
||||
onClick={() => addField(field)}
|
||||
className="rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
{field.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<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">
|
||||
@@ -386,7 +448,7 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
||||
<span>{totalBoxes.toFixed(2)}枚</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span>種もみ計画</span>
|
||||
<span>必要種もみ量</span>
|
||||
<span>{totalSeedKg.toFixed(3)}kg</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-gray-600">
|
||||
@@ -401,66 +463,38 @@ export default function RiceTransplantEditPage({ planId }: { planId?: number })
|
||||
</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">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="border-b bg-gray-50">
|
||||
<tr>
|
||||
<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">種もみ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">苗箱数</th>
|
||||
<th className="px-4 py-3 text-right font-medium text-gray-700">種もみkg</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{fieldRows.map(({ field, entry, defaultBoxes }) => (
|
||||
<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 text-right tabular-nums text-gray-600">{field.area_tan}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<span className="text-xs tabular-nums text-gray-500">{defaultBoxes}枚</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateEntry(field.id, 'installed_seedling_boxes', defaultBoxes)}
|
||||
className="rounded border border-gray-300 px-2 py-1 text-xs text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
反映
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<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>
|
||||
))}
|
||||
{fieldRows.map(({ field, defaultBoxes, boxCount }) => {
|
||||
const seedKg = (effectiveBoxes(field.id) * seedGrams) / 1000;
|
||||
return (
|
||||
<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 text-right tabular-nums text-gray-600">{field.area_tan}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<span className="text-xs tabular-nums text-gray-500">既定 {defaultBoxes || '0.00'}枚</span>
|
||||
<input
|
||||
value={boxCount}
|
||||
onChange={(e) => updateBoxCount(field.id, 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"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums text-gray-700">{seedKg.toFixed(3)}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -173,7 +173,6 @@ export interface RiceTransplantEntry {
|
||||
field_name?: string;
|
||||
field_area_tan?: string;
|
||||
installed_seedling_boxes: string;
|
||||
seed_grams_per_box: string;
|
||||
default_seedling_boxes: string;
|
||||
planned_boxes: string;
|
||||
planned_seed_kg: string;
|
||||
@@ -187,6 +186,7 @@ export interface RiceTransplantPlan {
|
||||
variety_name: string;
|
||||
crop_name: string;
|
||||
default_seed_grams_per_box: string;
|
||||
seedling_boxes_per_tan: string;
|
||||
notes: string;
|
||||
entries: RiceTransplantEntry[];
|
||||
field_count: number;
|
||||
|
||||
Reference in New Issue
Block a user