fix: corregir flujo de agendamiento del paciente
- 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>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
387b647abc
commit
6c248e1f98
+224
-360
@@ -7,7 +7,6 @@ import 'package:prosapp_web_app/models/service_status.dart';
|
|||||||
import 'package:prosapp_web_app/providers/professional_form_provider.dart';
|
import 'package:prosapp_web_app/providers/professional_form_provider.dart';
|
||||||
import 'package:prosapp_web_app/providers/professional_provider.dart';
|
import 'package:prosapp_web_app/providers/professional_provider.dart';
|
||||||
import 'package:prosapp_web_app/providers/calendar_services_provider.dart';
|
import 'package:prosapp_web_app/providers/calendar_services_provider.dart';
|
||||||
import 'package:prosapp_web_app/services/notifications_service.dart';
|
|
||||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||||
import 'package:prosapp_web_app/utils/time_of_day_extension.dart';
|
import 'package:prosapp_web_app/utils/time_of_day_extension.dart';
|
||||||
import 'package:prosapp_web_app/utils/time_of_day_utils.dart';
|
import 'package:prosapp_web_app/utils/time_of_day_utils.dart';
|
||||||
@@ -24,417 +23,282 @@ class CalendarView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CalendarViewState extends State<CalendarView> {
|
class _CalendarViewState extends State<CalendarView> {
|
||||||
List<Service>? _services;
|
// Use local time throughout — UTC dates can shift the calendar date near midnight
|
||||||
DateTime today = DateTime.now();
|
DateTime _today = DateTime.now();
|
||||||
late int numDay;
|
Profesional? _profesional;
|
||||||
|
List<Service> _services = [];
|
||||||
|
bool _loadingPro = true;
|
||||||
|
bool _loadingServices = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_normalizeDay();
|
||||||
today = DateTime.utc(today.year, today.month, today.day);
|
_fetchProfessional();
|
||||||
numDay = today.weekday;
|
|
||||||
|
|
||||||
_fetchProfessionalAndServices();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _fetchProfessionalAndServices() async {
|
void _normalizeDay() {
|
||||||
final professionalFormProvider =
|
// Strip time component; keep local date only
|
||||||
Provider.of<ProfessionalFormProvider>(context, listen: false);
|
_today = DateTime(_today.year, _today.month, _today.day);
|
||||||
final servicesProvider =
|
|
||||||
Provider.of<CalendarServicesProvider>(context, listen: false);
|
|
||||||
final proProvider =
|
|
||||||
Provider.of<ProfessionalProvider>(context, listen: false);
|
|
||||||
|
|
||||||
// Use public endpoint so we get THIS professional's schedule, not the viewer's own
|
|
||||||
final professional =
|
|
||||||
await proProvider.getProfessionalById(widget.professionalId);
|
|
||||||
professionalFormProvider.setProfesional(professional);
|
|
||||||
|
|
||||||
// Use public calendar endpoint to get services for this specific professional
|
|
||||||
final services =
|
|
||||||
await servicesProvider.getPublicServicesForProfessional(professional.id);
|
|
||||||
setState(() {
|
|
||||||
_services = services;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDaySelected(DateTime day, DateTime focusedDay) {
|
// Loads professional data once — does NOT re-run on day change
|
||||||
|
Future<void> _fetchProfessional() async {
|
||||||
|
setState(() => _loadingPro = true);
|
||||||
|
try {
|
||||||
|
final proProvider = Provider.of<ProfessionalProvider>(context, listen: false);
|
||||||
|
final fp = Provider.of<ProfessionalFormProvider>(context, listen: false);
|
||||||
|
final pro = await proProvider.getProfessionalById(widget.professionalId);
|
||||||
|
fp.setProfesional(pro);
|
||||||
|
if (mounted) setState(() { _profesional = pro; _loadingPro = false; });
|
||||||
|
await _fetchServices();
|
||||||
|
} catch (_) {
|
||||||
|
if (mounted) setState(() => _loadingPro = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches only services — called on day change and after initial load
|
||||||
|
Future<void> _fetchServices() async {
|
||||||
|
if (_profesional == null) return;
|
||||||
|
setState(() => _loadingServices = true);
|
||||||
|
try {
|
||||||
|
final sp = Provider.of<CalendarServicesProvider>(context, listen: false);
|
||||||
|
final list = await sp.getPublicServicesForProfessional(_profesional!.id);
|
||||||
|
if (mounted) setState(() { _services = list; _loadingServices = false; });
|
||||||
|
} catch (_) {
|
||||||
|
if (mounted) setState(() => _loadingServices = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDaySelected(DateTime day, DateTime _) {
|
||||||
setState(() {
|
setState(() {
|
||||||
today = day;
|
_today = DateTime(day.year, day.month, day.day);
|
||||||
numDay = today.weekday;
|
|
||||||
});
|
});
|
||||||
_fetchProfessionalAndServices();
|
// Professional data is cached — only re-fetch services
|
||||||
|
_fetchServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<ProfessionalFormProvider>(
|
if (_loadingPro || _profesional == null) {
|
||||||
builder: (context, professionalFormProvider, child) {
|
return const Center(child: CircularProgressIndicator());
|
||||||
if (professionalFormProvider.profesional == null) {
|
}
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final profesional = professionalFormProvider.profesional!;
|
return ListView(
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
return ListView(
|
children: [
|
||||||
physics: const ClampingScrollPhysics(),
|
Center(
|
||||||
children: [
|
child: ConstrainedBox(
|
||||||
Center(
|
constraints: const BoxConstraints(maxWidth: 900),
|
||||||
child: ConstrainedBox(
|
child: WhiteCard(
|
||||||
constraints: const BoxConstraints(maxWidth: 900),
|
title: 'Calendario',
|
||||||
child: WhiteCard(
|
child: Column(
|
||||||
title: 'Calendario',
|
children: [
|
||||||
child: Column(
|
TableCalendar(
|
||||||
children: [
|
locale: 'es_CO',
|
||||||
TableCalendar(
|
firstDay: DateTime.now(),
|
||||||
locale: 'es_CO',
|
lastDay: DateTime.now().add(const Duration(days: 180)),
|
||||||
firstDay: DateTime.now(),
|
focusedDay: _today,
|
||||||
lastDay: DateTime.now().add(const Duration(days: 180)),
|
availableGestures: AvailableGestures.all,
|
||||||
focusedDay: today,
|
onDaySelected: _onDaySelected,
|
||||||
availableGestures: AvailableGestures.all,
|
selectedDayPredicate: (day) => isSameDay(day, _today),
|
||||||
onDaySelected: _onDaySelected,
|
),
|
||||||
selectedDayPredicate: (day) => isSameDay(day, today),
|
const Divider(height: 0),
|
||||||
),
|
SizedBox(
|
||||||
const Divider(height: 0),
|
width: double.infinity,
|
||||||
SizedBox(
|
child: Padding(
|
||||||
width: double.infinity,
|
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
|
||||||
child: Padding(
|
child: Text(
|
||||||
padding: const EdgeInsets.symmetric(
|
DateFormat('dd MMMM yyyy', 'es').format(_today),
|
||||||
horizontal: 15,
|
style: const TextStyle(
|
||||||
vertical: 8,
|
color: Colors.black,
|
||||||
),
|
fontSize: 16,
|
||||||
child: Text(
|
fontWeight: FontWeight.w600,
|
||||||
DateFormat('dd MMMM yyyy', 'es').format(today),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.black,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 0),
|
),
|
||||||
|
const Divider(height: 0),
|
||||||
|
if (_loadingServices)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 32),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
else
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
..._rangesItems(
|
..._rangesItems(
|
||||||
_getScheduleFromNumDay(numDay, profesional),
|
_getScheduleFromNumDay(_today.weekday, _profesional!),
|
||||||
context,
|
context,
|
||||||
stepMinutes: profesional.slotDurationMinutes),
|
stepMinutes: _profesional!.slotDurationMinutes,
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScheduleEntity? _getScheduleFromNumDay(
|
ScheduleEntity? _getScheduleFromNumDay(int numDay, Profesional pro) {
|
||||||
int numDay, Profesional userProfessional) {
|
|
||||||
switch (numDay) {
|
switch (numDay) {
|
||||||
case 1:
|
case 1: return pro.schedules.monday;
|
||||||
return userProfessional.schedules.monday;
|
case 2: return pro.schedules.tuesday;
|
||||||
case 2:
|
case 3: return pro.schedules.wednesday;
|
||||||
return userProfessional.schedules.tuesday;
|
case 4: return pro.schedules.thursday;
|
||||||
case 3:
|
case 5: return pro.schedules.friday;
|
||||||
return userProfessional.schedules.wednesday;
|
case 6: return pro.schedules.saturday;
|
||||||
case 4:
|
case 7: return pro.schedules.sunday;
|
||||||
return userProfessional.schedules.thursday;
|
default: return null;
|
||||||
case 5:
|
|
||||||
return userProfessional.schedules.friday;
|
|
||||||
case 6:
|
|
||||||
return userProfessional.schedules.saturday;
|
|
||||||
case 7:
|
|
||||||
return userProfessional.schedules.sunday;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _rangesItems(ScheduleEntity? schedule, BuildContext context, {int stepMinutes = 30}) {
|
List<Widget> _rangesItems(ScheduleEntity? schedule, BuildContext context, {int stepMinutes = 30}) {
|
||||||
if (schedule == null) {
|
if (schedule == null || !schedule.enabled) {
|
||||||
return [
|
return [const Padding(padding: EdgeInsets.only(top: 20), child: Text('No hay horarios disponibles'))];
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(top: 20),
|
|
||||||
child: Text("No hay horarios disponibles"),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (!schedule.enabled) {
|
|
||||||
return [
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(top: 20),
|
|
||||||
child: Text("No hay horarios disponibles"),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final List<TimeOfDay> ranges;
|
||||||
if (schedule.continuousDay) {
|
if (schedule.continuousDay) {
|
||||||
if (schedule.range1Hour1 == null || schedule.range2Hour2 == null || schedule.range1Hour1!.compareTo(schedule.range2Hour2!) >= 0) { return [
|
if (schedule.range1Hour1 == null || schedule.range2Hour2 == null ||
|
||||||
const Padding(
|
schedule.range1Hour1!.compareTo(schedule.range2Hour2!) >= 0) {
|
||||||
padding: EdgeInsets.only(top: 20),
|
return [const Padding(padding: EdgeInsets.only(top: 20), child: Text('No hay horarios disponibles'))];
|
||||||
child: Text("No hay horarios disponibles"),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
ranges = TimeOfDayUtils.genRanges(schedule.range1Hour1!, schedule.range2Hour2!, stepMinutes: stepMinutes);
|
||||||
List<TimeOfDay> ranges = TimeOfDayUtils.genRanges(
|
|
||||||
schedule.range1Hour1!,
|
|
||||||
schedule.range2Hour2!,
|
|
||||||
stepMinutes: stepMinutes,
|
|
||||||
);
|
|
||||||
|
|
||||||
return rangesItemList(ranges, _services, today, context);
|
|
||||||
} else {
|
} else {
|
||||||
if (schedule.range1Hour1 == null || schedule.range1Hour2 == null || schedule.range2Hour1 == null || schedule.range2Hour2 == null || schedule.range1Hour1!.compareTo(schedule.range1Hour2!) >= 0 || schedule.range2Hour1!.compareTo(schedule.range2Hour2!) >= 0) {
|
if (schedule.range1Hour1 == null || schedule.range1Hour2 == null ||
|
||||||
return [
|
schedule.range2Hour1 == null || schedule.range2Hour2 == null ||
|
||||||
const Padding(
|
schedule.range1Hour1!.compareTo(schedule.range1Hour2!) >= 0 ||
|
||||||
padding: EdgeInsets.only(top: 20),
|
schedule.range2Hour1!.compareTo(schedule.range2Hour2!) >= 0) {
|
||||||
child: Text("No hay horarios disponibles"),
|
return [const Padding(padding: EdgeInsets.only(top: 20), child: Text('No hay horarios disponibles'))];
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
ranges = [
|
||||||
List<TimeOfDay> ranges1 = TimeOfDayUtils.genRanges(
|
...TimeOfDayUtils.genRanges(schedule.range1Hour1!, schedule.range1Hour2!, stepMinutes: stepMinutes),
|
||||||
schedule.range1Hour1!,
|
...TimeOfDayUtils.genRanges(schedule.range2Hour1!, schedule.range2Hour2!, stepMinutes: stepMinutes),
|
||||||
schedule.range1Hour2!,
|
|
||||||
stepMinutes: stepMinutes,
|
|
||||||
);
|
|
||||||
List<TimeOfDay> ranges2 = TimeOfDayUtils.genRanges(
|
|
||||||
schedule.range2Hour1!,
|
|
||||||
schedule.range2Hour2!,
|
|
||||||
stepMinutes: stepMinutes,
|
|
||||||
);
|
|
||||||
|
|
||||||
return [
|
|
||||||
...rangesItemList(ranges1, _services, today, context),
|
|
||||||
...rangesItemList(ranges2, _services, today, context),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _rangesItemList(ranges, _services, _today, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isBlocked(TimeOfDay hora1, List<Service>? events, DateTime selectedDay) {
|
// A slot is blocked (self-booked by the professional)
|
||||||
if (events == null) return false;
|
bool _isBlocked(TimeOfDay hora1, List<Service> services, DateTime selectedDay) {
|
||||||
final dayStr = selectedDay.toIso8601String().split('T').first;
|
final dayStr = _dayStr(selectedDay);
|
||||||
return events.any((e) =>
|
return services.any((e) =>
|
||||||
|
e.day == dayStr && e.range1Hour1 == hora1 && e.status == ServiceStatus.selfBooked);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A slot is occupied by a real patient booking
|
||||||
|
// Excludes: selfBooked (professional blocks), cancelled, denied
|
||||||
|
bool _isOccupied(TimeOfDay hora1, List<Service> services, DateTime selectedDay) {
|
||||||
|
final dayStr = _dayStr(selectedDay);
|
||||||
|
return services.any((e) =>
|
||||||
e.day == dayStr &&
|
e.day == dayStr &&
|
||||||
e.range1Hour1 == hora1 &&
|
e.range1Hour1 == hora1 &&
|
||||||
e.status == ServiceStatus.selfBooked);
|
e.status != ServiceStatus.selfBooked &&
|
||||||
|
e.status != ServiceStatus.cancelled &&
|
||||||
|
e.status != ServiceStatus.denied);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isHora1Ocupada(
|
String _dayStr(DateTime d) => '${d.year.toString().padLeft(4, '0')}-'
|
||||||
TimeOfDay hora1, List<Service>? events, DateTime selectedDay) {
|
'${d.month.toString().padLeft(2, '0')}-'
|
||||||
if (events != null) {
|
'${d.day.toString().padLeft(2, '0')}';
|
||||||
for (Service event in events) {
|
|
||||||
if (selectedDay.toIso8601String().split('T').first == event.day) {
|
|
||||||
if (hora1 == event.range1Hour1 &&
|
|
||||||
event.status != ServiceStatus.selfBooked) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> rangesItemList(List<TimeOfDay> ranges, List<Service>? events,DateTime selectedDay, BuildContext context) {
|
List<Widget> _rangesItemList(
|
||||||
final currentDateTime = DateTime.now();
|
List<TimeOfDay> ranges, List<Service> services, DateTime selectedDay, BuildContext context) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
return ranges.map((time) {
|
return ranges.map((time) {
|
||||||
final selectedDateTime = DateTime(
|
// Compare using local time to avoid timezone edge cases
|
||||||
selectedDay.year,
|
final slotDt = DateTime(selectedDay.year, selectedDay.month, selectedDay.day, time.hour, time.minute);
|
||||||
selectedDay.month,
|
final isPast = slotDt.isBefore(now.add(const Duration(hours: 3)));
|
||||||
selectedDay.day,
|
final isBlocked = _isBlocked(time, services, selectedDay);
|
||||||
time.hour,
|
final isOccupied = !isBlocked && _isOccupied(time, services, selectedDay);
|
||||||
time.minute,
|
|
||||||
|
if (isPast || isBlocked) {
|
||||||
|
return _slotCard(
|
||||||
|
time: time,
|
||||||
|
circleColor: Colors.grey,
|
||||||
|
icon: isBlocked ? Icons.block : Icons.access_time,
|
||||||
|
label: 'No disponible',
|
||||||
|
labelColor: Colors.red,
|
||||||
|
onTap: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOccupied) {
|
||||||
|
return _slotCard(
|
||||||
|
time: time,
|
||||||
|
circleGradient: const LinearGradient(
|
||||||
|
colors: [Colors.yellow, Colors.red, Colors.red],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
icon: Icons.access_time,
|
||||||
|
label: 'Ocupado',
|
||||||
|
labelColor: Colors.red,
|
||||||
|
onTap: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _slotCard(
|
||||||
|
time: time,
|
||||||
|
circleGradient: const LinearGradient(
|
||||||
|
colors: [Colors.blue, Colors.green],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
icon: Icons.access_time,
|
||||||
|
label: 'Disponible',
|
||||||
|
labelColor: Colors.green,
|
||||||
|
onTap: () => Navigator.pop(context, [selectedDay, time]),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedDateTime
|
|
||||||
.isBefore(currentDateTime.add(const Duration(hours: 3)))) {
|
|
||||||
return Card(
|
|
||||||
elevation: 4,
|
|
||||||
margin: const EdgeInsets.only(top: 15, left: 10, right: 10),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: ListTile(
|
|
||||||
contentPadding: const EdgeInsets.all(16),
|
|
||||||
leading: Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.grey,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(
|
|
||||||
Icons.access_time,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
ScheduleEntity.getFormatTime(time) ?? '',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: const Text(
|
|
||||||
'No disponible',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.red,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isBlocked(time, events, selectedDay)) {
|
|
||||||
return Card(
|
|
||||||
elevation: 4,
|
|
||||||
margin: const EdgeInsets.only(top: 15, left: 10, right: 10),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: ListTile(
|
|
||||||
contentPadding: const EdgeInsets.all(16),
|
|
||||||
leading: Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.grey,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(Icons.block, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
ScheduleEntity.getFormatTime(time) ?? '',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: const Text(
|
|
||||||
'No disponible',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.red,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isHora1Ocupada(time, events, selectedDay)) {
|
|
||||||
return Card(
|
|
||||||
elevation: 4,
|
|
||||||
margin: const EdgeInsets.only(top: 15, left: 10, right: 10),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: ListTile(
|
|
||||||
onTap: () {
|
|
||||||
if (events != null) {
|
|
||||||
for (Service event in events) {
|
|
||||||
if (selectedDay.toIso8601String().split('T').first ==
|
|
||||||
event.day) {
|
|
||||||
if (time == event.range1Hour1) {
|
|
||||||
NotificationsService.showSnackbar('Ocupado');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentPadding: const EdgeInsets.all(16),
|
|
||||||
leading: Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [Colors.yellow, Colors.red, Colors.red],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(
|
|
||||||
Icons.access_time,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
ScheduleEntity.getFormatTime(time) ?? '',
|
|
||||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
subtitle: const Text(
|
|
||||||
'Ocupado',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.red, fontSize: 13, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Card(
|
|
||||||
elevation: 4,
|
|
||||||
margin: const EdgeInsets.only(top: 15, left: 10, right: 10),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: ListTile(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context, [selectedDay, time]);
|
|
||||||
},
|
|
||||||
contentPadding: const EdgeInsets.all(16),
|
|
||||||
leading: Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [Colors.blue, Colors.green],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(
|
|
||||||
Icons.access_time,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
ScheduleEntity.getFormatTime(time) ?? '',
|
|
||||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
subtitle: const Text(
|
|
||||||
'Disponible',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.green,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _slotCard({
|
||||||
|
required TimeOfDay time,
|
||||||
|
Color? circleColor,
|
||||||
|
Gradient? circleGradient,
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required Color labelColor,
|
||||||
|
VoidCallback? onTap,
|
||||||
|
}) {
|
||||||
|
return Card(
|
||||||
|
elevation: 4,
|
||||||
|
margin: const EdgeInsets.only(top: 15, left: 10, right: 10),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: ListTile(
|
||||||
|
onTap: onTap,
|
||||||
|
contentPadding: const EdgeInsets.all(16),
|
||||||
|
leading: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: circleGradient == null ? circleColor : null,
|
||||||
|
gradient: circleGradient,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Center(child: Icon(icon, color: Colors.white)),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
ScheduleEntity.getFormatTime(time) ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: onTap == null ? Colors.grey : Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(color: labelColor, fontSize: 13, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ class _DashboardViewState extends State<DashboardView> {
|
|||||||
createdAt: DateTime.now().toIso8601String(),
|
createdAt: DateTime.now().toIso8601String(),
|
||||||
description: '',
|
description: '',
|
||||||
range1Hour1: _selectedHour!,
|
range1Hour1: _selectedHour!,
|
||||||
range1Hour2: _selectedHour!.add(hour: 2),
|
range1Hour2: _selectedHour!.add(minute: _professional!.professionalInfo.slotDurationMinutes),
|
||||||
rate: '',
|
rate: '',
|
||||||
status: ServiceStatus.pending,
|
status: ServiceStatus.pending,
|
||||||
location: serviceLocation,
|
location: serviceLocation,
|
||||||
|
|||||||
Reference in New Issue
Block a user