施肥計画・肥料マスタのエラーをインライン表示に変更
alert() で表示していたバリデーションエラー・保存エラー・削除エラーを 赤いインラインバナーで表示するように変更。 ブラウザがダイアログをブロックしても確実にユーザーに通知できるようにした。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(!isNew);
|
const [loading, setLoading] = useState(!isNew);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
|
|
||||||
// ─── 初期データ取得
|
// ─── 初期データ取得
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -286,9 +287,10 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
|
|
||||||
// ─── 保存(adjusted 優先、なければ calc 値を使用)
|
// ─── 保存(adjusted 優先、なければ calc 値を使用)
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!name.trim()) return alert('計画名を入力してください');
|
setSaveError(null);
|
||||||
if (!varietyId) return alert('品種を選択してください');
|
if (!name.trim()) { setSaveError('計画名を入力してください'); return; }
|
||||||
if (selectedFields.length === 0) return alert('圃場を1つ以上選択してください');
|
if (!varietyId) { setSaveError('品種を選択してください'); return; }
|
||||||
|
if (selectedFields.length === 0) { setSaveError('圃場を1つ以上選択してください'); return; }
|
||||||
|
|
||||||
const entries: { field_id: number; fertilizer_id: number; bags: number }[] = [];
|
const entries: { field_id: number; fertilizer_id: number; bags: number }[] = [];
|
||||||
selectedFields.forEach((field) => {
|
selectedFields.forEach((field) => {
|
||||||
@@ -303,8 +305,9 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0 && planFertilizers.length > 0) {
|
||||||
if (!confirm('袋数が1件も入力されていません。このまま保存しますか?\n(後から編集画面で袋数を入力できます)')) return;
|
setSaveError('袋数が1件も入力されていません。計算ボタンを押すか、直接数値を入力してください');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@@ -315,12 +318,11 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
} else {
|
} else {
|
||||||
await api.put(`/fertilizer/plans/${planId}/`, payload);
|
await api.put(`/fertilizer/plans/${planId}/`, payload);
|
||||||
}
|
}
|
||||||
alert('保存しました');
|
|
||||||
router.push('/fertilizer');
|
router.push('/fertilizer');
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const err = e as { response?: { data?: unknown } };
|
const err = e as { response?: { data?: unknown } };
|
||||||
console.error(err);
|
console.error(err);
|
||||||
alert('保存に失敗しました: ' + JSON.stringify(err.response?.data));
|
setSaveError('保存に失敗しました: ' + JSON.stringify(err.response?.data));
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -390,6 +392,15 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* エラーバナー */}
|
||||||
|
{saveError && (
|
||||||
|
<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>{saveError}</span>
|
||||||
|
<button onClick={() => setSaveError(null)} className="ml-auto shrink-0 text-red-400 hover:text-red-600">✕</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 基本情報 */}
|
{/* 基本情報 */}
|
||||||
<div className="bg-white rounded-lg shadow p-4 mb-4 flex flex-wrap gap-4 items-end">
|
<div className="bg-white rounded-lg shadow p-4 mb-4 flex flex-wrap gap-4 items-end">
|
||||||
<div className="flex-1 min-w-48">
|
<div className="flex-1 min-w-48">
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default function FertilizerMastersPage() {
|
|||||||
const [editingId, setEditingId] = useState<number | 'new' | null>(null);
|
const [editingId, setEditingId] = useState<number | 'new' | null>(null);
|
||||||
const [form, setForm] = useState(emptyForm());
|
const [form, setForm] = useState(emptyForm());
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchFertilizers();
|
fetchFertilizers();
|
||||||
@@ -94,12 +95,13 @@ export default function FertilizerMastersPage() {
|
|||||||
|
|
||||||
const handleDelete = async (id: number, name: string) => {
|
const handleDelete = async (id: number, name: string) => {
|
||||||
if (!confirm(`「${name}」を削除しますか?`)) return;
|
if (!confirm(`「${name}」を削除しますか?`)) return;
|
||||||
|
setDeleteError(null);
|
||||||
try {
|
try {
|
||||||
await api.delete(`/fertilizer/fertilizers/${id}/`);
|
await api.delete(`/fertilizer/fertilizers/${id}/`);
|
||||||
await fetchFertilizers();
|
await fetchFertilizers();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert('削除に失敗しました(施肥計画で使用中の肥料は削除できません)');
|
setDeleteError(`「${name}」の削除に失敗しました(施肥計画で使用中の肥料は削除できません)`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,6 +133,14 @@ export default function FertilizerMastersPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{deleteError && (
|
||||||
|
<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>{deleteError}</span>
|
||||||
|
<button onClick={() => setDeleteError(null)} className="ml-auto shrink-0 text-red-400 hover:text-red-600">✕</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="text-gray-500">読み込み中...</p>
|
<p className="text-gray-500">読み込み中...</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user