Compare commits
22 Commits
4a1db5ef27
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f04e1884d9 | ||
|
|
cc6823b071 | ||
|
|
b7b5ce3943 | ||
|
|
8de1ae70aa | ||
|
|
3901caf668 | ||
|
|
5f58c2c686 | ||
|
|
83525c2f59 | ||
|
|
627d7e4f59 | ||
|
|
9059b2b51e | ||
|
|
7d2eb1ebe2 | ||
|
|
3e2942b479 | ||
|
|
70fe3824b3 | ||
|
|
10f2b6f77f | ||
|
|
6dfcd0be06 | ||
|
|
1371eef648 | ||
|
|
ac0bc7b6a9 | ||
|
|
1c474e9692 | ||
|
|
0cd90e61db | ||
|
|
8de27de335 | ||
|
|
71b8258281 | ||
|
|
4516a74772 | ||
|
|
a42ccb5cda |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,3 +15,5 @@ postgres_data/
|
||||
nul
|
||||
|
||||
*.tsbuildinfo
|
||||
.mcp.json
|
||||
.codex
|
||||
|
||||
22
.vscode/mcp.json
vendored
Normal file
22
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"butler": {
|
||||
"command": "uv",
|
||||
"args": ["run", "python", "-m", "butler.mcp_facade"],
|
||||
"cwd": "../butler2"
|
||||
},
|
||||
"serena": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"--from",
|
||||
"git+https://github.com/oraios/serena",
|
||||
"serena",
|
||||
"start-mcp-server",
|
||||
"--context",
|
||||
"ide-assistant",
|
||||
"--project",
|
||||
"."
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,8 @@ ssh keinafarm-claude 'cd /home/keinasystem/keinasystem_t02 && \
|
||||
| 施肥計画 | `document/13_マスタードキュメント_施肥計画編.md` |
|
||||
| 運搬計画 | `document/14_マスタードキュメント_分配計画編.md` |
|
||||
| 田植え計画 | `document/16_マスタードキュメント_田植え計画編.md` |
|
||||
| 農薬散布管理 | `document/18_マスタードキュメント_農薬散布管理編.md` |
|
||||
| TODO管理 | `document/19_マスタードキュメント_TODO管理編.md` |
|
||||
| データモデル全体 | `document/03_データ仕様書.md` |
|
||||
|
||||
---
|
||||
|
||||
37
deploy_local.sh
Executable file
37
deploy_local.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# ローカル本番同等環境の起動スクリプト
|
||||
# 使用: bash deploy_local.sh
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "=== KeinaSystem ローカル本番環境 ==="
|
||||
|
||||
# .env ファイル確認
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "エラー: .env ファイルがありません"
|
||||
echo " .env.production.example を .env にコピーして値を設定してください"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[1/4] 停止..."
|
||||
docker compose -f docker-compose.local.yml down
|
||||
|
||||
echo "[2/4] ビルド..."
|
||||
docker compose -f docker-compose.local.yml build
|
||||
|
||||
echo "[3/4] 起動..."
|
||||
docker compose -f docker-compose.local.yml up -d
|
||||
|
||||
echo "[4/4] マイグレーション..."
|
||||
sleep 5
|
||||
docker compose -f docker-compose.local.yml exec backend python manage.py migrate
|
||||
|
||||
echo ""
|
||||
echo "=== 起動完了 ==="
|
||||
docker compose -f docker-compose.local.yml ps
|
||||
echo ""
|
||||
echo " フロントエンド: http://localhost:3000"
|
||||
echo " バックエンドAPI: http://localhost:8000/api/"
|
||||
echo ""
|
||||
echo "DBをサーバーと同期する場合: bash sync_db.sh"
|
||||
59
docker-compose.local.yml
Normal file
59
docker-compose.local.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
# ローカルでの本番同等テスト用
|
||||
# Traefikなし、ポート直接公開、本番用Dockerfileを使用
|
||||
# 使用: docker compose -f docker-compose.local.yml up -d
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgis/postgis:16-3.4
|
||||
container_name: keinasystem_db
|
||||
environment:
|
||||
POSTGRES_DB: keinasystem
|
||||
POSTGRES_USER: keinasystem
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data_local:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U keinasystem -d keinasystem"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.prod
|
||||
container_name: keinasystem_backend
|
||||
environment:
|
||||
DB_NAME: keinasystem
|
||||
DB_USER: keinasystem
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
DB_HOST: db
|
||||
DB_PORT: 5432
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
DEBUG: "False"
|
||||
ALLOWED_HOSTS: localhost,127.0.0.1
|
||||
CORS_ALLOWED_ORIGINS: http://localhost:3000
|
||||
MAIL_API_KEY: ${MAIL_API_KEY}
|
||||
FRONTEND_URL: http://localhost:3000
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.prod
|
||||
args:
|
||||
NEXT_PUBLIC_API_URL: http://localhost:8000
|
||||
container_name: keinasystem_frontend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
volumes:
|
||||
postgres_data_local:
|
||||
411
document/15_マスタードキュメント_トラクター作業編.md
Normal file
411
document/15_マスタードキュメント_トラクター作業編.md
Normal file
@@ -0,0 +1,411 @@
|
||||
# マスタードキュメント:トラクター作業記録機能
|
||||
|
||||
> **作成**: 2026-04-04
|
||||
> **最終更新**: 2026-04-10
|
||||
> **対象機能**: トラクター作業記録(畔塗・荒代掻き・植代掻き・耕耘)
|
||||
> **実装状況**: 畔塗のみ実装済み。荒代掻き・植代掻き・耕耘は設計中(Issue #21)
|
||||
> **対象 Issue**: `akira/keinasystem#21`
|
||||
|
||||
---
|
||||
|
||||
## 概要
|
||||
|
||||
農業生産者が、水稲作付け圃場に対して実施したトラクター作業を日付単位で記録する機能。
|
||||
対象圃場をまとめて選択し、保存時に作業記録一覧へ自動反映する。
|
||||
|
||||
対象作業種別:
|
||||
|
||||
| 種別 | 日本語名 | 説明 |
|
||||
|---|---|---|
|
||||
| `levee_work` | 畔塗 | 畦畔の補修・造成 |
|
||||
| `rough_harrowing` | 荒代掻き | 田植え前の粗い代掻き |
|
||||
| `transplant_harrowing` | 植代掻き | 田植え直前の仕上げ代掻き |
|
||||
| `cultivation` | 耕耘 | 土起こし・耕起 |
|
||||
|
||||
これらはいずれも**トラクターを用いた資材なし作業**であり、同一のデータモデルで管理する。
|
||||
|
||||
本機能は、施肥計画の散布実績と同様に
|
||||
「作業本体を専用テーブルで持ち、作業記録一覧には索引を自動生成する」
|
||||
という設計方針を採用する。
|
||||
|
||||
### 機能スコープ(IN / OUT)
|
||||
|
||||
| IN(本機能で扱う) | OUT(本機能では扱わない) |
|
||||
|---|---|
|
||||
| 日付単位の作業記録作成(全4種別) | 作業の工程管理 |
|
||||
| 水稲作付け圃場の候補抽出 | 作業者別の工数集計 |
|
||||
| 複数圃場の一括選択・保存 | 機械・資材の在庫管理 |
|
||||
| 作業記録一覧(WorkRecord)への自動反映 | 写真添付 |
|
||||
| 記録の編集・削除 | GPS軌跡連携 |
|
||||
| 対象圃場一覧の参照画面 | 汎用作業日誌への完全統合 |
|
||||
|
||||
---
|
||||
|
||||
## 背景と目的
|
||||
|
||||
現状システムには畔塗の記録機能があるが、同じトラクター作業である荒代掻き・植代掻き・耕耘は登録できない。
|
||||
これらは以下の共通点を持つため、統一モデルで管理する。
|
||||
|
||||
- 1日で複数圃場をまとめて実施することが多い
|
||||
- 対象圃場は当年の作付け計画と密接に関係する
|
||||
- 後から「いつ、どの圃場を実施したか」を一覧で見返したい
|
||||
- 使用資材がない(施肥・農薬とは区別される)
|
||||
|
||||
---
|
||||
|
||||
## データモデル
|
||||
|
||||
### TractorWorkSession(トラクター作業記録本体)
|
||||
|
||||
日付単位のトラクター作業記録。
|
||||
|
||||
| フィールド | 型 | 制約 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | int | PK | |
|
||||
| work_type | varchar(30) | required | 作業種別(下記参照) |
|
||||
| year | int | required | 年度フィルタ用。原則 `date.year` と一致させる |
|
||||
| date | DateField | required | 作業日 |
|
||||
| title | varchar(100) | required | 一覧表示タイトル。未指定時はサーバー側で work_type に応じたデフォルト値を補完する |
|
||||
| notes | text | blank | 備考 |
|
||||
| created_at | datetime | auto | |
|
||||
| updated_at | datetime | auto | |
|
||||
|
||||
#### work_type の値とデフォルトタイトル
|
||||
|
||||
| work_type | 表示名 | デフォルトタイトル |
|
||||
|---|---|---|
|
||||
| `levee_work` | 畔塗 | 水稲畔塗 |
|
||||
| `rough_harrowing` | 荒代掻き | 水稲荒代掻き |
|
||||
| `transplant_harrowing` | 植代掻き | 水稲植代掻き |
|
||||
| `cultivation` | 耕耘 | 水稲耕耘 |
|
||||
|
||||
- `year + date` の一意制約は付けない
|
||||
- 同日に種別違い・地区違いで複数記録を持てるようにする
|
||||
|
||||
### TractorWorkSessionItem(対象圃場明細)
|
||||
|
||||
トラクター作業記録に紐づく対象圃場一覧。
|
||||
|
||||
| フィールド | 型 | 制約 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | int | PK | |
|
||||
| session | FK(TractorWorkSession) | CASCADE | 親の作業記録 |
|
||||
| field | FK(fields.Field) | PROTECT | 対象圃場 |
|
||||
| plan | FK(plans.Plan) | SET_NULL, nullable | 保存時点の作付け計画参照 |
|
||||
| crop_name_snapshot | varchar(100) | required | 保存時点の作物名 |
|
||||
| variety_name_snapshot | varchar(100) | blank | 保存時点の品種名 |
|
||||
| created_at | datetime | auto | |
|
||||
| updated_at | datetime | auto | |
|
||||
|
||||
- `unique_together = ['session', 'field']`
|
||||
- 圃場名は `Field` を参照して表示する
|
||||
- 作物・品種は履歴保全のためスナップショット保持
|
||||
|
||||
### WorkRecord(作業記録索引)
|
||||
|
||||
既存 `apps/workrecords` の `WorkRecord` をトラクター作業に対応させる。
|
||||
|
||||
| 変更点 | 内容 |
|
||||
|---|---|
|
||||
| `work_type` enum | `TRACTOR_WORK = 'tractor_work'` を追加(`LEVEE_WORK` を置換) |
|
||||
| FK | `tractor_work_session = OneToOneField('tractor_work.TractorWorkSession', ...)` に改名 |
|
||||
|
||||
制約:
|
||||
|
||||
- `on_delete=CASCADE`
|
||||
- `null=True`, `blank=True`
|
||||
- `related_name='work_record'`
|
||||
|
||||
一覧表示時の想定値:
|
||||
|
||||
| 項目 | 値 |
|
||||
|---|---|
|
||||
| 作業日 | 作業記録の日付 |
|
||||
| 種別 | トラクター作業(work_type の日本語表示) |
|
||||
| タイトル | session.title |
|
||||
| 参照先 | 対象圃場一覧画面 |
|
||||
|
||||
---
|
||||
|
||||
## 候補圃場抽出ルール
|
||||
|
||||
候補は作付け計画 `Plan` から抽出する。
|
||||
|
||||
### 基本条件
|
||||
|
||||
- 指定年度の `Plan` であること
|
||||
- `crop.name = "水稲"` の圃場であること
|
||||
|
||||
### 補足
|
||||
|
||||
- 品種未設定でも `crop=水稲` なら候補に含める
|
||||
- 並び順は `field.display_order`, `field.id`
|
||||
|
||||
### 候補レスポンスで返す情報
|
||||
|
||||
| 項目 | 説明 |
|
||||
|---|---|
|
||||
| field_id | 圃場ID |
|
||||
| field_name | 圃場名 |
|
||||
| field_area_tan | 面積(反) |
|
||||
| group_name | グループ名 |
|
||||
| plan_id | 対応する作付け計画ID |
|
||||
| crop_name | 作物名 |
|
||||
| variety_name | 品種名 |
|
||||
| selected | 初期選択状態(原則 `true`) |
|
||||
|
||||
---
|
||||
|
||||
## 画面仕様
|
||||
|
||||
### 画面の位置づけ
|
||||
|
||||
日付と作業種別を先に決めて対象圃場を選ぶ「日報型UI」。
|
||||
1回の保存で複数圃場をまとめて記録する。
|
||||
|
||||
### 主要画面
|
||||
|
||||
#### 1. トラクター作業記録一覧画面(`/tractor-work`)
|
||||
|
||||
- 年度内の記録を一覧する
|
||||
- 作業種別でフィルター可能
|
||||
- 新規作成・既存記録の編集・削除
|
||||
|
||||
表示項目: 作業日 / 作業種別 / タイトル / 対象圃場数 / 面積合計 / 備考
|
||||
|
||||
#### 2. 作成・編集画面
|
||||
|
||||
入力項目:
|
||||
|
||||
- **作業種別**(畔塗 / 荒代掻き / 植代掻き / 耕耘)← 新規追加
|
||||
- 日付
|
||||
- タイトル(work_type に連動したデフォルト値を自動セット)
|
||||
- 備考
|
||||
- 対象圃場一覧(チェックボックス)
|
||||
|
||||
### 推奨UIイメージ
|
||||
|
||||
```text
|
||||
トラクター作業記録作成
|
||||
|
||||
[作業種別 荒代掻き ▼]
|
||||
[日付 2026-04-20]
|
||||
[タイトル 水稲荒代掻き]
|
||||
[備考 __________________ ]
|
||||
|
||||
対象圃場一覧
|
||||
[全選択] [全解除]
|
||||
|
||||
☑ 田中上 1.2反 上エリア コシヒカリ
|
||||
☑ 田中下 0.8反 上エリア あきたこまち
|
||||
☐ 山の前 1.5反 南エリア (未設定)
|
||||
|
||||
[保存]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API エンドポイント
|
||||
|
||||
すべて JWT 認証必須。
|
||||
|
||||
### トラクター作業記録
|
||||
|
||||
| メソッド | URL | 説明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/tractor-work/sessions/?year={year}` | 年度別一覧 |
|
||||
| POST | `/api/tractor-work/sessions/` | 新規作成 |
|
||||
| GET | `/api/tractor-work/sessions/{id}/` | 詳細取得 |
|
||||
| PUT/PATCH | `/api/tractor-work/sessions/{id}/` | 更新 |
|
||||
| DELETE | `/api/tractor-work/sessions/{id}/` | 削除 |
|
||||
|
||||
### 候補圃場取得
|
||||
|
||||
| メソッド | URL | 説明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/tractor-work/candidates/?year={year}` | 水稲作付け圃場候補を返す |
|
||||
|
||||
### リクエスト例(新規作成)
|
||||
|
||||
```json
|
||||
{
|
||||
"work_type": "rough_harrowing",
|
||||
"year": 2026,
|
||||
"date": "2026-04-20",
|
||||
"title": "水稲荒代掻き",
|
||||
"notes": "",
|
||||
"items": [
|
||||
{ "field": 5, "plan": 12 },
|
||||
{ "field": 6, "plan": 13 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `crop_name_snapshot` / `variety_name_snapshot` はクライアント送信不要。サーバーが `plan` から自動設定する
|
||||
- `plan` が `null` の場合は `field` に対応する当年 `Plan` から補完を試みる
|
||||
|
||||
### レスポンス例(詳細)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 3,
|
||||
"work_type": "rough_harrowing",
|
||||
"year": 2026,
|
||||
"date": "2026-04-20",
|
||||
"title": "水稲荒代掻き",
|
||||
"notes": "",
|
||||
"work_record_id": 15,
|
||||
"item_count": 2,
|
||||
"total_area_tan": "2.0000",
|
||||
"items": [
|
||||
{
|
||||
"id": 11,
|
||||
"field": 5,
|
||||
"field_name": "田中上",
|
||||
"plan": 12,
|
||||
"crop_name_snapshot": "水稲",
|
||||
"variety_name_snapshot": "コシヒカリ"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-04-20T08:00:00Z",
|
||||
"updated_at": "2026-04-20T08:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 業務フロー
|
||||
|
||||
### 1. 新規作成
|
||||
|
||||
1. ユーザーが作業種別・年度・日付を選ぶ
|
||||
2. システムが当年の水稲作付け圃場を候補表示する
|
||||
3. ユーザーが対象圃場を選択する
|
||||
4. 保存時に `TractorWorkSession` を作成する
|
||||
5. 明細として `TractorWorkSessionItem` を一括作成する
|
||||
6. 各明細の `crop_name_snapshot` / `variety_name_snapshot` をサーバー側で自動設定する
|
||||
7. `WorkRecord` を自動生成する(`update_or_create`)
|
||||
|
||||
### 2. 編集
|
||||
|
||||
1. ユーザーが既存の作業記録を開く
|
||||
2. 作業種別・日付・タイトル・備考・対象圃場を変更する
|
||||
3. 保存時に明細を再構成する
|
||||
4. `WorkRecord` 側の作業日・タイトルも同期更新する
|
||||
|
||||
### 3. 削除
|
||||
|
||||
1. ユーザーが作業記録を削除する
|
||||
2. 紐づく `TractorWorkSessionItem` は `CASCADE` で削除される
|
||||
3. 紐づく `WorkRecord` は `tractor_work_session` の `on_delete=CASCADE` により削除される
|
||||
|
||||
---
|
||||
|
||||
## 作業記録連携仕様
|
||||
|
||||
### 追加する種別
|
||||
|
||||
| enum値 | 表示名 |
|
||||
|---|---|
|
||||
| `tractor_work` | トラクター作業 |
|
||||
|
||||
### 自動生成ルール
|
||||
|
||||
- `work_date` = `session.date`
|
||||
- `work_type` = `tractor_work`
|
||||
- `title` = `session.title`(work_type 別デフォルトで補完済み)
|
||||
- `year` = `session.year`
|
||||
- `auto_created` = `True`
|
||||
- `tractor_work_session` = 対応する作業記録
|
||||
|
||||
### 同期タイミング
|
||||
|
||||
- 作成時・更新時: `update_or_create`
|
||||
- 削除時: `on_delete=CASCADE` により自動削除
|
||||
|
||||
---
|
||||
|
||||
## バリデーションルール
|
||||
|
||||
### 必須
|
||||
|
||||
- `work_type`
|
||||
- `year`
|
||||
- `date`
|
||||
- `items`(1件以上)
|
||||
|
||||
### 保存時チェック
|
||||
|
||||
- 選択圃場が0件の保存を禁止する
|
||||
- 同一セッション内で同じ圃場を重複登録しない
|
||||
- `year` は原則 `date.year` と一致しなければならない
|
||||
- `plan` が指定されている場合、`plan.field` と `field` は一致しなければならない
|
||||
- `plan.year` は `session.year` と一致しなければならない
|
||||
|
||||
### 業務上の許容
|
||||
|
||||
- 品種未設定の水稲圃場は保存可
|
||||
- 同日に別種別・別地区で複数記録を持てる
|
||||
- 一度作業した圃場を別日に再度記録することは可
|
||||
|
||||
---
|
||||
|
||||
## 実装方針
|
||||
|
||||
### 移行方針(levee_work → tractor_work)
|
||||
|
||||
既存 `apps/levee_work` を `apps/tractor_work` にアプリごと改名する。
|
||||
|
||||
- Django の `RenameModel` migration でテーブルを改名する
|
||||
- `work_type` フィールドを追加し、既存レコードは `levee_work` で埋める
|
||||
- `workrecords` の FK名・enum値も migration で更新する
|
||||
- API パスを `/levee-work/` → `/tractor-work/` に変更する
|
||||
- フロントエンドの `app/levee-work/` → `app/tractor-work/` に移動する
|
||||
|
||||
### バックエンド
|
||||
|
||||
- `Session` / `SessionItem` 構成を維持する
|
||||
- Serializer は `read` と `write` を分離する
|
||||
- 候補取得 API は `Plan` を起点に組み立てる
|
||||
- `sync_tractor_work_record(session)` で `WorkRecord` と同期する
|
||||
|
||||
### フロントエンド
|
||||
|
||||
- 既存の levee-work ページを tractor-work に移植する
|
||||
- 作業種別セレクタを追加し、選択に応じてデフォルトタイトルを自動セットする
|
||||
|
||||
---
|
||||
|
||||
## ソースファイル構成
|
||||
|
||||
### バックエンド
|
||||
|
||||
```
|
||||
backend/apps/tractor_work/
|
||||
├── models.py # TractorWorkSession, TractorWorkSessionItem
|
||||
├── serializers.py
|
||||
├── views.py
|
||||
├── urls.py
|
||||
├── admin.py
|
||||
└── migrations/
|
||||
├── 0001_initial.py # (levee_work から移行)
|
||||
└── 0002_rename_and_add_work_type.py
|
||||
```
|
||||
|
||||
変更ファイル:
|
||||
- `backend/apps/workrecords/models.py` — FK名・enum更新
|
||||
- `backend/apps/workrecords/services.py` — sync関数改名
|
||||
- `backend/keinasystem/settings.py` — INSTALLED_APPS更新
|
||||
- `backend/keinasystem/urls.py` — URLパス更新
|
||||
|
||||
### フロントエンド
|
||||
|
||||
```
|
||||
frontend/src/app/tractor-work/
|
||||
└── page.tsx
|
||||
```
|
||||
|
||||
変更ファイル:
|
||||
- `frontend/src/types/index.ts` — 型定義更新
|
||||
- `frontend/src/components/Navbar.tsx` — リンク更新
|
||||
- `frontend/src/app/workrecords/page.tsx` — 遷移先更新
|
||||
@@ -1,557 +0,0 @@
|
||||
# マスタードキュメント:畔塗作業機能
|
||||
|
||||
> **作成**: 2026-04-04
|
||||
> **最終更新**: 2026-04-04
|
||||
> **対象機能**: 畔塗作業記録(日付単位の圃場選択・作業記録索引連携)
|
||||
> **実装状況**: 実装予定(仕様策定版)
|
||||
|
||||
---
|
||||
|
||||
## 概要
|
||||
|
||||
農業生産者が、水稲作付け圃場に対して実施した「畔塗」作業を日付単位で記録する機能。
|
||||
対象圃場をまとめて選択し、保存時に作業記録一覧へ自動反映する。
|
||||
|
||||
本機能は、施肥計画の散布実績と同様に
|
||||
「作業本体を専用テーブルで持ち、作業記録一覧には索引を自動生成する」
|
||||
という設計方針を採用する。
|
||||
|
||||
### 機能スコープ(IN / OUT)
|
||||
|
||||
| IN(本機能で扱う) | OUT(本機能では扱わない) |
|
||||
|---|---|
|
||||
| 畔塗日単位の記録作成 | 畔塗作業の工程管理 |
|
||||
| 水稲作付け圃場の候補抽出 | 作業者別の工数集計 |
|
||||
| 複数圃場の一括選択・保存 | 機械・資材の在庫管理 |
|
||||
| 作業記録一覧(WorkRecord)への自動反映 | 写真添付 |
|
||||
| 畔塗記録の編集・削除 | GPS軌跡連携 |
|
||||
| 対象圃場一覧の参照画面 | 汎用作業日誌への完全統合 |
|
||||
|
||||
---
|
||||
|
||||
## 背景と目的
|
||||
|
||||
現状システムには、運搬や肥料散布のような作業実績を日付順に参照する仕組みがあるが、
|
||||
春作業の一つである畔塗については記録先が存在しない。
|
||||
|
||||
畔塗は次の特徴を持つ。
|
||||
|
||||
- 1日で複数圃場をまとめて実施することが多い
|
||||
- 対象圃場は当年の作付け計画と密接に関係する
|
||||
- 後から「いつ、どの圃場を畔塗したか」を一覧で見返したい
|
||||
|
||||
そのため、圃場ごとに単発レコードを大量に作るのではなく、
|
||||
`1日 = 1件の畔塗記録` とし、対象圃場を明細としてぶら下げる構成とする。
|
||||
|
||||
---
|
||||
|
||||
## データモデル
|
||||
|
||||
### LeveeWorkSession(畔塗記録本体)
|
||||
|
||||
日付単位の畔塗作業記録。
|
||||
|
||||
| フィールド | 型 | 制約 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | int | PK | |
|
||||
| year | int | required | 年度フィルタ用。既存機能に合わせて暦年を保持し、原則 `date.year` と一致させる |
|
||||
| date | DateField | required | 畔塗日 |
|
||||
| title | varchar(100) | required, default=`水稲畔塗` | 一覧表示タイトル。未指定時はサーバー側で `水稲畔塗` を補完する |
|
||||
| notes | text | blank | 備考 |
|
||||
| created_at | datetime | auto | |
|
||||
| updated_at | datetime | auto | |
|
||||
|
||||
- `year + date` の一意制約は付けない
|
||||
- 同日に午前・午後や地区別で複数記録を持てるようにする
|
||||
|
||||
### LeveeWorkSessionItem(畔塗対象圃場明細)
|
||||
|
||||
畔塗記録に紐づく対象圃場一覧。
|
||||
|
||||
| フィールド | 型 | 制約 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | int | PK | |
|
||||
| session | FK(LeveeWorkSession) | CASCADE | 親の畔塗記録 |
|
||||
| field | FK(fields.Field) | PROTECT | 対象圃場 |
|
||||
| plan | FK(plans.Plan) | SET_NULL, nullable | 保存時点の作付け計画参照 |
|
||||
| crop_name_snapshot | varchar(100) | required | 保存時点の作物名 |
|
||||
| variety_name_snapshot | varchar(100) | blank | 保存時点の品種名 |
|
||||
| created_at | datetime | auto | |
|
||||
| updated_at | datetime | auto | |
|
||||
|
||||
- `unique_together = ['session', 'field']`
|
||||
- 圃場名そのものは `Field` を参照して表示する
|
||||
- 作物・品種は履歴保全のためスナップショット保持を推奨する
|
||||
|
||||
### WorkRecord(作業記録索引)
|
||||
|
||||
既存 `apps/workrecords` の `WorkRecord` に畔塗種別を追加して連携する。
|
||||
|
||||
追加内容:
|
||||
|
||||
- `work_type` に `levee_work` を追加
|
||||
- `levee_work_session` への `OneToOne FK('levee_work.LeveeWorkSession')` を追加
|
||||
|
||||
想定制約:
|
||||
|
||||
- `on_delete=models.CASCADE`
|
||||
- `null=True`
|
||||
- `blank=True`
|
||||
- `related_name='work_record'`
|
||||
|
||||
削除方針:
|
||||
|
||||
- 親である `LeveeWorkSession` 削除時に、関連する `WorkRecord` は DB 制約の `CASCADE` で自動削除する
|
||||
- アプリケーション側での「紐づく WorkRecord を削除する」は、この DB 制約により満たされるものとして扱う
|
||||
|
||||
一覧表示時の想定値:
|
||||
|
||||
| 項目 | 値 |
|
||||
|---|---|
|
||||
| 作業日 | 畔塗記録の日付 |
|
||||
| 種別 | 畔塗 |
|
||||
| タイトル | 水稲畔塗 |
|
||||
| 参照先 | 畔塗した圃場一覧画面 |
|
||||
|
||||
---
|
||||
|
||||
## 候補圃場抽出ルール
|
||||
|
||||
畔塗対象候補は、作付け計画 `Plan` から抽出する。
|
||||
|
||||
### 基本条件
|
||||
|
||||
- 指定年度の `Plan` であること
|
||||
- `crop.name = "水稲"` の圃場であること
|
||||
- 圃場が存在すること
|
||||
|
||||
### 補足
|
||||
|
||||
- 判定条件は「品種が水稲」ではなく、原則として「作物が水稲」とする
|
||||
- `variety` は任意項目のため、品種未設定でも `crop=水稲` なら候補に含める
|
||||
- 並び順は `field.display_order`, `field.id`
|
||||
|
||||
### 候補レスポンスで返したい情報
|
||||
|
||||
| 項目 | 説明 |
|
||||
|---|---|
|
||||
| field_id | 圃場ID |
|
||||
| field_name | 圃場名 |
|
||||
| field_area_tan | 面積(反) |
|
||||
| group_name | グループ名 |
|
||||
| plan_id | 対応する作付け計画ID |
|
||||
| crop_name | 作物名 |
|
||||
| variety_name | 品種名 |
|
||||
| selected | 初期選択状態。候補圃場は原則 `true` を返し、全選択をデフォルトとする |
|
||||
|
||||
### 初期選択ルール
|
||||
|
||||
- 候補として返す水稲圃場は、原則すべて `selected=true` とする
|
||||
- 品種未設定の水稲圃場も `selected=true` とする
|
||||
- UI 上のチェック解除は、ユーザーが今回畔塗しない圃場を明示的に外すための操作と位置づける
|
||||
- 先行イメージ図にあった `☐ 山の前` は例示上の表現であり、初期ルールそのものではない
|
||||
|
||||
---
|
||||
|
||||
## 画面仕様
|
||||
|
||||
### 画面の位置づけ
|
||||
|
||||
畔塗機能は、日付を先に決めて対象圃場を選ぶ「日報型UI」とする。
|
||||
圃場ごとの個別登録画面ではなく、1回の保存で複数圃場をまとめて記録する。
|
||||
|
||||
### 主要画面
|
||||
|
||||
#### 1. 畔塗記録一覧画面
|
||||
|
||||
目的:
|
||||
|
||||
- 年度内の畔塗記録を一覧する
|
||||
- 新規作成画面へ遷移する
|
||||
- 既存記録の編集・削除を行う
|
||||
|
||||
表示項目:
|
||||
|
||||
- 畔塗日
|
||||
- タイトル
|
||||
- 対象圃場数
|
||||
- 対象圃場名の要約
|
||||
- 備考
|
||||
|
||||
#### 2. 畔塗記録作成・編集画面
|
||||
|
||||
入力項目:
|
||||
|
||||
- 日付
|
||||
- タイトル
|
||||
- 備考
|
||||
- 対象圃場一覧
|
||||
|
||||
対象圃場一覧の表示項目:
|
||||
|
||||
- 選択チェック
|
||||
- 圃場名
|
||||
- 面積
|
||||
- グループ
|
||||
- 作物
|
||||
- 品種
|
||||
|
||||
操作:
|
||||
|
||||
- 全選択
|
||||
- 全解除
|
||||
- 個別選択
|
||||
- 保存
|
||||
|
||||
初期表示ルール:
|
||||
|
||||
- 初回表示時は候補圃場を全選択状態で表示する
|
||||
- 編集時は保存済み明細に含まれる圃場を選択状態で復元する
|
||||
|
||||
### 推奨UIイメージ
|
||||
|
||||
```text
|
||||
畔塗記録作成
|
||||
|
||||
[日付 2026-04-20]
|
||||
[タイトル 水稲畔塗]
|
||||
[備考 __________________ ]
|
||||
|
||||
対象圃場一覧
|
||||
[全選択] [全解除]
|
||||
|
||||
☑ 田中上 1.2反 上エリア 水稲 コシヒカリ
|
||||
☑ 田中下 0.8反 上エリア 水稲 あきたこまち
|
||||
☐ 山の前 1.5反 南エリア 水稲 (未設定)
|
||||
|
||||
[保存]
|
||||
```
|
||||
|
||||
### 作業記録一覧への見え方
|
||||
|
||||
既存の作業記録一覧には次の形式で表示する。
|
||||
|
||||
| 列 | 表示内容 |
|
||||
|---|---|
|
||||
| 作業日 | 指定した日付 |
|
||||
| 種別 | 畔塗 |
|
||||
| タイトル | 水稲畔塗 |
|
||||
| 参照先 | 畔塗記録 #ID または対象圃場要約 |
|
||||
| 開く | 畔塗記録詳細画面へ遷移 |
|
||||
|
||||
---
|
||||
|
||||
## API エンドポイント
|
||||
|
||||
すべて JWT 認証必須。
|
||||
|
||||
### 畔塗記録
|
||||
|
||||
| メソッド | URL | 説明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/levee-work/sessions/?year={year}` | 年度別一覧 |
|
||||
| POST | `/api/levee-work/sessions/` | 新規作成 |
|
||||
| GET | `/api/levee-work/sessions/{id}/` | 詳細取得 |
|
||||
| PUT/PATCH | `/api/levee-work/sessions/{id}/` | 更新 |
|
||||
| DELETE | `/api/levee-work/sessions/{id}/` | 削除 |
|
||||
|
||||
### 候補圃場取得
|
||||
|
||||
| メソッド | URL | 説明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/levee-work/candidates/?year={year}` | 水稲作付け圃場候補を返す |
|
||||
|
||||
### レスポンス例(候補圃場)
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"field_id": 5,
|
||||
"field_name": "田中上",
|
||||
"field_area_tan": "1.2000",
|
||||
"group_name": "上エリア",
|
||||
"plan_id": 12,
|
||||
"crop_name": "水稲",
|
||||
"variety_name": "コシヒカリ",
|
||||
"selected": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### リクエスト例(新規作成)
|
||||
|
||||
```json
|
||||
{
|
||||
"year": 2026,
|
||||
"date": "2026-04-20",
|
||||
"title": "水稲畔塗",
|
||||
"notes": "",
|
||||
"items": [
|
||||
{
|
||||
"field": 5,
|
||||
"plan": 12
|
||||
},
|
||||
{
|
||||
"field": 6,
|
||||
"plan": 13
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
備考:
|
||||
|
||||
- `crop_name_snapshot` / `variety_name_snapshot` はクライアント送信項目ではない
|
||||
- サーバーが `plan` と `field` の整合を検証したうえで、保存時に `Plan` から自動設定する
|
||||
- `plan` が `null` の場合は、保存時点で参照できる `field` に対応する当年 `Plan` から補完を試みる
|
||||
|
||||
### レスポンス例(詳細)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 3,
|
||||
"year": 2026,
|
||||
"date": "2026-04-20",
|
||||
"title": "水稲畔塗",
|
||||
"notes": "",
|
||||
"work_record_id": 15,
|
||||
"item_count": 2,
|
||||
"items": [
|
||||
{
|
||||
"id": 11,
|
||||
"field": 5,
|
||||
"field_name": "田中上",
|
||||
"plan": 12,
|
||||
"crop_name_snapshot": "水稲",
|
||||
"variety_name_snapshot": "コシヒカリ"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"field": 6,
|
||||
"field_name": "田中下",
|
||||
"plan": 13,
|
||||
"crop_name_snapshot": "水稲",
|
||||
"variety_name_snapshot": "あきたこまち"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-04-20T08:00:00Z",
|
||||
"updated_at": "2026-04-20T08:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 業務フロー
|
||||
|
||||
### 1. 新規作成
|
||||
|
||||
1. ユーザーが年度と日付を選ぶ
|
||||
2. システムが当年の水稲作付け圃場を候補表示する
|
||||
3. ユーザーが対象圃場を選択する
|
||||
4. 保存時に `LeveeWorkSession` を作成する
|
||||
5. 明細として `LeveeWorkSessionItem` を一括作成する
|
||||
6. 各明細の `crop_name_snapshot` / `variety_name_snapshot` をサーバー側で自動設定する
|
||||
7. `WorkRecord` を自動生成または更新する
|
||||
|
||||
### 2. 編集
|
||||
|
||||
1. ユーザーが既存の畔塗記録を開く
|
||||
2. 日付・タイトル・備考・対象圃場を変更する
|
||||
3. 保存時に明細を再構成する
|
||||
4. `WorkRecord` 側の作業日・タイトルも同期更新する
|
||||
5. 明細のスナップショットも保存時点情報で再構成する
|
||||
|
||||
### 3. 削除
|
||||
|
||||
1. ユーザーが畔塗記録を削除する
|
||||
2. 紐づく `LeveeWorkSessionItem` は `CASCADE` で削除される
|
||||
3. 紐づく `WorkRecord` は `levee_work_session` の `on_delete=CASCADE` により削除される
|
||||
|
||||
---
|
||||
|
||||
## 作業記録連携仕様
|
||||
|
||||
畔塗記録保存時に `apps/workrecords` 側へ自動反映する。
|
||||
|
||||
### 追加する種別
|
||||
|
||||
| enum値 | 表示名 |
|
||||
|---|---|
|
||||
| `levee_work` | 畔塗 |
|
||||
|
||||
### 自動生成ルール
|
||||
|
||||
- `work_date` = `session.date`
|
||||
- `work_type` = `levee_work`
|
||||
- `title` = `session.title`
|
||||
- `year` = `session.year`
|
||||
- `auto_created` = `True`
|
||||
- `levee_work_session` = 対応する畔塗記録
|
||||
- `delivery_trip` = `None`
|
||||
- `spreading_session` = `None`
|
||||
|
||||
実装メモ:
|
||||
|
||||
- 既存の `sync_spreading_work_record()` と同様に、`update_or_create()` の `defaults` 内で他系統 FK を明示的に `None` へそろえる
|
||||
- `title` の未入力は `LeveeWorkSession` 保存時にサーバー側で `水稲畔塗` を補完するため、同期処理では補完済みの `session.title` をそのまま使う
|
||||
|
||||
### 同期タイミング
|
||||
|
||||
- 畔塗記録作成時: `update_or_create`
|
||||
- 畔塗記録更新時: `update_or_create`
|
||||
- 畔塗記録削除時: `levee_work_session` の `on_delete=CASCADE` により `WorkRecord` も自動削除される
|
||||
|
||||
---
|
||||
|
||||
## バリデーションルール
|
||||
|
||||
### 必須
|
||||
|
||||
- `year`
|
||||
- `date`
|
||||
- `items`(1件以上)
|
||||
|
||||
### 保存時チェック
|
||||
|
||||
- 選択圃場が0件の保存を禁止する
|
||||
- 同一セッション内で同じ圃場を重複登録しない
|
||||
- 候補外圃場の保存を原則禁止する
|
||||
- `year` は原則 `date.year` と一致しなければならない
|
||||
- `plan` が指定されている場合、その `plan.field` と `field` は一致しなければならない
|
||||
- `plan` が指定されている場合、その `plan.year` は `session.year` と一致しなければならない
|
||||
|
||||
### 業務上の許容
|
||||
|
||||
- 品種未設定の水稲圃場は保存可
|
||||
- 同日に別記録を複数作ることは可
|
||||
- 一度畔塗した圃場を別日に再度記録することは可
|
||||
|
||||
---
|
||||
|
||||
## 実装方針
|
||||
|
||||
### バックエンド
|
||||
|
||||
- 新規アプリ `apps/levee_work` を追加する案を第一候補とする
|
||||
- `Session` / `SessionItem` 構成でモデル化する
|
||||
- Serializer は `read` と `write` を分離する
|
||||
- 候補取得 API は `Plan` を起点に組み立てる
|
||||
- `sync_levee_work_record(session)` を作成して `WorkRecord` と同期する
|
||||
- `WorkRecord` から `LeveeWorkSession` への参照は、アプリ間循環参照を避けるため文字列参照 `OneToOneField('levee_work.LeveeWorkSession', ...)` を使う
|
||||
|
||||
### フロントエンド
|
||||
|
||||
- 画面候補: `frontend/src/app/levee-work/page.tsx`
|
||||
- 1画面完結の一覧 + 作成/編集パネル、または一覧画面 + 詳細画面のどちらでも可
|
||||
- 既存の `fertilizer/spreading` の「一覧 + 編集」導線を参考にする
|
||||
- `workrecords/page.tsx` に遷移先判定を追加する
|
||||
|
||||
### 命名方針
|
||||
|
||||
- ユーザー向け表示は「畔塗」で統一
|
||||
- コード上の英語名は `levee_work` または `levee_coating` が候補
|
||||
- 既存の `WorkRecord.WorkType` に追加する値は、短く意味がぶれない `levee_work` を推奨する
|
||||
|
||||
---
|
||||
|
||||
## 画面遷移案
|
||||
|
||||
```text
|
||||
作業記録一覧
|
||||
└─ 畔塗レコードの「開く」
|
||||
└─ 畔塗記録画面(該当セッションを編集状態で開く)
|
||||
|
||||
畔塗記録画面
|
||||
├─ 新規作成
|
||||
├─ 既存記録の編集
|
||||
└─ 保存後、作業記録一覧に反映
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 将来拡張
|
||||
|
||||
- 作業者名の保持
|
||||
- 使用機械の記録
|
||||
- 実施済み圃場を地図で確認
|
||||
- 写真添付
|
||||
- 代かき、耕起、播種など他作業への横展開
|
||||
- 汎用作業日誌基盤への統合
|
||||
|
||||
---
|
||||
|
||||
## 実装タスク案
|
||||
|
||||
1. `apps/levee_work` アプリ新設
|
||||
2. `LeveeWorkSession` / `LeveeWorkSessionItem` モデル追加
|
||||
3. migration 作成
|
||||
4. serializer / view / url 実装
|
||||
5. 候補圃場 API 実装
|
||||
6. `WorkRecord` に畔塗種別と参照FK追加
|
||||
7. `sync_levee_work_record` サービス実装
|
||||
8. フロントエンド一覧・作成画面実装
|
||||
9. 作業記録一覧の遷移先対応
|
||||
10. テスト追加
|
||||
|
||||
---
|
||||
|
||||
## 注意点と設計判断
|
||||
|
||||
### なぜ「圃場ごと1件」ではなく「日付ごと1件」か
|
||||
|
||||
- 実際の作業単位が日付ベースである
|
||||
- 一覧が見やすい
|
||||
- 既存の散布実績機能と整合する
|
||||
- 作業記録索引との親和性が高い
|
||||
|
||||
### なぜ作付け計画を参照するか
|
||||
|
||||
- 水稲圃場だけを自然に抽出できる
|
||||
- 年度との整合が取りやすい
|
||||
- 将来「未畔塗候補」や「前年比較」に発展させやすい
|
||||
|
||||
### スナップショットを持つ理由
|
||||
|
||||
- 後から作付け計画が変更されても、記録時点の情報を追える
|
||||
- 作業記録としての監査性を保ちやすい
|
||||
|
||||
### なぜ snapshot をクライアント入力にしないか
|
||||
|
||||
- `plan` と `field` からサーバーが一意に導出できる情報だから
|
||||
- クライアント送信にすると改ざんや不整合の余地が増えるから
|
||||
- API 入力を最小限に保った方が UI 実装が単純になるから
|
||||
|
||||
---
|
||||
|
||||
## ソースファイル追加想定
|
||||
|
||||
### バックエンド
|
||||
|
||||
- `backend/apps/levee_work/models.py`
|
||||
- `backend/apps/levee_work/serializers.py`
|
||||
- `backend/apps/levee_work/views.py`
|
||||
- `backend/apps/levee_work/urls.py`
|
||||
- `backend/apps/levee_work/admin.py`
|
||||
- `backend/apps/levee_work/migrations/0001_initial.py`
|
||||
- `backend/apps/workrecords/models.py`
|
||||
- `backend/apps/workrecords/services.py`
|
||||
- `backend/apps/workrecords/serializers.py`
|
||||
- `backend/apps/workrecords/views.py`
|
||||
- `backend/keinasystem/urls.py`
|
||||
|
||||
### フロントエンド
|
||||
|
||||
- `frontend/src/app/levee-work/page.tsx`
|
||||
- `frontend/src/types/index.ts`
|
||||
- `frontend/src/app/workrecords/page.tsx`
|
||||
|
||||
---
|
||||
|
||||
## まとめ
|
||||
|
||||
畔塗作業機能は、
|
||||
「当年の水稲作付け圃場を候補として出し、日付単位で複数圃場をまとめて記録し、作業記録一覧へ自動反映する」
|
||||
というシンプルな構成を基本とする。
|
||||
|
||||
この構成により、既存の作付け計画・作業記録の設計を壊さずに、
|
||||
春作業の記録を自然に追加できる。
|
||||
298
document/17_マスタードキュメント_ナビゲーション再編編.md
Normal file
298
document/17_マスタードキュメント_ナビゲーション再編編.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# マスタードキュメント:ナビゲーション再編
|
||||
|
||||
> **作成**: 2026-04-07
|
||||
> **最終更新**: 2026-04-07
|
||||
> **対象機能**: グローバルナビゲーション再編(トップメニュー整理・カテゴリ再編・PC/スマホ共通情報設計)
|
||||
> **実装状況**: 仕様策定完了・未実装
|
||||
|
||||
---
|
||||
|
||||
## 概要
|
||||
|
||||
機能追加に伴って共通ナビゲーションのトップレベル項目が増え、画面名ベースで並ぶ構造になってきたため、業務カテゴリ単位で再整理する。
|
||||
|
||||
今回の再編では、トップナビを `ホーム / 計画 / 実績 / マスター / 帳票・連携` の 5 分類に絞り、個別画面はドロップダウン配下に集約する。
|
||||
これにより、利用者が「どの画面名か」ではなく「何をしたいか」で画面を探せる状態を目指す。
|
||||
|
||||
また、URL 構造とメニュー構成は意図的に分離して扱う。既存 URL は安定性を優先して原則維持し、アクティブ判定はナビ定義側で吸収する。
|
||||
|
||||
### 機能スコープ(IN / OUT)
|
||||
|
||||
| IN(今回対象) | OUT(今回対象外) |
|
||||
|---|---|
|
||||
| PC ヘッダーのトップメニュー再編 | 各業務画面自体のUI改修 |
|
||||
| スマホ用ハンバーガーメニュー再編 | 権限別メニュー出し分け |
|
||||
| メニュー分類、並び順、開閉仕様 | お気に入り、ピン留め |
|
||||
| アクティブ判定ルール整理 | ダッシュボード内容の刷新 |
|
||||
| `NavGroup` / `NavItem` ベースのメニュー定義整理 | URL の全面変更 |
|
||||
| `作物` `品種` を将来のマスター画面として位置づけ | 矢印キー移動を含む高度なメニューアクセシビリティ |
|
||||
|
||||
---
|
||||
|
||||
## 背景と判断理由
|
||||
|
||||
### 現状の課題
|
||||
|
||||
- 横並びのトップメニュー数が多く、目的の画面を探しにくい
|
||||
- `計画` `実績` `設定` `補助機能` が同じ粒度で並んでいる
|
||||
- 画面名ベースで項目が増えており、業務単位でまとまっていない
|
||||
- 今後も機能追加が続くと、視認性と拡張性の両方が悪化する
|
||||
|
||||
### 採用した考え方
|
||||
|
||||
1. トップレベルは日常的に使う業務カテゴリだけに絞る
|
||||
2. 個別機能名ではなく、業務単位で束ねる
|
||||
3. URL はリソース識別子として安定性を優先し、メニュー構成とは分離する
|
||||
4. 例外的な URL 衝突のみナビ定義側のルールで吸収する
|
||||
|
||||
### 関連議論
|
||||
|
||||
- 判断理由、論点の切り分け、URL とメニューの関係整理は Gitea Issue `#13` に残す
|
||||
- 実装向けの決定事項は `改善案/ナビゲーション再編仕様書.md` に集約する
|
||||
- 本ドキュメントは、その内容を長期参照用に固定化したものとして扱う
|
||||
|
||||
---
|
||||
|
||||
## 情報設計
|
||||
|
||||
### トップレベル構成
|
||||
|
||||
1. ホーム
|
||||
2. 計画
|
||||
3. 実績
|
||||
4. マスター
|
||||
5. 帳票・連携
|
||||
|
||||
右上ユーザー操作:
|
||||
|
||||
- パスワード変更
|
||||
- ログアウト
|
||||
|
||||
### カテゴリ構成
|
||||
|
||||
#### ホーム
|
||||
|
||||
- ダッシュボード
|
||||
|
||||
#### 計画
|
||||
|
||||
- 作付け計画
|
||||
- 施肥計画
|
||||
- 田植え計画
|
||||
- 運搬計画
|
||||
|
||||
#### 実績
|
||||
|
||||
- 散布実績
|
||||
- 畔塗記録
|
||||
- 作業記録
|
||||
|
||||
#### マスター
|
||||
|
||||
- 圃場管理
|
||||
- 作物
|
||||
- 品種
|
||||
- 資材マスタ
|
||||
- 肥料マスタ
|
||||
|
||||
#### 帳票・連携
|
||||
|
||||
- 在庫管理
|
||||
- 帳票出力
|
||||
- データ取込
|
||||
- 気象
|
||||
- メール
|
||||
|
||||
### この分類にした理由
|
||||
|
||||
#### マスター
|
||||
|
||||
- `圃場管理` は圃場マスタとして独立性が高い
|
||||
- `作物` `品種` も本来マスター管理である
|
||||
- `資材マスタ` `肥料マスタ` はすでに独立画面が存在する
|
||||
|
||||
そのため、基礎データ管理を `マスター` に集約する。
|
||||
|
||||
#### 帳票・連携
|
||||
|
||||
- `在庫管理` `帳票出力` `データ取込` `気象` `メール` は完全に同質ではない
|
||||
- ただし、いずれも主作業そのものではなく、補助・参照・出力・連携の性質が強い
|
||||
|
||||
そのため、トップ階層を増やしすぎないための受け皿として `帳票・連携` にまとめる。
|
||||
|
||||
補足:
|
||||
|
||||
- `データ取込` は日常操作ではなく、年度切替時や初期設定時の補助導線とみなす
|
||||
- `メール` は個別トップにしない
|
||||
- `設定` は現状パスワード変更のみなので、右上ユーザー操作に残す
|
||||
|
||||
---
|
||||
|
||||
## 画面と所属カテゴリ
|
||||
|
||||
| カテゴリ | ラベル | パス |
|
||||
|---|---|---|
|
||||
| ホーム | ダッシュボード | `/dashboard` |
|
||||
| 計画 | 作付け計画 | `/allocation` |
|
||||
| 計画 | 施肥計画 | `/fertilizer` |
|
||||
| 計画 | 田植え計画 | `/rice-transplant` |
|
||||
| 計画 | 運搬計画 | `/distribution` |
|
||||
| 実績 | 散布実績 | `/fertilizer/spreading` |
|
||||
| 実績 | 畔塗記録 | `/levee-work` |
|
||||
| 実績 | 作業記録 | `/workrecords` |
|
||||
| マスター | 圃場管理 | `/fields` |
|
||||
| マスター | 作物 | 未実装(allocation 内管理を独立予定) |
|
||||
| マスター | 品種 | 未実装(allocation 内管理を独立予定) |
|
||||
| マスター | 資材マスタ | `/materials/masters` |
|
||||
| マスター | 肥料マスタ | `/fertilizer/masters` |
|
||||
| 帳票・連携 | 在庫管理 | `/materials` |
|
||||
| 帳票・連携 | 帳票出力 | `/reports` |
|
||||
| 帳票・連携 | データ取込 | `/import` |
|
||||
| 帳票・連携 | 気象 | `/weather` |
|
||||
| 帳票・連携 > メール | メール履歴 | `/mail/history` |
|
||||
| 帳票・連携 > メール | メールルール | `/mail/rules` |
|
||||
| 右上ユーザー操作 | パスワード変更 | `/settings/password` |
|
||||
|
||||
---
|
||||
|
||||
## URL とナビゲーションの関係
|
||||
|
||||
### 基本原則
|
||||
|
||||
1. URL はリソース・機能識別子として安定性を優先する
|
||||
2. メニュー構成とは意図的に分離して扱う
|
||||
3. メニュー再編のたびに URL を変更しない
|
||||
4. アクティブ判定はナビ定義側のルールで吸収する
|
||||
|
||||
### 採用理由
|
||||
|
||||
- URL をメニュー階層に合わせて変更すると、既存リンク、ブックマーク、テストへの影響が大きい
|
||||
- メニュー構成は将来も変わりうるため、URL にメニュー階層を埋め込むと変更コストが増える
|
||||
|
||||
### 衝突する既存パス
|
||||
|
||||
| 優先判定するパス | 所属カテゴリ | 除外される側 |
|
||||
|---|---|---|
|
||||
| `/fertilizer/spreading` | 実績 | 計画 > 施肥計画 |
|
||||
| `/materials/masters` | マスター | 帳票・連携 > 在庫管理 |
|
||||
|
||||
通常判定:
|
||||
|
||||
- `/fertilizer` `/fertilizer/new` `/fertilizer/[id]/edit` は `施肥計画`
|
||||
- `/materials` `/materials?tab=...` は `在庫管理`
|
||||
|
||||
---
|
||||
|
||||
## 表示仕様
|
||||
|
||||
### PC
|
||||
|
||||
- 左: ブランド名 `KeinaSystem`
|
||||
- 中央: トップメニュー 5 項目
|
||||
- 右: パスワード変更、ログアウト
|
||||
|
||||
表示ルール:
|
||||
|
||||
- `ホーム` は単独リンク
|
||||
- `計画` `実績` `マスター` `帳票・連携` はドロップダウン
|
||||
- 開いているメニューがある状態で別メニューを開く場合は、前のメニューを閉じる
|
||||
- メニュー外クリック、`Esc` キーで閉じる
|
||||
- 項目選択後は遷移して閉じる
|
||||
|
||||
### スマホ
|
||||
|
||||
- ハンバーガーメニューを採用する
|
||||
- `ホーム` は単独リンクで `/dashboard` へ遷移する
|
||||
- それ以外のカテゴリはアコーディオン形式で開閉する
|
||||
- 現在表示中の画面が属するカテゴリは初期状態で展開してよい
|
||||
- 項目タップ後はメニューを閉じて画面遷移する
|
||||
|
||||
---
|
||||
|
||||
## アクセシビリティ方針
|
||||
|
||||
- トップメニューへキーボードでフォーカス移動できること
|
||||
- `Enter` または `Space` でドロップダウンを開閉できること
|
||||
- ドロップダウン展開後、各項目へ `Tab` で到達できること
|
||||
- `Esc` で閉じられること
|
||||
- 現在位置が視覚的に分かること
|
||||
|
||||
### 初期実装でやらないこと
|
||||
|
||||
- 矢印キーによるドロップダウン項目間移動
|
||||
|
||||
これは Phase 1 の必須要件には含めず、将来のアクセシビリティ強化項目として扱う。
|
||||
|
||||
---
|
||||
|
||||
## 実装方針
|
||||
|
||||
### メニュー定義
|
||||
|
||||
メニュー定義はフラットな `group` 管理ではなく、階層構造で持つ。
|
||||
|
||||
```ts
|
||||
type NavItem = {
|
||||
label: string;
|
||||
href: string;
|
||||
match?: (pathname: string) => boolean;
|
||||
};
|
||||
|
||||
type NavGroup = {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'link' | 'group';
|
||||
href?: string;
|
||||
items?: NavItem[];
|
||||
};
|
||||
```
|
||||
|
||||
方針:
|
||||
|
||||
- グループ構成そのものが定義から読み取れることを優先する
|
||||
- 通常ケースは `href` ベースで扱う
|
||||
- `href` ベースで判定しきれない例外ケースだけ `match` を持つ
|
||||
- `match` は全件定義用ではなく、衝突回避用の最小限の逃げ道として使う
|
||||
|
||||
### Next.js App Router との関係
|
||||
|
||||
- Route Groups は、URL を変えずにコード構造を整理する手段として有効
|
||||
- ただし Route Groups はナビ判定そのものの代替ではなく、実装整理の補助手段として扱う
|
||||
|
||||
---
|
||||
|
||||
## 段階導入
|
||||
|
||||
### Phase 1
|
||||
|
||||
- トップナビを 5 分類へ再編する
|
||||
- `マスター` 配下に表示するのは `圃場管理` `資材マスタ` `肥料マスタ` のみ
|
||||
- `作物` `品種` はマスター体系には含めるが、独立画面がまだないため Phase 1 ではメニューに表示しない
|
||||
- PC / スマホともに同じ情報設計にそろえる
|
||||
|
||||
### Phase 2
|
||||
|
||||
- `作物管理` `品種管理` を独立画面として追加
|
||||
- `帳票・連携` 内の `メール` を必要に応じてサブグループ化
|
||||
|
||||
### Phase 3
|
||||
|
||||
- 将来マルチユーザー化した場合のみ再検討
|
||||
- 単独利用前提の間は実施対象外
|
||||
|
||||
---
|
||||
|
||||
## 受け入れ条件
|
||||
|
||||
- トップレベルに常時表示される業務メニューが 5 項目に収まっていること
|
||||
- 現在存在する主要画面が、いずれかのカテゴリに漏れなく所属していること
|
||||
- 各画面でアクティブ状態が期待通りに表示されること
|
||||
- PC とスマホで同じカテゴリ構成になっていること
|
||||
- メニューが増えても、トップレベルの項目数を増やさずに拡張できること
|
||||
|
||||
---
|
||||
|
||||
## 参照
|
||||
|
||||
- 議論の背景・判断理由: Gitea Issue `#13 メニューがごちゃごちゃしてきたので、整理する`
|
||||
- 実装向け詳細仕様: `改善案/ナビゲーション再編仕様書.md`
|
||||
639
document/18_マスタードキュメント_農薬散布管理編.md
Normal file
639
document/18_マスタードキュメント_農薬散布管理編.md
Normal file
@@ -0,0 +1,639 @@
|
||||
# マスタードキュメント:農薬散布管理機能
|
||||
|
||||
> **作成**: 2026-04-09
|
||||
> **最終更新**: 2026-04-09
|
||||
> **対象機能**: 農薬散布管理(農薬マスタ・散布記録・使用回数チェック・特別栽培向け成分数集計)
|
||||
> **実装状況**: 未着手(仕様確定済み)
|
||||
> **Gitea Issue**: akira/keinasystem#18
|
||||
|
||||
---
|
||||
|
||||
## 概要
|
||||
|
||||
農業生産者が散布した農薬を記録・管理し、農薬取締法に基づく使用基準(製品ごと・有効成分ごとの使用回数制限)への適合確認と、特別栽培認証用の成分数集計を行う機能。
|
||||
|
||||
### 機能スコープ(IN / OUT)
|
||||
|
||||
| IN(実装対象) | OUT(対象外) |
|
||||
|---|---|
|
||||
| 農薬マスタ管理(CRUD) | 農薬の在庫管理・購入管理 |
|
||||
| 農林水産省サイトからの農薬情報自動取得 | 農薬費用の管理 |
|
||||
| 散布イベント記録(圃場/グループ/作物/品種対象) | 希釈液の量管理 |
|
||||
| 製品ごとの使用回数チェック(年度×作物) | 農薬の廃棄記録 |
|
||||
| 有効成分ごとの総使用回数チェック(年度×作物) | 農薬散布マップ(GIS) |
|
||||
| 特別栽培用:節減対象農薬の使用成分数集計 | 農薬の処方箋・防除暦の自動作成 |
|
||||
| 回数超過アラート表示 | |
|
||||
|
||||
---
|
||||
|
||||
## 使用回数カウントのルール
|
||||
|
||||
農薬の使用回数は **製品単位** と **有効成分単位** の2軸で管理する。
|
||||
|
||||
### ルール1:製品ごとの使用回数
|
||||
|
||||
農薬製品(例: 住化スミチオン乳剤)を1シーズンに使用した回数 ≤ 登録情報の「本剤の使用回数」上限。
|
||||
|
||||
### ルール2:有効成分ごとの総使用回数
|
||||
|
||||
同一有効成分を含む複数製品を使用した場合、その有効成分の総使用回数として合算カウントする。
|
||||
|
||||
```
|
||||
例)「MEP乳剤A(上限3回)」と「MEP乳剤B(上限3回)」、MEP成分の総上限3回
|
||||
→ A剤2回 + B剤1回 = 合計3回 → OK
|
||||
→ A剤2回 + B剤2回 = 合計4回 → 超過!
|
||||
```
|
||||
|
||||
### ルール3:使用時期別カウント
|
||||
|
||||
育苗期・本圃期など時期別に別カウントになる場合がある(登録情報のテキストとして記録)。
|
||||
システムでは現フェーズで時期別の自動判定は行わず、登録情報テキストを参照情報として表示する。
|
||||
|
||||
### カウント対象外農薬(節減対象外)
|
||||
|
||||
以下の農薬は使用回数・成分数のカウントから除外する(`is_non_target` フラグで管理):
|
||||
|
||||
- 展着剤(`is_spreader` フラグでも管理)
|
||||
- 有機JAS別表2に掲げる農薬(除虫菊乳剤・硫黄剤・天敵生物農薬・性フェロモン剤等)
|
||||
- 化学合成でないと認められた農薬(カスガマイシン剤・ポリオキシン剤・バリダマイシン剤等)
|
||||
|
||||
### 特別栽培向け成分数集計
|
||||
|
||||
「節減対象農薬(`is_non_target=False`)の有効成分(`is_active=True`)が何種類使われたか」を年度×作物単位でカウントする。
|
||||
上限はなく、報告用の集計値として表示する。
|
||||
|
||||
---
|
||||
|
||||
## データモデル
|
||||
|
||||
### Pesticide(農薬マスタ)
|
||||
|
||||
**アプリ**: `apps/pesticide`
|
||||
**テーブル名**: `pesticide_pesticide`
|
||||
|
||||
| フィールド | 型 | 制約 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | BigAutoField | PK | |
|
||||
| name | CharField(200) | required | 農薬名(例: 住化スミチオン乳剤) |
|
||||
| pesticide_type | CharField(100) | blank | 農薬の種類(例: MEP乳剤) |
|
||||
| registration_number | CharField(20) | blank | 農薬登録番号(公式登録番号) |
|
||||
| system_id | CharField(20) | blank | 農水省サイトの内部ID(詳細URLに使用) |
|
||||
| purpose | CharField(100) | blank | 用途(例: 殺虫剤) |
|
||||
| formulation | CharField(100) | blank | 剤型(例: 乳剤) |
|
||||
| toxicity | CharField(20) | blank | 製剤毒性(普/毒/劇等) |
|
||||
| is_spreader | BooleanField | default=False | 展着剤フラグ |
|
||||
| is_non_target | BooleanField | default=False | 節減対象外フラグ(カウント除外) |
|
||||
| notes | TextField | blank | 備考 |
|
||||
| fetched_at | DateTimeField | null=True | 農水省サイトからの最終取得日時 |
|
||||
| created_at | DateTimeField | auto | |
|
||||
| updated_at | DateTimeField | auto | |
|
||||
|
||||
- `name` は unique 制約なし(同名で複数登録番号が存在しうる)
|
||||
- `is_spreader=True` の場合、`is_non_target` も自動的に `True` 扱いとする
|
||||
|
||||
### PesticideIngredient(有効成分)
|
||||
|
||||
**テーブル名**: `pesticide_pesticideingredient`
|
||||
|
||||
| フィールド | 型 | 制約 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | BigAutoField | PK | |
|
||||
| pesticide | FK(Pesticide) | CASCADE | |
|
||||
| name | CharField(200) | required | 成分名称(例: MEP) |
|
||||
| concentration | CharField(100) | blank | 含有濃度(例: 50.0%) |
|
||||
| is_active | BooleanField | default=True | 有効成分かどうか(False = その他成分) |
|
||||
|
||||
- `unique_together = ['pesticide', 'name']`
|
||||
|
||||
### PesticideIngredientLimit(有効成分の総使用回数上限:作物別)
|
||||
|
||||
**テーブル名**: `pesticide_pesticideingredientlimit`
|
||||
|
||||
農水省の「○○を含む農薬の総使用回数」は作物ごとに異なりうるため、有効成分本体とは分離して作物別に保持する。
|
||||
|
||||
| フィールド | 型 | 制約 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | BigAutoField | PK | |
|
||||
| pesticide | FK(Pesticide) | CASCADE | 取得元農薬 |
|
||||
| ingredient_name | CharField(200) | required | 成分名称(例: MEP) |
|
||||
| crop_name | CharField(200) | required | 作物名(農水省登録情報の表記、例: 稲) |
|
||||
| max_total_uses | IntegerField | null=True | この成分を含む農薬の総使用回数上限 |
|
||||
| use_timing_note | TextField | blank | 使用時期別制限のテキスト(例: 種もみへの処理は1回以内、…) |
|
||||
|
||||
- `unique_together = ['pesticide', 'ingredient_name', 'crop_name']`
|
||||
- 同一成分・同一作物であれば製品が異なっても上限値は同一(農水省登録情報の仕様)
|
||||
- 保存時バリデーション: 同一 `ingredient_name + crop_name` の既存レコードと異なる `max_total_uses` を保存しようとした場合はエラーにする
|
||||
- 使用回数チェック API の `ingredient_usage.max_total_uses` は、同一 `ingredient_name + crop_name` の値が一意であることを前提に単一値を返す
|
||||
|
||||
### PesticideProductLimit(製品の使用回数上限:作物別)
|
||||
|
||||
**テーブル名**: `pesticide_pesticideproductlimit`
|
||||
|
||||
農水省の適用表は作物ごとに上限が異なるため、作物名をキーとして保存する。
|
||||
|
||||
| フィールド | 型 | 制約 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | BigAutoField | PK | |
|
||||
| pesticide | FK(Pesticide) | CASCADE | |
|
||||
| crop_name | CharField(200) | required | 作物名(農水省登録情報の表記、例: 稲) |
|
||||
| max_uses | IntegerField | required | 本剤の使用回数上限 |
|
||||
| use_timing_note | TextField | blank | 使用時期・条件の補足テキスト |
|
||||
|
||||
- `unique_together = ['pesticide', 'crop_name']`
|
||||
|
||||
### PesticideCropAlias(農水省作物名と内部作物の対応)
|
||||
|
||||
**テーブル名**: `pesticide_pesticidecropalias`
|
||||
|
||||
農水省の適用表上の作物名と、内部 `plans.Crop` の作物を対応付けるための正規化テーブル。
|
||||
|
||||
| フィールド | 型 | 制約 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | BigAutoField | PK | |
|
||||
| crop | FK(plans.Crop) | PROTECT | 内部作物 |
|
||||
| alias_name | CharField(200) | required, unique | 農水省登録情報の作物名(例: 稲, 水稲) |
|
||||
| is_primary | BooleanField | default=False | 代表表記かどうか |
|
||||
|
||||
- 使用回数チェック時は `crop_id` から本テーブルを逆引きし、`PesticideProductLimit.crop_name` / `PesticideIngredientLimit.crop_name` と照合する
|
||||
- 初期データ例: `Crop=水稲` に対し `alias_name=稲`, `alias_name=水稲` を登録
|
||||
|
||||
### SprayEvent(散布イベント)
|
||||
|
||||
**テーブル名**: `pesticide_sprayevent`
|
||||
|
||||
1回の散布作業を1件として記録する。
|
||||
|
||||
| フィールド | 型 | 制約 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | BigAutoField | PK | |
|
||||
| year | IntegerField | required | 年度(集計フィルタ用) |
|
||||
| date | DateField | required | 散布日 |
|
||||
| target_type | CharField(20) | required | 対象種別: `field` / `group` / `crop` / `variety` |
|
||||
| target_field | FK(fields.Field) | null=True, PROTECT | 対象が圃場の場合 |
|
||||
| target_group | CharField(50) | blank | 対象が圃場グループの場合(group_name) |
|
||||
| target_crop | FK(plans.Crop) | null=True, PROTECT | 対象が作物の場合 |
|
||||
| target_variety | FK(plans.Variety) | null=True, PROTECT | 対象が品種の場合 |
|
||||
| notes | TextField | blank | 備考 |
|
||||
| created_at | DateTimeField | auto | |
|
||||
| updated_at | DateTimeField | auto | |
|
||||
|
||||
#### target_type 別のバリデーション
|
||||
|
||||
| target_type | 必須フィールド | 意味 |
|
||||
|---|---|---|
|
||||
| `field` | target_field | 特定の圃場1筆に散布 |
|
||||
| `group` | target_group | 同一 group_name の全圃場に散布 |
|
||||
| `crop` | target_crop | 特定の作物に対して散布(作付け計画と照合) |
|
||||
| `variety` | target_variety | 特定の品種に対して散布(作付け計画と照合) |
|
||||
|
||||
- 保存時に全対象圃場を `SprayEventResolvedField` として確定保存し、後日の作付け変更やグループ名変更があっても過去実績の集計結果が変わらないようにする
|
||||
|
||||
### SprayEventResolvedField(散布イベント対象圃場スナップショット)
|
||||
|
||||
**テーブル名**: `pesticide_sprayeventresolvedfield`
|
||||
|
||||
`target_type=group` / `crop` / `variety` のように複数圃場へ展開される散布について、保存時点で対象圃場を確定保存する。
|
||||
|
||||
| フィールド | 型 | 制約 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | BigAutoField | PK | |
|
||||
| event | FK(SprayEvent) | CASCADE | |
|
||||
| field | FK(fields.Field) | PROTECT | 対象圃場 |
|
||||
| field_name_snapshot | CharField(100) | required | 保存時点の圃場名 |
|
||||
| group_name_snapshot | CharField(50) | blank | 保存時点のグループ名 |
|
||||
| crop_name_snapshot | CharField(100) | required | 保存時点の作物名 |
|
||||
| variety_name_snapshot | CharField(100) | blank | 保存時点の品種名 |
|
||||
|
||||
- `unique_together = ['event', 'field']`
|
||||
- `target_type=field` の場合も 1 行作成しておくと、集計ロジックを統一しやすい
|
||||
|
||||
### SprayEventPesticide(散布農薬明細)
|
||||
|
||||
**テーブル名**: `pesticide_sprayeventpesticide`
|
||||
|
||||
1つの散布イベントに複数農薬を紐づける。
|
||||
|
||||
| フィールド | 型 | 制約 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | BigAutoField | PK | |
|
||||
| event | FK(SprayEvent) | CASCADE | |
|
||||
| pesticide | FK(Pesticide) | PROTECT | 使用農薬 |
|
||||
| dilution_ratio | CharField(50) | blank | 希釈倍率(例: 1000倍) |
|
||||
| amount_used | CharField(50) | blank | 使用量(例: 500mL、単位込みで自由記述) |
|
||||
| notes | TextField | blank | 備考 |
|
||||
|
||||
- `pesticide` は PROTECT(使用済み農薬は削除不可)
|
||||
- `unique_together = ['event', 'pesticide']`(同一イベント内で同じ農薬を2回登録不可)
|
||||
|
||||
---
|
||||
|
||||
## 使用回数集計の仕組み
|
||||
|
||||
### 集計単位
|
||||
|
||||
**年度 × 作物** を基本単位とする(農薬取締法上、使用回数は作物単位で管理する義務がある)。
|
||||
|
||||
- 集計対象作物は `SprayEventResolvedField.crop_name_snapshot` を正とする(圃場ごとに記録)
|
||||
- `target_type=field`/`group`/`crop`/`variety` の違いにかかわらず、保存時に全対象圃場の `SprayEventResolvedField` を作成し、各圃場の作物をスナップショットとして保持する
|
||||
- **グループ内に複数作物が混在する場合**、同一の散布イベント・散布農薬でも作物ごとに使用回数がカウントされる。例:グループ内に「水稲」3筆・「大豆」1筆が含まれる場合、そのイベントの農薬は水稲の回数にも大豆の回数にも +1 される
|
||||
- 使用回数上限の照合は、`SprayEventResolvedField.crop_name_snapshot` → `PesticideCropAlias` → `PesticideProductLimit` / `PesticideIngredientLimit` の順に行う
|
||||
|
||||
### 製品使用回数の集計
|
||||
|
||||
1イベント = 1散布作業 = 1回。`unique_together=['event', 'pesticide']` により同一イベント内で同一農薬は1行しか存在しないため、イベント単位でカウントして正確。
|
||||
|
||||
```
|
||||
製品使用回数(年度Y・作物C・農薬P)=
|
||||
COUNT(DISTINCT SprayEvent.id)
|
||||
where SprayEvent に SprayEventPesticide(pesticide=P) が紐づく
|
||||
かつ SprayEvent に SprayEventResolvedField(crop_name_snapshot=C) が紐づく
|
||||
かつ SprayEvent.year = Y
|
||||
```
|
||||
|
||||
※ 1イベントで複数圃場に散布しても「1回」とカウントする(1イベント=1散布作業)
|
||||
|
||||
### 有効成分総使用回数の集計
|
||||
|
||||
1回の散布作業(イベント)= 有効成分の使用回数1回。同一成分を含む複数製品を同一イベントで施用することは実務上なく、仮に混合散布しても農薬取締法上「1回の散布 = 1回の使用」と解釈される。
|
||||
|
||||
```
|
||||
有効成分総使用回数(年度Y・作物C・成分名I)=
|
||||
COUNT(DISTINCT SprayEvent.id)
|
||||
where SprayEvent に SprayEventPesticide が紐づく
|
||||
かつ SprayEventPesticide.pesticide の PesticideIngredient に
|
||||
name=I かつ is_active=True のものが存在する
|
||||
かつ SprayEventPesticide.pesticide.is_non_target=False
|
||||
かつ SprayEvent に SprayEventResolvedField(crop_name_snapshot=C) が紐づく
|
||||
かつ SprayEvent.year = Y
|
||||
```
|
||||
|
||||
※ `SprayEventResolvedField` は圃場ごとに複数行あるため、結合で行が増えても `DISTINCT SprayEvent.id` で 1散布作業を1回だけ数える
|
||||
|
||||
### 特別栽培・使用成分数の集計
|
||||
|
||||
```
|
||||
使用成分数(年度Y・作物C)=
|
||||
COUNT(DISTINCT PesticideIngredient.name)
|
||||
where 上記条件(年度Y・作物C)の散布イベントで使用された農薬に含まれる
|
||||
かつ PesticideIngredient.is_active=True
|
||||
かつ SprayEventPesticide.pesticide.is_non_target=False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API エンドポイント
|
||||
|
||||
すべて JWT 認証(`Authorization: Bearer <token>`)が必要。
|
||||
|
||||
### 農薬マスタ
|
||||
|
||||
| メソッド | URL | 説明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/pesticide/pesticides/` | 一覧取得 |
|
||||
| POST | `/api/pesticide/pesticides/` | 新規作成 |
|
||||
| GET | `/api/pesticide/pesticides/{id}/` | 詳細取得 |
|
||||
| PUT/PATCH | `/api/pesticide/pesticides/{id}/` | 更新 |
|
||||
| DELETE | `/api/pesticide/pesticides/{id}/` | 削除(使用中は 400) |
|
||||
| POST | `/api/pesticide/pesticides/fetch/` | 農水省サイトから情報取得 |
|
||||
|
||||
農薬マスタ レスポンス例:
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "住化スミチオン乳剤",
|
||||
"pesticide_type": "MEP乳剤",
|
||||
"registration_number": "4962",
|
||||
"system_id": "4962",
|
||||
"purpose": "殺虫剤",
|
||||
"formulation": "乳剤",
|
||||
"toxicity": "普",
|
||||
"is_spreader": false,
|
||||
"is_non_target": false,
|
||||
"notes": "",
|
||||
"fetched_at": "2026-04-09T10:00:00Z",
|
||||
"ingredients": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "MEP",
|
||||
"concentration": "50.0%",
|
||||
"is_active": true
|
||||
}
|
||||
],
|
||||
"product_limits": [
|
||||
{
|
||||
"id": 1,
|
||||
"crop_name": "稲",
|
||||
"max_uses": 2,
|
||||
"use_timing_note": "収穫21日前まで"
|
||||
}
|
||||
],
|
||||
"ingredient_limits": [
|
||||
{
|
||||
"id": 1,
|
||||
"ingredient_name": "MEP",
|
||||
"crop_name": "稲",
|
||||
"max_total_uses": 3,
|
||||
"use_timing_note": "種もみへの処理は1回以内、育苗箱散布は1回以内、本田では2回以内"
|
||||
}
|
||||
],
|
||||
"crop_aliases": [
|
||||
{
|
||||
"crop": 1,
|
||||
"crop_name": "水稲",
|
||||
"alias_name": "稲",
|
||||
"is_primary": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/pesticide/pesticides/fetch/`
|
||||
|
||||
農水省農薬登録情報提供システムから農薬情報を取得してマスタに保存する。
|
||||
取得に失敗した場合は `fetch_error` を返し、手動入力に切り替える。
|
||||
|
||||
リクエスト:
|
||||
```json
|
||||
{
|
||||
"name": "スミチオン"
|
||||
}
|
||||
```
|
||||
|
||||
レスポンス(成功):
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"candidates": [
|
||||
{
|
||||
"system_id": "4962",
|
||||
"name": "住化スミチオン乳剤",
|
||||
"pesticide_type": "MEP乳剤",
|
||||
"registration_number": "4962"
|
||||
},
|
||||
{
|
||||
"system_id": "4991",
|
||||
"name": "ホクコースミチオン乳剤",
|
||||
"pesticide_type": "MEP乳剤",
|
||||
"registration_number": "4991"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
候補が複数ある場合はフロントで選択させ、選択後に詳細取得リクエストを投げる:
|
||||
```json
|
||||
{ "system_id": "4962" }
|
||||
```
|
||||
|
||||
レスポンス(失敗):
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "農林水産省サイトへの接続に失敗しました。手動で入力してください。"
|
||||
}
|
||||
```
|
||||
|
||||
### 散布イベント
|
||||
|
||||
| メソッド | URL | 説明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/pesticide/events/?year={year}` | 年度別一覧 |
|
||||
| POST | `/api/pesticide/events/` | 新規作成 |
|
||||
| GET | `/api/pesticide/events/{id}/` | 詳細取得 |
|
||||
| PUT/PATCH | `/api/pesticide/events/{id}/` | 更新 |
|
||||
| DELETE | `/api/pesticide/events/{id}/` | 削除 |
|
||||
|
||||
散布イベント POST リクエスト例(圃場グループを対象に複数農薬散布):
|
||||
```json
|
||||
{
|
||||
"year": 2026,
|
||||
"date": "2026-05-10",
|
||||
"target_type": "group",
|
||||
"target_group": "田中エリア",
|
||||
"notes": "曇り、風弱し",
|
||||
"pesticides": [
|
||||
{
|
||||
"pesticide": 1,
|
||||
"dilution_ratio": "1000倍",
|
||||
"amount_used": "500mL"
|
||||
},
|
||||
{
|
||||
"pesticide": 3,
|
||||
"dilution_ratio": "2000倍",
|
||||
"amount_used": "200mL"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
散布イベント レスポンス例:
|
||||
```json
|
||||
{
|
||||
"id": 10,
|
||||
"year": 2026,
|
||||
"date": "2026-05-10",
|
||||
"target_type": "group",
|
||||
"target_group": "田中エリア",
|
||||
"target_display": "田中エリア(グループ)",
|
||||
"resolved_fields": [
|
||||
{
|
||||
"field": 5,
|
||||
"field_name_snapshot": "田中上",
|
||||
"group_name_snapshot": "田中エリア",
|
||||
"crop_name_snapshot": "水稲",
|
||||
"variety_name_snapshot": "コシヒカリ"
|
||||
}
|
||||
],
|
||||
"notes": "曇り、風弱し",
|
||||
"pesticides": [
|
||||
{
|
||||
"id": 15,
|
||||
"pesticide": 1,
|
||||
"pesticide_name": "住化スミチオン乳剤",
|
||||
"dilution_ratio": "1000倍",
|
||||
"amount_used": "500mL"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-04-09T10:00:00Z",
|
||||
"updated_at": "2026-04-09T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 使用回数チェック
|
||||
|
||||
#### `GET /api/pesticide/usage-summary/?year={year}&crop_id={crop_id}`
|
||||
|
||||
年度×作物単位で使用回数の集計・チェック結果を返す。
|
||||
|
||||
レスポンス例:
|
||||
```json
|
||||
{
|
||||
"year": 2026,
|
||||
"crop_id": 1,
|
||||
"crop_name": "水稲",
|
||||
"crop_aliases": ["稲", "水稲"],
|
||||
"product_usage": [
|
||||
{
|
||||
"pesticide_id": 1,
|
||||
"pesticide_name": "住化スミチオン乳剤",
|
||||
"used_count": 2,
|
||||
"max_uses": 2,
|
||||
"remaining": 0,
|
||||
"is_over": false
|
||||
}
|
||||
],
|
||||
"ingredient_usage": [
|
||||
{
|
||||
"ingredient_name": "MEP",
|
||||
"used_count": 2,
|
||||
"max_total_uses": 3,
|
||||
"remaining": 1,
|
||||
"is_over": false,
|
||||
"products_used": ["住化スミチオン乳剤"]
|
||||
}
|
||||
],
|
||||
"component_count": 2,
|
||||
"has_violation": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 農水省サイトスクレイピング仕様
|
||||
|
||||
### 対象サイト
|
||||
|
||||
農林水産省 農薬登録情報提供システム
|
||||
URL: `https://pesticide.maff.go.jp/`
|
||||
|
||||
### アクセスフロー
|
||||
|
||||
```
|
||||
1. GET /agricultural-chemicals/name-search/
|
||||
→ JSESSIONID クッキー + CSRF トークン(フォーム埋め込み)取得
|
||||
|
||||
2. POST /agricultural-chemicals/name-search
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Body: _csrf=<token>&agriculturalChemicalsName=<農薬名>&agriculturalChemicalsType=
|
||||
→ 302 リダイレクト先: /agricultural-chemicals/list
|
||||
|
||||
3. GET /agricultural-chemicals/list
|
||||
→ 検索結果一覧 HTML
|
||||
→ <a href="/agricultural-chemicals/details/{system_id}"> からリンク抽出
|
||||
|
||||
4. GET /agricultural-chemicals/details/{system_id}
|
||||
→ 詳細ページ HTML → 下記データをパース
|
||||
```
|
||||
|
||||
### 詳細ページ パース項目
|
||||
|
||||
**基本情報テーブル(`th[scope=col]` + `td` ペア):**
|
||||
|
||||
| th テキスト | 取得項目 | 保存先 |
|
||||
|---|---|---|
|
||||
| 登録番号 | 登録番号 | `registration_number` |
|
||||
| 農薬の種類 | 種類名 | `pesticide_type` |
|
||||
| 農薬の名称 | 農薬名 | `name` |
|
||||
| 用途 | 用途 | `purpose` |
|
||||
| 剤型 | 剤型 | `formulation` |
|
||||
| 製剤毒性 | 毒性区分 | `toxicity` |
|
||||
|
||||
**有効成分テーブル:**
|
||||
|
||||
- 「有効成分」行: `is_active=True`、成分名・含有濃度を取得
|
||||
- 「その他成分」行: `is_active=False`
|
||||
|
||||
**適用表(作物×病害虫ごとの行):**
|
||||
|
||||
各行のカラム(`data-label` 属性でカラム識別):
|
||||
|
||||
| data-label | 取得項目 | 保存先 |
|
||||
|---|---|---|
|
||||
| 作物名 | 作物名 | `PesticideProductLimit.crop_name` |
|
||||
| 本剤の使用回数 | 「N回以内」から N を抽出 | `PesticideProductLimit.max_uses` |
|
||||
| 使用時期 | テキストそのまま | `PesticideProductLimit.use_timing_note` |
|
||||
| `{成分名}を含む農薬の総使用回数` | 「N回以内(...)」から N と補足を抽出 | `PesticideIngredientLimit.max_total_uses` / `use_timing_note` |
|
||||
|
||||
**「総使用回数」テキストのパース規則:**
|
||||
|
||||
```
|
||||
入力例: "3回以内(種もみへの処理は1回以内、育苗箱散布は1回以内、本田では2回以内)"
|
||||
→ max_total_uses = 3
|
||||
→ use_timing_note = "種もみへの処理は1回以内、育苗箱散布は1回以内、本田では2回以内"
|
||||
|
||||
正規表現: r'(\d+)回以内(?:\((.+)\))?'
|
||||
```
|
||||
|
||||
**整合性チェック:**
|
||||
|
||||
- 同一 `ingredient_name + crop_name` に対して既存の `PesticideIngredientLimit.max_total_uses` と異なる値が取得された場合、その農薬の自動取込はエラーとし、手動確認を促す
|
||||
- `use_timing_note` の差異は許容し、より詳細なテキストで上書きしてよい
|
||||
|
||||
### 実装場所
|
||||
|
||||
`apps/pesticide/management/commands/fetch_pesticide.py`
|
||||
Django management command として実装。APIエンドポイントから呼び出す。
|
||||
|
||||
### 注意事項
|
||||
|
||||
- セッション(`requests.Session`)を使用し、クッキーとCSRFを維持する
|
||||
- アクセスは農薬マスタ登録時の1件ずつに限定(バルク取得は行わない)
|
||||
- 農水省サイトの内部ID(`system_id`)と農薬の公式登録番号は別物
|
||||
- タイムアウト: 10秒
|
||||
- 適用表の作物名は `PesticideCropAlias` で内部 `Crop` と対応付ける前提で保存する
|
||||
|
||||
---
|
||||
|
||||
## 画面仕様
|
||||
|
||||
### 農薬マスタ画面(`/pesticide/`)
|
||||
|
||||
- 登録済み農薬の一覧表示
|
||||
- 農薬名で検索 → 農水省サイトから候補を取得 → 選択して詳細取得 → 保存
|
||||
- 取得失敗時は手動入力フォームに切り替え
|
||||
- 展着剤フラグ・節減対象外フラグの編集
|
||||
|
||||
### 散布記録入力画面(`/pesticide/events/new`)
|
||||
|
||||
- 散布日・年度入力
|
||||
- 対象種別(圃場/グループ/作物/品種)選択 → 対象を選択
|
||||
- 農薬を追加(複数可): 農薬マスタから選択 + 希釈倍率 + 使用量
|
||||
- 保存時に使用回数チェックを実行し、超過がある場合は警告を表示(保存はブロックしない)
|
||||
|
||||
### 使用回数チェック画面(`/pesticide/usage`)
|
||||
|
||||
- 年度・作物でフィルタ
|
||||
- **製品使用回数テーブル**: 農薬名 / 使用回数 / 上限 / 残回数(超過時は赤表示)
|
||||
- **有効成分総使用回数テーブル**: 成分名 / 使用回数 / 上限 / 残回数 / 使用製品一覧(超過時は赤表示)
|
||||
- **特別栽培欄**: 節減対象農薬の使用成分数(報告用)
|
||||
|
||||
---
|
||||
|
||||
## 設計判断と制約
|
||||
|
||||
1. **散布対象の特定**: `target_type` + 対象FK/文字列で柔軟に対応。保存時に `SprayEventResolvedField` で対象圃場と作物を確定保存する。作付け計画(Plan)はあくまで保存時の解決に使うだけで、集計の正源ではない。
|
||||
2. **使用回数上限は作物別に保持**: 同一農薬でも作物ごとに上限が異なるため `PesticideProductLimit` と `PesticideIngredientLimit` を作物別に複数行保持する。
|
||||
3. **作物名の照合は別名テーブルで吸収**: 農水省表記の「稲」と内部の「水稲」のような差異を吸収するため、`PesticideCropAlias` を必須とする。
|
||||
4. **散布対象は保存時に確定保存する**: 後日のグループ名変更や作付け変更で過去実績の集計結果が変わらないよう、`SprayEventResolvedField` に圃場・作物をスナップショット保存する。`SprayEvent` 自体には作物情報を持たない。
|
||||
5. **有効成分総使用回数も「1イベント=1回」**: 同一成分を含む複数製品を同一イベントで施用することは実務上なく、仮に混合散布しても農薬取締法上「1回の散布=1回の使用」。製品使用回数と同様に `COUNT(DISTINCT SprayEvent.id)` で集計する。`SprayEventResolvedField` との結合で行が増えても `DISTINCT` で正確にカウントできる。
|
||||
6. **総使用回数はテキストパース**: 農水省サイトの「○○を含む農薬の総使用回数」カラムから正規表現で数値を抽出する。
|
||||
7. **有効成分上限の整合性は保存時に保証する**: 同一 `ingredient_name + crop_name` の `max_total_uses` は製品をまたいで一致している前提とし、異なる値を保存しようとした場合はエラーにする。
|
||||
8. **保存はブロックしない**: 使用回数超過は警告表示のみ。農薬散布の記録は法的義務があるため、超過でも保存できるようにする。
|
||||
9. **`SprayEventPesticide.pesticide` は PROTECT**: 散布記録に使用中の農薬は削除不可。
|
||||
10. **成分集計は `is_active=True` のみ対象**: 「その他成分」は総使用回数・特別栽培の成分数集計に含めない。
|
||||
11. **`is_spreader=True` は `is_non_target` 扱い**: 展着剤はカウント除外のため、展着剤フラグをセットすれば節減対象外フラグも自動的に True 扱い(DB保存は別フィールド)。
|
||||
|
||||
---
|
||||
|
||||
## ソースファイル索引(実装後に更新)
|
||||
|
||||
| ファイル | 説明 |
|
||||
|---|---|
|
||||
| `backend/apps/pesticide/models.py` | Pesticide, PesticideIngredient, PesticideIngredientLimit, PesticideProductLimit, PesticideCropAlias, SprayEvent, SprayEventResolvedField, SprayEventPesticide |
|
||||
| `backend/apps/pesticide/serializers.py` | DRF シリアライザ |
|
||||
| `backend/apps/pesticide/views.py` | ViewSet |
|
||||
| `backend/apps/pesticide/urls.py` | URL ルーティング |
|
||||
| `backend/apps/pesticide/management/commands/fetch_pesticide.py` | 農水省スクレイパー |
|
||||
| `frontend/src/app/pesticide/page.tsx` | 農薬マスタ一覧・散布記録 |
|
||||
| `frontend/src/app/pesticide/usage/page.tsx` | 使用回数チェック画面 |
|
||||
| `frontend/src/lib/types.ts` | 型定義(Pesticide, SprayEvent 等) |
|
||||
392
document/19_マスタードキュメント_TODO管理編.md
Normal file
392
document/19_マスタードキュメント_TODO管理編.md
Normal file
@@ -0,0 +1,392 @@
|
||||
# マスタードキュメント:TODO管理機能
|
||||
|
||||
> **作成**: 2026-04-10
|
||||
> **最終更新**: 2026-04-10(tractor_work 対応追記)
|
||||
> **対象機能**: TODO管理(作業指示・優先順位管理・実績連携導線)
|
||||
> **実装状況**: 設計完了・実装前
|
||||
> **対象 Issue**: `akira/keinasystem#17`
|
||||
|
||||
---
|
||||
|
||||
## 概要
|
||||
|
||||
繁忙期に「どれから手を付けるか」を管理するための TODO 機能。
|
||||
計画(施肥・田植え・運搬など)と実績の間に位置する「作業指示」レイヤー。
|
||||
|
||||
### 機能スコープ(IN / OUT)
|
||||
|
||||
| IN(MVP対象) | OUT(対象外) |
|
||||
|---|---|
|
||||
| TODO の作成・編集・削除 | 期日通知・リマインダー |
|
||||
| ステータス管理(todo / doing / done / canceled) | 複数ユーザー割り当て |
|
||||
| 優先順位管理(ドラッグ&ドロップ / 矢印移動) | コメント・添付ファイル |
|
||||
| 圃場単位の対象紐づけ | 工数見積・実績時間記録 |
|
||||
| 計画との紐づけ(施肥・田植え・運搬) | 完全な汎用ワークフローエンジン化 |
|
||||
| 計画画面からの TODO 生成 | 実績アプリ未実装領域の詳細実績入力 UI |
|
||||
| 完了時の実績入力画面への導線生成 | |
|
||||
| 完了済み・キャンセル済みの表示切り替え | |
|
||||
|
||||
### TODO の位置づけ
|
||||
|
||||
```
|
||||
計画(年間設計情報)
|
||||
↓ 生成
|
||||
TODO(実際に動く作業単位)
|
||||
↓ 完了
|
||||
実績(完了した事実)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## データモデル
|
||||
|
||||
### Todo(本体)
|
||||
|
||||
| フィールド | 型 | 必須 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | bigint | ✓ | PK |
|
||||
| year | integer | ✓ | 年度 |
|
||||
| title | varchar(200) | ✓ | タイトル |
|
||||
| description | text | | 説明 |
|
||||
| status | enum | ✓ | `todo / doing / done / canceled` |
|
||||
| priority | integer | ✓ | 小さいほど上位(1000刻み) |
|
||||
| due_date | date | | 期日 |
|
||||
| work_type | enum | ✓ | 作業種別(下記参照) |
|
||||
| work_subtype | varchar(30) | | トラクター作業の細別。`work_type=tractor_work` のときのみ必須(下記参照) |
|
||||
| should_link_record | boolean | ✓ | 完了時に実績連携導線を有効にするか |
|
||||
| completed_at | datetime | | 完了日時(差し戻し後も保持) |
|
||||
| canceled_at | datetime | | キャンセル日時 |
|
||||
| created_at | datetime | ✓ | |
|
||||
| updated_at | datetime | ✓ | |
|
||||
|
||||
#### ステータス遷移
|
||||
|
||||
- `todo` → `doing` → `done`(complete/ 専用エンドポイント経由)
|
||||
- `done` → `todo / doing`(差し戻し許可。completed_at は履歴として保持)
|
||||
- `canceled` への遷移は任意のタイミングで可
|
||||
|
||||
#### 作業種別(work_type)
|
||||
|
||||
MVP で採用する種別:
|
||||
|
||||
| 値 | 意味 |
|
||||
|---|---|
|
||||
| `general` | 一般(どれにも当てはまらない作業) |
|
||||
| `fertilization` | 施肥 |
|
||||
| `rice_transplant` | 田植え |
|
||||
| `delivery` | 運搬 |
|
||||
| `tractor_work` | トラクター作業(畔塗・荒代掻き・植代掻き・耕耘) |
|
||||
|
||||
将来追加(農薬散布管理アプリ実装時):
|
||||
|
||||
| 値 | 意味 |
|
||||
|---|---|
|
||||
| `pesticide` | 防除 |
|
||||
|
||||
#### work_subtype(トラクター作業の細別)
|
||||
|
||||
`work_type = tractor_work` のときのみ使用する。それ以外は `null`。
|
||||
|
||||
| 値 | 意味 |
|
||||
|---|---|
|
||||
| `levee_work` | 畔塗 |
|
||||
| `rough_harrowing` | 荒代掻き |
|
||||
| `transplant_harrowing` | 植代掻き |
|
||||
| `cultivation` | 耕耘 |
|
||||
|
||||
バリデーションルール:
|
||||
- `work_type = tractor_work` → `work_subtype` 必須
|
||||
- `work_type ≠ tractor_work` → `work_subtype` は null のみ許可
|
||||
|
||||
#### 並び順
|
||||
|
||||
- 基本は FILO(新規作成時は最上位へ)
|
||||
- 初回作成時:最上位 TODO の `priority - 1000` を割り当て
|
||||
- 一覧表示:`priority` 昇順
|
||||
- 並び替え後:表示対象全体を 1000, 2000, 3000... と再採番して保存
|
||||
- 完了・キャンセル済みも `priority` を保持
|
||||
|
||||
### TodoTargetField(対象圃場)
|
||||
|
||||
| フィールド | 型 | 必須 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | bigint | ✓ | PK |
|
||||
| todo | FK(Todo) | ✓ | CASCADE |
|
||||
| field | FK(fields.Field) | ✓ | PROTECT |
|
||||
| field_name_snapshot | varchar(100) | ✓ | 保存時点の圃場名 |
|
||||
| group_name_snapshot | varchar(50) | | 保存時点の group_name |
|
||||
| created_at | datetime | ✓ | |
|
||||
|
||||
- `unique_together = ['todo', 'field']`
|
||||
- 圃場グループは独立モデル化しない(`Field.group_name` を参照するのみ)
|
||||
- 計画に含まれる圃場の一部だけを対象にすることを許可する
|
||||
|
||||
### TodoCrop(分類補助:作物)
|
||||
|
||||
| フィールド | 型 | 必須 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | bigint | ✓ | PK |
|
||||
| todo | FK(Todo) | ✓ | CASCADE |
|
||||
| crop | FK(plans.Crop) | ✓ | PROTECT |
|
||||
| created_at | datetime | ✓ | |
|
||||
|
||||
- `unique_together = ['todo', 'crop']`
|
||||
|
||||
### TodoVariety(分類補助:品種)
|
||||
|
||||
| フィールド | 型 | 必須 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | bigint | ✓ | PK |
|
||||
| todo | FK(Todo) | ✓ | CASCADE |
|
||||
| variety | FK(plans.Variety) | ✓ | PROTECT |
|
||||
| created_at | datetime | ✓ | |
|
||||
|
||||
- `unique_together = ['todo', 'variety']`
|
||||
- 圃場が 0 件でも Crop / Variety だけの紐づけは許可(圃場未確定の準備作業など)
|
||||
|
||||
### TodoPlanLink(計画との紐づけ)
|
||||
|
||||
| フィールド | 型 | 必須 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | bigint | ✓ | PK |
|
||||
| todo | FK(Todo) | ✓ | CASCADE |
|
||||
| plan_type | enum | ✓ | `fertilization / rice_transplant / delivery` |
|
||||
| fertilization_plan | FK(fertilizer.FertilizationPlan) | | |
|
||||
| rice_transplant_plan | FK(plans.RiceTransplantPlan) | | |
|
||||
| delivery_plan | FK(分配計画モデル) | | |
|
||||
| created_at | datetime | ✓ | |
|
||||
|
||||
- 1 行に 1 種別のリンクのみ保持
|
||||
- `plan_type` に応じて対応 FK だけを埋める
|
||||
- `levee_work` は MVP では「計画リンクなしで持てる work_type」として扱う
|
||||
- 作付け計画(Plan)は TODO 生成元としては対象外
|
||||
|
||||
### TodoCompletionLink(完了時の実績連携索引)
|
||||
|
||||
| フィールド | 型 | 必須 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | bigint | ✓ | PK |
|
||||
| todo | FK(Todo) | ✓ | |
|
||||
| record_type | enum | ✓ | 実績種別(`fertilization` / `tractor_work` など) |
|
||||
| work_record | FK(workrecords.WorkRecord) | | 共通索引 |
|
||||
| spreading_session | FK(fertilizer.SpreadingSession) | | 施肥実績 |
|
||||
| tractor_work_session | FK(tractor_work.TractorWorkSession) | | トラクター作業実績 |
|
||||
| created_at | datetime | ✓ | |
|
||||
|
||||
- `todo` は OneToOne ではなく FK(1 TODO から複数実績への分割を許容)
|
||||
- 実績アプリが未実装の種別は空でよい
|
||||
|
||||
---
|
||||
|
||||
## API 仕様
|
||||
|
||||
### エンドポイント一覧
|
||||
|
||||
| メソッド | パス | 説明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/todos/` | 一覧取得 |
|
||||
| POST | `/api/todos/` | 作成 |
|
||||
| GET | `/api/todos/{id}/` | 詳細取得 |
|
||||
| PATCH | `/api/todos/{id}/` | 更新(status=done への変更は不可) |
|
||||
| DELETE | `/api/todos/{id}/` | 削除 |
|
||||
| PATCH | `/api/todos/reorder/` | 並び替え |
|
||||
| POST | `/api/todos/from-plan/` | 計画から TODO 生成 |
|
||||
| POST | `/api/todos/{id}/complete/` | 完了処理(実績連携導線を返す) |
|
||||
|
||||
### 重要な設計ルール
|
||||
|
||||
**完了処理の一本化**
|
||||
- `PATCH` での `status=done` 変更はバックエンドが拒否する
|
||||
- 完了は必ず `POST /api/todos/{id}/complete/` を通る
|
||||
- 理由:実績連携導線の生成を確実にするため。AI 実装者がセッションをまたいで実装する際のブレを防ぐ
|
||||
|
||||
**差し戻し時の挙動**
|
||||
- `done → todo/doing` は許可
|
||||
- `TodoCompletionLink` が存在する場合は、差し戻しを許可しつつ API レスポンスに警告と各実績レコードへの直リンクを返す
|
||||
- 実績レコード自体の削除は行わない(各実績アプリ側の責務)
|
||||
|
||||
**削除時の挙動**
|
||||
- `TodoCompletionLink` が存在する TODO を削除しようとした場合、警告と各実績レコードへの直リンクを返す
|
||||
- ユーザーが確認した上で削除を実行した場合は物理削除を許可する
|
||||
- `TodoCompletionLink` は TODO と一緒に削除(CASCADE)
|
||||
- 実績レコード自体は削除しない
|
||||
|
||||
### 一覧 GET `/api/todos/`
|
||||
|
||||
主なクエリパラメータ:
|
||||
|
||||
| パラメータ | デフォルト | 説明 |
|
||||
|---|---|---|
|
||||
| `status` | `todo,doing` | カンマ区切りで複数指定可 |
|
||||
| `include_closed` | `false` | true で完了・キャンセルも含む |
|
||||
| `work_type` | - | 作業種別フィルター |
|
||||
| `due` | - | `overdue / today / upcoming` |
|
||||
| `year` | - | 年度フィルター |
|
||||
|
||||
### 作成 POST `/api/todos/`
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "西田エリアの追肥",
|
||||
"description": "週内に先行実施",
|
||||
"status": "todo",
|
||||
"year": 2026,
|
||||
"due_date": "2026-04-12",
|
||||
"work_type": "fertilization",
|
||||
"should_link_record": true,
|
||||
"field_ids": [12, 18, 21],
|
||||
"crop_ids": [1],
|
||||
"variety_ids": [4],
|
||||
"plan_links": [
|
||||
{"plan_type": "fertilization", "plan_id": 8}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`plan_links` の変換:API 入力は `plan_type + plan_id` の組で受け、Serializer で対応 FK へ変換する。
|
||||
|
||||
### 並び替え PATCH `/api/todos/reorder/`
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{"id": 31, "priority": 1000},
|
||||
{"id": 27, "priority": 2000},
|
||||
{"id": 42, "priority": 3000}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 計画から TODO 生成 POST `/api/todos/from-plan/`
|
||||
|
||||
```json
|
||||
{
|
||||
"plan_type": "fertilization",
|
||||
"plan_id": 8,
|
||||
"title": "2026春肥の散布",
|
||||
"field_ids": [12, 18],
|
||||
"due_date": "2026-04-15",
|
||||
"should_link_record": true
|
||||
}
|
||||
```
|
||||
|
||||
- `field_ids` 未指定時は計画内の全圃場を初期対象にする
|
||||
- `work_type` は `plan_type` から自動補完する
|
||||
|
||||
### 完了処理 POST `/api/todos/{id}/complete/`
|
||||
|
||||
- `status=done` にする
|
||||
- `should_link_record=true` かつ対応実績アプリがある場合、関連画面へ遷移するための情報を返す
|
||||
- MVP では実績レコードの自動生成は行わず、導線情報の返却にとどめる
|
||||
|
||||
---
|
||||
|
||||
## バリデーション
|
||||
|
||||
- `done` 遷移時に `completed_at` を自動設定
|
||||
- `canceled` 遷移時に `canceled_at` を自動設定
|
||||
- `PATCH` で `status=done` を指定した場合は 400 エラーを返す
|
||||
- `field_ids` が計画外圃場を含む場合は `plan_links` が 1 件以上あるときのみエラーにする
|
||||
- `should_link_record=true` でも対応実績アプリが無い場合は保存を許可する
|
||||
- `TodoTargetField.field` は `PROTECT`(過去 TODO の対象圃場履歴を保全するため)
|
||||
- `work_type = tractor_work` の場合は `work_subtype` が必須(未指定時は 400 エラー)
|
||||
- `work_type ≠ tractor_work` の場合は `work_subtype` に値を指定した場合は 400 エラー
|
||||
|
||||
---
|
||||
|
||||
## UI 仕様
|
||||
|
||||
### 一覧画面 `/todos`
|
||||
|
||||
- デフォルト表示:todo / doing を priority 昇順で表示
|
||||
- 完了済み・キャンセル済みはフィルターで表示切り替え
|
||||
- 期限超過は赤系で強調、当日期限も強調表示
|
||||
- ドラッグ&ドロップで並び替え(難しければ矢印ボタンで代替)
|
||||
|
||||
表示カラム:タイトル / ステータス / 期日 / 作業種別 / 対象圃場数 / 紐づき計画
|
||||
|
||||
### 詳細画面 `/todos/{id}`
|
||||
|
||||
表示・編集:タイトル / 説明 / ステータス / 期日 / 作業種別 / 実績連携フラグ / 対象圃場 / 分類作物・品種 / 計画リンク
|
||||
|
||||
下部表示:実績連携先 / 完了日時 / 更新日時
|
||||
|
||||
### 作成導線
|
||||
|
||||
1. TODO 一覧から新規作成
|
||||
2. 計画詳細または一覧から TODO 生成(施肥・田植え・運搬の各計画画面)
|
||||
|
||||
---
|
||||
|
||||
## 実装ファイル構成
|
||||
|
||||
### Backend
|
||||
|
||||
```
|
||||
apps/todos/
|
||||
├── models.py # Todo, TodoTargetField, TodoCrop, TodoVariety, TodoPlanLink, TodoCompletionLink
|
||||
├── admin.py
|
||||
├── serializers.py
|
||||
├── views.py
|
||||
├── urls.py
|
||||
└── migrations/
|
||||
```
|
||||
|
||||
- `keinasystem/settings.py` に `apps.todos` を追加
|
||||
- `keinasystem/urls.py` に `/api/todos/` を追加
|
||||
|
||||
### Frontend
|
||||
|
||||
```
|
||||
frontend/src/app/todos/
|
||||
├── page.tsx # 一覧
|
||||
├── [id]/page.tsx # 詳細
|
||||
└── new/page.tsx # 作成
|
||||
```
|
||||
|
||||
### 実装順
|
||||
|
||||
1. モデル・admin・migration
|
||||
2. TODO CRUD API(一覧・詳細・作成・更新・削除)
|
||||
3. TODO 一覧・詳細 UI
|
||||
4. 並び替え API と UI
|
||||
5. 計画から TODO 生成(from-plan API + 各計画画面への導線)
|
||||
6. 完了処理 API と実績連携導線 UI
|
||||
|
||||
---
|
||||
|
||||
## 実績連携の考え方
|
||||
|
||||
### 施肥
|
||||
|
||||
`施肥計画 → 施肥TODO → 施肥実績`(SpreadingSession)の流れ。
|
||||
完了時は `SpreadingSession` 作成画面への導線を返す。対象圃場は `TodoTargetField` を初期値として渡す。
|
||||
|
||||
### トラクター作業
|
||||
|
||||
`tractor_work` 種別の TODO 完了時は `TractorWorkSession` 作成画面への導線を返す。
|
||||
`work_subtype` をクエリパラメータで渡し、作業種別セレクタの初期値として使う。
|
||||
対象圃場は `TodoTargetField` を初期値として渡す。
|
||||
|
||||
### 田植え
|
||||
|
||||
田植え実績アプリは今後実装予定。MVP では:
|
||||
- `rice_transplant` 種別の TODO を持てる
|
||||
- 完了時は「完了済みだが実績アプリ未接続」の状態も許容する
|
||||
- 将来の田植え実績導入時に `TodoCompletionLink` を拡張する
|
||||
|
||||
### 実績アプリが無い作業
|
||||
|
||||
`general` など、実績アプリに紐づかない TODO は `status=done` のみで完了とする。
|
||||
|
||||
---
|
||||
|
||||
## 未決定(実装時に判断)
|
||||
|
||||
以下は MVP 着手後に実装者が判断しながら決めてよい事項。
|
||||
|
||||
| 事項 | 方針 |
|
||||
|---|---|
|
||||
| 複数計画リンクの初回 UI | 内部構造は複数可。UI はまず 1 件中心で実装し、必要なら拡張する |
|
||||
| 並び替え対象の範囲 | フィルター中(todo/doing のみ)を再採番対象とするのが自然 |
|
||||
| 施肥完了時に渡す初期値の粒度 | SpreadingSession 作成画面の実装時に具体的な受け渡し仕様を決める |
|
||||
94
document/20_ローカルテスト環境.md
Normal file
94
document/20_ローカルテスト環境.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# ローカルテスト環境(Ubuntu PC)
|
||||
|
||||
本番同等の環境をローカルで起動し、サーバーのデータで動作確認するための手順。
|
||||
|
||||
---
|
||||
|
||||
## 構成
|
||||
|
||||
| ファイル | 用途 |
|
||||
|---------|------|
|
||||
| `docker-compose.local.yml` | 本番用Dockerfileを使用、Traefikなし、ポート直接公開 |
|
||||
| `deploy_local.sh` | ローカル環境のビルド・起動 |
|
||||
| `sync_db.sh` | サーバーのDBダンプをローカルに取り込む |
|
||||
| `.env` | 本番と同じ環境変数(git管理外) |
|
||||
|
||||
アクセス先:
|
||||
- フロントエンド: http://localhost:3000
|
||||
- バックエンドAPI: http://localhost:8000/api/
|
||||
|
||||
---
|
||||
|
||||
## 初回セットアップ
|
||||
|
||||
### 1. .env を作成
|
||||
|
||||
```bash
|
||||
cp .env.production.example .env
|
||||
# .env に本番と同じ値を設定する
|
||||
```
|
||||
|
||||
### 2. ローカル環境を起動
|
||||
|
||||
```bash
|
||||
bash deploy_local.sh
|
||||
```
|
||||
|
||||
ビルド(初回は10〜15分)→ 起動 → マイグレーションが自動実行される。
|
||||
|
||||
### 3. サーバーのDBを同期
|
||||
|
||||
**サーバー側で実行**(keinasystemユーザーで):
|
||||
```bash
|
||||
docker exec keinasystem_db pg_dump -U keinasystem keinasystem > /tmp/keinasystem_dump.sql
|
||||
```
|
||||
|
||||
**ローカル側で実行**:
|
||||
```bash
|
||||
bash sync_db.sh
|
||||
```
|
||||
|
||||
> `sync_db.sh` はリストア後に自動でマイグレーションを実行する。サーバーより新しいマイグレーションがローカルにある場合でも正しく動作する。
|
||||
|
||||
---
|
||||
|
||||
## 2回目以降の起動
|
||||
|
||||
```bash
|
||||
# 停止中の場合は起動
|
||||
docker compose -f docker-compose.local.yml up -d
|
||||
|
||||
# 停止
|
||||
docker compose -f docker-compose.local.yml down
|
||||
```
|
||||
|
||||
コードを変更した場合は再ビルドが必要:
|
||||
```bash
|
||||
bash deploy_local.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DBの再同期
|
||||
|
||||
サーバーのデータをローカルに反映したい時。
|
||||
|
||||
**サーバー側**(keinasystemユーザーで):
|
||||
```bash
|
||||
docker exec keinasystem_db pg_dump -U keinasystem keinasystem > /tmp/keinasystem_dump.sql
|
||||
```
|
||||
|
||||
**ローカル側**:
|
||||
```bash
|
||||
bash sync_db.sh
|
||||
```
|
||||
|
||||
> **注意**: ローカルのDBデータは上書きされる。ローカルで加えた変更は失われる。
|
||||
|
||||
---
|
||||
|
||||
## 注意事項
|
||||
|
||||
- `.env` は gitignore 対象(コミットしない)
|
||||
- ローカルDBは `postgres_data_local` ボリュームに保存(本番の `postgres_data` とは別)
|
||||
- `sync_db.sh` は SSH設定 `keinafarm`(`~/.ssh/config`)を使用
|
||||
@@ -1,213 +1,503 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { LogOut, Wheat, MapPin, FileText, Upload, LayoutDashboard, History, Shield, KeyRound, Cloud, Sprout, FlaskConical, Package, NotebookText, PencilLine, Construction, Tractor } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import {
|
||||
ChevronDown,
|
||||
Cloud,
|
||||
FileText,
|
||||
History,
|
||||
KeyRound,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
MapPin,
|
||||
Menu,
|
||||
NotebookText,
|
||||
Package,
|
||||
PencilLine,
|
||||
Shield,
|
||||
Sprout,
|
||||
Tractor,
|
||||
Truck,
|
||||
Upload,
|
||||
Construction,
|
||||
Wheat,
|
||||
X,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { logout } from '@/lib/api';
|
||||
|
||||
type NavItem = {
|
||||
label: string;
|
||||
href: string;
|
||||
icon?: LucideIcon;
|
||||
match?: (pathname: string) => boolean;
|
||||
};
|
||||
|
||||
type NavGroup = {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'link' | 'group';
|
||||
href?: string;
|
||||
icon?: LucideIcon;
|
||||
items?: NavItem[];
|
||||
};
|
||||
|
||||
const matchesHref = (pathname: string, href: string) =>
|
||||
pathname === href || pathname.startsWith(`${href}/`);
|
||||
|
||||
const navGroups: NavGroup[] = [
|
||||
{
|
||||
key: 'home',
|
||||
label: 'ホーム',
|
||||
type: 'link',
|
||||
href: '/dashboard',
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
key: 'planning',
|
||||
label: '計画',
|
||||
type: 'group',
|
||||
icon: Wheat,
|
||||
items: [
|
||||
{ label: '作付け計画', href: '/allocation', icon: Wheat },
|
||||
{
|
||||
label: '施肥計画',
|
||||
href: '/fertilizer',
|
||||
icon: Sprout,
|
||||
match: (pathname) =>
|
||||
matchesHref(pathname, '/fertilizer') &&
|
||||
!matchesHref(pathname, '/fertilizer/spreading') &&
|
||||
!matchesHref(pathname, '/fertilizer/masters'),
|
||||
},
|
||||
{ label: '田植え計画', href: '/rice-transplant', icon: Tractor },
|
||||
{ label: '運搬計画', href: '/distribution', icon: Truck },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'records',
|
||||
label: '実績',
|
||||
type: 'group',
|
||||
icon: NotebookText,
|
||||
items: [
|
||||
{
|
||||
label: '散布実績',
|
||||
href: '/fertilizer/spreading',
|
||||
icon: PencilLine,
|
||||
},
|
||||
{ label: '畔塗記録', href: '/levee-work', icon: Construction },
|
||||
{ label: '作業記録', href: '/workrecords', icon: NotebookText },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'masters',
|
||||
label: 'マスター',
|
||||
type: 'group',
|
||||
icon: Package,
|
||||
items: [
|
||||
{ label: '圃場管理', href: '/fields', icon: MapPin },
|
||||
{
|
||||
label: '資材マスタ',
|
||||
href: '/materials/masters',
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
label: '肥料マスタ',
|
||||
href: '/fertilizer/masters',
|
||||
icon: Sprout,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'support',
|
||||
label: '帳票・連携',
|
||||
type: 'group',
|
||||
icon: FileText,
|
||||
items: [
|
||||
{
|
||||
label: '在庫管理',
|
||||
href: '/materials',
|
||||
icon: Package,
|
||||
match: (pathname) =>
|
||||
matchesHref(pathname, '/materials') && !matchesHref(pathname, '/materials/masters'),
|
||||
},
|
||||
{ label: '帳票出力', href: '/reports', icon: FileText },
|
||||
{ label: 'データ取込', href: '/import', icon: Upload },
|
||||
{ label: '気象', href: '/weather', icon: Cloud },
|
||||
{ label: 'メール履歴', href: '/mail/history', icon: History },
|
||||
{ label: 'メールルール', href: '/mail/rules', icon: Shield },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const userActions: NavItem[] = [
|
||||
{ label: 'パスワード変更', href: '/settings/password', icon: KeyRound },
|
||||
];
|
||||
|
||||
export default function Navbar() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const navRef = useRef<HTMLElement>(null);
|
||||
const [openDesktopGroup, setOpenDesktopGroup] = useState<string | null>(null);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [openMobileGroups, setOpenMobileGroups] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePointerDown = (event: MouseEvent) => {
|
||||
if (!navRef.current?.contains(event.target as Node)) {
|
||||
setOpenDesktopGroup(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setOpenDesktopGroup(null);
|
||||
setMobileMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handlePointerDown);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handlePointerDown);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setOpenDesktopGroup(null);
|
||||
setMobileMenuOpen(false);
|
||||
setOpenMobileGroups((prev) => {
|
||||
const activeKey = getActiveGroupKey(pathname);
|
||||
if (!activeKey) return prev;
|
||||
return prev.includes(activeKey) ? prev : [activeKey];
|
||||
});
|
||||
}, [pathname]);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
};
|
||||
|
||||
const isActive = (path: string) => pathname === path;
|
||||
const navigateTo = (href: string) => {
|
||||
setOpenDesktopGroup(null);
|
||||
setMobileMenuOpen(false);
|
||||
router.push(href);
|
||||
};
|
||||
|
||||
const toggleDesktopGroup = (key: string) => {
|
||||
setOpenDesktopGroup((prev) => (prev === key ? null : key));
|
||||
};
|
||||
|
||||
const toggleMobileGroup = (key: string) => {
|
||||
setOpenMobileGroups((prev) =>
|
||||
prev.includes(key) ? prev.filter((groupKey) => groupKey !== key) : [...prev, key]
|
||||
);
|
||||
};
|
||||
|
||||
const toggleMobileMenu = () => {
|
||||
if (!mobileMenuOpen) {
|
||||
const activeKey = getActiveGroupKey(pathname);
|
||||
setOpenMobileGroups(activeKey ? [activeKey] : []);
|
||||
}
|
||||
setMobileMenuOpen((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex items-center space-x-8">
|
||||
<button onClick={() => router.push('/dashboard')} className="text-xl font-bold text-green-700 hover:text-green-800 transition-colors">
|
||||
<nav ref={navRef} className="border-b border-gray-200 bg-white shadow-sm">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
<div className="flex items-center gap-4 lg:gap-8">
|
||||
<button
|
||||
onClick={() => navigateTo('/dashboard')}
|
||||
className="text-lg font-bold text-green-700 transition-colors hover:text-green-800 sm:text-xl"
|
||||
>
|
||||
KeinaSystem
|
||||
</button>
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => router.push('/dashboard')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
isActive('/dashboard')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4 mr-2" />
|
||||
ホーム
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/allocation')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
isActive('/allocation')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<Wheat className="h-4 w-4 mr-2" />
|
||||
作付け計画
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/fields')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
isActive('/fields') || pathname?.startsWith('/fields/')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<MapPin className="h-4 w-4 mr-2" />
|
||||
圃場管理
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/reports')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
isActive('/reports') || pathname?.startsWith('/reports/')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
帳票出力
|
||||
</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>
|
||||
<button
|
||||
onClick={() => router.push('/mail/history')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
isActive('/mail/history')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<History className="h-4 w-4 mr-2" />
|
||||
メール履歴
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/mail/rules')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
isActive('/mail/rules')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<Shield className="h-4 w-4 mr-2" />
|
||||
メールルール
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/weather')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
isActive('/weather')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<Cloud className="h-4 w-4 mr-2" />
|
||||
気象
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/fertilizer')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
pathname?.startsWith('/fertilizer') && !pathname?.startsWith('/fertilizer/spreading')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<Sprout className="h-4 w-4 mr-2" />
|
||||
施肥計画
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/rice-transplant')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
pathname?.startsWith('/rice-transplant')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<Tractor className="h-4 w-4 mr-2" />
|
||||
田植え計画
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/fertilizer/spreading')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
pathname?.startsWith('/fertilizer/spreading')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<PencilLine className="h-4 w-4 mr-2" />
|
||||
散布実績
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/distribution')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
pathname?.startsWith('/distribution')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<FlaskConical className="h-4 w-4 mr-2" />
|
||||
運搬計画
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/levee-work')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
pathname?.startsWith('/levee-work')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<Construction className="h-4 w-4 mr-2" />
|
||||
畔塗記録
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/materials')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
pathname?.startsWith('/materials')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<Package className="h-4 w-4 mr-2" />
|
||||
在庫管理
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/workrecords')}
|
||||
className={`flex items-center px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
pathname?.startsWith('/workrecords')
|
||||
? 'text-green-700 bg-green-50'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<NotebookText className="h-4 w-4 mr-2" />
|
||||
作業記録
|
||||
</button>
|
||||
|
||||
<div className="hidden items-center gap-2 lg:flex">
|
||||
{navGroups.map((group) =>
|
||||
group.type === 'link' ? (
|
||||
<DesktopLinkButton
|
||||
key={group.key}
|
||||
group={group}
|
||||
pathname={pathname}
|
||||
onNavigate={navigateTo}
|
||||
/>
|
||||
) : (
|
||||
<DesktopGroupButton
|
||||
key={group.key}
|
||||
group={group}
|
||||
isOpen={openDesktopGroup === group.key}
|
||||
pathname={pathname}
|
||||
onNavigate={navigateTo}
|
||||
onToggle={toggleDesktopGroup}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
|
||||
<div className="hidden items-center gap-1 lg:flex">
|
||||
{userActions.map((item) => (
|
||||
<button
|
||||
onClick={() => router.push('/settings/password')}
|
||||
className="flex items-center px-3 py-2 text-sm text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-md transition-colors"
|
||||
title="パスワード変更"
|
||||
key={item.href}
|
||||
onClick={() => navigateTo(item.href)}
|
||||
className={`rounded-md px-3 py-2 text-sm transition-colors ${
|
||||
isItemActive(item, pathname)
|
||||
? 'bg-green-50 text-green-700'
|
||||
: 'text-gray-500 hover:bg-gray-100 hover:text-gray-700'
|
||||
}`}
|
||||
title={item.label}
|
||||
>
|
||||
<KeyRound className="h-4 w-4" />
|
||||
{item.icon ? <item.icon className="h-4 w-4" /> : item.label}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:text-gray-900 hover:bg-gray-100 rounded-md transition-colors"
|
||||
className="flex items-center rounded-md px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 hover:text-gray-900"
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-2" />
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
ログアウト
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 lg:hidden">
|
||||
<button
|
||||
onClick={() => navigateTo('/settings/password')}
|
||||
className={`rounded-md p-2 transition-colors ${
|
||||
isItemActive(userActions[0], pathname)
|
||||
? 'bg-green-50 text-green-700'
|
||||
: 'text-gray-500 hover:bg-gray-100 hover:text-gray-700'
|
||||
}`}
|
||||
title="パスワード変更"
|
||||
>
|
||||
<KeyRound className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleMobileMenu}
|
||||
className="rounded-md p-2 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
|
||||
aria-expanded={mobileMenuOpen}
|
||||
aria-label="メニューを開く"
|
||||
>
|
||||
{mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mobileMenuOpen && (
|
||||
<div className="border-t border-gray-200 py-3 lg:hidden">
|
||||
<div className="space-y-1">
|
||||
{navGroups.map((group) =>
|
||||
group.type === 'link' ? (
|
||||
<MobileLinkButton
|
||||
key={group.key}
|
||||
group={group}
|
||||
pathname={pathname}
|
||||
onNavigate={navigateTo}
|
||||
/>
|
||||
) : (
|
||||
<MobileGroupButton
|
||||
key={group.key}
|
||||
group={group}
|
||||
isOpen={openMobileGroups.includes(group.key)}
|
||||
pathname={pathname}
|
||||
onNavigate={navigateTo}
|
||||
onToggle={toggleMobileGroup}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="mt-3 flex w-full items-center rounded-lg px-3 py-3 text-sm text-gray-700 transition-colors hover:bg-gray-100 hover:text-gray-900"
|
||||
>
|
||||
<LogOut className="mr-3 h-4 w-4" />
|
||||
ログアウト
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function DesktopLinkButton({
|
||||
group,
|
||||
pathname,
|
||||
onNavigate,
|
||||
}: {
|
||||
group: NavGroup;
|
||||
pathname: string;
|
||||
onNavigate: (href: string) => void;
|
||||
}) {
|
||||
const active = isGroupActive(group, pathname);
|
||||
const Icon = group.icon;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => group.href && onNavigate(group.href)}
|
||||
className={`flex items-center rounded-md px-3 py-2 text-sm transition-colors ${
|
||||
active ? 'bg-green-50 text-green-700' : 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{Icon ? <Icon className="mr-2 h-4 w-4" /> : null}
|
||||
{group.label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function DesktopGroupButton({
|
||||
group,
|
||||
isOpen,
|
||||
pathname,
|
||||
onNavigate,
|
||||
onToggle,
|
||||
}: {
|
||||
group: NavGroup;
|
||||
isOpen: boolean;
|
||||
pathname: string;
|
||||
onNavigate: (href: string) => void;
|
||||
onToggle: (key: string) => void;
|
||||
}) {
|
||||
const active = isGroupActive(group, pathname);
|
||||
const Icon = group.icon;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => onToggle(group.key)}
|
||||
className={`flex items-center rounded-md px-3 py-2 text-sm transition-colors ${
|
||||
active || isOpen
|
||||
? 'bg-green-50 text-green-700'
|
||||
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'
|
||||
}`}
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
{Icon ? <Icon className="mr-2 h-4 w-4" /> : null}
|
||||
{group.label}
|
||||
<ChevronDown className={`ml-2 h-4 w-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{isOpen && group.items ? (
|
||||
<div className="absolute left-0 top-full z-20 mt-2 w-64 rounded-xl border border-gray-200 bg-white p-2 shadow-lg">
|
||||
{group.items.map((item) => (
|
||||
<button
|
||||
key={item.href}
|
||||
onClick={() => onNavigate(item.href)}
|
||||
className={`flex w-full items-center rounded-lg px-3 py-2 text-left text-sm transition-colors ${
|
||||
isItemActive(item, pathname)
|
||||
? 'bg-green-50 text-green-700'
|
||||
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{item.icon ? <item.icon className="mr-3 h-4 w-4" /> : null}
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileLinkButton({
|
||||
group,
|
||||
pathname,
|
||||
onNavigate,
|
||||
}: {
|
||||
group: NavGroup;
|
||||
pathname: string;
|
||||
onNavigate: (href: string) => void;
|
||||
}) {
|
||||
const active = isGroupActive(group, pathname);
|
||||
const Icon = group.icon;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => group.href && onNavigate(group.href)}
|
||||
className={`flex w-full items-center rounded-lg px-3 py-3 text-left text-sm transition-colors ${
|
||||
active ? 'bg-green-50 text-green-700' : 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{Icon ? <Icon className="mr-3 h-4 w-4" /> : null}
|
||||
{group.label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileGroupButton({
|
||||
group,
|
||||
isOpen,
|
||||
pathname,
|
||||
onNavigate,
|
||||
onToggle,
|
||||
}: {
|
||||
group: NavGroup;
|
||||
isOpen: boolean;
|
||||
pathname: string;
|
||||
onNavigate: (href: string) => void;
|
||||
onToggle: (key: string) => void;
|
||||
}) {
|
||||
const active = isGroupActive(group, pathname);
|
||||
const Icon = group.icon;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200">
|
||||
<button
|
||||
onClick={() => onToggle(group.key)}
|
||||
className={`flex w-full items-center justify-between rounded-lg px-3 py-3 text-left text-sm transition-colors ${
|
||||
active || isOpen
|
||||
? 'bg-green-50 text-green-700'
|
||||
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'
|
||||
}`}
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
{Icon ? <Icon className="mr-3 h-4 w-4" /> : null}
|
||||
{group.label}
|
||||
</span>
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{isOpen && group.items ? (
|
||||
<div className="space-y-1 border-t border-gray-200 px-2 py-2">
|
||||
{group.items.map((item) => (
|
||||
<button
|
||||
key={item.href}
|
||||
onClick={() => onNavigate(item.href)}
|
||||
className={`flex w-full items-center rounded-lg px-3 py-2.5 text-left text-sm transition-colors ${
|
||||
isItemActive(item, pathname)
|
||||
? 'bg-green-50 text-green-700'
|
||||
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{item.icon ? <item.icon className="mr-3 h-4 w-4" /> : null}
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function isGroupActive(group: NavGroup, pathname: string) {
|
||||
if (group.type === 'link') {
|
||||
return group.href ? matchesHref(pathname, group.href) : false;
|
||||
}
|
||||
|
||||
return group.items?.some((item) => isItemActive(item, pathname)) ?? false;
|
||||
}
|
||||
|
||||
function isItemActive(item: NavItem, pathname: string) {
|
||||
if (item.match) {
|
||||
return item.match(pathname);
|
||||
}
|
||||
return matchesHref(pathname, item.href);
|
||||
}
|
||||
|
||||
function getActiveGroupKey(pathname: string) {
|
||||
return navGroups.find((group) => isGroupActive(group, pathname))?.key ?? null;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
48
sync_db.sh
Executable file
48
sync_db.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# サーバーのDBをローカルに同期するスクリプト
|
||||
#
|
||||
# 事前準備(サーバー側でkeinasystemユーザーとして実行):
|
||||
# docker exec keinasystem_db pg_dump -U keinasystem keinasystem > /tmp/keinasystem_dump.sql
|
||||
#
|
||||
# 使用: bash sync_db.sh
|
||||
set -e
|
||||
|
||||
REMOTE_HOST="keinafarm"
|
||||
LOCAL_DUMP="/tmp/keinasystem_dump.sql"
|
||||
|
||||
echo "=== DBSync: サーバー → ローカル ==="
|
||||
|
||||
# 1. サーバーからdumpファイルをscpで取得
|
||||
echo "[1/4] サーバーからダンプファイルを取得..."
|
||||
scp "$REMOTE_HOST:/tmp/keinasystem_dump.sql" "$LOCAL_DUMP"
|
||||
echo " → ダンプ取得完了: $LOCAL_DUMP ($(du -sh $LOCAL_DUMP | cut -f1))"
|
||||
|
||||
# 2. ローカルのDBコンテナが起動しているか確認
|
||||
echo "[2/4] ローカルDBコンテナを確認..."
|
||||
if ! docker compose -f docker-compose.local.yml ps db 2>/dev/null | grep -q "running"; then
|
||||
echo " → ローカルDBコンテナが起動していません。起動します..."
|
||||
docker compose -f docker-compose.local.yml up -d db
|
||||
echo " → DB起動待機中..."
|
||||
sleep 10
|
||||
fi
|
||||
|
||||
# 3. 既存データをドロップして復元
|
||||
echo "[3/4] ローカルDBにリストア(既存データをリセット)..."
|
||||
# DBを一旦削除して再作成してからリストア
|
||||
docker compose -f docker-compose.local.yml exec -T db \
|
||||
psql -U keinasystem -d postgres -c "DROP DATABASE IF EXISTS keinasystem;" --quiet
|
||||
docker compose -f docker-compose.local.yml exec -T db \
|
||||
psql -U keinasystem -d postgres -c "CREATE DATABASE keinasystem OWNER keinasystem;" --quiet
|
||||
cat "$LOCAL_DUMP" | docker compose -f docker-compose.local.yml exec -T db \
|
||||
psql -U keinasystem -d keinasystem --quiet
|
||||
echo " → リストア完了"
|
||||
|
||||
# クリーンアップ
|
||||
rm -f "$LOCAL_DUMP"
|
||||
|
||||
# 4. マイグレーション(サーバーより新しいマイグレーションを適用)
|
||||
echo "[4/4] マイグレーション実行..."
|
||||
docker compose -f docker-compose.local.yml exec backend python manage.py migrate
|
||||
|
||||
echo ""
|
||||
echo "=== 同期完了 ==="
|
||||
4
test-results/.last-run.json
Normal file
4
test-results/.last-run.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": []
|
||||
}
|
||||
713
改善案/TODO管理機能仕様書案.md
Normal file
713
改善案/TODO管理機能仕様書案.md
Normal file
@@ -0,0 +1,713 @@
|
||||
# TODO管理機能仕様書案
|
||||
|
||||
> 作成日: 2026-04-09
|
||||
> 最終更新: 2026-04-09
|
||||
> 対象プロジェクト: `keinasystem`
|
||||
> 対象 Issue: `akira/keinasystem#17`
|
||||
> 位置づけ: 実装前ドラフト(レビュー反映版)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概要
|
||||
|
||||
繁忙期の作業を「どれから手を付けるか」の観点で整理するため、Redmine チケットライクな TODO 管理機能を追加する。
|
||||
|
||||
本機能は単なるメモではなく、以下の中間レイヤーとして位置付ける。
|
||||
|
||||
- 計画
|
||||
- TODO
|
||||
- 実績
|
||||
|
||||
将来的には、作付け計画を除く各種計画について、`計画 -> TODO -> 実績` の流れに挟める構造を目指す。
|
||||
ただし MVP では、まず TODO 管理の基本機能、対象圃場の管理、計画との紐づけ、完了時の実績連携導線を整備する。
|
||||
|
||||
---
|
||||
|
||||
## 2. 背景
|
||||
|
||||
現状は施肥計画、田植え計画、運搬計画などの個別機能はあるが、「今日やること」「今週先に処理すべきこと」を横断的に管理する仕組みがない。
|
||||
|
||||
そのため、繁忙期には以下の問題が起こりやすい。
|
||||
|
||||
- 作業の優先順位が頭の中や紙メモに依存する
|
||||
- 計画の一部だけを先に実行したい場合に管理しづらい
|
||||
- 実績入力までの間に「作業待ち」「着手中」の状態を置けない
|
||||
- 将来追加される作業系機能を共通の入口で扱えない
|
||||
|
||||
TODO 管理を導入し、計画単位ではなく「実際に動く作業単位」で優先順位と進行状態を管理できるようにする。
|
||||
|
||||
---
|
||||
|
||||
## 3. 目的
|
||||
|
||||
### 3.1 目指す状態
|
||||
|
||||
- 未着手・進行中の作業を優先順で一覧できる
|
||||
- TODO は計画に紐づくものと、独立したものの両方を扱える
|
||||
- 計画に紐づく TODO では、計画全体ではなく一部圃場だけを対象にできる
|
||||
- 完了時に、必要なものは実績系アプリへ連携できる
|
||||
- 将来増える作業系アプリでも同じ TODO 基盤を使える
|
||||
|
||||
### 3.2 今回の対象
|
||||
|
||||
- Django 新規アプリ `apps/todos`
|
||||
- Next.js 画面 `frontend/src/app/todos`
|
||||
- REST API `/api/todos/`
|
||||
- 計画画面からの TODO 生成導線
|
||||
|
||||
### 3.3 今回やらないこと
|
||||
|
||||
- 期日通知、リマインダー、メール通知
|
||||
- 複数ユーザー割り当て
|
||||
- コメント、添付ファイル
|
||||
- 工数見積、実績時間記録
|
||||
- 完全な汎用ワークフローエンジン化
|
||||
|
||||
---
|
||||
|
||||
## 4. 基本方針
|
||||
|
||||
### 4.1 TODO の位置づけ
|
||||
|
||||
TODO は「作業指示」兼「実行待ちキュー」として扱う。
|
||||
|
||||
- 計画は年間またはまとまり単位の設計情報
|
||||
- TODO は実際に動く単位の作業
|
||||
- 実績は実際に完了した事実
|
||||
|
||||
### 4.2 計画との関係
|
||||
|
||||
- 1 計画に対して複数 TODO を紐づけられる
|
||||
- 1 TODO は複数計画を参照できる
|
||||
- ただし TODO の実際の対象圃場は TODO 側で明示管理する
|
||||
- 計画に含まれる圃場の一部だけを TODO 対象にすることを許可する
|
||||
|
||||
### 4.3 実績との関係
|
||||
|
||||
- TODO 完了時に、実績アプリを持つ作業は実績生成の入口にする
|
||||
- ただし、すべての TODO が実績アプリを持つとは限らない
|
||||
- 計画なし TODO、実績なし TODO も許容する
|
||||
|
||||
### 4.4 圃場グループの扱い
|
||||
|
||||
圃場グループは独立モデル化しない。
|
||||
既存の `Field.group_name` を参照用の属性として扱うにとどめ、TODO の正式な対象管理は圃場単位で保持する。
|
||||
|
||||
理由:
|
||||
|
||||
- 現状のデータモデルに独立したグループモデルが存在しない
|
||||
- TODO 完了後に履歴の再現性を保つには、最終的に対象圃場を確定保持した方が安全
|
||||
|
||||
---
|
||||
|
||||
## 5. 機能スコープ
|
||||
|
||||
### 5.1 IN
|
||||
|
||||
- TODO の作成、編集、削除
|
||||
- ステータス管理
|
||||
- 優先順位管理
|
||||
- 圃場単位の対象紐づけ
|
||||
- 作物、品種の補助的な分類紐づけ
|
||||
- 計画との紐づけ
|
||||
- 計画画面から TODO を生成
|
||||
- 完了済み、キャンセル済みの表示切り替え
|
||||
- 期日の強調表示
|
||||
- 並び替え API
|
||||
|
||||
### 5.2 OUT
|
||||
|
||||
- 通知
|
||||
- 担当者管理
|
||||
- 承認フロー
|
||||
- 複数段階ステータス
|
||||
- 実績アプリ未実装領域の詳細実績入力 UI
|
||||
|
||||
---
|
||||
|
||||
## 6. 用語整理
|
||||
|
||||
| 用語 | 意味 |
|
||||
|---|---|
|
||||
| TODO | 実際に着手・進行・完了する作業単位 |
|
||||
| 計画リンク | TODO が参照する施肥計画、田植え計画など |
|
||||
| 対象圃場 | その TODO で実際に作業対象となる圃場 |
|
||||
| 実績連携 | TODO 完了時に各実績アプリへ情報を渡すこと |
|
||||
|
||||
---
|
||||
|
||||
## 7. データモデル方針
|
||||
|
||||
### 7.1 Todo
|
||||
|
||||
TODO 本体。
|
||||
|
||||
| フィールド | 型 | 必須 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | bigint | ✓ | PK |
|
||||
| year | integer | ✓ | 年度 |
|
||||
| title | varchar(200) | ✓ | タイトル |
|
||||
| description | text | | 説明 |
|
||||
| status | enum | ✓ | `todo / doing / done / canceled` |
|
||||
| priority | integer | ✓ | 小さいほど上位 |
|
||||
| due_date | date | | 期日 |
|
||||
| work_type | enum | ✓ | 作業種別 |
|
||||
| should_link_record | boolean | ✓ | 完了時に実績連携導線を有効にするか |
|
||||
| completed_at | datetime | | 完了日時 |
|
||||
| canceled_at | datetime | | キャンセル日時 |
|
||||
| created_at | datetime | ✓ | |
|
||||
| updated_at | datetime | ✓ | |
|
||||
|
||||
### 7.1.1 ステータス
|
||||
|
||||
- `todo`: 未着手
|
||||
- `doing`: 進行中
|
||||
- `done`: 完了
|
||||
- `canceled`: キャンセル
|
||||
|
||||
### 7.1.2 並び順
|
||||
|
||||
- 基本は FILO とする
|
||||
- 新規作成時は最上位へ入る
|
||||
- `priority` は 1000 刻みの整数で保存する
|
||||
- 初回作成時は最上位 TODO の `priority - 1000` を新規 TODO に割り当てる
|
||||
- 一覧では `priority` 昇順で表示する
|
||||
- ユーザーが並び替えた後は、表示順に 1000, 2000, 3000... と振り直して保存する
|
||||
- 既存レコードの一括インクリメントや小数 priority は採用しない
|
||||
- 完了、キャンセル済みも `priority` は保持する
|
||||
- 一覧のデフォルト表示は `todo / doing` のみを `priority` 昇順で表示する
|
||||
|
||||
補足:
|
||||
|
||||
- 1000 刻みは API の中間挿入余地ではなく、再採番時の可読性のために採用する
|
||||
- 並び順変更は常に表示対象全体を受け取って再採番する前提とする
|
||||
|
||||
### 7.1.3 作業種別
|
||||
|
||||
作業種別は「計画に対応するもの」と「計画に対応しないもの」の両方を含める。
|
||||
|
||||
初期採用(MVP):
|
||||
|
||||
- `general`: 一般
|
||||
- `fertilization`: 施肥
|
||||
- `rice_transplant`: 田植え
|
||||
- `delivery`: 運搬
|
||||
- `levee_work`: 畔塗
|
||||
|
||||
将来追加(アプリ実装時):
|
||||
|
||||
- `pesticide`: 防除(農薬散布管理アプリ実装時に追加)
|
||||
|
||||
補足:
|
||||
|
||||
- `general` はどれにも当てはまらない作業用に必須
|
||||
- 現行アプリに対応する種別のみで始め、新しい計画機能追加時に `work_type` を拡張する
|
||||
|
||||
### 7.2 TodoTargetField
|
||||
|
||||
TODO が実際に対象とする圃場。
|
||||
|
||||
| フィールド | 型 | 必須 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | bigint | ✓ | PK |
|
||||
| todo | FK(Todo) | ✓ | CASCADE |
|
||||
| field | FK(fields.Field) | ✓ | PROTECT |
|
||||
| field_name_snapshot | varchar(100) | ✓ | 保存時点の圃場名 |
|
||||
| group_name_snapshot | varchar(50) | | 保存時点の group_name |
|
||||
| created_at | datetime | ✓ | |
|
||||
|
||||
- `unique_together = ['todo', 'field']`
|
||||
|
||||
方針:
|
||||
|
||||
- TODO の対象管理は最終的に圃場単位で保持する
|
||||
- グループ、作物、品種から一括選択する UI は許可する
|
||||
- ただし保存時は対象圃場へ展開して保持する
|
||||
|
||||
### 7.3 TodoCrop / TodoVariety
|
||||
|
||||
TODO の分類補助用。
|
||||
|
||||
#### TodoCrop
|
||||
|
||||
| フィールド | 型 | 必須 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | bigint | ✓ | PK |
|
||||
| todo | FK(Todo) | ✓ | CASCADE |
|
||||
| crop | FK(plans.Crop) | ✓ | PROTECT |
|
||||
| created_at | datetime | ✓ | |
|
||||
|
||||
- `unique_together = ['todo', 'crop']`
|
||||
|
||||
#### TodoVariety
|
||||
|
||||
| フィールド | 型 | 必須 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | bigint | ✓ | PK |
|
||||
| todo | FK(Todo) | ✓ | CASCADE |
|
||||
| variety | FK(plans.Variety) | ✓ | PROTECT |
|
||||
| created_at | datetime | ✓ | |
|
||||
|
||||
- `unique_together = ['todo', 'variety']`
|
||||
|
||||
注意:
|
||||
|
||||
- 対象圃場の実体は `TodoTargetField` を正とする
|
||||
- `Crop` や `Variety` だけ紐づいていて圃場が 0 件の TODO は許可する
|
||||
- これにより、圃場未確定の準備作業も登録できる
|
||||
|
||||
### 7.4 TodoPlanLink
|
||||
|
||||
TODO と既存計画との紐づけ。
|
||||
|
||||
| フィールド | 型 | 必須 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | bigint | ✓ | PK |
|
||||
| todo | FK(Todo) | ✓ | CASCADE |
|
||||
| plan_type | enum | ✓ | 計画種別 |
|
||||
| fertilization_plan | FK | | 施肥計画 |
|
||||
| rice_transplant_plan | FK | | 田植え計画 |
|
||||
| delivery_plan | FK | | 運搬計画 |
|
||||
| created_at | datetime | ✓ | |
|
||||
|
||||
方針:
|
||||
|
||||
- 1 行に 1 種別のリンクだけを保持する
|
||||
- `plan_type` に応じて対応する FK だけを埋める
|
||||
- MVP は汎用 `GenericForeignKey` を使わず、明示 FK を優先する
|
||||
- 理由は API と serializer を単純に保ちやすいため
|
||||
|
||||
初期対象:
|
||||
|
||||
- 施肥計画 `FertilizationPlan`
|
||||
- 田植え計画 `RiceTransplantPlan`
|
||||
- 運搬計画 `DeliveryPlan`
|
||||
- 畔塗 `levee_work` は MVP では「計画リンクなしで持てる work_type」として扱う
|
||||
- 将来、畔塗に計画モデルが導入された時点で `TodoPlanLink` に追加する
|
||||
|
||||
補足:
|
||||
|
||||
- 作付け計画 `Plan` は「年内の計画情報」であり、TODO 生成元としては必須ではない
|
||||
- 当面は Issue 回答に合わせ、`作付け計画以外のすべての計画` を TODO の対象候補とする
|
||||
|
||||
### 7.5 TodoCompletionLink
|
||||
|
||||
完了時の実績連携先を記録する索引。
|
||||
|
||||
| フィールド | 型 | 必須 | 説明 |
|
||||
|---|---|---|---|
|
||||
| id | bigint | ✓ | PK |
|
||||
| todo | FK(Todo) | ✓ | TODO |
|
||||
| record_type | enum | ✓ | 実績種別 |
|
||||
| work_record | FK(workrecords.WorkRecord) | | 共通索引 |
|
||||
| spreading_session | FK(fertilizer.SpreadingSession) | | 施肥実績 |
|
||||
| rice_transplant_record_id | 将来 | | 田植え実績 |
|
||||
| created_at | datetime | ✓ | |
|
||||
|
||||
方針:
|
||||
|
||||
- 完了時に何へ連携したかを TODO 側から追えるようにする
|
||||
- `todo` は OneToOne に固定せず FK とする
|
||||
- 理由は 1 TODO から複数実績へ分割される可能性を残すため
|
||||
- 実績アプリが未実装の種別は空でよい
|
||||
- 将来の田植え実績導入時に拡張できる形にする
|
||||
|
||||
---
|
||||
|
||||
## 8. API 仕様案
|
||||
|
||||
### 8.1 一覧
|
||||
|
||||
- `GET /api/todos/`
|
||||
|
||||
主な query:
|
||||
|
||||
- `status=todo,doing`
|
||||
- `include_closed=true|false`
|
||||
- `work_type=...`
|
||||
- `due=overdue|today|upcoming`
|
||||
- `year=2026`
|
||||
|
||||
デフォルト:
|
||||
|
||||
- `include_closed=false`
|
||||
- `status=todo,doing`
|
||||
- `priority` 昇順
|
||||
|
||||
### 8.2 詳細取得
|
||||
|
||||
- `GET /api/todos/{id}/`
|
||||
|
||||
返却内容:
|
||||
|
||||
- TODO 本体
|
||||
- 対象圃場
|
||||
- 作物、品種
|
||||
- 計画リンク
|
||||
- 完了連携状況
|
||||
|
||||
### 8.3 作成
|
||||
|
||||
- `POST /api/todos/`
|
||||
|
||||
作成 payload 例:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "西田エリアの追肥",
|
||||
"description": "週内に先行実施",
|
||||
"status": "todo",
|
||||
"year": 2026,
|
||||
"due_date": "2026-04-12",
|
||||
"work_type": "fertilization",
|
||||
"should_link_record": true,
|
||||
"field_ids": [12, 18, 21],
|
||||
"crop_ids": [1],
|
||||
"variety_ids": [4],
|
||||
"plan_links": [
|
||||
{"plan_type": "fertilization", "plan_id": 8}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`plan_links` の吸収方針:
|
||||
|
||||
- API 入力は `plan_type + plan_id` の組で受ける
|
||||
- Serializer で `plan_type` を見て対応 FK へ変換する
|
||||
- 例:
|
||||
- `fertilization` -> `fertilization_plan_id`
|
||||
- `rice_transplant` -> `rice_transplant_plan_id`
|
||||
- `delivery` -> `delivery_plan_id`
|
||||
- DB 返却時は、フロントエンド向けに再び `plan_type + plan_id + plan_label` の形へ正規化して返す
|
||||
|
||||
### 8.4 更新
|
||||
|
||||
- `PATCH /api/todos/{id}/`
|
||||
|
||||
更新可能項目:
|
||||
|
||||
- タイトル
|
||||
- 説明
|
||||
- ステータス(`todo` / `doing` / `canceled` のみ。`done` への変更は不可)
|
||||
- 期日
|
||||
- 作業種別
|
||||
- 実績連携フラグ
|
||||
- 対象圃場
|
||||
- 分類
|
||||
- 計画リンク
|
||||
|
||||
**注意**: `status=done` への変更は `PATCH` では受け付けない。完了は必ず `POST /api/todos/{id}/complete/` を使うこと。理由は、完了時に実績連携導線の生成が必要なため、入口を一本化して実装のブレを防ぐ。
|
||||
|
||||
### 8.5 削除
|
||||
|
||||
- `DELETE /api/todos/{id}/`
|
||||
|
||||
ルール:
|
||||
|
||||
- `TodoCompletionLink` が存在する TODO を削除しようとした場合、警告と各実績レコードへの直リンクを返す
|
||||
- ユーザーが確認した上で削除を実行した場合は物理削除を許可する
|
||||
- `TodoCompletionLink` は TODO と一緒に削除する(CASCADE)
|
||||
- 実績レコード自体は削除しない(各実績アプリ側の責務)
|
||||
|
||||
### 8.6 並び替え
|
||||
|
||||
- `PATCH /api/todos/reorder/`
|
||||
|
||||
payload 例:
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{"id": 31, "priority": 1000},
|
||||
{"id": 27, "priority": 2000},
|
||||
{"id": 42, "priority": 3000}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
方針:
|
||||
|
||||
- 一括更新で保存する
|
||||
- DnD が難しい場合も、矢印移動 UI から同 API を呼ぶ
|
||||
|
||||
### 8.7 計画から TODO 生成
|
||||
|
||||
- `POST /api/todos/from-plan/`
|
||||
|
||||
payload 例:
|
||||
|
||||
```json
|
||||
{
|
||||
"plan_type": "fertilization",
|
||||
"plan_id": 8,
|
||||
"title": "2026春肥の散布",
|
||||
"field_ids": [12, 18],
|
||||
"due_date": "2026-04-15",
|
||||
"should_link_record": true
|
||||
}
|
||||
```
|
||||
|
||||
生成ルール:
|
||||
|
||||
- 既存計画をリンクする
|
||||
- `field_ids` 未指定時は計画内の全圃場を初期対象にする
|
||||
- `work_type` は `plan_type` から自動補完する
|
||||
- タイトルは自動生成可能にする
|
||||
|
||||
### 8.8 完了処理
|
||||
|
||||
- `POST /api/todos/{id}/complete/`
|
||||
|
||||
方針:
|
||||
|
||||
- `status=done` にする専用入口を用意する
|
||||
- `should_link_record=true` かつ対応実績アプリがある場合、関連画面へ遷移するための情報を返す
|
||||
- MVP で自動実績作成まで行うか、完了導線のみ返すかは実装時に選べるようにする
|
||||
|
||||
---
|
||||
|
||||
## 9. UI 仕様案
|
||||
|
||||
### 9.1 一覧画面 `/todos`
|
||||
|
||||
表示内容:
|
||||
|
||||
- 未着手、進行中 TODO を優先表示
|
||||
- タイトル
|
||||
- ステータス
|
||||
- 期日
|
||||
- 作業種別
|
||||
- 対象圃場数
|
||||
- 紐づき計画
|
||||
|
||||
操作:
|
||||
|
||||
- 新規作成
|
||||
- ステータス変更
|
||||
- 並び替え
|
||||
- 完了済み、キャンセル済み表示切り替え
|
||||
- 絞り込み
|
||||
|
||||
視覚表現:
|
||||
|
||||
- 期限超過は赤系
|
||||
- 当日期限は強調
|
||||
- 進行中は目立つバッジ表示
|
||||
|
||||
### 9.2 詳細画面 `/todos/{id}`
|
||||
|
||||
表示・編集項目:
|
||||
|
||||
- タイトル
|
||||
- 説明
|
||||
- ステータス
|
||||
- 期日
|
||||
- 作業種別
|
||||
- 実績連携フラグ
|
||||
- 対象圃場
|
||||
- 分類作物、分類品種
|
||||
- 計画リンク
|
||||
|
||||
下部表示:
|
||||
|
||||
- 実績連携先
|
||||
- 完了日時
|
||||
- 更新日時
|
||||
|
||||
### 9.3 作成導線
|
||||
|
||||
MVP では少なくとも以下の 2 導線を持つ。
|
||||
|
||||
1. TODO 一覧から新規作成
|
||||
2. 計画詳細または一覧から TODO 生成
|
||||
|
||||
### 9.4 計画画面からの導線
|
||||
|
||||
対象候補:
|
||||
|
||||
- 施肥計画
|
||||
- 田植え計画
|
||||
- 運搬計画
|
||||
|
||||
ボタン例:
|
||||
|
||||
- `TODOを作成`
|
||||
- `この計画からTODO生成`
|
||||
|
||||
初期値:
|
||||
|
||||
- タイトル
|
||||
- 作業種別
|
||||
- 対象圃場候補
|
||||
- `should_link_record`
|
||||
|
||||
---
|
||||
|
||||
## 10. 実績連携の考え方
|
||||
|
||||
### 10.1 基本原則
|
||||
|
||||
- TODO は実績そのものではない
|
||||
- ただし、実績入力の起点にはなる
|
||||
- すべての TODO が実績へ行くわけではない
|
||||
|
||||
### 10.2 施肥
|
||||
|
||||
将来像:
|
||||
|
||||
1. 施肥計画を作る
|
||||
2. TODO を生成する
|
||||
3. TODO を実施する
|
||||
4. 完了時に施肥実績へつなぐ
|
||||
|
||||
考え方:
|
||||
|
||||
- 従来の `施肥計画 -> 施肥実績` に対し、間に TODO が入れるようにする
|
||||
- TODO 完了時は `SpreadingSession` 作成導線へつなぐ
|
||||
- 対象圃場は TODO の `TodoTargetField` を初期値として渡す
|
||||
|
||||
### 10.3 田植え
|
||||
|
||||
田植え実績アプリは今後実装予定であるため、今回の TODO 側では以下を前提にする。
|
||||
|
||||
- `rice_transplant` 種別の TODO を持てる
|
||||
- 完了時に将来の田植え実績へ接続できるよう索引設計を残す
|
||||
- MVP 時点では「完了済みだが実績アプリ未接続」の状態も許容する
|
||||
|
||||
### 10.4 実績アプリが無い作業
|
||||
|
||||
- `general` など、実績アプリに紐づかない TODO を許容する
|
||||
- その場合は `status=done` のみで完了とする
|
||||
|
||||
---
|
||||
|
||||
## 11. バリデーション方針
|
||||
|
||||
- `done` に遷移したら `completed_at` を自動設定する
|
||||
- `canceled` に遷移したら `canceled_at` を自動設定する
|
||||
- `done` から `todo` または `doing` への差し戻しは MVP では許可する
|
||||
- 差し戻し時も `completed_at` はクリアせず履歴値として保持する
|
||||
- 差し戻し時に `TodoCompletionLink` が存在する場合は、差し戻し自体は許可しつつ、API レスポンスに警告と各実績レコードへの直リンクを返す
|
||||
- フロントはその警告を表示し、ユーザーが実績を削除したい場合は直リンクから遷移できるようにする(実績レコード自体の削除は TODO 側では行わない)
|
||||
- `plan_links` に紐づく計画の年度と TODO の利用年度が必要なら将来追加する
|
||||
- `field_ids` が計画外圃場を含む場合は、`plan_links` が 1 件以上ある場合のみエラーにする
|
||||
- 複数 `plan_links` がある場合は、それぞれの計画に対して対象圃場整合性を検証する
|
||||
- `should_link_record=true` でも、対応実績アプリが無い場合は保存を許可する
|
||||
- `TodoTargetField.field` は `PROTECT` を採用する
|
||||
- 理由は、過去 TODO の対象圃場履歴を崩さないことを優先するため
|
||||
|
||||
### 11.1 レビュー反映済み判断
|
||||
|
||||
- `done -> todo/doing` の差し戻しは許可する
|
||||
- 差し戻し後も `completed_at` は監査用の履歴値として保持する
|
||||
- `TodoTargetField.field` は運用上の削除容易性より履歴保全を優先し、`PROTECT` を維持する
|
||||
- 実績連携フラグ名は `should_link_record` で確定する
|
||||
|
||||
---
|
||||
|
||||
## 12. 実装方針
|
||||
|
||||
### 12.1 Backend
|
||||
|
||||
- `apps/todos/models.py`
|
||||
- `apps/todos/admin.py`
|
||||
- `apps/todos/serializers.py`
|
||||
- `apps/todos/views.py`
|
||||
- `apps/todos/urls.py`
|
||||
- `apps/todos/migrations/`
|
||||
- `keinasystem/settings.py` へ app 追加
|
||||
- `keinasystem/urls.py` へ `/api/todos/` 追加
|
||||
|
||||
### 12.2 Frontend
|
||||
|
||||
- `frontend/src/app/todos/page.tsx`
|
||||
- `frontend/src/app/todos/[id]/page.tsx`
|
||||
- `frontend/src/app/todos/new/page.tsx`
|
||||
- 必要に応じて `_components` 配下に分離
|
||||
- ナビゲーションへ TODO 追加
|
||||
|
||||
### 12.3 実装順
|
||||
|
||||
1. モデル、admin、serializer、migration の作成
|
||||
2. TODO 一覧と CRUD API
|
||||
3. TODO 一覧と詳細 UI
|
||||
4. 並び替え API と UI
|
||||
5. 計画から TODO 生成
|
||||
6. 完了時の実績連携導線
|
||||
7. `makemigrations` と `migrate` を実行
|
||||
|
||||
---
|
||||
|
||||
## 13. テスト観点
|
||||
|
||||
- TODO を新規作成できる
|
||||
- 対象圃場を複数紐づけできる
|
||||
- 計画の一部圃場だけを対象にできる
|
||||
- 完了済み、キャンセル済みの表示切り替えができる
|
||||
- 並び替え後に順番が保持される
|
||||
- 計画画面から TODO を生成できる
|
||||
- 実績アプリ未接続の TODO でも完了できる
|
||||
- 実績連携済み TODO の挙動が壊れない
|
||||
|
||||
---
|
||||
|
||||
## 14. 未確定事項
|
||||
|
||||
### 14.1 work_type enum の初回実装範囲(確定)
|
||||
|
||||
MVP は現行アプリ対応の5種別のみで開始する。
|
||||
|
||||
- `general`: 一般
|
||||
- `fertilization`: 施肥
|
||||
- `rice_transplant`: 田植え
|
||||
- `delivery`: 運搬
|
||||
- `levee_work`: 畔塗
|
||||
|
||||
`pesticide`(防除)は農薬散布管理アプリ実装時に追加する。
|
||||
|
||||
### 14.2 完了時の実績連携レベル(確定)
|
||||
|
||||
MVP は **B. 実績入力画面への導線生成** を採用する。
|
||||
|
||||
- A. 完了ステータス変更のみ → 採用しない
|
||||
- **B. 実績入力画面への導線生成 → 採用**
|
||||
- C. TODO 情報を使った実績レコード仮生成 → 後続検討
|
||||
|
||||
### 14.3 削除ポリシー
|
||||
|
||||
実績連携後の TODO をどう扱うか。
|
||||
|
||||
案:
|
||||
|
||||
- 物理削除禁止
|
||||
- 論理削除
|
||||
- 参照整合性チェック付き物理削除
|
||||
|
||||
### 14.4 work_type と計画種別の追加ルール
|
||||
|
||||
MVP では以下を前提とする。
|
||||
|
||||
- work_type は先に定義する
|
||||
- plan_link は実在する計画モデルだけを持つ
|
||||
- work_type が存在しても、対応する計画 FK が未実装のことはあり得る
|
||||
|
||||
将来、新しい計画機能が増えたときは以下を同時に更新する。
|
||||
|
||||
- `Todo.work_type` choices
|
||||
- `TodoPlanLink.plan_type`
|
||||
- 対応 FK
|
||||
- 計画から TODO 生成 API
|
||||
|
||||
---
|
||||
|
||||
## 15. 提案する MVP 決定案
|
||||
|
||||
実装着手しやすさを優先し、MVP では以下を採用することを提案する。
|
||||
|
||||
- TODO は `year` を持つ
|
||||
- 対象管理は `TodoTargetField` を正とする
|
||||
- `work_type` は `general / fertilization / rice_transplant / delivery / levee_work` を初期採用する(`pesticide` は農薬散布管理アプリ実装時に追加)
|
||||
- 計画リンクは明示 FK 方式で開始する
|
||||
- 実績連携フラグ名は `should_link_record` を採用する
|
||||
- 完了時はまず「実績入力画面への導線生成」を採用し、自動実績作成は後続検討とする
|
||||
- 並び替えは API 先行、UI は DnD 優先、難しければ矢印移動で代替する
|
||||
528
改善案/ナビゲーション再編仕様書.md
Normal file
528
改善案/ナビゲーション再編仕様書.md
Normal file
@@ -0,0 +1,528 @@
|
||||
# ナビゲーション再編仕様書
|
||||
|
||||
> 作成日: 2026-04-07
|
||||
> 対象: `frontend/src/components/Navbar.tsx`
|
||||
> 方針: 第一候補「上段5分類 + ドロップダウン」
|
||||
|
||||
---
|
||||
|
||||
## 0. 背景
|
||||
|
||||
現状のグローバルナビゲーションは、機能追加のたびに横並びのボタンを増やしており、以下の問題が出ている。
|
||||
|
||||
- 上位階層の導線が多すぎて、目的の画面を探しにくい
|
||||
- 「計画」「実績」「設定」「補助機能」が同じ粒度で並んでいる
|
||||
- メール関連のように、単独トップに置くほどではない機能が場所を取りやすい
|
||||
- 今後も機能追加が続くと、横幅不足と認知負荷の両方が悪化する
|
||||
|
||||
このため、トップナビは「日常的に使う業務カテゴリ」だけを見せ、個別画面はドロップダウン配下へ整理する。
|
||||
|
||||
---
|
||||
|
||||
## 1. 目的
|
||||
|
||||
### 1-1. 目指す状態
|
||||
|
||||
- 1階層目では「何をしたいか」で探せる
|
||||
- 似た役割の画面を同じカテゴリに集約する
|
||||
- 画面数が増えても、トップレベルの見た目を増やしすぎない
|
||||
- PC とスマホで同じ情報設計を維持する
|
||||
|
||||
### 1-2. 今回の対象
|
||||
|
||||
- 共通ヘッダー内のグローバルナビゲーション再編
|
||||
- メニュー分類、ラベル、並び順、開閉仕様の定義
|
||||
- 各画面がどのカテゴリに属するかの明確化
|
||||
|
||||
### 1-3. 今回やらないこと
|
||||
|
||||
- 各業務画面そのものの UI 改修
|
||||
- 権限別メニュー出し分け
|
||||
- お気に入り機能、ピン留め機能
|
||||
- ナビゲーションと連動したダッシュボード内容の刷新
|
||||
|
||||
### 1-4. 関連 Issue との役割分担
|
||||
|
||||
- Issue `#13 メニューがごちゃごちゃしてきたので、整理する` は、背景、論点、判断理由を残す親議論として扱う
|
||||
- 本仕様書は、その議論を踏まえた実装向けの決定事項をまとめる文書として扱う
|
||||
- 後から判断理由を確認したい場合は Issue `#13` を参照する
|
||||
|
||||
---
|
||||
|
||||
## 2. 基本方針
|
||||
|
||||
### 2-1. トップレベル構成
|
||||
|
||||
トップナビでは以下の 5 項目のみを常時表示する。
|
||||
|
||||
1. ホーム
|
||||
2. 計画
|
||||
3. 実績
|
||||
4. マスター
|
||||
5. 帳票・連携
|
||||
|
||||
右端には従来どおりユーザー操作を置く。
|
||||
|
||||
- パスワード変更
|
||||
- ログアウト
|
||||
|
||||
### 2-2. 設計ルール
|
||||
|
||||
- 毎日使う業務カテゴリだけをトップに置く
|
||||
- 個別機能名ではなく、業務単位で束ねる
|
||||
- 設定、履歴、通知、補助系は単独トップにしない
|
||||
- 同じ業務の前後工程は可能な限り同じカテゴリに寄せる
|
||||
|
||||
---
|
||||
|
||||
## 3. 情報設計
|
||||
|
||||
### 3-1. カテゴリ構成
|
||||
|
||||
#### ホーム
|
||||
|
||||
- ダッシュボード
|
||||
|
||||
#### 計画
|
||||
|
||||
- 作付け計画
|
||||
- 施肥計画
|
||||
- 田植え計画
|
||||
- 運搬計画
|
||||
|
||||
#### 実績
|
||||
|
||||
- 散布実績
|
||||
- 畔塗記録
|
||||
- 作業記録
|
||||
|
||||
#### マスター
|
||||
|
||||
- 圃場管理
|
||||
- 作物
|
||||
- 品種
|
||||
- 資材マスタ
|
||||
- 肥料マスタ
|
||||
|
||||
#### 帳票・連携
|
||||
|
||||
- 在庫管理
|
||||
- 帳票出力
|
||||
- データ取込
|
||||
- 気象
|
||||
- メール
|
||||
|
||||
### 3-2. メールの扱い
|
||||
|
||||
メール関連はトップ階層に個別表示しない。
|
||||
`帳票・連携 > メール` の中にまとめる。
|
||||
|
||||
内訳:
|
||||
|
||||
- メール履歴
|
||||
- メールルール
|
||||
|
||||
### 3-3. 設定の扱い
|
||||
|
||||
現状はパスワード変更のみのため、独立カテゴリにはしない。
|
||||
初期実装では右上アイコンからのパスワード変更導線を維持し、設定系機能が増えた場合に別途 `設定` グループ化を検討する。
|
||||
|
||||
---
|
||||
|
||||
## 4. 画面とメニューの対応
|
||||
|
||||
### 4-1. 現在の主要画面の所属
|
||||
|
||||
| カテゴリ | ラベル | パス |
|
||||
|---|---|---|
|
||||
| ホーム | ダッシュボード | `/dashboard` |
|
||||
| 計画 | 作付け計画 | `/allocation` |
|
||||
| 計画 | 施肥計画 | `/fertilizer` |
|
||||
| 計画 | 田植え計画 | `/rice-transplant` |
|
||||
| 計画 | 運搬計画 | `/distribution` |
|
||||
| 実績 | 散布実績 | `/fertilizer/spreading` |
|
||||
| 実績 | 畔塗記録 | `/levee-work` |
|
||||
| 実績 | 作業記録 | `/workrecords` |
|
||||
| マスター | 圃場管理 | `/fields` |
|
||||
| マスター | 作物 | `未実装: allocation 内管理を独立予定` |
|
||||
| マスター | 品種 | `未実装: allocation 内管理を独立予定` |
|
||||
| マスター | 資材マスタ | `/materials/masters` |
|
||||
| マスター | 肥料マスタ | `/fertilizer/masters` |
|
||||
| 帳票・連携 | 在庫管理 | `/materials` |
|
||||
| 帳票・連携 | 帳票出力 | `/reports` |
|
||||
| 帳票・連携 | データ取込 | `/import` |
|
||||
| 帳票・連携 | 気象 | `/weather` |
|
||||
| 帳票・連携 > メール | メール履歴 | `/mail/history` |
|
||||
| 帳票・連携 > メール | メールルール | `/mail/rules` |
|
||||
| 右上ユーザー操作 | パスワード変更 | `/settings/password` |
|
||||
|
||||
### 4-2. アクティブ表示ルール
|
||||
|
||||
- その画面自身、またはその配下詳細画面にいる場合、所属カテゴリをアクティブにする
|
||||
- ドロップダウン内の該当項目も個別にアクティブ表示する
|
||||
- パス接頭辞だけで判定するとカテゴリが衝突する画面があるため、より具体的なパスを優先して判定する
|
||||
- 特に `施肥計画` と `散布実績` はともに `/fertilizer` 配下を使うため、`/fertilizer/spreading` を先に判定し、`計画` 側から明示的に除外する
|
||||
- 同様に `在庫管理` と `資材マスタ` はともに `/materials` 配下を使うため、`/materials/masters` を先に判定し、`帳票・連携` 側の `在庫管理` から明示的に除外する
|
||||
- 例:
|
||||
- `/fertilizer` は `計画` をアクティブ
|
||||
- `/fertilizer/new` は `計画` をアクティブ
|
||||
- `/fertilizer/[id]` および `/fertilizer/[id]/edit` は `計画` をアクティブ
|
||||
- `/fertilizer/spreading` は `実績` をアクティブ
|
||||
- `/fertilizer/spreading?...` も `実績` をアクティブ
|
||||
- `/materials` は `帳票・連携` をアクティブ
|
||||
- `/materials/masters` は `マスター` をアクティブ
|
||||
- 例:
|
||||
- `/fields/123` の場合は `マスター` がアクティブ
|
||||
- `/fertilizer/10/edit` の場合は `計画` がアクティブ
|
||||
- `/mail/history` の場合は `帳票・連携` と `メール履歴` がアクティブ
|
||||
|
||||
---
|
||||
|
||||
## 5. PC 表示仕様
|
||||
|
||||
### 5-1. レイアウト
|
||||
|
||||
PC ではヘッダーを 3 ブロック構成とする。
|
||||
|
||||
1. 左: ブランド名 `KeinaSystem`
|
||||
2. 中央: トップメニュー 5 項目
|
||||
3. 右: パスワード変更、ログアウト
|
||||
|
||||
### 5-2. トップメニューの見せ方
|
||||
|
||||
- `ホーム` は単独リンク
|
||||
- `計画` `実績` `マスター` `帳票・連携` はドロップダウン付きメニュー
|
||||
- ラベルの横に開閉アイコンを表示する
|
||||
- 開いたメニューは白背景のパネルとして表示する
|
||||
|
||||
### 5-3. 開閉ルール
|
||||
|
||||
- クリックで開閉
|
||||
- 開いている他メニューがある場合は、それを閉じてから新しいメニューを開く
|
||||
- メニュー外クリックで閉じる
|
||||
- `Esc` キーで閉じる
|
||||
- キーボード操作は初期実装ではブラウザ標準の `Tab` 移動を基本とする
|
||||
- 矢印キーによるドロップダウン項目間移動は Phase 1 の必須要件には含めない
|
||||
- 項目クリック後は遷移して閉じる
|
||||
|
||||
### 5-4. ドロップダウンの表示内容
|
||||
|
||||
各項目は以下の順で並べる。
|
||||
|
||||
#### 計画
|
||||
|
||||
1. 作付け計画
|
||||
2. 施肥計画
|
||||
3. 田植え計画
|
||||
4. 運搬計画
|
||||
|
||||
#### 実績
|
||||
|
||||
1. 散布実績
|
||||
2. 畔塗記録
|
||||
3. 作業記録
|
||||
|
||||
#### マスター
|
||||
|
||||
1. 圃場管理
|
||||
2. 作物
|
||||
3. 品種
|
||||
4. 資材マスタ
|
||||
5. 肥料マスタ
|
||||
|
||||
#### 帳票・連携
|
||||
|
||||
1. 在庫管理
|
||||
2. 帳票出力
|
||||
3. データ取込
|
||||
4. 気象
|
||||
5. メール
|
||||
|
||||
`メール` は 2 段構造にする方法と、直接展開せず一覧モーダル風に見せる方法があるが、初期実装ではシンプルさを優先し、`帳票・連携` ドロップダウン内に個別リンクを直接置く。
|
||||
|
||||
初期実装の並び:
|
||||
|
||||
1. 在庫管理
|
||||
2. 帳票出力
|
||||
3. データ取込
|
||||
4. 気象
|
||||
5. メール履歴
|
||||
6. メールルール
|
||||
|
||||
なお情報設計上の名称としては `メール` を維持し、将来的に機能が増えた時点で再度サブグループ化する。
|
||||
|
||||
---
|
||||
|
||||
## 6. スマホ表示仕様
|
||||
|
||||
### 6-1. 基本方針
|
||||
|
||||
スマホではハンバーガーメニューを採用する。
|
||||
PC と同じカテゴリ構成を維持し、見た目だけ縦並びにする。
|
||||
|
||||
### 6-2. 表示ルール
|
||||
|
||||
- 初期状態ではロゴ、メニューボタン、ログアウト系導線のみ表示
|
||||
- メニューボタン押下で全画面または右スライドのメニューを開く
|
||||
- カテゴリはアコーディオン形式で開閉する
|
||||
|
||||
### 6-3. 並び順
|
||||
|
||||
1. ホーム
|
||||
2. 計画
|
||||
3. 実績
|
||||
4. マスター
|
||||
5. 帳票・連携
|
||||
6. パスワード変更
|
||||
7. ログアウト
|
||||
|
||||
### 6-4. 開閉ルール
|
||||
|
||||
- `ホーム` は単独リンクとし、タップ時はそのまま `/dashboard` へ遷移する
|
||||
- カテゴリ見出しタップで開閉
|
||||
- 現在表示中の画面が属するカテゴリは初期状態で展開してよい
|
||||
- 項目タップ後はメニューを閉じて画面遷移する
|
||||
|
||||
---
|
||||
|
||||
## 7. ラベル方針
|
||||
|
||||
### 7-1. トップ階層ラベル
|
||||
|
||||
- 短く、役割が伝わる言葉を使う
|
||||
- 詳細機能名は 2 階層目へ寄せる
|
||||
|
||||
### 7-2. 用語ルール
|
||||
|
||||
- `ホーム` は利用者に最も分かりやすいため維持
|
||||
- `計画` は作付け、施肥、田植え、運搬を含む包括名として使用
|
||||
- `実績` は記録系業務のまとめ先とする
|
||||
- `マスター` は日々の業務を支える基礎データ管理の置き場とする
|
||||
- `帳票・連携` は出力、取込、通知、補助参照のまとめ先とする
|
||||
|
||||
将来 `帳票・連携` に機能が増えすぎた場合は、次の再編を検討する。
|
||||
|
||||
- `帳票`
|
||||
- `連携`
|
||||
- `通知`
|
||||
|
||||
---
|
||||
|
||||
## 8. アイコン方針
|
||||
|
||||
### 8-1. トップ階層
|
||||
|
||||
トップ階層には必要最低限のアイコンのみ使用する。
|
||||
|
||||
- ホーム: 家またはダッシュボード系
|
||||
- 計画: 作物または計画系
|
||||
- 実績: チェック、記録系
|
||||
- マスター: 設定、リスト、データベース系
|
||||
- 帳票・連携: ファイル、送受信、クラウド系
|
||||
|
||||
ただし、文字認識を優先し、アイコンは補助扱いとする。
|
||||
|
||||
### 8-2. ドロップダウン内
|
||||
|
||||
現行アイコンを流用してよいが、すべてに付ける必要はない。
|
||||
視認性よりも一覧性を優先し、テキスト中心でも可。
|
||||
|
||||
---
|
||||
|
||||
## 9. 操作性・アクセシビリティ要件
|
||||
|
||||
- キーボードでトップメニューにフォーカス移動できること
|
||||
- `Enter` または `Space` でドロップダウンを開閉できること
|
||||
- ドロップダウン展開後、各項目へ `Tab` で到達できること
|
||||
- `Esc` で閉じられること
|
||||
- 矢印キーによる項目間移動は初期実装の必須要件には含めず、将来のアクセシビリティ強化項目として扱う
|
||||
- 現在位置が視覚的に分かること
|
||||
- タップ領域は十分に確保すること
|
||||
- スマホで誤タップしにくい行間と余白を確保すること
|
||||
|
||||
---
|
||||
|
||||
## 10. 実装方針
|
||||
|
||||
### 10-1. コンポーネント構成案
|
||||
|
||||
`Navbar.tsx` を以下の責務に分ける。
|
||||
|
||||
- ブランド表示
|
||||
- トップメニュー定義
|
||||
- ドロップダウン表示
|
||||
- モバイルメニュー表示
|
||||
- 右端ユーザー操作
|
||||
|
||||
必要に応じて次のような補助構成へ分割してよい。
|
||||
|
||||
- `navGroups` 定数
|
||||
- `DesktopNav`
|
||||
- `MobileNav`
|
||||
|
||||
### 10-2. メニュー定義データ化
|
||||
|
||||
個別ボタンの直書きはやめ、カテゴリ配列で管理する。
|
||||
メニュー定義はフラットな `group` 管理ではなく、階層構造で持つ。
|
||||
|
||||
方針:
|
||||
|
||||
- グループ構成そのものが定義から読み取れることを優先する
|
||||
- 通常ケースは `href` ベースで扱う
|
||||
- `href` ベースで判定しきれない例外ケースだけ `match` を持つ
|
||||
- `match` は全件定義用ではなく、衝突回避用の最小限の逃げ道として使う
|
||||
|
||||
想定イメージ:
|
||||
|
||||
```ts
|
||||
type NavItem = {
|
||||
label: string;
|
||||
href: string;
|
||||
match?: (pathname: string) => boolean;
|
||||
};
|
||||
|
||||
type NavGroup = {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'link' | 'group';
|
||||
href?: string;
|
||||
items?: NavItem[];
|
||||
};
|
||||
|
||||
const navGroups = [
|
||||
{
|
||||
key: 'home',
|
||||
label: 'ホーム',
|
||||
type: 'link',
|
||||
href: '/dashboard',
|
||||
},
|
||||
{
|
||||
key: 'planning',
|
||||
label: '計画',
|
||||
type: 'group',
|
||||
items: [
|
||||
{ label: '作付け計画', href: '/allocation' },
|
||||
{ label: '施肥計画', href: '/fertilizer' },
|
||||
{ label: '田植え計画', href: '/rice-transplant' },
|
||||
{ label: '運搬計画', href: '/distribution' },
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### 10-3. アクティブ判定
|
||||
|
||||
アクティブ判定は、現在の `pathname` と各項目の対応パターンで管理する。
|
||||
|
||||
基本原則:
|
||||
|
||||
- URL はリソース・機能識別子として安定性を優先し、メニュー階層とは分離して扱う
|
||||
- メニュー再編のたびに URL を変更しない
|
||||
- アクティブ判定はナビ定義側のルールで吸収する
|
||||
- ただし、全件をルーターのように再定義するのではなく、通常ケースは `href` ベース、衝突ケースだけ `match` を使う
|
||||
|
||||
補足:
|
||||
|
||||
- Next.js App Router の Route Groups は、URL を変えずにコード構造を整理する手段としては有効
|
||||
- ただし Route Groups はナビ判定そのものの代替ではなく、実装整理の補助手段として扱う
|
||||
|
||||
パス接頭辞衝突が起きる組み合わせは、具体パス優先で次のように扱う。
|
||||
|
||||
| 優先判定するパス | 所属カテゴリ | 除外される側 |
|
||||
|---|---|---|
|
||||
| `/fertilizer/spreading` | 実績 | 計画 > 施肥計画 |
|
||||
| `/materials/masters` | マスター | 帳票・連携 > 在庫管理 |
|
||||
|
||||
通常判定の例:
|
||||
|
||||
- `/fertilizer`
|
||||
- `/fertilizer/new`
|
||||
- `/fertilizer/[id]/edit`
|
||||
|
||||
はすべて `施肥計画` 所属として扱う。
|
||||
|
||||
- `/materials`
|
||||
- `/materials?tab=...`
|
||||
|
||||
は `在庫管理` 所属として扱う。
|
||||
|
||||
実装上は、上記の衝突ケースのみ `NavItem.match` を使う想定とする。
|
||||
|
||||
---
|
||||
|
||||
## 11. 段階導入案
|
||||
|
||||
### Phase 1
|
||||
|
||||
- PC ナビを 5 分類へ再編
|
||||
- `作物` `品種` はマスター体系に含めるが、Phase 1 ではメニューに表示しない
|
||||
- Phase 1 の `マスター` 配下に表示するのは `圃場管理` `資材マスタ` `肥料マスタ` のみとする
|
||||
- `作物` `品種` は未実装項目として仕様上は位置づけ、独立画面が用意できる Phase 2 でメニュー表示を開始する
|
||||
- モバイルも同じ情報設計へ変更
|
||||
|
||||
### Phase 2
|
||||
|
||||
- `作物管理` `品種管理` を独立画面として追加
|
||||
- `帳票・連携` 内の `メール` をサブグループ化
|
||||
- よく使う画面の履歴や最近使った機能を補助表示
|
||||
|
||||
### Phase 3
|
||||
|
||||
- 将来マルチユーザー化した場合の再設計検討
|
||||
- 権限や担当業務ごとの表示最適化の要否整理
|
||||
- 単独利用を前提とする間は、Phase 3 は実施対象外とする
|
||||
|
||||
---
|
||||
|
||||
## 12. 受け入れ条件
|
||||
|
||||
- トップレベルに常時表示される業務メニューが 5 項目に収まっていること
|
||||
- 現在存在する主要画面が、いずれかのカテゴリに漏れなく所属していること
|
||||
- 各画面でアクティブ状態が期待通りに表示されること
|
||||
- PC とスマホで同じカテゴリ構成になっていること
|
||||
- メニューが増えても、トップレベルの項目数を増やさずに拡張できること
|
||||
|
||||
---
|
||||
|
||||
## 13. 想定される懸念と対応
|
||||
|
||||
### 13-1. 「マスター」に含める範囲
|
||||
|
||||
現時点では、`圃場管理` `作物` `品種` `資材マスタ` `肥料マスタ` を `マスター` として集約する。
|
||||
日々の入力対象ではなく、業務の前提データを整える画面群としてまとめるのが自然である。
|
||||
|
||||
補足:
|
||||
|
||||
- `圃場管理` は圃場マスタとして独立性が高い
|
||||
- `作物` と `品種` も本来マスター管理であり、現状は allocation 画面内で扱っているだけで、メニュー上は独立させる前提で考える
|
||||
- `資材マスタ` と `肥料マスタ` はすでに独立ページがあり、`マスター` 配下に置くのが自然である
|
||||
- 将来、地図、圃場グループ、所有者管理などが増えた場合は、`圃場マスタ` の再独立も検討する
|
||||
|
||||
### 13-2. 「帳票・連携」が少し広い
|
||||
|
||||
現時点では `在庫管理` `帳票出力` `データ取込` `気象` `メール` をまとめる。
|
||||
完全に同じ性質ではないが、いずれも補助業務、参照、連携、出力に近い機能であり、トップ階層を増やしすぎないために同居させる。
|
||||
|
||||
### 13-3. 「データ取込」は日常操作ではない
|
||||
|
||||
`データ取込` は日常的に何度も使う画面ではなく、年度切替時や初期設定時に使う補助導線である。
|
||||
そのためトップレベル常設には置かず、`帳票・連携` 配下に置く判断は妥当とする。
|
||||
|
||||
ただし今後、取込対象や運用頻度が増えた場合は、`設定` または `運用` 系カテゴリへの移設も検討対象とする。
|
||||
|
||||
### 13-4. 既存ユーザーが場所を見失う
|
||||
|
||||
初期導入時は以下を行う。
|
||||
|
||||
- 並び順をできるだけ業務の流れに合わせる
|
||||
- ドロップダウン内で既存画面名は変更しない
|
||||
- ダッシュボード上に主要導線を残す
|
||||
|
||||
---
|
||||
|
||||
## 14. 結論
|
||||
|
||||
現行の 1 画面 1 ボタン方式は、今後の機能追加に対して拡張性が低い。
|
||||
そのため、トップナビは `ホーム / 計画 / 実績 / マスター / 帳票・連携` の 5 分類へ再編し、個別画面はドロップダウン配下に置く。
|
||||
|
||||
この構成により、利用者は「画面名」ではなく「やりたい業務」から機能を探せるようになり、将来の機能追加にも耐えやすくなる。
|
||||
Reference in New Issue
Block a user