From 4afe37968bd06248e3ce0ffbfa3e6badf20cd8f2 Mon Sep 17 00:00:00 2001 From: Akira Date: Thu, 19 Feb 2026 13:07:16 +0900 Subject: [PATCH] =?UTF-8?q?A-1=EF=BC=88=E3=83=80=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E3=83=9C=E3=83=BC=E3=83=89=E7=94=BB=E9=9D=A2=EF=BC=89?= =?UTF-8?q?=E3=81=AE=E5=AE=9F=E8=A3=85=E3=81=8C=E5=AE=8C=E4=BA=86=E3=81=97?= =?UTF-8?q?=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 実装内容: バックエンド: summary APIに total_fields, assigned_fields, unassigned_fields を追加 フロントエンド: /dashboard に新画面を作成 概要サマリー: 全圃場数 / 作付け済み / 未割当(警告アイコン付き) 作物別集計テーブル(筆数・面積・合計行) クイックアクセス: 4つのボタン(作付け計画・圃場管理・帳票出力・データ取込) 年度セレクタで切替可能 Navbar: 「ホーム」ボタン追加、KeinaSystemロゴクリックでダッシュボードへ ルート (/): /allocation → /dashboard にリダイレクト先変更 http://localhost:3000/dashboard で確認できます。 残りタスク: A-7(検索・フィルタ)のみです --- CLAUDE.md | 4 +- backend/apps/plans/views.py | 9 + .../06_ドキュメントvs実装_差異レポート.md | 19 +- frontend/src/app/dashboard/page.tsx | 166 ++++++++++++++++++ frontend/src/app/fields/page.tsx | 2 +- frontend/src/app/page.tsx | 2 +- frontend/src/components/Navbar.tsx | 17 +- frontend/tsconfig.json | 2 +- 8 files changed, 206 insertions(+), 15 deletions(-) create mode 100644 frontend/src/app/dashboard/page.tsx diff --git a/CLAUDE.md b/CLAUDE.md index 379256c..c690d37 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -202,6 +202,7 @@ Variety (品種マスタ) - 圃場一覧・詳細・新規作成 - データ取込画面 - 申請書ダウンロード画面 + - ダッシュボード画面(概要サマリー、作物別集計、クイックアクセス) 6. **対応付け可視化・紐づけ管理** (E-2): - 圃場一覧「対応表」モード(共済漢字地名・中山間所在地の一覧表示、直接紐づけ追加・解除) - 圃場詳細画面の共済/中山間リンク管理(+追加、×解除、面積参考表示) @@ -215,8 +216,7 @@ Variety (品種マスタ) 4. **パフォーマンス**: N+1問題が一部存在(現状は問題ないが、データ増加時に対応必要) ### 🔜 次の実装タスク(優先順) -1. **A-1**: ダッシュボード画面 -2. **A-7**: 検索・フィルタ +1. **A-7**: 検索・フィルタ 詳細は `document/06_ドキュメントvs実装_差異レポート.md` を参照 diff --git a/backend/apps/plans/views.py b/backend/apps/plans/views.py index c1ec375..45419f1 100644 --- a/backend/apps/plans/views.py +++ b/backend/apps/plans/views.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from django.db.models import Sum from .models import Crop, Variety, Plan from .serializers import CropSerializer, VarietySerializer, PlanSerializer +from apps.fields.models import Field class CropViewSet(viewsets.ModelViewSet): @@ -48,8 +49,16 @@ class PlanViewSet(viewsets.ModelViewSet): by_crop[crop_name]['count'] += 1 by_crop[crop_name]['area'] += float(plan.field.area_tan) + total_fields = Field.objects.count() + assigned_field_ids = plans.values_list('field_id', flat=True).distinct() + assigned_count = assigned_field_ids.count() + unassigned_count = total_fields - assigned_count + return Response({ 'year': int(year), + 'total_fields': total_fields, + 'assigned_fields': assigned_count, + 'unassigned_fields': unassigned_count, 'total_plans': plans.count(), 'total_area': float(total_area), 'by_crop': list(by_crop.values()) diff --git a/document/06_ドキュメントvs実装_差異レポート.md b/document/06_ドキュメントvs実装_差異レポート.md index 200264e..c1cab29 100644 --- a/document/06_ドキュメントvs実装_差異レポート.md +++ b/document/06_ドキュメントvs実装_差異レポート.md @@ -8,14 +8,17 @@ ## A. ドキュメントに書かれているが実装されていないもの -### A-1: ダッシュボード画面 +### ~~A-1: ダッシュボード画面~~ ✅ 対応済み -- **ドキュメント**: 画面設計書 画面2 - 概要サマリー(全圃場数/作付け済み/未割当)、クイックアクセスボタン、最近の変更履歴 -- **実装**: `/` はトークンの有無で `/allocation` か `/login` にリダイレクトするだけ -- **影響**: なくても作付け計画画面から全機能にアクセス可能。Navbarで各画面に遷移できる -- **状態**: 🔜 未着手 - -**対応方針**: 将来、機能追加する時には、ここにボタンが増えていく形式になっていくはずなので必要です +- **対応内容**: + - `/dashboard` にダッシュボード画面を新設。`/` はログイン済みなら `/dashboard` にリダイレクト + - 概要サマリー: 全圃場数、作付け済み筆数、未割当筆数(警告アイコン付き) + - 作物別集計テーブル: 作物名、筆数、面積(反)、合計行 + - クイックアクセス: 作付け計画・圃場管理・帳票出力・データ取込への4ボタン + - 年度セレクタで年度切替可能 + - Navbarに「ホーム」ボタン追加、KeinaSystemロゴクリックでダッシュボードに遷移 + - バックエンド: summary APIに `total_fields`, `assigned_fields`, `unassigned_fields` を追加 +- **対応日**: 2026-02-19 --- @@ -224,7 +227,7 @@ | カテゴリ | 項目 | 状態 | |---------|------|------| -| A-1 | ダッシュボード画面 | 🔜 未着手 | +| A-1 | ダッシュボード画面 | ✅ 完了 | | A-2 | チェックボックス一括操作 | ✅ 完了 | | A-3 | 前年度コピーボタン | ✅ 完了 | | A-4 | 品種インライン追加・削除 | ✅ 完了 | diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx new file mode 100644 index 0000000..341b33c --- /dev/null +++ b/frontend/src/app/dashboard/page.tsx @@ -0,0 +1,166 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { api } from '@/lib/api'; +import Navbar from '@/components/Navbar'; +import { Wheat, MapPin, FileText, Upload, Loader2, AlertTriangle } from 'lucide-react'; + +interface SummaryData { + year: number; + total_fields: number; + assigned_fields: number; + unassigned_fields: number; + total_plans: number; + total_area: number; + by_crop: { crop: string; count: number; area: number }[]; +} + +export default function DashboardPage() { + const router = useRouter(); + const currentYear = new Date().getFullYear(); + const [year, setYear] = useState(currentYear); + const [summary, setSummary] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchSummary = async () => { + setLoading(true); + try { + const res = await api.get(`/plans/summary/?year=${year}`); + setSummary(res.data); + } catch (error) { + console.error('Failed to fetch summary:', error); + } finally { + setLoading(false); + } + }; + fetchSummary(); + }, [year]); + + const years = []; + for (let y = currentYear + 1; y >= currentYear - 3; y--) { + years.push(y); + } + + return ( +
+ +
+ {/* ヘッダー */} +
+

ダッシュボード

+ +
+ + {loading ? ( +
+ +
+ ) : summary ? ( +
+ {/* 概要サマリーカード */} +
+
+

全圃場数

+

{summary.total_fields}

+
+
+

作付け済み

+

{summary.assigned_fields}

+
+
+

未割当

+
+

0 ? 'text-amber-500' : 'text-gray-400'}`}> + {summary.unassigned_fields} +

+ {summary.unassigned_fields > 0 && ( + + )} +
+
+
+ + {/* 作物別集計 */} + {summary.by_crop.length > 0 && ( +
+

作物別集計

+ + + + + + + + + + {summary.by_crop.map((item) => ( + + + + + + ))} + + + + + + +
作物筆数面積(反)
{item.crop}{item.count}{item.area.toFixed(1)}
合計{summary.total_plans}{summary.total_area.toFixed(1)}
+
+ )} + + {/* クイックアクセス */} +
+

クイックアクセス

+
+ + + + +
+
+
+ ) : ( +
+ データの取得に失敗しました +
+ )} +
+
+ ); +} diff --git a/frontend/src/app/fields/page.tsx b/frontend/src/app/fields/page.tsx index e981901..ddf6dca 100644 --- a/frontend/src/app/fields/page.tsx +++ b/frontend/src/app/fields/page.tsx @@ -34,7 +34,7 @@ export default function FieldsPage() { try { const response = await api.get(`/fields/?ordering=${sortOrder}`); setFields(response.data); - const groups = [...new Set(response.data.map((f: Field) => f.group_name).filter(Boolean))] as string[]; + const groups = Array.from(new Set(response.data.map((f: Field) => f.group_name).filter(Boolean))); setUniqueGroups(groups.sort()); } catch (error) { console.error('Failed to fetch fields:', error); diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index dbfa9e1..f4072c0 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -9,7 +9,7 @@ export default function Home() { useEffect(() => { const token = localStorage.getItem('accessToken'); if (token) { - router.push('/allocation'); + router.push('/dashboard'); } else { router.push('/login'); } diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index d6da2d2..cd149bc 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,7 +1,7 @@ 'use client'; import { useRouter, usePathname } from 'next/navigation'; -import { LogOut, Wheat, MapPin, FileText, Upload } from 'lucide-react'; +import { LogOut, Wheat, MapPin, FileText, Upload, LayoutDashboard } from 'lucide-react'; import { logout } from '@/lib/api'; export default function Navbar() { @@ -19,8 +19,21 @@ export default function Navbar() {
-

KeinaSystem

+
+