package db import ( "context" "time" ) const locationColumns = "id, tenant_id, name, type, capacity, timezone, created_at" func (r *PGRepository) ListLocationsByTenant(ctx context.Context, tenantID string) ([]LocationRecord, error) { rows, err := r.pool.Query(ctx, ` SELECT `+locationColumns+` FROM locations WHERE tenant_id = $1 ORDER BY name `, tenantID) if err != nil { return nil, err } defer rows.Close() var records []LocationRecord for rows.Next() { var rec LocationRecord if err := rows.Scan(&rec.ID, &rec.TenantID, &rec.Name, &rec.Type, &rec.Capacity, &rec.Timezone, &rec.CreatedAt); err != nil { return nil, err } records = append(records, rec) } return records, rows.Err() } func (r *PGRepository) GetLocationByID(ctx context.Context, locationID string) (LocationRecord, error) { var rec LocationRecord err := r.pool.QueryRow(ctx, ` SELECT `+locationColumns+` FROM locations WHERE id = $1 `, locationID).Scan(&rec.ID, &rec.TenantID, &rec.Name, &rec.Type, &rec.Capacity, &rec.Timezone, &rec.CreatedAt) return rec, err } func (r *PGRepository) CreateLocation(ctx context.Context, params CreateLocationParams) (LocationRecord, error) { var rec LocationRecord err := r.pool.QueryRow(ctx, ` INSERT INTO locations (tenant_id, name, type, capacity, timezone) VALUES ($1, $2, $3, $4, $5) RETURNING `+locationColumns+` `, params.TenantID, params.Name, params.Type, params.Capacity, params.Timezone).Scan(&rec.ID, &rec.TenantID, &rec.Name, &rec.Type, &rec.Capacity, &rec.Timezone, &rec.CreatedAt) return rec, err } func (r *PGRepository) UpdateLocation(ctx context.Context, locationID string, params UpdateLocationParams) (LocationRecord, error) { var rec LocationRecord err := r.pool.QueryRow(ctx, ` UPDATE locations SET name = COALESCE($2, name), type = COALESCE($3, type), capacity = COALESCE($4, capacity), timezone = COALESCE($5, timezone), updated_at = now() WHERE id = $1 RETURNING `+locationColumns+` `, locationID, params.Name, params.Type, params.Capacity, params.Timezone).Scan(&rec.ID, &rec.TenantID, &rec.Name, &rec.Type, &rec.Capacity, &rec.Timezone, &rec.CreatedAt) return rec, err } func (r *PGRepository) DeleteLocation(ctx context.Context, locationID string) error { _, err := r.pool.Exec(ctx, `DELETE FROM locations WHERE id = $1`, locationID) return err } func (r *PGRepository) ListBlockedDaysByTenant(ctx context.Context, tenantID string, from time.Time, to time.Time) ([]BlockedDayRecord, error) { rows, err := r.pool.Query(ctx, ` SELECT id, tenant_id, staff_id, starts_at, ends_at, kind, reason, created_at FROM availability_exceptions WHERE tenant_id = $1 AND starts_at <= $3 AND ends_at >= $2 ORDER BY starts_at `, tenantID, from, to) if err != nil { return nil, err } defer rows.Close() var records []BlockedDayRecord for rows.Next() { var rec BlockedDayRecord if err := rows.Scan(&rec.ID, &rec.TenantID, &rec.StaffID, &rec.StartsAt, &rec.EndsAt, &rec.Kind, &rec.Reason, &rec.CreatedAt); err != nil { return nil, err } records = append(records, rec) } return records, rows.Err() } func (r *PGRepository) CreateBlockedDay(ctx context.Context, params CreateBlockedDayParams) (BlockedDayRecord, error) { var rec BlockedDayRecord err := r.pool.QueryRow(ctx, ` INSERT INTO availability_exceptions (tenant_id, staff_id, starts_at, ends_at, kind, reason) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, tenant_id, staff_id, starts_at, ends_at, kind, reason, created_at `, params.TenantID, params.StaffID, params.StartsAt, params.EndsAt, params.Kind, params.Reason).Scan(&rec.ID, &rec.TenantID, &rec.StaffID, &rec.StartsAt, &rec.EndsAt, &rec.Kind, &rec.Reason, &rec.CreatedAt) return rec, err } func (r *PGRepository) UpdateBlockedDay(ctx context.Context, blockedDayID string, params UpdateBlockedDayParams) (BlockedDayRecord, error) { var rec BlockedDayRecord err := r.pool.QueryRow(ctx, ` UPDATE availability_exceptions SET starts_at = COALESCE($2, starts_at), ends_at = COALESCE($3, ends_at), kind = COALESCE($4, kind), reason = COALESCE($5, reason) WHERE id = $1 RETURNING id, tenant_id, staff_id, starts_at, ends_at, kind, reason, created_at `, blockedDayID, params.StartsAt, params.EndsAt, params.Kind, params.Reason).Scan(&rec.ID, &rec.TenantID, &rec.StaffID, &rec.StartsAt, &rec.EndsAt, &rec.Kind, &rec.Reason, &rec.CreatedAt) return rec, err } func (r *PGRepository) DeleteBlockedDay(ctx context.Context, blockedDayID string) error { _, err := r.pool.Exec(ctx, `DELETE FROM availability_exceptions WHERE id = $1`, blockedDayID) return err } func (r *PGRepository) ListWorkingHoursByTenant(ctx context.Context, tenantID string, staffID *string) ([]WorkingHoursRecord, error) { query := ` SELECT tenant_id, staff_id, day_of_week, starts_local::text, ends_local::text FROM availability_rules WHERE tenant_id = $1 AND staff_id IS NULL ORDER BY day_of_week ` args := []any{tenantID} if staffID != nil { query = ` SELECT tenant_id, staff_id, day_of_week, starts_local::text, ends_local::text FROM availability_rules WHERE tenant_id = $1 AND staff_id = $2 ORDER BY day_of_week ` args = append(args, *staffID) } rows, err := r.pool.Query(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() var records []WorkingHoursRecord for rows.Next() { var rec WorkingHoursRecord if err := rows.Scan(&rec.TenantID, &rec.StaffID, &rec.DayOfWeek, &rec.StartsLocal, &rec.EndsLocal); err != nil { return nil, err } records = append(records, rec) } return records, rows.Err() } func (r *PGRepository) UpdateWorkingHours(ctx context.Context, tenantID string, staffID *string, dayOfWeek int, params UpdateWorkingHoursParams) error { deleteQuery := ` DELETE FROM availability_rules WHERE tenant_id = $1 AND day_of_week = $2 AND staff_id IS NULL ` args := []any{tenantID, dayOfWeek} if staffID != nil { deleteQuery = ` DELETE FROM availability_rules WHERE tenant_id = $1 AND day_of_week = $2 AND staff_id = $3 ` args = append(args, *staffID) } if params.IsOpen != nil && !*params.IsOpen { _, err := r.pool.Exec(ctx, deleteQuery, args...) return err } if params.StartsLocal == nil || params.EndsLocal == nil { return nil } // availability_rules has no unique constraint on (tenant, day): replace the // scoped row so repeated updates stay idempotent. if _, err := r.pool.Exec(ctx, deleteQuery, args...); err != nil { return err } _, err := r.pool.Exec(ctx, ` INSERT INTO availability_rules (id, tenant_id, staff_id, day_of_week, starts_local, ends_local) VALUES (gen_random_uuid(), $1, $2, $3, $4::time, $5::time) `, tenantID, staffID, dayOfWeek, *params.StartsLocal, *params.EndsLocal) return err }