The two calendars each built their own list of hours. The patient's
enforced a 3-hour booking lead time; the professional's did not, so at
15:00 an 08:00-18:00 agenda reported "3 libres" (15, 16, 17) that no
patient could actually take.
Both now go through SlotGenerator. The professional still sees the
near-term hours (blocking the next hour is legitimate) but they are
labelled "Sin reserva" and excluded from the "libres" count, so the
number on his agenda means what the patient sees.
Also in this pass:
- endOf() clamps the slot end at 23:59; a 120-minute slot booked at
23:00 was sending "25:00" to the backend.
- ScheduleEntity.copyWith can set an hour back to null, so returning a
day to jornada continua no longer keeps the split hours around.
- Removed the Firebase-era schedule map ('habilitado', 'range1Hour1')
together with the dead ProfessionalEntity.fromDocument that fed it.
- Settings loads guard on mounted and swallow failures instead of
calling setState after dispose.
- "Pedir cita" says what is missing instead of doing nothing.
Tests: 9 passing, including the clamp and the empty/inverted ranges.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/services/public-calendar/{professionalId} exists after all — the web has been
using it all along. My earlier probes missed it because they guessed the URL
shape, and because it is keyed by the professionals row id rather than the
owning user id, which ProfessionalEntity was discarding.
Adds ProfessionalEntity.recordId and points the client calendar at the public
endpoint. Until now a client browsing a professional's availability was served
their own agenda, so every slot looked free and two people could book the same
hour. A failed load now hides the slots and offers a retry instead of showing
them as available.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DateTime.toString() renders "2026-07-13 00:00:00.000Z" while the backend
sends "2026-07-13T00:00:00.000Z", so every day comparison was false and no
booked slot was ever detected. Both calendars showed fully free days.
The client calendar now also treats an unloaded agenda as busy instead of
free: rendering a taken slot as available leads straight to a double booking.
Note: the client still reads /services/professional/calendar, which returns
the *caller's* calendar. There is no endpoint for another professional's
agenda, so a client cannot yet see which of that professional's slots are
taken. That needs a backend endpoint.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Root cause behind most "stuck loading" reports: the backend changed shape
(schedules became an array, location_preferences a string) while the mobile
parser still hard-cast to Map/int. The TypeError was swallowed by a silent
catch that returned null, and screens only handled the success state, so a
parse failure rendered as a permanent spinner. Same class of bug appeared
across service lists via non-null map lookups and a total absence of
request timeouts.
Ported from prosappweb:
- in-app suggestions (POST /suggestions)
- policies/terms from GET /settings/policies
- configurable appointment length (slot_duration_minutes)
- block/unblock calendar slots (POST /services/block)
- GPS city detection on the profile (Nominatim)
- server-side professional search with haversine distance
- retry cooldown after a rejected professional application
Reliability:
- parse schedules array (day_of_week 0=Mon) and string location_preferences
- read times as wall clock, so 08:00 stays 08:00 across timezones
- carry minutes into hours in TimeOfDay.add; a minute-based step used to
loop forever and freeze the calendar (covered by test/time_slots_test.dart)
- semver update check instead of string equality, which blocked every build
that did not exactly match the configured version
- request timeouts across all repositories
- surface HTTP >= 400 instead of reporting failed writes as success
- error states with retry instead of an indefinite shimmer
Includes pre-existing uncommitted work from the UI redesign.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Replace all Firebase* repositories with Api* repositories using HTTP + SharedPreferences JWT
- Remove Firebase.initializeApp() and firebase_messaging background handler from main.dart
- Update DI (app_di.dart) to inject Api* repositories instead of Firebase* ones
- Replace all Timestamp/cloud_firestore usage with ISO 8601 String dates
- Stub PhoneVerificationService (Firebase phone OTP → backend OTP when implemented)
- Add ApiService singleton with JWT management in lib/services/
- Legacy firebase_*_repository.dart files preserved for Fase 4 cleanup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>