Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ convex-seeds/backup
.mcp.json
CLAUDE.local.md

character.md
character.md
.cognac/
22 changes: 22 additions & 0 deletions cognac.config.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
3 changes: 2 additions & 1 deletion convex/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -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(),
Expand Down
39 changes: 8 additions & 31 deletions convex/position/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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 };
},
});
Expand All @@ -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 };
},
});
191 changes: 1 addition & 190 deletions convex/shop/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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() },
Expand Down
Loading
Loading