From 923dd5dece8411872f2e9f8143827e0bebdec45c Mon Sep 17 00:00:00 2001 From: Akira Date: Sun, 15 Feb 2026 13:34:32 +0900 Subject: [PATCH] =?UTF-8?q?Day=209=20=E5=AE=8C=E4=BA=86=20=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=E5=86=85=E5=AE=B9:=201.=20backend/apps/fields/views.p?= =?UTF-8?q?y=20-=20FieldViewSet=E3=82=92ModelViewSet=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4=EF=BC=88=E6=9B=B8=E3=81=8D=E8=BE=BC=E3=81=BF=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=EF=BC=89=202.=20frontend/src/components/Navbar.tsx=20?= =?UTF-8?q?-=20=E5=9C=83=E5=A0=B4=E7=AE=A1=E7=90=86=E3=83=AA=E3=83=B3?= =?UTF-8?q?=E3=82=AF=E8=BF=BD=E5=8A=A0=203.=20frontend/src/app/fields/page?= =?UTF-8?q?.tsx=20-=20=E5=9C=83=E5=A0=B4=E4=B8=80=E8=A6=A7=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=204.=20frontend/src/app/fields/new/page.tsx=20-=20?= =?UTF-8?q?=E6=96=B0=E8=A6=8F=E4=BD=9C=E6=88=90=E7=94=BB=E9=9D=A2=205.=20f?= =?UTF-8?q?rontend/src/app/fields/[id]/page.tsx=20-=20=E7=B7=A8=E9=9B=86?= =?UTF-8?q?=E7=94=BB=E9=9D=A2=20API=20CRUD=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E7=B5=90=E6=9E=9C:=20-=20POST=20/api/fields/=20=E2=86=92=20201?= =?UTF-8?q?=20Created=20-=20GET=20/api/fields/=20=E2=86=92=20200=20OK=20-?= =?UTF-8?q?=20PATCH=20/api/fields/{id}/=20=E2=86=92=20200=20OK=20-=20DELET?= =?UTF-8?q?E=20/api/fields/{id}/=20=E2=86=92=20204=20No=20Content=20?= =?UTF-8?q?=E3=83=96=E3=83=A9=E3=82=A6=E3=82=B6=E3=81=A7=20http://localhos?= =?UTF-8?q?t:3000/fields=20=E3=81=8B=E3=82=89=E5=9C=83=E5=A0=B4=E3=81=AECR?= =?UTF-8?q?UD=E6=93=8D=E4=BD=9C=E3=81=8C=E5=8F=AF=E8=83=BD=E3=81=A7?= =?UTF-8?q?=E3=81=99=E3=80=82=20=E6=AC=A1=E3=81=AE=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=E3=81=AB=E7=A7=BB=E3=82=8A=E3=81=BE=E3=81=99=E3=81=8B=EF=BC=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/apps/fields/views.py | 2 +- frontend/src/app/fields/[id]/page.tsx | 240 ++++++++++++++++++++++++++ frontend/src/app/fields/new/page.tsx | 178 +++++++++++++++++++ frontend/src/app/fields/page.tsx | 156 +++++++++++++++++ frontend/src/components/Navbar.tsx | 34 +++- 5 files changed, 605 insertions(+), 5 deletions(-) create mode 100644 frontend/src/app/fields/[id]/page.tsx create mode 100644 frontend/src/app/fields/new/page.tsx create mode 100644 frontend/src/app/fields/page.tsx diff --git a/backend/apps/fields/views.py b/backend/apps/fields/views.py index 1ffef4b..ba17b6b 100644 --- a/backend/apps/fields/views.py +++ b/backend/apps/fields/views.py @@ -8,7 +8,7 @@ from .models import OfficialKyosaiField, OfficialChusankanField, Field from .serializers import FieldSerializer -class FieldViewSet(viewsets.ReadOnlyModelViewSet): +class FieldViewSet(viewsets.ModelViewSet): queryset = Field.objects.all() serializer_class = FieldSerializer permission_classes = [permissions.AllowAny] diff --git a/frontend/src/app/fields/[id]/page.tsx b/frontend/src/app/fields/[id]/page.tsx new file mode 100644 index 0000000..3d0f93c --- /dev/null +++ b/frontend/src/app/fields/[id]/page.tsx @@ -0,0 +1,240 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter, useParams } from 'next/navigation'; +import { api } from '@/lib/api'; +import { Field } from '@/types'; +import Navbar from '@/components/Navbar'; +import { ArrowLeft, Save } from 'lucide-react'; + +export default function EditFieldPage() { + const router = useRouter(); + const params = useParams(); + const fieldId = parseInt(params.id as string); + + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(''); + const [notFound, setNotFound] = useState(false); + + const [formData, setFormData] = useState({ + name: '', + address: '', + area_tan: '', + area_m2: '', + owner_name: '', + }); + + useEffect(() => { + fetchField(); + }, [fieldId]); + + const fetchField = async () => { + try { + const response = await api.get(`/fields/${fieldId}/`); + const field: Field = response.data; + setFormData({ + name: field.name || '', + address: field.address || '', + area_tan: field.area_tan?.toString() || '', + area_m2: field.area_m2?.toString() || '', + owner_name: field.owner_name || '', + }); + } catch (err: unknown) { + console.error('Failed to fetch field:', err); + const axiosError = err as { response?: { status?: number } }; + if (axiosError.response?.status === 404) { + setNotFound(true); + } + } finally { + setLoading(false); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setSaving(true); + + try { + const data = { + name: formData.name, + address: formData.address || null, + area_tan: formData.area_tan ? parseFloat(formData.area_tan) : null, + area_m2: formData.area_m2 ? parseInt(formData.area_m2) : null, + owner_name: formData.owner_name || null, + }; + + await api.patch(`/fields/${fieldId}/`, data); + router.push('/fields'); + } catch (err: unknown) { + console.error('Failed to update field:', err); + setError('保存に失敗しました'); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+ +
+
読み込み中...
+
+
+ ); + } + + if (notFound) { + return ( +
+ +
+
+

圃場が見つかりません。

+ +
+
+
+ ); + } + + return ( +
+ +
+
+ +
+ +
+

圃場編集

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+
+
+
+
+ ); +} diff --git a/frontend/src/app/fields/new/page.tsx b/frontend/src/app/fields/new/page.tsx new file mode 100644 index 0000000..fbc571a --- /dev/null +++ b/frontend/src/app/fields/new/page.tsx @@ -0,0 +1,178 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { api } from '@/lib/api'; +import Navbar from '@/components/Navbar'; +import { ArrowLeft, Save } from 'lucide-react'; + +export default function NewFieldPage() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const [formData, setFormData] = useState({ + name: '', + address: '', + area_tan: '', + area_m2: '', + owner_name: '', + }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + + try { + const data = { + name: formData.name, + address: formData.address || null, + area_tan: formData.area_tan ? parseFloat(formData.area_tan) : null, + area_m2: formData.area_m2 ? parseInt(formData.area_m2) : null, + owner_name: formData.owner_name || null, + }; + + await api.post('/fields/', data); + router.push('/fields'); + } catch (err: unknown) { + console.error('Failed to create field:', err); + setError('保存に失敗しました'); + } finally { + setLoading(false); + } + }; + + return ( +
+ +
+
+ +
+ +
+

新規圃場作成

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+
+
+
+
+ ); +} diff --git a/frontend/src/app/fields/page.tsx b/frontend/src/app/fields/page.tsx new file mode 100644 index 0000000..9ddae30 --- /dev/null +++ b/frontend/src/app/fields/page.tsx @@ -0,0 +1,156 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { api } from '@/lib/api'; +import { Field } from '@/types'; +import Navbar from '@/components/Navbar'; +import { Plus, Pencil, Trash2 } from 'lucide-react'; + +export default function FieldsPage() { + const router = useRouter(); + const [fields, setFields] = useState([]); + const [loading, setLoading] = useState(true); + const [deleting, setDeleting] = useState(null); + + useEffect(() => { + fetchFields(); + }, []); + + const fetchFields = async () => { + setLoading(true); + try { + const response = await api.get('/fields/'); + setFields(response.data); + } catch (error) { + console.error('Failed to fetch fields:', error); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (id: number) => { + if (!confirm('この圃場を削除してもよろしいですか?')) { + return; + } + + setDeleting(id); + try { + await api.delete(`/fields/${id}/`); + await fetchFields(); + } catch (error) { + console.error('Failed to delete field:', error); + alert('削除に失敗しました'); + } finally { + setDeleting(null); + } + }; + + if (loading) { + return ( +
+ +
+
読み込み中...
+
+
+ ); + } + + return ( +
+ +
+
+

圃場管理

+ +
+ + {fields.length === 0 ? ( +
+

圃場データがありません。「新規作成」ボタンから追加してください。

+
+ ) : ( +
+
+ + + + + + + + + + + + + {fields.map((field) => ( + + + + + + + + + ))} + +
+ 圃場名 + + 住所 + + 面積(反) + + 面積(m2) + + 所有者 + + 操作 +
+
+ {field.name} +
+
+
+ {field.address || '-'} +
+
+ {field.area_tan || '-'} + + {field.area_m2 || '-'} + + {field.owner_name || '-'} + +
+ + +
+
+
+
+ )} +
+
+ ); +} diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 3c84490..61ab29e 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,23 +1,49 @@ 'use client'; -import { useRouter } from 'next/navigation'; -import { LogOut } from 'lucide-react'; +import { useRouter, usePathname } from 'next/navigation'; +import { LogOut, Wheat, MapPin } from 'lucide-react'; import { logout } from '@/lib/api'; export default function Navbar() { const router = useRouter(); + const pathname = usePathname(); const handleLogout = () => { logout(); }; + const isActive = (path: string) => pathname === path; + return (