feat(schema): add is_super_user to staff table

Add boolean is_super_user column (default false) to staff table.
Update Staff interface in shared types.
Mark first manager as super user in both seed modes.
Update test fixtures to include isSuperUser field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
groombook-ci[bot]
2026-03-28 20:20:14 +00:00
parent f1b85bf294
commit 314c59b565
8 changed files with 101 additions and 8 deletions
@@ -17,6 +17,7 @@ const MANAGER: StaffRow = {
oidcSub: "oidc-manager-sub", oidcSub: "oidc-manager-sub",
userId: null, userId: null,
role: "manager", role: "manager",
isSuperUser: true,
name: "Manager McManager", name: "Manager McManager",
email: "manager@example.com", email: "manager@example.com",
active: true, active: true,
+1
View File
@@ -9,6 +9,7 @@ const MANAGER: StaffRow = {
oidcSub: "oidc-manager-sub", oidcSub: "oidc-manager-sub",
userId: null, userId: null,
role: "manager", role: "manager",
isSuperUser: true,
name: "Manager McManager", name: "Manager McManager",
email: "manager@example.com", email: "manager@example.com",
active: true, active: true,
+1
View File
@@ -10,6 +10,7 @@ const MANAGER: StaffRow = {
oidcSub: "oidc-manager-sub", oidcSub: "oidc-manager-sub",
userId: "ba-user-manager", userId: "ba-user-manager",
role: "manager", role: "manager",
isSuperUser: true,
name: "Manager McManager", name: "Manager McManager",
email: "manager@example.com", email: "manager@example.com",
active: true, active: true,
@@ -0,0 +1,84 @@
CREATE TYPE "public"."waitlist_status" AS ENUM('active', 'notified', 'expired', 'cancelled');--> statement-breakpoint
CREATE TABLE "account" (
"id" text PRIMARY KEY NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
"user_id" text NOT NULL,
"access_token" text,
"refresh_token" text,
"id_token" text,
"access_token_expires_at" timestamp,
"refresh_token_expires_at" timestamp,
"scope" text,
"password" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "session" (
"id" text PRIMARY KEY NOT NULL,
"expires_at" timestamp NOT NULL,
"token" text NOT NULL,
"ip_address" text,
"user_agent" text,
"user_id" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "session_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"email_verified" boolean DEFAULT false NOT NULL,
"image" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "user_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "verification" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "waitlist_entries" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"client_id" uuid NOT NULL,
"pet_id" uuid NOT NULL,
"service_id" uuid NOT NULL,
"preferred_date" text NOT NULL,
"preferred_time" text NOT NULL,
"status" "waitlist_status" DEFAULT 'active' NOT NULL,
"notified_at" timestamp,
"expires_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "appointments" ADD COLUMN "confirmation_status" text DEFAULT 'pending' NOT NULL;--> statement-breakpoint
ALTER TABLE "appointments" ADD COLUMN "confirmed_at" timestamp;--> statement-breakpoint
ALTER TABLE "appointments" ADD COLUMN "cancelled_at" timestamp;--> statement-breakpoint
ALTER TABLE "appointments" ADD COLUMN "confirmation_token" text;--> statement-breakpoint
ALTER TABLE "appointments" ADD COLUMN "customer_notes" text;--> statement-breakpoint
ALTER TABLE "pets" ADD COLUMN "photo_key" text;--> statement-breakpoint
ALTER TABLE "pets" ADD COLUMN "photo_uploaded_at" timestamp;--> statement-breakpoint
ALTER TABLE "staff" ADD COLUMN "user_id" text;--> statement-breakpoint
ALTER TABLE "staff" ADD COLUMN "is_super_user" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "staff" ADD COLUMN "ical_token" text;--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "waitlist_entries" ADD CONSTRAINT "waitlist_entries_client_id_clients_id_fk" FOREIGN KEY ("client_id") REFERENCES "public"."clients"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "waitlist_entries" ADD CONSTRAINT "waitlist_entries_pet_id_pets_id_fk" FOREIGN KEY ("pet_id") REFERENCES "public"."pets"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "waitlist_entries" ADD CONSTRAINT "waitlist_entries_service_id_services_id_fk" FOREIGN KEY ("service_id") REFERENCES "public"."services"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "idx_waitlist_client_id" ON "waitlist_entries" USING btree ("client_id");--> statement-breakpoint
CREATE INDEX "idx_waitlist_preferred_date" ON "waitlist_entries" USING btree ("preferred_date");--> statement-breakpoint
CREATE INDEX "idx_waitlist_status" ON "waitlist_entries" USING btree ("status");--> statement-breakpoint
ALTER TABLE "staff" ADD CONSTRAINT "staff_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "appointments" ADD CONSTRAINT "appointments_confirmation_token_unique" UNIQUE("confirmation_token");--> statement-breakpoint
ALTER TABLE "staff" ADD CONSTRAINT "staff_ical_token_unique" UNIQUE("ical_token");
+1
View File
@@ -52,6 +52,7 @@ export function buildStaff(overrides: Partial<StaffRow> = {}): StaffRow {
oidcSub: `oidc-${id}`, oidcSub: `oidc-${id}`,
userId: null, userId: null,
role: "groomer", role: "groomer",
isSuperUser: false,
active: true, active: true,
icalToken: null, icalToken: null,
createdAt: new Date("2025-01-01T00:00:00Z"), createdAt: new Date("2025-01-01T00:00:00Z"),
+2
View File
@@ -159,6 +159,8 @@ export const staff = pgTable("staff", {
// Better-Auth user ID — links staff business record to auth identity // Better-Auth user ID — links staff business record to auth identity
userId: text("user_id").references(() => user.id, { onDelete: "set null" }), userId: text("user_id").references(() => user.id, { onDelete: "set null" }),
role: staffRoleEnum("role").notNull().default("groomer"), role: staffRoleEnum("role").notNull().default("groomer"),
// Super users bypass appointment-booking restrictions and access admin panels
isSuperUser: boolean("is_super_user").notNull().default(false),
active: boolean("active").notNull().default(true), active: boolean("active").notNull().default(true),
// Token for iCal calendar feed subscription (no auth required) // Token for iCal calendar feed subscription (no auth required)
icalToken: text("ical_token").unique(), icalToken: text("ical_token").unique(),
+10 -8
View File
@@ -287,6 +287,7 @@ async function seedKnownUsers() {
email: "demo-manager@groombook.dev", email: "demo-manager@groombook.dev",
oidcSub: "demo-manager-001", oidcSub: "demo-manager-001",
role: "manager", role: "manager",
isSuperUser: true,
active: true, active: true,
}); });
console.log("✓ Created staff 'Demo Manager' (oidcSub: demo-manager-001)"); console.log("✓ Created staff 'Demo Manager' (oidcSub: demo-manager-001)");
@@ -384,24 +385,24 @@ async function seed() {
// ── Staff ── // ── Staff ──
// Deterministic staff IDs so they can be referenced in scripts/tests // Deterministic staff IDs so they can be referenced in scripts/tests
const managerStaff = [ const managerStaff = [
{ id: uuid(), name: "Jordan Lee", email: "jordan@groombook.dev", role: "manager" as const }, { id: uuid(), name: "Jordan Lee", email: "jordan@groombook.dev", role: "manager" as const, isSuperUser: true },
]; ];
const receptionistStaff = [ const receptionistStaff = [
{ id: uuid(), name: "Sam Rivera", email: "sam@groombook.dev", role: "receptionist" as const }, { id: uuid(), name: "Sam Rivera", email: "sam@groombook.dev", role: "receptionist" as const, isSuperUser: false },
]; ];
const groomers = [ const groomers = [
{ id: uuid(), name: "Sarah Mitchell", email: "sarah@groombook.dev", role: "groomer" as const }, { id: uuid(), name: "Sarah Mitchell", email: "sarah@groombook.dev", role: "groomer" as const, isSuperUser: false },
{ id: uuid(), name: "James Park", email: "james@groombook.dev", role: "groomer" as const }, { id: uuid(), name: "James Park", email: "james@groombook.dev", role: "groomer" as const, isSuperUser: false },
{ id: uuid(), name: "Maria Gonzalez", email: "maria@groombook.dev", role: "groomer" as const }, { id: uuid(), name: "Maria Gonzalez", email: "maria@groombook.dev", role: "groomer" as const, isSuperUser: false },
]; ];
// Bathers are groomers by role but serve as the secondary staff (bather) on appointments // Bathers are groomers by role but serve as the secondary staff (bather) on appointments
const bathers = [ const bathers = [
{ id: uuid(), name: "Tyler Johnson", email: "tyler@groombook.dev", role: "groomer" as const }, { id: uuid(), name: "Tyler Johnson", email: "tyler@groombook.dev", role: "groomer" as const, isSuperUser: false },
{ id: uuid(), name: "Ashley Chen", email: "ashley@groombook.dev", role: "groomer" as const }, { id: uuid(), name: "Ashley Chen", email: "ashley@groombook.dev", role: "groomer" as const, isSuperUser: false },
{ id: uuid(), name: "Devon Williams", email: "devon@groombook.dev", role: "groomer" as const }, { id: uuid(), name: "Devon Williams", email: "devon@groombook.dev", role: "groomer" as const, isSuperUser: false },
]; ];
const allStaff = [...managerStaff, ...receptionistStaff, ...groomers, ...bathers]; const allStaff = [...managerStaff, ...receptionistStaff, ...groomers, ...bathers];
@@ -411,6 +412,7 @@ async function seed() {
name: s.name, name: s.name,
email: s.email, email: s.email,
role: s.role, role: s.role,
isSuperUser: s.isSuperUser,
active: true, active: true,
}); });
} }
+1
View File
@@ -72,6 +72,7 @@ export interface Staff {
name: string; name: string;
email: string; email: string;
role: "groomer" | "receptionist" | "manager"; role: "groomer" | "receptionist" | "manager";
isSuperUser: boolean;
active: boolean; active: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;