散布実績レビュー修正: バグ修正・仕様適合・デッドコード削除
- 候補API: 運搬済みフィルタ(date IS NOT NULL)を追加。 delivery_plan_id指定時は全明細表示、年度全体時のみ日付フィルタ適用 - StockTransaction.spreading_item: CASCADE→SET_NULL に修正(仕様7.3準拠) - perform_destroy: SET_NULL対応でUSEを明示削除してからsession削除 - ConfirmSpreadingModal.tsx: 未使用のため削除 - FertilizerEditPage.tsx: 旧散布確定関連デッドコード全除去 (isConfirmed/confirmedAt state, handleUnconfirm, 確定取消ボタン, 確定済みバナー) - services.py: 未使用のto_decimal_or_zero削除 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,10 +56,3 @@ def sync_stock_uses_for_spreading_session(session):
|
|||||||
fertilization_plan=None,
|
fertilization_plan=None,
|
||||||
spreading_item=item,
|
spreading_item=item,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def to_decimal_or_zero(value):
|
|
||||||
try:
|
|
||||||
return Decimal(str(value))
|
|
||||||
except Exception:
|
|
||||||
return Decimal('0')
|
|
||||||
|
|||||||
@@ -381,8 +381,10 @@ class SpreadingSessionViewSet(viewsets.ModelViewSet):
|
|||||||
return SpreadingSessionSerializer
|
return SpreadingSessionSerializer
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
|
from apps.materials.models import StockTransaction
|
||||||
year = instance.year
|
year = instance.year
|
||||||
affected_pairs = {(item.field_id, item.fertilizer_id) for item in instance.items.all()}
|
affected_pairs = {(item.field_id, item.fertilizer_id) for item in instance.items.all()}
|
||||||
|
StockTransaction.objects.filter(spreading_item__session=instance).delete()
|
||||||
instance.delete()
|
instance.delete()
|
||||||
sync_actual_bags_for_pairs(year, affected_pairs)
|
sync_actual_bags_for_pairs(year, affected_pairs)
|
||||||
|
|
||||||
@@ -485,6 +487,8 @@ class SpreadingCandidatesView(APIView):
|
|||||||
delivery_queryset = DeliveryTripItem.objects.filter(trip__delivery_plan__year=year)
|
delivery_queryset = DeliveryTripItem.objects.filter(trip__delivery_plan__year=year)
|
||||||
if delivery_plan_id:
|
if delivery_plan_id:
|
||||||
delivery_queryset = delivery_queryset.filter(trip__delivery_plan_id=delivery_plan_id)
|
delivery_queryset = delivery_queryset.filter(trip__delivery_plan_id=delivery_plan_id)
|
||||||
|
else:
|
||||||
|
delivery_queryset = delivery_queryset.filter(trip__date__isnull=False)
|
||||||
delivery_rows = delivery_queryset.values(
|
delivery_rows = delivery_queryset.values(
|
||||||
'field_id',
|
'field_id',
|
||||||
'field__name',
|
'field__name',
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 5.0 on 2026-03-17 10:44
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('fertilizer', '0008_spreadingsession_fertilizationentry_actual_bags_and_more'),
|
||||||
|
('materials', '0003_stocktransaction_spreading_item_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stocktransaction',
|
||||||
|
name='spreading_item',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_transactions', to='fertilizer.spreadingsessionitem', verbose_name='散布実績明細'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -207,7 +207,7 @@ class StockTransaction(models.Model):
|
|||||||
)
|
)
|
||||||
spreading_item = models.ForeignKey(
|
spreading_item = models.ForeignKey(
|
||||||
'fertilizer.SpreadingSessionItem',
|
'fertilizer.SpreadingSessionItem',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name='stock_transactions',
|
related_name='stock_transactions',
|
||||||
|
|||||||
@@ -1,308 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
import { Loader2, X } from 'lucide-react';
|
|
||||||
|
|
||||||
import { api } from '@/lib/api';
|
|
||||||
import { FertilizationPlan } from '@/types';
|
|
||||||
|
|
||||||
interface ConfirmSpreadingModalProps {
|
|
||||||
plan: FertilizationPlan | null;
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onConfirmed: () => Promise<void> | void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActualMap = Record<string, string>;
|
|
||||||
|
|
||||||
const entryKey = (fieldId: number, fertilizerId: number) => `${fieldId}-${fertilizerId}`;
|
|
||||||
type EntryMatrix = Record<number, Record<number, string>>;
|
|
||||||
|
|
||||||
export default function ConfirmSpreadingModal({
|
|
||||||
plan,
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onConfirmed,
|
|
||||||
}: ConfirmSpreadingModalProps) {
|
|
||||||
const [actuals, setActuals] = useState<ActualMap>({});
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen || !plan) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextActuals: ActualMap = {};
|
|
||||||
plan.entries.forEach((entry) => {
|
|
||||||
nextActuals[entryKey(entry.field, entry.fertilizer)] = String(entry.bags);
|
|
||||||
});
|
|
||||||
setActuals(nextActuals);
|
|
||||||
setError(null);
|
|
||||||
}, [isOpen, plan]);
|
|
||||||
|
|
||||||
const layout = useMemo(() => {
|
|
||||||
if (!plan) {
|
|
||||||
return {
|
|
||||||
fields: [] as { id: number; name: string; areaTan: string | undefined }[],
|
|
||||||
fertilizers: [] as { id: number; name: string }[],
|
|
||||||
planned: {} as EntryMatrix,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldMap = new Map<number, { id: number; name: string; areaTan: string | undefined }>();
|
|
||||||
const fertilizerMap = new Map<number, { id: number; name: string }>();
|
|
||||||
const planned: EntryMatrix = {};
|
|
||||||
|
|
||||||
plan.entries.forEach((entry) => {
|
|
||||||
if (!fieldMap.has(entry.field)) {
|
|
||||||
fieldMap.set(entry.field, {
|
|
||||||
id: entry.field,
|
|
||||||
name: entry.field_name ?? `圃場ID:${entry.field}`,
|
|
||||||
areaTan: entry.field_area_tan,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!fertilizerMap.has(entry.fertilizer)) {
|
|
||||||
fertilizerMap.set(entry.fertilizer, {
|
|
||||||
id: entry.fertilizer,
|
|
||||||
name: entry.fertilizer_name ?? `肥料ID:${entry.fertilizer}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!planned[entry.field]) {
|
|
||||||
planned[entry.field] = {};
|
|
||||||
}
|
|
||||||
planned[entry.field][entry.fertilizer] = String(entry.bags);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
fields: Array.from(fieldMap.values()),
|
|
||||||
fertilizers: Array.from(fertilizerMap.values()),
|
|
||||||
planned,
|
|
||||||
};
|
|
||||||
}, [plan]);
|
|
||||||
|
|
||||||
if (!isOpen || !plan) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
|
||||||
setSaving(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.post(`/fertilizer/plans/${plan.id}/confirm_spreading/`, {
|
|
||||||
entries: plan.entries.map((entry) => ({
|
|
||||||
field_id: entry.field,
|
|
||||||
fertilizer_id: entry.fertilizer,
|
|
||||||
actual_bags: Number(actuals[entryKey(entry.field, entry.fertilizer)] || 0),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
await onConfirmed();
|
|
||||||
onClose();
|
|
||||||
} catch (e: unknown) {
|
|
||||||
console.error(e);
|
|
||||||
const detail =
|
|
||||||
typeof e === 'object' &&
|
|
||||||
e !== null &&
|
|
||||||
'response' in e &&
|
|
||||||
typeof e.response === 'object' &&
|
|
||||||
e.response !== null &&
|
|
||||||
'data' in e.response
|
|
||||||
? JSON.stringify(e.response.data)
|
|
||||||
: '散布確定に失敗しました。';
|
|
||||||
setError(detail);
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const numericValue = (value: string | undefined) => {
|
|
||||||
const parsed = parseFloat(value ?? '0');
|
|
||||||
return isNaN(parsed) ? 0 : parsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
const actualTotalByField = (fieldId: number) =>
|
|
||||||
layout.fertilizers.reduce(
|
|
||||||
(sum, fertilizer) => sum + numericValue(actuals[entryKey(fieldId, fertilizer.id)]),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
const actualTotalByFertilizer = (fertilizerId: number) =>
|
|
||||||
layout.fields.reduce(
|
|
||||||
(sum, field) => sum + numericValue(actuals[entryKey(field.id, fertilizerId)]),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/40 px-4">
|
|
||||||
<div className="max-h-[92vh] w-full max-w-[95vw] overflow-hidden rounded-2xl bg-white shadow-2xl">
|
|
||||||
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900">
|
|
||||||
散布確定: 「{plan.name}」
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
施肥計画編集と同じ並びで、各セルの計画値を確認しながら実績数量を入力します。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="rounded-full p-2 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
|
|
||||||
>
|
|
||||||
<X className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-h-[calc(92vh-144px)] overflow-y-auto bg-gray-50 px-6 py-5">
|
|
||||||
{error && (
|
|
||||||
<div className="mb-4 rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mb-4 rounded-lg bg-white p-4 shadow">
|
|
||||||
<div className="grid gap-3 text-sm text-gray-700 sm:grid-cols-4">
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-gray-500">年度</div>
|
|
||||||
<div className="font-medium">{plan.year}年度</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-gray-500">作物 / 品種</div>
|
|
||||||
<div className="font-medium">
|
|
||||||
{plan.crop_name} / {plan.variety_name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-gray-500">対象圃場</div>
|
|
||||||
<div className="font-medium">{plan.field_count}筆</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-gray-500">肥料数</div>
|
|
||||||
<div className="font-medium">{plan.fertilizer_count}種</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3 rounded-lg border border-sky-200 bg-sky-50 px-4 py-3 text-xs text-sky-800">
|
|
||||||
各セルの灰色表示が計画値、入力欄が散布実績です。「0」を入力したセルは未散布として引当解除されます。
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="overflow-x-auto rounded-lg bg-white shadow">
|
|
||||||
<table className="min-w-full text-sm border-collapse">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="border border-gray-200 px-4 py-3 text-left font-medium text-gray-700 whitespace-nowrap">
|
|
||||||
圃場名
|
|
||||||
</th>
|
|
||||||
<th className="border border-gray-200 px-3 py-3 text-right font-medium text-gray-700 whitespace-nowrap">
|
|
||||||
面積(反)
|
|
||||||
</th>
|
|
||||||
{layout.fertilizers.map((fertilizer) => (
|
|
||||||
<th
|
|
||||||
key={fertilizer.id}
|
|
||||||
className="border border-gray-200 px-3 py-2 text-center font-medium text-gray-700 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<div>{fertilizer.name}</div>
|
|
||||||
<div className="mt-0.5 text-[11px] font-normal text-gray-400">
|
|
||||||
計画 / 実績
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
<th className="border border-gray-200 px-3 py-3 text-right font-medium text-gray-700 whitespace-nowrap">
|
|
||||||
実績合計
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{layout.fields.map((field) => (
|
|
||||||
<tr key={field.id} className="hover:bg-gray-50">
|
|
||||||
<td className="border border-gray-200 px-4 py-2 whitespace-nowrap text-gray-800">
|
|
||||||
{field.name}
|
|
||||||
</td>
|
|
||||||
<td className="border border-gray-200 px-3 py-2 text-right text-gray-600 whitespace-nowrap">
|
|
||||||
{field.areaTan ?? '-'}
|
|
||||||
</td>
|
|
||||||
{layout.fertilizers.map((fertilizer) => {
|
|
||||||
const key = entryKey(field.id, fertilizer.id);
|
|
||||||
const planned = layout.planned[field.id]?.[fertilizer.id];
|
|
||||||
const hasEntry = planned !== undefined;
|
|
||||||
return (
|
|
||||||
<td key={key} className="border border-gray-200 px-2 py-2">
|
|
||||||
{hasEntry ? (
|
|
||||||
<div className="flex flex-col items-end gap-1">
|
|
||||||
<span className="text-[11px] text-gray-400">
|
|
||||||
計画 {planned}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="0.1"
|
|
||||||
value={actuals[key] ?? ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
setActuals((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[key]: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="w-20 rounded-md border border-gray-300 px-2 py-1 text-right text-sm focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center text-gray-300">-</div>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<td className="border border-gray-200 px-3 py-2 text-right font-medium text-gray-700">
|
|
||||||
{actualTotalByField(field.id).toFixed(2)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
<tfoot className="bg-gray-50 font-semibold">
|
|
||||||
<tr>
|
|
||||||
<td className="border border-gray-200 px-4 py-2">合計</td>
|
|
||||||
<td className="border border-gray-200 px-3 py-2 text-right text-gray-500">
|
|
||||||
{layout.fields
|
|
||||||
.reduce((sum, field) => sum + (parseFloat(field.areaTan ?? '0') || 0), 0)
|
|
||||||
.toFixed(2)}
|
|
||||||
</td>
|
|
||||||
{layout.fertilizers.map((fertilizer) => (
|
|
||||||
<td
|
|
||||||
key={fertilizer.id}
|
|
||||||
className="border border-gray-200 px-3 py-2 text-right text-gray-700"
|
|
||||||
>
|
|
||||||
{actualTotalByFertilizer(fertilizer.id).toFixed(2)}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
<td className="border border-gray-200 px-3 py-2 text-right text-green-700">
|
|
||||||
{layout.fields
|
|
||||||
.reduce((sum, field) => sum + actualTotalByField(field.id), 0)
|
|
||||||
.toFixed(2)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 border-t border-gray-200 px-6 py-4">
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-700 transition hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
キャンセル
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleConfirm}
|
|
||||||
disabled={saving}
|
|
||||||
className="inline-flex items-center gap-2 rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-60"
|
|
||||||
>
|
|
||||||
{saving && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
||||||
一括確定
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { ChevronLeft, Plus, X, Calculator, Save, FileDown, Undo2, Sprout } from 'lucide-react';
|
import { ChevronLeft, Plus, X, Calculator, Save, FileDown, Sprout } from 'lucide-react';
|
||||||
import Navbar from '@/components/Navbar';
|
import Navbar from '@/components/Navbar';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { Crop, FertilizationPlan, Fertilizer, Field, StockSummary } from '@/types';
|
import { Crop, FertilizationPlan, Fertilizer, Field, StockSummary } from '@/types';
|
||||||
@@ -66,8 +66,6 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
const [roundedColumns, setRoundedColumns] = useState<Set<number>>(new Set());
|
const [roundedColumns, setRoundedColumns] = useState<Set<number>>(new Set());
|
||||||
const [stockByMaterialId, setStockByMaterialId] = useState<Record<number, StockSummary>>({});
|
const [stockByMaterialId, setStockByMaterialId] = useState<Record<number, StockSummary>>({});
|
||||||
const [initialPlanTotals, setInitialPlanTotals] = useState<Record<number, number>>({});
|
const [initialPlanTotals, setInitialPlanTotals] = useState<Record<number, number>>({});
|
||||||
const [isConfirmed, setIsConfirmed] = useState(false);
|
|
||||||
const [confirmedAt, setConfirmedAt] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(!isNew);
|
const [loading, setLoading] = useState(!isNew);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -103,9 +101,6 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
setName(plan.name);
|
setName(plan.name);
|
||||||
setYear(plan.year);
|
setYear(plan.year);
|
||||||
setVarietyId(plan.variety);
|
setVarietyId(plan.variety);
|
||||||
setIsConfirmed(false);
|
|
||||||
setConfirmedAt(null);
|
|
||||||
|
|
||||||
const fertIds = Array.from(new Set(plan.entries.map((e) => e.fertilizer)));
|
const fertIds = Array.from(new Set(plan.entries.map((e) => e.fertilizer)));
|
||||||
const ferts = fertsRes.data.filter((f: Fertilizer) => fertIds.includes(f.id));
|
const ferts = fertsRes.data.filter((f: Fertilizer) => fertIds.includes(f.id));
|
||||||
setPlanFertilizers(ferts);
|
setPlanFertilizers(ferts);
|
||||||
@@ -202,7 +197,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
|
|
||||||
// ─── 肥料追加・削除
|
// ─── 肥料追加・削除
|
||||||
const addFertilizer = (fert: Fertilizer) => {
|
const addFertilizer = (fert: Fertilizer) => {
|
||||||
if (isConfirmed) return;
|
|
||||||
if (planFertilizers.find((f) => f.id === fert.id)) return;
|
if (planFertilizers.find((f) => f.id === fert.id)) return;
|
||||||
setPlanFertilizers((prev) => [...prev, fert]);
|
setPlanFertilizers((prev) => [...prev, fert]);
|
||||||
setCalcSettings((prev) => [...prev, { fertilizer_id: fert.id, method: 'per_tan', param: '' }]);
|
setCalcSettings((prev) => [...prev, { fertilizer_id: fert.id, method: 'per_tan', param: '' }]);
|
||||||
@@ -210,7 +205,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeFertilizer = (id: number) => {
|
const removeFertilizer = (id: number) => {
|
||||||
if (isConfirmed) return;
|
|
||||||
setPlanFertilizers((prev) => prev.filter((f) => f.id !== id));
|
setPlanFertilizers((prev) => prev.filter((f) => f.id !== id));
|
||||||
setCalcSettings((prev) => prev.filter((s) => s.fertilizer_id !== id));
|
setCalcSettings((prev) => prev.filter((s) => s.fertilizer_id !== id));
|
||||||
const dropCol = (m: Matrix): Matrix => {
|
const dropCol = (m: Matrix): Matrix => {
|
||||||
@@ -229,14 +224,14 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
|
|
||||||
// ─── 圃場追加・削除
|
// ─── 圃場追加・削除
|
||||||
const addField = (field: Field) => {
|
const addField = (field: Field) => {
|
||||||
if (isConfirmed) return;
|
|
||||||
if (selectedFields.find((f) => f.id === field.id)) return;
|
if (selectedFields.find((f) => f.id === field.id)) return;
|
||||||
setSelectedFields((prev) => [...prev, field]);
|
setSelectedFields((prev) => [...prev, field]);
|
||||||
setShowFieldPicker(false);
|
setShowFieldPicker(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeField = (id: number) => {
|
const removeField = (id: number) => {
|
||||||
if (isConfirmed) return;
|
|
||||||
setSelectedFields((prev) => prev.filter((f) => f.id !== id));
|
setSelectedFields((prev) => prev.filter((f) => f.id !== id));
|
||||||
setCalcMatrix((prev) => { const next = { ...prev }; delete next[id]; return next; });
|
setCalcMatrix((prev) => { const next = { ...prev }; delete next[id]; return next; });
|
||||||
setAdjusted((prev) => { const next = { ...prev }; delete next[id]; return next; });
|
setAdjusted((prev) => { const next = { ...prev }; delete next[id]; return next; });
|
||||||
@@ -246,7 +241,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
|
|
||||||
// ─── 自動計算
|
// ─── 自動計算
|
||||||
const runCalc = async (setting: CalcSetting) => {
|
const runCalc = async (setting: CalcSetting) => {
|
||||||
if (isConfirmed) return;
|
|
||||||
if (!setting.param) {
|
if (!setting.param) {
|
||||||
setSaveError('パラメータを入力してください');
|
setSaveError('パラメータを入力してください');
|
||||||
return;
|
return;
|
||||||
@@ -305,7 +300,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
|
|
||||||
// ─── セル更新(adjusted を更新)
|
// ─── セル更新(adjusted を更新)
|
||||||
const updateCell = (fieldId: number, fertId: number, value: string) => {
|
const updateCell = (fieldId: number, fertId: number, value: string) => {
|
||||||
if (isConfirmed) return;
|
|
||||||
setAdjusted((prev) => {
|
setAdjusted((prev) => {
|
||||||
const next = { ...prev };
|
const next = { ...prev };
|
||||||
if (!next[fieldId]) next[fieldId] = {};
|
if (!next[fieldId]) next[fieldId] = {};
|
||||||
@@ -316,7 +311,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
|
|
||||||
// ─── 列単位で四捨五入 / 元に戻す(トグル)
|
// ─── 列単位で四捨五入 / 元に戻す(トグル)
|
||||||
const roundColumn = (fertId: number) => {
|
const roundColumn = (fertId: number) => {
|
||||||
if (isConfirmed) return;
|
|
||||||
if (roundedColumns.has(fertId)) {
|
if (roundedColumns.has(fertId)) {
|
||||||
// 元に戻す: adjusted からこの列を削除 → calc値が再び表示される
|
// 元に戻す: adjusted からこの列を削除 → calc値が再び表示される
|
||||||
setAdjusted((prev) => {
|
setAdjusted((prev) => {
|
||||||
@@ -390,10 +385,6 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
// ─── 保存(adjusted 優先、なければ calc 値を使用)
|
// ─── 保存(adjusted 優先、なければ calc 値を使用)
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaveError(null);
|
setSaveError(null);
|
||||||
if (isConfirmed) {
|
|
||||||
setSaveError('確定済みの施肥計画は編集できません。');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!name.trim()) { setSaveError('計画名を入力してください'); return; }
|
if (!name.trim()) { setSaveError('計画名を入力してください'); return; }
|
||||||
if (!varietyId) { setSaveError('品種を選択してください'); return; }
|
if (!varietyId) { setSaveError('品種を選択してください'); return; }
|
||||||
if (selectedFields.length === 0) { setSaveError('圃場を1つ以上選択してください'); return; }
|
if (selectedFields.length === 0) { setSaveError('圃場を1つ以上選択してください'); return; }
|
||||||
@@ -434,31 +425,6 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── 確定取消
|
|
||||||
const handleUnconfirm = async () => {
|
|
||||||
if (!planId) return;
|
|
||||||
setSaveError(null);
|
|
||||||
try {
|
|
||||||
await api.post(`/fertilizer/plans/${planId}/unconfirm/`);
|
|
||||||
setIsConfirmed(false);
|
|
||||||
setConfirmedAt(null);
|
|
||||||
// 引当が再作成されるので在庫情報を再取得
|
|
||||||
const stockRes = await api.get('/materials/fertilizer-stock/');
|
|
||||||
setStockByMaterialId(
|
|
||||||
stockRes.data.reduce(
|
|
||||||
(acc: Record<number, StockSummary>, summary: StockSummary) => {
|
|
||||||
acc[summary.material_id] = summary;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
setSaveError('確定取消に失敗しました');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── PDF出力
|
// ─── PDF出力
|
||||||
const handlePdf = async () => {
|
const handlePdf = async () => {
|
||||||
if (!planId) return;
|
if (!planId) return;
|
||||||
@@ -514,15 +480,6 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
この施肥計画から散布実績へ進む
|
この施肥計画から散布実績へ進む
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!isNew && isConfirmed && (
|
|
||||||
<button
|
|
||||||
onClick={handleUnconfirm}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 border border-amber-300 rounded-lg text-sm text-amber-700 hover:bg-amber-50"
|
|
||||||
>
|
|
||||||
<Undo2 className="h-4 w-4" />
|
|
||||||
確定取消
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!isNew && (
|
{!isNew && (
|
||||||
<button
|
<button
|
||||||
onClick={handlePdf}
|
onClick={handlePdf}
|
||||||
@@ -534,11 +491,11 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving || isConfirmed}
|
disabled={saving}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<Save className="h-4 w-4" />
|
<Save className="h-4 w-4" />
|
||||||
{isConfirmed ? '確定済み' : saving ? '保存中...' : '保存'}
|
{saving ? '保存中...' : '保存'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -552,16 +509,6 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isConfirmed && (
|
|
||||||
<div className="mb-4 flex items-start gap-2 bg-sky-50 border border-sky-300 text-sky-800 rounded-lg px-4 py-3 text-sm">
|
|
||||||
<span className="font-bold shrink-0">i</span>
|
|
||||||
<span>
|
|
||||||
この施肥計画は散布確定済みです。
|
|
||||||
{confirmedAt ? ` 確定日時: ${new Date(confirmedAt).toLocaleString('ja-JP')}` : ''}
|
|
||||||
</span>
|
|
||||||
</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">
|
||||||
@@ -571,7 +518,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="例: 2025年度 コシヒカリ 元肥"
|
placeholder="例: 2025年度 コシヒカリ 元肥"
|
||||||
disabled={isConfirmed}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -580,7 +527,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
className="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
|
className="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
value={year}
|
value={year}
|
||||||
onChange={(e) => setYear(parseInt(e.target.value))}
|
onChange={(e) => setYear(parseInt(e.target.value))}
|
||||||
disabled={isConfirmed}
|
|
||||||
>
|
>
|
||||||
{years.map((y) => <option key={y} value={y}>{y}年度</option>)}
|
{years.map((y) => <option key={y} value={y}>{y}年度</option>)}
|
||||||
</select>
|
</select>
|
||||||
@@ -591,7 +538,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
value={varietyId}
|
value={varietyId}
|
||||||
onChange={(e) => setVarietyId(e.target.value ? parseInt(e.target.value) : '')}
|
onChange={(e) => setVarietyId(e.target.value ? parseInt(e.target.value) : '')}
|
||||||
disabled={isConfirmed}
|
|
||||||
>
|
>
|
||||||
<option value="">品種を選択</option>
|
<option value="">品種を選択</option>
|
||||||
{crops.map((crop) => (
|
{crops.map((crop) => (
|
||||||
@@ -617,7 +564,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowFieldPicker(true)}
|
onClick={() => setShowFieldPicker(true)}
|
||||||
disabled={isConfirmed}
|
|
||||||
className="flex items-center gap-1 text-xs text-green-600 hover:text-green-800 border border-green-300 rounded px-2 py-1 disabled:opacity-40 disabled:cursor-not-allowed"
|
className="flex items-center gap-1 text-xs text-green-600 hover:text-green-800 border border-green-300 rounded px-2 py-1 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />圃場を追加
|
<Plus className="h-3 w-3" />圃場を追加
|
||||||
@@ -637,7 +584,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
{f.name}({f.area_tan}反)
|
{f.name}({f.area_tan}反)
|
||||||
<button
|
<button
|
||||||
onClick={() => removeField(f.id)}
|
onClick={() => removeField(f.id)}
|
||||||
disabled={isConfirmed}
|
|
||||||
className="text-green-400 hover:text-red-500 disabled:opacity-40 disabled:cursor-not-allowed"
|
className="text-green-400 hover:text-red-500 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
@@ -657,14 +604,14 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={calcNewOnly}
|
checked={calcNewOnly}
|
||||||
onChange={(e) => setCalcNewOnly(e.target.checked)}
|
onChange={(e) => setCalcNewOnly(e.target.checked)}
|
||||||
disabled={isConfirmed}
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
未入力圃場のみ
|
未入力圃場のみ
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowFertPicker(true)}
|
onClick={() => setShowFertPicker(true)}
|
||||||
disabled={isConfirmed}
|
|
||||||
className="flex items-center gap-1 text-xs text-green-600 hover:text-green-800 border border-green-300 rounded px-2 py-1 disabled:opacity-40 disabled:cursor-not-allowed"
|
className="flex items-center gap-1 text-xs text-green-600 hover:text-green-800 border border-green-300 rounded px-2 py-1 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />肥料を追加
|
<Plus className="h-3 w-3" />肥料を追加
|
||||||
@@ -687,7 +634,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
className="border border-gray-300 rounded px-2 py-1 text-xs"
|
className="border border-gray-300 rounded px-2 py-1 text-xs"
|
||||||
value={setting.method}
|
value={setting.method}
|
||||||
onChange={(e) => updateCalcSetting(fert.id, 'method', e.target.value)}
|
onChange={(e) => updateCalcSetting(fert.id, 'method', e.target.value)}
|
||||||
disabled={isConfirmed}
|
|
||||||
>
|
>
|
||||||
{Object.entries(METHOD_LABELS).map(([k, v]) => (
|
{Object.entries(METHOD_LABELS).map(([k, v]) => (
|
||||||
<option key={k} value={k}>{v}</option>
|
<option key={k} value={k}>{v}</option>
|
||||||
@@ -700,19 +647,19 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
value={setting.param}
|
value={setting.param}
|
||||||
onChange={(e) => updateCalcSetting(fert.id, 'param', e.target.value)}
|
onChange={(e) => updateCalcSetting(fert.id, 'param', e.target.value)}
|
||||||
placeholder="値"
|
placeholder="値"
|
||||||
disabled={isConfirmed}
|
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-500 w-24">{METHOD_UNIT[setting.method]}</span>
|
<span className="text-xs text-gray-500 w-24">{METHOD_UNIT[setting.method]}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => runCalc(setting)}
|
onClick={() => runCalc(setting)}
|
||||||
disabled={isConfirmed}
|
|
||||||
className="flex items-center gap-1 text-xs bg-blue-50 border border-blue-300 text-blue-700 rounded px-3 py-1 hover:bg-blue-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
className="flex items-center gap-1 text-xs bg-blue-50 border border-blue-300 text-blue-700 rounded px-3 py-1 hover:bg-blue-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<Calculator className="h-3 w-3" />計算
|
<Calculator className="h-3 w-3" />計算
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeFertilizer(fert.id)}
|
onClick={() => removeFertilizer(fert.id)}
|
||||||
disabled={isConfirmed}
|
|
||||||
className="ml-auto text-gray-300 hover:text-red-500 disabled:opacity-40 disabled:cursor-not-allowed"
|
className="ml-auto text-gray-300 hover:text-red-500 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
@@ -766,7 +713,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
(袋)
|
(袋)
|
||||||
<button
|
<button
|
||||||
onClick={() => roundColumn(f.id)}
|
onClick={() => roundColumn(f.id)}
|
||||||
disabled={isConfirmed}
|
|
||||||
className={`inline-flex items-center justify-center w-5 h-5 rounded font-bold leading-none ${
|
className={`inline-flex items-center justify-center w-5 h-5 rounded font-bold leading-none ${
|
||||||
isRounded
|
isRounded
|
||||||
? 'bg-amber-100 text-amber-600 hover:bg-amber-200'
|
? 'bg-amber-100 text-amber-600 hover:bg-amber-200'
|
||||||
@@ -810,7 +757,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => updateCell(field.id, fert.id, e.target.value)}
|
onChange={(e) => updateCell(field.id, fert.id, e.target.value)}
|
||||||
placeholder="-"
|
placeholder="-"
|
||||||
disabled={isConfirmed}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{actualVal !== undefined && (
|
{actualVal !== undefined && (
|
||||||
@@ -865,7 +812,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
<button
|
<button
|
||||||
key={f.id}
|
key={f.id}
|
||||||
onClick={() => addField(f)}
|
onClick={() => addField(f)}
|
||||||
disabled={isConfirmed}
|
|
||||||
className="w-full text-left px-3 py-2 hover:bg-green-50 rounded text-sm flex justify-between disabled:opacity-40 disabled:cursor-not-allowed"
|
className="w-full text-left px-3 py-2 hover:bg-green-50 rounded text-sm flex justify-between disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<span>{f.name}</span>
|
<span>{f.name}</span>
|
||||||
@@ -880,7 +827,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
<button
|
<button
|
||||||
key={f.id}
|
key={f.id}
|
||||||
onClick={() => addField(f)}
|
onClick={() => addField(f)}
|
||||||
disabled={isConfirmed}
|
|
||||||
className="w-full text-left px-3 py-2 hover:bg-gray-50 rounded text-sm flex justify-between disabled:opacity-40 disabled:cursor-not-allowed"
|
className="w-full text-left px-3 py-2 hover:bg-gray-50 rounded text-sm flex justify-between disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<span>{f.name}</span>
|
<span>{f.name}</span>
|
||||||
@@ -908,7 +855,7 @@ export default function FertilizerEditPage({ planId }: { planId?: number }) {
|
|||||||
<button
|
<button
|
||||||
key={f.id}
|
key={f.id}
|
||||||
onClick={() => addFertilizer(f)}
|
onClick={() => addFertilizer(f)}
|
||||||
disabled={isConfirmed}
|
|
||||||
className="w-full text-left px-3 py-2 hover:bg-green-50 rounded text-sm disabled:opacity-40 disabled:cursor-not-allowed"
|
className="w-full text-left px-3 py-2 hover:bg-green-50 rounded text-sm disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<span className="font-medium">{f.name}</span>
|
<span className="font-medium">{f.name}</span>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user