在庫・出荷管理アプリを作ってます④:出荷予定登録(Next.js × Supabase)

目次

前回のおさらい

これまでの実装で、以下の機能を作成しました:

今回は、流通業者が出荷予定を登録する機能を実装します。流通業者が事前に出荷予定を入力することで、後の工程で生産者が当日の出荷情報を確認できるようになります。

データベース設計(Supabase)

テーブル構造

planned_shipmentsテーブルと命名し、以下の構造で設計しました:

sql-- 実際に使用している主要フィールド
id              uuid            primary key (自動生成)
created_at      timestamptz     default: now()
created_by      uuid            誰が作ったか
destination     text            出荷先
shipment_date   date            出荷日
quantity        int4            数量 (箱)
note_caution    text            注意事項

-- その他のフィールド(将来的に使用予定)
image_url, tracking_code, destination_group, quantity_kg, 
total_quantity, group_id, price_code, price, status, destination_id

設計の経緯

実は、最初にChatGPTに相談した際に「考えられる項目は全部作っておいた方が困らない」とアドバイスされ、16項目のテーブルを作成しました。

しかし、実際に実装してみると使用しているのは7項目のみ。残りの項目は現時点では未使用です。

これが正解だったかどうかは、プロジェクト完成後に振り返ってみたいと思います。

Supabase RLS設定

セキュリティ面では、SupabaseのRow Level Security(RLS)を使用して権限制御を実装しました。

実装したポリシー

-- 削除:自分が登録したもののみ
CREATE POLICY "DELETE: 自分が登録したもののみ削除可"
ON "public"."planned_shipments"
FOR DELETE
USING ((created_by = auth.uid()));

-- 登録:流通業者・管理者のみ
CREATE POLICY "INSERT: 流通業者・管理者のみ登録可"
ON "public"."planned_shipments"
FOR INSERT
WITH CHECK (
(role() = ANY (ARRAY['trader'::text, 'admin'::text])) AND
(created_by = auth.uid())
);

-- 閲覧:流通業者・管理者のみ
CREATE POLICY "SELECT: 流通業者・管理者のみ閲覧可"
ON "public"."planned_shipments"
FOR SELECT
USING (role() = ANY (ARRAY['trader'::text, 'admin'::text]));

-- 更新:自分が登録したもののみ
CREATE POLICY "UPDATE: 自分が登録したもののみ編集可"
ON "public"."planned_shipments"
FOR UPDATE
USING ((created_by = auth.uid()))
WITH CHECK (
(role() = ANY (ARRAY['trader'::text, 'admin'::text])) AND
(created_by = auth.uid())
);

RLSの利点

  • フロントエンドで権限チェック不要:データベースレベルで制御
  • セキュリティが確実:APIを直接叩かれても安全
  • シンプルな実装:フロントエンドのコードがスッキリ

フォーム実装

レスポンシブ対応

PC版とモバイル版で異なるUIを提供しています:

typescript// カスタムフックで画面サイズを判定
export function useIsMobile(breakpoint = 768) {
  const [isMobile, setIsMobile] = useState(false);
  
  useEffect(() => {
    if (typeof window === "undefined") return;
    
    const mediaQuery = window.matchMedia(`(max-width: ${breakpoint}px)`);
    setIsMobile(mediaQuery.matches);
    
    const handleResize = (e: MediaQueryListEvent) => {
      setIsMobile(e.matches);
    };
    
    mediaQuery.addEventListener("change", handleResize);
    return () => mediaQuery.removeEventListener("change", handleResize);
  }, [breakpoint]);
  
  return isMobile;
}
typescript// 使用例
const isMobile = useIsMobile();

return (
  <div>
    {isMobile ? (
      <MobileFlexibleShipmentTable {...props} />
    ) : (
      <FlexibleShipmentTable {...props} />
    )}
  </div>
);

PC版:表形式で一括入力

  • 複数行を表形式で表示
  • 「行を追加」ボタンで動的に行を追加
  • 一括登録処理

モバイル版:縦並び入力

  • 各項目を縦に並べて表示
  • スマホでの操作性を重視
  • 1件ずつ登録する形式

登録処理の実装

メイン処理

typescriptconst handleSubmit = async () => {
  // 入力値のバリデーション
  const validRows = rows.filter((r) => r.date && r.destination);
  
  // データベース用のデータ整形
  const insertData = validRows.map((row) => ({
    created_by: userId,
    destination: row.destination,
    shipment_date: row.date,
    quantity: row.quantity,
    note_caution: row.note_caution,
  }));
  
  // Supabaseへの一括登録
  const { error } = await supabase
    .from("planned_shipments")
    .insert(insertData);
  
  if (error) {
    alert("登録失敗: " + error.message);
  } else {
    alert("出荷予定を登録しました!");
    // フォームをリセット
    setRows(
      Array(5).fill({ date: "", destination: "", quantity: "", note_caution: "" })
    );
  }
};

出荷先マスタとの連携

typescript// 出荷先のリストを取得
useEffect(() => {
  const fetchDestinations = async () => {
    const { data, error } = await supabase
      .from("destinations")
      .select("name");
    if (!error && data) {
      setDestinations(data.map((d) => d.name));
    }
  };
  fetchDestinations();
}, []);

③で登録したマスタデータを活用し、プルダウンで出荷先を選択できるようにしています。

実装のポイント

1. バリデーション

  • 必須項目チェック:日付と出荷先が入力されている行のみ登録
  • 空行の除外:入力されていない行は自動的に除外

2. UX配慮

  • エラーハンドリング:登録失敗時にわかりやすいメッセージ
  • 成功時の処理:登録完了後にフォームをリセット
  • 行の追加:必要に応じて入力行を増やせる

3. 権限制御

  • 認証チェック:ログインしていない場合はログイン画面へリダイレクト
  • RLS連携:データベースレベルでの権限制御

動作確認

実際に流通業者アカウントでログインし、出荷予定を登録してSupabaseの管理画面で確認しました:

  • ✅ 複数行の一括登録が正常に動作
  • ✅ RLSによる権限制御が機能
  • ✅ PC・モバイル両方でレスポンシブ対応
  • ✅ 出荷先マスタとの連携が正常

今後の課題

  1. バリデーション強化:数量の範囲チェックなど
  2. 編集機能:登録済みデータの修正
  3. 検索・フィルタ:大量データの管理

次回予告

今回の実装では、コンポーネント化を積極的に行い、PC用とモバイル用で別々のコンポーネントを作成しました。

しかし、実装してみて「本当にコンポーネント化する必要があったのか?」「ベタ書きでも良かったのでは?」という疑問が浮かんできました。

次回は、React初心者がコンポーネント化してみた結果について、リアルな体験談を踏まえて詳しく検証してみたいと思います。

目次