栽培管理アプリを作ってます⑨:農薬使用回数カウントとリセット履歴機能を実装した話

前回は作業履歴の検索機能を強化し、カレンダー表示やフィルタ機能を実装しました。今回は、農薬使用回数のカウント機能リセット履歴の表示を実装した話をします。

なぜ農薬使用回数のカウントが必要なのか

農薬には使用回数の制限があります。これは農薬取締法で定められており、GAP認証を取得する上でも欠かせない記録です。

例えば、ある農薬が「アスパラガスに対して年3回まで」と登録されている場合、それを超えて使用することはできません。

ただし、ここで問題が。

作物によってリセットタイミングが違う

  • アスパラガスなど: 栽培終了時にリセット
  • ニラなど: 収穫期ごとにリセット(栽培は継続)

私の場合、主栽培であるアスパラガス含めて収穫が終わればそこで終わりとなるので、リセットするという概念はあまりないかもしれませんが、
過去に栽培経験があるニラなんかは、収穫しては伸びて、収穫しては伸びて….という作物なので、リセットという概念が必要になります。

これを実装できないかと考えていました。

課題:畝ごとのカウントが重複する問題

最初に直面したのは、リセット履歴だけ回数がおかしいという問題。

図2: ちょっとわかりにくいが最初に畝1~4を散布して、その後に畝1を散布→同じハウス内でいうと2回散布

しかし、リセット履歴には「5回使用」と表示されてしまう。

図3: リセット履歴が「5回使用」と誤表示

原因:畝ごとに別カウントされていた

デバッグを進めると、原因が判明しました。

データベースの構造を確認すると、1回の作業登録に対して、複数の畝が紐付いていました:

作業登録1回目(12/21)
├─ 畝1で散布
├─ 畝2で散布  
├─ 畝3で散布
└─ 畝4で散布

作業登録2回目(12/23)
└─ 畝1で散布

本来なら「2回の作業登録」=「2回使用」とカウントすべきところを、誤って畝の数(5個)でカウントしてしまっていたんです。

つまり:

  • ❌ 間違い: 畝1(1回)+ 畝2(1回)+ 畝3(1回)+ 畝4(1回)+ 畝1(1回)= 5回
  • ✅ 正解: 作業登録1回目 + 作業登録2回目 = 2回

つまり、登録単位でカウントすべきところを、畝単位でカウントしていました。

解決策:畝の選択状態で集計方法を変更。

現在期間と全期間が正しく動いているロジックを参考に、以下のように修正しました:

Supabaseのテーブル構造

cult_logs(作業記録テーブル)
┌─────────┬────────────┬──────────┐
│ log_id  │ log_date   │ field_id │
├─────────┼────────────┼──────────┤
│ 168     │ 2025-12-21 │ 8        │ ← 1回目の作業登録
│ 169     │ 2025-12-23 │ 8        │ ← 2回目の作業登録
└─────────┴────────────┴──────────┘

cult_log_pesticides(農薬詳細テーブル)
┌─────────┬──────────────┬──────────┐
│ log_id  │ pesticide_id │ bed_id   │
├─────────┼──────────────┼──────────┤
│ 168     │ 85           │ 1        │ ← 1回目:畝1
│ 168     │ 85           │ 2        │ ← 1回目:畝2
│ 168     │ 85           │ 3        │ ← 1回目:畝3
│ 168     │ 85           │ 4        │ ← 1回目:畝4
│ 169     │ 85           │ 1        │ ← 2回目:畝1
└─────────┴──────────────┴──────────┘

log_idは「作業登録のID」です。同じlog_idで複数の畝に散布した記録が、cult_log_pesticidesテーブルに複数行として保存されています。

// 畝が選択されている場合: その畝だけカウント
if (bedId !== null) {
  // log_id + pesticide_id でユニーク判定
  const key = `${log.id}_${pesticide_id}`;
  periodCounts[period.id][pesticide_id] = count;
}
// ハウス全体の場合: 各畝の最大値を取得
else {
  // 各畝ごとにカウント → 最大値を取得
  periodCounts[period.id][pesticide_id] = Math.max(...bedCounts);
}

これで、畝を選択した場合もハウス全体を見る場合も、正しくカウントされるようになりました。

完成した機能

1. ハウス選択でリセット履歴を表示

図7: ハウス選択時の表示
  • 現在期間: 1回
  • リセット履歴:
    • 1回目: 2回使用(12/21〜12/23)
    • 2回目: 1回使用(12/23〜12/26)
  • 全期間: 4回

2. リセット機能

ハウスを選択すると、リセットボタンが表示されます。

リセットすると:

  • 現在のカウント期間が終了
  • 新しい期間が開始
  • 過去のカウントは履歴として保存

データをどうやって実装したか

データの取得タイミングを使い分ける

Next.js App Routerでは、どこでデータを取得するかが重要です。

今回は以下のように使い分けました:

取得場所取得タイミング用途
Server Component
(page.tsx)
ページを開いた時農薬マスタ、ハウス一覧など
変わらないデータ
Client Component
(useEffect)
ハウスや畝を選択した時リセット履歴など
選択に応じて変わるデータ
Server Actionsリセットボタンを押した時データの更新
// page.tsx(Server Component)
export default async function Page() {
  // 初回データ取得
  const { data } = await supabase.from("...").select();
  return <Component data={data} />;
}

// actions.ts(Server Action)
"use server";
export async function getResetHistoryAction(fieldId: number) {
  // 動的データ取得
  const { data } = await supabase.from("...").select();
  return data;
}

// Client Component
useEffect(() => {
  // Server Actionを呼び出し
  getResetHistoryAction(fieldId).then(setData);
}, [fieldId]);

具体例:リセット履歴の取得

最初は「ページを開いた時に全ての履歴を取得する」方式を試しました。

しかし、これだと:

  • ❌ 関係ないハウスの履歴も取得してしまう
  • ❌ 畝を選択しても表示が変わらない

そこで、ハウスや畝を選択した時に、その都度データを取り直す方式に変更しました。

// ハウスや畝が変更されたら、データを取り直す
useEffect(() => {
  if (selectedFieldId !== null) {
    // Server Actionを呼び出して、選択されたハウス・畝の履歴だけ取得
    getResetHistoryAction(selectedFieldId, selectedBedId)
      .then((data) => setResetHistory(data));
  }
}, [selectedFieldId, selectedBedId]); // ← この配列が重要!

最後の[selectedFieldId, selectedBedId]が重要で、これは「この2つの値が変わったら、上のコードを実行し直してね」という意味です。

つまり:

  • ✅ ハウスを変更 → リセット履歴を取り直す
  • ✅ 畝を変更 → リセット履歴を取り直す
  • ✅ 常に選択中のハウス・畝に合った履歴が表示される

この仕組みのおかげで、ユーザーの操作に応じて表示が動的に変わるようになりました。


目次