Files
Bookra/apps/backend/internal/db/staff.go
T
Tomas DvorakandDevin 09f780e7a6 feat(catalog): add service and staff management end-to-end
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>
2026-09-18 08:52:25 +02:00

80 lines
2.5 KiB
Go

package db
import (
"context"
"strings"
)
const staffColumns = "id, tenant_id, location_id, display_name, active, created_at"
func (r *PGRepository) ListStaffByTenant(ctx context.Context, tenantID string) ([]StaffRecord, error) {
rows, err := r.pool.Query(ctx, `
SELECT `+staffColumns+`
FROM staff
WHERE tenant_id = $1
ORDER BY created_at ASC
`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var records []StaffRecord
for rows.Next() {
var rec StaffRecord
if err := rows.Scan(&rec.ID, &rec.TenantID, &rec.LocationID, &rec.DisplayName, &rec.Active, &rec.CreatedAt); err != nil {
return nil, err
}
records = append(records, rec)
}
return records, rows.Err()
}
func (r *PGRepository) GetStaffByID(ctx context.Context, staffID string) (StaffRecord, error) {
var rec StaffRecord
err := r.pool.QueryRow(ctx, `
SELECT `+staffColumns+`
FROM staff
WHERE id = $1
`, staffID).Scan(&rec.ID, &rec.TenantID, &rec.LocationID, &rec.DisplayName, &rec.Active, &rec.CreatedAt)
return rec, err
}
func (r *PGRepository) CreateStaff(ctx context.Context, params CreateStaffParams) (StaffRecord, error) {
var rec StaffRecord
err := r.pool.QueryRow(ctx, `
INSERT INTO staff (tenant_id, location_id, display_name, active)
VALUES ($1, $2, $3, $4)
RETURNING `+staffColumns+`
`, params.TenantID, params.LocationID, params.DisplayName, params.Active).Scan(
&rec.ID, &rec.TenantID, &rec.LocationID, &rec.DisplayName, &rec.Active, &rec.CreatedAt)
return rec, err
}
func (r *PGRepository) UpdateStaff(ctx context.Context, staffID string, params UpdateStaffParams) (StaffRecord, error) {
// Tri-state location update: untouched when the request omits the field,
// NULL when it is explicitly empty, otherwise reassigned.
locationSet := params.LocationID != nil
var locationValue any
if locationSet && strings.TrimSpace(*params.LocationID) != "" {
locationValue = strings.TrimSpace(*params.LocationID)
}
var rec StaffRecord
err := r.pool.QueryRow(ctx, `
UPDATE staff
SET display_name = COALESCE($2, display_name),
location_id = CASE WHEN $3 THEN $4::uuid ELSE location_id END,
active = COALESCE($5, active)
WHERE id = $1
RETURNING `+staffColumns+`
`, staffID, params.DisplayName, locationSet, locationValue, params.Active).Scan(
&rec.ID, &rec.TenantID, &rec.LocationID, &rec.DisplayName, &rec.Active, &rec.CreatedAt)
return rec, err
}
func (r *PGRepository) DeleteStaff(ctx context.Context, staffID string) error {
_, err := r.pool.Exec(ctx, `DELETE FROM staff WHERE id = $1`, staffID)
return err
}