その判断で進めました。在庫管理を先に固めるように切り替えて、手元の実装もそちらを優先して直しています。

今回入れたのは、在庫履歴の編集・削除対応と、種子資材を資材マスタ側で品種に直接結び付ける流れです。backend/apps/materials/views.py と backend/apps/materials/serializers.py で stock-transactions の更新を許可しつつ、計画や実績に紐づく履歴はロック扱いにしました。画面側は frontend/src/app/materials/page.tsx、frontend/src/app/materials/_components/StockOverview.tsx、frontend/src/app/materials/_components/StockTransactionForm.tsx を更新して、手動登録の入出庫履歴をあとから編集・削除できるようにしています。

あわせて frontend/src/app/materials/masters/page.tsx と frontend/src/app/materials/_components/MaterialForm.tsx で、種子 タブから品種を直接選んで紐付ける形にしました。重複を避けるため、作付け計画側の品種管理モーダル frontend/src/app/allocation/page.tsx から種子資材の紐付け UI は外しています。Issue #2 にもこの方針でコメント追記済みです。

確認できたのは python3 -m py_compile までで、フロントのビルドは frontend/node_modules が無いためまだ回していません。次はブラウザで在庫管理の操作感を確認してから、田植え計画側の細部を詰めるのがよさそうです。
This commit is contained in:
akira
2026-04-05 11:43:03 +09:00
parent a38472e4a0
commit 491f05eee8
9 changed files with 249 additions and 178 deletions

View File

@@ -2,7 +2,7 @@
import { useState, useEffect, useMemo } from 'react';
import { api } from '@/lib/api';
import { Field, Crop, Material, Plan } from '@/types';
import { Field, Crop, Plan } from '@/types';
import Navbar from '@/components/Navbar';
import { Menu, X, BarChart3, ChevronRight, ChevronDown, ArrowUp, ArrowDown, Copy, Settings, Trash2, CheckSquare, Search } from 'lucide-react';
@@ -23,7 +23,6 @@ export default function AllocationPage() {
const [fields, setFields] = useState<Field[]>([]);
const [crops, setCrops] = useState<Crop[]>([]);
const [plans, setPlans] = useState<Plan[]>([]);
const [seedMaterials, setSeedMaterials] = useState<Material[]>([]);
const [year, setYear] = useState<number>(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('allocationYear');
@@ -61,16 +60,14 @@ export default function AllocationPage() {
const fetchData = async (background = false) => {
if (!background) setLoading(true);
try {
const [fieldsRes, cropsRes, plansRes, seedMaterialsRes] = await Promise.all([
const [fieldsRes, cropsRes, plansRes] = await Promise.all([
api.get('/fields/?ordering=group_name,display_order,id'),
api.get('/plans/crops/'),
api.get(`/plans/?year=${year}`),
api.get('/materials/materials/?material_type=seed'),
]);
setFields(fieldsRes.data);
setCrops(cropsRes.data);
setPlans(plansRes.data);
setSeedMaterials(seedMaterialsRes.data);
} catch (error) {
console.error('Failed to fetch data:', error);
} finally {
@@ -384,20 +381,6 @@ export default function AllocationPage() {
}
};
const handleUpdateVarietySeedMaterial = async (varietyId: number, seedMaterialId: string) => {
try {
const variety = crops.flatMap((crop) => crop.varieties).find((item) => item.id === varietyId);
if (!variety) return;
await api.patch(`/plans/varieties/${varietyId}/`, {
seed_material: seedMaterialId ? parseInt(seedMaterialId, 10) : null,
});
await fetchData(true);
} catch (error) {
console.error('Failed to update variety seed material:', error);
alert('種子在庫の紐付け更新に失敗しました');
}
};
const toggleFieldSelection = (fieldId: number) => {
setSelectedFields((prev) => {
const next = new Set(prev);
@@ -1079,15 +1062,6 @@ export default function AllocationPage() {
initialValue={v.default_seedling_boxes_per_tan}
onSave={handleUpdateVarietyDefaultBoxes}
/>
<div className="mt-3">
<VarietySeedMaterialForm
varietyId={v.id}
initialValue={v.seed_material ? String(v.seed_material) : ''}
initialLabel={v.seed_material_name}
materials={seedMaterials}
onSave={handleUpdateVarietySeedMaterial}
/>
</div>
</li>
))}
</ul>
@@ -1196,60 +1170,3 @@ function VarietyDefaultBoxesForm({
</div>
);
}
function VarietySeedMaterialForm({
varietyId,
initialValue,
initialLabel,
materials,
onSave,
}: {
varietyId: number;
initialValue: string;
initialLabel: string | null;
materials: Material[];
onSave: (varietyId: number, seedMaterialId: 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>
<select
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"
>
<option value=""></option>
{materials.map((material) => (
<option key={material.id} value={material.id}>
{material.name}
</option>
))}
</select>
<p className="mt-1 text-xs text-gray-500">
: {initialLabel || '未設定'}
</p>
</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>
);
}