From a61e64ded0a18d35ebcf5e1590bb923f21a74942 Mon Sep 17 00:00:00 2001 From: Guillermo Pages Date: Fri, 7 Nov 2025 13:23:53 +0100 Subject: [PATCH] feat(admin): align UI with Backend Brooke Phase 2 contracts, flip mocks to real endpoints Phase 2 Integration Updates: - Profile UI: Read/write address and contact fields from settings.address/settings.contact JSONB structure - Court dependencies: Updated modal to show 3 counts (slot_definitions, slot_instances_future, slot_instances_booked) - Error codes: Fixed duplicate court check to use court_name_duplicate (matches backend) - Mock flags: Flipped all 9 USE_MOCKS flags to false (courts.ts x7, materialisation.ts x2) Profile Tab (ClubProfileTab.tsx): - Read address fields from profile.settings?.address?.line_1 etc. - Read contact fields from profile.settings?.contact?.phone etc. - Write to settings structure preserving existing settings - Added Info icon tooltips: "Stored in settings until native fields ship" - Removed Integration section (provider/remote_club_id not in API) Court Dependencies Modal (ClubCourtsTab.tsx): - Display slot_instances_future count (was upcoming_bookings) - Display slot_instances_booked count (new field) - Updated deletion instructions for 3-count structure Types (courts.ts): - Created ClubProfileSettings interface for JSONB structure - Updated ClubProfile to use settings instead of flat fields - Updated CourtDependencies with court_id + 3 dependency counts - Updated ClubProfileUpdateRequest to support settings API Clients: - courts.ts: Updated mock data to match API contracts (settings structure, 3 counts, court_name_duplicate) - materialisation.ts: Flipped USE_MOCKS to false for real backend integration Integration Ready: - All contract mismatches resolved - Build succeeds - Ready for Phase 1 smoke tests against BUILD:290-291 Related: Backend Brooke BUILD:290 (courts CRUD) + BUILD:291 (club profile PATCH) Contract references: docs/owners/payloads/court-api-contract.md, club-profile-api-contract.md --- .drone.yml.back | 152 ------------------ .../clubs/[club_id]/tabs/ClubCourtsTab.tsx | 23 ++- .../clubs/[club_id]/tabs/ClubProfileTab.tsx | 104 ++++++------ src/lib/api/courts.ts | 50 +++--- src/lib/api/materialisation.ts | 4 +- src/types/courts.ts | 45 +++--- 6 files changed, 129 insertions(+), 249 deletions(-) delete mode 100644 .drone.yml.back diff --git a/.drone.yml.back b/.drone.yml.back deleted file mode 100644 index a41ab47..0000000 --- a/.drone.yml.back +++ /dev/null @@ -1,152 +0,0 @@ -kind: pipeline -type: docker -name: default -trigger: - branch: - - master - -steps: -- name: debug-secrets - image: alpine - environment: - VAULT_API_URL: - from_secret: VAULT_API_URL - commands: - - 'echo "Docker Registry URL: $${VAULT_API_URL}"' - when: - event: - - push - - tag - -# Make the image available for next step -- name: publish - image: plugins/docker - settings: - build_args: - - NEXT_PUBLIC_PYTHON_API_URL=https://api.playchoo.com - - NEXT_PUBLIC_AUTH_BACKEND_URL=https://auth.api.playchoo.com - - NEXT_PUBLIC_SWISSOID_TARGET_SERVICE_HANDLE=playchoo - - NEXT_PUBLIC_APP_VERSION=1.0.10 - dockerfile: docker/Dockerfile - context: . - registry: registry.sn48.zivili.ch - repo: registry.sn48.zivili.ch/meow/playchoo-nextjs - tags: - - "amd64-1.0.0" - - "latest" - username: - from_secret: PORTUS_USER - password: - from_secret: PORTUS_PASSWORD - debug: true - launch_debug: true - # make sure to replace image with same tag - force_tag: true - when: - event: - - push - - tag - -- name: deploy - image: registry.sn48.zivili.ch/meow/drone-deploy:amd64-1.0.0 - pull: never - settings: - ssh_port: - from_secret: SSH_PORT - # this is required for the moment to generate the .docker/config.json - # drone is failing to do it on its own at the moment - dockerconfigjson: - from_secret: dockerconfigjson - # use portus or directly docker logins - portus_user: - from_secret: PORTUS_USER - portus_password: - from_secret: PORTUS_PASSWORD - # used by deploy to login to deploy server - ssh_host: - from_secret: SSH_HOST - ssh_user: - from_secret: SSH_USER - ssh_key: - from_secret: SSH_KEY - ssh_fingerprint: - from_secret: SSH_FINGERPRINT - # used by the deploy script to gather all project's .env values from vault - drone_agent1_token: - from_secret: DRONE_AGENT1_TOKEN - # used by deploy script to know where to gather secrets from - vault_api_url: - from_secret: VAULT_API_URL - ---- -kind: secret -name: SSH_HOST -get: - path: kv/data/__drone-admin-secrets - name: SSH_HOST - ---- -kind: secret -name: SSH_USER -get: - path: kv/data/__drone-admin-secrets - name: SSH_USER - ---- -kind: secret -name: SSH_KEY -get: - path: kv/data/__drone-admin-secrets - name: SSH_KEY - ---- -kind: secret -name: DRONE_AGENT1_TOKEN -get: - path: kv/data/__drone-admin-secrets - name: DRONE_AGENT1_TOKEN - ---- -kind: secret -name: VAULT_API_URL -get: - path: kv/data/__drone-admin-secrets - name: VAULT_API_URL - ---- -kind: secret -name: PORTUS_USER -get: - path: kv/data/__drone-admin-secrets - name: PORTUS_USER - ---- -kind: secret -name: PORTUS_PASSWORD -get: - path: kv/data/__drone-admin-secrets - name: PORTUS_PASSWORD - ---- -kind: secret -name: dockerconfigjson -get: - path: kv/data/__drone-admin-secrets - name: dockerconfigjson - -image_pull_secrets: - from_secret: dockerconfigjson - ---- -kind: secret -name: SSH_PORT -get: - path: kv/data/__drone-admin-secrets - name: SSH_PORT - ---- -kind: secret -name: SSH_FINGERPRINT -get: - path: kv/data/__drone-admin-secrets - name: SSH_FINGERPRINT diff --git a/src/app/[locale]/admin/clubs/[club_id]/tabs/ClubCourtsTab.tsx b/src/app/[locale]/admin/clubs/[club_id]/tabs/ClubCourtsTab.tsx index d4e3928..b35f8d7 100644 --- a/src/app/[locale]/admin/clubs/[club_id]/tabs/ClubCourtsTab.tsx +++ b/src/app/[locale]/admin/clubs/[club_id]/tabs/ClubCourtsTab.tsx @@ -276,7 +276,7 @@ function CourtFormModal({ clubId, court, onClose, onSuccess }: CourtFormModalPro if (nameError) { setFieldError(nameError.message); } - } else if (result.error.code === 'duplicate_court_name') { + } else if (result.error.code === 'court_name_duplicate') { setFieldError(result.error.detail); } else { setError(result.error.detail); @@ -496,11 +496,19 @@ function DependenciesBlockingModal({ court, dependencies, onClose }: Dependencie )} - {dependencies.dependencies.upcoming_bookings > 0 && ( + {dependencies.dependencies.slot_instances_future > 0 && (
- Upcoming bookings: + Future slot instances: - {dependencies.dependencies.upcoming_bookings} + {dependencies.dependencies.slot_instances_future} + +
+ )} + {dependencies.dependencies.slot_instances_booked > 0 && ( +
+ Booked slot instances: + + {dependencies.dependencies.slot_instances_booked}
)} @@ -514,8 +522,11 @@ function DependenciesBlockingModal({ court, dependencies, onClose }: Dependencie {dependencies.dependencies.slot_definitions > 0 && (
  • Delete or reassign all slot definitions
  • )} - {dependencies.dependencies.upcoming_bookings > 0 && ( -
  • Cancel or move all upcoming bookings
  • + {dependencies.dependencies.slot_instances_future > 0 && ( +
  • Delete future slot instances
  • + )} + {dependencies.dependencies.slot_instances_booked > 0 && ( +
  • Cancel or move all booked slot instances
  • )} diff --git a/src/app/[locale]/admin/clubs/[club_id]/tabs/ClubProfileTab.tsx b/src/app/[locale]/admin/clubs/[club_id]/tabs/ClubProfileTab.tsx index c5b898c..8be2c58 100644 --- a/src/app/[locale]/admin/clubs/[club_id]/tabs/ClubProfileTab.tsx +++ b/src/app/[locale]/admin/clubs/[club_id]/tabs/ClubProfileTab.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; -import { Loader2, AlertCircle, CheckCircle } from 'lucide-react'; +import { Loader2, AlertCircle, CheckCircle, Info } from 'lucide-react'; import { getClubProfile, updateClubProfile } from '@/src/lib/api/courts'; import type { ClubProfile, ClubProfileUpdateRequest} from '@/src/types/courts'; import { COMMON_TIMEZONES, isValidEmail, isValidUrl } from '@/src/types/courts'; @@ -43,17 +43,17 @@ export default function ClubProfileTab({ clubId, onUpdate }: ClubProfileTabProps const prof = result.data; setProfile(prof); - // Populate form + // Populate form - read from settings structure setName(prof.name); setTimezone(prof.timezone); - setAddressLine1(prof.address_line_1 || ''); - setAddressLine2(prof.address_line_2 || ''); - setCity(prof.city || ''); - setPostalCode(prof.postal_code || ''); - setCountry(prof.country || ''); - setPhone(prof.phone || ''); - setEmail(prof.email || ''); - setWebsite(prof.website || ''); + setAddressLine1(prof.settings?.address?.line_1 || ''); + setAddressLine2(prof.settings?.address?.line_2 || ''); + setCity(prof.settings?.address?.city || ''); + setPostalCode(prof.settings?.address?.postal_code || ''); + setCountry(prof.settings?.address?.country || ''); + setPhone(prof.settings?.contact?.phone || ''); + setEmail(prof.settings?.contact?.email || ''); + setWebsite(prof.settings?.contact?.website || ''); setError(null); } else { @@ -97,17 +97,29 @@ export default function ClubProfileTab({ clubId, onUpdate }: ClubProfileTabProps setError(null); setSuccess(false); + // Build settings structure, preserving existing settings + const updatedSettings = { + ...profile?.settings, + address: { + ...profile?.settings?.address, + line_1: addressLine1.trim() || undefined, + line_2: addressLine2.trim() || undefined, + city: city.trim() || undefined, + postal_code: postalCode.trim() || undefined, + country: country.trim() || undefined, + }, + contact: { + ...profile?.settings?.contact, + phone: phone.trim() || undefined, + email: email.trim() || undefined, + website: website.trim() || undefined, + }, + }; + const request: ClubProfileUpdateRequest = { name: name.trim(), timezone, - address_line_1: addressLine1.trim() || undefined, - address_line_2: addressLine2.trim() || undefined, - city: city.trim() || undefined, - postal_code: postalCode.trim() || undefined, - country: country.trim() || undefined, - phone: phone.trim() || undefined, - email: email.trim() || undefined, - website: website.trim() || undefined, + settings: updatedSettings, }; const result = await updateClubProfile(clubId, request); @@ -143,17 +155,17 @@ export default function ClubProfileTab({ clubId, onUpdate }: ClubProfileTabProps function handleCancel() { if (!profile) return; - // Reset form to original values + // Reset form to original values - read from settings structure setName(profile.name); setTimezone(profile.timezone); - setAddressLine1(profile.address_line_1 || ''); - setAddressLine2(profile.address_line_2 || ''); - setCity(profile.city || ''); - setPostalCode(profile.postal_code || ''); - setCountry(profile.country || ''); - setPhone(profile.phone || ''); - setEmail(profile.email || ''); - setWebsite(profile.website || ''); + setAddressLine1(profile.settings?.address?.line_1 || ''); + setAddressLine2(profile.settings?.address?.line_2 || ''); + setCity(profile.settings?.address?.city || ''); + setPostalCode(profile.settings?.address?.postal_code || ''); + setCountry(profile.settings?.address?.country || ''); + setPhone(profile.settings?.contact?.phone || ''); + setEmail(profile.settings?.contact?.email || ''); + setWebsite(profile.settings?.contact?.website || ''); setErrors({}); setError(null); } @@ -260,7 +272,15 @@ export default function ClubProfileTab({ clubId, onUpdate }: ClubProfileTabProps {/* Location */}
    -

    Location

    +
    +

    Location

    +
    + +
    + Stored in settings until native fields ship +
    +
    +
    {/* Address Line 1 */} @@ -348,7 +368,15 @@ export default function ClubProfileTab({ clubId, onUpdate }: ClubProfileTabProps {/* Contact */}
    -

    Contact

    +
    +

    Contact

    +
    + +
    + Stored in settings until native fields ship +
    +
    +
    {/* Phone */} @@ -415,24 +443,6 @@ export default function ClubProfileTab({ clubId, onUpdate }: ClubProfileTabProps
    - {/* Integration (read-only) */} -
    -

    Integration

    - -
    -
    - Provider: - {profile.provider} -
    - {profile.provider !== 'local' && profile.remote_club_id && ( -
    - Remote Club ID: - {profile.remote_club_id} -
    - )} -
    -
    - {/* Actions */}