在庫管理機能 Phase 1 実装(apps/materials + フロントエンド)
Backend: - apps/materials 新規作成(Material, FertilizerProfile, PesticideProfile, StockTransaction) - 資材マスタ CRUD API(/api/materials/materials/) - 入出庫履歴 API(/api/materials/stock-transactions/) - 在庫集計 API(/api/materials/stock-summary/) - 既存 Fertilizer に material OneToOneField 追加(0005マイグレーション、データ移行込み) Frontend: - /materials: 在庫一覧画面(タブフィルタ、履歴展開、入出庫モーダル) - /materials/masters: 資材マスタ管理(肥料/農薬/その他タブ、インライン編集) - Navbar に「在庫管理」メニュー追加 - Material/StockTransaction/StockSummary 型定義追加 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
341
frontend/src/app/materials/_components/MaterialForm.tsx
Normal file
341
frontend/src/app/materials/_components/MaterialForm.tsx
Normal file
@@ -0,0 +1,341 @@
|
||||
'use client';
|
||||
|
||||
import { Check, X } from 'lucide-react';
|
||||
|
||||
import { Material } from '@/types';
|
||||
|
||||
export type MaterialTab = 'fertilizer' | 'pesticide' | 'misc';
|
||||
|
||||
export interface MaterialFormState {
|
||||
name: string;
|
||||
material_type: Material['material_type'];
|
||||
maker: string;
|
||||
stock_unit: Material['stock_unit'];
|
||||
is_active: boolean;
|
||||
notes: string;
|
||||
fertilizer_profile: {
|
||||
capacity_kg: string;
|
||||
nitrogen_pct: string;
|
||||
phosphorus_pct: string;
|
||||
potassium_pct: string;
|
||||
};
|
||||
pesticide_profile: {
|
||||
registration_no: string;
|
||||
formulation: string;
|
||||
usage_unit: string;
|
||||
dilution_ratio: string;
|
||||
active_ingredient: string;
|
||||
category: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MaterialFormProps {
|
||||
tab: MaterialTab;
|
||||
form: MaterialFormState;
|
||||
saving: boolean;
|
||||
onBaseFieldChange: (
|
||||
field: keyof Omit<MaterialFormState, 'fertilizer_profile' | 'pesticide_profile'>,
|
||||
value: string | boolean
|
||||
) => void;
|
||||
onFertilizerFieldChange: (
|
||||
field: keyof MaterialFormState['fertilizer_profile'],
|
||||
value: string
|
||||
) => void;
|
||||
onPesticideFieldChange: (
|
||||
field: keyof MaterialFormState['pesticide_profile'],
|
||||
value: string
|
||||
) => void;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const inputClassName =
|
||||
'w-full rounded-md border border-gray-300 px-2 py-1 text-sm focus:border-green-500 focus:outline-none focus:ring-1 focus:ring-green-500';
|
||||
|
||||
export default function MaterialForm({
|
||||
tab,
|
||||
form,
|
||||
saving,
|
||||
onBaseFieldChange,
|
||||
onFertilizerFieldChange,
|
||||
onPesticideFieldChange,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: MaterialFormProps) {
|
||||
if (tab === 'fertilizer') {
|
||||
return (
|
||||
<tr className="bg-green-50">
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
value={form.name}
|
||||
onChange={(e) => onBaseFieldChange('name', e.target.value)}
|
||||
placeholder="資材名"
|
||||
autoFocus
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
value={form.maker}
|
||||
onChange={(e) => onBaseFieldChange('maker', e.target.value)}
|
||||
placeholder="メーカー"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
type="number"
|
||||
step="0.001"
|
||||
value={form.fertilizer_profile.capacity_kg}
|
||||
onChange={(e) => onFertilizerFieldChange('capacity_kg', e.target.value)}
|
||||
placeholder="kg"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={form.fertilizer_profile.nitrogen_pct}
|
||||
onChange={(e) => onFertilizerFieldChange('nitrogen_pct', e.target.value)}
|
||||
placeholder="%"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={form.fertilizer_profile.phosphorus_pct}
|
||||
onChange={(e) => onFertilizerFieldChange('phosphorus_pct', e.target.value)}
|
||||
placeholder="%"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={form.fertilizer_profile.potassium_pct}
|
||||
onChange={(e) => onFertilizerFieldChange('potassium_pct', e.target.value)}
|
||||
placeholder="%"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<StockUnitSelect
|
||||
value={form.stock_unit}
|
||||
onChange={(value) => onBaseFieldChange('stock_unit', value)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
value={form.notes}
|
||||
onChange={(e) => onBaseFieldChange('notes', e.target.value)}
|
||||
placeholder="備考"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.is_active}
|
||||
onChange={(e) => onBaseFieldChange('is_active', e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<ActionButtons onSave={onSave} onCancel={onCancel} saving={saving} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
if (tab === 'pesticide') {
|
||||
return (
|
||||
<tr className="bg-green-50">
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
value={form.name}
|
||||
onChange={(e) => onBaseFieldChange('name', e.target.value)}
|
||||
placeholder="資材名"
|
||||
autoFocus
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
value={form.maker}
|
||||
onChange={(e) => onBaseFieldChange('maker', e.target.value)}
|
||||
placeholder="メーカー"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
value={form.pesticide_profile.registration_no}
|
||||
onChange={(e) => onPesticideFieldChange('registration_no', e.target.value)}
|
||||
placeholder="登録番号"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
value={form.pesticide_profile.formulation}
|
||||
onChange={(e) => onPesticideFieldChange('formulation', e.target.value)}
|
||||
placeholder="剤型"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
value={form.pesticide_profile.active_ingredient}
|
||||
onChange={(e) => onPesticideFieldChange('active_ingredient', e.target.value)}
|
||||
placeholder="有効成分"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
value={form.pesticide_profile.category}
|
||||
onChange={(e) => onPesticideFieldChange('category', e.target.value)}
|
||||
placeholder="分類"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<StockUnitSelect
|
||||
value={form.stock_unit}
|
||||
onChange={(value) => onBaseFieldChange('stock_unit', value)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
value={form.notes}
|
||||
onChange={(e) => onBaseFieldChange('notes', e.target.value)}
|
||||
placeholder="備考"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.is_active}
|
||||
onChange={(e) => onBaseFieldChange('is_active', e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<ActionButtons onSave={onSave} onCancel={onCancel} saving={saving} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="bg-green-50">
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
value={form.name}
|
||||
onChange={(e) => onBaseFieldChange('name', e.target.value)}
|
||||
placeholder="資材名"
|
||||
autoFocus
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<select
|
||||
className={inputClassName}
|
||||
value={form.material_type}
|
||||
onChange={(e) => onBaseFieldChange('material_type', e.target.value)}
|
||||
>
|
||||
<option value="other">その他</option>
|
||||
<option value="seedling">種苗</option>
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
value={form.maker}
|
||||
onChange={(e) => onBaseFieldChange('maker', e.target.value)}
|
||||
placeholder="メーカー"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<StockUnitSelect
|
||||
value={form.stock_unit}
|
||||
onChange={(value) => onBaseFieldChange('stock_unit', value)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
className={inputClassName}
|
||||
value={form.notes}
|
||||
onChange={(e) => onBaseFieldChange('notes', e.target.value)}
|
||||
placeholder="備考"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.is_active}
|
||||
onChange={(e) => onBaseFieldChange('is_active', e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<ActionButtons onSave={onSave} onCancel={onCancel} saving={saving} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionButtons({
|
||||
onSave,
|
||||
onCancel,
|
||||
saving,
|
||||
}: {
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
saving: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<button
|
||||
onClick={onSave}
|
||||
disabled={saving}
|
||||
className="text-green-600 transition hover:text-green-800 disabled:opacity-50"
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="text-gray-400 transition hover:text-gray-600"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StockUnitSelect({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: Material['stock_unit'];
|
||||
onChange: (value: Material['stock_unit']) => void;
|
||||
}) {
|
||||
return (
|
||||
<select
|
||||
className={inputClassName}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value as Material['stock_unit'])}
|
||||
>
|
||||
<option value="bag">袋</option>
|
||||
<option value="bottle">本</option>
|
||||
<option value="kg">kg</option>
|
||||
<option value="liter">L</option>
|
||||
<option value="piece">個</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user