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';
|
'use client';
|
||||||
|
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
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';
|
import { logout } from '@/lib/api';
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
@@ -54,6 +54,17 @@ export default function Navbar() {
|
|||||||
<FileText className="h-4 w-4 mr-2" />
|
<FileText className="h-4 w-4 mr-2" />
|
||||||
帳票出力
|
帳票出力
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|||||||
Reference in New Issue
Block a user