Build a complete gym class booking and membership system for fitness studios
* Preview is for reference only. Actual results may vary depending on the AI model, variable values, and tools used.
You are a senior full-stack engineer building a complete gym class booking and membership management system for a Malaysian fitness studio. This system replaces expensive international software like Mindbody or Glofox with a custom, locally-hosted solution.
Gym Name: {{gym_name}}
Class Types: {{class_types}}
Membership Tiers: {{membership_tiers}}
Max Class Capacity: {{max_capacity}} people per class
Tech stack: Next.js 14 (App Router), TypeScript, Tailwind CSS, shadcn/ui, Supabase (Postgres + Auth + RLS), deployed on Vercel.
---
## SECTION 1: DATABASE SCHEMA
Create the following Supabase/Postgres tables with full column definitions:
**members**
- id (uuid, PK, default gen_random_uuid())
- full_name (text, NOT NULL)
- email (text, UNIQUE, NOT NULL)
- phone (text) — Malaysian format: +60XXXXXXXXX
- ic_number (text) — Malaysian IC or passport
- date_of_birth (date)
- emergency_contact_name (text)
- emergency_contact_phone (text)
- profile_photo_url (text)
- status (text, enum: 'active' | 'inactive' | 'frozen', default: 'active')
- no_show_count (integer, default: 0)
- booking_suspended_until (timestamptz)
- created_at (timestamptz, default now())
- Indexes: email, phone, status
**membership_plans**
- id (uuid, PK)
- name (text) — e.g. Basic, Standard, Premium
- price_monthly (numeric(10,2), NOT NULL)
- class_credits_per_month (integer, NULL = unlimited)
- can_book_peak_hours (boolean, default: false)
- freeze_allowed_days_per_year (integer, default: 30)
- description (text)
- is_active (boolean, default: true)
- sort_order (integer)
**memberships**
- id (uuid, PK)
- member_id (uuid, FK → members, ON DELETE CASCADE)
- plan_id (uuid, FK → membership_plans)
- start_date (date, NOT NULL)
- end_date (date, NOT NULL)
- status (text, enum: 'active' | 'expired' | 'frozen' | 'cancelled')
- auto_renew (boolean, default: true)
- credits_remaining (integer, NULL = unlimited)
- freeze_start (date)
- freeze_end (date)
- freeze_days_used (integer, default: 0)
- created_at (timestamptz)
- Indexes: member_id, status, end_date
**instructors**
- id (uuid, PK)
- full_name (text, NOT NULL)
- bio (text)
- specializations (text[]) — array e.g. ['Yoga', 'HIIT']
- profile_photo_url (text)
- phone (text)
- email (text)
- is_active (boolean, default: true)
- created_at (timestamptz)
**classes**
- id (uuid, PK)
- name (text, NOT NULL)
- name_en (text)
- description (text)
- duration_minutes (integer, NOT NULL)
- color_hex (text) — for calendar color coding
- difficulty_level (text, enum: 'beginner' | 'intermediate' | 'advanced')
- drop_in_price (numeric(10,2)) — single class fee in RM
- is_active (boolean, default: true)
**class_schedule**
- id (uuid, PK)
- class_id (uuid, FK → classes)
- instructor_id (uuid, FK → instructors)
- scheduled_date (date, NOT NULL)
- start_time (time, NOT NULL)
- end_time (time, NOT NULL)
- room (text)
- max_capacity (integer, NOT NULL, default: {{max_capacity}})
- booking_cutoff_hours (integer, default: 1)
- cancellation_deadline_hours (integer, default: 2)
- is_peak_hour (boolean, default: false)
- status (text, enum: 'scheduled' | 'cancelled' | 'completed', default: 'scheduled')
- cancellation_reason (text)
- is_recurring (boolean, default: false)
- recurrence_rule (text) — RRULE string
- created_at (timestamptz)
- Indexes: scheduled_date, class_id, instructor_id, status
**bookings**
- id (uuid, PK)
- member_id (uuid, FK → members)
- schedule_id (uuid, FK → class_schedule)
- booking_time (timestamptz, default now())
- status (text, enum: 'confirmed' | 'waitlisted' | 'cancelled' | 'attended' | 'no_show')
- waitlist_position (integer, NULL — only set when status = 'waitlisted')
- payment_type (text, enum: 'membership' | 'drop_in')
- amount_paid (numeric(10,2), default: 0)
- cancellation_time (timestamptz)
- cancellation_reason (text)
- credit_refunded (boolean, default: false)
- Unique constraint: (member_id, schedule_id)
- Indexes: member_id, schedule_id, status
**attendance**
- id (uuid, PK)
- booking_id (uuid, FK → bookings, UNIQUE)
- check_in_time (timestamptz, default now())
- check_in_method (text, enum: 'qr_code' | 'manual' | 'app')
- checked_in_by (uuid) — staff user id, nullable
**payments**
- id (uuid, PK)
- member_id (uuid, FK → members)
- membership_id (uuid, FK → memberships, NULL)
- booking_id (uuid, FK → bookings, NULL)
- amount (numeric(10,2), NOT NULL)
- currency (text, default: 'MYR')
- payment_method (text, enum: 'cash' | 'card' | 'online_banking' | 'ewallet')
- status (text, enum: 'pending' | 'completed' | 'failed' | 'refunded')
- transaction_ref (text)
- paid_at (timestamptz)
- notes (text)
- Indexes: member_id, paid_at, status
RLS Policies:
- Public SELECT on classes, instructors, class_schedule (status = 'scheduled')
- Authenticated members can SELECT/INSERT their own bookings and payments
- Admin role (email in admins table) has full access to all tables
- Always wrap auth.uid() as (select auth.uid()) in RLS policies for performance
---
## SECTION 2: WEEKLY CLASS SCHEDULE PAGE (/schedule)
Build the main member-facing schedule page:
- Desktop: 7-column grid (Mon–Sun), rows are hourly time slots from 06:00–22:00
- Mobile: vertical list grouped by day with a day-picker tab bar at top
- Each class card shows: color-coded left border (by class type), class name (bold), instructor name, time range, duration badge, difficulty pill, capacity bar
- Capacity bar: green when >50% available, yellow when <30%, red when full
- Full = show 'Join Waitlist' button instead of 'Book Now'
- Filter bar: dropdowns for Class Type, Instructor, Difficulty Level; 'Today' shortcut button
- Clicking a card opens a Sheet/Drawer (shadcn Sheet component) with full details:
- Class description, instructor bio with photo
- Cancellation policy reminder
- Member's current credit balance
- CTA: 'Confirm Booking' (yellow button) or 'Join Waitlist'
- Fetch schedule data in Server Component using Supabase server client; pass to Client Component for interactivity
---
## SECTION 3: BOOKING LOGIC & WAITLIST
Implement as a Next.js Server Action (`/lib/actions/bookings.ts`):
bookClass(memberId, scheduleId):
1. Check member status is 'active' and booking_suspended_until is null or past
2. Check membership is valid (status = 'active', end_date >= today)
3. Check peak hour restriction: if is_peak_hour and plan.can_book_peak_hours = false → reject
4. Check booking cutoff: if now() > scheduled_date + start_time - booking_cutoff_hours → reject
5. Count confirmed bookings for schedule: if < max_capacity → insert booking (status: 'confirmed')
6. If at capacity → insert booking (status: 'waitlisted', waitlist_position = current_max + 1)
7. Deduct 1 credit if payment_type = 'membership' and credits_remaining is not null
8. Return success/error with booking details
cancelBooking(bookingId, memberId):
1. Verify booking belongs to member
2. Check cancellation window: if now() < class start - cancellation_deadline_hours → refund credit, set credit_refunded = true
3. Update booking status to 'cancelled', record cancellation_time
4. Trigger waitlist promotion: find first 'waitlisted' booking for same schedule, promote to 'confirmed'
5. Notify promoted member (log to notifications table or trigger edge function)
---
## SECTION 4: MEMBER REGISTRATION & PROFILE
Registration page (/register):
- Multi-step form: Step 1 = personal info, Step 2 = choose membership plan, Step 3 = payment
- Personal info: full name, email, phone (+60 format with validation), IC number, DOB, emergency contact
- Plan selection: display {{membership_tiers}} as comparison cards with feature lists and RM pricing
- Payment method: Cash / Bank Transfer / E-Wallet (Touch 'n Go, GrabPay, Boost) — offline confirmation flow
- On submit: create member record + membership record + payment record
Member dashboard (/member/[id]):
- Header: avatar, name, membership plan badge, status chip
- Stats row: days until expiry, credits remaining, classes this month, attendance rate
- Upcoming bookings card: next 5 classes with cancel button (shows refund eligibility)
- Tabs: Upcoming | History | Payments
- History tab: all past bookings with attended/no_show status
- QR Code tab: member's unique QR code for gym check-in (encode member id)
- Freeze membership button: opens dialog to select freeze dates (validates against plan limits)
---
## SECTION 5: INSTRUCTOR MANAGEMENT (/admin/instructors)
- Instructor card grid: photo, name, active class types as colored badges, this week's class count
- Add/Edit instructor form in a Sheet panel (no page navigation)
- Instructor detail view: weekly schedule calendar, total classes this month, average attendance rate
- Instructor public profile page (/instructor/[id]) showing their upcoming classes
---
## SECTION 6: CHECK-IN & ATTENDANCE (/checkin)
Front-desk check-in interface (optimized for tablet/iPad):
- QR Code scanner (use jsQR or html5-qrcode library) — scans member QR, auto-looks up today's booking
- Manual search: type member name or phone, instant search results dropdown
- On member found: show photo, name, today's booked class, check-in button
- One-tap check-in → writes to attendance table, updates booking status to 'attended'
- Today's class roster view: select a class → see all booked members with check-in status
- Bulk mark attendance: select all present, mark absent as 'no_show'
- No-show counter: if booking status set to 'no_show', increment member.no_show_count
- Auto-suspend: if no_show_count >= 3, set booking_suspended_until = now() + 7 days
---
## SECTION 7: BOOKING HISTORY & CANCELLATION (/bookings)
Member-facing booking history page:
- Tabs: Upcoming | Past | Cancelled | Waitlisted
- Each row: class name + color dot, date/time, instructor, status badge, action button
- Upcoming: show 'Cancel' button with time remaining before cutoff
- Cancellation modal: clearly state whether credit will be refunded, confirm/abort
- Past: show attendance status (Attended ✓ / No Show ✗)
- Export: download attendance history as CSV for personal records
- Waitlist position indicator: 'You are #2 on the waitlist'
---
## SECTION 8: PAYMENT TRACKING (/admin/payments)
Admin payments page:
- Sortable, filterable table: member name, amount, type (Membership Fee / Drop-in), method, status, date
- Filter panel: date range picker, payment method multi-select, status filter
- 'Record Payment' button: manual payment entry form for cash/offline transactions
- Row actions: mark as completed, add notes, issue refund
- Summary cards at top: Today's Revenue, This Month's Revenue, Outstanding/Pending Payments
- Monthly revenue chart (recharts bar chart): membership fees vs drop-in fees stacked
---
## SECTION 9: ADMIN DASHBOARD (/admin)
Today's overview dashboard:
- Metric cards row: Today's classes, Total bookings today, Check-ins completed, Attendance rate %
- Revenue cards: Today / This Month / MoM Growth %
- Expiring memberships table: members expiring in 7 days, with 'Renew Now' quick action
- Today's timeline: hourly schedule with each class showing booked/capacity ratio as mini progress bar
- Top classes this month: ranked list with booking counts
- New member registrations chart: 30-day sparkline
- Quick actions: Add Member, Create Class, Record Payment, Export Report
---
## SECTION 10: BUSINESS RULES SUMMARY
- Booking cutoff: 1 hour before class start (configurable per schedule)
- Cancellation window: 2 hours before class — credits refunded within window, forfeited after
- Waitlist auto-promotion: when a cancellation occurs, first waitlisted member is auto-promoted; they have 30 minutes to confirm or forfeit spot to next in queue
- Membership freeze: up to 30 days/year (plan-defined), end_date extends by freeze duration
- Drop-in pricing: charged when member has no active membership or zero credits remaining
- Peak hour restriction: Basic plan members cannot book 07:00–09:00 or 18:00–20:00 slots (is_peak_hour flag on class_schedule)
- No-show policy: 3 no-shows trigger 7-day booking suspension; counter resets monthly
- Malaysian localization throughout: RM currency, +60 phone validation, Asia/Kuala_Lumpur timezone (UTC+8), date format DD/MM/YYYY
---
## DELIVERABLES
Provide complete, runnable code including:
1. Full SQL migration files with all tables, indexes, RLS policies, and seed data
2. All Next.js pages, layouts, and components
3. Server Actions for all mutations (booking, cancellation, membership ops)
4. Supabase Edge Function for daily membership expiry check and no-show counter reset
5. .env.local.example with all required environment variables
6. README with setup steps, Vercel deployment guide, and first-admin setup instructions
Use shadcn/ui components throughout. Apply cn() for all conditional classes. Keep all monetary values in MYR. Format all phone numbers in +60 format. Display all datetimes in Asia/Kuala_Lumpur timezone.