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_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/utils/time_of_day_extension.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> {
|
||||
List<Service>? _services;
|
||||
DateTime today = DateTime.now();
|
||||
late int numDay;
|
||||
// Use local time throughout — UTC dates can shift the calendar date near midnight
|
||||
DateTime _today = DateTime.now();
|
||||
Profesional? _profesional;
|
||||
List<Service> _services = [];
|
||||
bool _loadingPro = true;
|
||||
bool _loadingServices = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
today = DateTime.utc(today.year, today.month, today.day);
|
||||
numDay = today.weekday;
|
||||
|
||||
_fetchProfessionalAndServices();
|
||||
_normalizeDay();
|
||||
_fetchProfessional();
|
||||
}
|
||||
|
||||
void _fetchProfessionalAndServices() async {
|
||||
final professionalFormProvider =
|
||||
Provider.of<ProfessionalFormProvider>(context, listen: false);
|
||||
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 _normalizeDay() {
|
||||
// Strip time component; keep local date only
|
||||
_today = DateTime(_today.year, _today.month, _today.day);
|
||||
}
|
||||
|
||||
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(() {
|
||||
today = day;
|
||||
numDay = today.weekday;
|
||||
_today = DateTime(day.year, day.month, day.day);
|
||||
});
|
||||
_fetchProfessionalAndServices();
|
||||
// Professional data is cached — only re-fetch services
|
||||
_fetchServices();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<ProfessionalFormProvider>(
|
||||
builder: (context, professionalFormProvider, child) {
|
||||
if (professionalFormProvider.profesional == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
if (_loadingPro || _profesional == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final profesional = professionalFormProvider.profesional!;
|
||||
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: WhiteCard(
|
||||
title: 'Calendario',
|
||||
child: Column(
|
||||
children: [
|
||||
TableCalendar(
|
||||
locale: 'es_CO',
|
||||
firstDay: DateTime.now(),
|
||||
lastDay: DateTime.now().add(const Duration(days: 180)),
|
||||
focusedDay: today,
|
||||
availableGestures: AvailableGestures.all,
|
||||
onDaySelected: _onDaySelected,
|
||||
selectedDayPredicate: (day) => isSameDay(day, today),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 15,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Text(
|
||||
DateFormat('dd MMMM yyyy', 'es').format(today),
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: WhiteCard(
|
||||
title: 'Calendario',
|
||||
child: Column(
|
||||
children: [
|
||||
TableCalendar(
|
||||
locale: 'es_CO',
|
||||
firstDay: DateTime.now(),
|
||||
lastDay: DateTime.now().add(const Duration(days: 180)),
|
||||
focusedDay: _today,
|
||||
availableGestures: AvailableGestures.all,
|
||||
onDaySelected: _onDaySelected,
|
||||
selectedDayPredicate: (day) => isSameDay(day, _today),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
|
||||
child: Text(
|
||||
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(
|
||||
children: [
|
||||
..._rangesItems(
|
||||
_getScheduleFromNumDay(numDay, profesional),
|
||||
context,
|
||||
stepMinutes: profesional.slotDurationMinutes),
|
||||
_getScheduleFromNumDay(_today.weekday, _profesional!),
|
||||
context,
|
||||
stepMinutes: _profesional!.slotDurationMinutes,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
ScheduleEntity? _getScheduleFromNumDay(
|
||||
int numDay, Profesional userProfessional) {
|
||||
ScheduleEntity? _getScheduleFromNumDay(int numDay, Profesional pro) {
|
||||
switch (numDay) {
|
||||
case 1:
|
||||
return userProfessional.schedules.monday;
|
||||
case 2:
|
||||
return userProfessional.schedules.tuesday;
|
||||
case 3:
|
||||
return userProfessional.schedules.wednesday;
|
||||
case 4:
|
||||
return userProfessional.schedules.thursday;
|
||||
case 5:
|
||||
return userProfessional.schedules.friday;
|
||||
case 6:
|
||||
return userProfessional.schedules.saturday;
|
||||
case 7:
|
||||
return userProfessional.schedules.sunday;
|
||||
default:
|
||||
return null;
|
||||
case 1: return pro.schedules.monday;
|
||||
case 2: return pro.schedules.tuesday;
|
||||
case 3: return pro.schedules.wednesday;
|
||||
case 4: return pro.schedules.thursday;
|
||||
case 5: return pro.schedules.friday;
|
||||
case 6: return pro.schedules.saturday;
|
||||
case 7: return pro.schedules.sunday;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _rangesItems(ScheduleEntity? schedule, BuildContext context, {int stepMinutes = 30}) {
|
||||
if (schedule == null) {
|
||||
return [
|
||||
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"),
|
||||
)
|
||||
];
|
||||
if (schedule == null || !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.range1Hour1 == null || schedule.range2Hour2 == null || schedule.range1Hour1!.compareTo(schedule.range2Hour2!) >= 0) { return [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Text("No hay horarios disponibles"),
|
||||
)
|
||||
];
|
||||
if (schedule.range1Hour1 == null || schedule.range2Hour2 == null ||
|
||||
schedule.range1Hour1!.compareTo(schedule.range2Hour2!) >= 0) {
|
||||
return [const Padding(padding: EdgeInsets.only(top: 20), child: Text('No hay horarios disponibles'))];
|
||||
}
|
||||
|
||||
List<TimeOfDay> ranges = TimeOfDayUtils.genRanges(
|
||||
schedule.range1Hour1!,
|
||||
schedule.range2Hour2!,
|
||||
stepMinutes: stepMinutes,
|
||||
);
|
||||
|
||||
return rangesItemList(ranges, _services, today, context);
|
||||
ranges = TimeOfDayUtils.genRanges(schedule.range1Hour1!, schedule.range2Hour2!, stepMinutes: stepMinutes);
|
||||
} 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) {
|
||||
return [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Text("No hay horarios disponibles"),
|
||||
)
|
||||
];
|
||||
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) {
|
||||
return [const Padding(padding: EdgeInsets.only(top: 20), child: Text('No hay horarios disponibles'))];
|
||||
}
|
||||
|
||||
List<TimeOfDay> ranges1 = TimeOfDayUtils.genRanges(
|
||||
schedule.range1Hour1!,
|
||||
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),
|
||||
ranges = [
|
||||
...TimeOfDayUtils.genRanges(schedule.range1Hour1!, schedule.range1Hour2!, stepMinutes: stepMinutes),
|
||||
...TimeOfDayUtils.genRanges(schedule.range2Hour1!, schedule.range2Hour2!, stepMinutes: stepMinutes),
|
||||
];
|
||||
}
|
||||
|
||||
return _rangesItemList(ranges, _services, _today, context);
|
||||
}
|
||||
|
||||
bool _isBlocked(TimeOfDay hora1, List<Service>? events, DateTime selectedDay) {
|
||||
if (events == null) return false;
|
||||
final dayStr = selectedDay.toIso8601String().split('T').first;
|
||||
return events.any((e) =>
|
||||
// A slot is blocked (self-booked by the professional)
|
||||
bool _isBlocked(TimeOfDay hora1, List<Service> services, DateTime selectedDay) {
|
||||
final dayStr = _dayStr(selectedDay);
|
||||
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.range1Hour1 == hora1 &&
|
||||
e.status == ServiceStatus.selfBooked);
|
||||
e.status != ServiceStatus.selfBooked &&
|
||||
e.status != ServiceStatus.cancelled &&
|
||||
e.status != ServiceStatus.denied);
|
||||
}
|
||||
|
||||
bool _isHora1Ocupada(
|
||||
TimeOfDay hora1, List<Service>? events, DateTime selectedDay) {
|
||||
if (events != null) {
|
||||
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;
|
||||
}
|
||||
String _dayStr(DateTime d) => '${d.year.toString().padLeft(4, '0')}-'
|
||||
'${d.month.toString().padLeft(2, '0')}-'
|
||||
'${d.day.toString().padLeft(2, '0')}';
|
||||
|
||||
List<Widget> rangesItemList(List<TimeOfDay> ranges, List<Service>? events,DateTime selectedDay, BuildContext context) {
|
||||
final currentDateTime = DateTime.now();
|
||||
List<Widget> _rangesItemList(
|
||||
List<TimeOfDay> ranges, List<Service> services, DateTime selectedDay, BuildContext context) {
|
||||
final now = DateTime.now();
|
||||
|
||||
return ranges.map((time) {
|
||||
final selectedDateTime = DateTime(
|
||||
selectedDay.year,
|
||||
selectedDay.month,
|
||||
selectedDay.day,
|
||||
time.hour,
|
||||
time.minute,
|
||||
// Compare using local time to avoid timezone edge cases
|
||||
final slotDt = DateTime(selectedDay.year, selectedDay.month, selectedDay.day, time.hour, time.minute);
|
||||
final isPast = slotDt.isBefore(now.add(const Duration(hours: 3)));
|
||||
final isBlocked = _isBlocked(time, services, selectedDay);
|
||||
final isOccupied = !isBlocked && _isOccupied(time, services, selectedDay);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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(),
|
||||
description: '',
|
||||
range1Hour1: _selectedHour!,
|
||||
range1Hour2: _selectedHour!.add(hour: 2),
|
||||
range1Hour2: _selectedHour!.add(minute: _professional!.professionalInfo.slotDurationMinutes),
|
||||
rate: '',
|
||||
status: ServiceStatus.pending,
|
||||
location: serviceLocation,
|
||||
|
||||
Reference in New Issue
Block a user