Without notifyListeners() after isLoading=true, the Consumer would
briefly show stale data (or 'service not found') from a previous
navigation before the API call completes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- When professional accepts 'both' modes, add office/delivery toggle so
the patient can explicitly choose instead of defaulting to delivery.
Reset _bookAsDelivery when a professional is selected; office-only
professionals still force office, delivery-only force delivery.
- Fix 'sin servicio' on ServiceView: getServiceForUser was calling
/users/:professionalId using the professionals-table UUID (not user UUID),
returning 404 and setting service=null. Use the embedded professionals.users
data from findById instead.
- Convert ServiceView to StatefulWidget; fetch in initState to avoid
re-fetching on every parent rebuild.
- Remove client-side FCM notification from _requestService; backend
create() now notifies the professional directly with the server key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Service.fromJson: lat/lng con Decimal de Prisma llegaban como string y
el cast 'as num?' lanzaba TypeError silencioso → lista de servicios vacía.
Mismo patrón que average_score, fix: double.tryParse(?.toString())
- service_status: agregar enumToStringService que mapea el enum de Flutter
al string que espera el DTO del backend ('pending','accepted', etc.).
changeServiceStatus enviaba el índice entero → @IsEnum rechazaba → el
profesional no podía aceptar ni rechazar solicitudes
- maps_service: agregar MapsService.reverseGeocode que usa el Geocoder del
Maps JS SDK ya cargado, sin requerir la Geocoding API habilitada por separado
- dashboard_view: _reverseGeocode usa el JS Geocoder en vez del endpoint HTTP
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Agrega getters patientLat/patientLng al ProfessionalsProvider
- Calcula distancia Haversine entre la ubicación del paciente y el consultorio
del profesional, mostrándola en azul (ej: "1.3 km" o "800 m") en la card
- Muestra la dirección del consultorio (icono de tienda) en ambos layouts:
horizontal (compact) y vertical (grid)
- Distancia solo visible cuando el paciente tiene coordenadas registradas
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Clear button (✕) now resets _cityMismatch and _detectedCity so banner and CTA unblock
- _citiesMatch: replace bidirectional contains with equality + word-prefix check to prevent 'Cali' matching 'Calima'
- ProfessionalsProvider: track _currentUserId to reset state on user change (cross-user leak)
- ProfessionalsProvider: store _lastSearch so setLocationContext preserves active search term
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Professionals filtered by user city + proximity (lat/lng) to selected address
- ProfessionalsProvider: setLocationContext(city, lat, lng) updates filter and reloads
- Dashboard: detect city mismatch after reverse geocode, block booking if wrong city
- Dashboard: autocomplete biased toward user's city
- Dashboard: setLocationContext called before navigating to professionals list
- Dashboard: keyboard no longer shifts map layout on tablet (MediaQuery viewInsets=0)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- getProfessionals() now accepts optional search param sent to ?search=
- updateCity only triggers the initial load (city no longer used as filter)
- Search field debounces 450ms before firing API call
- TextEditingController added so clear button also resets the field
- Client-side filtering removed (API now handles it and returns top 7)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
updateCity skipped getProfessionals() when both _city and the incoming
city were null (null != null = false). Added _initialized flag so the
first call always triggers the load regardless of city value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove fp.clear() from profile initState: if profesional is already
loaded (e.g. navigating from schedule page) the form renders instantly
without a spinner; if null the Consumer already shows the spinner.
- Add .catchError() so the spinner always resolves even on API failure.
- Remove redundant GET /professionals/me reload inside updateProfesionalProfileInfo:
data is already correct from copyProfesionalWith, so the save button
no longer waits for an extra round-trip after the PATCH.
- Wrap save button in try/finally so _saving always resets to false.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
isProModeActive was in-memory only — a browser refresh reset it to
false even when the URL was a professional route.
Fix: read/write 'isProModeActive' from SharedPreferences in
ProfessionalProvider (constructor, toggleProMode, setIsProModeActive,
logout). Also clear the flag in AuthProvider.isAuthenticated() when the
user's proState is not active, so a stale value can't trap a non-pro user
in professional mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ServicesProvider: add blockSlot() calling POST /services/block and
unblockSlot() calling PATCH /services/:id/status with cancelled
- ProfessionalCalendarView:
- Fix _services always-null bug: getServicesForProfessional() returns
void; now reads from sp.services after it resolves
- Differentiate self_booked (blocked) vs regular occupied slots
- Show amber 'Bloqueado' state with lock icon for self_booked services
- Show lock button on available slots → confirm dialog → blockSlot()
- Show unlock button on blocked slots → confirm dialog → unblockSlot()
- Reload calendar after block/unblock so UI reflects new state
- Header stats now show separate counts: ocupadas / bloqueadas / libres
- _ActionButton widget for reusable tap-target icon buttons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Critical fixes for the scheduling flow:
- ScheduleEntity: add toScheduleDto() with snake_case keys, day_of_week,
and HH:MM zero-padded format required by the backend DTO @Matches validator
- Schedules: add toSchedulesArray() converting the day-keyed object to the
array format expected by PATCH /professionals/me/schedules
- ProfessionalFormProvider: updateProfesionalProfileScheduleInfo now calls
the correct endpoint (/professionals/me/schedules) instead of /professionals/me
which was stripping the schedules field via ValidationPipe whitelist
- Service.formatTimeOfDay: zero-pad hours and minutes so POST /services passes
the @Matches(/^\d{2}:\d{2}$/) DTO validation
- ProfessionalProvider: add getProfessionalById() calling /professionals/:id
(public endpoint) to load any professional's data, not the viewer's own
- CalendarServicesProvider: add getPublicServicesForProfessional() calling
/services/public-calendar/:id so the user calendar shows the target
professional's booked slots, not the viewer's own services
- CalendarView: use getProfessionalById + getPublicServicesForProfessional
so the calendar correctly reflects the selected professional's schedule
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prisma serializes Decimal fields (rate, latitude, longitude) as JSON
strings, causing 'as num?' casts to throw in fromDocument(). The catch
block in getProfessional() then returned an empty Profesional, so saving
overwrote all existing data with empty strings.
Also added try-catch in all patch API calls to surface errors to user
instead of silently failing with a stuck loading button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use GET /professionals/me (JWT-authenticated) instead of /professionals/{uid}
to avoid silent failures when the user_id lookup fails
- After save, re-fetch from server so fp.profesional reflects confirmed server state
on the next page navigation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix typo aditional_address -> additional_address in toDocument() and fromDocument()
- Clear ProfessionalFormProvider before fetch in initState() so TextFormField initialValue always applies to fresh data
- Add clear() to ProfessionalFormProvider; call it on logout to avoid leaking data between users
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After logout or on app start with no/invalid token, the browser URL
might be #/dashboard or #/auth/login. Using addPostFrameCallback to
navigate to phoneLoginRoute once the navigator is ready ensures the
user always lands on the phone login screen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Eliminado Flutter Form widget, validación manual por campo
- submitForReview() en provider sin depender de formKey
- Errores inline visibles (cédula obligatoria, profesión obligatoria)
- Loading spinner en el botón mientras envía
- try/catch con snackbar de error si falla la API
- Orden de campos: foto cédula, profesión, RETHUS (opcional), especializaciones (opcional)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- professional_form_provider: agregar rethusValidated al constructor Profesional
- professional_provider: agregar rethusCode y rethusValidated al fallback del constructor
- profile_view: quitar await de refreshUser() que retorna void
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Navbar: hoverColor tenue por tema (azul/blanco) en vez del gris por defecto de Flutter web
- SettingsProvider: lazy:false + timeout 10s para evitar spinner gris permanente en soporte
- SupportView: muestra contenido con defaults inmediatamente sin bloquear en isLoading
- Modelo Profesional: campos rethus_code y rethus_validated
- ProfessionalFormProvider: soporte para rethus_code en copyProfesionalWith
- RequestProfessionalView: campo RETHUS opcional en formulario + fix color dropdown profesión
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Detectar usuario nuevo (nombre == telefono) y redirigir
a pantalla setup-name antes del dashboard
- SetupNameView: bienvenida con campo de nombre obligatorio
- AuthProvider.updateName() para guardar via PATCH /users/me
- NavbarAvatar: iniciales con color por inicial del nombre
en lugar de no-image.jpg cuando no hay foto
- Perfil: mismo avatar de iniciales en la pantalla de perfil
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CitiesProvider.isCityAvailable() y findMatchingCity() con comparacion flexible
- Al mover el mapa extrae la ciudad del geocodificado inverso
- Si la ciudad no esta en la lista muestra aviso amarillo y bloquea solicitud
- Boton de solicitar deshabilitado cuando ciudad no esta disponible
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ThemeProvider con persistencia en SharedPreferences
- Toggle animado en sidebar (sol/luna con switch visual)
- Temas light y dark completos en MaterialApp
- Campana en navbar con badge rojo y panel inferior de notificaciones
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Service.fromJson: string status/location enums, ISO8601 time parsing, additional_address field name
- _loadServices: unwrap {data:[...]} paginated response; extract embedded user from response instead of extra API calls
- calendar_services_provider: same response unwrap; fallback user when not embedded
- Add ProState import where needed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>