在庫・出荷管理アプリを作ってます⑨:出荷先マスタの全面改善 〜基本機能から本格システムへ〜

目次

はじめに

前回からUI/UXの改善を進めています。
基本的な出荷先マスタ機能は実装しましたが、
今回は、この出荷先マスタを実運用レベルまで改善した過程を記録します。

改善前の課題整理

1. 出荷グループの未実装

  • 問題:データベースには格納されているが、画面に反映されていない
  • 影響:分類管理ができない、検索性が低い

2. 削除確認の不在

  • 問題:削除ボタンを押すと何の確認もなく即座に削除される
  • 影響:誤削除のリスク、ユーザーの不安感

3. UIフィードバックの不足

  • 問題:ホバー効果がない、クリック可能かわからない
  • 影響:操作性の悪さ、直感的でないUI

4. 機能不足

  • 問題:検索・フィルター機能がない
  • 影響:データが増えると管理困難

5. コンポーネント設計の課題

  • 問題:3つのファイルに分割したが再利用性に疑問
  • 影響:保守性、拡張性の問題

段階的改善プロセス

Phase 1: 検索・フィルター機能の実装

Before(改善前)

// 並び替えのみの単純な実装
const sorted = [...destinations].sort((a, b) => {
  if (sortOrder === "name") {
    return a.name.localeCompare(b.name);
  } else {
    return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
  }
});

After(改善後)

// 複合的な検索・フィルター・ソート処理
useEffect(() => {
  let filtered = [...destinations];
  
  // グループフィルタ
  if (selectedGroup !== "all") {
    if (selectedGroup === "unset") {
      filtered = filtered.filter(d => !d.destination_group);
    } else {
      filtered = filtered.filter(d => d.destination_group === selectedGroup);
    }
  }
  
  // 検索フィルタ
  if (searchTerm) {
    filtered = filtered.filter(d =>
      d.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }
  
  // ソート
  filtered.sort((a, b) => {
    if (sortOrder === "name") {
      return a.name.localeCompare(b.name, "ja");
    } else {
      return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
    }
  });
  
  setFilteredDestinations(filtered);
}, [destinations, selectedGroup, searchTerm, sortOrder]);

改善内容:

  • 出荷先名での部分一致検索
  • グループでの絞り込み(「未設定」グループも含む)
  • 適用中フィルターの視覚化(タグ表示)
  • 日本語ロケール対応の並び替え

Phase 2: 削除確認ダイアログの実装

Before(改善前)

// 即座に削除される危険な実装
<button
  onClick={() => onDelete(d.id)}
  className="text-red-600 underline"
>
  削除
</button>

After(改善後)

// 2段階確認の安全な実装
const [deleteConfirm, setDeleteConfirm] = useState<{
  id: string;
  name: string;
} | null>(null);

const handleDeleteClick = (destination: Destination) => {
  setDeleteConfirm({
    id: destination.id,
    name: destination.name,
  });
};

// 削除確認ダイアログ(モーダル)
{deleteConfirm && (
  <div className="fixed inset-0 bg-black bg-opacity-50">
    <div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
      {/* 警告アイコン + 確認メッセージ + 対象データ表示 */}
      <div className="flex space-x-3">
        <button onClick={handleDeleteCancel}>キャンセル</button>
        <button onClick={handleDeleteConfirm}>削除する</button>
      </div>
    </div>
  </div>
)}

改善内容:

  • 削除対象の明確な表示
  • 警告アイコンでの視覚的注意喚起
  • 取り消し不可の明示
  • キャンセル・実行の明確な選択肢

Phase 3: UI/UX全面改善

統計情報の追加

// リアルタイム統計表示
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
  <div className="bg-white p-4 rounded-lg border">
    <div className="text-2xl font-bold text-blue-600">{destinations.length}</div>
    <div className="text-sm text-gray-600">総登録数</div>
  </div>
  <div className="bg-white p-4 rounded-lg border">
    <div className="text-2xl font-bold text-green-600">{availableGroups.length}</div>
    <div className="text-sm text-gray-600">グループ数</div>
  </div>
  <div className="bg-white p-4 rounded-lg border">
    <div className="text-2xl font-bold text-orange-600">{filteredDestinations.length}</div>
    <div className="text-sm text-gray-600">表示中</div>
  </div>
</div>

ホバー効果・フィードバック改善

// Before: フィードバックなし
<button className="text-red-600 underline">削除</button>

// After: 適切なフィードバック
<button className="text-red-600 hover:text-red-800 hover:bg-red-50 px-3 py-1 rounded-lg transition duration-200">
  削除
</button>

Phase 4: 出荷グループ機能の完全実装

新機能:グループ管理システム

  1. グループ専用管理ページ ( /destinations/groups )
  2. 色分け管理 – 視覚的な分類
  3. 有効/無効切り替え – 論理削除対応
  4. CRUD完備 – 新規作成、編集、削除

新規登録での連携

// プルダウンでのグループ選択
<MobileSelect
  id="destination-group"
  value={groupId}
  onChange={(e) => setGroupId(e.target.value)}
>
  <option value="">グループを選択(任意)</option>
  {availableGroups.map((group) => (
    <option key={group.id} value={group.id}>
      {group.name}
      {group.description && ` - ${group.description}`}
    </option>
  ))}
</MobileSelect>

// 選択中グループのプレビュー
{groupId && (
  <div className="mt-3 p-3 bg-blue-50 rounded-lg">
    <div className="flex items-center space-x-2">
      <div
        className="w-4 h-4 rounded-full"
        style={{ backgroundColor: selectedGroup.color }}
      ></div>
      <span className="text-sm font-medium text-blue-800">
        {selectedGroup.name}
      </span>
    </div>
  </div>
)}

Phase 5: レスポンシブ対応強化

MobileInputコンポーネントの活用

// PC/モバイル両対応の入力フィールド
<MobileInput
  id="search"
  type="search"
  label="出荷先名で検索"
  placeholder="出荷先名を入力..."
  value={searchTerm}
  onChange={(e) => setSearchTerm(e.target.value)}
  className="md:!px-3 md:!py-2 md:!text-sm md:!rounded-lg"
/>

改善の成果

定量的改善

項目改善前改善後
機能数基本CRUD(4機能)検索・フィルター・統計など(12機能+)
画面数1画面3画面(一覧・新規・グループ管理)
削除安全性確認なし2段階確認
検索性なし名前検索+グループフィルター
UI要素数最小限統計カード・フィルタータグ・モーダル等

定性的改善

  • 第一印象:シンプルすぎる → プロフェッショナル
  • 操作性:不安になる → 安心して使える
  • 管理性:データ増加で破綻 → スケーラブル
  • 拡張性:限定的 → グループ管理で分類可能

ビフォーアフター

[改善前の画像]

  • 最小限の項目のみ
  • 検索機能なし
  • 新規登録と登録一覧が同じ画面

[改善後の画像]

  • 検索・フィルター完備
  • 統計情報表示
  • 削除確認ダイアログ
  • グループ管理機能
  • 新規登録を別画面に

実装のポイント

1. 段階的改善のアプローチ

一度に全てを変更するのではなく、機能ごとに段階的に改善することで、既存機能を壊すリスクを最小化しました。

2. ユーザー体験重視の設計

「動く」だけでなく「使いやすい」を重視し、確認ダイアログや統計表示などのUX向上に注力しました。

3. 実用性を考慮した機能選択

理論的に可能な機能ではなく、農業現場で実際に必要になる機能(グループ分け、検索等)を優先実装しました。

4. レスポンシブ設計の徹底

PC・モバイル両対応のコンポーネント(MobileInput等)を活用し、どのデバイスでも使いやすいUIを実現しました。

技術的な学び

1. コンポーネント分離の効果

当初疑問視していた3ファイル分離は、今回の大幅改善時に威力を発揮しました。機能ごとの責任分離により、安全に改修できました。

2. 状態管理の重要性

検索・フィルター・ソート状態の管理により、ユーザーの操作状況を適切に保持し、直感的なUXを提供できました。

3. バリデーションとエラーハンドリング

ユーザーの誤操作を防ぐ仕組み(削除確認、入力検証等)の重要性を改めて実感しました。

今後の展望

短期的改善

  • 一括編集機能の追加
  • インポート・エクスポート機能
  • より高度な検索(部分一致、正規表現等)

中長期的発展

  • 出荷実績データとの連携分析
  • 使用頻度に基づく自動並び替え
  • AIを活用した出荷先推薦機能

まとめ

基本的なCRUD機能から本格的な業務システムレベルへの改善を通じて、「機能を作る」と「システムを育てる」の違いを実感しました。

特に農業のような現場重視の分野では、技術的な実装よりもユーザビリティと実用性が成功の鍵になることを学びました。

次回は、この改善された出荷先マスタを活用した出荷予定登録機能の改善について記録予定です。

関連記事

目次