After converting ServiceView to StatefulWidget, the build() method and
helper callbacks still referenced 'type' as a bare name instead of
widget.type, causing compilation failure.
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>
- day: usar formato ISO 8601 "yyyy-MM-dd" — @IsDateString() rechazaba
el formato de DateTime.toString() que usa espacio en vez de T
- location_preference: enviar string "office"/"delivery" con la clave
correcta — se enviaba 'location' con un entero (enum index)
- Construir el payload directamente en vez de usar Service.toDocument()
que no coincide con CreateServiceDto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- calendar_view: separar carga del profesional (1 vez) de la carga de
servicios (por cada cambio de día), eliminando re-fetches innecesarios
- calendar_view: usar DateTime local en vez de UTC para evitar desfase
de fecha en zonas horarias (Colombia UTC-5)
- calendar_view: excluir explícitamente cancelled/denied en _isOccupied
y refactorizar los 4 tipos de card en un único método _slotCard
- dashboard_view: usar slotDurationMinutes del profesional en range1Hour2
en vez de 2 horas hardcodeadas, respetando la duración real del slot
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>
- Muestra marcadores azules en el mapa para profesionales con coordenadas
reales y modalidad presencial (office/both), filtrados por ciudad del paciente.
Tocar un marcador navega directamente al calendario del profesional.
- Agrega tabla de coordenadas hardcodeadas para 30 ciudades colombianas como
fallback inmediato en _centerOnUserCity, evitando que el mapa cargue en
Medellín cuando la Geocoding API no está habilitada en el proyecto GCP.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_initMaps ahora geocodifica la ciudad del perfil del usuario (_centerOnUserCity)
antes de mostrar el mapa. Así el mapa arranca en Bucaramanga si el usuario
es de Bucaramanga, y el autocomplete ya tiene coordenadas correctas desde
el primer keystroke, sin esperar GPS.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Default _mapCenter era Medellín (nuevo fallback neutro) pero se usaba como
bias de coordenadas en el autocomplete antes de que GPS/geocoding terminara.
Ahora:
- _mapPositioned=false hasta que GPS o _centerOnUserCity actualice el centro
- Autocomplete omite parámetro location hasta que _mapPositioned=true
- Mientras tanto, la ciudad del usuario se agrega al query de texto para biasear
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _requestService: omite validación de dirección si locationPreferences=office
- Usa la dirección y coordenadas del consultorio del profesional en ese caso
- Botón y warnings de ciudad deshabilitados solo en modo domicilio/ambos
- Tarjeta inferior muestra dirección del consultorio cuando es office-only
- location_preference del servicio refleja correctamente office vs delivery
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _mapCenter default cambiado de Bogotá a coordenadas más neutras
- Cuando GPS no está disponible/denegado, _centerOnUserCity() geocodifica
la ciudad del perfil del usuario y centra el mapa ahí
- El autocomplete usa _mapCenter como bias → resultados ahora corresponden
a la ciudad del usuario, no a Bogotá hardcodeado
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>
- 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>
GET /services/professional returns all statuses including cancelled.
After unblocking (self_booked → cancelled), _isOccupied matched the
cancelled service because it only excluded selfBooked.
Exclude cancelled and denied from _isOccupied so unblocked slots
correctly revert to Disponible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CalendarView now treats self_booked services as unavailable:
- _isBlocked() filters slots with selfBooked status from the range list
- _isHora1Ocupada() excludes selfBooked so they don't show as Ocupado
- Blocked slots render as grey "No disponible" with block icon
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>
Without a timeout, a hanging backend request left the schedule page showing
an infinite spinner forever. Timeout ensures any hung request fails fast and
the spinner resolves. The catchError fallback in schedule_view handles the
edge case where getProfessional rejects unexpectedly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When toggling off jornada continua, range1Hour2 defaults to 12:00 and
range2Hour1 defaults to 14:00 if not already set. Existing values are
preserved so the user can still edit them freely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a day is toggled on and has no hours set yet, automatically
apply continuous-day defaults (08:00 - 18:00) so the user can
save immediately without having to pick times manually.
Also clear fp.profesional in initState to ensure fresh data loads.
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>
- professional_calendar_view: styled TableCalendar with app colors,
day header with date block + stats pills (occupied/available),
time slot cards with accent strip + status icon, clean empty state
- services_requests_view: card list with status-colored accent strips,
avatar initials fallback, date/time row, colored status badges with
icons for all ServiceStatus values, proper empty state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- professional_profile_view: gradient header card with avatar overlay,
sectioned cards (service modality, address, rate, payment chips, schedule),
animated payment method chips, full-width save button with loading state
- sidebar: teal-dark background in pro mode vs navy in user mode,
animated mode indicator pill, muted toggle button in pro mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Stop swallowing DELETE errors silently; show snackbar instead
- After successful reset, reload professional from server and notify providers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add missing api_service.dart import (without it the file fails to compile)
- Initialize _loadingSettings = false so button renders before async settings load
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Usuario model: proRejectedAt parsed from professionals.updated_at
- _StatusCard: shows countdown or retry button based on rejection_wait_days setting
- Retry clears the rejected record via DELETE /professionals/me
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows document type locked to 'Cédula de ciudadanía' and adds required
number input field above the photo upload.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
context.read doesn't subscribe to changes, so the form stayed visible
after submitForReview even though AuthProvider was updated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Profesional.empty() factory for users with no professional record
- initState: catch error from getProfessional and show empty form instead of infinite spinner
- Backend upsert: create professional record if none exists (no longer throws 404)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Display: only prepend + if number doesn't already start with +
URL: strip + and spaces so WhatsApp API receives digits only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Profile view: gradient hero banner with overlapping avatar, status badges (phone verified, email, pro state), gender field, city from flat API endpoint, manual validation
- nginx: JS files use no-cache/must-revalidate instead of 1y immutable to prevent stale deploys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Solo se puede seleccionar de la lista de ciudades registradas
- GPS detecta ciudad → si está en la lista: preselecciona y muestra banner verde
- GPS detecta ciudad → si NO está en lista: banner amarillo con mensaje de no disponibilidad
- No hay campo de texto libre, solo selección de lista
- Botón muestra "Continuar con [Ciudad]" solo cuando hay selección
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Carga ciudades de GET /locations/cities/all
- Detecta ciudad via GPS → Nominatim
- Muestra ciudad GPS detectada en banner verde
- Preselecciona automáticamente en la lista si coincide
- Buscador en tiempo real con filtro
- Ciudad GPS sin seleccionar muestra badge "GPS"
- Botón deshabilitado hasta seleccionar una ciudad
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>
- Grid responsivo (1-4 columnas según ancho)
- Tarjeta con header gradiente, avatar centrado superpuesto y badge de score
- Modo compacto (lista horizontal) en pantallas pequeñas
- Buscador en tiempo real por nombre, profesión o ciudad
- Chips de métodos de pago (Datáfono, Nequi, Transferencia)
- Dark mode completo, avatar con inicial como fallback
- Estado vacío con ícono y mensaje contextual
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- logo.dart: path correcto assets/prosapp-logo.png + errorBuilder
- services_view.dart: rediseño con tarjetas, dark mode, avatar con inicial, badges de estado
- services_history_view.dart: mismo rediseño + badges completado/cancelado/rechazado
- request_professional_view.dart: secciones con cards, botones de upload con estado visual,
estados pending/rejected mejorados, dark mode completo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Navbar: reemplaza hoverColor deprecated por IconButton.styleFrom(overlayColor)
- Support: rediseño completo con tarjetas, secciones y manejo correcto de URLs vacías
- Políticas/Términos: carga desde GET /settings/policies y muestra en dialog in-app
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>