From 2499492b5177710679decb9df2302f6984840a07 Mon Sep 17 00:00:00 2001 From: y-natani Date: Tue, 10 Feb 2026 22:45:34 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EF=BD=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/ARCHITECTURE.md | 14 ++ doc/INDEX.md | 1 + ...37\350\203\275\344\273\225\346\247\230.md" | 0 ...25\343\203\210\347\256\241\347\220\206.md" | 119 +++++++++ .../DayTabs/index.stories.tsx | 19 +- .../StaffingRequirement/DayTabs/index.tsx | 28 ++- .../StaffingTable/StepperCell.tsx | 21 +- .../StaffingTable/index.tsx | 50 ++-- .../Shift/StaffingRequirement/index.tsx | 238 ++++++++++++------ 9 files changed, 391 insertions(+), 99 deletions(-) rename "doc/spec/2026-02-08_\343\202\267\343\203\225\343\203\210\347\267\250\351\233\206\346\251\237\350\203\275\344\273\225\346\247\230.md" => "doc/features/detail/2026-02-08_\343\202\267\343\203\225\343\203\210\347\267\250\351\233\206\346\251\237\350\203\275\344\273\225\346\247\230.md" (100%) create mode 100644 "doc/features/\343\202\267\343\203\225\343\203\210\347\256\241\347\220\206.md" diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index c4ef9e85..5eec528b 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -37,6 +37,7 @@ convex/ | スキル管理 | - | `StaffDetail`内 | `staffSkill/queries`, `mutations` | | ユーザー管理 | `MyPage`, `Settings` | `User/UserRegister`, `Setting/UserSetting` | `user/queries`, `mutations` | | 招待機能 | `Invite` | `Shop/MemberAddModal` | `invite/queries`, `mutations` | +| シフト管理 | `Shops/ShiftsPage`, `RecruitmentNewPage`, `RecruitmentDetailPage`, `ShiftConfirmPage`, `StaffingSettingsPage` | `Shift/ShiftForm`, `RecruitmentForm`, `RecruitmentList`, `RecruitmentDetail`, `RecruitmentNew`, `StaffingRequirement` | `requiredStaffing/queries`, `mutations` | --- @@ -90,6 +91,18 @@ convex/ | `src/components/features/Shop/MemberAddModal/` | スタッフ招待モーダル | | `convex/invite/` | DB操作 | +### シフト管理 +| ファイルパス | 責務 | +|-------------|------| +| `src/routes/_auth/shops/$shopId/shifts/` | ルーティング | +| `src/components/pages/Shops/ShiftsPage/` | useQuery、エラー/ローディング処理 | +| `src/components/pages/Shops/RecruitmentNewPage/` | 募集作成ページ | +| `src/components/pages/Shops/RecruitmentDetailPage/` | 募集詳細ページ | +| `src/components/pages/Shops/ShiftConfirmPage/` | シフト確定ページ | +| `src/components/pages/Shops/StaffingSettingsPage/` | 必要人員設定ページ | +| `src/components/features/Shift/` | ドメインロジック、UI | +| `convex/requiredStaffing/` | 必要人員DB操作 | + --- ## データフロー図 @@ -172,6 +185,7 @@ convex/ | `userAtom` | ログインユーザー情報 | メモリ | | `selectedShopAtom` | 選択中の店舗情報 | localStorage | | `hasSelectedShopAtom` | 店舗選択済み判定(派生) | - | +| ShiftForm Atoms | シフト編集状態(Jotai Provider内スコープ) | メモリ | --- diff --git a/doc/INDEX.md b/doc/INDEX.md index 0539b6fc..5104d896 100644 --- a/doc/INDEX.md +++ b/doc/INDEX.md @@ -12,6 +12,7 @@ | スキル管理 | [スキル管理.md](features/スキル管理.md) | スタッフのスキルレベル管理 | | ユーザー管理 | [ユーザー管理.md](features/ユーザー管理.md) | 認証、プロフィール管理 | | 招待機能 | [招待機能.md](features/招待機能.md) | マジックリンクでスタッフ招待 | +| シフト管理 | [シフト管理.md](features/シフト管理.md) | シフト編集、募集管理、必要人員設定 | ## 関連ドキュメント diff --git "a/doc/spec/2026-02-08_\343\202\267\343\203\225\343\203\210\347\267\250\351\233\206\346\251\237\350\203\275\344\273\225\346\247\230.md" "b/doc/features/detail/2026-02-08_\343\202\267\343\203\225\343\203\210\347\267\250\351\233\206\346\251\237\350\203\275\344\273\225\346\247\230.md" similarity index 100% rename from "doc/spec/2026-02-08_\343\202\267\343\203\225\343\203\210\347\267\250\351\233\206\346\251\237\350\203\275\344\273\225\346\247\230.md" rename to "doc/features/detail/2026-02-08_\343\202\267\343\203\225\343\203\210\347\267\250\351\233\206\346\251\237\350\203\275\344\273\225\346\247\230.md" diff --git "a/doc/features/\343\202\267\343\203\225\343\203\210\347\256\241\347\220\206.md" "b/doc/features/\343\202\267\343\203\225\343\203\210\347\256\241\347\220\206.md" new file mode 100644 index 00000000..01743f08 --- /dev/null +++ "b/doc/features/\343\202\267\343\203\225\343\203\210\347\256\241\347\220\206.md" @@ -0,0 +1,119 @@ +# シフト管理 + +## 概要 + +スタッフのシフト編集・管理を行う機能。シフト募集の作成、シフト確定、必要人員設定を含む。 +PC版はドラッグ操作によるペイント/消去/リサイズ、SP版はBottomSheetによるSelect式編集に対応。 + +## 関連ファイル + +- **Routes**: `src/routes/_auth/shops/$shopId/shifts/` +- **Pages**: `src/components/pages/Shops/` (ShiftsPage, RecruitmentNewPage, RecruitmentDetailPage, ShiftConfirmPage, StaffingSettingsPage) +- **Features**: `src/components/features/Shift/` +- **Convex**: `convex/requiredStaffing/` + +## 主な機能 + +- シフト編集(PC: ドラッグペイント/消去/リサイズ / SP: BottomSheet + Select) +- 日別ビュー / 一覧ビュー切替(常時マウント、状態保持) +- Undo/Redo(最大50件) +- ポジション配置・消去・単体リサイズ・連動リサイズ +- 充足度サマリー(色/数値切替、展開/折畳) +- ソート(デフォルト/希望順/出勤順、両ビュー共有) +- 自動正規化(マージ + 休憩自動挿入) +- シフト募集の作成・管理 +- 必要人員設定(StaffingRequirement) +- Read-Onlyモード +- スタッフハイライト + +## データモデル + +```typescript +// ShiftForm内で使用する主要型(詳細は types.ts 参照) +ShiftData = { + id: string; + staffId: string; + staffName: string; + date: string; // "2026-01-28" + requestedTime: { start: string; end: string } | null; + positions: PositionSegment[]; +} + +PositionSegment = { + id: string; + positionId: string; + positionName: string; + color: string; // HEXカラー + start: string; // "09:00" + end: string; // "17:00" +} + +RequiredStaffingData = { + dayOfWeek: number; // 0=日, 1=月, ..., 6=土 + slots: { hour: number; position: string; requiredCount: number }[]; +} +``` + +## 画面一覧 + +| 画面 | パス | 説明 | +|------|------|------| +| シフト管理 | `/shops/:shopId/shifts` | シフト編集メイン画面 | +| シフト募集作成 | `/shops/:shopId/shifts/recruitments/new` | 募集作成 | +| シフト募集詳細 | `/shops/:shopId/shifts/recruitments/:id` | 募集詳細・シフト確定 | +| シフト確定 | `/shops/:shopId/shifts/recruitments/:id/confirm` | シフト確定画面 | +| 必要人員設定 | `/shops/:shopId/shifts/settings` | 必要人員の設定 | + +## コンポーネント構成 + +``` +Shift/ +├── ShiftForm/ # シフト編集メイン(PC/SP対応) +│ ├── pc/ # PC版ビュー +│ │ ├── DailyView/ # 日別ビュー(ShiftGrid, PositionToolbar, DateTabs等) +│ │ └── OverviewView/ # 一覧ビュー(OverviewHeader, StaffRow等) +│ ├── sp/ # SP版ビュー +│ │ ├── DailyView/ # 日別ビュー(StaffCard, ShiftEditSheet等) +│ │ └── OverviewView/ # 一覧ビュー(DateCard) +│ ├── shared/ # 共有コンポーネント(SortMenu) +│ ├── hooks/ # フック(useShiftFormInit, useUndoRedo) +│ └── utils/ # ユーティリティ(shiftOperations, calculations等) +├── RecruitmentForm/ # 募集フォーム +├── RecruitmentList/ # 募集一覧 +├── RecruitmentDetail/ # 募集詳細 +├── RecruitmentNew/ # 募集新規作成 +└── StaffingRequirement/ # 必要人員設定 + ├── StaffingTable/ # 人員テーブル + ├── WeeklyHeatmap/ # 週間ヒートマップ + ├── SetupWizard/ # 初期設定ウィザード + ├── AIGenerateForm/ # AI生成フォーム + └── ... # DaySelector, DayTabs, SummaryBar等 +``` + +## API + +### Queries +- `requiredStaffing.queries` - 必要人員データ取得 + +### Mutations +- `requiredStaffing.mutations` - 必要人員データ更新 + +## 状態管理(Jotai - ShiftForm内スコープ) + +ShiftFormはJotai Providerでスコープされたアトムを使用(グローバルストアとは独立)。 + +| Atom | 責務 | +|------|------| +| `shiftConfigAtom` | 外部Props設定(shopId, staffs, positions, dates等) | +| `viewModeAtom` | 日別/一覧ビュー切替 | +| `selectedDateAtom` | 選択中の日付 | +| `sortModeAtom` | ソートモード(default/request/startTime) | +| `shiftsHistoryAtom` | Undo/Redo履歴(past/present/future) | +| `shiftsAtom` | シフトデータ(書き込み時に自動正規化) | +| `toolModeAtom` | ツールモード(select/assign/erase、PCのみ) | +| `selectedPositionIdAtom` | 選択中ポジションID | + +## 仕様書 + +- [シフト編集機能仕様](./detail//2026-02-08_シフト編集機能仕様.md) - ShiftFormの詳細仕様(操作・イベント・表示仕様) +- [シフト管理機能仕様](../spec/2026-01-03_シフト管理機能.md) - シフト管理全体の仕様 diff --git a/src/components/features/Shift/StaffingRequirement/DayTabs/index.stories.tsx b/src/components/features/Shift/StaffingRequirement/DayTabs/index.stories.tsx index edc6b92d..359d779b 100644 --- a/src/components/features/Shift/StaffingRequirement/DayTabs/index.stories.tsx +++ b/src/components/features/Shift/StaffingRequirement/DayTabs/index.stories.tsx @@ -13,10 +13,16 @@ const meta = { export default meta; type Story = StoryObj; +// 月〜金が設定済み、土日祝は未設定 +const CONFIGURED_WEEKDAYS = [1, 2, 3, 4, 5]; +// 全曜日設定済み +const ALL_CONFIGURED = [0, 1, 2, 3, 4, 5, 6, 7]; + export const Basic: Story = { args: { selectedDay: 1, onChange: () => {}, + configuredDays: CONFIGURED_WEEKDAYS, }, }; @@ -24,6 +30,7 @@ export const Sunday: Story = { args: { selectedDay: 0, onChange: () => {}, + configuredDays: CONFIGURED_WEEKDAYS, }, }; @@ -31,6 +38,15 @@ export const Saturday: Story = { args: { selectedDay: 6, onChange: () => {}, + configuredDays: ALL_CONFIGURED, + }, +}; + +export const NoneConfigured: Story = { + args: { + selectedDay: 1, + onChange: () => {}, + configuredDays: [], }, }; @@ -38,7 +54,7 @@ export const Saturday: Story = { const InteractiveDayTabs = () => { const [selectedDay, setSelectedDay] = useState(1); - return ; + return ; }; export const Interactive: Story = { @@ -46,5 +62,6 @@ export const Interactive: Story = { args: { selectedDay: 1, onChange: () => {}, + configuredDays: CONFIGURED_WEEKDAYS, }, }; diff --git a/src/components/features/Shift/StaffingRequirement/DayTabs/index.tsx b/src/components/features/Shift/StaffingRequirement/DayTabs/index.tsx index c7e5a35a..9ba24567 100644 --- a/src/components/features/Shift/StaffingRequirement/DayTabs/index.tsx +++ b/src/components/features/Shift/StaffingRequirement/DayTabs/index.tsx @@ -1,23 +1,39 @@ import { Tabs } from "@chakra-ui/react"; -import { DAY_LABELS, getDayColor, WEEKDAY_ORDER } from "../constants"; +import { getDayColor, WEEKDAY_ORDER } from "../constants"; + +const DAY_TAB_LABELS = ["日", "月", "火", "水", "木", "金", "土", "祝"] as const; type DayTabsProps = { selectedDay: number; onChange: (day: number) => void; + configuredDays: number[]; }; -export const DayTabs = ({ selectedDay, onChange }: DayTabsProps) => { +export const DayTabs = ({ selectedDay, onChange, configuredDays }: DayTabsProps) => { + const getTabColor = (dayIndex: number) => { + if (!configuredDays.includes(dayIndex)) return "gray.400"; + return getDayColor(dayIndex); + }; + return ( onChange(Number.parseInt(e.value, 10))} - variant="outline" + variant="line" + colorPalette="teal" + size="sm" > - + {/* 月曜始まり: 月火水木金土日祝 */} {WEEKDAY_ORDER.map((dayIndex) => ( - - {DAY_LABELS[dayIndex]} + + {DAY_TAB_LABELS[dayIndex]} ))} diff --git a/src/components/features/Shift/StaffingRequirement/StaffingTable/StepperCell.tsx b/src/components/features/Shift/StaffingRequirement/StaffingTable/StepperCell.tsx index 04f3ebbe..1dac3b49 100644 --- a/src/components/features/Shift/StaffingRequirement/StaffingTable/StepperCell.tsx +++ b/src/components/features/Shift/StaffingRequirement/StaffingTable/StepperCell.tsx @@ -7,11 +7,28 @@ type StepperCellProps = { min?: number; max?: number; disabled?: boolean; + isChanged?: boolean; }; -export const StepperCell = ({ value, onChange, min = 0, max = 10, disabled = false }: StepperCellProps) => { +export const StepperCell = ({ + value, + onChange, + min = 0, + max = 10, + disabled = false, + isChanged = false, +}: StepperCellProps) => { return ( - + void; disabled?: boolean; + initialStaffing?: StaffingEntry[]; }; export const StaffingTable = ({ @@ -20,6 +21,7 @@ export const StaffingTable = ({ staffing, onChange, disabled = false, + initialStaffing, }: StaffingTableProps) => { // 営業時間から時間帯リストを生成 const hours = useMemo(() => { @@ -42,12 +44,30 @@ export const StaffingTable = ({ return map; }, [staffing]); + // 初期値マップ(変更検出用) + const initialMap = useMemo(() => { + if (!initialStaffing) return {}; + const map: Record = {}; + for (const entry of initialStaffing) { + const key = `${entry.hour}-${entry.position}`; + map[key] = entry.requiredCount; + } + return map; + }, [initialStaffing]); + // 人員数取得 const getCount = (hour: number, position: string) => { const key = `${hour}-${position}`; return staffingMap[key] ?? 0; }; + // 変更検出 + const checkChanged = (hour: number, position: string) => { + if (!initialStaffing) return false; + const key = `${hour}-${position}`; + return (staffingMap[key] ?? 0) !== (initialMap[key] ?? 0); + }; + // 人員数更新 const handleCountChange = (hour: number, position: string, value: number) => { const clampedValue = Math.max(0, Math.min(10, value)); @@ -65,10 +85,14 @@ export const StaffingTable = ({ onChange(newStaffing); }; - // 1日の合計人時を計算 - const totalPersonHours = useMemo(() => { - return staffing.reduce((sum, entry) => sum + entry.requiredCount, 0); - }, [staffing]); + // ポジション別合計 + const positionTotals = useMemo(() => { + const totals: Record = {}; + for (const pos of positions) { + totals[pos.name] = staffing.filter((e) => e.position === pos.name).reduce((sum, e) => sum + e.requiredCount, 0); + } + return totals; + }, [staffing, positions]); return ( @@ -76,11 +100,14 @@ export const StaffingTable = ({ - + 時間帯 {positions.map((pos) => ( {pos.name} + + ({positionTotals[pos.name] ?? 0}) + ))} 合計 @@ -100,6 +127,7 @@ export const StaffingTable = ({ value={getCount(hour, pos.name)} onChange={(value) => handleCountChange(hour, pos.name, value)} disabled={disabled} + isChanged={checkChanged(hour, pos.name)} /> ))} @@ -117,16 +145,6 @@ export const StaffingTable = ({ - - {/* 合計表示 */} - - - 1日の合計:{" "} - - {totalPersonHours}人時 - - - ); }; diff --git a/src/components/features/Shift/StaffingRequirement/index.tsx b/src/components/features/Shift/StaffingRequirement/index.tsx index ba5c4fb3..e2d9a676 100644 --- a/src/components/features/Shift/StaffingRequirement/index.tsx +++ b/src/components/features/Shift/StaffingRequirement/index.tsx @@ -1,19 +1,22 @@ -import { Box, Button, Container, Flex, Heading, Icon, Text } from "@chakra-ui/react"; +import { Box, Button, Container, Flex, Heading, Icon, SegmentGroup, Text } from "@chakra-ui/react"; import { useMemo, useState } from "react"; -import { LuCalendarDays, LuCopy, LuPencilLine, LuRefreshCw, LuRotateCcw, LuSave, LuSettings } from "react-icons/lu"; -import { useDialog } from "@/src/components/ui/Dialog"; +import { LuCopy, LuRefreshCw, LuRotateCcw, LuSave, LuSettings } from "react-icons/lu"; +import { Dialog, useDialog } from "@/src/components/ui/Dialog"; import { Title } from "@/src/components/ui/Title"; import { toaster } from "@/src/components/ui/toaster"; import { CopyModal } from "./CopyModal"; -import { DAY_LABELS } from "./constants"; +import { DAY_COUNT } from "./constants"; import { DayTabs } from "./DayTabs"; import { RegenerateModal } from "./RegenerateModal"; import { StaffingTable } from "./StaffingTable"; -import { SummaryBar } from "./SummaryBar"; import type { AIInput, PositionType, ShopType, StaffingEntry } from "./types"; -import { calculateWeeklySummary } from "./utils/summaryCalculations"; import { WeeklyHeatmap } from "./WeeklyHeatmap"; +const VIEW_OPTIONS = [ + { value: "daily", label: "日別" }, + { value: "overview", label: "一覧" }, +]; + // Convex DBから取得されるフラット化された必要人員レコード type RequiredStaffingFlat = { _id: string; @@ -47,8 +50,8 @@ export const StaffingRequirement = ({ isSaving = false, isCopying = false, }: StaffingRequirementProps) => { - // ビューモード(週間俯瞰 / 日別編集) - const [viewMode, setViewMode] = useState<"weekly" | "daily">("daily"); + // ビューモード(日別 / 一覧) + const [viewMode, setViewMode] = useState<"daily" | "overview">("daily"); // 曜日タブ選択(月曜=1をデフォルト) const [selectedDay, setSelectedDay] = useState(1); @@ -56,6 +59,11 @@ export const StaffingRequirement = ({ // モーダル管理 const copyModal = useDialog(); const regenerateModal = useDialog(); + const resetDialog = useDialog(); + const unsavedDialog = useDialog(); + + // 未保存警告時の移動先曜日 + const [pendingDay, setPendingDay] = useState(null); // AI入力の保存(作り直す時に前回値を使用) const [aiInput, setAiInput] = useState({ shopType: "", customerCount: "" }); @@ -84,11 +92,28 @@ export const StaffingRequirement = ({ // 変更フラグ const [hasChanges, setHasChanges] = useState(false); - // 週間サマリー - const summary = useMemo( - () => calculateWeeklySummary({ staffingMap, hours, positions }), - [staffingMap, hours, positions], - ); + // 設定済み曜日の算出(DayTabsの濃淡表示用) + const configuredDays = useMemo(() => { + const days: number[] = []; + for (let day = 0; day < DAY_COUNT; day++) { + const hasData = hours.some((hour) => + positions.some((pos) => (staffingMap[`${day}-${hour}-${pos.name}`] ?? 0) > 0), + ); + if (hasData) days.push(day); + } + return days; + }, [staffingMap, hours, positions]); + + // 選択中の曜日の初期値(変更ハイライト用) + const currentDayInitialStaffing = useMemo(() => { + const result: StaffingEntry[] = []; + for (const item of initialStaffing) { + if (item.dayOfWeek === selectedDay) { + result.push({ hour: item.hour, position: item.position, requiredCount: item.requiredCount }); + } + } + return result; + }, [initialStaffing, selectedDay]); // 選択中の曜日のstaffing配列を生成 const currentDayStaffing = useMemo(() => { @@ -102,6 +127,44 @@ export const StaffingRequirement = ({ return result; }, [staffingMap, selectedDay, hours, positions]); + // 曜日タブ切替(未保存チェック付き) + const handleDayChange = (newDay: number) => { + if (hasChanges) { + setPendingDay(newDay); + unsavedDialog.open(); + } else { + setSelectedDay(newDay); + } + }; + + // 未保存の変更を破棄して移動 + const handleDiscardAndMove = () => { + if (pendingDay === null) return; + // 現在の曜日のデータを初期値に復元 + setStaffingMap((prev) => { + const newMap = { ...prev }; + // まず現曜日のキーをすべて0にリセット + for (const hour of hours) { + for (const pos of positions) { + const key = `${selectedDay}-${hour}-${pos.name}`; + newMap[key] = 0; + } + } + // 初期値で上書き + for (const item of initialStaffing) { + if (item.dayOfWeek === selectedDay) { + const key = `${item.dayOfWeek}-${item.hour}-${item.position}`; + newMap[key] = item.requiredCount; + } + } + return newMap; + }); + setSelectedDay(pendingDay); + setHasChanges(false); + setPendingDay(null); + unsavedDialog.close(); + }; + // StaffingTableからの変更を受け取る const handleStaffingChange = (newStaffing: StaffingEntry[]) => { setStaffingMap((prev) => { @@ -223,37 +286,20 @@ export const StaffingRequirement = ({ - {/* 週間サマリー */} - - {/* ビューモード切替 */} - - - + + + - {/* 週間俯瞰モード */} - {viewMode === "weekly" && ( + {/* 一覧モード */} + {viewMode === "overview" && ( )} - {/* 日別編集モード */} + {/* 日別モード */} {viewMode === "daily" && ( <> {/* 曜日タブ + アクションボタン */} - + - {onResetSetup && ( - - )} + ) : ( + + )} + + {hasChanges && ( + + 未保存の変更があります + + )} + + + )} + {/* 未保存警告ダイアログ */} + { + setPendingDay(null); + unsavedDialog.close(); + }} + onSubmit={handleDiscardAndMove} + submitLabel="破棄して移動" + submitColorPalette="red" + role="alertdialog" + > + 現在の曜日に未保存の変更があります。変更を破棄して移動しますか? + + + {/* 初期設定リセット確認ダイアログ */} + { + onResetSetup?.(); + resetDialog.close(); + }} + submitLabel="やり直す" + submitColorPalette="red" + role="alertdialog" + > + すべての必要人員設定が削除され、最初からやり直しになります。 + + この操作は取り消せません。 + + + {/* コピーモーダル */} - - {/* アクションボタン */} - - - ); }; From c641abf44f28328e44fd730d9b9fd8b49fcb6f0c Mon Sep 17 00:00:00 2001 From: y-natani Date: Wed, 11 Feb 2026 01:29:19 +0900 Subject: [PATCH 2/3] fix --- ...37\350\203\275\344\273\225\346\247\230.md" | 924 ++++++++++++++++++ ...25\343\203\210\347\256\241\347\220\206.md" | 25 +- ...44\343\202\242\343\202\246\343\203\210.md" | 304 ++++++ .../AIGenerateForm/index.tsx | 36 +- .../AIInputFields/index.tsx | 51 + .../StaffingRequirement/CopyModal/index.tsx | 65 +- .../StaffingRequirement/DaySelector/index.tsx | 3 +- .../StaffingRequirement/DayTabs/index.tsx | 12 +- .../MobileAccordionView/index.tsx | 147 ++- .../MobileActionBar/index.stories.tsx | 46 + .../MobileActionBar/index.tsx | 44 + .../StaffingRequirement/QuickNavBar/index.tsx | 39 +- .../RegenerateModal/index.tsx | 65 +- .../StaffingRequirement/SetupWizard/index.tsx | 49 +- .../StaffingTable/StepperCell.tsx | 10 +- .../StaffingTable/index.tsx | 81 +- .../WeeklyHeatmap/index.tsx | 5 +- .../Shift/StaffingRequirement/constants.ts | 17 +- .../StaffingRequirement/index.stories.tsx | 29 +- .../Shift/StaffingRequirement/index.tsx | 308 ++---- .../StaffingRequirement/useDayNavigation.ts | 45 + .../StaffingRequirement/useStaffingData.ts | 157 +++ .../StaffingRequirement/utils/dayHelpers.ts | 6 + .../utils/generateMockStaffing.ts | 14 +- .../utils/heatmapCalculations.ts | 10 +- .../utils/staffingMapHelpers.test.ts | 89 ++ .../utils/staffingMapHelpers.ts | 42 + .../utils/summaryCalculations.ts | 5 +- .../utils/timeHelpers.test.ts | 24 + .../StaffingRequirement/utils/timeHelpers.ts | 11 + 30 files changed, 2182 insertions(+), 481 deletions(-) create mode 100644 "doc/features/detail/2026-02-10_\345\277\205\350\246\201\344\272\272\345\223\241\350\250\255\345\256\232\346\251\237\350\203\275\344\273\225\346\247\230.md" create mode 100644 "doc/plans/2026-02-10_\345\277\205\350\246\201\344\272\272\345\223\241\350\250\255\345\256\232SP\347\211\210\343\203\254\343\202\244\343\202\242\343\202\246\343\203\210.md" create mode 100644 src/components/features/Shift/StaffingRequirement/AIInputFields/index.tsx create mode 100644 src/components/features/Shift/StaffingRequirement/MobileActionBar/index.stories.tsx create mode 100644 src/components/features/Shift/StaffingRequirement/MobileActionBar/index.tsx create mode 100644 src/components/features/Shift/StaffingRequirement/useDayNavigation.ts create mode 100644 src/components/features/Shift/StaffingRequirement/useStaffingData.ts create mode 100644 src/components/features/Shift/StaffingRequirement/utils/dayHelpers.ts create mode 100644 src/components/features/Shift/StaffingRequirement/utils/staffingMapHelpers.test.ts create mode 100644 src/components/features/Shift/StaffingRequirement/utils/staffingMapHelpers.ts create mode 100644 src/components/features/Shift/StaffingRequirement/utils/timeHelpers.test.ts create mode 100644 src/components/features/Shift/StaffingRequirement/utils/timeHelpers.ts diff --git "a/doc/features/detail/2026-02-10_\345\277\205\350\246\201\344\272\272\345\223\241\350\250\255\345\256\232\346\251\237\350\203\275\344\273\225\346\247\230.md" "b/doc/features/detail/2026-02-10_\345\277\205\350\246\201\344\272\272\345\223\241\350\250\255\345\256\232\346\251\237\350\203\275\344\273\225\346\247\230.md" new file mode 100644 index 00000000..4c396728 --- /dev/null +++ "b/doc/features/detail/2026-02-10_\345\277\205\350\246\201\344\272\272\345\223\241\350\250\255\345\256\232\346\251\237\350\203\275\344\273\225\346\247\230.md" @@ -0,0 +1,924 @@ +# 必要人員設定機能 仕様書 + +**作成日**: 2026-02-10 +**更新日**: 2026-02-11 +**目的**: 必要人員設定に関する全機能・操作・振る舞いを網羅的に記録する。 +**方針**: コードを正とする。ユーザー視点の機能・操作を記述する。 +**対象**: PC版・SP版 + +--- + +## 1. 概要 + +管理者が店舗の時間帯別・ポジション別の必要人員数を設定するための機能。 +曜日ごとに異なる人員配置を設定でき、AI提案による自動生成にも対応する。 +PC版はテーブル形式、SP版はアコーディオン形式で表示し、レスポンシブに切り替わる。 + +### URL + +`/shops/:shopId/shifts/settings` + +### 画面遷移 + +``` +[シフト管理] → [必要人員設定] + │ + ├─ データなし → SetupWizard(初期設定) + │ ├─ Step 1: AI生成 or スキップ + │ └─ Step 2: 編集・曜日選択 → 保存 + │ + └─ データあり → StaffingRequirement(メインビュー) + ├─ 日別ビュー(デフォルト) + └─ 一覧ビュー(WeeklyHeatmap) +``` + +### ビューモード + +| モード | 表示 | 編集可否 | +|--------|------|----------| +| 日別ビュー | 1曜日分の必要人員をテーブル(PC)/アコーディオン(SP)で表示 | 可 | +| 一覧ビュー | 全曜日のヒートマップを表示 | 不可(曜日クリックで日別に遷移) | + +--- + +## 2. 入口の型定義 + +### 2.1 StaffingRequirement Props + +```typescript +type StaffingRequirementProps = { + shopId: string; + shop: ShopType; + positions: PositionType[]; + initialStaffing: RequiredStaffingFlat[]; + onSave: (params: { + dayOfWeek: number; + staffing: StaffingEntry[]; + aiInput?: AIInput; + }) => Promise; + onCopy: (params: { + sourceDayOfWeek: number; + targetDaysOfWeek: number[]; + }) => Promise; + onResetSetup?: () => void; + isSaving?: boolean; + isCopying?: boolean; +}; +``` + +### 2.2 SetupWizard Props + +```typescript +type SetupWizardProps = { + openTime: string; + closeTime: string; + positions: PositionType[]; + onSave: (patterns: PatternType[], aiInput?: AIInput) => void; + onCancel: () => void; +}; +``` + +### 2.3 ドメインモデル + +```typescript +// 1時間帯 × 1ポジションの必要人員エントリ +type StaffingEntry = { + hour: number; // 0-23 + position: string; // "ホール", "キッチン" 等 + requiredCount: number; // 必要人数 +}; + +// AI入力情報 +type AIInput = { + shopType: string; // 店舗タイプの説明 + customerCount: string; // 来客数の説明 +}; + +// ポジション +type PositionType = { + _id: string; + name: string; +}; + +// 店舗情報(StaffingRequirement用サブセット) +type ShopType = { + _id: string; + shopName: string; + openTime: string; // "09:00" + closeTime: string; // "22:00" +}; + +// SetupWizardのパターン +type PatternType = { + id: string; + staffing: StaffingEntry[]; + appliedDays: number[]; // 適用する曜日インデックス配列 +}; + +// Convex DBからのフラット化レコード +type RequiredStaffingFlat = { + _id: string; + shopId: string; + dayOfWeek: number; // 0=日, 1=月, ..., 6=土, 7=祝 + hour: number; + position: string; + requiredCount: number; +}; +``` + +### 2.4 DBスキーマ(Convex) + +```typescript +const requiredStaffing = defineTable({ + shopId: v.id("shops"), + dayOfWeek: v.number(), // 0=日, 1=月, ..., 6=土, 7=祝 + staffing: v.array( + v.object({ + hour: v.number(), + position: v.string(), + requiredCount: v.number(), + }) + ), + aiInput: v.optional( + v.object({ + shopType: v.string(), + customerCount: v.string(), + }) + ), + createdAt: v.number(), + updatedAt: v.number(), +}).index("by_shop", ["shopId"]); +``` + +### 2.5 モード型 + +```typescript +type ViewMode = "daily" | "overview"; +``` + +--- + +## 3. ビジネス定数 + +| 定数 | 値 | 用途 | +|------|-----|------| +| 必要人員 最小値 | 0 | StepperCellの下限 | +| 必要人員 最大値 | 10 | StepperCellの上限 | +| 曜日数 | 8 | 日月火水木金土祝 | +| デフォルト選択曜日 | 1(月曜) | 日別ビュー初期表示 | + +### 曜日インデックス + +| index | 曜日 | 表示色 | +|-------|------|--------| +| 0 | 日 | `red.500` | +| 1 | 月 | デフォルト | +| 2 | 火 | デフォルト | +| 3 | 水 | デフォルト | +| 4 | 木 | デフォルト | +| 5 | 金 | デフォルト | +| 6 | 土 | `blue.500` | +| 7 | 祝 | `red.500` | + +### 表示順序(月曜始まり) + +`[1, 2, 3, 4, 5, 6, 0, 7]` → 月火水木金土日祝 + +### ヒートマップ色段階(5段階) + +| 充足率 | 色 | +|--------|-----| +| 0(データなし) | `gray.50` | +| 1-25% | `blue.100` | +| 26-50% | `blue.300` | +| 51-75% | `blue.500` | +| 76-100% | `blue.700` | + +※ 充足率は `count / maxCount`(全セル中の最大値に対する比率) + +### 時間帯グループ(SP版アコーディオン用) + +| ラベル | 開始時刻 | 終了時刻 | +|--------|----------|----------| +| 朝 | 6:00 | 11:00 | +| 昼 | 11:00 | 14:00 | +| 午後 | 14:00 | 17:00 | +| 夕方 | 17:00 | 21:00 | +| 夜 | 21:00 | 30:00 | + +※ 営業時間に該当する時間帯グループのみ表示される + +--- + +## 4. 機能一覧 + +| # | 機能 | 概要 | +|---|------|------| +| 1 | ビュー切替 | 日別 ↔ 一覧をSegmentGroupで切替 | +| 2 | 曜日タブ切替 | 月火水木金土日祝の8タブで曜日を選択 | +| 3 | 人員数編集 | StepperCellで時間帯×ポジションの人員数を増減 | +| 4 | 保存 | 現在の曜日の設定をDBに保存 | +| 5 | 他曜日へコピー | 現在の曜日の設定を複数の曜日に一括コピー | +| 6 | AI再生成 | 店舗情報を入力してAIで人員配置を再生成 | +| 7 | 初期設定やり直し | 全データを削除してSetupWizardに戻る | +| 8 | 未保存警告 | 曜日切替時に未保存データがあれば警告 | +| 9 | 一覧→日別ナビ | ヒートマップの曜日クリックで日別ビューに遷移 | +| 10 | 初期設定ウィザード | AI生成 or 手動で初回設定を行う(2ステップ) | +| 11 | パターン管理 | ウィザード内で複数パターンの人員配置を作成 | +| 12 | 曜日一括選択 | DaySelectorで平日/休日/全てを一括選択 | +| 13 | SP版メニュー | BottomSheetでコピー/再生成/リセットを操作 | + +--- + +## 5. 操作・イベント一覧 + +### 5.1 メインビュー: 日別モード + +#### ヘッダー + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| 戻るリンク | クリック | `/shops/:shopId/shifts` に遷移 | +| ビュー切替(SegmentGroup) | クリック | 日別 ↔ 一覧を切替 | +| メニューボタン(SP版のみ) | クリック | メニューBottomSheetを表示 | + +#### 曜日タブ(DayTabs) + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| 曜日タブ | クリック | 未保存変更がなければ曜日を切替。あれば未保存警告ダイアログを表示 | + +#### アクションボタン(PC版・タブ横) + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| コピーボタン | クリック | CopyModalを表示 | +| 作り直すボタン | クリック | RegenerateModalを表示 | + +#### テーブル(StaffingTable・PC版) + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| StepperCell [+] ボタン | クリック | 人員数を+1(最大10) | +| StepperCell [−] ボタン | クリック | 人員数を-1(最小0) | + +#### アコーディオン(MobileAccordionView・SP版) + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| 時間帯グループヘッダー | クリック | アコーディオンの開閉(複数同時展開可能) | +| StepperCell [+] ボタン | クリック | 人員数を+1(最大10) | +| StepperCell [−] ボタン | クリック | 人員数を-1(最小0) | + +#### PC版アクションバー(テーブル下部固定) + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| 初期設定をやり直すボタン | クリック | リセット確認ダイアログを表示 | +| 保存するボタン | クリック | 現在の曜日の設定を保存。未変更時はdisabled | + +#### SP版アクションバー(MobileActionBar) + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| 保存ボタン | クリック | 現在の曜日の設定を保存。未変更時はdisabled | + +#### SP版メニューBottomSheet + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| 他の曜日にコピー | クリック | BottomSheetを閉じてCopyModalを表示 | +| AIで作り直す | クリック | BottomSheetを閉じてRegenerateModalを表示 | +| 初期設定をやり直す | クリック | BottomSheetを閉じてリセット確認ダイアログを表示(`onResetSetup`がある場合のみ) | + +#### 未保存警告ダイアログ + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| 破棄して移動ボタン | クリック | 未保存の変更を初期値に復元し、曜日を切替 | +| キャンセルボタン | クリック | ダイアログを閉じる(曜日切替をキャンセル) | + +#### リセット確認ダイアログ + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| やり直すボタン | クリック | `onResetSetup` を呼び出し、SetupWizardに切替 | +| キャンセルボタン | クリック | ダイアログを閉じる | + +#### CopyModal(PC: Dialog / SP: BottomSheet) + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| コピー先曜日チェックボックス | クリック | コピー先曜日を選択/解除。コピー元曜日はdisabled | +| 平日リンク | クリック | 月〜金を一括選択 | +| 休日リンク | クリック | 土日祝を一括選択 | +| 全てリンク | クリック | 全曜日を一括選択(コピー元除く) | +| コピーするボタン | クリック | 現在の曜日を保存→選択した曜日にコピー | + +#### RegenerateModal(PC: Dialog / SP: BottomSheet) + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| 店舗タイプTextarea | 入力 | AI生成用の店舗タイプを入力 | +| 来客数Input | 入力 | AI生成用の来客数を入力 | +| 作り直すボタン | クリック | AIで現在の曜日の人員配置を再生成。店舗タイプ未入力時はdisabled | + +### 5.2 メインビュー: 一覧モード + +#### WeeklyHeatmap + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| 曜日ヘッダー | クリック | 日別ビューに切替 + 該当曜日を選択 | +| セル | クリック | 日別ビューに切替 + 該当曜日を選択 | + +### 5.3 SetupWizard + +#### Step 1: AIGenerateForm + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| 店舗タイプTextarea | 入力 | 店舗タイプを入力 | +| 来客数Input | 入力 | 来客数を入力 | +| 提案してもらうボタン | クリック | AI生成を実行→Step 2に遷移。店舗タイプ未入力時はdisabled | +| スキップリンク | クリック | 空のパターンで Step 2に遷移(手動入力モード) | + +#### Step 2: 編集・曜日選択 + +| 操作対象 | 操作 | 結果 | +|---------|------|------| +| パターンタブ | クリック | 編集対象のパターンを切替(複数パターン時のみ表示) | +| StaffingTable | 操作 | パターンの人員配置を編集 | +| DaySelector | 操作 | パターンの適用曜日を選択 | +| 別パターンを追加ボタン | クリック | 現在のパターンをコピーして新パターンを作成。全曜日割当済みなら非表示 | +| 保存するボタン | クリック | 全パターンを保存。パターンにstaffingまたは適用曜日がなければdisabled | +| 戻るボタン | クリック | Step 2→Step 1に戻る、Step 1→onCancelを呼び出し | + +--- + +## 6. ユースケース + +### UC1: 初期設定(AI生成あり) + +1. 必要人員設定画面に遷移(データなし) +2. SetupWizardが表示される +3. Step 1: 店舗タイプ(例: "カフェ、ランチメインで夜は軽め")と来客数を入力 +4. 「提案してもらう」をクリック +5. AI生成結果でStep 2に遷移(デフォルトで平日に適用) +6. テーブルで人員数を微調整 +7. 必要に応じて「別パターンを追加」で休日用パターンを作成 +8. 全曜日にパターンが割り当てられたら「保存する」をクリック +9. 全曜日分が一括でDBに保存される + +### UC2: 初期設定(手動入力) + +1. SetupWizard Step 1で「スキップ」をクリック +2. 空のテーブルでStep 2に遷移 +3. StepperCellで各時間帯×ポジションの人員数を手動入力 +4. 適用曜日を選択 +5. 「保存する」をクリック + +### UC3: 日別編集 + +1. メインビュー日別モードで曜日タブを選択 +2. StepperCellで人員数を編集 +3. 変更されたセルにはオレンジ背景が表示される +4. 「保存する」をクリック(未変更時はdisabled) +5. 保存成功でトースト「必要人員設定を保存しました」 + +### UC4: 曜日切替(未保存変更あり) + +1. テーブルを編集(hasChanges = true) +2. 別の曜日タブをクリック +3. 「未保存の変更があります」ダイアログが表示 +4. 「破棄して移動」→ 現在の曜日を初期値に復元、選択曜日を切替 +5. 「キャンセル」→ 曜日切替をキャンセル + +### UC5: 他曜日へコピー + +1. 日別モードで「コピー」をクリック(PC: ボタン / SP: メニューBottomSheet) +2. CopyModal(PC: Dialog / SP: BottomSheet)でコピー先曜日を選択(平日/休日/全てで一括選択可) +3. 「コピーする」をクリック +4. 処理: 現在の曜日を保存→選択先にコピー→ローカルstateも更新 +5. 保存成功でトースト「設定をコピーしました」 + +### UC6: AI再生成 + +1. 日別モードで「作り直す」をクリック(PC: ボタン / SP: メニューBottomSheet) +2. RegenerateModal(PC: Dialog / SP: BottomSheet)に前回のAI入力値がプリセット表示 +3. 必要に応じて店舗タイプ・来客数を編集 +4. 「作り直す」をクリック +5. 現在の曜日の人員配置がAI生成結果で上書き(hasChanges = true) +6. トースト「AIで再生成しました」 +7. テーブルで確認・微調整後、「保存する」で保存 + +### UC7: 一覧→日別ナビゲーション + +1. 一覧ビューでヒートマップのセルまたは曜日ヘッダーをクリック +2. 日別ビューに自動切替 +3. クリックした曜日が選択される + +### UC8: 初期設定やり直し + +1. アクションバーの「初期設定をやり直す」をクリック(PC: stickyバー / SP: メニューBottomSheet) +2. 確認ダイアログ「すべての必要人員設定が削除され、最初からやり直しになります」+「この操作は取り消せません」 +3. 「やり直す」→ SetupWizardに切替 +4. 「キャンセル」→ ダイアログを閉じる + +--- + +## 7. 表示仕様 + +### 7.1 曜日タブの色分け + +| 状態 | 文字色 | +|------|--------| +| 設定済み・土曜 | `blue.500` | +| 設定済み・日曜/祝日 | `red.500` | +| 設定済み・平日 | デフォルト | +| 未設定 | `gray.400` | + +### 7.2 StepperCellの変更ハイライト + +| 状態 | 背景色 | +|------|--------| +| 変更あり | `orange.50` | +| 変更なし | `transparent` | + +### 7.3 StepperCellのレスポンシブ + +| プロパティ | PC (md以上) | SP (base) | +|-----------|------------|-----------| +| ボタンサイズ | `2xs` | `xs` | +| gap | 1 | 2 | +| px | 1 | 2 | +| py | 0.5 | 1 | +| トランジション | `all 0.15s ease` | 同左 | + +### 7.4 テーブルレイアウト(PC版) + +``` ++----------+--------+----------+------+------+ +| 時間帯 | ホール | キッチン | レジ | 合計 | +| | (12) | (8) | (4) | | ++----------+--------+----------+------+------+ +| 9:00-10:00 | [−]1[+] | [−]1[+] | [−]0[+] | 2 | +| 10:00-11:00| [−]1[+] | [−]1[+] | [−]0[+] | 2 | +| 11:00-12:00| [−]3[+] | [−]2[+] | [−]1[+] | 6 | +| ... | ... | ... | ... | . | ++----------+--------+----------+------+------+ +``` + +- ヘッダー行: sticky表示(top: 0, zIndex: 10) +- ポジション名の横にポジション別合計を`(N)`で表示 +- 行末に時間帯別合計を表示 +- SP版では非表示(`display: none`) + +### 7.5 アコーディオンレイアウト(SP版) + +``` +┌──────────────────────────────────┐ +│ ▼ 朝 9:00〜12:00 │ ← 時間帯グループ +├──────────────────────────────────┤ +│ 9:00-10:00 │ +│ ホール [−] 1 [+] │ +│ キッチン [−] 1 [+] │ +│ レジ [−] 0 [+] │ +│──────────────────────────────────│ +│ 10:00-11:00 │ +│ ホール [−] 1 [+] │ +│ ... │ +└──────────────────────────────────┘ +┌──────────────────────────────────┐ +│ ▶ 昼 11:00〜14:00 │ ← 折り畳み状態 +└──────────────────────────────────┘ +``` + +- TIME_PERIODSに基づく時間帯グループごとのアコーディオン +- 複数同時展開可能(`multiple collapsible`) +- デフォルトで最初のグループが開いた状態 +- 営業時間に該当するグループのみ表示 +- PC版では非表示(`display: none`) + +### 7.6 WeeklyHeatmap + +``` ++------+--+--+--+--+--+--+--+--+ +| 時間 | 月| 火| 水| 木| 金| 土| 日| 祝| ++------+--+--+--+--+--+--+--+--+ +| 9:00 | | | | | | | | | ← 色付きセル(数値表示) +| 10:00| | | | | | | | | +| ... | ++------+--+--+--+--+--+--+--+--+ +| 合計 | 24| 24| 24| 24| 24| 30| 20| 30| ++------+--+--+--+--+--+--+--+--+ + 少 ■■■■■ 多(最大N人) +``` + +- セルにポジション合計人数を表示(0は空欄) +- セルの背景色はヒートマップ色段階(gray.50〜blue.700) +- `blue.700`セルは白文字 +- フッター行に曜日別合計(0は`-`) +- 凡例: 色グラデーション + 最大人数 +- PC版: フルサイズテーブル +- SP版: コンパクトグリッド(小さいpadding、横スクロール対応) + +### 7.7 PC版アクションバー + +- テーブル下部にsticky表示(bottom: 0, zIndex: 10) +- 左: 「初期設定をやり直す」ボタン(`onResetSetup`がある場合のみ、赤系) +- 右: 未保存メッセージ(`orange.600`)+ 「保存する」ボタン(teal) +- 上辺に影(`boxShadow: "0 -2px 4px rgba(0,0,0,0.04)"`) +- SP版では非表示(`display: none`) + +### 7.8 SP版アクションバー(MobileActionBar) + +- bottom: 60px(BottomMenu上部)に固定 +- 高さ: 48px +- 右寄せレイアウト +- 保存ボタン(teal、pill形状 `borderRadius: full`) + - 未保存時: オレンジドットインジケーター表示 + - 未変更時: disabled +- 上辺に影(`boxShadow: "0 -2px 8px rgba(0, 0, 0, 0.08)"`) +- PC版では非表示(`display: none`) + +### 7.9 SP版メニューBottomSheet + +- ヘッダー横の「メニュー」ボタンから開く +- タイトル: 「メニュー」 +- 3つのアクションボタン(大きめサイズ、outline、縦並び): + - 「他の曜日にコピー」(LuCopyアイコン) + - 「AIで作り直す」(LuRefreshCwアイコン) + - 「初期設定をやり直す」(赤系、LuRotateCcwアイコン、`onResetSetup`がある場合のみ) + +### 7.10 CopyModal + +- PC版: Dialog / SP版: BottomSheet +- タイトル: 「コピー先を選択」 +- コピー元曜日名を表示: 「{曜日}曜日の設定を他の曜日にコピーします。」 +- DaySelector: 2行レイアウト(月〜金 / 土日祝) +- 一括選択リンク: 平日 / 休日 / 全て +- 注意: 「選択した曜日の設定は上書きされます」(`orange.600`) +- SP版: ボタンはBottomSheet内にインライン表示(コピーする + キャンセル) + +### 7.11 RegenerateModal + +- PC版: Dialog / SP版: BottomSheet +- タイトル: 「AIで作り直す」 +- AIInputFieldsコンポーネント: + - 説明文: 「AIが必要人員を提案するための参考情報です。詳しく書くほど精度が上がります。」 + - 店舗タイプ(Textarea、2行、placeholder: "例: カフェ、ランチメインで夜は軽め...") + - 来客数(Input、placeholder: "例: 平日80人、土日120人くらい") +- 前回値がプリセット表示 +- 送信ボタン: 「作り直す」(店舗タイプ未入力時disabled) +- SP版: ボタンはBottomSheet内にインライン表示(作り直す + キャンセル) + +### 7.12 SetupWizard + +#### ステップインジケーター + +``` +Step 1: AI生成 → Step 2: 調整・保存 +``` + +- 現在のステップは `teal.600` + bold +- 非アクティブステップは `gray.400` + +#### AIGenerateForm + +- FormCardレイアウト(アイコン: LuBot、タイトル: "AIで人員配置を提案") +- AIInputFieldsコンポーネント(店舗タイプ + 来客数) +- 営業時間とポジション一覧を参考情報として表示(`gray.50`背景) +- 「提案してもらう」ボタン(teal、LuSparklesアイコン) +- スキップリンク: 「手動で入力する場合は スキップ」 + +#### パターン管理 + +- 複数パターン時のみパターンタブを表示 +- 現在のパターン: `solid` + `teal` +- 非選択パターン: `outline` + `gray` +- AI生成時のデフォルト適用曜日: 平日(月〜金) +- スキップ時のデフォルト適用曜日: 平日(月〜金) +- 新パターン追加時: 現在のパターンのstaffingをコピー + 未使用曜日の先頭を自動割当 + +#### Step 2 アクションボタン + +- PC版: 左に「戻る」、右に「別パターンを追加」+「保存する」 +- SP版: fixed action bar(bottom: 60px, 高さ 48px)で同じボタン構成(サイズsm) +- SP版は120px の bottom padding でコンテンツがaction barに隠れないようにする + +### 7.13 QuickNavBar(SP版・未使用) + +- bottom: 60px に固定表示(MobileActionBar同様の位置) +- 時間帯グループボタン(pill形状)でスムーススクロールナビゲーション +- 1グループ以下かつchildren無しの場合はレンダリングしない +- アクティブ: solid + teal / 非アクティブ: outline + gray +- PC版では非表示 + +### 7.14 SummaryBar(未使用) + +- `gray.50`背景のFlexバー +- 3つのメトリクス: + - LuChartBarアイコン + 「週合計: {N}人時」 + - LuClockアイコン + 「ピーク: {曜日} {時間}({N}人)」(peakInfoがある場合のみ) + - LuCalendarCheckアイコン + 「{N}/8日設定済」 + +--- + +## 8. 自動処理ルール + +### 8.1 時間帯リスト生成 + +- `parseHour(time)`: "HH:MM" 形式から時(hour)を数値で取得 +- `generateHourRange(openTime, closeTime)`: openTime〜closeTimeの各時間を配列化 +- 例: openTime="09:00", closeTime="22:00" → `[9, 10, 11, ..., 21]` + +### 8.2 設定済み曜日判定 + +- 全8曜日について、いずれかの時間帯×ポジションで `requiredCount > 0` があれば「設定済み」 +- DayTabsの文字色濃淡に反映 + +### 8.3 曜日色判定 + +- `getDayColor(dayIndex)`: + - 土曜(6) → `"blue.500"` + - 日曜(0)/祝日(7) → `"red.500"` + - 平日 → `undefined`(デフォルト色) + +### 8.4 内部データ構造 + +#### staffingMap(3次元キー: useStaffingData内) + +```typescript +// キー形式: `${dayOfWeek}-${hour}-${position}` +// 例: "1-9-ホール" → 2(月曜9時のホールに2人必要) +Record +``` + +- `createStaffingKey(day, hour, position)` でキー生成 +- `createStaffingMapFromFlat(records)` でDBレコードから変換 + +#### staffingMap(2次元キー: StaffingTable/MobileAccordionView内) + +```typescript +// キー形式: `${hour}-${position}` +// 例: "9-ホール" → 2 +Record +``` + +- `createHourPositionKey(hour, position)` でキー生成 +- `createStaffingMapFromEntries(staffing)` でStaffingEntry[]から変換 + +#### エントリ更新 + +- `updateStaffingEntry(staffing, hour, position, value)`: 値を0-10にclampして新配列を返す(immutable) + +### 8.5 コピー時の処理順序 + +1. 現在の曜日をDBに保存(`onSave`) +2. DBでコピー元→コピー先にデータ複製(`onCopy`) +3. ローカル `staffingMap` も即座に更新(`copyToTargetDays`) + +### 8.6 未保存変更の復元 + +曜日切替時に「破棄して移動」を選択した場合(`useDayNavigation`フック): +1. `pendingDay` に遷移先の曜日を保存 +2. 未保存ダイアログを表示 +3. 「破棄して移動」→ `onResetCurrentDay()` で現在の曜日を初期値に復元 +4. `setSelectedDay(pendingDay)` で曜日切替 +5. `hasChanges` を `false` に戻す + +`resetCurrentDay()` の処理: +1. 現在の曜日のキーを全て0にリセット +2. `initialStaffing` から現在の曜日のデータを再適用 +3. `hasChanges` を `false` に戻す + +### 8.7 AI生成ロジック(モック版・TODO) + +現在は仮の生成ロジック(`generateMockStaffing`): +- ランチタイム(11-14時): 各ポジション3人 +- ディナータイム(18-21時): 各ポジション3人 +- その他: 1人 +- キッチンは -1(`Math.max(1, count - 1)`) +- その他ポジションはランチ時のみ1、それ以外は0 + +※ 将来的にAI APIに置き換え予定 + +### 8.8 ヒートマップ計算 + +`calculateHeatmapData({ staffingMap, hours, positions })`: +1. 各セル(時間×曜日)のポジション合計を算出 +2. 全セル中の最大値(maxCount)を追跡 +3. 各セルの色を `getColorToken(count, maxCount)` で決定 +4. 曜日別合計(dailyTotals)を算出 + +`getColorToken(count, maxCount)`: +- 0 or maxCount=0 → `gray.50` +- ratio ≤ 0.25 → `blue.100` +- ratio ≤ 0.50 → `blue.300` +- ratio ≤ 0.75 → `blue.500` +- ratio > 0.75 → `blue.700` + +### 8.9 週間サマリー計算(SummaryBar用・未使用) + +`calculateWeeklySummary({ staffingMap, hours, positions })`: +- weeklyTotalPersonHours: 全曜日×全時間×全ポジションの合計 +- peakInfo: 最大合計の曜日・時間帯(`{ day: "月", hour: "12:00", count: 8 }`) +- configuredDaysCount: データが入っている曜日数 + +--- + +## 9. データフロー + +### 9.1 全体フロー + +``` +[Route] /shops/:shopId/shifts/settings + │ shopId パラメータ抽出 + ▼ +[Page] StaffingSettingsPage + │ useQuery: shop, positions, requiredStaffing + │ useMutation: upsert, copyToMultipleDays, saveAll + │ データフラット化(nested → flat) + ├─ hasNoData || showWizard + │ ▼ + │ [SetupWizard] + │ │ saveAll mutation(全曜日一括) + │ ▼ + │ [Convex DB] + │ + └─ データあり + ▼ + [StaffingRequirement] + │ + ├─ useStaffingData: staffingMap管理 + │ ├─ staffingMap(3次元Record) + │ ├─ hasChanges + │ ├─ configuredDays + │ ├─ currentDayStaffing / currentDayInitialStaffing + │ ├─ handleStaffingChange / resetCurrentDay + │ ├─ buildStaffingArray / copyToTargetDays + │ └─ applyRegenerated + │ + ├─ useDayNavigation: 曜日切替 + 未保存警告 + │ ├─ handleDayChange + │ ├─ handleDiscardAndMove + │ └─ unsavedDialog + │ + ├─ onSave → upsert mutation(曜日単位) + ├─ onCopy → copyToMultipleDays mutation + └─ 子コンポーネント + ├─ DayTabs(曜日切替) + ├─ StaffingTable(PC: Table / SP: MobileAccordionView) + ├─ WeeklyHeatmap(一覧ビュー) + ├─ CopyModal(PC: Dialog / SP: BottomSheet) + ├─ RegenerateModal(PC: Dialog / SP: BottomSheet) + ├─ MobileActionBar(SP版保存ボタン) + └─ BottomSheet(SP版メニュー) +``` + +### 9.2 DB構造 → コンポーネント変換 + +``` +DB (nested): +[ + { + _id, shopId, + dayOfWeek: 1, + staffing: [ + { hour: 9, position: "ホール", requiredCount: 2 }, + { hour: 9, position: "キッチン", requiredCount: 1 }, + ... + ] + }, + ... +] + + ↓ flatMap変換(StaffingSettingsPage) + +フラット配列 (RequiredStaffingFlat[]): +[ + { _id, shopId, dayOfWeek: 1, hour: 9, position: "ホール", requiredCount: 2 }, + { _id, shopId, dayOfWeek: 1, hour: 9, position: "キッチン", requiredCount: 1 }, + ... +] + + ↓ Map変換(useStaffingData内 createStaffingMapFromFlat) + +staffingMap (Record): +{ + "1-9-ホール": 2, + "1-9-キッチン": 1, + ... +} +``` + +### 9.3 Convex Mutations + +| Mutation | 用途 | 引数 | +|----------|------|------| +| `upsert` | 曜日単位の保存/更新 | shopId, dayOfWeek, staffing[], aiInput? | +| `copyToMultipleDays` | 複数曜日への一括コピー | shopId, sourceDayOfWeek, targetDaysOfWeek[] | +| `saveAll` | 全曜日分一括保存(初期設定用) | shopId, settings[{dayOfWeek, staffing[]}], aiInput? | + +### 9.4 Convex Queries + +| Query | 用途 | 引数 | +|-------|------|------| +| `getByShopId` | 全曜日分の設定を取得 | shopId | +| `getByShopIdAndDay` | 特定曜日の設定を取得 | shopId, dayOfWeek | + +### 9.5 バリデーション + +| 項目 | ルール | 実施場所 | +|------|--------|----------| +| dayOfWeek | 0〜7の範囲 | Convex mutation (`upsert`) | +| requiredCount | 0〜10の範囲 | フロント (`StepperCell`, `updateStaffingEntry`) | +| コピー元データ | 存在必須 | Convex mutation (`copyToMultipleDays`) | +| SetupWizard保存 | 全パターンにstaffing + appliedDaysが必要 | フロント (`SetupWizard`) | + +### 9.6 トースト通知 + +| アクション | 成功メッセージ | 失敗メッセージ | +|-----------|--------------|--------------| +| 保存 | 「必要人員設定を保存しました」 | 「保存に失敗しました」 | +| コピー | 「設定をコピーしました」 | 「コピーに失敗しました」 | +| AI再生成 | 「AIで再生成しました」 | - | +| 初期設定保存 | 「初期設定を保存しました」 | 「保存に失敗しました」 | + +--- + +## 10. コンポーネント構成 + +``` +StaffingRequirement/ +├── index.tsx # メインコンポーネント +├── index.stories.tsx # Storybook +├── types.ts # 型定義 +├── constants.ts # 定数(DAY_LABELS, WEEKDAY_ORDER, TIME_PERIODS等) +├── useStaffingData.ts # staffingMap管理カスタムフック +├── useDayNavigation.ts # 曜日切替+未保存警告カスタムフック +├── AIGenerateForm/ # AI生成フォーム(Step 1) +│ └── index.tsx +├── AIInputFields/ # AI入力フィールド共通コンポーネント +│ └── index.tsx +├── CopyModal/ # コピーモーダル(PC: Dialog / SP: BottomSheet) +│ └── index.tsx +├── DaySelector/ # 曜日チェックボックス(一括選択付き) +│ └── index.tsx +├── DayTabs/ # 曜日タブ +│ └── index.tsx +├── MobileAccordionView/ # SP版: 時間帯グループアコーディオン +│ └── index.tsx +├── MobileActionBar/ # SP版: 固定保存ボタン +│ └── index.tsx +├── QuickNavBar/ # SP版: 時間帯ナビゲーションバー(未使用) +│ └── index.tsx +├── RegenerateModal/ # AI再生成モーダル(PC: Dialog / SP: BottomSheet) +│ └── index.tsx +├── SetupWizard/ # 初期設定ウィザード(2ステップ) +│ └── index.tsx +├── StaffingTable/ # 人員テーブル(PC: Table / SP: MobileAccordionView) +│ ├── index.tsx +│ └── StepperCell.tsx # +/-ステッパーセル +├── SummaryBar/ # 週間サマリー(未使用) +│ └── index.tsx +├── WeeklyHeatmap/ # 週間ヒートマップ +│ └── index.tsx +└── utils/ + ├── dayHelpers.ts # 曜日色ヘルパー + ├── generateMockStaffing.ts # AI生成モック + ├── heatmapCalculations.ts # ヒートマップ計算 + ├── staffingMapHelpers.ts # Map操作ヘルパー + ├── staffingMapHelpers.test.ts # テスト + ├── summaryCalculations.ts # サマリー計算 + ├── timeHelpers.ts # 時間解析ヘルパー + └── timeHelpers.test.ts # テスト +``` + +--- + +## 11. レスポンシブ対応一覧 + +| 機能 | PC (md以上) | SP (base) | +|------|-------------|-----------| +| ビュー切替 | SegmentGroup | 同左 | +| メニュー | タブ横にコピー/作り直すボタン | ヘッダー横にメニューボタン→BottomSheet | +| 曜日タブ | コンパクト表示 | 全幅、横スクロール | +| 人員テーブル | Table形式(sticky header) | MobileAccordionView(時間帯グループ) | +| アクションバー | sticky bottom bar(保存+リセット) | MobileActionBar(保存のみ) | +| CopyModal | Dialog | BottomSheet | +| RegenerateModal | Dialog | BottomSheet | +| WeeklyHeatmap | フルサイズテーブル | コンパクトグリッド(横スクロール) | +| SetupWizard操作 | inline ボタン | fixed action bar(bottom: 60px) | + +--- + +## 12. 未実装・TODO + +| 項目 | 現状 | 備考 | +|------|------|------| +| AI API連携 | モック生成(`generateMockStaffing`) | AIGenerateForm, RegenerateModal両方で使用。将来的にAI APIに置き換え予定 | +| requiredStaffingクエリ | 一時的にモック(`[]`) | `pnpm convex:dev` 実行後に有効化 | +| SummaryBar | コンポーネント定義済み・未使用 | 週間統計(総人時、ピーク情報、設定済み曜日数)。計算ロジック(summaryCalculations.ts)も実装済み | +| QuickNavBar | コンポーネント定義済み・未使用 | SP版の時間帯グループ間スムーススクロールナビ | diff --git "a/doc/features/\343\202\267\343\203\225\343\203\210\347\256\241\347\220\206.md" "b/doc/features/\343\202\267\343\203\225\343\203\210\347\256\241\347\220\206.md" index 01743f08..7e80ae20 100644 --- "a/doc/features/\343\202\267\343\203\225\343\203\210\347\256\241\347\220\206.md" +++ "b/doc/features/\343\202\267\343\203\225\343\203\210\347\256\241\347\220\206.md" @@ -82,21 +82,33 @@ Shift/ ├── RecruitmentList/ # 募集一覧 ├── RecruitmentDetail/ # 募集詳細 ├── RecruitmentNew/ # 募集新規作成 -└── StaffingRequirement/ # 必要人員設定 - ├── StaffingTable/ # 人員テーブル +└── StaffingRequirement/ # 必要人員設定(PC/SP対応) + ├── StaffingTable/ # 人員テーブル(PC: Table / SP: MobileAccordionView) ├── WeeklyHeatmap/ # 週間ヒートマップ - ├── SetupWizard/ # 初期設定ウィザード + ├── SetupWizard/ # 初期設定ウィザード(2ステップ) ├── AIGenerateForm/ # AI生成フォーム - └── ... # DaySelector, DayTabs, SummaryBar等 + ├── AIInputFields/ # AI入力フィールド共通 + ├── CopyModal/ # コピーモーダル(PC: Dialog / SP: BottomSheet) + ├── RegenerateModal/ # AI再生成モーダル(PC: Dialog / SP: BottomSheet) + ├── MobileAccordionView/ # SP版: 時間帯アコーディオン + ├── MobileActionBar/ # SP版: 固定保存ボタン + ├── QuickNavBar/ # SP版: 時間帯ナビ(未使用) + ├── DaySelector/ # 曜日チェックボックス + ├── DayTabs/ # 曜日タブ + ├── SummaryBar/ # 週間サマリー(未使用) + └── utils/ # ヘルパー関数(時間、Map操作、ヒートマップ計算等) ``` ## API ### Queries -- `requiredStaffing.queries` - 必要人員データ取得 +- `requiredStaffing.queries.getByShopId` - 店舗の全曜日分の必要人員設定を取得 +- `requiredStaffing.queries.getByShopIdAndDay` - 特定曜日の必要人員設定を取得 ### Mutations -- `requiredStaffing.mutations` - 必要人員データ更新 +- `requiredStaffing.mutations.upsert` - 曜日単位の保存/更新 +- `requiredStaffing.mutations.copyToMultipleDays` - 複数曜日への一括コピー +- `requiredStaffing.mutations.saveAll` - 全曜日分一括保存(初期設定用) ## 状態管理(Jotai - ShiftForm内スコープ) @@ -116,4 +128,5 @@ ShiftFormはJotai Providerでスコープされたアトムを使用(グロー ## 仕様書 - [シフト編集機能仕様](./detail//2026-02-08_シフト編集機能仕様.md) - ShiftFormの詳細仕様(操作・イベント・表示仕様) +- [必要人員設定機能仕様](./detail/2026-02-10_必要人員設定機能仕様.md) - StaffingRequirementの詳細仕様(PC/SP対応、全操作・表示・データフロー) - [シフト管理機能仕様](../spec/2026-01-03_シフト管理機能.md) - シフト管理全体の仕様 diff --git "a/doc/plans/2026-02-10_\345\277\205\350\246\201\344\272\272\345\223\241\350\250\255\345\256\232SP\347\211\210\343\203\254\343\202\244\343\202\242\343\202\246\343\203\210.md" "b/doc/plans/2026-02-10_\345\277\205\350\246\201\344\272\272\345\223\241\350\250\255\345\256\232SP\347\211\210\343\203\254\343\202\244\343\202\242\343\202\246\343\203\210.md" new file mode 100644 index 00000000..48c82576 --- /dev/null +++ "b/doc/plans/2026-02-10_\345\277\205\350\246\201\344\272\272\345\223\241\350\250\255\345\256\232SP\347\211\210\343\203\254\343\202\244\343\202\242\343\202\246\343\203\210.md" @@ -0,0 +1,304 @@ +# 必要人員設定 SP版レイアウト実装計画 + +**作成日**: 2026-02-10 +**関連仕様**: `doc/features/detail/2026-02-10_必要人員設定機能仕様.md` +**対象**: `src/components/features/Shift/StaffingRequirement/` + +--- + +## 1. 背景・目的 + +必要人員設定(StaffingRequirement)は現在PC版のみ対応。 +SP版レイアウトを追加し、スマートフォンでも快適に人員設定の確認・編集を行えるようにする。 + +### 現状の問題点 + +1. **固定バーの重複** - QuickNavBar(`bottom:70px`) + ActionBar(`sticky bottom:0`) + BottomMenu(60px) が三重に重なり、コンテンツ領域を圧迫 +2. **StepperCellのタッチターゲット不足** - `size="2xs"`(約24x24px)は推奨44pxを大幅に下回る +3. **アクションボタンの配置問題** - コピー/作り直すボタンがDayTabsとのFlex wrapで不自然な位置に +4. **ポジション小計が見えない** - PC版テーブルヘッダーの`(12)`のような合計がSPでは消える +5. **コピー/再生成モーダルがDialog** - SP版ではBottomSheetが自然 + +--- + +## 2. 決定事項 + +- スワイプジェスチャー: **不採用**(DayTabsタップのみ) +- CopyModal/RegenerateModal SP表示: **BottomSheet** +- SecondaryActions配置: **スクロール末尾** +- 表示方式: **アコーディオン**(時間帯で折りたたみ、中は1時間単位の設定) + +--- + +## 3. ワイヤー + +### 3.1 日別モード(メイン編集ビュー) + +``` ++------------------------------------------+ +| < シフト管理 | +| [Settings] 必要人員設定 | +| cafe de paris | ++------------------------------------------+ +| [日別][一覧] | ++------------------------------------------+ +| [月][火][水][木][金][土][日][祝] | ++------------------------------------------+ +| 月曜日 ホール:12 キッチン:8 計:20 | ← NEW: DaySummaryRow ++------------------------------------------+ +| | +| v 朝 (9:00-11:00) 4人時 v | +| +---------------------------------------+ | +| | 9:00-10:00 | | +| | ホール [−] 1 [+] | | +| | キッチン [−] 1 [+] | | +| +---------------------------------------+ | +| | 10:00-11:00 | | +| | ホール [−] 1 [+] | | +| | キッチン [−] 1 [+] | | +| +---------------------------------------+ | +| | +| > ランチ (11:00-14:00) 18人時 | +| > 午後 (14:00-17:00) 6人時 | +| > ディナー (17:00-21:00) 24人時 | +| > 夜 (21:00-22:00) 2人時 | +| | +| ─── その他の操作 ─────────────────────── | ← NEW: SecondaryActions +| [コピー] [作り直す] | +| [初期設定をやり直す] | +| | +| (padding: 120px) | ++------------------------------------------+ +| [朝][ランチ][午後][ディナー][夜] [●保存] | ← NEW: MobileActionBar ++------------------------------------------+ +| マイページ シフト 店舗 スタッフ メニュー | ← BottomMenu (60px) ++------------------------------------------+ +``` + +### 3.2 未保存状態の表現 + +``` +通常時: +| [朝][ランチ][午後][ディナー][夜] [保存] | + ~~~~~~~~ + disabled, gray + +変更あり: +| [朝][ランチ][午後][ディナー][夜] [●保存] | + ~~~~~~~~ + enabled, teal + orange dot +``` + +### 3.3 一覧モード(WeeklyHeatmap) + +``` ++------------------------------------------+ +| < シフト管理 | +| [Settings] 必要人員設定 | +| cafe de paris | ++------------------------------------------+ +| [日別][一覧] | ++------------------------------------------+ +| +| 時間 月 火 水 木 金 土 日 祝 | +| ─── ── ── ── ── ── ── ── ── | +| 9 2 2 2 2 2 3 2 3 | +| 10 2 2 2 2 2 3 2 3 | +| 11 6 6 6 6 6 8 4 8 | +| 12 6 6 6 6 6 8 4 8 | +| ... | +| ─── ── ── ── ── ── ── ── ── | +| 計 40 40 40 40 40 52 28 52 | +| | +| 少 ■■■■■ 多(最大8人) | +| | +| ※ 曜日をタップすると日別ビューに移動 | ++------------------------------------------+ +| マイページ シフト 店舗 スタッフ メニュー | ++------------------------------------------+ +``` + +- MobileActionBar非表示(編集なし) +- 既存WeeklyHeatmapコンパクト版を活用 + +### 3.4 コピーフロー(BottomSheet) + +``` ++------------------------------------------+ +| (暗転オーバーレイ) | ++==========================================+ +| コピー先を選択 [X] | ++------------------------------------------+ +| 月曜日の設定を他の曜日にコピーします | +| | +| コピー先 [平日] [休日] [全て] | +| | +| [v] 火 [v] 水 [v] 木 [v] 金 | +| [ ] 土 [ ] 日 [ ] 祝 | +| | +| ⚠ 選択した曜日の設定は上書きされます | +| | +| [キャンセル] [コピーする] | ++------------------------------------------+ +``` + +### 3.5 AI再生成フロー(BottomSheet) + +``` ++==========================================+ +| AIで作り直す [X] | ++------------------------------------------+ +| どんなお店ですか? | +| ┌────────────────────────────────┐ | +| │ カフェ、ランチメインで夜は軽め │ | +| └────────────────────────────────┘ | +| | +| 1日の来客数は?(ざっくりでOK) | +| ┌────────────────────────────────┐ | +| │ 平日80人、土日120人くらい │ | +| └────────────────────────────────┘ | +| | +| [キャンセル] [作り直す] | ++------------------------------------------+ +``` + +### 3.6 SetupWizard Step2(編集・曜日選択) + +``` ++------------------------------------------+ +| Step 1: AI生成 > Step 2: 調整・保存 | ++------------------------------------------+ +| [パターン1][パターン2] <パターンタブ> | ++------------------------------------------+ +| v 朝 (9:00-11:00) 4人時 v | +| ... (MobileAccordionViewと同じ) | +| > ランチ (11:00-14:00) 18人時 | +| > 午後 (14:00-17:00) 6人時 | +| > ディナー (17:00-21:00) 18人時 | ++------------------------------------------+ +| この設定を適用する曜日 | +| [平日] [休日] [全て] | +| [v] 月 [v] 火 [v] 水 [v] 木 [v] 金 | +| [ ] 土 [ ] 日 [ ] 祝 | ++------------------------------------------+ +| (padding: 120px) | ++------------------------------------------+ +| [<< 戻る] [+別パターン] [保存する] | ++------------------------------------------+ +| [朝][ランチ][午後][ディナー][夜] | ++------------------------------------------+ +| マイページ シフト 店舗 スタッフ メニュー | ++------------------------------------------+ +``` + +### 3.7 確認ダイアログ(未保存警告・リセット) + +- **中央Dialogのまま**(BottomSheetにしない) +- 警告・確認系は中央配置が適切 + +--- + +## 4. コンポーネント設計 + +### 新規コンポーネント + +| コンポーネント | パス | 責務 | +|--------------|------|------| +| DaySummaryRow | `StaffingRequirement/DaySummaryRow/index.tsx` | 選択曜日名 + ポジション別合計 + 全体合計の1行表示。SP専用(`display={{ base: "block", md: "none" }}`) | +| MobileActionBar | `StaffingRequirement/MobileActionBar/index.tsx` | QuickNavBar + 保存ボタン統合バー。`fixed bottom: 60px`, 高さ48px | +| SecondaryActions | `StaffingRequirement/SecondaryActions/index.tsx` | コピー・作り直す・リセットをスクロール末尾に表示。SP専用 | + +### 既存コンポーネントの変更 + +| コンポーネント | ファイル | 変更内容 | +|--------------|---------|---------| +| StepperCell | `StaffingTable/StepperCell.tsx` | `size="2xs"` → `size={{ base: "xs", md: "2xs" }}` | +| MobileAccordionView | `MobileAccordionView/index.tsx` | QuickNavBar内部レンダリングを削除(MobileActionBarに統合) | +| QuickNavBar | `QuickNavBar/index.tsx` | children slot追加(保存ボタン挿入用) | +| CopyModal | `CopyModal/index.tsx` | SP: BottomSheet / PC: Dialog のレスポンシブ切替 | +| RegenerateModal | `RegenerateModal/index.tsx` | SP: BottomSheet / PC: Dialog のレスポンシブ切替 | +| index.tsx | `index.tsx` | SP用レイアウト統合、アクションバー/ボタンのdisplay切替 | +| SetupWizard | `SetupWizard/index.tsx` | SP時のアクションバー・QuickNavBar対応 | + +### スペーシング定義 + +| 要素 | 高さ | 位置 | +|------|------|------| +| BottomMenu | 60px | `fixed bottom: 0` | +| MobileActionBar | 48px | `fixed bottom: 60px` | +| 固定領域合計 | 108px | | +| コンテンツ末尾余白 | 120px | `pb="120px"` (余裕込み) | + +--- + +## 5. 実装ステップ + +| # | タスク | 影響ファイル | 依存 | +|---|--------|------------|------| +| 1 | StepperCellタッチターゲット拡大 | `StepperCell.tsx` | なし | +| 2 | DaySummaryRow新規作成 + stories | `DaySummaryRow/index.tsx`, `index.stories.tsx` | なし | +| 3 | MobileAccordionViewからQuickNavBar分離 | `MobileAccordionView/index.tsx` | なし | +| 4 | MobileActionBar新規作成 + stories | `MobileActionBar/index.tsx`, `index.stories.tsx` | #3 | +| 5 | SecondaryActions新規作成 + stories | `SecondaryActions/index.tsx`, `index.stories.tsx` | なし | +| 6 | index.tsx SP用レイアウト統合 | `index.tsx` | #2, #4, #5 | +| 7 | CopyModal/RegenerateModal BottomSheet対応 | `CopyModal/index.tsx`, `RegenerateModal/index.tsx` | なし | +| 8 | SetupWizard SP対応 | `SetupWizard/index.tsx` | #3, #4 | +| 9 | Storybook SPビューポート追加 | `index.stories.tsx` | #6 | + +### 対象ファイルツリー + +``` +src/components/features/Shift/StaffingRequirement/ +├── index.tsx ← 修正(SP レイアウト統合) +├── index.stories.tsx ← 修正(SPビューポートストーリー追加) +├── StaffingTable/ +│ └── StepperCell.tsx ← 修正(タッチターゲット拡大) +├── MobileAccordionView/ +│ └── index.tsx ← 修正(QuickNavBar分離) +├── QuickNavBar/ +│ └── index.tsx ← 修正(children slot追加) +├── CopyModal/ +│ └── index.tsx ← 修正(BottomSheet対応) +├── RegenerateModal/ +│ └── index.tsx ← 修正(BottomSheet対応) +├── SetupWizard/ +│ └── index.tsx ← 修正(SP アクションバー) +├── DaySummaryRow/ ← 新規 +│ ├── index.tsx +│ └── index.stories.tsx +├── MobileActionBar/ ← 新規 +│ ├── index.tsx +│ └── index.stories.tsx +└── SecondaryActions/ ← 新規 + ├── index.tsx + └── index.stories.tsx +``` + +--- + +## 6. 検証方法 + +- Storybook: SPビューポート(375px幅)でのストーリー追加 +- 手動確認: Chrome DevTools モバイルエミュレーション +- チェックポイント: + - [ ] StepperCellが指で楽に押せるか(44px以上) + - [ ] QuickNavBar + 保存ボタンが1段に収まるか + - [ ] 3段固定バー問題が解消されているか(合計108px以内) + - [ ] CopyModal/RegenerateModalがBottomSheetで表示されるか + - [ ] SetupWizard Step2がSPで操作できるか + - [ ] ヒートマップが横スクロールで見れるか + - [ ] アコーディオン開閉で1時間単位の設定が見えるか + +--- + +## 8. 現在の進捗 + +- [x] ステップ1: StepperCellタッチターゲット拡大 +- [x] ステップ2: DaySummaryRow新規作成 +- [x] ステップ3: MobileAccordionViewからQuickNavBar分離 +- [x] ステップ4: MobileActionBar新規作成 +- [x] ステップ5: SecondaryActions新規作成 +- [x] ステップ6: index.tsx SP用レイアウト統合 +- [x] ステップ7: CopyModal/RegenerateModal BottomSheet対応 +- [x] ステップ8: SetupWizard SP対応 +- [x] ステップ9: Storybook SPビューポート追加 diff --git a/src/components/features/Shift/StaffingRequirement/AIGenerateForm/index.tsx b/src/components/features/Shift/StaffingRequirement/AIGenerateForm/index.tsx index b0decf17..3265fb6b 100644 --- a/src/components/features/Shift/StaffingRequirement/AIGenerateForm/index.tsx +++ b/src/components/features/Shift/StaffingRequirement/AIGenerateForm/index.tsx @@ -1,7 +1,8 @@ -import { Box, Button, Field, Flex, Icon, Input, Text, Textarea, VStack } from "@chakra-ui/react"; +import { Box, Button, Flex, Icon, Text, VStack } from "@chakra-ui/react"; import { useState } from "react"; import { LuBot, LuSparkles } from "react-icons/lu"; import { FormCard } from "@/src/components/ui/FormCard"; +import { AIInputFields } from "../AIInputFields"; import type { AIInput, PositionType, StaffingEntry } from "../types"; import { generateMockStaffing } from "../utils/generateMockStaffing"; @@ -31,7 +32,6 @@ export const AIGenerateForm = ({ const aiInput: AIInput = { shopType, customerCount }; // TODO: 実際のAI API呼び出しに置き換え - // 仮の生成ロジック const generatedStaffing = generateMockStaffing(openTime, closeTime, positions); onGenerate(generatedStaffing, aiInput); @@ -42,31 +42,13 @@ export const AIGenerateForm = ({ return ( - - どんなお店ですか? -