configurable slot duration: UI picker + genRanges stepMinutes

- Fix TimeOfDayExtension.add() to handle minute overflow via total-minutes math
- genRanges() now takes stepMinutes param (default 30, clamp 5-480)
- Profesional model: slotDurationMinutes field (parsed from slot_duration_minutes)
- ProfessionalFormProvider: saves slot duration alongside schedules on Guardar
- schedule_view: _SlotDurationPicker chip selector (15/20/30/45/60/90/120 min)
- ProfessionalCalendarView + CalendarView: pass slotDurationMinutes to genRanges

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-12 19:25:08 -05:00
co-authored by Claude Sonnet 4.6
parent 46acad0b55
commit 920ae7144d
7 changed files with 105 additions and 20 deletions
+63 -2
View File
@@ -11,6 +11,63 @@ import 'package:flutter/material.dart';
import 'package:prosapp_web_app/ui/shared/widgets/schedule_day_tile.dart';
import 'package:provider/provider.dart';
// ── Slot duration options ──────────────────────────────────────────────────
const _kSlotOptions = [
(15, '15 min'),
(20, '20 min'),
(30, '30 min'),
(45, '45 min'),
(60, '1 hora'),
(90, '1 h 30'),
(120, '2 horas'),
];
class _SlotDurationPicker extends StatelessWidget {
final int value;
final ValueChanged<int> onChanged;
const _SlotDurationPicker({required this.value, required this.onChanged});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Duración de cada cita',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: _kSlotOptions.map((opt) {
final (mins, label) = opt;
final selected = value == mins;
return ChoiceChip(
label: Text(label),
selected: selected,
onSelected: (_) => onChanged(mins),
selectedColor: Colors.blue.shade400,
labelStyle: TextStyle(
color: selected ? Colors.white : null,
fontWeight: selected ? FontWeight.w700 : FontWeight.normal,
),
);
}).toList(),
),
const SizedBox(height: 4),
Text(
'Los usuarios verán bloques de $value min en el calendario.',
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
),
],
),
);
}
}
class ScheduleView extends StatefulWidget {
const ScheduleView({super.key});
@@ -540,9 +597,13 @@ class _ScheduleViewState extends State<ScheduleView> {
);
},
),
const Divider(
height: 20,
const Divider(height: 20),
_SlotDurationPicker(
value: profesional.slotDurationMinutes,
onChanged: (v) => professionalFormProvider
.copyProfesionalWith(slotDurationMinutes: v),
),
const Divider(height: 20),
Container(
margin: const EdgeInsets.only(top: 10),
child: ConstrainedBox(