mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 07:41:16 +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>
80 lines
2.5 KiB
Go
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
|
|
}
|