diff --git a/lib/ui/views/professional_calendar_view.dart b/lib/ui/views/professional_calendar_view.dart index e1fb721..c71744d 100644 --- a/lib/ui/views/professional_calendar_view.dart +++ b/lib/ui/views/professional_calendar_view.dart @@ -12,12 +12,21 @@ import 'package:table_calendar/table_calendar.dart'; import 'package:flutter/material.dart'; const _kPrimary = Color(0xFF1565C0); -const _kAccent = Color(0xFF42A4EF); -const _kBg = Color(0xFFF4F6FA); -const _kCard = Colors.white; const _kAvailable = Color(0xFF16A34A); const _kOccupied = Color(0xFFDC2626); +extension _Th on BuildContext { + ThemeData get _t => Theme.of(this); + Color get bg => _t.scaffoldBackgroundColor; + Color get card => _t.cardColor; + Color get onSurface => _t.colorScheme.onSurface; + Color get muted => _t.colorScheme.onSurface.withOpacity(0.55); + Color get subtle => _t.colorScheme.onSurface.withOpacity(0.35); + bool get isDark => _t.brightness == Brightness.dark; + Color get shadow => isDark ? Colors.transparent : Colors.black.withOpacity(0.07); + Color get shadowSm => isDark ? Colors.transparent : Colors.black.withOpacity(0.04); +} + class ProfessionalCalendarView extends StatefulWidget { const ProfessionalCalendarView({super.key}); @@ -26,8 +35,7 @@ class ProfessionalCalendarView extends StatefulWidget { _ProfessionalCalendarViewState(); } -class _ProfessionalCalendarViewState - extends State { +class _ProfessionalCalendarViewState extends State { DateTime _selected = DateTime.now(); List? _services; bool _loading = true; @@ -56,31 +64,27 @@ class _ProfessionalCalendarViewState @override Widget build(BuildContext context) { return Container( - color: _kBg, + color: context.bg, child: Consumer( builder: (context, fp, _) { if (fp.profesional == null || _loading) { - return const Center( - child: CircularProgressIndicator(color: _kPrimary), - ); + return const Center(child: CircularProgressIndicator(color: _kPrimary)); } final pro = fp.profesional!; final schedule = _scheduleFor(_selected.weekday, pro); final slots = _buildSlots(schedule); - final occupied = slots - .where((t) => _isOccupied(t, _services, _selected)) - .length; + final occupied = slots.where((t) => _isOccupied(t, _services, _selected)).length; return ListView( physics: const ClampingScrollPhysics(), padding: const EdgeInsets.only(bottom: 32), children: [ - _calendarCard(), - _dayHeader(schedule, slots.length, occupied), + _calendarCard(context), + _dayHeader(context, schedule, slots.length, occupied), if (slots.isEmpty) - _emptyState() + _emptyState(context) else - ..._slotCards(slots, schedule), + ..._slotCards(context, slots), ], ); }, @@ -88,24 +92,16 @@ class _ProfessionalCalendarViewState ); } - // ── Calendar card ───────────────────────────────────────────────────────── - - Widget _calendarCard() { + Widget _calendarCard(BuildContext context) { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: Container( margin: const EdgeInsets.fromLTRB(16, 20, 16, 0), decoration: BoxDecoration( - color: _kCard, + color: context.card, borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.07), - blurRadius: 16, - offset: const Offset(0, 4), - ) - ], + boxShadow: [BoxShadow(color: context.shadow, blurRadius: 16, offset: const Offset(0, 4))], ), child: ClipRRect( borderRadius: BorderRadius.circular(16), @@ -119,48 +115,25 @@ class _ProfessionalCalendarViewState selectedDayPredicate: (d) => isSameDay(d, _selected), calendarStyle: CalendarStyle( todayDecoration: BoxDecoration( - border: Border.all(color: _kPrimary, width: 2), - shape: BoxShape.circle, - ), - todayTextStyle: const TextStyle( - color: _kPrimary, - fontWeight: FontWeight.w700, - ), - selectedDecoration: const BoxDecoration( - color: _kPrimary, - shape: BoxShape.circle, - ), - selectedTextStyle: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w700, - ), + border: Border.all(color: _kPrimary, width: 2), shape: BoxShape.circle), + todayTextStyle: const TextStyle(color: _kPrimary, fontWeight: FontWeight.w700), + selectedDecoration: const BoxDecoration(color: _kPrimary, shape: BoxShape.circle), + selectedTextStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700), weekendTextStyle: TextStyle(color: Colors.red.shade400), + defaultTextStyle: TextStyle(color: context.onSurface), outsideDaysVisible: false, ), - headerStyle: const HeaderStyle( + headerStyle: HeaderStyle( formatButtonVisible: false, titleCentered: true, titleTextStyle: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w700, - color: Color(0xFF1E293B), - ), - leftChevronIcon: - Icon(Icons.chevron_left, color: _kPrimary), - rightChevronIcon: - Icon(Icons.chevron_right, color: _kPrimary), + fontSize: 15, fontWeight: FontWeight.w700, color: context.onSurface), + leftChevronIcon: const Icon(Icons.chevron_left, color: _kPrimary), + rightChevronIcon: const Icon(Icons.chevron_right, color: _kPrimary), ), - daysOfWeekStyle: const DaysOfWeekStyle( - weekdayStyle: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Color(0xFF64748B), - ), - weekendStyle: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Color(0xFFEF4444), - ), + daysOfWeekStyle: DaysOfWeekStyle( + weekdayStyle: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: context.muted), + weekendStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFFEF4444)), ), ), ), @@ -169,10 +142,7 @@ class _ProfessionalCalendarViewState ); } - // ── Day header + stats ──────────────────────────────────────────────────── - - Widget _dayHeader( - ScheduleEntity? schedule, int total, int occupied) { + Widget _dayHeader(BuildContext context, ScheduleEntity? schedule, int total, int occupied) { final dayName = DateFormat('EEEE', 'es').format(_selected); final dateStr = DateFormat('d MMMM yyyy', 'es').format(_selected); final available = total - occupied; @@ -185,98 +155,43 @@ class _ProfessionalCalendarViewState margin: const EdgeInsets.fromLTRB(16, 12, 16, 0), padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), decoration: BoxDecoration( - color: _kCard, + color: context.card, borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 12, - offset: const Offset(0, 2), - ) - ], + boxShadow: [BoxShadow(color: context.shadowSm, blurRadius: 12, offset: const Offset(0, 2))], ), - child: Row( - children: [ - // Date block - Container( - width: 48, - height: 52, - decoration: BoxDecoration( - color: _kPrimary, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - DateFormat('d').format(_selected), - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w800, - height: 1, - ), - ), - Text( - DateFormat('MMM', 'es').format(_selected).toUpperCase(), - style: const TextStyle( - color: Colors.white70, - fontSize: 10, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _capitalize(dayName), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - color: Color(0xFF1E293B), - ), - ), - Text( - dateStr, - style: const TextStyle( - fontSize: 12, - color: Color(0xFF94A3B8), - ), - ), - ], - ), - ), - if (hasSchedule && total > 0) ...[ - _StatPill( - label: '$occupied', - sublabel: 'ocupadas', - color: _kOccupied, - ), - const SizedBox(width: 8), - _StatPill( - label: '$available', - sublabel: 'libres', - color: _kAvailable, - ), - ], + child: Row(children: [ + Container( + width: 48, height: 52, + decoration: BoxDecoration(color: _kPrimary, borderRadius: BorderRadius.circular(12)), + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Text(DateFormat('d').format(_selected), + style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w800, height: 1)), + Text(DateFormat('MMM', 'es').format(_selected).toUpperCase(), + style: const TextStyle(color: Colors.white70, fontSize: 10, fontWeight: FontWeight.w600)), + ]), + ), + const SizedBox(width: 14), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(_capitalize(dayName), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: context.onSurface)), + Text(dateStr, style: TextStyle(fontSize: 12, color: context.subtle)), + ])), + if (hasSchedule && total > 0) ...[ + _StatPill(label: '$occupied', sublabel: 'ocupadas', color: _kOccupied), + const SizedBox(width: 8), + _StatPill(label: '$available', sublabel: 'libres', color: _kAvailable), ], - ), + ]), ), ), ); } - // ── Slot cards ──────────────────────────────────────────────────────────── - - List _slotCards(List slots, ScheduleEntity? schedule) { + List _slotCards(BuildContext context, List slots) { return slots.map((time) { - final occupied = _isOccupied(time, _services, _selected); - final matchService = occupied ? _serviceFor(time, _services, _selected) : null; + final occ = _isOccupied(time, _services, _selected); + final matchService = occ ? _serviceFor(time, _services, _selected) : null; + final color = occ ? _kOccupied : _kAvailable; return Center( child: ConstrainedBox( @@ -284,114 +199,54 @@ class _ProfessionalCalendarViewState child: Container( margin: const EdgeInsets.fromLTRB(16, 8, 16, 0), decoration: BoxDecoration( - color: _kCard, + color: context.card, borderRadius: BorderRadius.circular(14), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.04), - blurRadius: 8, - offset: const Offset(0, 2), - ) - ], + boxShadow: [BoxShadow(color: context.shadowSm, blurRadius: 8, offset: const Offset(0, 2))], ), child: ClipRRect( borderRadius: BorderRadius.circular(14), child: IntrinsicHeight( - child: Row( - children: [ - // Color accent strip - Container( - width: 4, - color: occupied ? _kOccupied : _kAvailable, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14, vertical: 12), - child: Row( - children: [ - // Time badge - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: (occupied ? _kOccupied : _kAvailable) - .withOpacity(0.08), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - ScheduleEntity.getFormatTime(time) ?? '', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w800, - color: - occupied ? _kOccupied : _kAvailable, - ), - ), - ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Text( - occupied ? 'Ocupado' : 'Disponible', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: occupied - ? _kOccupied - : _kAvailable, - ), - ), - if (matchService != null && - matchService.description.isNotEmpty) - Text( - matchService.description, - style: const TextStyle( - fontSize: 12, - color: Color(0xFF64748B), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ) - else if (!occupied) - const Text( - 'Horario libre para nuevas citas', - style: TextStyle( - fontSize: 11, - color: Color(0xFF94A3B8), - ), - ), - ], - ), - ), - // Status icon - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: (occupied ? _kOccupied : _kAvailable) - .withOpacity(0.1), - shape: BoxShape.circle, - ), - child: Icon( - occupied - ? Icons.event_busy_outlined - : Icons.event_available_outlined, - size: 17, - color: - occupied ? _kOccupied : _kAvailable, - ), - ), - ], + child: Row(children: [ + Container(width: 4, color: color), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + child: Row(children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(8)), + child: Text(ScheduleEntity.getFormatTime(time) ?? '', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w800, color: color)), ), - ), + const SizedBox(width: 14), + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(occ ? 'Ocupado' : 'Disponible', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: color)), + if (matchService != null && matchService.description.isNotEmpty) + Text(matchService.description, + style: TextStyle(fontSize: 12, color: context.muted), + maxLines: 1, overflow: TextOverflow.ellipsis) + else if (!occ) + Text('Horario libre para nuevas citas', + style: TextStyle(fontSize: 11, color: context.subtle)), + ], + )), + Container( + width: 32, height: 32, + decoration: BoxDecoration( + color: color.withOpacity(0.1), shape: BoxShape.circle), + child: Icon( + occ ? Icons.event_busy_outlined : Icons.event_available_outlined, + size: 17, color: color), + ), + ]), ), - ], - ), + ), + ]), ), ), ), @@ -400,9 +255,7 @@ class _ProfessionalCalendarViewState }).toList(); } - // ── Empty state ─────────────────────────────────────────────────────────── - - Widget _emptyState() { + Widget _emptyState(BuildContext context) { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), @@ -410,76 +263,40 @@ class _ProfessionalCalendarViewState margin: const EdgeInsets.fromLTRB(16, 12, 16, 0), padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 24), decoration: BoxDecoration( - color: _kCard, + color: context.card, borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 12, - offset: const Offset(0, 2), - ) - ], + boxShadow: [BoxShadow(color: context.shadowSm, blurRadius: 12, offset: const Offset(0, 2))], ), - child: Column( - children: [ - Container( - width: 64, - height: 64, - decoration: BoxDecoration( - color: const Color(0xFF94A3B8).withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon(Icons.event_busy_outlined, - size: 32, color: Color(0xFF94A3B8)), - ), - const SizedBox(height: 16), - const Text( - 'Sin horario este día', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w700, - color: Color(0xFF475569), - ), - ), - const SizedBox(height: 6), - const Text( - 'No tienes horario de atención configurado\npara este día de la semana.', + child: Column(children: [ + Container( + width: 64, height: 64, + decoration: BoxDecoration( + color: context.subtle.withOpacity(0.1), shape: BoxShape.circle), + child: Icon(Icons.event_busy_outlined, size: 32, color: context.subtle), + ), + const SizedBox(height: 16), + Text('Sin horario este día', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: context.muted)), + const SizedBox(height: 6), + Text('No tienes horario de atención configurado\npara este día de la semana.', textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13, - color: Color(0xFF94A3B8), - height: 1.5, - ), - ), - ], - ), + style: TextStyle(fontSize: 13, color: context.subtle, height: 1.5)), + ]), ), ), ); } - // ── Helpers ─────────────────────────────────────────────────────────────── - - ScheduleEntity? _scheduleFor(int weekday, Profesional pro) { - return switch (weekday) { - 1 => pro.schedules.monday, - 2 => pro.schedules.tuesday, - 3 => pro.schedules.wednesday, - 4 => pro.schedules.thursday, - 5 => pro.schedules.friday, - 6 => pro.schedules.saturday, - _ => pro.schedules.sunday, - }; - } + ScheduleEntity? _scheduleFor(int weekday, Profesional pro) => switch (weekday) { + 1 => pro.schedules.monday, 2 => pro.schedules.tuesday, + 3 => pro.schedules.wednesday, 4 => pro.schedules.thursday, + 5 => pro.schedules.friday, 6 => pro.schedules.saturday, + _ => pro.schedules.sunday, + }; List _buildSlots(ScheduleEntity? s) { - if (s == null || - !s.enabled || - s.range1Hour1 == null || - s.range2Hour2 == null) return []; - if (s.continuousDay) { - return TimeOfDayUtils.genRanges(s.range1Hour1!, s.range2Hour2!); - } + if (s == null || !s.enabled || s.range1Hour1 == null || s.range2Hour2 == null) return []; + if (s.continuousDay) return TimeOfDayUtils.genRanges(s.range1Hour1!, s.range2Hour2!); if (s.range1Hour2 == null || s.range2Hour1 == null) return []; return [ ...TimeOfDayUtils.genRanges(s.range1Hour1!, s.range1Hour2!), @@ -487,42 +304,28 @@ class _ProfessionalCalendarViewState ]; } - bool _isOccupied( - TimeOfDay time, List? services, DateTime day) { + bool _isOccupied(TimeOfDay time, List? services, DateTime day) { if (services == null) return false; final dayStr = day.toIso8601String().split('T').first; - return services - .any((s) => s.day == dayStr && s.range1Hour1 == time); + return services.any((s) => s.day == dayStr && s.range1Hour1 == time); } - Service? _serviceFor( - TimeOfDay time, List? services, DateTime day) { + Service? _serviceFor(TimeOfDay time, List? services, DateTime day) { if (services == null) return null; final dayStr = day.toIso8601String().split('T').first; try { - return services - .firstWhere((s) => s.day == dayStr && s.range1Hour1 == time); - } catch (_) { - return null; - } + return services.firstWhere((s) => s.day == dayStr && s.range1Hour1 == time); + } catch (_) { return null; } } - String _capitalize(String s) => - s.isEmpty ? s : s[0].toUpperCase() + s.substring(1); + String _capitalize(String s) => s.isEmpty ? s : s[0].toUpperCase() + s.substring(1); } -// ── Stat pill ───────────────────────────────────────────────────────────── - class _StatPill extends StatelessWidget { final String label; final String sublabel; final Color color; - - const _StatPill({ - required this.label, - required this.sublabel, - required this.color, - }); + const _StatPill({required this.label, required this.sublabel, required this.color}); @override Widget build(BuildContext context) { @@ -533,28 +336,12 @@ class _StatPill extends StatelessWidget { borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withOpacity(0.25)), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - label, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w800, - color: color, - height: 1, - ), - ), - Text( - sublabel, - style: TextStyle( - fontSize: 9, - color: color.withOpacity(0.8), - fontWeight: FontWeight.w600, - ), - ), - ], - ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Text(label, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: color, height: 1)), + Text(sublabel, + style: TextStyle(fontSize: 9, color: color.withOpacity(0.8), fontWeight: FontWeight.w600)), + ]), ); } } diff --git a/lib/ui/views/professional_profile_view.dart b/lib/ui/views/professional_profile_view.dart index afc7a21..ee80456 100644 --- a/lib/ui/views/professional_profile_view.dart +++ b/lib/ui/views/professional_profile_view.dart @@ -14,13 +14,24 @@ import 'package:prosapp_web_app/services/notifications_service.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -// ── Constants ────────────────────────────────────────────────────────────── const _kPrimary = Color(0xFF1565C0); const _kAccent = Color(0xFF42A4EF); -const _kBg = Color(0xFFF4F6FA); -const _kCard = Colors.white; -// ── Entry ────────────────────────────────────────────────────────────────── +// Theme-aware color helpers +extension _Th on BuildContext { + ThemeData get _t => Theme.of(this); + Color get bg => _t.scaffoldBackgroundColor; + Color get card => _t.cardColor; + Color get onSurface => _t.colorScheme.onSurface; + Color get muted => _t.colorScheme.onSurface.withOpacity(0.55); + Color get subtle => _t.colorScheme.onSurface.withOpacity(0.35); + Color get divider => _t.dividerColor; + bool get isDark => _t.brightness == Brightness.dark; + Color get shadow => isDark ? Colors.transparent : Colors.black.withOpacity(0.07); + Color get shadowSm => isDark ? Colors.transparent : Colors.black.withOpacity(0.04); + Color get inputFill => isDark ? _t.cardColor : const Color(0xFFF8FAFC); + Color get chipBg => isDark ? _t.colorScheme.onSurface.withOpacity(0.06) : Colors.grey.shade50; +} class ProfessionalProfileView extends StatefulWidget { const ProfessionalProfileView({super.key}); @@ -46,7 +57,7 @@ class _ProfessionalProfileViewState extends State { @override Widget build(BuildContext context) { return Container( - color: _kBg, + color: context.bg, child: ListView( physics: const ClampingScrollPhysics(), padding: const EdgeInsets.only(bottom: 32), @@ -56,7 +67,7 @@ class _ProfessionalProfileViewState extends State { } } -// ── Profile header card ─────────────────────────────────────────────────── +// ── Profile header ──────────────────────────────────────────────────────── class _ProfileHeader extends StatelessWidget { const _ProfileHeader(); @@ -66,7 +77,6 @@ class _ProfileHeader extends StatelessWidget { final auth = Provider.of(context).user!; final pfp = Provider.of(context); final fp = Provider.of(context); - final picture = pfp.user?.picture; final profession = fp.profesional?.profession; @@ -76,19 +86,12 @@ class _ProfileHeader extends StatelessWidget { child: Container( margin: const EdgeInsets.fromLTRB(16, 20, 16, 0), decoration: BoxDecoration( - color: _kCard, + color: context.card, borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.07), - blurRadius: 16, - offset: const Offset(0, 4), - ) - ], + boxShadow: [BoxShadow(color: context.shadow, blurRadius: 16, offset: const Offset(0, 4))], ), child: Column( children: [ - // Banner gradient Container( height: 110, decoration: const BoxDecoration( @@ -97,11 +100,9 @@ class _ProfileHeader extends StatelessWidget { begin: Alignment.topLeft, end: Alignment.bottomRight, ), - borderRadius: - BorderRadius.vertical(top: Radius.circular(16)), + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), ), - // Avatar overlapping the banner Transform.translate( offset: const Offset(0, -44), child: Column( @@ -114,72 +115,49 @@ class _ProfileHeader extends StatelessWidget { height: 88, decoration: BoxDecoration( shape: BoxShape.circle, - border: Border.all(color: _kCard, width: 4), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.12), - blurRadius: 8, - ) - ], + border: Border.all(color: context.card, width: 4), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.12), blurRadius: 8)], ), child: ClipOval( child: picture != null && picture.isNotEmpty ? FadeInImage.assetNetwork( - placeholder: 'loader.gif', - image: picture, - fit: BoxFit.cover, - ) + placeholder: 'loader.gif', image: picture, fit: BoxFit.cover) : Container( color: _kAccent.withOpacity(0.15), - child: const Icon(Icons.person, - size: 44, color: _kAccent), + child: const Icon(Icons.person, size: 44, color: _kAccent), ), ), ), _CameraButton( onTap: () async { - final result = await FilePicker.platform - .pickFiles(withData: true); + final result = await FilePicker.platform.pickFiles(withData: true); if (result == null) return; final bytes = result.files.first.bytes; - if (bytes == null) return; - if (!context.mounted) return; + if (bytes == null || !context.mounted) return; NotificationsService.showBusyIndicator(context); await pfp.uploadPicture(bytes); if (!context.mounted) return; - Provider.of(context, listen: false) - .refreshUser(); + Provider.of(context, listen: false).refreshUser(); Navigator.pop(context); }, ), ], ), const SizedBox(height: 8), - Text( - auth.name, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w700, - color: Color(0xFF1E293B), - ), - ), + Text(auth.name, + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.w700, color: context.onSurface)), if (profession != null && profession.isNotEmpty) ...[ const SizedBox(height: 4), Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 3), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3), decoration: BoxDecoration( - color: _kAccent.withOpacity(0.1), + color: _kAccent.withOpacity(0.12), borderRadius: BorderRadius.circular(20), ), - child: Text( - profession, - style: const TextStyle( - color: _kPrimary, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), + child: Text(profession, + style: const TextStyle( + color: _kPrimary, fontSize: 12, fontWeight: FontWeight.w600)), ), ], const SizedBox(height: 16), @@ -208,7 +186,7 @@ class _CameraButton extends StatelessWidget { decoration: BoxDecoration( color: _kPrimary, shape: BoxShape.circle, - border: Border.all(color: _kCard, width: 2), + border: Border.all(color: context.card, width: 2), ), child: const Icon(Icons.camera_alt, size: 14, color: Colors.white), ), @@ -252,8 +230,7 @@ class _ProfileFormState extends State<_ProfileForm> { _first = false; _rateEnabled = settings.tarifas ? pro.ratePreferences : false; if (pro.locationPreferences == LocationPreferences.both) { - _delivery = true; - _office = true; + _delivery = true; _office = true; } else if (pro.locationPreferences == LocationPreferences.office) { _office = true; } else { @@ -269,114 +246,91 @@ class _ProfileFormState extends State<_ProfileForm> { autovalidateMode: AutovalidateMode.always, child: Column( children: [ - // ── Modalidad ────────────────────────────────────────── _SectionCard( icon: Icons.tune_outlined, title: 'Modalidad de servicio', - child: Column( - children: [ - _ToggleRow( - icon: Icons.delivery_dining_outlined, - title: 'Servicio a domicilio', - subtitle: 'Vas donde está el cliente', - value: _delivery, - onChanged: settings.domicilios - ? (v) => setState(() { - _delivery = v; - if (!v) _office = true; - }) - : null, - ), - const _Divider(), - _ToggleRow( - icon: Icons.store_mall_directory_outlined, - title: 'Servicio en consultorio / sitio', - subtitle: 'El cliente viene a tu local', - value: _office, - onChanged: (v) => setState(() { - if (settings.domicilios == true) { - _office = v; - if (!v) _delivery = true; - } else { - _office = true; - _delivery = false; - } - }), - ), - ], - ), + child: Column(children: [ + _ToggleRow( + icon: Icons.delivery_dining_outlined, + title: 'Servicio a domicilio', + subtitle: 'Vas donde está el cliente', + value: _delivery, + onChanged: settings.domicilios + ? (v) => setState(() { _delivery = v; if (!v) _office = true; }) + : null, + ), + _Divider(), + _ToggleRow( + icon: Icons.store_mall_directory_outlined, + title: 'Servicio en consultorio / sitio', + subtitle: 'El cliente viene a tu local', + value: _office, + onChanged: (v) => setState(() { + if (settings.domicilios == true) { + _office = v; + if (!v) _delivery = true; + } else { + _office = true; _delivery = false; + } + }), + ), + ]), ), - // ── Dirección ───────────────────────────────────────── if (_office) _SectionCard( icon: Icons.location_on_outlined, title: 'Dirección del consultorio', - child: Column( - children: [ - _Field( - initial: pro.address, - hint: 'Calle 123 # 45-67', - label: 'Dirección principal', - icon: Icons.map_outlined, - onChanged: (v) => - fp.copyProfesionalWith(address: v), - ), - const SizedBox(height: 12), - _Field( - initial: pro.aditionalAddress, - hint: 'Piso 2, Consultorio 204', - label: 'Piso / Apartamento / Conjunto', - icon: Icons.apartment_outlined, - onChanged: (v) => - fp.copyProfesionalWith(aditionalAddress: v), - ), - ], - ), + child: Column(children: [ + _Field( + initial: pro.address, + hint: 'Calle 123 # 45-67', + label: 'Dirección principal', + icon: Icons.map_outlined, + onChanged: (v) => fp.copyProfesionalWith(address: v), + ), + const SizedBox(height: 12), + _Field( + initial: pro.aditionalAddress, + hint: 'Piso 2, Consultorio 204', + label: 'Piso / Apartamento / Conjunto', + icon: Icons.apartment_outlined, + onChanged: (v) => fp.copyProfesionalWith(aditionalAddress: v), + ), + ]), ), - // ── Tarifa ──────────────────────────────────────────── if (settings.tarifas) _SectionCard( icon: Icons.payments_outlined, title: 'Tarifa del servicio', - child: Column( - children: [ - _ToggleRow( - icon: Icons.attach_money, - title: 'Mostrar tarifa en el perfil', - subtitle: - 'Los clientes verán el precio por consulta', - value: _rateEnabled, - onChanged: (v) => - setState(() => _rateEnabled = v), + child: Column(children: [ + _ToggleRow( + icon: Icons.attach_money, + title: 'Mostrar tarifa en el perfil', + subtitle: 'Los clientes verán el precio por consulta', + value: _rateEnabled, + onChanged: (v) => setState(() => _rateEnabled = v), + ), + if (_rateEnabled) ...[ + _Divider(), + _Field( + initial: pro.rate, + hint: '50000', + label: 'Tarifa (COP)', + icon: Icons.monetization_on_outlined, + keyboardType: TextInputType.number, + validator: (v) { + if (v == null || v.isEmpty) return 'La tarifa es obligatoria'; + if (!RegExp(r'^[0-9]+$').hasMatch(v)) return 'Solo números'; + return null; + }, + onChanged: (v) => fp.copyProfesionalWith(rate: v), ), - if (_rateEnabled) ...[ - const _Divider(), - _Field( - initial: pro.rate, - hint: '50000', - label: 'Tarifa (COP)', - icon: Icons.monetization_on_outlined, - keyboardType: TextInputType.number, - validator: (v) { - if (v == null || v.isEmpty) { - return 'La tarifa es obligatoria'; - } - if (!RegExp(r'^[0-9]+$').hasMatch(v)) { - return 'Solo números'; - } - return null; - }, - onChanged: (v) => - fp.copyProfesionalWith(rate: v), - ), - ], ], - ), + ]), ), - // ── Métodos de pago ─────────────────────────────────── _SectionCard( icon: Icons.credit_card_outlined, title: 'Métodos de pago', @@ -389,104 +343,70 @@ class _ProfileFormState extends State<_ProfileForm> { label: 'Datáfono', selected: pro.paymentMethods.datafono, onTap: () => fp.copyProfesionalWith( - paymentMethods: pro.paymentMethods.copyWith( - datafono: !pro.paymentMethods.datafono, - ), - ), + paymentMethods: pro.paymentMethods.copyWith(datafono: !pro.paymentMethods.datafono)), ), _PayChip( icon: Icons.phone_android_outlined, label: 'Nequi', selected: pro.paymentMethods.nequi, onTap: () => fp.copyProfesionalWith( - paymentMethods: pro.paymentMethods - .copyWith(nequi: !pro.paymentMethods.nequi), - ), + paymentMethods: pro.paymentMethods.copyWith(nequi: !pro.paymentMethods.nequi)), ), _PayChip( icon: Icons.account_balance_outlined, label: 'Transferencia', selected: pro.paymentMethods.transferencia, onTap: () => fp.copyProfesionalWith( - paymentMethods: pro.paymentMethods.copyWith( - transferencia: - !pro.paymentMethods.transferencia, - ), - ), + paymentMethods: pro.paymentMethods.copyWith(transferencia: !pro.paymentMethods.transferencia)), ), ], ), ), - // ── Horarios ────────────────────────────────────────── _SectionCard( icon: Icons.schedule_outlined, title: 'Horarios de atención', trailing: TextButton.icon( - onPressed: () => NavigationService.replaceTo( - Flurorouter.professionalScheduleRoute), - icon: const Icon(Icons.edit_outlined, - size: 16, color: _kPrimary), - label: const Text('Editar', - style: - TextStyle(color: _kPrimary, fontSize: 13)), - ), - child: Column( - children: _buildScheduleRows(pro.schedules), + onPressed: () => NavigationService.replaceTo(Flurorouter.professionalScheduleRoute), + icon: const Icon(Icons.edit_outlined, size: 16, color: _kPrimary), + label: const Text('Editar', style: TextStyle(color: _kPrimary, fontSize: 13)), ), + child: Column(children: _buildScheduleRows(pro.schedules, context)), ), - // ── Botón guardar ───────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 4), child: SizedBox( width: double.infinity, height: 48, child: ElevatedButton( - onPressed: _saving - ? null - : () async { - setState(() => _saving = true); - LocationPreferences lp; - if (_delivery && _office) { - lp = LocationPreferences.both; - } else if (_delivery) { - lp = LocationPreferences.delivery; - } else { - lp = LocationPreferences.office; - } - fp.copyProfesionalWith( - locationPreferences: lp, - ratePreferences: _rateEnabled, - ); - await fp.updateProfesionalProfileInfo(user.id); - if (mounted) setState(() => _saving = false); - }, + onPressed: _saving ? null : () async { + setState(() => _saving = true); + LocationPreferences lp; + if (_delivery && _office) { + lp = LocationPreferences.both; + } else if (_delivery) { + lp = LocationPreferences.delivery; + } else { + lp = LocationPreferences.office; + } + fp.copyProfesionalWith(locationPreferences: lp, ratePreferences: _rateEnabled); + await fp.updateProfesionalProfileInfo(user.id); + if (mounted) setState(() => _saving = false); + }, style: ElevatedButton.styleFrom( backgroundColor: _kPrimary, foregroundColor: Colors.white, - disabledBackgroundColor: - _kPrimary.withOpacity(0.6), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + disabledBackgroundColor: _kPrimary.withOpacity(0.6), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 0, ), child: _saving ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ) - : const Text( - 'Guardar cambios', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600), - ), + width: 20, height: 20, + child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) + : const Text('Guardar cambios', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), ), ), ), @@ -499,94 +419,62 @@ class _ProfileFormState extends State<_ProfileForm> { }); } - List _buildScheduleRows(dynamic schedules) { + List _buildScheduleRows(dynamic schedules, BuildContext context) { const days = [ - ('Lunes', 'monday'), - ('Martes', 'tuesday'), - ('Miércoles', 'wednesday'), - ('Jueves', 'thursday'), - ('Viernes', 'friday'), - ('Sábado', 'saturday'), - ('Domingo', 'sunday'), + ('Lunes', 'monday'), ('Martes', 'tuesday'), ('Miércoles', 'wednesday'), + ('Jueves', 'thursday'), ('Viernes', 'friday'), ('Sábado', 'saturday'), ('Domingo', 'sunday'), ]; - return days.asMap().entries.map((entry) { final i = entry.key; final (name, field) = entry.value; final ScheduleEntity? sch = switch (field) { - 'monday' => schedules.monday, - 'tuesday' => schedules.tuesday, - 'wednesday' => schedules.wednesday, - 'thursday' => schedules.thursday, - 'friday' => schedules.friday, - 'saturday' => schedules.saturday, + 'monday' => schedules.monday, 'tuesday' => schedules.tuesday, + 'wednesday' => schedules.wednesday, 'thursday' => schedules.thursday, + 'friday' => schedules.friday, 'saturday' => schedules.saturday, _ => schedules.sunday, }; final time = _formatTime(sch); final active = sch?.enabled == true; - - return Column( - children: [ - if (i > 0) const _Divider(), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 4, vertical: 6), - child: Row( - children: [ - SizedBox( - width: 90, - child: Text( - name, - style: TextStyle( + return Column(children: [ + if (i > 0) _Divider(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), + child: Row(children: [ + SizedBox( + width: 90, + child: Text(name, + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: active - ? const Color(0xFF1E293B) - : Colors.grey.shade400, - ), - ), - ), - Container( - width: 7, - height: 7, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: active - ? Colors.green.shade400 - : Colors.grey.shade300, - ), - ), - const SizedBox(width: 10), - Expanded( - child: Text( - time, - style: TextStyle( - fontSize: 12, - color: active - ? const Color(0xFF475569) - : Colors.grey.shade400, - ), - ), - ), - ], + color: active ? context.onSurface : context.subtle)), ), - ), - ], - ); + Container( + width: 7, height: 7, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: active ? Colors.green.shade400 : context.subtle, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text(time, + style: TextStyle( + fontSize: 12, + color: active ? context.muted : context.subtle)), + ), + ]), + ), + ]); }).toList(); } String _formatTime(ScheduleEntity? s) { if (s == null || !s.enabled) return 'No disponible'; - if (s.range1Hour1 == null || s.range2Hour2 == null) { - return 'Sin horario definido'; - } + if (s.range1Hour1 == null || s.range2Hour2 == null) return 'Sin horario definido'; if (s.continuousDay) { return '${ScheduleEntity.getFormatTime(s.range1Hour1)} – ${ScheduleEntity.getFormatTime(s.range2Hour2)}'; } - if (s.range1Hour2 == null || s.range2Hour1 == null) { - return 'Sin horario definido'; - } + if (s.range1Hour2 == null || s.range2Hour1 == null) return 'Sin horario definido'; return '${ScheduleEntity.getFormatTime(s.range1Hour1)}–${ScheduleEntity.getFormatTime(s.range1Hour2)} · ${ScheduleEntity.getFormatTime(s.range2Hour1)}–${ScheduleEntity.getFormatTime(s.range2Hour2)}'; } } @@ -598,64 +486,36 @@ class _SectionCard extends StatelessWidget { final String title; final Widget child; final Widget? trailing; - - const _SectionCard({ - required this.icon, - required this.title, - required this.child, - this.trailing, - }); + const _SectionCard({required this.icon, required this.title, required this.child, this.trailing}); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.fromLTRB(16, 12, 16, 0), decoration: BoxDecoration( - color: _kCard, + color: context.card, borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 12, - offset: const Offset(0, 2), - ) - ], + boxShadow: [BoxShadow(color: context.shadowSm, blurRadius: 12, offset: const Offset(0, 2))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 14, 12, 0), - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: _kPrimary.withOpacity(0.08), - borderRadius: BorderRadius.circular(8), - ), - child: Icon(icon, size: 17, color: _kPrimary), - ), - const SizedBox(width: 10), - Expanded( - child: Text( - title, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: Color(0xFF1E293B), - ), - ), - ), - if (trailing != null) trailing!, - ], - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 10, 16, 14), - child: child, + child: Row(children: [ + Container( + width: 32, height: 32, + decoration: BoxDecoration( + color: _kPrimary.withOpacity(0.08), borderRadius: BorderRadius.circular(8)), + child: Icon(icon, size: 17, color: _kPrimary), + ), + const SizedBox(width: 10), + Expanded(child: Text(title, + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: context.onSurface))), + if (trailing != null) trailing!, + ]), ), + Padding(padding: const EdgeInsets.fromLTRB(16, 10, 16, 14), child: child), ], ), ); @@ -668,56 +528,23 @@ class _ToggleRow extends StatelessWidget { final String subtitle; final bool value; final ValueChanged? onChanged; - - const _ToggleRow({ - required this.icon, - required this.title, - required this.subtitle, - required this.value, - this.onChanged, - }); + const _ToggleRow({required this.icon, required this.title, required this.subtitle, + required this.value, this.onChanged}); @override Widget build(BuildContext context) { - final enabled = onChanged != null; return Opacity( - opacity: enabled ? 1.0 : 0.45, - child: Row( - children: [ - Icon(icon, - size: 20, - color: value ? _kPrimary : Colors.grey.shade400), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF1E293B), - ), - ), - Text( - subtitle, - style: const TextStyle( - fontSize: 11, - color: Color(0xFF94A3B8), - ), - ), - ], - ), - ), - Switch( - value: value, - onChanged: onChanged, - activeColor: _kPrimary, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ], - ), + opacity: onChanged != null ? 1.0 : 0.45, + child: Row(children: [ + Icon(icon, size: 20, color: value ? _kPrimary : context.subtle), + const SizedBox(width: 12), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: context.onSurface)), + Text(subtitle, style: TextStyle(fontSize: 11, color: context.subtle)), + ])), + Switch(value: value, onChanged: onChanged, activeColor: _kPrimary, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), + ]), ); } } @@ -727,13 +554,7 @@ class _PayChip extends StatelessWidget { final String label; final bool selected; final VoidCallback onTap; - - const _PayChip({ - required this.icon, - required this.label, - required this.selected, - required this.onTap, - }); + const _PayChip({required this.icon, required this.label, required this.selected, required this.onTap}); @override Widget build(BuildContext context) { @@ -743,39 +564,24 @@ class _PayChip extends StatelessWidget { duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( - color: selected - ? _kPrimary.withOpacity(0.08) - : Colors.grey.shade50, + color: selected ? _kPrimary.withOpacity(0.08) : context.chipBg, border: Border.all( - color: selected ? _kPrimary : Colors.grey.shade200, - width: 1.5, - ), + color: selected ? _kPrimary : context.divider, width: 1.5), borderRadius: BorderRadius.circular(10), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, - size: 18, - color: selected ? _kPrimary : Colors.grey.shade400), - const SizedBox(width: 8), - Text( - label, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: selected ? _kPrimary : Colors.grey.shade500, - ), - ), - const SizedBox(width: 6), - AnimatedOpacity( - opacity: selected ? 1 : 0, - duration: const Duration(milliseconds: 180), - child: const Icon(Icons.check_circle_rounded, - size: 15, color: _kPrimary), - ), - ], - ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(icon, size: 18, color: selected ? _kPrimary : context.subtle), + const SizedBox(width: 8), + Text(label, + style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, + color: selected ? _kPrimary : context.muted)), + const SizedBox(width: 6), + AnimatedOpacity( + opacity: selected ? 1 : 0, + duration: const Duration(milliseconds: 180), + child: const Icon(Icons.check_circle_rounded, size: 15, color: _kPrimary), + ), + ]), ), ); } @@ -789,16 +595,8 @@ class _Field extends StatelessWidget { final TextInputType? keyboardType; final String? Function(String?)? validator; final ValueChanged onChanged; - - const _Field({ - required this.initial, - required this.hint, - required this.label, - required this.icon, - required this.onChanged, - this.keyboardType, - this.validator, - }); + const _Field({required this.initial, required this.hint, required this.label, + required this.icon, required this.onChanged, this.keyboardType, this.validator}); @override Widget build(BuildContext context) { @@ -807,41 +605,28 @@ class _Field extends StatelessWidget { keyboardType: keyboardType, validator: validator, onChanged: onChanged, - style: const TextStyle(fontSize: 13), + style: TextStyle(fontSize: 13, color: context.onSurface), decoration: InputDecoration( labelText: label, hintText: hint, - prefixIcon: - Icon(icon, size: 18, color: Colors.grey.shade500), - contentPadding: - const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - labelStyle: const TextStyle( - fontSize: 13, - color: Color(0xFF64748B), - ), + prefixIcon: Icon(icon, size: 18, color: context.subtle), + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + labelStyle: TextStyle(fontSize: 13, color: context.muted), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade200), - ), + borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: context.divider)), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade200), - ), + borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: context.divider)), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: _kPrimary, width: 1.5), - ), + borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: _kPrimary, width: 1.5)), filled: true, - fillColor: const Color(0xFFF8FAFC), + fillColor: context.inputFill, ), ); } } class _Divider extends StatelessWidget { - const _Divider(); - @override Widget build(BuildContext context) => - Divider(height: 16, color: Colors.grey.shade100); + Divider(height: 16, color: context.divider); } diff --git a/lib/ui/views/services_requests_view.dart b/lib/ui/views/services_requests_view.dart index d58a4a6..edf463e 100644 --- a/lib/ui/views/services_requests_view.dart +++ b/lib/ui/views/services_requests_view.dart @@ -10,8 +10,18 @@ import 'package:prosapp_web_app/providers/auth_provider.dart'; import 'package:prosapp_web_app/providers/services_provider.dart'; const _kPrimary = Color(0xFF1565C0); -const _kBg = Color(0xFFF4F6FA); -const _kCard = Colors.white; + +extension _Th on BuildContext { + ThemeData get _t => Theme.of(this); + Color get bg => _t.scaffoldBackgroundColor; + Color get card => _t.cardColor; + Color get onSurface => _t.colorScheme.onSurface; + Color get muted => _t.colorScheme.onSurface.withOpacity(0.55); + Color get subtle => _t.colorScheme.onSurface.withOpacity(0.35); + bool get isDark => _t.brightness == Brightness.dark; + Color get shadow => isDark ? Colors.transparent : Colors.black.withOpacity(0.05); + Color get shadowSm => isDark ? Colors.transparent : Colors.black.withOpacity(0.04); +} class ServicesRequestsView extends StatelessWidget { const ServicesRequestsView({super.key}); @@ -23,72 +33,51 @@ class ServicesRequestsView extends StatelessWidget { Provider.of(context, listen: false).user!.id); return Container( - color: _kBg, + color: context.bg, child: Consumer( builder: (context, sp, _) { if (sp.isLoading) { - return const Center( - child: CircularProgressIndicator(color: _kPrimary), - ); + return const Center(child: CircularProgressIndicator(color: _kPrimary)); } return ListView( physics: const ClampingScrollPhysics(), padding: const EdgeInsets.only(bottom: 32), children: [ - // Header Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 4), - child: Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: _kPrimary.withOpacity(0.08), - borderRadius: BorderRadius.circular(10), - ), - child: const Icon(Icons.inbox_outlined, - size: 19, color: _kPrimary), - ), - const SizedBox(width: 10), - const Text( - 'Solicitudes', + child: Row(children: [ + Container( + width: 36, height: 36, + decoration: BoxDecoration( + color: _kPrimary.withOpacity(0.08), + borderRadius: BorderRadius.circular(10)), + child: const Icon(Icons.inbox_outlined, size: 19, color: _kPrimary), + ), + const SizedBox(width: 10), + Text('Solicitudes', style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w800, - color: Color(0xFF1E293B), - ), - ), - const Spacer(), - if (sp.services.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: _kPrimary.withOpacity(0.1), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${sp.services.length}', + fontSize: 18, fontWeight: FontWeight.w800, color: context.onSurface)), + const Spacer(), + if (sp.services.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: _kPrimary.withOpacity(0.1), + borderRadius: BorderRadius.circular(20)), + child: Text('${sp.services.length}', style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w700, - color: _kPrimary, - ), - ), - ), - ], - ), + fontSize: 13, fontWeight: FontWeight.w700, color: _kPrimary)), + ), + ]), ), ), ), - if (sp.services.isEmpty) - _emptyState() + _emptyState(context) else ...sp.services.map((data) => _RequestCard(data: data)), ], @@ -98,7 +87,7 @@ class ServicesRequestsView extends StatelessWidget { ); } - Widget _emptyState() { + Widget _emptyState(BuildContext context) { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), @@ -106,49 +95,24 @@ class ServicesRequestsView extends StatelessWidget { margin: const EdgeInsets.fromLTRB(16, 12, 16, 0), padding: const EdgeInsets.symmetric(vertical: 56, horizontal: 24), decoration: BoxDecoration( - color: _kCard, + color: context.card, borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 12, - offset: const Offset(0, 2), - ) - ], + boxShadow: [BoxShadow(color: context.shadow, blurRadius: 12, offset: const Offset(0, 2))], ), - child: Column( - children: [ - Container( - width: 64, - height: 64, - decoration: BoxDecoration( - color: const Color(0xFF94A3B8).withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon(Icons.inbox_outlined, - size: 32, color: Color(0xFF94A3B8)), - ), - const SizedBox(height: 16), - const Text( - 'Sin solicitudes pendientes', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w700, - color: Color(0xFF475569), - ), - ), - const SizedBox(height: 6), - const Text( - 'Cuando un cliente solicite tus servicios\naparecerá aquí.', + child: Column(children: [ + Container( + width: 64, height: 64, + decoration: BoxDecoration(color: context.subtle.withOpacity(0.1), shape: BoxShape.circle), + child: Icon(Icons.inbox_outlined, size: 32, color: context.subtle), + ), + const SizedBox(height: 16), + Text('Sin solicitudes pendientes', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: context.muted)), + const SizedBox(height: 6), + Text('Cuando un cliente solicite tus servicios\naparecerá aquí.', textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13, - color: Color(0xFF94A3B8), - height: 1.5, - ), - ), - ], - ), + style: TextStyle(fontSize: 13, color: context.subtle, height: 1.5)), + ]), ), ), ); @@ -164,8 +128,7 @@ class _RequestCard extends StatelessWidget { final service = data.service; final user = data.user; final statusInfo = _statusInfo(service.status); - final dateStr = DateFormat('dd MMM yyyy', 'es') - .format(DateTime.parse(service.day)); + final dateStr = DateFormat('dd MMM yyyy', 'es').format(DateTime.parse(service.day)); final timeStr = ScheduleEntity.getFormatTime(service.range1Hour1) ?? ''; return Center( @@ -174,15 +137,9 @@ class _RequestCard extends StatelessWidget { child: Container( margin: const EdgeInsets.fromLTRB(16, 8, 16, 0), decoration: BoxDecoration( - color: _kCard, + color: context.card, borderRadius: BorderRadius.circular(14), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ) - ], + boxShadow: [BoxShadow(color: context.shadowSm, blurRadius: 10, offset: const Offset(0, 2))], ), child: ClipRRect( borderRadius: BorderRadius.circular(14), @@ -192,121 +149,74 @@ class _RequestCard extends StatelessWidget { onTap: () => NavigationService.replaceTo( '/dashboard/professional/service/${service.id}'), child: IntrinsicHeight( - child: Row( - children: [ - // Status accent strip - Container(width: 4, color: statusInfo.color), - // Avatar - Padding( - padding: const EdgeInsets.all(14), - child: Stack( - children: [ - Container( - width: 52, - height: 52, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: statusInfo.color.withOpacity(0.3), - width: 2, - ), - ), - child: ClipOval( - child: (user.picture == null || - user.picture!.isEmpty) - ? Container( - color: _kPrimary.withOpacity(0.1), - child: Center( - child: Text( - user.name.isNotEmpty - ? user.name[0].toUpperCase() - : '?', - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: _kPrimary, - ), - ), - ), - ) - : FadeInImage.assetNetwork( - placeholder: 'loader.gif', - image: user.picture!, - fit: BoxFit.cover, - ), - ), - ), - ], - ), - ), - // Content - Expanded( - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - user.name, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: Color(0xFF1E293B), - ), - ), - if (service.description.isNotEmpty) ...[ - const SizedBox(height: 2), - Text( - '"${service.description}"', - style: const TextStyle( - fontSize: 12, - color: Color(0xFF64748B), - fontStyle: FontStyle.italic, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - const SizedBox(height: 6), - Row( - children: [ - const Icon(Icons.calendar_today_outlined, - size: 12, - color: Color(0xFF94A3B8)), - const SizedBox(width: 4), - Text( - '$dateStr · $timeStr', - style: const TextStyle( - fontSize: 12, - color: Color(0xFF64748B), - fontWeight: FontWeight.w500, + child: Row(children: [ + Container(width: 4, color: statusInfo.color), + Padding( + padding: const EdgeInsets.all(14), + child: Container( + width: 52, height: 52, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: statusInfo.color.withOpacity(0.3), width: 2)), + child: ClipOval( + child: (user.picture == null || user.picture!.isEmpty) + ? Container( + color: _kPrimary.withOpacity(0.1), + child: Center( + child: Text( + user.name.isNotEmpty ? user.name[0].toUpperCase() : '?', + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.w700, color: _kPrimary), ), ), - ], - ), - ], - ), + ) + : FadeInImage.assetNetwork( + placeholder: 'loader.gif', image: user.picture!, fit: BoxFit.cover), ), ), - // Status + arrow - Padding( - padding: const EdgeInsets.fromLTRB(8, 14, 12, 14), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 14), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, children: [ - _StatusBadge(info: statusInfo), - const SizedBox(height: 8), - Icon(Icons.chevron_right, - size: 18, - color: Colors.grey.shade400), + Text(user.name, + style: TextStyle( + fontSize: 14, fontWeight: FontWeight.w700, color: context.onSurface)), + if (service.description.isNotEmpty) ...[ + const SizedBox(height: 2), + Text('"${service.description}"', + style: TextStyle( + fontSize: 12, color: context.muted, fontStyle: FontStyle.italic), + maxLines: 1, overflow: TextOverflow.ellipsis), + ], + const SizedBox(height: 6), + Row(children: [ + Icon(Icons.calendar_today_outlined, size: 12, color: context.subtle), + const SizedBox(width: 4), + Text('$dateStr · $timeStr', + style: TextStyle( + fontSize: 12, color: context.muted, fontWeight: FontWeight.w500)), + ]), ], ), ), - ], - ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(8, 14, 12, 14), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _StatusBadge(info: statusInfo), + const SizedBox(height: 8), + Icon(Icons.chevron_right, size: 18, color: context.subtle), + ], + ), + ), + ]), ), ), ), @@ -324,28 +234,18 @@ class _StatusBadge extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: - const EdgeInsets.symmetric(horizontal: 9, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4), decoration: BoxDecoration( color: info.color.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: info.color.withOpacity(0.3)), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(info.icon, size: 11, color: info.color), - const SizedBox(width: 4), - Text( - info.label, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w700, - color: info.color, - ), - ), - ], - ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(info.icon, size: 11, color: info.color), + const SizedBox(width: 4), + Text(info.label, + style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: info.color)), + ]), ); } } @@ -357,21 +257,19 @@ class _StatusInfo { const _StatusInfo(this.label, this.color, this.icon); } -_StatusInfo _statusInfo(ServiceStatus status) { - return switch (status) { - ServiceStatus.pending => - const _StatusInfo('Pendiente', Color(0xFFD97706), Icons.hourglass_top_outlined), - ServiceStatus.acepted => - const _StatusInfo('Aceptado', Color(0xFF1565C0), Icons.check_circle_outline), - ServiceStatus.active => - const _StatusInfo('En curso', Color(0xFF16A34A), Icons.play_circle_outline), - ServiceStatus.completed => - const _StatusInfo('Completado', Color(0xFF64748B), Icons.task_alt_outlined), - ServiceStatus.denied => - const _StatusInfo('Rechazado', Color(0xFFDC2626), Icons.cancel_outlined), - ServiceStatus.cancelled => - const _StatusInfo('Cancelado', Color(0xFF9CA3AF), Icons.remove_circle_outline), - ServiceStatus.selfBooked => - const _StatusInfo('Reservado', Color(0xFF7C3AED), Icons.bookmark_outline), - }; -} +_StatusInfo _statusInfo(ServiceStatus status) => switch (status) { + ServiceStatus.pending => + const _StatusInfo('Pendiente', Color(0xFFD97706), Icons.hourglass_top_outlined), + ServiceStatus.acepted => + const _StatusInfo('Aceptado', Color(0xFF1565C0), Icons.check_circle_outline), + ServiceStatus.active => + const _StatusInfo('En curso', Color(0xFF16A34A), Icons.play_circle_outline), + ServiceStatus.completed => + const _StatusInfo('Completado', Color(0xFF64748B), Icons.task_alt_outlined), + ServiceStatus.denied => + const _StatusInfo('Rechazado', Color(0xFFDC2626), Icons.cancel_outlined), + ServiceStatus.cancelled => + const _StatusInfo('Cancelado', Color(0xFF9CA3AF), Icons.remove_circle_outline), + ServiceStatus.selfBooked => + const _StatusInfo('Reservado', Color(0xFF7C3AED), Icons.bookmark_outline), +};