Add rice transplant planning feature
This commit is contained in:
@@ -367,6 +367,34 @@ export default function AllocationPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateCropSeedInventory = async (cropId: number, seedInventoryKg: string) => {
|
||||
try {
|
||||
const crop = crops.find((item) => item.id === cropId);
|
||||
if (!crop) return;
|
||||
await api.patch(`/plans/crops/${cropId}/`, {
|
||||
seed_inventory_kg: seedInventoryKg,
|
||||
});
|
||||
await fetchData(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to update crop seed inventory:', error);
|
||||
alert('種もみ在庫の更新に失敗しました');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateVarietyDefaultBoxes = async (varietyId: number, defaultBoxes: string) => {
|
||||
try {
|
||||
const variety = crops.flatMap((crop) => crop.varieties).find((item) => item.id === varietyId);
|
||||
if (!variety) return;
|
||||
await api.patch(`/plans/varieties/${varietyId}/`, {
|
||||
default_seedling_boxes_per_tan: defaultBoxes,
|
||||
});
|
||||
await fetchData(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to update variety default boxes:', error);
|
||||
alert('品種デフォルトの更新に失敗しました');
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFieldSelection = (fieldId: number) => {
|
||||
setSelectedFields((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -1029,18 +1057,34 @@ export default function AllocationPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{managerCropId && (
|
||||
<div className="mb-4 rounded-lg border border-amber-200 bg-amber-50 p-3">
|
||||
<p className="mb-2 text-xs font-semibold text-amber-800">田植え計画用設定</p>
|
||||
<CropSeedInventoryForm
|
||||
crop={crops.find((crop) => crop.id === managerCropId) || null}
|
||||
onSave={handleUpdateCropSeedInventory}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{managerCropId && getVarietiesForCrop(managerCropId).length > 0 ? (
|
||||
<ul className="space-y-2">
|
||||
{getVarietiesForCrop(managerCropId).map((v) => (
|
||||
<li key={v.id} className="flex items-center justify-between p-2 rounded hover:bg-gray-50">
|
||||
<span className="text-sm text-gray-900">{v.name}</span>
|
||||
<button
|
||||
onClick={() => handleDeleteVariety(v.id, v.name)}
|
||||
className="text-red-400 hover:text-red-600 p-1"
|
||||
title="削除"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
<li key={v.id} className="rounded border border-gray-200 p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-900">{v.name}</span>
|
||||
<button
|
||||
onClick={() => handleDeleteVariety(v.id, v.name)}
|
||||
className="text-red-400 hover:text-red-600 p-1"
|
||||
title="削除"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<VarietyDefaultBoxesForm
|
||||
varietyId={v.id}
|
||||
initialValue={v.default_seedling_boxes_per_tan}
|
||||
onSave={handleUpdateVarietyDefaultBoxes}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -1105,3 +1149,90 @@ function VarietyAddForm({ cropId, onAdd }: { cropId: number | null; onAdd: (name
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CropSeedInventoryForm({
|
||||
crop,
|
||||
onSave,
|
||||
}: {
|
||||
crop: Crop | null;
|
||||
onSave: (cropId: number, seedInventoryKg: string) => Promise<void>;
|
||||
}) {
|
||||
const [value, setValue] = useState(crop?.seed_inventory_kg ?? '0');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(crop?.seed_inventory_kg ?? '0');
|
||||
}, [crop?.id, crop?.seed_inventory_kg]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!crop) return;
|
||||
setSaving(true);
|
||||
await onSave(crop.id, value);
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<label className="mb-1 block text-xs text-gray-600">種もみ在庫(kg)</label>
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
inputMode="decimal"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!crop || saving}
|
||||
className="rounded-md bg-green-600 px-3 py-2 text-sm text-white hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VarietyDefaultBoxesForm({
|
||||
varietyId,
|
||||
initialValue,
|
||||
onSave,
|
||||
}: {
|
||||
varietyId: number;
|
||||
initialValue: string;
|
||||
onSave: (varietyId: number, defaultBoxes: string) => Promise<void>;
|
||||
}) {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
await onSave(varietyId, value);
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<label className="mb-1 block text-xs text-gray-600">反当苗箱枚数デフォルト</label>
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
inputMode="decimal"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="rounded-md bg-green-600 px-3 py-2 text-sm text-white hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user