mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
Services and staff existed as schema + booking FK targets but had no management surface. Adds full CRUD: repository (pgx + in-memory with matching FK semantics), catalog service validation with tenant isolation, MaxStaff entitlement enforcement, protected API routes, OpenAPI schemas, and dashboard screens for both entities. Dashboard summary now resolves staff names on bookings and computes setup completion from real configured state. Booking modal selects from the tenant's actual services; blocked days can be scoped per staff member. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
172 lines
4.6 KiB
Go
172 lines
4.6 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
const serviceColumns = "id, tenant_id, name, duration_minutes, buffer_before_minutes, buffer_after_minutes, price_cents, created_at"
|
|
|
|
func (r *PGRepository) ListServicesByTenant(ctx context.Context, tenantID string) ([]ServiceRecord, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT `+serviceColumns+`
|
|
FROM services
|
|
WHERE tenant_id = $1
|
|
ORDER BY created_at ASC
|
|
`, tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var records []ServiceRecord
|
|
for rows.Next() {
|
|
var record ServiceRecord
|
|
if err := rows.Scan(
|
|
&record.ID,
|
|
&record.TenantID,
|
|
&record.Name,
|
|
&record.DurationMinutes,
|
|
&record.BufferBeforeMinutes,
|
|
&record.BufferAfterMinutes,
|
|
&record.PriceCents,
|
|
&record.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
return records, rows.Err()
|
|
}
|
|
|
|
func (r *PGRepository) GetServiceByID(ctx context.Context, serviceID string) (ServiceRecord, error) {
|
|
var record ServiceRecord
|
|
err := r.pool.QueryRow(ctx, `
|
|
SELECT `+serviceColumns+`
|
|
FROM services
|
|
WHERE id = $1
|
|
`, serviceID).Scan(
|
|
&record.ID,
|
|
&record.TenantID,
|
|
&record.Name,
|
|
&record.DurationMinutes,
|
|
&record.BufferBeforeMinutes,
|
|
&record.BufferAfterMinutes,
|
|
&record.PriceCents,
|
|
&record.CreatedAt,
|
|
)
|
|
return record, err
|
|
}
|
|
|
|
func (r *PGRepository) CreateService(ctx context.Context, params CreateServiceParams) (ServiceRecord, error) {
|
|
var record ServiceRecord
|
|
err := r.pool.QueryRow(ctx, `
|
|
INSERT INTO services (tenant_id, name, duration_minutes, buffer_before_minutes, buffer_after_minutes, price_cents)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING `+serviceColumns+`
|
|
`, params.TenantID, params.Name, params.DurationMinutes, params.BufferBeforeMinutes, params.BufferAfterMinutes, params.PriceCents).Scan(
|
|
&record.ID,
|
|
&record.TenantID,
|
|
&record.Name,
|
|
&record.DurationMinutes,
|
|
&record.BufferBeforeMinutes,
|
|
&record.BufferAfterMinutes,
|
|
&record.PriceCents,
|
|
&record.CreatedAt,
|
|
)
|
|
return record, err
|
|
}
|
|
|
|
func (r *PGRepository) UpdateService(ctx context.Context, serviceID string, params UpdateServiceParams) (ServiceRecord, error) {
|
|
var record ServiceRecord
|
|
err := r.pool.QueryRow(ctx, `
|
|
UPDATE services
|
|
SET name = COALESCE($2, name),
|
|
duration_minutes = COALESCE($3, duration_minutes),
|
|
buffer_before_minutes = COALESCE($4, buffer_before_minutes),
|
|
buffer_after_minutes = COALESCE($5, buffer_after_minutes),
|
|
price_cents = COALESCE($6, price_cents)
|
|
WHERE id = $1
|
|
RETURNING `+serviceColumns+`
|
|
`, serviceID, params.Name, params.DurationMinutes, params.BufferBeforeMinutes, params.BufferAfterMinutes, params.PriceCents).Scan(
|
|
&record.ID,
|
|
&record.TenantID,
|
|
&record.Name,
|
|
&record.DurationMinutes,
|
|
&record.BufferBeforeMinutes,
|
|
&record.BufferAfterMinutes,
|
|
&record.PriceCents,
|
|
&record.CreatedAt,
|
|
)
|
|
return record, err
|
|
}
|
|
|
|
func (r *PGRepository) DeleteService(ctx context.Context, serviceID string) error {
|
|
_, err := r.pool.Exec(ctx, `DELETE FROM services WHERE id = $1`, serviceID)
|
|
return err
|
|
}
|
|
|
|
func (r *PGRepository) ListAvailabilityRulesByTenant(ctx context.Context, tenantID string) ([]AvailabilityRuleRecord, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT id, tenant_id, staff_id, day_of_week, starts_local, ends_local
|
|
FROM availability_rules
|
|
WHERE tenant_id = $1
|
|
ORDER BY day_of_week ASC, starts_local ASC
|
|
`, tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var records []AvailabilityRuleRecord
|
|
for rows.Next() {
|
|
var record AvailabilityRuleRecord
|
|
if err := rows.Scan(
|
|
&record.ID,
|
|
&record.TenantID,
|
|
&record.StaffID,
|
|
&record.DayOfWeek,
|
|
&record.StartsLocal,
|
|
&record.EndsLocal,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
return records, rows.Err()
|
|
}
|
|
|
|
func (r *PGRepository) ListClassSessionsByTenant(ctx context.Context, tenantID string, from time.Time, limit int) ([]ClassSessionRecord, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT cs.id, cs.tenant_id, cs.template_id, cs.location_id, ct.title, cs.starts_at, cs.ends_at, cs.capacity
|
|
FROM class_sessions cs
|
|
INNER JOIN class_templates ct ON ct.id = cs.template_id
|
|
WHERE cs.tenant_id = $1 AND cs.starts_at >= $2
|
|
ORDER BY cs.starts_at ASC
|
|
LIMIT $3
|
|
`, tenantID, from, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var records []ClassSessionRecord
|
|
for rows.Next() {
|
|
var record ClassSessionRecord
|
|
if err := rows.Scan(
|
|
&record.ID,
|
|
&record.TenantID,
|
|
&record.TemplateID,
|
|
&record.LocationID,
|
|
&record.Title,
|
|
&record.StartsAt,
|
|
&record.EndsAt,
|
|
&record.Capacity,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
return records, rows.Err()
|
|
}
|