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:
280
frontend/src/app/import/page.tsx
Normal file
280
frontend/src/app/import/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { LogOut, Wheat, MapPin, FileText } from 'lucide-react';
|
||||
import { LogOut, Wheat, MapPin, FileText, Upload } from 'lucide-react';
|
||||
import { logout } from '@/lib/api';
|
||||
|
||||
export default function Navbar() {
|
||||
@@ -54,6 +54,17 @@ export default function Navbar() {
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
帳票出力
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/import')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
isActive('/import')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
データ取込
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
|
||||
Reference in New Issue
Block a user