施肥散布実績機能を実装し運搬・作業記録・在庫連携を追加

This commit is contained in:
Akira
2026-03-17 19:28:52 +09:00
parent 865d53ed9a
commit 140d5e5a4d
31 changed files with 2053 additions and 248 deletions

View File

@@ -0,0 +1,138 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { ChevronLeft, NotebookText } from 'lucide-react';
import Navbar from '@/components/Navbar';
import { api } from '@/lib/api';
import { WorkRecord } from '@/types';
const CURRENT_YEAR = new Date().getFullYear();
const YEAR_KEY = 'workRecordYear';
export default function WorkRecordsPage() {
const router = useRouter();
const [year, setYear] = useState<number>(() => {
if (typeof window !== 'undefined') {
return parseInt(localStorage.getItem(YEAR_KEY) || String(CURRENT_YEAR), 10);
}
return CURRENT_YEAR;
});
const [records, setRecords] = useState<WorkRecord[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
localStorage.setItem(YEAR_KEY, String(year));
void fetchRecords();
}, [year]);
const fetchRecords = async () => {
setLoading(true);
setError(null);
try {
const res = await api.get(`/workrecords/?year=${year}`);
setRecords(res.data);
} catch (e) {
console.error(e);
setError('作業記録の読み込みに失敗しました。');
} finally {
setLoading(false);
}
};
const moveToSource = (record: WorkRecord) => {
if (record.spreading_session) {
router.push(`/fertilizer/spreading?session=${record.spreading_session}`);
return;
}
if (record.delivery_plan_id) {
router.push(`/distribution/${record.delivery_plan_id}/edit`);
}
};
const years = Array.from({ length: 5 }, (_, i) => CURRENT_YEAR + 1 - i);
return (
<div className="min-h-screen bg-gray-50">
<Navbar />
<main className="mx-auto max-w-6xl px-4 py-8">
<div className="mb-6 flex items-center gap-3">
<button onClick={() => router.push('/fertilizer')} className="text-gray-500 hover:text-gray-700">
<ChevronLeft className="h-5 w-5" />
</button>
<NotebookText className="h-6 w-6 text-green-700" />
<h1 className="text-2xl font-bold text-gray-900"></h1>
</div>
<div className="mb-6 flex items-center gap-3">
<label className="text-sm font-medium text-gray-700">:</label>
<select
value={year}
onChange={(e) => setYear(Number(e.target.value))}
className="rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
>
{years.map((y) => (
<option key={y} value={y}>
{y}
</option>
))}
</select>
</div>
{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="overflow-hidden rounded-lg bg-white shadow-sm">
{loading ? (
<div className="px-5 py-8 text-sm text-gray-500">...</div>
) : records.length === 0 ? (
<div className="px-5 py-8 text-sm text-gray-400"></div>
) : (
<table className="w-full text-sm">
<thead className="border-b bg-gray-50">
<tr>
<th className="px-4 py-3 text-left font-medium text-gray-700"></th>
<th className="px-4 py-3 text-left font-medium text-gray-700"></th>
<th className="px-4 py-3 text-left font-medium text-gray-700"></th>
<th className="px-4 py-3 text-left font-medium text-gray-700"></th>
<th className="px-4 py-3" />
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{records.map((record) => (
<tr key={record.id} className="hover:bg-gray-50">
<td className="px-4 py-3 text-gray-700">{record.work_date}</td>
<td className="px-4 py-3 text-gray-700">{record.work_type_display}</td>
<td className="px-4 py-3 font-medium text-gray-900">{record.title}</td>
<td className="px-4 py-3 text-gray-600">
{record.spreading_session
? `散布実績 #${record.spreading_session}`
: record.delivery_plan_name
? `${record.delivery_plan_name}`
: '-'}
</td>
<td className="px-4 py-3 text-right">
{(record.spreading_session || record.delivery_plan_id) && (
<button
onClick={() => moveToSource(record)}
className="rounded border border-gray-300 px-2.5 py-1.5 text-xs text-gray-700 hover:bg-gray-100"
>
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</main>
</div>
);
}