A-6 完了。 本セッションの進捗まとめ:

タスク	内容	状態
A-3	前年度コピーボタン	 完了
A-4	品種のインライン追加・削除	 完了
A-5	PDFプレビュー機能	 完了
A-6	エクスポート機能	 完了
残りタスク:

A-2: チェックボックス・一括操作
A-1: ダッシュボード画面
A-7: 検索・フィルタ
確認ポイント:

作付け計画 (/allocation): 年度セレクタの横に「前年度コピー」「品種管理」ボタン、品種セレクトに「+ 新しい品種を追加...」
帳票出力 (/reports): 各帳票にプレビュー/ダウンロードの2ボタン
データ取込 (/import): ページ下部に「データエクスポート」(ZIPダウンロード)
This commit is contained in:
Akira
2026-02-19 12:21:17 +09:00
parent 23cb4d3118
commit 8b5e0fc66e
9 changed files with 497 additions and 128 deletions

View File

@@ -3,7 +3,7 @@
import { useState } from 'react';
import { api } from '@/lib/api';
import Navbar from '@/components/Navbar';
import { FileDown, Loader2 } from 'lucide-react';
import { FileDown, Eye, Loader2 } from 'lucide-react';
const downloadPdf = async (url: string, filename: string) => {
const response = await api.get(url, { responseType: 'blob' });
@@ -18,31 +18,35 @@ const downloadPdf = async (url: string, filename: string) => {
window.URL.revokeObjectURL(downloadUrl);
};
const previewPdf = async (url: string) => {
const response = await api.get(url, { responseType: 'blob' });
const blob = new Blob([response.data], { type: 'application/pdf' });
const previewUrl = window.URL.createObjectURL(blob);
window.open(previewUrl, '_blank');
};
export default function ReportsPage() {
const [year, setYear] = useState<number>(2025);
const [downloading, setDownloading] = useState<string | null>(null);
const [busy, setBusy] = useState<string | null>(null);
const handleDownloadKyosai = async () => {
setDownloading('kyosai');
const handleAction = async (action: 'download' | 'preview', type: 'kyosai' | 'chusankan') => {
const key = `${action}-${type}`;
setBusy(key);
try {
await downloadPdf(`/reports/kyosai/${year}/`, `水稲共済細目書_${year}.pdf`);
const url = `/reports/${type}/${year}/`;
if (action === 'download') {
const filename = type === 'kyosai'
? `水稲共済細目書_${year}.pdf`
: `中山間交付金申請書_${year}.pdf`;
await downloadPdf(url, filename);
} else {
await previewPdf(url);
}
} catch (error) {
console.error('Download failed:', error);
alert('ダウンロードに失敗しました');
console.error(`${action} failed:`, error);
alert(`${action === 'download' ? 'ダウンロード' : 'プレビュー'}に失敗しました`);
} finally {
setDownloading(null);
}
};
const handleDownloadChusankan = async () => {
setDownloading('chusankan');
try {
await downloadPdf(`/reports/chusankan/${year}/`, `中山間交付金申请书_${year}.pdf`);
} catch (error) {
console.error('Download failed:', error);
alert('ダウンロードに失敗しました');
} finally {
setDownloading(null);
setBusy(null);
}
};
@@ -70,41 +74,63 @@ export default function ReportsPage() {
</div>
<div className="space-y-4">
<button
onClick={handleDownloadKyosai}
disabled={downloading !== null}
className="w-full flex items-center justify-center px-4 py-3 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{downloading === 'kyosai' ? (
<>
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
...
</>
) : (
<>
<FileDown className="h-5 w-5 mr-2" />
</>
)}
</button>
{/* 水稲共済細目書 */}
<div className="border rounded-lg p-4">
<h3 className="font-medium text-gray-900 mb-3"></h3>
<div className="flex gap-3">
<button
onClick={() => handleAction('preview', 'kyosai')}
disabled={busy !== null}
className="flex-1 flex items-center justify-center px-4 py-2.5 border border-green-600 text-green-700 rounded-md hover:bg-green-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{busy === 'preview-kyosai' ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />...</>
) : (
<><Eye className="h-4 w-4 mr-2" /></>
)}
</button>
<button
onClick={() => handleAction('download', 'kyosai')}
disabled={busy !== null}
className="flex-1 flex items-center justify-center px-4 py-2.5 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{busy === 'download-kyosai' ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />...</>
) : (
<><FileDown className="h-4 w-4 mr-2" /></>
)}
</button>
</div>
</div>
<button
onClick={handleDownloadChusankan}
disabled={downloading !== null}
className="w-full flex items-center justify-center px-4 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{downloading === 'chusankan' ? (
<>
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
...
</>
) : (
<>
<FileDown className="h-5 w-5 mr-2" />
</>
)}
</button>
{/* 中山間交付金申請書 */}
<div className="border rounded-lg p-4">
<h3 className="font-medium text-gray-900 mb-3"></h3>
<div className="flex gap-3">
<button
onClick={() => handleAction('preview', 'chusankan')}
disabled={busy !== null}
className="flex-1 flex items-center justify-center px-4 py-2.5 border border-blue-600 text-blue-700 rounded-md hover:bg-blue-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{busy === 'preview-chusankan' ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />...</>
) : (
<><Eye className="h-4 w-4 mr-2" /></>
)}
</button>
<button
onClick={() => handleAction('download', 'chusankan')}
disabled={busy !== null}
className="flex-1 flex items-center justify-center px-4 py-2.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{busy === 'download-chusankan' ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />...</>
) : (
<><FileDown className="h-4 w-4 mr-2" /></>
)}
</button>
</div>
</div>
</div>
</div>
</div>