目次
はじめに
前回の記事では、作業履歴画面で肥料・農薬の詳細を表示できるようにしました。
今回は、作業履歴の検索・フィルタ機能を大幅に強化した話をお届けします。
特に考えたのはカレンダー機能の実装です。実は出荷アプリと栽培アプリで異なるアプローチを採用しています。
今回実装した機能:
- 折りたたみ式カレンダー表示
- 作業種類をボタン式フィルタに変更
- カレンダーとフィルタの連動
カレンダー実装の2つのアプローチ
私は現在、出荷管理アプリと栽培管理アプリの2つを開発しています。どちらもカレンダー機能が必要でしたが、用途に応じて実装方法を変えました。
出荷アプリ:<input type="date"> を採用

出荷アプリでは、HTML標準の<input type="date">を採用しました。
用途
出荷予定日を入力するためのシンプルなカレンダー
実装コード
<input type="date" />
選んだ理由
- 日付の入力だけが目的
- ブラウザ標準機能で十分
- 実装コストがほぼゼロ
- モバイル対応も自動
メリット
- ✅ ライブラリ不要
- ✅ 実装が超簡単(1行で完結)
- ✅ OS標準のUIで使いやすい
- ✅ 入力バリデーションも自動
- ✅ アクセシビリティ対応済み
デメリット
- ❌ 見た目のカスタマイズができない
- ❌ 複雑な機能は追加できない
- ❌ 表示だけの用途には向かない
- ❌ ブラウザによって見た目が異なる
栽培アプリ:完全手作りカレンダー

栽培アプリでは、ライブラリを使わず完全手作りでカレンダーを実装しました。
用途
作業履歴を検索・フィルタリングするための表示用カレンダー
手作りにした理由
- 日付を表示して検索したい
- 作業があった日に●マークを表示したい
- フィルタと連動させたい
- デザインを完全にコントロールしたい
実装の核心部分
// 月の日数を計算
const daysInMonth = new Date(year, month + 1, 0).getDate();
// 月初の曜日を計算(0=日曜、6=土曜)
const firstDay = new Date(year, month, 1).getDay();
// 7列のグリッドで表示
<div style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: 4
}}>
{/* 曜日ヘッダー */}
{["日", "月", "火", "水", "木", "金", "土"].map(day => (
<div key={day}>{day}</div>
))}
{/* 空白セル(月初まで) */}
{Array.from({ length: firstDay }).map((_, i) => (
<div key={`empty-${i}`} />
))}
{/* 日付セル */}
{Array.from({ length: daysInMonth }).map((_, i) => {
const day = i + 1;
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const hasWork = workDates.has(dateStr);
return (
<div key={day} onClick={() => handleDateClick(dateStr)}>
{day}
{hasWork && <span>●</span>}
</div>
);
})}
</div>
メリット
- ✅ 完全にカスタマイズ可能
- ✅ 複雑な機能も実装できる
- ✅ フィルタとの連動が自由
- ✅ デザインを完全にコントロール
- ✅ 作業日の可視化が可能
デメリット
- ❌ 実装コストが高い
- ❌ Date計算のロジックが必要
- ❌ バグの可能性がある
- ❌ メンテナンスコストがかかる
栽培アプリで実装した機能
栽培アプリで実装した具体的な機能です。

1. 折りたたみ式カレンダー
仕様
- 初期状態は閉じている
- 「カレンダーを表示」ボタンで展開
- 月の切り替え(◀ ▶ ボタン)
- 作業があった日に緑の●マーク
- 日付クリックでその日の作業だけ表示
2. 作業種類をボタン式に変更
Before:プルダウン選択

<select value={filterLogType}>
<option value="">すべて</option>
<option value="灌水">灌水</option>
<option value="施肥">施肥</option>
<option value="防除">防除</option>
</select>
After:ボタン式(トグル)

<button onClick={() => handleToggle("灌水")}>
灌水
</button>
<button onClick={() => handleToggle("施肥")}>
施肥
</button>
<button onClick={() => handleToggle("防除")}>
防除
</button>
<button onClick={() => handleToggle("その他")}>
その他
</button>
改善ポイント
- ✅ ワンクリックでフィルタ適用
- ✅ 視認性が向上(アイコン付き)
- ✅ トグル式で直感的
- ✅ 選択状態が色で分かりやすい
トグルロジック
const handleLogTypeToggle = (type: string) => {
if (filterLogType === type) {
setFilterLogType(null); // 同じボタン → 解除
} else {
setFilterLogType(type); // 別のボタン → 適用
}
};
3. カレンダーとフィルタの連動
【画像挿入位置】フィルタ連動のデモGIF(灌水ボタン押すと●が変わる)
仕様
- 灌水ボタンを押す → 灌水した日だけ●が表示
- ハウスを選択 → そのハウスで作業した日だけ●が表示
- 複数フィルタの組み合わせが可能
- 例:「No.1ハウス」+「灌水」= No.1で灌水した日だけ●
実装の核心
// フィルタ適用後のログから作業日を集計
const workDates = useMemo(() => {
const dates = new Set<string>();
logs.forEach(log => {
// ハウスフィルタ
if (filterFieldId && log.cult_fields.id !== filterFieldId) return;
// 畝フィルタ
if (filterBedId && log.cult_beds.id !== filterBedId) return;
// 作業種類フィルタ
if (filterLogType && log.log_type !== filterLogType) return;
// 日付フィルタは除外(カレンダー表示用)
// if (selectedDate && log.log_date !== selectedDate) return;
// 条件を満たした日付を追加
dates.add(log.log_date);
});
return dates;
}, [logs, filterFieldId, filterBedId, filterLogType]);
ポイント
useMemoで効率的に計算- フィルタ変更時に自動で再計算
- 日付フィルタは除外(選択中でも全ての作業日を表示)
工夫した点
1. 選択中でも他の作業日の●を表示
当初の仕様:
- 14日を選択 → 14日以外の●が消える
問題点:
- どの日に作業があるか分からなくなる
- 別の日を選びたいときに不便
改善後:
- 14日を選択 → 他の作業日も●が表示されたまま
- 選択中の日は緑色の背景で強調
実装の変更点:
// Before(日付フィルタを含めて集計)
const workDates = useMemo(() => {
return filteredLogs.map(log => log.log_date);
}, [filteredLogs]);
// After(日付フィルタを除外して集計)
const workDates = useMemo(() => {
const dates = new Set<string>();
logs.forEach(log => {
if (filterFieldId && log.cult_fields.id !== filterFieldId) return;
if (filterLogType && log.log_type !== filterLogType) return;
// selectedDate は除外!
dates.add(log.log_date);
});
return dates;
}, [logs, filterFieldId, filterLogType]); // selectedDate を依存配列から除外
2. 日付の直接切り替え
当初の仕様:
- 14日を選択 → 16日を選びたい
- 手順:14日をクリックして解除 → 16日をクリック(2回)
改善後:
- 14日を選択 → 16日をクリック(1回で切り替え)
実装:
const handleDateClick = (dateStr: string) => {
if (selectedDate === dateStr) {
setSelectedDate(null); // 同じ日付 → 解除
} else {
setSelectedDate(dateStr); // 別の日付 → 直接切り替え
}
};
シンプルですが、UXが大きく向上します!
まとめ
今回は作業履歴の検索機能を大幅に強化しました。
実装した機能:
- ✅ 折りたたみ式カレンダー
- ✅ ボタン式フィルタ
- ✅ カレンダーとフィルタの連動
- ✅ 日付選択機能
カレンダー実装の使い分け:
- 入力用 →
<input type="date">(出荷アプリ) - 表示・検索用 → 手作りカレンダー(栽培アプリ)
手作りカレンダーはこれが正解かどうか、ということもありますが、
しばらく実装してみて様子を見たいと思います。
