Day 11 完了

実装内容:
1. frontend/src/components/Navbar.tsx - データ取込リンク追加
2. frontend/src/app/import/page.tsx - データインポート画面
機能:
- 共済マスタ取込(POST /api/fields/import/kyosai/)
- 実圃場データ取込(POST /api/fields/import/yoshida/)
- ファイル選択 (.ods)
- 結果表示(作成件数、更新件数)
- エラー表示
API動作確認:
- /api/fields/import/kyosai/ → HTTP 400(ファイルなし時)
- /api/fields/import/yoshida/ → HTTP 400(ファイルなし時)
ブラウザで http://localhost:3000/import からデータインポートが可能です。
次の工程に移りますか?
This commit is contained in:
Akira
2026-02-15 14:02:46 +09:00
parent 6334c6deaa
commit 592aedb665
2 changed files with 292 additions and 1 deletions

View File

@@ -0,0 +1,280 @@
'use client';
import { useState, useRef } from 'react';
import { api } from '@/lib/api';
import Navbar from '@/components/Navbar';
import { Upload, Loader2, CheckCircle, XCircle } from 'lucide-react';
interface ImportResult {
success: boolean;
message: string;
created?: number;
updated?: number;
}
export default function ImportPage() {
const [kyosaiFile, setKyosaiFile] = useState<File | null>(null);
const [yoshidaFile, setYoshidaFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false);
const [kyosaiResult, setKyosaiResult] = useState<ImportResult | null>(null);
const [yoshidaResult, setYoshidaResult] = useState<ImportResult | null>(null);
const kyosaiInputRef = useRef<HTMLInputElement>(null);
const yoshidaInputRef = useRef<HTMLInputElement>(null);
const handleKyosaiUpload = async () => {
if (!kyosaiFile) {
alert('ファイルを選択してください');
return;
}
setUploading(true);
setKyosaiResult(null);
try {
const formData = new FormData();
formData.append('file', kyosaiFile);
const response = await api.post('/fields/import/kyosai/', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
const data = response.data;
setKyosaiResult({
success: true,
message: data.message || 'インポートが完了しました',
created: data.created,
updated: data.updated,
});
} catch (error: unknown) {
console.error('Upload failed:', error);
let errorMessage = 'アップロードに失敗しました';
if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as { response?: { data?: { error?: string } } };
if (axiosError.response?.data?.error) {
errorMessage = axiosError.response.data.error;
}
}
setKyosaiResult({
success: false,
message: errorMessage,
});
} finally {
setUploading(false);
}
};
const handleYoshidaUpload = async () => {
if (!yoshidaFile) {
alert('ファイルを選択してください');
return;
}
setUploading(true);
setYoshidaResult(null);
try {
const formData = new FormData();
formData.append('file', yoshidaFile);
const response = await api.post('/fields/import/yoshida/', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
const data = response.data;
setYoshidaResult({
success: true,
message: data.message || 'インポートが完了しました',
created: data.created,
updated: data.updated,
});
} catch (error: unknown) {
console.error('Upload failed:', error);
let errorMessage = 'アップロードに失敗しました';
if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as { response?: { data?: { error?: string } } };
if (axiosError.response?.data?.error) {
errorMessage = axiosError.response.data.error;
}
}
setYoshidaResult({
success: false,
message: errorMessage,
});
} finally {
setUploading(false);
}
};
return (
<div className="min-h-screen bg-gray-50">
<Navbar />
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 className="text-2xl font-bold text-gray-900 mb-6"></h1>
<div className="space-y-6">
{/* 共済マスタ取込 */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-2">
</h2>
<p className="text-sm text-gray-600 mb-4">
k_num, s_num, address...
</p>
<div className="flex items-center gap-4 mb-4">
<input
ref={kyosaiInputRef}
type="file"
accept=".ods"
onChange={(e) => setKyosaiFile(e.target.files?.[0] || null)}
className="hidden"
/>
<button
onClick={() => kyosaiInputRef.current?.click()}
className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
>
{kyosaiFile ? kyosaiFile.name : 'ファイルを選択'}
</button>
{kyosaiFile && (
<button
onClick={() => {
setKyosaiFile(null);
setKyosaiResult(null);
}}
className="text-gray-500 hover:text-gray-700"
>
</button>
)}
</div>
<button
onClick={handleKyosaiUpload}
disabled={!kyosaiFile || uploading}
className="w-full flex items-center justify-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{uploading ? (
<>
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
...
</>
) : (
<>
<Upload className="h-5 w-5 mr-2" />
</>
)}
</button>
{kyosaiResult && (
<div
className={`mt-4 p-3 rounded-md flex items-center ${
kyosaiResult.success
? 'bg-green-50 text-green-700'
: 'bg-red-50 text-red-700'
}`}
>
{kyosaiResult.success ? (
<CheckCircle className="h-5 w-5 mr-2" />
) : (
<XCircle className="h-5 w-5 mr-2" />
)}
<div>
<p className="font-medium">{kyosaiResult.message}</p>
{kyosaiResult.success && kyosaiResult.created !== undefined && (
<p className="text-sm">
: {kyosaiResult.created} / : {kyosaiResult.updated}
</p>
)}
</div>
</div>
)}
</div>
{/* 実圃場データ取込 */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-2">
</h2>
<p className="text-sm text-gray-600 mb-4">
</p>
<div className="flex items-center gap-4 mb-4">
<input
ref={yoshidaInputRef}
type="file"
accept=".ods"
onChange={(e) => setYoshidaFile(e.target.files?.[0] || null)}
className="hidden"
/>
<button
onClick={() => yoshidaInputRef.current?.click()}
className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
>
{yoshidaFile ? yoshidaFile.name : 'ファイルを選択'}
</button>
{yoshidaFile && (
<button
onClick={() => {
setYoshidaFile(null);
setYoshidaResult(null);
}}
className="text-gray-500 hover:text-gray-700"
>
</button>
)}
</div>
<button
onClick={handleYoshidaUpload}
disabled={!yoshidaFile || uploading}
className="w-full flex items-center justify-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{uploading ? (
<>
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
...
</>
) : (
<>
<Upload className="h-5 w-5 mr-2" />
</>
)}
</button>
{yoshidaResult && (
<div
className={`mt-4 p-3 rounded-md flex items-center ${
yoshidaResult.success
? 'bg-green-50 text-green-700'
: 'bg-red-50 text-red-700'
}`}
>
{yoshidaResult.success ? (
<CheckCircle className="h-5 w-5 mr-2" />
) : (
<XCircle className="h-5 w-5 mr-2" />
)}
<div>
<p className="font-medium">{yoshidaResult.message}</p>
{yoshidaResult.success && yoshidaResult.created !== undefined && (
<p className="text-sm">
: {yoshidaResult.created} / : {yoshidaResult.updated}
</p>
)}
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
}