feat: implement customer registration flow and business hours system

Major changes:
- Add customer registration with email/phone lookup (app/booking/registro)
- Add customers API endpoint (app/api/customers/route)
- Implement business hours for locations (mon-fri 10-7, sat 10-6, sun closed)
- Fix availability function type casting issues
- Add business hours utilities (lib/utils/business-hours.ts)
- Update Location type to include business_hours JSONB
- Add mock payment component for testing
- Remove Supabase auth from booking flow
- Fix /cita redirect path in booking flow

Database migrations:
- Add category column to services table
- Add business_hours JSONB column to locations table
- Fix availability functions with proper type casting
- Update get_detailed_availability to use business_hours

Features:
- Customer lookup by email or phone
- Auto-redirect to registration if customer not found
- Pre-fill customer data if exists
- Business hours per day of week
- Location-specific opening/closing times
This commit is contained in:
Marco Gallegos
2026-01-17 00:29:49 -06:00
parent fb60178c86
commit 583a25a6f6
56 changed files with 2676 additions and 491 deletions

View File

@@ -8,6 +8,22 @@ export type InvitationStatus = 'pending' | 'used' | 'expired'
export type ResourceType = 'station' | 'room' | 'equipment'
export type AuditAction = 'create' | 'update' | 'delete' | 'reset_invitations' | 'payment' | 'status_change'
export interface DayHours {
open: string
close: string
is_closed: boolean
}
export interface BusinessHours {
monday: DayHours
tuesday: DayHours
wednesday: DayHours
thursday: DayHours
friday: DayHours
saturday: DayHours
sunday: DayHours
}
/** Represents a salon location with timezone and contact info */
export interface Location {
id: string
@@ -16,6 +32,7 @@ export interface Location {
address?: string
phone?: string
is_active: boolean
business_hours?: BusinessHours
created_at: string
updated_at: string
}
@@ -151,8 +168,8 @@ export type Database = {
Tables: {
locations: {
Row: Location
Insert: Omit<Location, 'id' | 'created_at' | 'updated_at'>
Update: Partial<Omit<Location, 'id' | 'created_at'>>
Insert: Omit<Location, 'id' | 'created_at' | 'updated_at'> & { business_hours?: BusinessHours }
Update: Partial<Omit<Location, 'id' | 'created_at'> & { business_hours?: BusinessHours }>
}
resources: {
Row: Resource

16
lib/supabase/admin.ts Normal file
View File

@@ -0,0 +1,16 @@
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!
// Admin Supabase client for server-side operations with service role
export const supabaseAdmin = createClient(
supabaseUrl,
supabaseServiceRoleKey,
{
auth: {
autoRefreshToken: false,
persistSession: false
}
}
)

View File

@@ -6,16 +6,4 @@ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
// Public Supabase client for client-side operations
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
// Admin Supabase client for server-side operations with service role
export const supabaseAdmin = createClient(
supabaseUrl,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
auth: {
autoRefreshToken: false,
persistSession: false
}
}
)
export default supabase

View File

@@ -0,0 +1,84 @@
import type { BusinessHours, DayHours } from '@/lib/db/types'
const DAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] as const
type DayOfWeek = typeof DAYS[number]
export function getDayOfWeek(date: Date): DayOfWeek {
return DAYS[date.getDay()]
}
export function isOpenNow(businessHours: BusinessHours, date = new Date): boolean {
const day = getDayOfWeek(date)
const hours = businessHours[day]
if (!hours || hours.is_closed) {
return false
}
const now = date
const [openHour, openMinute] = hours.open.split(':').map(Number)
const [closeHour, closeMinute] = hours.close.split(':').map(Number)
const openTime = new Date(now)
openTime.setHours(openHour, openMinute, 0, 0)
const closeTime = new Date(now)
closeTime.setHours(closeHour, closeMinute, 0, 0)
return now >= openTime && now < closeTime
}
export function getNextOpenTime(businessHours: BusinessHours, from = new Date): Date | null {
const checkDate = new Date(from)
for (let i = 0; i < 7; i++) {
const day = getDayOfWeek(checkDate)
const hours = businessHours[day]
if (hours && !hours.is_closed) {
const [openHour, openMinute] = hours.open.split(':').map(Number)
const openTime = new Date(checkDate)
openTime.setHours(openHour, openMinute, 0, 0)
if (openTime > from) {
return openTime
}
openTime.setDate(openTime.getDate() + 1)
return openTime
}
checkDate.setDate(checkDate.getDate() + 1)
}
return null
}
export function isTimeWithinHours(time: string, dayHours: DayHours): boolean {
if (dayHours.is_closed) {
return false
}
const [hour, minute] = time.split(':').map(Number)
const checkMinutes = hour * 60 + minute
const [openHour, openMinute] = dayHours.open.split(':').map(Number)
const [closeHour, closeMinute] = dayHours.close.split(':').map(Number)
const openMinutes = openHour * 60 + openMinute
const closeMinutes = closeHour * 60 + closeMinute
return checkMinutes >= openMinutes && checkMinutes < closeMinutes
}
export function getBusinessHoursString(dayHours: DayHours): string {
if (dayHours.is_closed) {
return 'Cerrado'
}
return `${dayHours.open} - ${dayHours.close}`
}
export function getTodayHours(businessHours: BusinessHours): string {
const day = getDayOfWeek(new Date())
return getBusinessHoursString(businessHours[day])
}

View File

@@ -1,4 +1,4 @@
import { supabaseAdmin } from '@/lib/supabase/client'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* generateShortId function that generates a unique short ID using Supabase RPC.