frontend/src/app/import/page.tsx に中山間マスタ取込セクションを追加しました: - State追加: chusankanFile, chusankanResult, chusankanInputRef - アップロード関数: handleChusankanUpload (endpoint: /fields/import/chusankan/) - UI追加: 「中山間マスタ取込」セクション(黄色) ビルド成功。http://localhost:3000/import で確認できます。
407 lines
14 KiB
TypeScript
407 lines
14 KiB
TypeScript
'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 [chusankanFile, setChusankanFile] = useState<File | null>(null);
|
||
const [uploading, setUploading] = useState(false);
|
||
const [kyosaiResult, setKyosaiResult] = useState<ImportResult | null>(null);
|
||
const [yoshidaResult, setYoshidaResult] = useState<ImportResult | null>(null);
|
||
const [chusankanResult, setChusankanResult] = useState<ImportResult | null>(null);
|
||
const kyosaiInputRef = useRef<HTMLInputElement>(null);
|
||
const yoshidaInputRef = useRef<HTMLInputElement>(null);
|
||
const chusankanInputRef = 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);
|
||
}
|
||
};
|
||
|
||
const handleChusankanUpload = async () => {
|
||
if (!chusankanFile) {
|
||
alert('ファイルを選択してください');
|
||
return;
|
||
}
|
||
|
||
setUploading(true);
|
||
setChusankanResult(null);
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('file', chusankanFile);
|
||
|
||
const response = await api.post('/fields/import/chusankan/', formData, {
|
||
headers: {
|
||
'Content-Type': 'multipart/form-data',
|
||
},
|
||
});
|
||
|
||
const data = response.data;
|
||
setChusankanResult({
|
||
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;
|
||
}
|
||
}
|
||
setChusankanResult({
|
||
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">
|
||
中山間指定データをインポートします(ID, 大字, 字, 地番...)
|
||
</p>
|
||
|
||
<div className="flex items-center gap-4 mb-4">
|
||
<input
|
||
ref={chusankanInputRef}
|
||
type="file"
|
||
accept=".ods"
|
||
onChange={(e) => setChusankanFile(e.target.files?.[0] || null)}
|
||
className="hidden"
|
||
/>
|
||
<button
|
||
onClick={() => chusankanInputRef.current?.click()}
|
||
className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
|
||
>
|
||
{chusankanFile ? chusankanFile.name : 'ファイルを選択'}
|
||
</button>
|
||
{chusankanFile && (
|
||
<button
|
||
onClick={() => {
|
||
setChusankanFile(null);
|
||
setChusankanResult(null);
|
||
}}
|
||
className="text-gray-500 hover:text-gray-700"
|
||
>
|
||
クリア
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleChusankanUpload}
|
||
disabled={!chusankanFile || uploading}
|
||
className="w-full flex items-center justify-center px-4 py-2 bg-yellow-600 text-white rounded-md hover:bg-yellow-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>
|
||
|
||
{chusankanResult && (
|
||
<div
|
||
className={`mt-4 p-3 rounded-md flex items-center ${
|
||
chusankanResult.success
|
||
? 'bg-green-50 text-green-700'
|
||
: 'bg-red-50 text-red-700'
|
||
}`}
|
||
>
|
||
{chusankanResult.success ? (
|
||
<CheckCircle className="h-5 w-5 mr-2" />
|
||
) : (
|
||
<XCircle className="h-5 w-5 mr-2" />
|
||
)}
|
||
<div>
|
||
<p className="font-medium">{chusankanResult.message}</p>
|
||
{chusankanResult.success && chusankanResult.created !== undefined && (
|
||
<p className="text-sm">
|
||
作成: {chusankanResult.created}件 / 更新: {chusankanResult.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>
|
||
);
|
||
}
|