Files
prosappco/lib/screens/professional/professional_schedule_screen.dart
T
Lizandro GuarnizoandClaude Opus 5 0a8e5d11a2 fix(agenda): one slot generator for professional and patient
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>
2026-08-25 13:17:03 -05:00

172 lines
5.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:injector/injector.dart';
import 'package:professional_repository/professional_repository.dart';
import 'package:prosappco/components/general_primary_button.dart';
import 'package:prosappco/screens/professional/components/schedule_item.dart';
import 'package:prosappco/utils/schedule_validation.dart';
class ProfessionalScheduleScreen extends StatefulWidget {
Schedules schedules;
ProfessionalScheduleScreen({
super.key,
required this.schedules,
});
@override
State<ProfessionalScheduleScreen> createState() =>
_ProfessionalScheduleScreenState();
}
class _ProfessionalScheduleScreenState
extends State<ProfessionalScheduleScreen> {
final _repo = Injector.appInstance.get<ApiProfessionalRepository>();
Schedules schedules = Schedules.empty;
bool _saving = false;
bool _dirty = false;
@override
void initState() {
super.initState();
schedules = widget.schedules;
}
void _update(Schedules next) {
setState(() {
schedules = next;
_dirty = true;
});
}
/// Saving now writes to the backend instead of just handing the object back:
/// the old flow depended on the profile screen being saved afterwards, so
/// leaving without that second save silently discarded everything.
Future<void> _save() async {
final problems = validateSchedules(schedules);
if (problems.isNotEmpty) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(problems.first)),
);
return;
}
setState(() => _saving = true);
try {
await _repo.updateSchedules(schedules);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Horario guardado')),
);
Navigator.of(context).pop(schedules);
} catch (e) {
if (!mounted) return;
setState(() => _saving = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'No se pudo guardar el horario. Revisa tu conexión e inténtalo de nuevo.')),
);
}
}
Future<bool> _confirmLeave() async {
if (!_dirty || _saving) return true;
final leave = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('¿Salir sin guardar?'),
content: const Text('Perderás los cambios que hiciste en tu horario.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Seguir editando')),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Salir sin guardar')),
],
),
);
return leave ?? false;
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvoked: (didPop) async {
if (didPop) return;
if (await _confirmLeave() && mounted) Navigator.of(context).pop();
},
child: Scaffold(
appBar: AppBar(
title: const Text('Horario'),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Column(
children: [
ScheduleItem(
label: "Lunes",
schedule: schedules.monday,
onChanged: (s) {
_update(schedules.copyWith(monday: s));
},
),
ScheduleItem(
label: "Martes",
schedule: schedules.tuesday,
onChanged: (s) {
_update(schedules.copyWith(tuesday: s));
},
),
ScheduleItem(
label: "Miercoles",
schedule: schedules.wednesday,
onChanged: (s) {
_update(schedules.copyWith(wednesday: s));
},
),
ScheduleItem(
label: "Jueves",
schedule: schedules.thursday,
onChanged: (s) {
_update(schedules.copyWith(thursday: s));
},
),
ScheduleItem(
label: "Viernes",
schedule: schedules.friday,
onChanged: (s) {
_update(schedules.copyWith(friday: s));
},
),
ScheduleItem(
label: "Sabado",
schedule: schedules.saturday,
onChanged: (s) {
_update(schedules.copyWith(saturday: s));
},
),
ScheduleItem(
label: "Domingo",
schedule: schedules.sunday,
onChanged: (s) {
_update(schedules.copyWith(sunday: s));
},
),
const SizedBox(height: 20),
GeneralPrimaryButton(
label: _saving ? "Guardando…" : "Guardar",
onPressed: _saving ? () {} : _save,
),
const SizedBox(height: 20),
],
),
),
),
),
);
}
}