従業員用WEBアプリ 要件定義書

AIへの一括プロンプト用 — このドキュメント全文を貼り付けるだけでシステムを構築可能

項目内容
バージョン1.1(2026-03-14)
実績システムランチデリ(クリニック向け弁当注文システム)
構築所要時間約1セッション(AIプロンプト1回)

1. システム概要・目的

1.1 概要

小〜中規模事業所(クリニック・施設等)の従業員が、社内で提供される弁当を スマホ/PCから注文し、管理者がスプレッドシートで集計・帳票出力するWebアプリケーション。

1.2 解決する課題

1.3 利用者

ロール人数目安利用デバイス主な操作
従業員5〜50名スマホ(メイン)注文・履歴確認
管理者1〜3名PC(メイン)メニュー管理・発注集計・帳票出力
責任者(例: 院長)1名-全メニュー自動注文(操作不要)

1.4 システム構成図

[ブラウザ(スマホ/PC)] | v [Firebase Hosting] ── React + TypeScript + Vite(SPA) | +──> [Firebase Auth] メール+パスワード認証 +──> [Cloud Firestore] 注文・メニュー・ユーザー・設定 +──> [GAS WebアプリURL] スプレッドシート連携 API | v [Google Apps Script] コンテナバインド・V8ランタイム | v [Googleスプレッドシート] ├── ダッシュボード 注文データ一覧 ├── 受取サイン表 印刷用帳票 └── 労務士提出用 月次レポート

2. 技術スタック

レイヤー技術バージョン備考
フレームワークReact + TypeScript19.x / 5.xVite でビルド
ビルドツールVite7.x
ホスティングFirebase Hosting-CDN配信
認証Firebase Auth-メール+パスワード
データベースCloud Firestore-リアルタイム購読対応
帳票エンジンGoogle Apps Script(V8)-コンテナバインド
スプレッドシートGoogle Sheets-GAS経由で読み書き
アイコンlucide-react-
ルーティングreact-router-dom7.x
E2EテストPlaywright-オプション
注意: Vercel Serverless Functionsによるデータ永続化は禁止。 データは全てFirestoreに保存する。Google Sheetsはデータベースではなくレポート出力先として使用する。

3. データモデル(Firestore)

3.1 コレクション一覧

users

フィールド必須説明
uidstring*Firebase Auth UID(ドキュメントID)
namestring*氏名
emailstring*メールアドレス
employeeNumberstring*社員番号
isAdminboolean*管理者フラグ
createdAtTimestamp*登録日時

menu_items

フィールド必須説明
itemIdstring*ドキュメントID(自動生成)
namestring*メニュー名
descriptionstring説明
pricenumber*価格(税込)
storeNamestring店舗名
imageUrlstring画像URL
minOrderQuantitynumber最低注文数(省略時は制限なし)
isActiveboolean*有効/無効
createdAtTimestamp*

deliverySchedules

フィールド必須説明
scheduleIdstring*ドキュメントID
menuItemIdstring*紐づくメニューID
orderDeadlineTimestamp*注文締切日時
deliveryDateDay1Timestamp*配送日1
deliveryDateDay2Timestamp*配送日2
isActiveboolean*有効/無効
createdAtTimestamp*

orders

v1.1変更: 個人情報最小化のため userName フィールドを廃止。 管理者画面での表示時は userId から users コレクションをlookupして取得する。
フィールド必須説明
orderIdstring*ドキュメントID
userIdstring*注文者UID
menuItemIdstring*メニューID
scheduleIdstring*配送スケジュールID
deliveryChoicestring*day1 / day2 / day1_flexible / day2_flexible / both
deliveryDateTimestamp*元の配送日(day1ベース)
confirmedDeliveryDateTimestamp確定配送日(管理者が設定)
statusstring*pending / delivered / cancelled
createdAtTimestamp*

invited_users

フィールド必須説明
emailstring*招待メール(ドキュメントID)
namestring*氏名
employeeNumberstring*社員番号
isAdminboolean*管理者フラグ
invitedAtTimestamp*

settings

フィールド必須説明
gasUrlstringGAS WebアプリURL
spreadsheetUrlstringスプレッドシートURL

audit_logs v1.1追加

フィールド必須説明
actionstring*操作種別(grant_admin / delete_user / add_menu 等)
targetIdstring操作対象のID
performedBystring*操作者のUID
performerEmailstring操作者のメールアドレス
timestampTimestamp*操作日時
detailsstring操作の詳細説明
監査ログの制約: クライアントからは create のみ可能。update / delete は禁止(改ざん防止)。 読み取りは管理者のみ。performedBy は認証済みユーザーのUID と一致しなければ書き込み拒否。

3.2 DeliveryChoice 型定義

type DeliveryChoice = 'day1' | 'day2' | 'day1_flexible' | 'day2_flexible' | 'both'

// day1: 配送日1を希望
// day2: 配送日2を希望
// day1_flexible: 配送日1希望だが変更可
// day2_flexible: 配送日2希望だが変更可
// both: 両日とも希望(責任者用)

4. ユーザーロールと認証

4.1 認証フロー

  1. 管理者が invited_users にメールアドレス・社員番号・氏名を事前登録
  2. 従業員が招待メールのリンクからアプリにアクセス
  3. 新規登録画面でメール+パスワードを設定
  4. 登録時に invited_users と照合し、一致すれば users コレクションに情報をコピー
  5. 招待にないメールアドレスは登録拒否

4.2 ロール

ロール判定権限
従業員isAdmin = false注文の作成・自分の注文閲覧/キャンセル・プロフィール編集
管理者isAdmin = true全注文の閲覧/編集/削除・メニュー管理・スケジュール管理・帳票出力・ユーザー招待・設定変更

4.3 AuthContext

React Contextで認証状態を管理。useAuth() フックで以下を提供:

アプリ初期化時にFirestoreの settings コレクションからGAS URLを読み込み、localStorageにキャッシュ。

5. 画面一覧・ルーティング

パス画面名アクセス概要
/loginログイン未認証メール+パスワード入力
/register新規登録未認証招待済みメールのみ登録可
/forgot-passwordパスワード再設定未認証リセットメール送信
/auth/action認証アクション全員メール内リンクの処理(パスワードリセット確認)
/ホーム認証必須注文可能なメニュー一覧、締切情報
/order注文認証必須メニュー選択→配送日選択→注文確定
/history注文履歴認証必須自分の注文一覧、キャンセル操作
/profileプロフィール認証必須氏名変更、パスワード変更
/admin管理ダッシュボード管理者タブ構成(注文/メニュー/スケジュール/ユーザー/設定)
未認証ユーザーが認証必須ページにアクセスした場合は /login にリダイレクト。 管理者以外が /admin にアクセスした場合は / にリダイレクト。

6. 機能要件(従業員側)

6.1 ホーム画面

6.2 注文画面

6.3 注文履歴

6.4 プロフィール

7. 機能要件(管理者側)

7.1 管理ダッシュボード — タブ構成

「注文」タブ

「メニュー」タブ

「スケジュール」タブ

「ユーザー」タブ

「設定」タブ

7.2 スプレッドシート自動同期

自動同期仕様(デバウンス + ハッシュ比較):
  1. 注文データ(orders)に変更検知
  2. 5秒のデバウンスタイマーを開始(連続変更を集約)
  3. 注文データのハッシュ値(orderId + status + confirmedDeliveryDate)を生成
  4. 前回同期時のハッシュと比較し、変更がなければスキップ
  5. 変更があればGASの syncOrders エンドポイントにPOST

7.3 配送日の確定フロー

8. Google Apps Script 連携

8.1 GAS設計方針

8.2 エンドポイント一覧

actionHTTP説明優先度
statusGETヘルスチェックMUST
syncOrdersPOST注文データをダッシュボードシートに同期MUST
generateSignSheetPOST受取サイン表を生成(selectedDates配列)MUST
generateLaborReportPOST労務士提出用レポートを生成(selectedDates配列)MUST
sendOrderConfirmationPOST注文確認メール送信SHOULD
sendInvitationEmailPOST招待メール送信SHOULD
sendDeadlineReminderPOST締切リマインダーメールNICE
sendDeliveryReminderPOST配送前リマインダーメールNICE
setupTriggersPOST自動リマインダートリガー設定NICE

8.3 受取サイン表の仕様

金銭管理に関わるため、以下の仕様を厳密に実装すること。

8.4 スプレッドシートのシート構成

シート名用途生成タイミング
ダッシュボード注文データ一覧(syncOrdersで更新)自動同期時
受取サイン表印刷用サイン表generateSignSheet実行時
労務士提出用月次レポートgenerateLaborReport実行時
設定システム設定(systemName, adminEmail等)initSettings実行時

9. ビジネスルール

9.1 注文ルール

  1. 注文は締切日時(orderDeadline)前のみ受付可能
  2. 同一ユーザーが同一スケジュールに重複注文は不可
  3. キャンセルは締切前のみ可能(管理者は締切後もキャンセル可)
  4. 配送済み(delivered)ステータスの注文はキャンセル不可

9.2 責任者(院長)ルール

社員番号1のユーザーは特別扱い:
  • 全メニューの全配送日に自動で注文が追加される
  • 受取サイン表・集計に自動的に含まれる
  • 実際にFirestoreに注文レコードがなくても、帳票生成時にemployeeDataに追加

9.3 招待ルール

  1. invited_users に登録されたメールアドレスのみ新規登録可能
  2. 管理者フラグは招待時に設定(登録時に自分でisAdmin=trueにすることは不可)
  3. 招待メールにはアプリURLと登録手順を記載

9.4 配送日ルール

  1. 各メニューに配送日1(day1)と配送日2(day2)の2日間を設定
  2. 従業員は注文時にどちらの日を希望するか選択
  3. flexible 付きは「この日希望だが変更OK」の意味
  4. 管理者は注文数に応じて配送日を調整可能
  5. スプレッドシート出力時は deliveryChoice に基づいて正しい日付を使用

10. セキュリティ要件(v1.1強化)

v1.1で大幅に強化。 個人情報(氏名・メール・連絡先・社員番号)は管理者(isAdmin=true)のみがアクセス可能。 一般従業員は自分自身のデータのみ閲覧できる。

10.1 アクセス制御方針

コレクションget(個別取得)list(一覧取得)createupdatedelete
users本人 or 管理者管理者のみ本人(isAdmin設定禁止)本人(isAdmin変更不可)or 管理者管理者のみ
orders本人 or 管理者本人 or 管理者本人のuserIdのみ本人 or 管理者本人 or 管理者
invited_users認証済み(登録確認用)管理者のみ管理者のみ管理者のみ管理者のみ
menu_items認証済み認証済み管理者のみ管理者のみ管理者のみ
deliverySchedules認証済み認証済み管理者のみ管理者のみ管理者のみ
settings管理者のみ管理者のみ管理者のみ管理者のみ管理者のみ
app_settings管理者のみ管理者のみ管理者のみ管理者のみ管理者のみ
audit_logs管理者のみ管理者のみ認証済み(本人UIDのみ)禁止禁止

10.2 Firestore Security Rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function isAdmin() {
      return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true;
    }
    function isOwner(docUserId) { return request.auth.uid == docUserId; }
    function isAuthenticated() { return request.auth != null; }

    match /invited_users/{email} {
      allow get: if isAuthenticated();
      allow list: if isAuthenticated() && isAdmin();
      allow write: if isAuthenticated() && isAdmin();
    }

    match /users/{userId} {
      allow get: if isAuthenticated() && (isOwner(userId) || isAdmin());
      allow list: if isAuthenticated() && isAdmin();
      allow create: if isAuthenticated() && request.auth.uid == userId
        && !("isAdmin" in request.resource.data);
      allow update: if isAuthenticated() && (
        (isOwner(userId) && !("isAdmin" in request.resource.data
          && request.resource.data.isAdmin != resource.data.isAdmin))
        || isAdmin());
      allow delete: if isAuthenticated() && isAdmin();
    }

    match /orders/{orderId} {
      allow read: if isAuthenticated() &&
        (resource.data.userId == request.auth.uid || isAdmin());
      allow create: if isAuthenticated() &&
        request.resource.data.userId == request.auth.uid;
      allow update, delete: if isAuthenticated() &&
        (resource.data.userId == request.auth.uid || isAdmin());
    }

    match /menu_items/{itemId} {
      allow read: if isAuthenticated();
      allow write: if isAuthenticated() && isAdmin();
    }
    match /deliverySchedules/{scheduleId} {
      allow read: if isAuthenticated();
      allow write: if isAuthenticated() && isAdmin();
    }
    match /app_settings/{settingId} {
      allow read: if isAuthenticated() && isAdmin();
      allow write: if isAuthenticated() && isAdmin();
    }
    match /settings/{settingId} {
      allow read: if isAuthenticated() && isAdmin();
      allow write: if isAuthenticated() && isAdmin();
    }
    match /audit_logs/{logId} {
      allow read: if isAuthenticated() && isAdmin();
      allow create: if isAuthenticated() &&
        request.resource.data.performedBy == request.auth.uid;
      allow update, delete: if false;
    }
  }
}

10.3 個人情報最小化

10.4 監査ログ

10.5 セッション管理

10.6 HTTPセキュリティヘッダー(Firebase Hosting)

ヘッダー目的
Content-Security-Policydefault-src 'self'; script-src 'self' google/gstatic; ...XSS防止
X-Content-Type-OptionsnosniffMIMEスニッフィング防止
X-Frame-OptionsDENYクリックジャッキング防止
Referrer-Policystrict-origin-when-cross-originリファラー情報の制限
Permissions-Policycamera=(), microphone=(), geolocation=()不要なAPI無効化

10.7 App Check(オプション — reCAPTCHA v3)

10.8 その他セキュリティ要件

11. 非機能要件

項目要件
レスポンシブスマホファースト設計。モバイル(375px〜)〜デスクトップ(1280px)対応
パフォーマンス初回ロード3秒以内(Firebase Hosting CDN)
リアルタイム性注文データはFirestore onSnapshot でリアルタイム更新
同時接続50名程度の同時利用を想定
印刷対応サイン表・労務士レポートはA4印刷に最適化
アクセシビリティコントラスト比4.5:1以上、色だけで情報を伝えない
PWAインストールプロンプト表示(オプション)

12. デプロイ構成

12.1 Firebase プロジェクト設定

# Firebase 初期化
firebase init
# → Hosting (dist) + Firestore を選択

# 環境変数(.env)
VITE_FIREBASE_API_KEY=xxx
VITE_FIREBASE_AUTH_DOMAIN=xxx.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=xxx
VITE_FIREBASE_STORAGE_BUCKET=xxx
VITE_FIREBASE_MESSAGING_SENDER_ID=xxx
VITE_FIREBASE_APP_ID=xxx

12.2 デプロイコマンド

# Webアプリのビルド&デプロイ
npx vite build && firebase deploy --only hosting

# Firestore Security Rules のデプロイ
firebase deploy --only firestore:rules

# GAS のデプロイ(clasp使用時)
cd gas && clasp push --force && clasp deploy -i "DEPLOYMENT_ID" -d "説明"

12.3 GAS の手動セットアップ

  1. Googleスプレッドシートを作成
  2. 「拡張機能」→「Apps Script」でエディタを開く
  3. gas/Code.gs の内容を貼り付けて保存
  4. 「デプロイ」→「新しいデプロイ」→ ウェブアプリ(実行: 自分、アクセス: 全員)
  5. 承認して表示されたURLをアプリの設定画面に登録

13. AIプロンプト(コピペ用)

以下のプロンプトをAIに貼り付けるだけで、本システムの基盤を一括生成できます。 カスタマイズ箇所は 【...】 で示しています。

以下の要件で従業員用WEBアプリを作成してください。

## プロジェクト名
【例: ランチデリ — お弁当注文システム】

## 技術スタック
- React 19 + TypeScript + Vite 7(SPA)
- Firebase Hosting(CDN配信)
- Firebase Auth(メール+パスワード認証)
- Cloud Firestore(データベース)
- Google Apps Script(帳票生成・メール送信)
- lucide-react(アイコン)
- react-router-dom v7(ルーティング)

## ユーザーロール
1. 従業員(isAdmin=false): 注文の作成・自分の注文閲覧/キャンセル
2. 管理者(isAdmin=true): 全機能アクセス
3. 責任者(社員番号1): 全メニュー・全配送日に自動注文

## 認証フロー
- 管理者が invited_users コレクションにメール・氏名・社員番号を事前登録
- 従業員は招待済みメールでのみ新規登録可能
- 登録時 isAdmin=true の自己設定は Firestore Security Rules で禁止

## Firestoreコレクション

### users(ドキュメントID = Firebase Auth UID)
- name: string, email: string, employeeNumber: string
- isAdmin: boolean, createdAt: Timestamp

### menu_items(自動ID)
- name: string, description?: string, price: number
- storeName?: string, imageUrl?: string
- minOrderQuantity?: number(未入力時はフィールド自体を保存しない)
- isActive: boolean, createdAt: Timestamp

### deliverySchedules(自動ID)
- menuItemId: string, orderDeadline: Timestamp
- deliveryDateDay1: Timestamp, deliveryDateDay2: Timestamp
- isActive: boolean, createdAt: Timestamp

### orders(自動ID)— userNameは保存しない(個人情報最小化)
- userId: string, menuItemId: string, scheduleId: string
- deliveryChoice: 'day1'|'day2'|'day1_flexible'|'day2_flexible'|'both'
- deliveryDate: Timestamp, confirmedDeliveryDate?: Timestamp
- status: 'pending'|'delivered'|'cancelled'
- createdAt: Timestamp
- ※ 管理者画面でのユーザー名表示は userId から users コレクションをlookup

### audit_logs(自動ID)— 監査ログ(改ざん不可)
- action: string, targetId?: string
- performedBy: string(操作者UID), performerEmail?: string
- timestamp: Timestamp, details?: string

### invited_users(ドキュメントID = email)
- name: string, employeeNumber: string
- isAdmin: boolean, invitedAt: Timestamp

### settings(ドキュメントID = 'main')
- gasUrl: string, spreadsheetUrl: string

## 画面一覧
| パス | 画面 | アクセス |
|------|------|----------|
| /login | ログイン | 未認証 |
| /register | 新規登録(招待メール照合) | 未認証 |
| /forgot-password | パスワード再設定 | 未認証 |
| /auth/action | メールリンク処理 | 全員 |
| / | ホーム(注文可能メニュー一覧) | 認証必須 |
| /order | 注文(メニュー選択→配送日選択→確定) | 認証必須 |
| /history | 注文履歴(キャンセル操作あり) | 認証必須 |
| /profile | プロフィール編集 | 認証必須 |
| /admin | 管理ダッシュボード(タブ構成) | 管理者のみ |

## 管理ダッシュボードのタブ
1. **注文タブ**: 発注サマリー(配送日別カード)+ 注文一覧
2. **メニュータブ**: CRUD + コピー + 有効/無効切替
3. **スケジュールタブ**: 配送日1/2・締切日の設定
4. **ユーザータブ**: 招待管理 + 登録済みユーザー一覧
5. **設定タブ**: GAS URL・スプレッドシートURL設定

## 発注サマリーUI仕様
- 配送日ごとにカードを横並び(日付順、背景色は日付ごとに色分け)
- カード全体がタップで選択/解除(checkboxはビジュアルのみ)
- 右上 x ボタンのみが個別削除(stopPropagationで選択と分離)
- 全選択チェックボックス + 一括削除ボタン
- 選択状態のカードに青い枠線(#3b82f6)
- 配送済みカードは opacity:0.6 +「配送済み」ラベル
- カード内メニュー名タップで変更依頼モーダル(stopPropagation)

## スプレッドシート自動同期
- orders変更検知 → 5秒デバウンス → ハッシュ比較 → 変更時のみGAS POST
- ハッシュ = orders.map(o => `${o.orderId}:${o.status}:${o.confirmedDeliveryDate}`).join('|')
- 手動同期ボタンも設置

## GAS(Google Apps Script)
- コンテナバインドスクリプト(V8ランタイム)
- executeAs: USER_DEPLOYING / access: ANYONE_ANONYMOUS
- doGet: action=status でヘルスチェック
- doPost: action パラメータでルーティング

### 必須エンドポイント
- syncOrders: 注文データをダッシュボードシートに同期
- generateSignSheet(selectedDates): 受取サイン表を生成
- generateLaborReport(selectedDates): 労務士レポートを生成

### 受取サイン表の仕様(金銭管理に関わる — 厳密に実装)
- 入力: selectedDates(ISO日付配列)でフィルタ(月や期間ではなく完全一致)
- 上部テーブル: 行3=メニュー名(結合)、行4=日付|サイン|...
- メニュー順: 最も早い配送日を持つメニューが左
- 日付ソート: parseDateLabel()でDate変換し実値比較(文字列ソート禁止)
- 責任者(社員番号1)は全メニュー・全日に自動追加
- 下部テーブル(集計):
  - ヘッダー: メニュー(A-B列結合)| 日付1 | 日付2 | ... | 合計
  - A-B結合のため日付列はC列から開始(B列に日付を置くと結合で隠れる)
  - 個数はemployeeData(責任者自動追加済み)からカウント

## undefinedフィルタリング
Firestoreへの書き込み前に undefined 値を除去するヘルパー関数を使用:
```typescript
function removeUndefined(obj: Record<string, unknown>): Record<string, unknown> {
  const cleaned: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(obj)) {
    if (value !== undefined) { cleaned[key] = value; }
  }
  return cleaned;
}
```

## Firestore Security Rules(v1.1強化)
- users: get=本人or管理者、list=管理者のみ、create=本人(isAdmin禁止)、update=本人(isAdmin変更不可)or管理者
- orders: read=本人or管理者、create=本人のuserIdのみ、update/delete=本人or管理者
- invited_users: get=認証済み、list/write=管理者のみ
- menu_items / deliverySchedules: read=認証済み、write=管理者のみ
- settings / app_settings: read/write=管理者のみ
- audit_logs: read=管理者のみ、create=認証済み(performedBy=本人UID)、update/delete=禁止

## セキュリティ対策
- 個人情報最小化: ordersにuserNameを保存しない(lookup方式)
- 監査ログ: 管理者操作を自動記録(改ざん不可)
- セッション管理: 30分無操作で自動ログアウト
- HTTPヘッダー: CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy
- App Check: reCAPTCHA v3で不正クライアント防止(オプション)

## 環境変数(.env)
VITE_FIREBASE_API_KEY, VITE_FIREBASE_AUTH_DOMAIN,
VITE_FIREBASE_PROJECT_ID, VITE_FIREBASE_STORAGE_BUCKET,
VITE_FIREBASE_MESSAGING_SENDER_ID, VITE_FIREBASE_APP_ID

## 非機能要件
- スマホファースト(375px〜1280px対応)
- リアルタイム更新(Firestore onSnapshot)
- A4印刷最適化(サイン表・レポート)
- アクセシビリティ: コントラスト比4.5:1以上

## 注意事項
- Google SheetsはDBではなくレポート出力先として使用
- Vercel Serverless Functionsでのデータ永続化は禁止
- イミュータブルパターン(オブジェクト変異禁止)
- 日本語UIとコメント