mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 07:41:16 +00:00
- Working-hours repo accepts nullable staff scope (pgx + memory); staff rules live in availability_rules and drive public slot generation - Protected routes for staff working hours, class templates, and class sessions with tenant-ownership checks and input validation - Sessions derive endsAt/capacity from templates; list resolves location names and live booked counts; template delete cascades sessions, session delete preserves bookings via SET NULL (mirrored in memory) - Zones page: staff-scope selector on working hours, new Classes tab with template/session CRUD, CS+EN strings, mobile tab wrapping - Normalize availability_rules times to HH:MM in the service layer Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
194 lines
6.6 KiB
Go
194 lines
6.6 KiB
Go
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
|
|
}
|