diff --git a/.gitignore b/.gitignore index 03d43b94..92843f90 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ convex-seeds/backup .mcp.json CLAUDE.local.md -character.md \ No newline at end of file +character.md +.cognac/ diff --git a/cognac.config.ts b/cognac.config.ts new file mode 100644 index 00000000..61388f50 --- /dev/null +++ b/cognac.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "@yn1323/cognac"; + +export default defineConfig({ + port: 4000, + git: { + defaultBranch: "develop", + }, + ci: { + maxRetries: 5, + }, + discussion: { + maxRounds: 3, + minPersonas: 2, + maxPersonas: 4, + }, + claude: { + maxTurnsExecution: 30, + maxTurnsDiscussion: 1, + stdoutTimeoutMs: 600000, + processMaxRetries: 2, + }, +}); diff --git a/convex/helpers.ts b/convex/helpers.ts index 5e9e8612..961c4466 100644 --- a/convex/helpers.ts +++ b/convex/helpers.ts @@ -2,7 +2,7 @@ import { ConvexError } from "convex/values"; import type { Id } from "./_generated/dataModel"; import type { MutationCtx, QueryCtx } from "./_generated/server"; -import { DEFAULT_POSITIONS, SKILL_LEVELS } from "./constants"; +import { DEFAULT_POSITIONS, POSITION_COLORS, SKILL_LEVELS } from "./constants"; // セキュアなトークン生成(crypto APIを使用) export const generateToken = () => { @@ -172,6 +172,7 @@ export const initializeDefaultPositions = async (ctx: MutationCtx, shopId: Id<"s const positionId = await ctx.db.insert("shopPositions", { shopId, name: DEFAULT_POSITIONS[i], + color: POSITION_COLORS[i % POSITION_COLORS.length], order: i, isDeleted: false, createdAt: Date.now(), diff --git a/convex/position/mutations.ts b/convex/position/mutations.ts index 87264803..c416acca 100644 --- a/convex/position/mutations.ts +++ b/convex/position/mutations.ts @@ -7,8 +7,12 @@ */ import { ConvexError, v } from "convex/values"; import { mutation } from "../_generated/server"; -import { DEFAULT_POSITIONS, POSITION_COLORS, SKILL_LEVELS } from "../constants"; -import { requireShop } from "../helpers"; +import { POSITION_COLORS } from "../constants"; +import { + initializeDefaultPositions as initializeDefaultPositionsHelper, + initializeStaffSkills as initializeStaffSkillsHelper, + requireShop, +} from "../helpers"; // ポジション作成 export const create = mutation({ @@ -161,20 +165,7 @@ export const initializeDefaultPositions = mutation({ shopId: v.id("shops"), }, handler: async (ctx, args) => { - const positionIds: string[] = []; - - for (let i = 0; i < DEFAULT_POSITIONS.length; i++) { - const positionId = await ctx.db.insert("shopPositions", { - shopId: args.shopId, - name: DEFAULT_POSITIONS[i], - color: POSITION_COLORS[i % POSITION_COLORS.length], - order: i, - isDeleted: false, - createdAt: Date.now(), - }); - positionIds.push(positionId); - } - + const positionIds = await initializeDefaultPositionsHelper(ctx, args.shopId); return { success: true, positionIds }; }, }); @@ -186,21 +177,7 @@ export const initializeStaffSkills = mutation({ staffId: v.id("staffs"), }, handler: async (ctx, args) => { - const positions = await ctx.db - .query("shopPositions") - .withIndex("by_shop", (q) => q.eq("shopId", args.shopId)) - .filter((q) => q.neq(q.field("isDeleted"), true)) - .collect(); - - for (const position of positions) { - await ctx.db.insert("staffSkills", { - staffId: args.staffId, - positionId: position._id, - level: SKILL_LEVELS[0], // "未経験" - updatedAt: Date.now(), - }); - } - + await initializeStaffSkillsHelper(ctx, args.shopId, args.staffId); return { success: true }; }, }); diff --git a/convex/shop/mutations.ts b/convex/shop/mutations.ts index 57b1fcde..9d04dcbc 100644 --- a/convex/shop/mutations.ts +++ b/convex/shop/mutations.ts @@ -8,11 +8,8 @@ */ import { ConvexError, v } from "convex/values"; import { mutation } from "../_generated/server"; -import { SHOP_SUBMIT_FREQUENCY, SHOP_TIME_UNIT, SKILL_LEVELS } from "../constants"; +import { SHOP_SUBMIT_FREQUENCY, SHOP_TIME_UNIT } from "../constants"; import { - createDefaultSkills, - getStaff, - getStaffByEmail, initializeDefaultPositions, initializeStaffSkills, isValidTimeFormat, @@ -176,192 +173,6 @@ export const remove = mutation({ }, }); -// スタッフを店舗に追加(オーナーのみ) -export const addStaff = mutation({ - args: { - shopId: v.id("shops"), - email: v.string(), - displayName: v.string(), - authId: v.string(), - skills: v.optional( - v.array( - v.object({ - position: v.string(), - level: v.string(), - }), - ), - ), - maxWeeklyHours: v.optional(v.number()), - }, - handler: async (ctx, args) => { - await requireShop(ctx, args.shopId); - - const trimmedEmail = args.email.trim().toLowerCase(); - const trimmedDisplayName = args.displayName.trim(); - - if (!trimmedEmail) { - throw new ConvexError({ message: "メールアドレスは必須です", code: "EMPTY_EMAIL" }); - } - if (!trimmedDisplayName) { - throw new ConvexError({ message: "表示名は必須です", code: "EMPTY_DISPLAY_NAME" }); - } - - // 同じ店舗に同じメールアドレスのスタッフがいないかチェック - const existingStaff = await getStaffByEmail(ctx, args.shopId, trimmedEmail); - if (existingStaff) { - throw new ConvexError({ message: "このメールアドレスは既に登録されています", code: "EMAIL_ALREADY_EXISTS" }); - } - - // skillsが渡されなかった場合、全ポジション「未経験」で初期化 - const skills = args.skills ?? createDefaultSkills(); - - const staffId = await ctx.db.insert("staffs", { - shopId: args.shopId, - email: trimmedEmail, - displayName: trimmedDisplayName, - status: "active", - skills, // 後方互換のため残す - maxWeeklyHours: args.maxWeeklyHours, - invitedBy: args.authId, - createdAt: Date.now(), - isDeleted: false, - }); - - // 新テーブルにもスキルを初期化 - await initializeStaffSkills(ctx, args.shopId, staffId); - - return { success: true, staffId }; - }, -}); - -// スタッフを退職処理(オーナーのみ) -export const resignStaff = mutation({ - args: { - shopId: v.id("shops"), - staffId: v.id("staffs"), - authId: v.string(), - resignationReason: v.optional(v.string()), - }, - handler: async (ctx, args) => { - await requireShop(ctx, args.shopId); - - const staff = await getStaff(ctx, args.staffId); - if (!staff || staff.shopId !== args.shopId) { - throw new ConvexError({ message: "スタッフが見つかりません", code: "STAFF_NOT_FOUND" }); - } - - if (staff.status === "resigned") { - throw new ConvexError({ message: "このスタッフは既に退職済みです", code: "ALREADY_RESIGNED" }); - } - - await ctx.db.patch(args.staffId, { - status: "resigned", - resignedAt: Date.now(), - resignationReason: args.resignationReason, - }); - - return { success: true }; - }, -}); - -// スタッフ情報更新(オーナーのみ) -export const updateStaffInfo = mutation({ - args: { - shopId: v.id("shops"), - staffId: v.id("staffs"), - authId: v.string(), - email: v.optional(v.string()), - displayName: v.optional(v.string()), - skills: v.optional( - v.array( - v.object({ - positionId: v.id("shopPositions"), - level: v.string(), - }), - ), - ), - maxWeeklyHours: v.optional(v.union(v.number(), v.null())), - memo: v.optional(v.string()), - workStyleNote: v.optional(v.string()), - hourlyWage: v.optional(v.union(v.number(), v.null())), - }, - handler: async (ctx, args) => { - const staff = await getStaff(ctx, args.staffId); - if (!staff || staff.shopId !== args.shopId) { - throw new ConvexError({ message: "スタッフが見つかりません", code: "STAFF_NOT_FOUND" }); - } - - const fieldsToUpdate: Partial<{ - email: string; - displayName: string; - maxWeeklyHours: number | undefined; - memo: string; - workStyleNote: string; - hourlyWage: number | undefined; - }> = {}; - - if (args.email !== undefined) { - fieldsToUpdate.email = args.email.trim().toLowerCase(); - } - if (args.displayName !== undefined) { - fieldsToUpdate.displayName = args.displayName.trim(); - } - if (args.maxWeeklyHours !== undefined) { - fieldsToUpdate.maxWeeklyHours = args.maxWeeklyHours ?? undefined; - } - if (args.memo !== undefined) { - fieldsToUpdate.memo = args.memo; - } - if (args.workStyleNote !== undefined) { - fieldsToUpdate.workStyleNote = args.workStyleNote; - } - if (args.hourlyWage !== undefined) { - fieldsToUpdate.hourlyWage = args.hourlyWage ?? undefined; - } - - // スキルの更新(staffSkillsテーブル) - if (args.skills !== undefined) { - // 既存のスキルを取得 - const existingSkills = await ctx.db - .query("staffSkills") - .withIndex("by_staff", (q) => q.eq("staffId", args.staffId)) - .collect(); - - const existingSkillMap = new Map(existingSkills.map((s) => [s.positionId, s])); - - for (const skillInput of args.skills) { - if (!SKILL_LEVELS.includes(skillInput.level as (typeof SKILL_LEVELS)[number])) { - throw new ConvexError({ message: "無効なスキルレベルです", code: "INVALID_LEVEL" }); - } - - const existing = existingSkillMap.get(skillInput.positionId); - - if (existing) { - // 既存のスキルを更新 - await ctx.db.patch(existing._id, { - level: skillInput.level, - updatedAt: Date.now(), - }); - } else { - // 新規スキルを作成 - await ctx.db.insert("staffSkills", { - staffId: args.staffId, - positionId: skillInput.positionId, - level: skillInput.level, - updatedAt: Date.now(), - }); - } - } - } - - if (Object.keys(fieldsToUpdate).length > 0) { - await ctx.db.patch(args.staffId, fieldsToUpdate); - } - - return { success: true }; - }, -}); - // テスト用データリセット(メールアドレス指定) export const resetUserByEmail = mutation({ args: { email: v.string() }, diff --git a/convex/staff/mutations.ts b/convex/staff/mutations.ts new file mode 100644 index 00000000..1cd2e4dc --- /dev/null +++ b/convex/staff/mutations.ts @@ -0,0 +1,196 @@ +/** + * スタッフドメイン - ミューテーション(書き込み操作) + * + * 責務: + * - スタッフの追加・退職・情報更新 + */ +import { ConvexError, v } from "convex/values"; +import { mutation } from "../_generated/server"; +import { SKILL_LEVELS } from "../constants"; +import { createDefaultSkills, getStaff, getStaffByEmail, initializeStaffSkills, requireShop } from "../helpers"; + +// スタッフを店舗に追加(オーナーのみ) +export const addStaff = mutation({ + args: { + shopId: v.id("shops"), + email: v.string(), + displayName: v.string(), + authId: v.string(), + skills: v.optional( + v.array( + v.object({ + position: v.string(), + level: v.string(), + }), + ), + ), + maxWeeklyHours: v.optional(v.number()), + }, + handler: async (ctx, args) => { + await requireShop(ctx, args.shopId); + + const trimmedEmail = args.email.trim().toLowerCase(); + const trimmedDisplayName = args.displayName.trim(); + + if (!trimmedEmail) { + throw new ConvexError({ message: "メールアドレスは必須です", code: "EMPTY_EMAIL" }); + } + if (!trimmedDisplayName) { + throw new ConvexError({ message: "表示名は必須です", code: "EMPTY_DISPLAY_NAME" }); + } + + // 同じ店舗に同じメールアドレスのスタッフがいないかチェック + const existingStaff = await getStaffByEmail(ctx, args.shopId, trimmedEmail); + if (existingStaff) { + throw new ConvexError({ message: "このメールアドレスは既に登録されています", code: "EMAIL_ALREADY_EXISTS" }); + } + + // skillsが渡されなかった場合、全ポジション「未経験」で初期化 + const skills = args.skills ?? createDefaultSkills(); + + const staffId = await ctx.db.insert("staffs", { + shopId: args.shopId, + email: trimmedEmail, + displayName: trimmedDisplayName, + status: "active", + skills, // 後方互換のため残す + maxWeeklyHours: args.maxWeeklyHours, + invitedBy: args.authId, + createdAt: Date.now(), + isDeleted: false, + }); + + // 新テーブルにもスキルを初期化 + await initializeStaffSkills(ctx, args.shopId, staffId); + + return { success: true, staffId }; + }, +}); + +// スタッフを退職処理(オーナーのみ) +export const resignStaff = mutation({ + args: { + shopId: v.id("shops"), + staffId: v.id("staffs"), + authId: v.string(), + resignationReason: v.optional(v.string()), + }, + handler: async (ctx, args) => { + await requireShop(ctx, args.shopId); + + const staff = await getStaff(ctx, args.staffId); + if (!staff || staff.shopId !== args.shopId) { + throw new ConvexError({ message: "スタッフが見つかりません", code: "STAFF_NOT_FOUND" }); + } + + if (staff.status === "resigned") { + throw new ConvexError({ message: "このスタッフは既に退職済みです", code: "ALREADY_RESIGNED" }); + } + + await ctx.db.patch(args.staffId, { + status: "resigned", + resignedAt: Date.now(), + resignationReason: args.resignationReason, + }); + + return { success: true }; + }, +}); + +// スタッフ情報更新(オーナーのみ) +export const updateStaffInfo = mutation({ + args: { + shopId: v.id("shops"), + staffId: v.id("staffs"), + authId: v.string(), + email: v.optional(v.string()), + displayName: v.optional(v.string()), + skills: v.optional( + v.array( + v.object({ + positionId: v.id("shopPositions"), + level: v.string(), + }), + ), + ), + maxWeeklyHours: v.optional(v.union(v.number(), v.null())), + memo: v.optional(v.string()), + workStyleNote: v.optional(v.string()), + hourlyWage: v.optional(v.union(v.number(), v.null())), + }, + handler: async (ctx, args) => { + const staff = await getStaff(ctx, args.staffId); + if (!staff || staff.shopId !== args.shopId) { + throw new ConvexError({ message: "スタッフが見つかりません", code: "STAFF_NOT_FOUND" }); + } + + const fieldsToUpdate: Partial<{ + email: string; + displayName: string; + maxWeeklyHours: number | undefined; + memo: string; + workStyleNote: string; + hourlyWage: number | undefined; + }> = {}; + + if (args.email !== undefined) { + fieldsToUpdate.email = args.email.trim().toLowerCase(); + } + if (args.displayName !== undefined) { + fieldsToUpdate.displayName = args.displayName.trim(); + } + if (args.maxWeeklyHours !== undefined) { + fieldsToUpdate.maxWeeklyHours = args.maxWeeklyHours ?? undefined; + } + if (args.memo !== undefined) { + fieldsToUpdate.memo = args.memo; + } + if (args.workStyleNote !== undefined) { + fieldsToUpdate.workStyleNote = args.workStyleNote; + } + if (args.hourlyWage !== undefined) { + fieldsToUpdate.hourlyWage = args.hourlyWage ?? undefined; + } + + // スキルの更新(staffSkillsテーブル) + if (args.skills !== undefined) { + // 既存のスキルを取得 + const existingSkills = await ctx.db + .query("staffSkills") + .withIndex("by_staff", (q) => q.eq("staffId", args.staffId)) + .collect(); + + const existingSkillMap = new Map(existingSkills.map((s) => [s.positionId, s])); + + for (const skillInput of args.skills) { + if (!SKILL_LEVELS.includes(skillInput.level as (typeof SKILL_LEVELS)[number])) { + throw new ConvexError({ message: "無効なスキルレベルです", code: "INVALID_LEVEL" }); + } + + const existing = existingSkillMap.get(skillInput.positionId); + + if (existing) { + // 既存のスキルを更新 + await ctx.db.patch(existing._id, { + level: skillInput.level, + updatedAt: Date.now(), + }); + } else { + // 新規スキルを作成 + await ctx.db.insert("staffSkills", { + staffId: args.staffId, + positionId: skillInput.positionId, + level: skillInput.level, + updatedAt: Date.now(), + }); + } + } + } + + if (Object.keys(fieldsToUpdate).length > 0) { + await ctx.db.patch(args.staffId, fieldsToUpdate); + } + + return { success: true }; + }, +}); diff --git a/convex/staffSkill/queries.ts b/convex/staffSkill/queries.ts index 24055f42..f77542dc 100644 --- a/convex/staffSkill/queries.ts +++ b/convex/staffSkill/queries.ts @@ -51,6 +51,15 @@ export const listByShop = query({ .filter((q) => q.neq(q.field("isDeleted"), true)) .collect(); + // ポジション一覧を事前に一括取得してMap化(N+1回避) + const positions = await ctx.db + .query("shopPositions") + .withIndex("by_shop", (q) => q.eq("shopId", args.shopId)) + .filter((q) => q.neq(q.field("isDeleted"), true)) + .collect(); + + const positionMap = new Map(positions.map((p) => [p._id, p])); + // 各スタッフのスキルを取得 const staffSkillsMap = new Map< string, @@ -63,31 +72,21 @@ export const listByShop = query({ .withIndex("by_staff", (q) => q.eq("staffId", staff._id)) .collect(); - const skillsWithPositions = await Promise.all( - skills.map(async (skill) => { - const position = await ctx.db.get(skill.positionId); + const skillsWithPositions = skills + .map((skill) => { + const position = positionMap.get(skill.positionId); + if (!position) return null; return { - positionId: skill.positionId, - positionName: position?.name ?? "", + positionId: skill.positionId as string, + positionName: position.name, level: skill.level, - order: position?.order ?? 0, - isDeleted: position?.isDeleted ?? true, + order: position.order, }; - }), - ); + }) + .filter((s): s is NonNullable => s !== null) + .sort((a, b) => a.order - b.order); - staffSkillsMap.set( - staff._id, - skillsWithPositions - .filter((s) => !s.isDeleted) - .sort((a, b) => a.order - b.order) - .map(({ positionId, positionName, level, order }) => ({ - positionId, - positionName, - level, - order, - })), - ); + staffSkillsMap.set(staff._id, skillsWithPositions); } return Object.fromEntries(staffSkillsMap); diff --git a/package.json b/package.json index f0d2f671..d8ec5acb 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@vitejs/plugin-react": "5.1.2", "@vitest/browser": "3.2.4", "@vitest/coverage-v8": "3.2.4", + "@yn1323/cognac": "^0.2.2", "chromatic": "13.3.4", "dotenv": "17.2.3", "jsdom": "27.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 389747dc..d3bd9372 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,7 +34,7 @@ importers: version: 1.144.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/router-plugin': specifier: 1.144.0 - version: 1.144.0(@tanstack/react-router@1.144.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + version: 1.144.0(@tanstack/react-router@1.144.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) convex: specifier: 1.31.2 version: 1.31.2(@clerk/clerk-react@5.59.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) @@ -77,13 +77,13 @@ importers: version: 1.57.0 '@storybook/addon-docs': specifier: 9.1.17 - version: 9.1.17(@types/react@19.2.7)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))) + version: 9.1.17(@types/react@19.2.7)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))) '@storybook/addon-vitest': specifier: 9.1.17 - version: 9.1.17(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)))(vitest@3.2.4) + version: 9.1.17(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)))(vitest@3.2.4) '@storybook/react-vite': specifier: 9.1.17 - version: 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.50.1)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)))(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + version: 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.50.1)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)))(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) '@tanstack/react-devtools': specifier: 0.9.0 version: 0.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.9) @@ -110,13 +110,16 @@ importers: version: 0.10.11 '@vitejs/plugin-react': specifier: 5.1.2 - version: 5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + version: 5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) '@vitest/browser': specifier: 3.2.4 - version: 3.2.4(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))(vitest@3.2.4) + version: 3.2.4(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))(vitest@3.2.4) '@vitest/coverage-v8': specifier: 3.2.4 version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4) + '@yn1323/cognac': + specifier: ^0.2.2 + version: 0.2.2 chromatic: specifier: 13.3.4 version: 13.3.4 @@ -134,7 +137,7 @@ importers: version: 4.1.0 storybook: specifier: 9.1.17 - version: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + version: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) tsx: specifier: 4.21.0 version: 4.21.0 @@ -143,13 +146,13 @@ importers: version: 5.9.3 vite: specifier: 7.3.0 - version: 7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0) + version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) vite-tsconfig-paths: specifier: 6.0.3 - version: 6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + version: 6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) vitest: specifier: 3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@1.21.7)(jsdom@27.4.0)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@27.4.0)(tsx@4.21.0) web-vitals: specifier: 5.1.0 version: 5.1.0 @@ -1583,6 +1586,10 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@yn1323/cognac@0.2.2': + resolution: {integrity: sha512-oexYp2GM0PqVVeuUQdFYxoJZ9gVOaAqIe7ZxQuPGggCO+AJ5b/HJaOC2epYmDR49MmbXZA0ZxguUPS62335qmw==} + hasBin: true + '@zag-js/accordion@1.29.1': resolution: {integrity: sha512-3laCyoAsInYPooQU5+tgwxiejU25M20etHbbZ6FIql8VRhKemYakpLaVdcXoFQXpwnnsVfyRv88fHYse+eR8vQ==} @@ -2030,6 +2037,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -2565,6 +2576,10 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + jotai@2.16.1: resolution: {integrity: sha512-vrHcAbo3P7Br37C8Bv6JshMtlKMPqqmx0DDREtTjT4nf3QChDrYdbH+4ik/9V0cXA57dK28RkJ5dctYvavcIlg==} engines: {node: '>=12.20.0'} @@ -4378,12 +4393,12 @@ snapshots: '@istanbuljs/schema@0.1.3': {} - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))': dependencies: glob: 10.4.5 magic-string: 0.30.19 react-docgen-typescript: 2.4.0(typescript@5.9.3) - vite: 7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) optionalDependencies: typescript: 5.9.3 @@ -4598,44 +4613,44 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@storybook/addon-docs@9.1.17(@types/react@19.2.7)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)))': + '@storybook/addon-docs@9.1.17(@types/react@19.2.7)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.3) - '@storybook/csf-plugin': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))) + '@storybook/csf-plugin': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))) '@storybook/icons': 1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@storybook/react-dom-shim': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))) + '@storybook/react-dom-shim': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-vitest@9.1.17(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)))(vitest@3.2.4)': + '@storybook/addon-vitest@9.1.17(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)))(vitest@3.2.4)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) prompts: 2.4.2 - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) ts-dedent: 2.2.0 optionalDependencies: - '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))(vitest@3.2.4) '@vitest/runner': 3.2.4 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@1.21.7)(jsdom@27.4.0)(tsx@4.21.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@27.4.0)(tsx@4.21.0) transitivePeerDependencies: - react - react-dom - '@storybook/builder-vite@9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)))(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))': + '@storybook/builder-vite@9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))': dependencies: - '@storybook/csf-plugin': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))) - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + '@storybook/csf-plugin': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))) + storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) ts-dedent: 2.2.0 - vite: 7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) - '@storybook/csf-plugin@9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)))': + '@storybook/csf-plugin@9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)))': dependencies: - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -4645,39 +4660,39 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@storybook/react-dom-shim@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)))': + '@storybook/react-dom-shim@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)))': dependencies: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) - '@storybook/react-vite@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.50.1)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)))(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))': + '@storybook/react-vite@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.50.1)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)))(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) '@rollup/pluginutils': 5.3.0(rollup@4.50.1) - '@storybook/builder-vite': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)))(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) - '@storybook/react': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)))(typescript@5.9.3) + '@storybook/builder-vite': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) + '@storybook/react': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)))(typescript@5.9.3) find-up: 7.0.0 magic-string: 0.30.19 react: 19.2.3 react-docgen: 8.0.1 react-dom: 19.2.3(react@19.2.3) resolve: 1.22.10 - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) tsconfig-paths: 4.2.0 - vite: 7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) transitivePeerDependencies: - rollup - supports-color - typescript - '@storybook/react@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)))(typescript@5.9.3)': + '@storybook/react@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)))(typescript@5.9.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))) + '@storybook/react-dom-shim': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + storybook: 9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) optionalDependencies: typescript: 5.9.3 @@ -4800,7 +4815,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.144.0(@tanstack/react-router@1.144.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))': + '@tanstack/router-plugin@1.144.0(@tanstack/react-router@1.144.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) @@ -4818,7 +4833,7 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.144.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - vite: 7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -4945,7 +4960,7 @@ snapshots: dependencies: '@types/node': 24.10.4 - '@vitejs/plugin-react@5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))': + '@vitejs/plugin-react@5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -4953,20 +4968,20 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.53 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) transitivePeerDependencies: - supports-color - '@vitest/browser@3.2.4(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))(vitest@3.2.4)': + '@vitest/browser@3.2.4(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) '@vitest/utils': 3.2.4 magic-string: 0.30.19 sirv: 3.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@1.21.7)(jsdom@27.4.0)(tsx@4.21.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@27.4.0)(tsx@4.21.0) ws: 8.18.3 optionalDependencies: playwright: 1.57.0 @@ -4991,9 +5006,9 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@1.21.7)(jsdom@27.4.0)(tsx@4.21.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@27.4.0)(tsx@4.21.0) optionalDependencies: - '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))(vitest@3.2.4) transitivePeerDependencies: - supports-color @@ -5005,13 +5020,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))': + '@vitest/mocker@3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -5039,6 +5054,11 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + '@yn1323/cognac@0.2.2': + dependencies: + commander: 12.1.0 + jiti: 2.6.1 + '@zag-js/accordion@1.29.1': dependencies: '@zag-js/anatomy': 1.29.1 @@ -5780,6 +5800,8 @@ snapshots: color-name@1.1.4: {} + commander@12.1.0: {} + config-chain@1.1.13: dependencies: ini: 1.3.8 @@ -6310,6 +6332,8 @@ snapshots: jiti@1.21.7: {} + jiti@2.6.1: {} + jotai@2.16.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.7)(react@19.2.3): optionalDependencies: '@babel/core': 7.28.5 @@ -7022,13 +7046,13 @@ snapshots: std-env@3.9.0: {} - storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)): + storybook@9.1.17(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)): dependencies: '@storybook/global': 5.0.0 '@testing-library/jest-dom': 6.8.0 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) '@vitest/spy': 3.2.4 better-opn: 3.0.2 esbuild: 0.25.9 @@ -7300,13 +7324,13 @@ snapshots: unist-util-stringify-position: 3.0.3 vfile-message: 3.1.4 - vite-node@3.2.4(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0): + vite-node@3.2.4(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) transitivePeerDependencies: - '@types/node' - jiti @@ -7321,18 +7345,18 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)): + vite-tsconfig-paths@6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)): dependencies: debug: 4.4.1 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) transitivePeerDependencies: - supports-color - typescript - vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0): + vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0): dependencies: esbuild: 0.27.1 fdir: 6.5.0(picomatch@4.0.3) @@ -7343,14 +7367,14 @@ snapshots: optionalDependencies: '@types/node': 24.10.4 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.6.1 tsx: 4.21.0 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@1.21.7)(jsdom@27.4.0)(tsx@4.21.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@27.4.0)(tsx@4.21.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0)) + '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -7368,13 +7392,13 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0) - vite-node: 3.2.4(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) + vite-node: 3.2.4(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.4 - '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(jiti@1.21.7)(tsx@4.21.0))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0))(vitest@3.2.4) jsdom: 27.4.0 transitivePeerDependencies: - jiti diff --git a/src/components/features/Shop/MemberAddModal/index.tsx b/src/components/features/Shop/MemberAddModal/index.tsx index 566439f3..b753d1bb 100644 --- a/src/components/features/Shop/MemberAddModal/index.tsx +++ b/src/components/features/Shop/MemberAddModal/index.tsx @@ -34,7 +34,7 @@ const roleOptions: { value: MemberRole; label: string; description: string; subD ]; export const MemberAddModal = ({ shopId, authId, isOpen, onOpenChange, onClose, onSuccess }: MemberAddModalProps) => { - const addStaff = useMutation(api.shop.mutations.addStaff); + const addStaff = useMutation(api.staff.mutations.addStaff); const createInvite = useMutation(api.invite.mutations.create); const { diff --git a/src/components/features/Shop/Position/PositionAddForm.tsx b/src/components/features/Shop/Position/PositionAddForm.tsx new file mode 100644 index 00000000..ceb9d4e4 --- /dev/null +++ b/src/components/features/Shop/Position/PositionAddForm.tsx @@ -0,0 +1,84 @@ +import { Box, Button, Field, Flex, Icon, Input, Text } from "@chakra-ui/react"; +import { LuPlus } from "react-icons/lu"; +import { POSITION_NAME_MAX_LENGTH } from "@/src/constants/validations"; + +type PositionAddFormProps = { + isAdding: boolean; + newPositionName: string; + addError: string | null; + isMaxReached: boolean; + disabled?: boolean; + isCreating?: boolean; + onAdd: () => void; + onCancel: () => void; + onChange: (value: string) => void; + onStartAdding: () => void; +}; + +export const PositionAddForm = ({ + isAdding, + newPositionName, + addError, + isMaxReached, + disabled, + isCreating, + onAdd, + onCancel, + onChange, + onStartAdding, +}: PositionAddFormProps) => { + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + onAdd(); + } else if (e.key === "Escape") { + onCancel(); + } + }; + + if (isAdding) { + return ( + + + + + onChange(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="新しいポジション名" + maxLength={POSITION_NAME_MAX_LENGTH} + size="sm" + autoFocus + /> + + + + + {addError && ( + + {addError} + + )} + + + ); + } + + return ( + + ); +}; diff --git a/src/components/features/Shop/Position/SortablePositionItem.tsx b/src/components/features/Shop/Position/SortablePositionItem.tsx new file mode 100644 index 00000000..59e8f9a2 --- /dev/null +++ b/src/components/features/Shop/Position/SortablePositionItem.tsx @@ -0,0 +1,148 @@ +import { Box, Field, Flex, Icon, IconButton, Input, Text } from "@chakra-ui/react"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import type { ReactNode } from "react"; +import { LuCheck, LuGripVertical, LuPencil, LuTrash2, LuX } from "react-icons/lu"; +import { POSITION_NAME_MAX_LENGTH } from "@/src/constants/validations"; + +export type SortablePositionItemProps = { + id: string; + name: string; + isEditing: boolean; + editingName: string; + onEditStart: () => void; + onEditCancel: () => void; + onEditChange: (value: string) => void; + onEditSave: () => void; + onDeleteClick: () => void; + editError: string | null; + disabled?: boolean; + isUpdating?: boolean; + /** 通常表示時の名前の前に表示(カラードット等) */ + namePrefix?: ReactNode; + /** 編集モード時に追加表示(カラーピッカー等) */ + editExtra?: ReactNode; +}; + +export const SortablePositionItem = ({ + id, + name, + isEditing, + editingName, + onEditStart, + onEditCancel, + onEditChange, + onEditSave, + onDeleteClick, + editError, + disabled, + isUpdating, + namePrefix, + editExtra, +}: SortablePositionItemProps) => { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id, + disabled, + }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + onEditSave(); + } else if (e.key === "Escape") { + onEditCancel(); + } + }; + + return ( + + + {/* ドラッグハンドル */} + + + + + {isEditing ? ( + + + + onEditChange(e.target.value)} + onKeyDown={handleKeyDown} + maxLength={POSITION_NAME_MAX_LENGTH} + size="sm" + autoFocus + aria-invalid={!!editError} + /> + + + + + + + + + {editExtra} + {editError && ( + + {editError} + + )} + + ) : ( + <> + {namePrefix} + + {name} + + + + + + + + + + + )} + + + ); +}; diff --git a/src/components/features/Shop/Position/validatePositionName.ts b/src/components/features/Shop/Position/validatePositionName.ts new file mode 100644 index 00000000..5dda50bc --- /dev/null +++ b/src/components/features/Shop/Position/validatePositionName.ts @@ -0,0 +1,9 @@ +import { POSITION_NAME_MAX_LENGTH } from "@/src/constants/validations"; + +export const validatePositionName = (name: string, existingNames: string[]): string | null => { + const trimmed = name.trim(); + if (!trimmed) return "ポジション名を入力してください"; + if (trimmed.length > POSITION_NAME_MAX_LENGTH) return `${POSITION_NAME_MAX_LENGTH}文字以内で入力してください`; + if (existingNames.some((n) => n === trimmed)) return "このポジション名は既に存在します"; + return null; +}; diff --git a/src/components/features/Shop/PositionEditor/index.tsx b/src/components/features/Shop/PositionEditor/index.tsx index 4b8321ab..cf93d149 100644 --- a/src/components/features/Shop/PositionEditor/index.tsx +++ b/src/components/features/Shop/PositionEditor/index.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Field, Flex, Icon, IconButton, Input, Text, VStack } from "@chakra-ui/react"; +import { Flex, Icon, Text, VStack } from "@chakra-ui/react"; import { closestCenter, DndContext, @@ -12,13 +12,14 @@ import { arrayMove, SortableContext, sortableKeyboardCoordinates, - useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; import { useState } from "react"; -import { LuCheck, LuGripVertical, LuInfo, LuPencil, LuPlus, LuTrash2, LuX } from "react-icons/lu"; -import { POSITION_MAX_COUNT, POSITION_NAME_MAX_LENGTH } from "@/src/constants/validations"; +import { LuInfo } from "react-icons/lu"; +import { POSITION_MAX_COUNT } from "@/src/constants/validations"; +import { PositionAddForm } from "../Position/PositionAddForm"; +import { SortablePositionItem } from "../Position/SortablePositionItem"; +import { validatePositionName } from "../Position/validatePositionName"; export type LocalPosition = { id: string; @@ -32,140 +33,6 @@ type PositionEditorProps = { disabled?: boolean; }; -// 個別ポジションアイテム(ドラッグ可能) -type PositionItemProps = { - position: LocalPosition; - isEditing: boolean; - editingName: string; - onEditStart: () => void; - onEditCancel: () => void; - onEditChange: (value: string) => void; - onEditSave: () => void; - onDeleteClick: () => void; - editError: string | null; - disabled?: boolean; -}; - -const PositionItem = ({ - position, - isEditing, - editingName, - onEditStart, - onEditCancel, - onEditChange, - onEditSave, - onDeleteClick, - editError, - disabled, -}: PositionItemProps) => { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id: position.id, - disabled, - }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - onEditSave(); - } else if (e.key === "Escape") { - onEditCancel(); - } - }; - - return ( - - - {/* ドラッグハンドル */} - - - - - {isEditing ? ( - // 編集モード - - - - onEditChange(e.target.value)} - onKeyDown={handleKeyDown} - maxLength={POSITION_NAME_MAX_LENGTH} - size="sm" - autoFocus - aria-invalid={!!editError} - /> - - - - - - - - - {editError && ( - - {editError} - - )} - - ) : ( - // 通常表示 - <> - - {position.name} - - - - - - - - - - - )} - - - ); -}; - -// メインコンポーネント export const PositionEditor = ({ positions, onChange, disabled }: PositionEditorProps) => { const [isAdding, setIsAdding] = useState(false); const [newPositionName, setNewPositionName] = useState(""); @@ -174,7 +41,6 @@ export const PositionEditor = ({ positions, onChange, disabled }: PositionEditor const [editingName, setEditingName] = useState(""); const [editError, setEditError] = useState(null); - // DnD sensors const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { @@ -184,22 +50,14 @@ export const PositionEditor = ({ positions, onChange, disabled }: PositionEditor const isMaxReached = positions.length >= POSITION_MAX_COUNT; - // 追加処理 const handleAdd = () => { const trimmedName = newPositionName.trim(); - if (!trimmedName) { - setAddError("ポジション名を入力してください"); - return; - } - - if (trimmedName.length > POSITION_NAME_MAX_LENGTH) { - setAddError(`${POSITION_NAME_MAX_LENGTH}文字以内で入力してください`); - return; - } - - // 重複チェック(ローカル) - if (positions.some((p) => p.name === trimmedName)) { - setAddError("このポジション名は既に存在します"); + const error = validatePositionName( + trimmedName, + positions.map((p) => p.name), + ); + if (error) { + setAddError(error); return; } @@ -215,31 +73,20 @@ export const PositionEditor = ({ positions, onChange, disabled }: PositionEditor setAddError(null); }; - // 編集開始 const handleEditStart = (position: LocalPosition) => { setEditingId(position.id); setEditingName(position.name); setEditError(null); }; - // 編集保存 const handleEditSave = () => { if (!editingId) return; const trimmedName = editingName.trim(); - if (!trimmedName) { - setEditError("ポジション名を入力してください"); - return; - } - - if (trimmedName.length > POSITION_NAME_MAX_LENGTH) { - setEditError(`${POSITION_NAME_MAX_LENGTH}文字以内で入力してください`); - return; - } - - // 重複チェック(ローカル、自分以外) - if (positions.some((p) => p.id !== editingId && p.name === trimmedName)) { - setEditError("このポジション名は既に存在します"); + const otherNames = positions.filter((p) => p.id !== editingId).map((p) => p.name); + const error = validatePositionName(trimmedName, otherNames); + if (error) { + setEditError(error); return; } @@ -249,19 +96,16 @@ export const PositionEditor = ({ positions, onChange, disabled }: PositionEditor setEditError(null); }; - // 編集キャンセル const handleEditCancel = () => { setEditingId(null); setEditingName(""); setEditError(null); }; - // 削除処理(新規作成時は確認なしで即削除) const handleDelete = (positionId: string) => { onChange(positions.filter((p) => p.id !== positionId)); }; - // ドラッグ終了 const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; @@ -277,27 +121,16 @@ export const PositionEditor = ({ positions, onChange, disabled }: PositionEditor } }; - // 追加フォームのキーダウン - const handleAddKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - handleAdd(); - } else if (e.key === "Escape") { - setIsAdding(false); - setNewPositionName(""); - setAddError(null); - } - }; - return ( - {/* ポジション一覧 */} p.id)} strategy={verticalListSortingStrategy}> {positions.map((position) => ( - handleEditStart(position)} @@ -313,62 +146,25 @@ export const PositionEditor = ({ positions, onChange, disabled }: PositionEditor - {/* 追加フォーム */} - {isAdding ? ( - - - - - { - setNewPositionName(e.target.value); - setAddError(null); - }} - onKeyDown={handleAddKeyDown} - placeholder="新しいポジション名" - maxLength={POSITION_NAME_MAX_LENGTH} - size="sm" - autoFocus - /> - - - - - {addError && ( - - {addError} - - )} - - - ) : ( - - )} + { + setIsAdding(false); + setNewPositionName(""); + setAddError(null); + }} + onChange={(value) => { + setNewPositionName(value); + setAddError(null); + }} + onStartAdding={() => setIsAdding(true)} + /> - {/* 最大件数到達メッセージ */} {isMaxReached && ( @@ -378,7 +174,6 @@ export const PositionEditor = ({ positions, onChange, disabled }: PositionEditor )} - {/* ヒント */} {positions.length > 1 && !isMaxReached && ( ドラッグで並び順を変更できます diff --git a/src/components/features/Shop/PositionManager/index.tsx b/src/components/features/Shop/PositionManager/index.tsx index 878966d0..abd5acc2 100644 --- a/src/components/features/Shop/PositionManager/index.tsx +++ b/src/components/features/Shop/PositionManager/index.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Field, Flex, Icon, IconButton, Input, Text, VStack } from "@chakra-ui/react"; +import { Box, Flex, Icon, Text, VStack } from "@chakra-ui/react"; import { closestCenter, DndContext, @@ -12,14 +12,12 @@ import { arrayMove, SortableContext, sortableKeyboardCoordinates, - useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; import { useMutation } from "convex/react"; import { useAtomValue } from "jotai"; import { useState } from "react"; -import { LuCheck, LuGripVertical, LuInfo, LuPencil, LuPlus, LuTag, LuTrash2, LuX } from "react-icons/lu"; +import { LuInfo, LuTag } from "react-icons/lu"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import { POSITION_COLORS } from "@/convex/constants"; @@ -27,8 +25,11 @@ import { ColorPicker } from "@/src/components/ui/ColorPicker"; import { Dialog, useDialog } from "@/src/components/ui/Dialog"; import { FormCard } from "@/src/components/ui/FormCard"; import { toaster } from "@/src/components/ui/toaster"; -import { POSITION_MAX_COUNT, POSITION_NAME_MAX_LENGTH } from "@/src/constants/validations"; +import { POSITION_MAX_COUNT } from "@/src/constants/validations"; import { userAtom } from "@/src/stores/user"; +import { PositionAddForm } from "../Position/PositionAddForm"; +import { SortablePositionItem } from "../Position/SortablePositionItem"; +import { validatePositionName } from "../Position/validatePositionName"; type PositionType = { _id: Id<"shopPositions">; @@ -42,180 +43,6 @@ type PositionManagerProps = { positions: PositionType[]; }; -// 個別ポジションアイテム(ドラッグ可能) -type PositionItemProps = { - position: PositionType; - isEditing: boolean; - editingName: string; - editingColor: string; - onEditStart: () => void; - onEditCancel: () => void; - onEditChange: (value: string) => void; - onColorChange: (color: string) => void; - onEditSave: () => void; - onDeleteClick: () => void; - isUpdating: boolean; - editError: string | null; -}; - -const PositionItem = ({ - position, - isEditing, - editingName, - editingColor, - onEditStart, - onEditCancel, - onEditChange, - onColorChange, - onEditSave, - onDeleteClick, - isUpdating, - editError, -}: PositionItemProps) => { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id: position._id, - }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - onEditSave(); - } else if (e.key === "Escape") { - onEditCancel(); - } - }; - - return ( - - - {/* ドラッグハンドル */} - - - - - {isEditing ? ( - // 編集モード - - - - onEditChange(e.target.value)} - onKeyDown={handleKeyDown} - maxLength={POSITION_NAME_MAX_LENGTH} - size="sm" - autoFocus - aria-invalid={!!editError} - /> - - - - - - - - - - {editError && ( - - {editError} - - )} - - ) : ( - // 通常表示 - <> - - - {position.name} - - - - - - - - - - - )} - - - ); -}; - -// 削除確認ダイアログ -type DeleteDialogProps = { - positionName: string; - positionId: Id<"shopPositions"> | null; - isOpen: boolean; - onOpenChange: (details: { open: boolean }) => void; - onClose: () => void; - onConfirm: () => void; - isDeleting: boolean; -}; - -const DeleteDialog = ({ positionName, isOpen, onOpenChange, onClose, onConfirm, isDeleting }: DeleteDialogProps) => { - return ( - - - 「{positionName}」を削除しますか? - - - ); -}; - -// メインコンポーネント export const PositionManager = ({ shopId, positions: initialPositions }: PositionManagerProps) => { const user = useAtomValue(userAtom); const [positions, setPositions] = useState(initialPositions); @@ -225,14 +52,12 @@ export const PositionManager = ({ shopId, positions: initialPositions }: Positio const [editingId, setEditingId] = useState | null>(null); const [editingName, setEditingName] = useState(""); const [editError, setEditError] = useState(null); + const [editingColor, setEditingColor] = useState(""); const [deleteTargetId, setDeleteTargetId] = useState | null>(null); const [deleteTargetName, setDeleteTargetName] = useState(""); const deleteDialog = useDialog(); - const [editingColor, setEditingColor] = useState(""); - - // Mutations const createPosition = useMutation(api.position.mutations.create); const updatePositionName = useMutation(api.position.mutations.updateName); const updatePositionColor = useMutation(api.position.mutations.updateColor); @@ -243,7 +68,6 @@ export const PositionManager = ({ shopId, positions: initialPositions }: Positio const [isUpdating, setIsUpdating] = useState(false); const [isDeleting, setIsDeleting] = useState(false); - // DnD sensors const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { @@ -253,24 +77,16 @@ export const PositionManager = ({ shopId, positions: initialPositions }: Positio const isMaxReached = positions.length >= POSITION_MAX_COUNT; - // 追加処理 const handleAdd = async () => { if (!user.authId) return; const trimmedName = newPositionName.trim(); - if (!trimmedName) { - setAddError("ポジション名を入力してください"); - return; - } - - if (trimmedName.length > POSITION_NAME_MAX_LENGTH) { - setAddError(`${POSITION_NAME_MAX_LENGTH}文字以内で入力してください`); - return; - } - - // 重複チェック(ローカル) - if (positions.some((p) => p.name === trimmedName)) { - setAddError("このポジション名は既に存在します"); + const error = validatePositionName( + trimmedName, + positions.map((p) => p.name), + ); + if (error) { + setAddError(error); return; } @@ -305,7 +121,6 @@ export const PositionManager = ({ shopId, positions: initialPositions }: Positio } }; - // 編集開始 const handleEditStart = (position: PositionType) => { setEditingId(position._id); setEditingName(position.name); @@ -313,24 +128,14 @@ export const PositionManager = ({ shopId, positions: initialPositions }: Positio setEditError(null); }; - // 編集保存 const handleEditSave = async () => { if (!user.authId || !editingId) return; const trimmedName = editingName.trim(); - if (!trimmedName) { - setEditError("ポジション名を入力してください"); - return; - } - - if (trimmedName.length > POSITION_NAME_MAX_LENGTH) { - setEditError(`${POSITION_NAME_MAX_LENGTH}文字以内で入力してください`); - return; - } - - // 重複チェック(ローカル、自分以外) - if (positions.some((p) => p._id !== editingId && p.name === trimmedName)) { - setEditError("このポジション名は既に存在します"); + const otherNames = positions.filter((p) => p._id !== editingId).map((p) => p.name); + const error = validatePositionName(trimmedName, otherNames); + if (error) { + setEditError(error); return; } @@ -343,7 +148,6 @@ export const PositionManager = ({ shopId, positions: initialPositions }: Positio authId: user.authId, }); - // カラーが変更されていたら更新 if ( editingColor !== (currentPosition?.color ?? POSITION_COLORS[currentPosition?.order ?? 0 % POSITION_COLORS.length]) @@ -369,21 +173,18 @@ export const PositionManager = ({ shopId, positions: initialPositions }: Positio } }; - // 編集キャンセル const handleEditCancel = () => { setEditingId(null); setEditingName(""); setEditError(null); }; - // 削除クリック const handleDeleteClick = (position: PositionType) => { setDeleteTargetId(position._id); setDeleteTargetName(position.name); deleteDialog.open(); }; - // 削除確定 const handleDeleteConfirm = async () => { if (!user.authId || !deleteTargetId) return; @@ -409,7 +210,6 @@ export const PositionManager = ({ shopId, positions: initialPositions }: Positio } }; - // ドラッグ終了 const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event; @@ -427,7 +227,6 @@ export const PositionManager = ({ shopId, positions: initialPositions }: Positio authId: user.authId, }); } catch { - // エラー時は元に戻す setPositions(positions); toaster.error({ title: "並び順の更新に失敗しました" }); } @@ -435,17 +234,6 @@ export const PositionManager = ({ shopId, positions: initialPositions }: Positio } }; - // 追加フォームのキーダウン - const handleAddKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - handleAdd(); - } else if (e.key === "Escape") { - setIsAdding(false); - setNewPositionName(""); - setAddError(null); - } - }; - return ( <> - {/* ポジション一覧 */} p._id)} strategy={verticalListSortingStrategy}> {positions.map((position) => ( - handleEditStart(position)} onEditCancel={handleEditCancel} onEditChange={setEditingName} - onColorChange={setEditingColor} onEditSave={handleEditSave} onDeleteClick={() => handleDeleteClick(position)} isUpdating={isUpdating} editError={editingId === position._id ? editError : null} + namePrefix={ + + } + editExtra={} /> ))} - {/* 追加フォーム */} - {isAdding ? ( - - - - - { - setNewPositionName(e.target.value); - setAddError(null); - }} - onKeyDown={handleAddKeyDown} - placeholder="新しいポジション名" - maxLength={POSITION_NAME_MAX_LENGTH} - size="sm" - autoFocus - /> - - - - - {addError && ( - - {addError} - - )} - - - ) : ( - - )} + { + setIsAdding(false); + setNewPositionName(""); + setAddError(null); + }} + onChange={(value) => { + setNewPositionName(value); + setAddError(null); + }} + onStartAdding={() => setIsAdding(true)} + /> - {/* 最大件数到達メッセージ */} {isMaxReached && ( @@ -548,7 +307,6 @@ export const PositionManager = ({ shopId, positions: initialPositions }: Positio )} - {/* ヒント */} {positions.length > 1 && !isMaxReached && ( ドラッグで並び順を変更できます @@ -557,16 +315,21 @@ export const PositionManager = ({ shopId, positions: initialPositions }: Positio - {/* 削除確認ダイアログ */} - + onSubmit={handleDeleteConfirm} + submitLabel="削除する" + submitColorPalette="red" + isLoading={isDeleting} + role="alertdialog" + > + + 「{deleteTargetName}」を削除しますか? + + ); }; diff --git a/src/components/features/Staff/StaffEdit/index.tsx b/src/components/features/Staff/StaffEdit/index.tsx index af461c5f..3b24b0e9 100644 --- a/src/components/features/Staff/StaffEdit/index.tsx +++ b/src/components/features/Staff/StaffEdit/index.tsx @@ -49,8 +49,8 @@ type StaffEditProps = { export const StaffEdit = ({ staff, shop, positions, staffSkills }: StaffEditProps) => { const navigate = useNavigate(); const user = useAtomValue(userAtom); - const updateStaffInfo = useMutation(api.shop.mutations.updateStaffInfo); - const resignStaff = useMutation(api.shop.mutations.resignStaff); + const updateStaffInfo = useMutation(api.staff.mutations.updateStaffInfo); + const resignStaff = useMutation(api.staff.mutations.resignStaff); const [resignationReason, setResignationReason] = useState(""); const [isResigning, setIsResigning] = useState(false); diff --git a/src/components/features/Staff/StaffEditModal/index.tsx b/src/components/features/Staff/StaffEditModal/index.tsx index 6e031659..175ca21c 100644 --- a/src/components/features/Staff/StaffEditModal/index.tsx +++ b/src/components/features/Staff/StaffEditModal/index.tsx @@ -41,7 +41,7 @@ export const StaffEditModal = ({ staffId, shopId, isOpen, onOpenChange, onClose, isOpen ? { staffId: staffId as Id<"staffs"> } : "skip", ); - const updateStaffInfo = useMutation(api.shop.mutations.updateStaffInfo); + const updateStaffInfo = useMutation(api.staff.mutations.updateStaffInfo); const isLoading = staff === undefined || positions === undefined || staffSkills === undefined; const hasError = staff === null;