完璧に動作しています。
テスト 結果 確定取消 API ✅ is_confirmed: false, confirmed_at: null USE トランザクション削除 ✅ current_stock が 27.5→32 に復帰 引当再作成 ✅ reserved_stock = 5.000 に復帰 追加した変更: stock_service.py:81-93 — unconfirm_spreading(): USE削除→確定フラグリセット→引当再作成 fertilizer/views.py — unconfirm アクション(POST /api/fertilizer/plans/{id}/unconfirm/) fertilizer/page.tsx — 一覧に「確定取消」ボタン(確定済み計画のみ表示) FertilizerEditPage.tsx — 編集画面ヘッダーに「確定取消」ボタン + 在庫情報再取得
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Plus, Pencil, Trash2, FileDown, Sprout } from 'lucide-react';
|
||||
import { Plus, Pencil, Trash2, FileDown, Sprout, BadgeCheck, Undo2 } from 'lucide-react';
|
||||
|
||||
import ConfirmSpreadingModal from './_components/ConfirmSpreadingModal';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import { api } from '@/lib/api';
|
||||
import { FertilizationPlan } from '@/types';
|
||||
@@ -21,6 +23,8 @@ export default function FertilizerPage() {
|
||||
const [plans, setPlans] = useState<FertilizationPlan[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const [confirmTarget, setConfirmTarget] = useState<FertilizationPlan | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('fertilizerYear', String(year));
|
||||
@@ -41,6 +45,7 @@ export default function FertilizerPage() {
|
||||
|
||||
const handleDelete = async (id: number, name: string) => {
|
||||
setDeleteError(null);
|
||||
setActionError(null);
|
||||
try {
|
||||
await api.delete(`/fertilizer/plans/${id}/`);
|
||||
await fetchPlans();
|
||||
@@ -50,7 +55,19 @@ export default function FertilizerPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnconfirm = async (id: number, name: string) => {
|
||||
setActionError(null);
|
||||
try {
|
||||
await api.post(`/fertilizer/plans/${id}/unconfirm/`);
|
||||
await fetchPlans();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setActionError(`「${name}」の確定取消に失敗しました`);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePdf = async (id: number, name: string) => {
|
||||
setActionError(null);
|
||||
try {
|
||||
const res = await api.get(`/fertilizer/plans/${id}/pdf/`, { responseType: 'blob' });
|
||||
const url = URL.createObjectURL(new Blob([res.data], { type: 'application/pdf' }));
|
||||
@@ -61,7 +78,7 @@ export default function FertilizerPage() {
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('PDF出力に失敗しました');
|
||||
setActionError('PDF出力に失敗しました');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -115,6 +132,14 @@ export default function FertilizerPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actionError && (
|
||||
<div className="mb-4 flex items-start gap-2 bg-red-50 border border-red-300 text-red-700 rounded-lg px-4 py-3 text-sm">
|
||||
<span className="font-bold shrink-0">⚠</span>
|
||||
<span>{actionError}</span>
|
||||
<button onClick={() => setActionError(null)} className="ml-auto shrink-0 text-red-400 hover:text-red-600">✕</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<p className="text-gray-500">読み込み中...</p>
|
||||
) : plans.length === 0 ? (
|
||||
@@ -135,6 +160,7 @@ export default function FertilizerPage() {
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 font-medium text-gray-700">計画名</th>
|
||||
<th className="text-left px-4 py-3 font-medium text-gray-700">作物 / 品種</th>
|
||||
<th className="text-left px-4 py-3 font-medium text-gray-700">状態</th>
|
||||
<th className="text-right px-4 py-3 font-medium text-gray-700">圃場数</th>
|
||||
<th className="text-right px-4 py-3 font-medium text-gray-700">肥料種数</th>
|
||||
<th className="px-4 py-3"></th>
|
||||
@@ -142,15 +168,52 @@ export default function FertilizerPage() {
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{plans.map((plan) => (
|
||||
<tr key={plan.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3 font-medium">{plan.name}</td>
|
||||
<tr
|
||||
key={plan.id}
|
||||
className={plan.is_confirmed ? 'bg-sky-50 hover:bg-sky-100/60' : 'hover:bg-gray-50'}
|
||||
>
|
||||
<td className="px-4 py-3 font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{plan.name}</span>
|
||||
{plan.is_confirmed && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-sky-100 px-2 py-0.5 text-xs text-sky-700">
|
||||
<BadgeCheck className="h-3.5 w-3.5" />
|
||||
確定済み
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-600">
|
||||
{plan.crop_name} / {plan.variety_name}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-600">
|
||||
{plan.is_confirmed
|
||||
? `散布確定 ${plan.confirmed_at ? new Date(plan.confirmed_at).toLocaleString('ja-JP') : ''}`
|
||||
: '未確定'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-gray-600">{plan.field_count}筆</td>
|
||||
<td className="px-4 py-3 text-right text-gray-600">{plan.fertilizer_count}種</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
{!plan.is_confirmed ? (
|
||||
<button
|
||||
onClick={() => setConfirmTarget(plan)}
|
||||
className="flex items-center gap-1 px-2.5 py-1.5 text-xs border border-emerald-300 rounded hover:bg-emerald-50 text-emerald-700"
|
||||
title="散布確定"
|
||||
>
|
||||
<BadgeCheck className="h-3.5 w-3.5" />
|
||||
散布確定
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleUnconfirm(plan.id, plan.name)}
|
||||
className="flex items-center gap-1 px-2.5 py-1.5 text-xs border border-amber-300 rounded hover:bg-amber-50 text-amber-700"
|
||||
title="確定取消"
|
||||
>
|
||||
<Undo2 className="h-3.5 w-3.5" />
|
||||
確定取消
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handlePdf(plan.id, plan.name)}
|
||||
className="flex items-center gap-1 px-2.5 py-1.5 text-xs border border-gray-300 rounded hover:bg-gray-100 text-gray-700"
|
||||
@@ -184,6 +247,13 @@ export default function FertilizerPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConfirmSpreadingModal
|
||||
isOpen={confirmTarget !== null}
|
||||
plan={confirmTarget}
|
||||
onClose={() => setConfirmTarget(null)}
|
||||
onConfirmed={fetchPlans}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user