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 (