plans
2026-05-06
2026 05 06 Simplified Show System

Simplified Show + Occurrences System Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Simplify the show system by removing batch generation, denormalizing prices, and adding occurrence codes (e.g., TMTL260521N).

Architecture: Two-level model: shows (landing page content) → occurrences (buyable product instances with unique codes). Each admin manually creates or duplicates occurrences. No batch generation.

Tech Stack: Convex + Zod validation, Next.js frontend


Background

User Requirements:

  • Shows have a short code (e.g., TMTL for "The Mentalist")
  • Each occurrence gets a unique code: {showCode}{YYMMDD}{L|N} (e.g., TMTL260521N)
  • No batch generation — admin manually creates or duplicates each occurrence
  • Price denormalization — no override fields

File Structure

packages/backend/convex/
├── schema.ts                              # Rename tables, update fields
├── lib/
│   └── zod.ts                           # Already exists
├── domains/
│   └── shows/
│       ├── shows.ts                     # Show template CRUD (rename from shows.ts)
│       └── occurrences.ts               # Occurrence CRUD (simplified)
└── http/
    └── onepay.ts                       # Unchanged

apps/frontend/
├── components/home/show-schedule-preview.tsx   # Uses api.occurrences.listForSchedulePreview
├── app/booking/
│   └── page.tsx                        # Uses show picker (update to new schema)
└── app/admin/
    └── (existing pages)                # Update to new schema

Task 1: Update Schema — Rename Tables and Simplify Fields

Files:

  • Modify: packages/backend/convex/schema.ts

  • Step 1: Read current schema

Read packages/backend/convex/schema.ts to understand current structure.

  • Step 2: Rename showTemplates to shows
shows: defineTable({
  code: v.string(),           // e.g., "TMTL"
  title: v.string(),
  tagline: v.string(),
  description: v.string(),
  embeddedVideo: v.optional(v.string()),
  gallery: v.array(v.string()),
  supportedTicketTypes: v.array(v.union(
    v.literal("DINNER_THEATRE"),
    v.literal("SHOW_ONLY")
  )),
  defaultDinnerPrice: v.number(),
  defaultShowOnlyPrice: v.number(),
  defaultCapacity: v.number(),
  status: v.union(v.literal("DRAFT"), v.literal("ACTIVE"), v.literal("ARCHIVED")),
  slug: v.string(),
})
  .index("by_status", ["status"])
  .index("by_slug", ["slug"])
  .index("by_code", ["code"]),
  • Step 3: Rename showOccurrences to occurrences and simplify
occurrences: defineTable({
  showId: v.id("shows"),         // Reference to shows (was templateId)
  code: v.string(),               // e.g., "TMTL260521N" (unique product ID)
  date: v.string(),              // "YYYY-MM-DD"
  time: v.string(),              // "19:30"
  dinnerPrice: v.number(),        // Denormalized — no override
  showOnlyPrice: v.number(),     // Denormalized — no override
  showOnlyEnabled: v.boolean(),
  actualCapacity: v.number(),
  bookedCount: v.number(),
  status: v.union(
    v.literal("SCHEDULED"),
    v.literal("CANCELLED"),
    v.literal("SOLD_OUT")
  ),
  assignedTables: v.array(v.any()),
})
  .index("by_show", ["showId"])
  .index("by_date", ["date"])
  .index("by_show_date", ["showId", "date"])
  .index("by_date_status", ["date", "status"])
  .index("by_code", ["code"]),
  • Step 4: Update reservations table reference
reservations: defineTable({
  occurrenceId: v.id("occurrences"), // Was showOccurrences
  // ... rest unchanged
});
  • Step 5: Commit
git add packages/backend/convex/schema.ts
git commit -m "refactor: rename showTemplates→shows, showOccurrences→occurrences, denormalize prices"

Task 2: Create Occurrence Code Generator

Files:

  • Create: packages/backend/convex/domains/shows/code-generator.ts

  • Step 1: Write occurrence code generator

// packages/backend/convex/domains/shows/code-generator.ts
 
export type ShowTime = "L" | "N"; // Lunch or Night
 
/**
 * Generate unique occurrence code: {showCode}{YYMMDD}{L|N}
 * e.g., TMTL260521N
 */
export function generateOccurrenceCode(
  showCode: string,
  date: string, // "YYYY-MM-DD"
  time: ShowTime,
): string {
  const datePart = date.replace(/-/g, ""); // "2026-05-21" → "260521"
  return `${showCode}${datePart}${time}`;
}
 
/**
 * Parse occurrence code back to components
 */
export function parseOccurrenceCode(code: string): {
  showCode: string;
  date: string;
  time: ShowTime;
} | null {
  const match = code.match(/^([A-Z]+)(\d{6})([LN])$/);
  if (!match) return null;
  const [, showCode, datePart, time] = match;
  // Reconstruct date: "260521" → "2026-05-21"
  const year = "20" + datePart.slice(0, 2);
  const month = datePart.slice(2, 4);
  const day = datePart.slice(4, 6);
  return {
    showCode,
    date: `${year}-${month}-${day}`,
    time: time as ShowTime,
  };
}
  • Step 2: Commit
git add packages/backend/convex/domains/shows/code-generator.ts
git commit -m "feat: add occurrence code generator"

Task 3: Update Shows Functions (Rename + Simplify)

Files:

  • Modify: packages/backend/convex/domains/shows/shows.ts

  • Step 1: Read current shows.ts

Read packages/backend/convex/domains/shows/shows.ts.

  • Step 2: Update to use new table name shows and new fields
import { zQuery, zMutation, z } from "../../../lib/zod";
import { zid } from "convex-helpers/server/zod3";
 
export const listAll = zQuery({
  args: {},
  returns: z.array(
    z.object({
      _id: zid("shows"),
      code: z.string(),
      title: z.string(),
      slug: z.string(),
      status: z.enum(["DRAFT", "ACTIVE", "ARCHIVED"]),
    }),
  ),
  handler: async (ctx) => {
    return await ctx.db.query("shows").collect();
  },
});
 
export const listActive = zQuery({
  args: {},
  returns: z.array(
    z.object({
      _id: zid("shows"),
      code: z.string(),
      title: z.string(),
      tagline: z.string(),
      slug: z.string(),
      gallery: z.array(z.string()),
    }),
  ),
  handler: async (ctx) => {
    return await ctx.db
      .query("shows")
      .withIndex("by_status", (q) => q.eq("status", "ACTIVE"))
      .collect();
  },
});
 
export const getBySlug = zQuery({
  args: { slug: z.string() },
  returns: z
    .object({
      _id: zid("shows"),
      code: z.string(),
      title: z.string(),
      tagline: z.string(),
      description: z.string(),
      embeddedVideo: z.string().optional(),
      gallery: z.array(z.string()),
      supportedTicketTypes: z.array(z.enum(["DINNER_THEATRE", "SHOW_ONLY"])),
      defaultDinnerPrice: z.number(),
      defaultShowOnlyPrice: z.number(),
      defaultCapacity: z.number(),
    })
    .nullable(),
  handler: async (ctx, { slug }) => {
    return await ctx.db
      .query("shows")
      .withIndex("by_slug", (q) => q.eq("slug", slug))
      .first();
  },
});
 
export const createShow = zMutation({
  args: z.object({
    code: z.string().min(2).max(10),
    title: z.string().min(1).max(200),
    slug: z.string().regex(/^[a-z0-9-]+$/),
    tagline: z.string(),
    description: z.string(),
    embeddedVideo: z.string().optional(),
    gallery: z.array(z.string()),
    supportedTicketTypes: z.array(z.enum(["DINNER_THEATRE", "SHOW_ONLY"])),
    defaultDinnerPrice: z.number().nonnegative(),
    defaultShowOnlyPrice: z.number().nonnegative(),
    defaultCapacity: z.number().int().positive(),
    status: z.enum(["DRAFT", "ACTIVE", "ARCHIVED"]).default("DRAFT"),
  }),
  handler: async (ctx, args) => {
    const id = await ctx.db.insert("shows", {
      ...args,
      createdAt: Date.now(),
      updatedAt: Date.now(),
    });
    return id;
  },
});
 
export const updateShow = zMutation({
  args: z.object({
    id: zid("shows"),
    code: z.string().min(2).max(10).optional(),
    title: z.string().min(1).max(200).optional(),
    slug: z
      .string()
      .regex(/^[a-z0-9-]+$/)
      .optional(),
    tagline: z.string().optional(),
    description: z.string().optional(),
    embeddedVideo: z.string().optional(),
    gallery: z.array(z.string()).optional(),
    supportedTicketTypes: z
      .array(z.enum(["DINNER_THEATRE", "SHOW_ONLY"]))
      .optional(),
    defaultDinnerPrice: z.number().nonnegative().optional(),
    defaultShowOnlyPrice: z.number().nonnegative().optional(),
    defaultCapacity: z.number().int().positive().optional(),
    status: z.enum(["DRAFT", "ACTIVE", "ARCHIVED"]).optional(),
  }),
  handler: async (ctx, args) => {
    const { id, ...updates } = args;
    await ctx.db.patch(id, { ...updates, updatedAt: Date.now() });
    return id;
  },
});
 
export const archiveShow = zMutation({
  args: { id: zid("shows") },
  handler: async (ctx, { id }) => {
    await ctx.db.patch(id, { status: "ARCHIVED", updatedAt: Date.now() });
    return id;
  },
});
  • Step 3: Commit
git add packages/backend/convex/domains/shows/shows.ts
git commit -m "refactor: update shows functions for new schema"

Task 4: Simplify Occurrences Functions (Remove Batch, Denormalize)

Files:

  • Modify: packages/backend/convex/domains/shows/occurrences.ts

  • Create: packages/backend/convex/domains/shows/occurrences.ts (replace existing)

  • Step 1: Read current occurrences.ts

Read packages/backend/convex/domains/shows/occurrences.ts (already read in context).

  • Step 2: Rewrite with simplified schema
import { zQuery, zMutation, z } from "../../../lib/zod";
import { zid } from "convex-helpers/server/zod3";
import { generateOccurrenceCode, ShowTime } from "./code-generator";
 
export type ShowScheduleItem = {
  date: string;
  dayLabel: string;
  time: string;
  title: string;
  subtitle: string;
  image: string;
  remaining: number;
  slug: string;
  code: string;
};
 
function formatTime(time: string): string {
  const [hours, minutes] = time.split(":").map(Number);
  const period = hours >= 12 ? "PM" : "AM";
  const hour12 = hours % 12 || 12;
  return `${hour12}:${String(minutes).padStart(2, "0")} ${period}`;
}
 
function getDayLabel(dateStr: string): string {
  const days = [
    "Sunday",
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday",
  ];
  const date = new Date(dateStr + "T00:00:00");
  return days[date.getDay()];
}
 
// Get upcoming occurrences for homepage carousel
export const listUpcoming = zQuery({
  args: { limit: z.number().optional() },
  returns: z.array(
    z.object({
      _id: zid("occurrences"),
      code: z.string(),
      date: z.string(),
      time: z.string(),
      dinnerPrice: z.number(),
      showOnlyPrice: z.number(),
      showOnlyEnabled: z.boolean(),
      actualCapacity: z.number(),
      bookedCount: z.number(),
      remaining: z.number(),
      showTitle: z.string(),
      showSlug: z.string(),
      showCode: z.string(),
    }),
  ),
  handler: async (ctx, { limit = 8 }) => {
    const today = new Date().toISOString().split("T")[0];
    const occurrences = await ctx.db
      .query("occurrences")
      .withIndex("by_date_status", (q) => q.gte("date", today))
      .collect();
 
    // Filter to scheduled only, join with show, sort by date+time
    const scheduled = occurrences.filter((o) => o.status === "SCHEDULED");
 
    // Batch fetch shows
    const showIds = [...new Set(scheduled.map((o) => o.showId))];
    const shows = await Promise.all(showIds.map((id) => ctx.db.get(id)));
    const showMap = new Map(shows.filter(Boolean).map((s) => [s!._id, s!]));
 
    return scheduled
      .map((o) => {
        const show = showMap.get(o.showId);
        if (!show) return null;
        return {
          _id: o._id,
          code: o.code,
          date: o.date,
          time: o.time,
          dinnerPrice: o.dinnerPrice,
          showOnlyPrice: o.showOnlyPrice,
          showOnlyEnabled: o.showOnlyEnabled,
          actualCapacity: o.actualCapacity,
          bookedCount: o.bookedCount,
          remaining: o.actualCapacity - o.bookedCount,
          showTitle: show.title,
          showSlug: show.slug,
          showCode: show.code,
        };
      })
      .filter(Boolean)
      .sort((a, b) => {
        const dateCompare = a!.date.localeCompare(b!.date);
        if (dateCompare !== 0) return dateCompare;
        return a!.time.localeCompare(b!.time);
      })
      .slice(0, limit);
  },
});
 
// Get all scheduled occurrences for dashboard (today + future, sorted)
export const listForDashboard = zQuery({
  args: {},
  returns: z.array(
    z.object({
      _id: zid("occurrences"),
      code: z.string(),
      showId: zid("shows"),
      showTitle: z.string(),
      date: z.string(),
      time: z.string(),
      dinnerPrice: z.number(),
      showOnlyPrice: z.number(),
      showOnlyEnabled: z.boolean(),
      actualCapacity: z.number(),
      bookedCount: z.number(),
      status: z.enum(["SCHEDULED", "CANCELLED", "SOLD_OUT"]),
    }),
  ),
  handler: async (ctx) => {
    const today = new Date().toISOString().split("T")[0];
    const occurrences = await ctx.db
      .query("occurrences")
      .withIndex("by_date_status", (q) => q.gte("date", today))
      .collect();
 
    // Fetch all shows for joining
    const showIds = [...new Set(occurrences.map((o) => o.showId))];
    const shows = await Promise.all(showIds.map((id) => ctx.db.get(id)));
    const showMap = new Map(shows.filter(Boolean).map((s) => [s!._id, s!]));
 
    return occurrences
      .filter((o) => o.status === "SCHEDULED")
      .map((o) => {
        const show = showMap.get(o.showId);
        return {
          _id: o._id,
          code: o.code,
          showId: o.showId,
          showTitle: show?.title ?? "Unknown Show",
          date: o.date,
          time: o.time,
          dinnerPrice: o.dinnerPrice,
          showOnlyPrice: o.showOnlyPrice,
          showOnlyEnabled: o.showOnlyEnabled,
          actualCapacity: o.actualCapacity,
          bookedCount: o.bookedCount,
          status: o.status,
        };
      })
      .sort((a, b) => {
        const dateCompare = a.date.localeCompare(b.date);
        if (dateCompare !== 0) return dateCompare;
        return a.time.localeCompare(b.time);
      });
  },
});
 
// Get occurrences for a specific show
export const byShow = zQuery({
  args: { showId: zid("shows") },
  returns: z.array(
    z.object({
      _id: zid("occurrences"),
      code: z.string(),
      date: z.string(),
      time: z.string(),
      dinnerPrice: z.number(),
      showOnlyPrice: z.number(),
      showOnlyEnabled: z.boolean(),
      actualCapacity: z.number(),
      bookedCount: z.number(),
      status: z.enum(["SCHEDULED", "CANCELLED", "SOLD_OUT"]),
    }),
  ),
  handler: async (ctx, { showId }) => {
    const today = new Date().toISOString().split("T")[0];
    return await ctx.db
      .query("occurrences")
      .withIndex("by_show_date", (q) =>
        q.eq("showId", showId).gte("date", today),
      )
      .collect();
  },
});
 
// Get single occurrence with show info
export const getWithShow = zQuery({
  args: { id: zid("occurrences") },
  returns: z
    .object({
      occurrence: z.object({
        _id: zid("occurrences"),
        code: z.string(),
        showId: zid("shows"),
        date: z.string(),
        time: z.string(),
        dinnerPrice: z.number(),
        showOnlyPrice: z.number(),
        showOnlyEnabled: z.boolean(),
        actualCapacity: z.number(),
        bookedCount: z.number(),
        status: z.enum(["SCHEDULED", "CANCELLED", "SOLD_OUT"]),
      }),
      show: z.object({
        _id: zid("shows"),
        code: z.string(),
        title: z.string(),
        tagline: z.string(),
        description: z.string(),
        gallery: z.array(z.string()),
        supportedTicketTypes: z.array(z.enum(["DINNER_THEATRE", "SHOW_ONLY"])),
      }),
    })
    .nullable(),
  handler: async (ctx, { id }) => {
    const occurrence = await ctx.db.get("occurrences", id);
    if (!occurrence) return null;
    const show = await ctx.db.get("shows", occurrence.showId);
    if (!show) return null;
    return { occurrence, show };
  },
});
 
// Create single occurrence (admin manually creates)
export const createOccurrence = zMutation({
  args: z.object({
    showId: zid("shows"),
    date: z.string(), // "YYYY-MM-DD"
    time: z.string(), // "19:30"
    timeSlot: z.enum(["L", "N"]), // Lunch or Night
    dinnerPrice: z.number(),
    showOnlyPrice: z.number(),
    showOnlyEnabled: z.boolean(),
    actualCapacity: z.number(),
  }),
  returns: z.object({ id: zid("occurrences"), code: z.string() }),
  handler: async (ctx, args) => {
    const {
      showId,
      date,
      time,
      timeSlot,
      dinnerPrice,
      showOnlyPrice,
      showOnlyEnabled,
      actualCapacity,
    } = args;
 
    // Get show to get the code
    const show = await ctx.db.get("shows", showId);
    if (!show) throw new Error("Show not found");
 
    // Generate unique code
    const code = generateOccurrenceCode(show.code, date, timeSlot);
 
    // Check for duplicate code
    const existing = await ctx.db
      .query("occurrences")
      .withIndex("by_code", (q) => q.eq("code", code))
      .first();
    if (existing)
      throw new Error(`Occurrence with code ${code} already exists`);
 
    const id = await ctx.db.insert("occurrences", {
      showId,
      code,
      date,
      time,
      dinnerPrice,
      showOnlyPrice,
      showOnlyEnabled,
      actualCapacity,
      bookedCount: 0,
      status: "SCHEDULED",
      assignedTables: [],
      createdAt: Date.now(),
      updatedAt: Date.now(),
    });
    return { id, code };
  },
});
 
// Duplicate an existing occurrence (admin creates copy with new date)
export const duplicateOccurrence = zMutation({
  args: z.object({
    occurrenceId: zid("occurrences"),
    newDate: z.string(), // "YYYY-MM-DD"
    newTime: z.string(), // "19:30"
    newTimeSlot: z.enum(["L", "N"]),
  }),
  returns: z.object({ id: zid("occurrences"), code: z.string() }),
  handler: async (ctx, args) => {
    const { occurrenceId, newDate, newTime, newTimeSlot } = args;
 
    const original = await ctx.db.get("occurrences", occurrenceId);
    if (!original) throw new Error("Occurrence not found");
 
    // Get show to get the code
    const show = await ctx.db.get("shows", original.showId);
    if (!show) throw new Error("Show not found");
 
    // Generate new code
    const newCode = generateOccurrenceCode(show.code, newDate, newTimeSlot);
 
    // Check for duplicate code
    const existing = await ctx.db
      .query("occurrences")
      .withIndex("by_code", (q) => q.eq("code", newCode))
      .first();
    if (existing)
      throw new Error(`Occurrence with code ${newCode} already exists`);
 
    const id = await ctx.db.insert("occurrences", {
      showId: original.showId,
      code: newCode,
      date: newDate,
      time: newTime,
      dinnerPrice: original.dinnerPrice,
      showOnlyPrice: original.showOnlyPrice,
      showOnlyEnabled: original.showOnlyEnabled,
      actualCapacity: original.actualCapacity,
      bookedCount: 0,
      status: "SCHEDULED",
      assignedTables: [],
      createdAt: Date.now(),
      updatedAt: Date.now(),
    });
    return { id, code: newCode };
  },
});
 
// Update occurrence (admin edits)
export const updateOccurrence = zMutation({
  args: z.object({
    id: zid("occurrences"),
    date: z.string().optional(),
    time: z.string().optional(),
    dinnerPrice: z.number().optional(),
    showOnlyPrice: z.number().optional(),
    showOnlyEnabled: z.boolean().optional(),
    actualCapacity: z.number().optional(),
    status: z.enum(["SCHEDULED", "CANCELLED", "SOLD_OUT"]).optional(),
  }),
  handler: async (ctx, args) => {
    const { id, ...updates } = args;
    await ctx.db.patch(id, { ...updates, updatedAt: Date.now() });
    return id;
  },
});
 
// Cancel occurrence
export const cancelOccurrence = zMutation({
  args: { id: zid("occurrences") },
  handler: async (ctx, { id }) => {
    await ctx.db.patch(id, { status: "CANCELLED", updatedAt: Date.now() });
    return id;
  },
});
 
// Get availability info for real-time scarcity signals
export const getAvailability = zQuery({
  args: { occurrenceId: zid("occurrences") },
  returns: z
    .object({
      occurrenceId: zid("occurrences"),
      code: z.string(),
      showTitle: z.string(),
      date: z.string(),
      time: z.string(),
      totalCapacity: z.number(),
      bookedCount: z.number(),
      remaining: z.number(),
      status: z.enum(["SCHEDULED", "CANCELLED", "SOLD_OUT"]),
      showOnlyEnabled: z.boolean(),
      dinnerPrice: z.number(),
      showOnlyPrice: z.number(),
      badge: z.enum(["AVAILABLE", "FEW_LEFT", "SOLD_OUT"]),
    })
    .nullable(),
  handler: async (ctx, { occurrenceId }) => {
    const occ = await ctx.db.get("occurrences", occurrenceId);
    if (!occ) return null;
 
    const show = await ctx.db.get("shows", occ.showId);
    const remaining = occ.actualCapacity - occ.bookedCount;
 
    return {
      occurrenceId: occ._id,
      code: occ.code,
      showTitle: show?.title ?? "Unknown Show",
      date: occ.date,
      time: occ.time,
      totalCapacity: occ.actualCapacity,
      bookedCount: occ.bookedCount,
      remaining,
      status: occ.status,
      showOnlyEnabled: occ.showOnlyEnabled,
      dinnerPrice: occ.dinnerPrice,
      showOnlyPrice: occ.showOnlyPrice,
      badge:
        remaining > 10 ? "AVAILABLE" : remaining > 0 ? "FEW_LEFT" : "SOLD_OUT",
    };
  },
});
 
// Get occurrences for schedule preview (homepage)
export const listForSchedulePreview = zQuery({
  args: { limit: z.number().optional() },
  returns: z.array(
    z.object({
      date: z.string(),
      dayLabel: z.string(),
      time: z.string(),
      title: z.string(),
      subtitle: z.string(),
      image: z.string(),
      remaining: z.number(),
      slug: z.string(),
      code: z.string(),
    }),
  ),
  handler: async (ctx, { limit = 30 }) => {
    const today = new Date().toISOString().split("T")[0];
 
    const allOccurrences = await ctx.db
      .query("occurrences")
      .withIndex("by_date", (q) => q.gte("date", today))
      .collect();
 
    const scheduled = allOccurrences
      .filter((o) => o.status === "SCHEDULED")
      .sort((a, b) => {
        const dateCompare = a.date.localeCompare(b.date);
        if (dateCompare !== 0) return dateCompare;
        return a.time.localeCompare(b.time);
      });
 
    // Fetch all unique shows
    const showIds = [...new Set(scheduled.map((o) => o.showId))];
    const shows = await Promise.all(showIds.map((id) => ctx.db.get(id)));
    const showMap = new Map(shows.filter(Boolean).map((s) => [s!._id, s!]));
 
    const items: ShowScheduleItem[] = [];
    for (const occ of scheduled) {
      const show = showMap.get(occ.showId);
      if (!show || show.status !== "ACTIVE") continue;
 
      const image = show.gallery.length > 0 ? show.gallery[0] : "";
 
      items.push({
        date: occ.date,
        dayLabel: getDayLabel(occ.date),
        time: formatTime(occ.time),
        title: show.title,
        subtitle: show.tagline,
        image,
        remaining: occ.actualCapacity - occ.bookedCount,
        slug: show.slug,
        code: occ.code,
      });
 
      if (items.length >= limit) break;
    }
 
    return items;
  },
});
  • Step 3: Commit
git add packages/backend/convex/domains/shows/occurrences.ts
git commit -m "refactor: simplify occurrences - remove batch generation, denormalize prices, add codes"

Task 5: Update Frontend Show Schedule Preview

Files:

  • Modify: apps/frontend/components/home/show-schedule-preview.tsx

  • Step 1: Read current component

cat apps/frontend/components/home/show-schedule-preview.tsx
  • Step 2: Update to use new API response shape

The component should use api.occurrences.listForSchedulePreview which now returns code field.

  • Step 3: Commit
git add apps/frontend/components/home/show-schedule-preview.tsx
git commit -m "refactor: update show-schedule-preview for new occurrence schema"

Task 6: Update Booking Flow (Show Picker)

Files:

  • Modify: apps/frontend/app/booking/page.tsx or relevant components

  • Step 1: Read current booking page

  • Step 2: Update to use new schema (shows instead of showTemplates, occurrences with codes)

  • Step 3: Commit

git add apps/frontend/app/booking/
git commit -m "refactor: update booking flow for simplified show system"

Task 7: Update Admin Dashboard (If Needed)

Files:

  • Modify: apps/frontend/app/admin/ relevant pages

  • Step 1: Check admin pages that use shows/occurrences

  • Step 2: Update to new API paths and response shapes

  • Step 3: Commit

git add apps/frontend/app/admin/
git commit -m "refactor: update admin pages for simplified show system"

Summary of Changes

AreaBeforeAfter
Table: showsshowTemplatesshows
Table: occurrencesshowOccurrencesoccurrences
Price fieldsdefaultDinnerPrice + dinnerPriceOverridedinnerPrice (denormalized)
Price override fieldsshowOnlyPriceOverride, dinnerPriceOverrideREMOVED
Batch generationgenerateBatch mutationREMOVED
Occurrence codeNone{showCode}{YYMMDD}{L|N} (e.g., TMTL260521N)
Show codeslug onlyNew code field (e.g., TMTL)
Create occurrenceComplex with overridescreateOccurrence with timeSlot (L/N)
Duplicate occurrenceN/AduplicateOccurrence mutation

API Changes

Old APINew API
api.shows.listActiveapi.shows.listActive (updated return type)
api.occurrences.listUpcomingapi.occurrences.listUpcoming (includes code)
api.occurrences.generateBatchREMOVED
api.occurrences.createOccurrenceapi.occurrences.createOccurrence (simplified args)
N/Aapi.occurrences.duplicateOccurrence (new)
api.occurrences.listForSchedulePreviewapi.occurrences.listForSchedulePreview (includes code)

Testing Checklist

  • Create a new show with code (e.g., "TMTL")
  • Create an occurrence manually — verify code generation (e.g., TMTL260521N)
  • Duplicate an occurrence — verify new code is generated
  • List occurrences — verify codes appear correctly
  • Schedule preview on homepage — verify codes displayed
  • Booking flow — verify show picker works with new schema
  • Admin dashboard — verify occurrence management works
  • Verify no batch generation endpoints exist