Files
keinasystem/frontend/src/app/import/page.tsx
Akira c78945b44b 修正完了
frontend/src/app/import/page.tsx に中山間マスタ取込セクションを追加しました:
- State追加: chusankanFile, chusankanResult, chusankanInputRef
- アップロード関数: handleChusankanUpload (endpoint: /fields/import/chusankan/)
- UI追加: 「中山間マスタ取込」セクション(黄色)
ビルド成功。http://localhost:3000/import で確認できます。
2026-02-15 14:34:02 +09:00

407 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}