diff --git a/lib/ui/shared/sidebar.dart b/lib/ui/shared/sidebar.dart index a4a9a88..d7f04e7 100644 --- a/lib/ui/shared/sidebar.dart +++ b/lib/ui/shared/sidebar.dart @@ -29,13 +29,62 @@ class Sidebar extends StatelessWidget { return Container( width: 220, height: double.infinity, - decoration: buildBoxDecoration(), + decoration: buildBoxDecoration(professionalProvider.isProModeActive), child: ListView( physics: const ClampingScrollPhysics(), children: [ const Logo(), const SizedBox(height: 8), + // Mode indicator pill + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: professionalProvider.isProModeActive + ? const Color(0xFF16A34A).withOpacity(0.18) + : const Color(0xFF42A4EF).withOpacity(0.14), + border: Border.all( + color: professionalProvider.isProModeActive + ? const Color(0xFF22C55E).withOpacity(0.5) + : const Color(0xFF42A4EF).withOpacity(0.4), + width: 1, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + professionalProvider.isProModeActive + ? Icons.work_outline + : Icons.person_outline, + size: 13, + color: professionalProvider.isProModeActive + ? const Color(0xFF4ADE80) + : const Color(0xFF7DD3FC), + ), + const SizedBox(width: 6), + Text( + professionalProvider.isProModeActive + ? 'Modo Profesional' + : 'Modo Usuario', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: professionalProvider.isProModeActive + ? const Color(0xFF4ADE80) + : const Color(0xFF7DD3FC), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 4), + if (professionalProvider.isProModeActive) ...[ const TextSeparator(text: 'Profesional'), MenuItem( @@ -147,13 +196,20 @@ class Sidebar extends StatelessWidget { navigateTo(Flurorouter.requestProfessionalRoute); } }, - child: Container( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), padding: const EdgeInsets.symmetric(vertical: 11), decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFF42A4EF), Color(0xFF1565C0)], + gradient: LinearGradient( + colors: professionalProvider.isProModeActive + ? [const Color(0xFF374151), const Color(0xFF1F2937)] + : [const Color(0xFF42A4EF), const Color(0xFF1565C0)], ), borderRadius: BorderRadius.circular(10), + border: professionalProvider.isProModeActive + ? Border.all( + color: Colors.white.withOpacity(0.1), width: 1) + : null, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -162,14 +218,20 @@ class Sidebar extends StatelessWidget { professionalProvider.isProModeActive ? Icons.person_outlined : Icons.work_outline, - color: Colors.white, + color: professionalProvider.isProModeActive + ? Colors.white70 + : Colors.white, size: 16, ), const SizedBox(width: 8), Text( - 'Modo ${professionalProvider.isProModeActive ? 'Usuario' : 'Profesional'}', - style: const TextStyle( - color: Colors.white, + professionalProvider.isProModeActive + ? 'Ir a modo Usuario' + : 'Ir a modo Profesional', + style: TextStyle( + color: professionalProvider.isProModeActive + ? Colors.white70 + : Colors.white, fontSize: 13, fontWeight: FontWeight.w600, ), @@ -193,16 +255,15 @@ class Sidebar extends StatelessWidget { ); } - BoxDecoration buildBoxDecoration() => const BoxDecoration( + BoxDecoration buildBoxDecoration(bool isProMode) => BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [ - Color(0xFF0D1B3E), - Color(0xFF0A1628), - ], + colors: isProMode + ? const [Color(0xFF0A1F18), Color(0xFF061510)] + : const [Color(0xFF0D1B3E), Color(0xFF0A1628)], ), - boxShadow: [ + boxShadow: const [ BoxShadow( color: Colors.black38, blurRadius: 12, diff --git a/lib/ui/views/professional_profile_view.dart b/lib/ui/views/professional_profile_view.dart index 32dc3cf..afc7a21 100644 --- a/lib/ui/views/professional_profile_view.dart +++ b/lib/ui/views/professional_profile_view.dart @@ -11,12 +11,17 @@ import 'package:prosapp_web_app/providers/settings_provider.dart'; import 'package:prosapp_web_app/router/router.dart'; import 'package:prosapp_web_app/services/navigation_service.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/inputs/custom_inputs.dart'; -import 'package:prosapp_web_app/ui/labels/custom_labels.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 ────────────────────────────────────────────────────────────────── + class ProfessionalProfileView extends StatefulWidget { const ProfessionalProfileView({super.key}); @@ -26,510 +31,817 @@ class ProfessionalProfileView extends StatefulWidget { } class _ProfessionalProfileViewState extends State { - Usuario? user; - late ProfessionalFormProvider professionalFormProvider; - @override void initState() { super.initState(); - - final authProvider = Provider.of(context, listen: false); - final profileFormProvider = - Provider.of(context, listen: false); - professionalFormProvider = - Provider.of(context, listen: false); - - final proProvider = - Provider.of(context, listen: false); - - proProvider.getProfessional(authProvider.user!.id).then((value) { - professionalFormProvider.setProfesional(value); - }); - - profileFormProvider.user = authProvider.user; - setState(() { - user = authProvider.user; - }); + final auth = Provider.of(context, listen: false); + final pfp = Provider.of(context, listen: false); + final fp = Provider.of(context, listen: false); + pfp.user = auth.user; + Provider.of(context, listen: false) + .getProfessional(auth.user!.id) + .then(fp.setProfesional); } @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - if (constraints.maxWidth < 700) { - return ListView( - physics: const ClampingScrollPhysics(), - children: const [SizedBox(height: 10), _ProfileViewBody()], - ); - } else { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: ListView( - physics: const ClampingScrollPhysics(), - children: const [SizedBox(height: 10), _ProfileViewBody()], - ), - ); - } - }); + return Container( + color: _kBg, + child: ListView( + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.only(bottom: 32), + children: const [_ProfileHeader(), _ProfileForm()], + ), + ); } } -class _ProfileViewBody extends StatelessWidget { - const _ProfileViewBody(); +// ── Profile header card ─────────────────────────────────────────────────── + +class _ProfileHeader extends StatelessWidget { + const _ProfileHeader(); @override Widget build(BuildContext context) { + 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; + return Center( child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 900), - child: const Column( - children: [ - _AvatarContainer(containerFull: true), - _ProfileViewForm(), - ], + constraints: const BoxConstraints(maxWidth: 720), + child: Container( + margin: const EdgeInsets.fromLTRB(16, 20, 16, 0), + decoration: BoxDecoration( + color: _kCard, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.07), + blurRadius: 16, + offset: const Offset(0, 4), + ) + ], + ), + child: Column( + children: [ + // Banner gradient + Container( + height: 110, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [_kAccent, _kPrimary], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: + BorderRadius.vertical(top: Radius.circular(16)), + ), + ), + // Avatar overlapping the banner + Transform.translate( + offset: const Offset(0, -44), + child: Column( + children: [ + Stack( + alignment: Alignment.bottomRight, + children: [ + Container( + width: 88, + height: 88, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: _kCard, 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, + ) + : Container( + color: _kAccent.withOpacity(0.15), + child: const Icon(Icons.person, + size: 44, color: _kAccent), + ), + ), + ), + _CameraButton( + onTap: () async { + 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; + NotificationsService.showBusyIndicator(context); + await pfp.uploadPicture(bytes); + if (!context.mounted) return; + 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), + ), + ), + if (profession != null && profession.isNotEmpty) ...[ + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 3), + decoration: BoxDecoration( + color: _kAccent.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + profession, + style: const TextStyle( + color: _kPrimary, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + const SizedBox(height: 16), + ], + ), + ), + ], + ), ), ), ); } } -class _ProfileViewForm extends StatefulWidget { - const _ProfileViewForm(); - - @override - State<_ProfileViewForm> createState() => _ProfileViewFormState(); -} - -class _ProfileViewFormState extends State<_ProfileViewForm> { - bool _serviceDeliverySwitchValue = false; - bool _serviceSiteSwitchValue = false; - bool _serviceRateSwitchValue = false; - - bool first = true; +class _CameraButton extends StatelessWidget { + final VoidCallback onTap; + const _CameraButton({required this.onTap}); @override Widget build(BuildContext context) { - final profileFormProvider = Provider.of(context); - final settingsProvider = Provider.of(context); - final user = profileFormProvider.user!; + return GestureDetector( + onTap: onTap, + child: Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: _kPrimary, + shape: BoxShape.circle, + border: Border.all(color: _kCard, width: 2), + ), + child: const Icon(Icons.camera_alt, size: 14, color: Colors.white), + ), + ); + } +} - return Consumer( - builder: (context, professionalFormProvider, child) { - if (professionalFormProvider.profesional == null) { - return const Center( - child: CircularProgressIndicator(), +// ── Form ────────────────────────────────────────────────────────────────── + +class _ProfileForm extends StatefulWidget { + const _ProfileForm(); + + @override + State<_ProfileForm> createState() => _ProfileFormState(); +} + +class _ProfileFormState extends State<_ProfileForm> { + bool _delivery = false; + bool _office = false; + bool _rateEnabled = false; + bool _saving = false; + bool _first = true; + + @override + Widget build(BuildContext context) { + final pfp = Provider.of(context); + final settings = Provider.of(context).settings; + + return Consumer(builder: (context, fp, _) { + if (fp.profesional == null || settings == null) { + return const Padding( + padding: EdgeInsets.all(40), + child: Center(child: CircularProgressIndicator(color: _kPrimary)), ); } - if (settingsProvider.settings == null) { - return const Center( - child: CircularProgressIndicator(), - ); - } + final pro = fp.profesional!; + final user = pfp.user!; - final settings = settingsProvider.settings!; - final profesional = professionalFormProvider.profesional!; - if (first) { - first = false; - - if (settings.tarifas) { - _serviceRateSwitchValue = profesional.ratePreferences; + if (_first) { + _first = false; + _rateEnabled = settings.tarifas ? pro.ratePreferences : false; + if (pro.locationPreferences == LocationPreferences.both) { + _delivery = true; + _office = true; + } else if (pro.locationPreferences == LocationPreferences.office) { + _office = true; } else { - _serviceRateSwitchValue = false; - } - - if (profesional.locationPreferences == LocationPreferences.both) { - _serviceSiteSwitchValue = true; - _serviceDeliverySwitchValue = true; - } else if (profesional.locationPreferences == - LocationPreferences.office) { - _serviceSiteSwitchValue = true; - _serviceDeliverySwitchValue = false; - } else if (profesional.locationPreferences == - LocationPreferences.delivery) { - _serviceSiteSwitchValue = false; - _serviceDeliverySwitchValue = true; + _delivery = true; } } - return LayoutBuilder(builder: (context, constraints) { - double columnWidth = constraints.maxWidth < 600 ? 95 : 150; - - return WhiteCard( - title: 'Información profesional', + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 720), child: Form( - key: professionalFormProvider.profileFormKey, + key: fp.profileFormKey, autovalidateMode: AutovalidateMode.always, child: Column( children: [ - const SizedBox(height: 10), - SwitchListTile( - title: const Text('Servicio a domicilio'), - value: _serviceDeliverySwitchValue, - onChanged: (value) { - setState(() { - _serviceDeliverySwitchValue = value; - - if (settings.domicilios == true) { - _serviceDeliverySwitchValue = value; - if (!_serviceDeliverySwitchValue) { - _serviceSiteSwitchValue = true; - } - } else { - _serviceDeliverySwitchValue = false; - } - }); - }, - ), - const SizedBox(height: 10), - SwitchListTile( - title: const Text('Servicio en sitio / consultorio'), - value: _serviceSiteSwitchValue, - onChanged: (value) { - setState(() { - if (settings.domicilios == true) { - _serviceSiteSwitchValue = value; - if (!_serviceSiteSwitchValue) { - _serviceDeliverySwitchValue = true; - } - } else { - _serviceSiteSwitchValue = true; - _serviceDeliverySwitchValue = false; - } - }); - }, - ), - if (_serviceSiteSwitchValue) - Column( + // ── Modalidad ────────────────────────────────────────── + _SectionCard( + icon: Icons.tune_outlined, + title: 'Modalidad de servicio', + child: Column( children: [ - TextFormField( - initialValue: profesional.address, - // validator: (value) { - // if (value == null || value.isEmpty) { - // return 'La dirección es obligatoria'; - // } - // return null; - // }, - onChanged: (value) {}, - decoration: CustomInputs.formInputDecoration( - hint: 'Ingresa tu dirección', - label: 'Dirección', - icon: Icons.location_on_outlined, - ), + _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 SizedBox(height: 12), - TextFormField( - initialValue: profesional.aditionalAddress, - decoration: CustomInputs.formInputDecoration( - hint: 'Ingresa tu piso / apartamento / conjunto', - label: 'Piso / Apartamento / Conjunto', - icon: Icons.email_outlined, - ), - onChanged: (value) { - professionalFormProvider.copyProfesionalWith( - aditionalAddress: value); - }, + 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; + } + }), ), ], ), - if (settings.tarifas == true) ...[ - const SizedBox(height: 20), - SwitchListTile( - title: const Text('Tarifa'), - value: _serviceRateSwitchValue, - onChanged: (value) { - setState(() { - _serviceRateSwitchValue = value; - }); - }, - ), - if (_serviceRateSwitchValue) - Column( + ), + + // ── Dirección ───────────────────────────────────────── + if (_office) + _SectionCard( + icon: Icons.location_on_outlined, + title: 'Dirección del consultorio', + child: Column( children: [ - TextFormField( - keyboardType: TextInputType.number, - initialValue: profesional.rate, - validator: (value) { - if (value == null || value.isEmpty) { - return 'La tarifa es obligatoria'; - } - final numericRegex = RegExp(r'^[0-9]+$'); - if (!numericRegex.hasMatch(value)) { - return 'Por favor, ingresa solo números'; - } - return null; - }, - onChanged: (value) { - professionalFormProvider.copyProfesionalWith( - rate: value); - }, - decoration: CustomInputs.formInputDecoration( - hint: 'Ingresa la tarifa del servicio', - label: 'Tarifa', - icon: Icons.attach_money, - ), + _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), ), ], ), - ], - const SizedBox(height: 20), - Text('Metodos de pago', style: CustomLabels.h3), - CheckboxListTile( - title: const Text('Datafono'), - value: profesional.paymentMethods.datafono, - onChanged: (value) { - professionalFormProvider.copyProfesionalWith( - paymentMethods: profesional.paymentMethods - .copyWith(datafono: value)); - }), - const SizedBox(height: 10), - CheckboxListTile( - title: const Text('Nequi'), - value: profesional.paymentMethods.nequi, - onChanged: (value) { - professionalFormProvider.copyProfesionalWith( - paymentMethods: profesional.paymentMethods - .copyWith(nequi: value)); - }), - const SizedBox(height: 10), - CheckboxListTile( - title: const Text('Transferencia bancaria'), - value: profesional.paymentMethods.transferencia, - onChanged: (value) { - professionalFormProvider.copyProfesionalWith( - paymentMethods: profesional.paymentMethods - .copyWith(transferencia: value)); - }), - const SizedBox(height: 20), - Text('Horarios de atenciòn', style: CustomLabels.h3), - const SizedBox(height: 10), - GestureDetector( - onTap: () { - NavigationService.replaceTo( - Flurorouter.professionalScheduleRoute); - }, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Table( - columnWidths: { - 0: FixedColumnWidth(columnWidth), - }, - border: - TableBorder.all(color: Colors.grey.withOpacity(0.3)), + ), + + // ── Tarifa ──────────────────────────────────────────── + if (settings.tarifas) + _SectionCard( + icon: Icons.payments_outlined, + title: 'Tarifa del servicio', + child: Column( children: [ - _buildTableRow( - 'Lunes', profesional.schedules.monday, context), - _buildTableRow( - 'Martes', profesional.schedules.tuesday, context), - _buildTableRow('Miercoles', - profesional.schedules.wednesday, context), - _buildTableRow( - 'Jueves', profesional.schedules.thursday, context), - _buildTableRow( - 'Viernes', profesional.schedules.friday, context), - _buildTableRow( - 'Sabado', profesional.schedules.saturday, context), - _buildTableRow( - 'Domingo', profesional.schedules.sunday, context), + _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) ...[ + 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), + ), + ], ], ), ), - ), - const SizedBox(height: 20), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 130), - child: ElevatedButton( - onPressed: () async { - LocationPreferences locationPreferences; - - if (_serviceDeliverySwitchValue && - _serviceSiteSwitchValue) { - locationPreferences = LocationPreferences.both; - } else if (_serviceDeliverySwitchValue) { - locationPreferences = LocationPreferences.delivery; - } else if (_serviceSiteSwitchValue) { - locationPreferences = LocationPreferences.office; - } else { - locationPreferences = LocationPreferences.office; - } - - professionalFormProvider.copyProfesionalWith( - locationPreferences: locationPreferences, - ratePreferences: _serviceRateSwitchValue, - ); - - await professionalFormProvider - .updateProfesionalProfileInfo(user.id); - }, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - Colors.blue.shade400, + // ── Métodos de pago ─────────────────────────────────── + _SectionCard( + icon: Icons.credit_card_outlined, + title: 'Métodos de pago', + child: Wrap( + spacing: 10, + runSpacing: 10, + children: [ + _PayChip( + icon: Icons.point_of_sale_outlined, + label: 'Datáfono', + selected: pro.paymentMethods.datafono, + onTap: () => fp.copyProfesionalWith( + paymentMethods: pro.paymentMethods.copyWith( + datafono: !pro.paymentMethods.datafono, + ), + ), ), - shape: - WidgetStateProperty.all(const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(5)), - )), - shadowColor: WidgetStateProperty.all(Colors.transparent), - ), - child: const Text( - 'Guardar', - style: TextStyle(color: Colors.white), + _PayChip( + icon: Icons.phone_android_outlined, + label: 'Nequi', + selected: pro.paymentMethods.nequi, + onTap: () => fp.copyProfesionalWith( + 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, + ), + ), + ), + ], + ), + ), + + // ── 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), + ), + ), + + // ── 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); + }, + style: ElevatedButton.styleFrom( + backgroundColor: _kPrimary, + foregroundColor: Colors.white, + 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), + ), ), ), ), - const SizedBox(height: 10), + const SizedBox(height: 16), ], ), ), - ); - }); + ), + ); }); } - TableRow _buildTableRow( - String day, ScheduleEntity schedules, BuildContext context) { - return TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - decoration: const BoxDecoration( - color: Colors.blueAccent, - ), - child: Text( - day, - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.white, - ), + List _buildScheduleRows(dynamic schedules) { + const days = [ + ('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, + _ => 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( + 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, + ), + ), + ), + ], ), ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text( - timeList(schedules, context), - style: const TextStyle( - color: Colors.black87, - ), - ), - ), - ), - ], - ); + ], + ); + }).toList(); } - String timeList(ScheduleEntity? schedule, BuildContext context) { - if (schedule == null) { - return 'No hay horarios'; + String _formatTime(ScheduleEntity? s) { + if (s == null || !s.enabled) return 'No disponible'; + if (s.range1Hour1 == null || s.range2Hour2 == null) { + return 'Sin horario definido'; } - if (!schedule.enabled) { - return 'No hay horarios'; + if (s.continuousDay) { + return '${ScheduleEntity.getFormatTime(s.range1Hour1)} – ${ScheduleEntity.getFormatTime(s.range2Hour2)}'; } - if (schedule.range1Hour1 == null || schedule.range2Hour2 == null) { - return 'No hay horarios'; - } - if (schedule.continuousDay) { - return '${ScheduleEntity.getFormatTime(schedule.range1Hour1)} a ${ScheduleEntity.getFormatTime(schedule.range2Hour2)}'; - } else { - if (schedule.range1Hour2 == null || schedule.range2Hour1 == null) { - return 'No hay horarios'; - } - - return '${ScheduleEntity.getFormatTime(schedule.range1Hour1)} a ${ScheduleEntity.getFormatTime(schedule.range1Hour2)} - ${ScheduleEntity.getFormatTime(schedule.range2Hour1)} a ${ScheduleEntity.getFormatTime(schedule.range2Hour2)}'; + 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)}'; } } -class _AvatarContainer extends StatelessWidget { - final bool containerFull; +// ── Reusable widgets ────────────────────────────────────────────────────── - const _AvatarContainer({required this.containerFull}); +class _SectionCard extends StatelessWidget { + final IconData icon; + final String title; + final Widget child; + final Widget? trailing; + + const _SectionCard({ + required this.icon, + required this.title, + required this.child, + this.trailing, + }); @override Widget build(BuildContext context) { - final user = Provider.of(context).user!; - final profileFormProvider = Provider.of(context); - - final image = (profileFormProvider.user!.picture == '' || - profileFormProvider.user!.picture == null) - ? const Image(image: AssetImage('no-image.jpg')) - : FadeInImage.assetNetwork( - placeholder: 'loader.gif', - fit: BoxFit.cover, - image: profileFormProvider.user!.picture!, - ); - - return WhiteCard( - width: containerFull ? null : 250, - child: SizedBox( - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(user.name, style: CustomLabels.h2), - const SizedBox(height: 10), - SizedBox( - width: 160, - height: 160, - child: Stack( - children: [ - SizedBox( - width: 200, - height: 200, - child: ClipOval(child: image), + return Container( + margin: const EdgeInsets.fromLTRB(16, 12, 16, 0), + decoration: BoxDecoration( + color: _kCard, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + 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), ), - Positioned( - bottom: 5, - right: 5, - child: Container( - width: 45, - height: 45, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - border: Border.all(color: Colors.white, width: 5), - ), - child: FloatingActionButton( - onPressed: () async { - FilePickerResult? result = - await FilePicker.platform.pickFiles( - withData: true, - ); - - if (result != null) { - PlatformFile file = result.files.first; - Uint8List? fileBytes = file.bytes; - - if (fileBytes != null) { - NotificationsService.showBusyIndicator(context); - await profileFormProvider - .uploadPicture(fileBytes); - Provider.of(context, listen: false) - .refreshUser(); - - Navigator.pop(context); - } - } else {} - }, - backgroundColor: Colors.indigo, - elevation: 0, - child: const Icon( - Icons.camera_alt_outlined, - size: 20, - color: Colors.white, - ), - ), + 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, + ), + ], + ), + ); + } +} + +class _ToggleRow extends StatelessWidget { + final IconData icon; + final String title; + 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, + }); + + @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, + ), + ], + ), + ); + } +} + +class _PayChip extends StatelessWidget { + final IconData icon; + final String label; + final bool selected; + final VoidCallback onTap; + + const _PayChip({ + required this.icon, + required this.label, + required this.selected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: selected + ? _kPrimary.withOpacity(0.08) + : Colors.grey.shade50, + border: Border.all( + color: selected ? _kPrimary : Colors.grey.shade200, + 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(height: 20), + 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), + ), ], ), ), ); } } + +class _Field extends StatelessWidget { + final String? initial; + final String hint; + final String label; + final IconData icon; + 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, + }); + + @override + Widget build(BuildContext context) { + return TextFormField( + initialValue: initial, + keyboardType: keyboardType, + validator: validator, + onChanged: onChanged, + style: const TextStyle(fontSize: 13), + 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), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: _kPrimary, width: 1.5), + ), + filled: true, + fillColor: const Color(0xFFF8FAFC), + ), + ); + } +} + +class _Divider extends StatelessWidget { + const _Divider(); + + @override + Widget build(BuildContext context) => + Divider(height: 16, color: Colors.grey.shade100); +}