#12 対応を入れました。
畔塗記録 API に total_area_tan を追加して、一覧の各記録に「圃場数 / 面積合計」が出るようにしました。あわせて、作成・編集フォームの「対象圃場一覧」にも、選択中の合計面積を表示しています。主な変更は serializers.py、tests.py、page.tsx、index.ts です。 確認できたこと: docker compose -f docker-compose.develop.yml exec backend python manage.py test apps.levee_work OK docker exec keinasystem_frontend npm run build OK まだコミットはしていません。必要ならこのままコミットして push します。
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from decimal import Decimal
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from apps.plans.models import Plan
|
from apps.plans.models import Plan
|
||||||
@@ -34,6 +35,7 @@ class LeveeWorkSessionSerializer(serializers.ModelSerializer):
|
|||||||
items = LeveeWorkSessionItemReadSerializer(many=True, read_only=True)
|
items = LeveeWorkSessionItemReadSerializer(many=True, read_only=True)
|
||||||
work_record_id = serializers.IntegerField(source='work_record.id', read_only=True)
|
work_record_id = serializers.IntegerField(source='work_record.id', read_only=True)
|
||||||
item_count = serializers.SerializerMethodField()
|
item_count = serializers.SerializerMethodField()
|
||||||
|
total_area_tan = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = LeveeWorkSession
|
model = LeveeWorkSession
|
||||||
@@ -45,6 +47,7 @@ class LeveeWorkSessionSerializer(serializers.ModelSerializer):
|
|||||||
'notes',
|
'notes',
|
||||||
'work_record_id',
|
'work_record_id',
|
||||||
'item_count',
|
'item_count',
|
||||||
|
'total_area_tan',
|
||||||
'items',
|
'items',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
@@ -53,6 +56,10 @@ class LeveeWorkSessionSerializer(serializers.ModelSerializer):
|
|||||||
def get_item_count(self, obj):
|
def get_item_count(self, obj):
|
||||||
return len(obj.items.all())
|
return len(obj.items.all())
|
||||||
|
|
||||||
|
def get_total_area_tan(self, obj):
|
||||||
|
total = sum((item.field.area_tan or Decimal('0')) for item in obj.items.all())
|
||||||
|
return str(total)
|
||||||
|
|
||||||
|
|
||||||
class LeveeWorkSessionItemWriteInputSerializer(serializers.Serializer):
|
class LeveeWorkSessionItemWriteInputSerializer(serializers.Serializer):
|
||||||
field = serializers.IntegerField()
|
field = serializers.IntegerField()
|
||||||
|
|||||||
58
backend/apps/levee_work/tests.py
Normal file
58
backend/apps/levee_work/tests.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from apps.fields.models import Field
|
||||||
|
from apps.plans.models import Crop, Plan, Variety
|
||||||
|
|
||||||
|
from .models import LeveeWorkSession, LeveeWorkSessionItem
|
||||||
|
from .serializers import LeveeWorkSessionSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class LeveeWorkSessionSerializerTests(TestCase):
|
||||||
|
def test_total_area_tan_is_included(self):
|
||||||
|
crop = Crop.objects.create(name='水稲')
|
||||||
|
variety = Variety.objects.create(crop=crop, name='にこまる')
|
||||||
|
field_a = Field.objects.create(
|
||||||
|
name='足川北上',
|
||||||
|
address='高知県高岡郡',
|
||||||
|
area_tan='1.2000',
|
||||||
|
area_m2=1200,
|
||||||
|
owner_name='吉田',
|
||||||
|
group_name='北',
|
||||||
|
display_order=1,
|
||||||
|
)
|
||||||
|
field_b = Field.objects.create(
|
||||||
|
name='足川南',
|
||||||
|
address='高知県高岡郡',
|
||||||
|
area_tan='0.8000',
|
||||||
|
area_m2=800,
|
||||||
|
owner_name='吉田',
|
||||||
|
group_name='南',
|
||||||
|
display_order=2,
|
||||||
|
)
|
||||||
|
plan_a = Plan.objects.create(field=field_a, year=2026, crop=crop, variety=variety, notes='')
|
||||||
|
plan_b = Plan.objects.create(field=field_b, year=2026, crop=crop, variety=variety, notes='')
|
||||||
|
session = LeveeWorkSession.objects.create(
|
||||||
|
year=2026,
|
||||||
|
date='2026-04-06',
|
||||||
|
title='水稲畔塗',
|
||||||
|
notes='',
|
||||||
|
)
|
||||||
|
LeveeWorkSessionItem.objects.create(
|
||||||
|
session=session,
|
||||||
|
field=field_a,
|
||||||
|
plan=plan_a,
|
||||||
|
crop_name_snapshot='水稲',
|
||||||
|
variety_name_snapshot='にこまる',
|
||||||
|
)
|
||||||
|
LeveeWorkSessionItem.objects.create(
|
||||||
|
session=session,
|
||||||
|
field=field_b,
|
||||||
|
plan=plan_b,
|
||||||
|
crop_name_snapshot='水稲',
|
||||||
|
variety_name_snapshot='にこまる',
|
||||||
|
)
|
||||||
|
|
||||||
|
data = LeveeWorkSessionSerializer(session).data
|
||||||
|
|
||||||
|
self.assertEqual(data['item_count'], 2)
|
||||||
|
self.assertEqual(data['total_area_tan'], '2.0000')
|
||||||
@@ -203,6 +203,10 @@ function LeveeWorkPageContent() {
|
|||||||
return sortedCandidates.filter((candidate) => form.selectedFieldIds.has(candidate.field_id));
|
return sortedCandidates.filter((candidate) => form.selectedFieldIds.has(candidate.field_id));
|
||||||
}, [form, sortedCandidates]);
|
}, [form, sortedCandidates]);
|
||||||
|
|
||||||
|
const selectedAreaTan = useMemo(() => {
|
||||||
|
return selectedCandidates.reduce((sum, candidate) => sum + Number(candidate.field_area_tan || '0'), 0);
|
||||||
|
}, [selectedCandidates]);
|
||||||
|
|
||||||
const handleSort = (nextKey: SortKey) => {
|
const handleSort = (nextKey: SortKey) => {
|
||||||
if (sortKey === nextKey) {
|
if (sortKey === nextKey) {
|
||||||
setSortDirection((current) => (current === 'asc' ? 'desc' : 'asc'));
|
setSortDirection((current) => (current === 'asc' ? 'desc' : 'asc'));
|
||||||
@@ -335,7 +339,9 @@ function LeveeWorkPageContent() {
|
|||||||
>
|
>
|
||||||
<div className="text-sm font-medium text-gray-900">{session.title}</div>
|
<div className="text-sm font-medium text-gray-900">{session.title}</div>
|
||||||
<div className="mt-1 text-sm text-gray-600">{session.date}</div>
|
<div className="mt-1 text-sm text-gray-600">{session.date}</div>
|
||||||
<div className="mt-1 text-xs text-gray-500">{session.item_count}圃場</div>
|
<div className="mt-1 text-xs text-gray-500">
|
||||||
|
{session.item_count}圃場 / {Number(session.total_area_tan).toFixed(2)}反
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -388,7 +394,9 @@ function LeveeWorkPageContent() {
|
|||||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-3">
|
<div className="mb-3 flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-sm font-medium text-gray-900">対象圃場一覧</h2>
|
<h2 className="text-sm font-medium text-gray-900">対象圃場一覧</h2>
|
||||||
<p className="text-xs text-gray-500">{selectedCount} / {candidates.length} 圃場を選択中</p>
|
<p className="text-xs text-gray-500">
|
||||||
|
{selectedCount} / {candidates.length} 圃場を選択中 / 合計 {selectedAreaTan.toFixed(2)}反
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ export interface LeveeWorkSession {
|
|||||||
notes: string;
|
notes: string;
|
||||||
work_record_id: number | null;
|
work_record_id: number | null;
|
||||||
item_count: number;
|
item_count: number;
|
||||||
|
total_area_tan: string;
|
||||||
items: LeveeWorkSessionItem[];
|
items: LeveeWorkSessionItem[];
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user