diff --git a/lib/blocs/service_bloc/service_bloc.dart b/lib/blocs/service_bloc/service_bloc.dart index f500c24..88f084c 100644 --- a/lib/blocs/service_bloc/service_bloc.dart +++ b/lib/blocs/service_bloc/service_bloc.dart @@ -17,10 +17,14 @@ class ServiceBloc extends Bloc { : _serviceRepository = serviceRepository, super(CreateServiceInitial()) { on(_onCreateService); + on(_onLoadService); + on(_onUpdateServiceStatus); } void _onCreateService(CreateService event, Emitter emit) async { try { + emit(CreateServiceLoading()); + ServiceEntity service = ServiceEntity( professionalId: event.professionalId, professionalScored: false, @@ -40,12 +44,37 @@ class ServiceBloc extends Bloc { location: event.location, ); - log('xd -- $service'); + String serviceId = await _serviceRepository.createService(service); - await _serviceRepository.createService(service); - - emit(const CreateServiceSuccess()); + emit(CreateServiceSuccess(serviceId)); } catch (e) { + log(e.toString()); + emit(CreateServiceFailure()); + } + } + + void _onLoadService(LoadService event, Emitter emit) async { + try { + final serviceStream = _serviceRepository.getService(event.serviceId); + + await for (var service in serviceStream) { + emit(ServiceLoaded(service)); + } + } catch (e) { + log(e.toString()); + emit(CreateServiceFailure()); + } + } + + void _onUpdateServiceStatus( + UpdateServiceStatus event, Emitter emit) async { + try { + emit(CreateServiceLoading()); + await _serviceRepository.updateServiceStatus( + event.serviceId, event.newStatus); + emit(ServiceStatusUpdated(event.newStatus)); + } catch (e) { + log(e.toString()); emit(CreateServiceFailure()); } } diff --git a/lib/blocs/service_bloc/service_event.dart b/lib/blocs/service_bloc/service_event.dart index 0ba543c..f8a56e4 100644 --- a/lib/blocs/service_bloc/service_event.dart +++ b/lib/blocs/service_bloc/service_event.dart @@ -7,6 +7,25 @@ abstract class ServiceEvent extends Equatable { List get props => []; } +class LoadService extends ServiceEvent { + final String serviceId; + + const LoadService(this.serviceId); + + @override + List get props => [serviceId]; +} + +class UpdateServiceStatus extends ServiceEvent { + final String serviceId; + final ServiceStatus newStatus; + + const UpdateServiceStatus(this.serviceId, this.newStatus); + + @override + List get props => [serviceId, newStatus]; +} + class CreateService extends ServiceEvent { final String professionalId; final bool? professionalScored; diff --git a/lib/blocs/service_bloc/service_state.dart b/lib/blocs/service_bloc/service_state.dart index 3234de7..2db804c 100644 --- a/lib/blocs/service_bloc/service_state.dart +++ b/lib/blocs/service_bloc/service_state.dart @@ -7,6 +7,15 @@ abstract class ServiceState extends Equatable { List get props => []; } +class ServiceLoaded extends ServiceState { + final ServiceEntity service; + + const ServiceLoaded(this.service); + + @override + List get props => [service]; +} + class CreateServiceInitial extends ServiceState {} class CreateServiceFailure extends ServiceState {} @@ -14,8 +23,19 @@ class CreateServiceFailure extends ServiceState {} class CreateServiceLoading extends ServiceState {} class CreateServiceSuccess extends ServiceState { - const CreateServiceSuccess(); + final String serviceId; + + const CreateServiceSuccess(this.serviceId); @override - List get props => []; + List get props => [serviceId]; +} + +class ServiceStatusUpdated extends ServiceState { + final ServiceStatus newStatus; + + const ServiceStatusUpdated(this.newStatus); + + @override + List get props => [newStatus]; } diff --git a/lib/components/general_drawer.dart b/lib/components/general_drawer.dart index 1815aca..a4056af 100644 --- a/lib/components/general_drawer.dart +++ b/lib/components/general_drawer.dart @@ -9,11 +9,13 @@ import 'package:prosappco/components/general_drawer_item.dart'; import 'package:prosappco/screens/configuration/configuration_screen.dart'; import 'package:prosappco/screens/configuration/configuration_support_screen.dart'; import 'package:prosappco/screens/lists/professional_list_screen.dart'; +import 'package:prosappco/screens/lists/user_service_list_screen.dart'; import 'package:prosappco/screens/professional/professional_calendar_screen.dart'; import 'package:prosappco/screens/professional/professional_denied_screen.dart'; import 'package:prosappco/screens/professional/professional_form_screen.dart'; import 'package:prosappco/screens/professional/professional_pending_screen.dart'; import 'package:prosappco/screens/professional/professional_profile_screen.dart'; +import 'package:prosappco/screens/profile/profile_screen.dart'; import 'package:prosappco/screens/user/user_history_services_screen.dart'; import 'package:prosappco/screens/web/web_view_screen.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -81,7 +83,7 @@ class GeneralDrawer extends StatelessWidget { context, CupertinoPageRoute( builder: (context) => - const ProfessionalListScreen(), + const UserServiceListScreen(), // const UserServicesScreen(), ), ); @@ -230,54 +232,79 @@ class GeneralDrawer extends StatelessWidget { if (myUserState.status == MyUserStatus.success) { final user = myUserState.user!; - switch (user.proState) { - case ProState.active: - Navigator.pop(context); + if (user.name != null && + user.email != null && + user.city != null && + user.phone != null) { + switch (user.proState) { + case ProState.active: + Navigator.pop(context); - context.read().add(const SwitchProModeEvent()); + context + .read() + .add(const SwitchProModeEvent()); - break; - case ProState.inactive: - Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => const ProfessionalFormScreen(), - ), - ); - break; - case ProState.pending: - Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => const ProfessionalPendingScreen(), - ), - ); - break; - case ProState.denied: - Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => const ProfessionalDeniedScreen(), - ), - ); - break; + break; + case ProState.inactive: + Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => const ProfessionalFormScreen(), + ), + ); + break; + case ProState.pending: + Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => const ProfessionalPendingScreen(), + ), + ); + break; + case ProState.denied: + Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => const ProfessionalDeniedScreen(), + ), + ); + break; + } + } else { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Por favor, completa tu perfil'), + )); + + Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => const ProfileScreen(), + ), + ); } } else {} }, style: FilledButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - padding: const EdgeInsets.symmetric(vertical: 15), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - )), + backgroundColor: Theme.of(context).colorScheme.primary, + padding: const EdgeInsets.symmetric(vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), child: (state is LoadedModeProState) ? Text( state.isProModeActive ? 'Modo cliente' : 'Modo profesional', style: const TextStyle(color: Colors.white, fontSize: 18), ) - : const CircularProgressIndicator( - color: Colors.white, + : const Text( + 'Modo profesional', + style: TextStyle(color: Colors.white, fontSize: 18), ), + + // const CircularProgressIndicator( + // color: Colors.white, + // ), ); } diff --git a/lib/components/general_primary_button.dart b/lib/components/general_primary_button.dart index 1b664d4..9bf0115 100644 --- a/lib/components/general_primary_button.dart +++ b/lib/components/general_primary_button.dart @@ -4,12 +4,14 @@ class GeneralPrimaryButton extends StatelessWidget { final VoidCallback onPressed; final String label; final bool isEnabled; + final Color? color; const GeneralPrimaryButton({ super.key, required this.onPressed, required this.label, this.isEnabled = true, + this.color, }); @override @@ -17,7 +19,7 @@ class GeneralPrimaryButton extends StatelessWidget { return ElevatedButton( onPressed: isEnabled ? onPressed : null, style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, + backgroundColor: color ?? Theme.of(context).colorScheme.primary, elevation: 5, minimumSize: Size( MediaQuery.of(context).size.width * 0.5, diff --git a/lib/screens/lists/user_service_list_screen.dart b/lib/screens/lists/user_service_list_screen.dart new file mode 100644 index 0000000..4e291d9 --- /dev/null +++ b/lib/screens/lists/user_service_list_screen.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class UserServiceListScreen extends StatefulWidget { + const UserServiceListScreen({super.key}); + + @override + State createState() => _UserServiceListScreenState(); +} + +class _UserServiceListScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Mis Servicios'), + ), + body: ListView.builder( + itemCount: 10, + itemBuilder: (_, __) { + return ListTile( + onTap: () {}, + title: Text('Servicio ${__ + 1}'), + ); + }), + ); + } +} diff --git a/lib/screens/professional/components/schedule_item.dart b/lib/screens/professional/components/schedule_item.dart index 3b522b5..d0d7b7e 100644 --- a/lib/screens/professional/components/schedule_item.dart +++ b/lib/screens/professional/components/schedule_item.dart @@ -72,7 +72,7 @@ class ScheduleItem extends StatelessWidget { children: [ timePickerField( context: context, - text: schedule.getFormatTime(schedule.range1Hour1), + text: ScheduleEntity.getFormatTime(schedule.range1Hour1), onPick: (pickedTime) { onChanged.call(schedule.copyWith(range1Hour1: pickedTime)); }, @@ -88,7 +88,7 @@ class ScheduleItem extends StatelessWidget { visible: !schedule.continuousDay, child: timePickerField( context: context, - text: schedule.getFormatTime(schedule.range1Hour2), + text: ScheduleEntity.getFormatTime(schedule.range1Hour2), onPick: (pickedTime) { onChanged.call(schedule.copyWith(range1Hour2: pickedTime)); }, @@ -104,7 +104,7 @@ class ScheduleItem extends StatelessWidget { visible: !schedule.continuousDay, child: timePickerField( context: context, - text: schedule.getFormatTime(schedule.range2Hour1), + text: ScheduleEntity.getFormatTime(schedule.range2Hour1), onPick: (pickedTime) { onChanged.call(schedule.copyWith(range2Hour1: pickedTime)); }, @@ -122,7 +122,7 @@ class ScheduleItem extends StatelessWidget { ), timePickerField( context: context, - text: schedule.getFormatTime(schedule.range2Hour2), + text: ScheduleEntity.getFormatTime(schedule.range2Hour2), onPick: (pickedTime) { onChanged.call(schedule.copyWith(range2Hour2: pickedTime)); }, diff --git a/lib/screens/professional/professional_profile_screen.dart b/lib/screens/professional/professional_profile_screen.dart index 7645506..8e97d90 100644 --- a/lib/screens/professional/professional_profile_screen.dart +++ b/lib/screens/professional/professional_profile_screen.dart @@ -510,6 +510,8 @@ class _ProfessionalProfileScreenState extends State { context.read().add( UpdateProfessionalBannerInfo(fileBanner: _imageFile?.path), ); + + log('xd -- si pasa'); }, label: 'Guardar', ), @@ -588,13 +590,13 @@ class _ProfessionalProfileScreenState extends State { return 'N/A'; } if (schedule.continuousDay) { - return '${schedule.range1Hour1?.format(context).toString()} - ${schedule.range2Hour2?.format(context).toString()}'; + return '${ScheduleEntity.getFormatTime(schedule.range1Hour1)} - ${ScheduleEntity.getFormatTime(schedule.range2Hour2)}'; } else { if (schedule.range1Hour2 == null || schedule.range2Hour1 == null) { return 'N/A'; } - return '${schedule.range1Hour1?.format(context).toString()} - ${schedule.range1Hour2?.format(context).toString()}; ${schedule.range2Hour1?.format(context).toString()} - ${schedule.range2Hour2?.format(context).toString()}'; + return '${ScheduleEntity.getFormatTime(schedule.range1Hour1)} - ${ScheduleEntity.getFormatTime(schedule.range1Hour2)}; ${ScheduleEntity.getFormatTime(schedule.range2Hour1)} - ${ScheduleEntity.getFormatTime(schedule.range2Hour2)}'; } } } diff --git a/lib/screens/professional/professional_schedule_screen.dart b/lib/screens/professional/professional_schedule_screen.dart index ab60541..6ebad56 100644 --- a/lib/screens/professional/professional_schedule_screen.dart +++ b/lib/screens/professional/professional_schedule_screen.dart @@ -31,66 +31,69 @@ class _ProfessionalScheduleScreenState title: const Text('Horario'), ), body: SingleChildScrollView( - child: Column( - children: [ - ScheduleItem( - label: "Lunes", - schedule: schedules.monday, - onChanged: (s) { - setState(() => schedules = schedules.copyWith(monday: s)); - }, - ), - ScheduleItem( - label: "Martes", - schedule: schedules.thursday, - onChanged: (s) { - setState(() => schedules = schedules.copyWith(thursday: s)); - }, - ), - ScheduleItem( - label: "Miercoles", - schedule: schedules.wednesday, - onChanged: (s) { - setState(() => schedules = schedules.copyWith(wednesday: s)); - }, - ), - ScheduleItem( - label: "Jueves", - schedule: schedules.tuesday, - onChanged: (s) { - setState(() => schedules = schedules.copyWith(tuesday: s)); - }, - ), - ScheduleItem( - label: "Viernes", - schedule: schedules.friday, - onChanged: (s) { - setState(() => schedules = schedules.copyWith(friday: s)); - }, - ), - ScheduleItem( - label: "Sabado", - schedule: schedules.saturday, - onChanged: (s) { - setState(() => schedules = schedules.copyWith(saturday: s)); - }, - ), - ScheduleItem( - label: "Domingo", - schedule: schedules.sunday, - onChanged: (s) { - setState(() => schedules = schedules.copyWith(sunday: s)); - }, - ), - const SizedBox(height: 20), - GeneralPrimaryButton( - label: "Guardar", - onPressed: () { - Navigator.of(context).pop(schedules); - }, - ), - const SizedBox(height: 20), - ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Column( + children: [ + ScheduleItem( + label: "Lunes", + schedule: schedules.monday, + onChanged: (s) { + setState(() => schedules = schedules.copyWith(monday: s)); + }, + ), + ScheduleItem( + label: "Martes", + schedule: schedules.thursday, + onChanged: (s) { + setState(() => schedules = schedules.copyWith(thursday: s)); + }, + ), + ScheduleItem( + label: "Miercoles", + schedule: schedules.wednesday, + onChanged: (s) { + setState(() => schedules = schedules.copyWith(wednesday: s)); + }, + ), + ScheduleItem( + label: "Jueves", + schedule: schedules.tuesday, + onChanged: (s) { + setState(() => schedules = schedules.copyWith(tuesday: s)); + }, + ), + ScheduleItem( + label: "Viernes", + schedule: schedules.friday, + onChanged: (s) { + setState(() => schedules = schedules.copyWith(friday: s)); + }, + ), + ScheduleItem( + label: "Sabado", + schedule: schedules.saturday, + onChanged: (s) { + setState(() => schedules = schedules.copyWith(saturday: s)); + }, + ), + ScheduleItem( + label: "Domingo", + schedule: schedules.sunday, + onChanged: (s) { + setState(() => schedules = schedules.copyWith(sunday: s)); + }, + ), + const SizedBox(height: 20), + GeneralPrimaryButton( + label: "Guardar", + onPressed: () { + Navigator.of(context).pop(schedules); + }, + ), + const SizedBox(height: 20), + ], + ), ), ), ); diff --git a/lib/screens/profile/profile_register_email_screen.dart b/lib/screens/profile/profile_register_email_screen.dart index 6183a95..59f52a2 100644 --- a/lib/screens/profile/profile_register_email_screen.dart +++ b/lib/screens/profile/profile_register_email_screen.dart @@ -6,6 +6,8 @@ import 'package:prosappco/blocs/auth_bloc/auth_bloc.dart'; import 'package:prosappco/blocs/my_user_bloc/my_user_bloc.dart'; import 'package:prosappco/components/general_primary_button.dart'; +final _formKey = GlobalKey(); + class ProfileRegisterEmailScreen extends StatefulWidget { const ProfileRegisterEmailScreen({super.key}); @@ -14,13 +16,38 @@ class ProfileRegisterEmailScreen extends StatefulWidget { _ProfileRegisterEmailScreenState(); } -class _ProfileRegisterEmailScreenState extends State { +class _ProfileRegisterEmailScreenState + extends State { final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _confirmPasswordController = + TextEditingController(); late final AuthBloc authBloc; late String verificationCode; + String? validateEmail(String? email) { + RegExp emailRegex = RegExp(r'^[\w\.-]+@[\w-]+\.\w{2,3}(\.\w{2,3})?$'); + final isEmailValid = emailRegex.hasMatch(email ?? ''); + + if (isEmailValid) { + return null; + } else { + return 'Ingresa un correo electronico valido'; + } + } + + String? validatePassword(String? password) { + RegExp passwordRegex = RegExp(r'^(?=.*?[0-9])'); + final isPasswordValid = passwordRegex.hasMatch(password ?? ''); + + if (isPasswordValid) { + return null; + } else { + return 'La contraseña debe tener al menos un número'; + } + } + @override void initState() { super.initState(); @@ -31,6 +58,7 @@ class _ProfileRegisterEmailScreenState extends State void dispose() { _emailController.dispose(); _passwordController.dispose(); + _confirmPasswordController.dispose(); super.dispose(); } @@ -46,7 +74,6 @@ class _ProfileRegisterEmailScreenState extends State child: BlocConsumer( listener: (context, state) { if (state is AuthStateSuccess) { - // here update email and password autentication authBloc.add( AuthEventAddEmailAndPassword( email: _emailController.text, @@ -82,7 +109,6 @@ class _ProfileRegisterEmailScreenState extends State numberOfFields: 6, fieldWidth: 35, borderColor: const Color(0xFF512DA8), - // showFieldAsBox: true, onCodeChanged: (String code) { verificationCode = code; }, @@ -110,7 +136,6 @@ class _ProfileRegisterEmailScreenState extends State @override Widget build(BuildContext context) { - // _showLoginModal(context); return BlocProvider( create: (context) => authBloc, child: BlocListener( @@ -126,9 +151,7 @@ class _ProfileRegisterEmailScreenState extends State Navigator.of(context).pop(); } - if (state is AuthStateRequiresRecentLogin) { - // _showLoginModal(context, state); - } + if (state is AuthStateRequiresRecentLogin) {} }, child: BlocBuilder( builder: (context, state) { @@ -139,112 +162,113 @@ class _ProfileRegisterEmailScreenState extends State appBar: AppBar( title: const Text('Agregar correo'), ), - body: Center( - // Centro del contenido - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 40, vertical: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextFormField( - controller: _emailController, - decoration: const InputDecoration( - labelText: 'Email', - prefixIcon: Icon(Icons.email_rounded), - hintText: 'Email', - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10.0), - )), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.red), - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Colors.red, width: 2.0), - ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 40, vertical: 10), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + controller: _emailController, + decoration: InputDecoration( + labelText: 'Email', + prefixIcon: const Icon(Icons.email_rounded), + hintText: 'Email', + border: inputBorder(), + errorBorder: inputBorderError(), + focusedErrorBorder: inputBorderFocus(), + ), + validator: validateEmail, + autovalidateMode: + AutovalidateMode.onUserInteraction, + ), + const SizedBox(height: 20), + TextFormField( + controller: _passwordController, + obscureText: true, + decoration: InputDecoration( + labelText: 'Contraseña', + prefixIcon: const Icon(Icons.lock_rounded), + hintText: 'Contraseña', + border: inputBorder(), + errorBorder: inputBorderError(), + focusedErrorBorder: inputBorderFocus(), + ), + validator: validatePassword, + autovalidateMode: + AutovalidateMode.onUserInteraction, + ), + const SizedBox(height: 20), + TextFormField( + controller: _confirmPasswordController, + obscureText: true, + decoration: InputDecoration( + labelText: 'Confirmar contraseña', + prefixIcon: const Icon(Icons.lock_rounded), + hintText: 'Contraseña', + border: inputBorder(), + errorBorder: inputBorderError(), + focusedErrorBorder: inputBorderFocus(), + ), + validator: (value) { + if (value != _passwordController.text) { + return 'Las contraseñas no coinciden'; + } + if (value!.isEmpty) { + return 'La contraseña es obligatoria'; + } + return null; + }, + autovalidateMode: + AutovalidateMode.onUserInteraction, + ), + const SizedBox(height: 30), + ], ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Por favor, ingrese su contraseña'; - } - return null; - }, ), - const SizedBox(height: 20), - TextFormField( - controller: _passwordController, - obscureText: true, - decoration: const InputDecoration( - labelText: 'Contraseña', - prefixIcon: Icon(Icons.lock_rounded), - hintText: 'Contraseña', - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10.0), - )), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.red), - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Colors.red, width: 2.0), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Por favor, ingrese su contraseña'; - } - return null; - }, - ), - const SizedBox(height: 20), - TextFormField( - controller: _passwordController, - obscureText: true, - decoration: const InputDecoration( - labelText: 'Confirmar contraseña', - prefixIcon: Icon(Icons.lock_rounded), - hintText: 'Contraseña', - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10.0), - )), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.red), - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Colors.red, width: 2.0), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Por favor, ingrese su contraseña'; - } - return null; - }, - ), - const SizedBox(height: 30), - GeneralPrimaryButton( - onPressed: () { - if (_emailController.text.isEmpty) { - return; - } - - if (_passwordController.text.isEmpty) { - return; - } - _showLoginModal(context, state); - }, - label: 'Guardar', - ), - ], + ), ), ), - ), + const Divider( + height: 1, + thickness: 0.5, + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 15, + ), + child: FilledButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + _showLoginModal(context, state); + } + }, + style: FilledButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.primary, + padding: const EdgeInsets.symmetric(vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Container( + alignment: Alignment.center, + width: double.infinity, + child: const Text( + 'Guardar', + style: TextStyle(color: Colors.white, fontSize: 18), + ), + ), + ), + ), + ], ), ); } else { @@ -255,4 +279,27 @@ class _ProfileRegisterEmailScreenState extends State ), ); } + + OutlineInputBorder inputBorderFocus() { + return OutlineInputBorder( + borderSide: BorderSide( + color: Colors.red, + width: 2.0, + ), + ); + } + + OutlineInputBorder inputBorderError() { + return OutlineInputBorder( + borderSide: BorderSide(color: Colors.red), + ); + } + + OutlineInputBorder inputBorder() { + return OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10.0), + ), + ); + } } diff --git a/lib/screens/profile/profile_screen.dart b/lib/screens/profile/profile_screen.dart index 1a40326..d803a88 100644 --- a/lib/screens/profile/profile_screen.dart +++ b/lib/screens/profile/profile_screen.dart @@ -98,180 +98,201 @@ class _ProfileScreenState extends State { _birthdayController.text = state.user!.birthday ?? ''; _genderController.text = state.user!.gender ?? ''; - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 40, vertical: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - pictureWidget(state, context), - const SizedBox(height: 30), - TextFormField( - controller: _nameController, - decoration: const InputDecoration( - labelText: 'Nombre', - prefixIcon: Icon(Icons.person), - hintText: 'Nombre (obligatorio)', - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10.0), - )), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.red), - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Colors.red, width: 2.0), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Por favor, ingrese su nombre'; - } - return null; - }, - ), - const SizedBox(height: 20.0), - TextFormField( - controller: _cityController, - readOnly: true, - onTap: () async { - final cityName = await Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const CityListScreen(); + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 40, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + pictureWidget(state, context), + const SizedBox(height: 30), + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Nombre', + prefixIcon: Icon(Icons.person), + hintText: 'Nombre (obligatorio)', + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10.0), + )), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.red), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.red, width: 2.0), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Por favor, ingrese su nombre'; + } + return null; }, ), - ); + const SizedBox(height: 20.0), + TextFormField( + controller: _cityController, + readOnly: true, + onTap: () async { + final cityName = await Navigator.push( + context, + CupertinoPageRoute( + builder: (BuildContext context) { + return const CityListScreen(); + }, + ), + ); - if (cityName != null) { - _cityController.text = cityName; - } - }, - decoration: const InputDecoration( - labelText: 'Ciudad', - prefixIcon: Icon(Icons.near_me_rounded), - hintText: 'Selecciona tu ciudad', - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10.0), - )), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.red), - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Colors.red, width: 2.0), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Por favor, ingrese su nombre'; - } - return null; - }, - ), - _birthdayController.text.isEmpty && - _genderController.text.isEmpty - ? Column( - children: [ - const SizedBox(height: 20.0), - BirthdayPicker( - onDateSelected: (birthDay) { - _birthdayController.text = - DateFormat('dd/MM/yyyy') - .format(birthDay); - }, - controller: _birthdayController, + if (cityName != null) { + _cityController.text = cityName; + } + }, + decoration: const InputDecoration( + labelText: 'Ciudad', + prefixIcon: Icon(Icons.near_me_rounded), + hintText: 'Selecciona tu ciudad', + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10.0), + )), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.red), ), - const SizedBox(height: 20.0), - GenderDropdown( - controller: _genderController, + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.red, width: 2.0), ), - ], - ) - : const SizedBox(), - const SizedBox(height: 20), - ProfileItem( - title: 'Configurar inicio de sesión con correo', - subtitle: _emailController.text, - leading: Icons.email_rounded, - onTap: () { - if (state.user!.name == null || - state.user!.name == '') { - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Por favor, ingrese su nombre'))); - return; - } - - if (state.user!.city == null || - state.user!.city == '') { - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Por favor, ingrese su nombre'))); - return; - } - - Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - _emailController.text.isEmpty - ? ProfileRegisterEmailScreen() - : ProfileUpdatePasswordScreen( - email: _emailController.text)), - ); - }, - ), - const SizedBox(height: 20), - ProfileItem( - title: 'Configurar inicio de sesión con celular', - subtitle: _phoneController.text, - leading: Icons.phone_iphone_rounded, - onTap: () { - if (state.user!.name == null || - state.user!.name == '') { - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Por favor, ingrese su nombre'))); - return; - } - - if (state.user!.city == null || - state.user!.city == '') { - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Por favor, ingrese su nombre'))); - return; - } - - if (_phoneController.text.isEmpty) { - Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - const ProfileRegisterPhoneScreen(), ), - ); - } - }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Por favor, ingrese su nombre'; + } + return null; + }, + ), + _birthdayController.text.isEmpty && + _genderController.text.isEmpty + ? Column( + children: [ + const SizedBox(height: 20.0), + BirthdayPicker( + onDateSelected: (birthDay) { + _birthdayController.text = + DateFormat('dd/MM/yyyy') + .format(birthDay); + }, + controller: _birthdayController, + ), + const SizedBox(height: 20.0), + GenderDropdown( + controller: _genderController, + ), + ], + ) + : const SizedBox(), + const SizedBox(height: 20), + ProfileItem( + title: 'Configurar inicio de sesión con correo', + subtitle: _emailController.text, + leading: Icons.email_rounded, + onTap: () { + if (state.user!.name == null || + state.user!.name == '') { + ScaffoldMessenger.of(context) + .clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Por favor, ingrese su nombre'))); + return; + } + + if (state.user!.city == null || + state.user!.city == '') { + ScaffoldMessenger.of(context) + .clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Por favor, ingrese su ciudad'))); + return; + } + + Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => _emailController + .text.isEmpty + ? ProfileRegisterEmailScreen() + : ProfileUpdatePasswordScreen( + email: _emailController.text)), + ); + }, + ), + const SizedBox(height: 20), + ProfileItem( + title: + 'Configurar inicio de sesión con celular', + subtitle: _phoneController.text, + leading: Icons.phone_iphone_rounded, + onTap: () { + if (state.user!.name == null || + state.user!.name == '') { + ScaffoldMessenger.of(context) + .clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Por favor, ingrese su nombre'))); + return; + } + + if (state.user!.city == null || + state.user!.city == '') { + ScaffoldMessenger.of(context) + .clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Por favor, ingrese su nombre'))); + return; + } + + if (_phoneController.text.isEmpty) { + Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => + const ProfileRegisterPhoneScreen(), + ), + ); + } + }, + ), + const SizedBox(height: 10), + ], + ), ), - const SizedBox(height: 60.0), - saveButton(state, context), - ], + ), ), - ), + const Divider( + height: 1, + thickness: 0.5, + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 15, + ), + child: saveButton(state, context), + ), + ], ); } else { return const Center(child: CircularProgressIndicator()); @@ -302,6 +323,8 @@ class _ProfileScreenState extends State { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Por favor, ingrese su ciudad'))); + + return; } final myUser = state.user!.copyWith( @@ -325,17 +348,14 @@ class _ProfileScreenState extends State { Navigator.pop(context); }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - padding: const EdgeInsets.symmetric(vertical: 5), + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), + borderRadius: BorderRadius.circular(10), ), - shadowColor: Colors.grey, - // elevation: 0, ), child: Container( - constraints: const BoxConstraints(maxWidth: 300.0, minHeight: 50.0), alignment: Alignment.center, child: isLoading ? const CircularProgressIndicator( @@ -345,8 +365,7 @@ class _ProfileScreenState extends State { 'Actualizar', style: TextStyle( color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, + fontSize: 18, ), ), ), diff --git a/lib/screens/user/user_calendar_screen.dart b/lib/screens/user/user_calendar_screen.dart index 0d8235a..8aa6124 100644 --- a/lib/screens/user/user_calendar_screen.dart +++ b/lib/screens/user/user_calendar_screen.dart @@ -350,7 +350,7 @@ class UserCalendarScreenState extends State { ), ), title: Text( - time.format(context), + ScheduleEntity.getFormatTime(time) ?? '', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), ), subtitle: const Text( diff --git a/lib/screens/user/user_map_screen.dart b/lib/screens/user/user_map_screen.dart index 5f74711..a7d13f7 100644 --- a/lib/screens/user/user_map_screen.dart +++ b/lib/screens/user/user_map_screen.dart @@ -1,17 +1,25 @@ import 'dart:async'; +import 'dart:developer'; +import 'dart:io'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_polyline_points/flutter_polyline_points.dart'; import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:injector/injector.dart'; import 'package:intl/intl.dart'; +import 'package:professional_repository/professional_repository.dart'; import 'package:prosappco/blocs/my_user_bloc/my_user_bloc.dart'; import 'package:prosappco/blocs/professional_list_bloc/professional_list_bloc.dart'; import 'package:prosappco/blocs/service_bloc/service_bloc.dart'; +import 'package:prosappco/constansts.dart'; +import 'package:prosappco/screens/lists/city_list_screen.dart'; import 'package:prosappco/screens/lists/professional_list_screen.dart'; +import 'package:prosappco/screens/user/user_service_screen.dart'; +import 'package:prosappco/screens/web/web_view_screen.dart'; import 'package:prosappco/utils/time_of_day_extension.dart'; import 'package:service_repository/service_repository.dart'; import 'package:setting_repository/setting_repository.dart'; @@ -42,13 +50,42 @@ class _UserMapScreenState extends State { ServiceLocationPreferences? serviceLocationPreference; UserProfessional? profesionalSeleccionado; + Map polylines = {}; + bool isClearButtonVisible = false; + bool isLoading = false; + + Set markers = {}; + BitmapDescriptor? _markerIcon; + @override void initState() { super.initState(); _loadSettings(); + + getLocation(); + + if (Platform.isAndroid) { + BitmapDescriptor.fromAssetImage( + const ImageConfiguration(size: Size(2, 2)), + 'images/pro_marke_android.png', + ).then((icon) { + setState(() { + _markerIcon = icon; + }); + }); + } else { + BitmapDescriptor.fromAssetImage( + const ImageConfiguration(size: Size(1, 1)), + 'images/pro_marke.png', + ).then((icon) { + setState(() { + _markerIcon = icon; + }); + }); + } } void _loadSettings() { @@ -63,115 +100,143 @@ class _UserMapScreenState extends State { Widget build(BuildContext context) { return BlocProvider( create: (context) => Injector.appInstance.get(), - child: BlocBuilder( + child: BlocConsumer( + listener: (context, serviceState) { + if (serviceState is CreateServiceLoading) { + isLoading = true; + } + if (serviceState is CreateServiceFailure) { + isLoading = false; + } + + if (serviceState is CreateServiceSuccess) { + isLoading = false; + + Navigator.push( + context, + CupertinoPageRoute( + builder: (BuildContext context) { + return UserServiceScreen(serviceId: serviceState.serviceId); + }, + ), + ); + } + }, builder: (context, state) { - return Column( - children: [ - Expanded( - child: Stack( - children: [ - GoogleMap( - myLocationEnabled: true, - // polylines: Set.of(polylines.values), - onMapCreated: (GoogleMapController controller) { - _mapController.complete(controller); - }, - onCameraIdle: () { - getLocationName( - coordenadas.latitude, coordenadas.longitude) - .then((value) => setState(() { - _addressController.text = value; - })); - }, - onCameraMove: (position) { - if (serviceLocationPreference != - ServiceLocationPreferences.office) { - coordenadas = position.target; - } - }, - initialCameraPosition: const CameraPosition( - target: LatLng(7.1253900, -73.1198000), - zoom: 16, - ), - myLocationButtonEnabled: false, - ), - Positioned( - top: 10, - left: 0, - child: Builder( - builder: (context) { - return ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - shape: const CircleBorder(), - elevation: 3, - minimumSize: const Size(50, 50), + return BlocBuilder( + builder: (context, myUserState) { + return Column( + children: [ + Expanded( + child: Stack( + children: [ + GoogleMap( + myLocationEnabled: true, + polylines: Set.of(polylines.values), + onMapCreated: (GoogleMapController controller) { + _mapController.complete(controller); + }, + markers: { + ...markers, + Marker( + markerId: const MarkerId('currentLocation'), + position: _currentP ?? const LatLng(0, 0), ), - child: const Icon( - Icons.menu, - color: Colors.black, - size: 35, - ), - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - ); - }, - ), - ), - const Positioned( - bottom: 30, - right: 0, - left: 0, - top: 0, - child: Icon( - Icons.location_on, - size: 40, - color: Color(0xFFFF0000), - ), - ), - Positioned( - top: 10, - right: 10, - child: FloatingActionButton( - onPressed: () async { - try { - Position position = await _determinePosition(); - - if (mounted) { - setState(() { - _currentP = LatLng( - position.latitude, - position.longitude, - ); - }); - - _animateCameraToPosition(_currentP!); + }, + onCameraIdle: () { + getLocationName( + coordenadas.latitude, coordenadas.longitude) + .then((value) => setState(() { + _addressController.text = value; + })); + }, + onCameraMove: (position) { + if (serviceLocationPreference != + ServiceLocationPreferences.office) { + coordenadas = position.target; } - } catch (e) { - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Por favor activa la ubicacion'), - ), - ); - } - }, - elevation: 0, - child: const Icon( - Icons.gps_fixed, - size: 30, + }, + initialCameraPosition: const CameraPosition( + target: LatLng(7.1253900, -73.1198000), + zoom: 16, + ), + myLocationButtonEnabled: false, ), - ), + Positioned( + top: 10, + left: 0, + child: Builder( + builder: (context) { + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + shape: const CircleBorder(), + elevation: 3, + minimumSize: const Size(50, 50), + ), + child: const Icon( + Icons.menu, + color: Colors.black, + size: 35, + ), + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + ); + }, + ), + ), + const Positioned( + bottom: 30, + right: 0, + left: 0, + top: 0, + child: Icon( + Icons.location_on, + size: 40, + color: Color(0xFFFF0000), + ), + ), + Positioned( + top: 10, + right: 10, + child: FloatingActionButton( + onPressed: () async { + try { + Position position = await _determinePosition(); + + setState(() { + _currentP = LatLng( + position.latitude, + position.longitude, + ); + }); + + _animateCameraToPosition(_currentP!); + } catch (e) { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Por favor activa la ubicacion'), + ), + ); + } + }, + elevation: 0, + child: const Icon( + Icons.gps_fixed, + size: 30, + ), + ), + ), + ], ), - ], - ), - ), - BlocListener( - listener: (context, state) {}, - child: buildBottom(context, state), - ), - ], + ), + buildBottom(context, myUserState), + ], + ); + }, ); }, ), @@ -199,6 +264,8 @@ class _UserMapScreenState extends State { })); if (datos != null) { + polylines.clear(); + fechaSeleccionada = datos[0]; horaSeleccionada = datos[1]; serviceLocationPreference = datos[2]; @@ -220,6 +287,31 @@ class _UserMapScreenState extends State { profesionalSeleccionado!.professionalInfo.latitude, profesionalSeleccionado!.professionalInfo.longitude, ); + + if (_currentP != null) { + getPolylinePoints( + _currentP!, + LatLng( + profesionalSeleccionado!.professionalInfo.latitude, + profesionalSeleccionado!.professionalInfo.longitude, + )).then( + (coordinates) => { + generatePolyLineFromPoints(coordinates), + }, + ); + } + + markers.add( + Marker( + icon: _markerIcon!, + markerId: MarkerId( + profesionalSeleccionado!.professionalInfo.id), + position: LatLng( + profesionalSeleccionado!.professionalInfo.latitude, + profesionalSeleccionado!.professionalInfo.longitude, + ), + ), + ); } else {} isClearButtonVisible = true; @@ -267,7 +359,7 @@ class _UserMapScreenState extends State { controller: TextEditingController( text: horaSeleccionada == null ? '' - : horaSeleccionada!.format(context), + : ScheduleEntity.getFormatTime(horaSeleccionada), ), decoration: const InputDecoration( prefixIcon: Icon(Icons.watch_later_outlined), @@ -278,9 +370,9 @@ class _UserMapScreenState extends State { ], ), const SizedBox(height: 15), - const TextField( - // controller: _observacionController, - decoration: InputDecoration( + TextField( + controller: _observationController, + decoration: const InputDecoration( prefixIcon: Icon(Icons.message_outlined), hintText: 'Observaciones', ), @@ -293,37 +385,39 @@ class _UserMapScreenState extends State { children: [ Expanded( child: FilledButton( - onPressed: () { - if (serviceLocationPreference == - ServiceLocationPreferences.office) { - context.read().add( - CreateService( - professionalId: - profesionalSeleccionado!.professionalInfo.id, - userId: state.user!.id, - address: profesionalSeleccionado! - .professionalInfo.address, - aditionalAddress: profesionalSeleccionado! - .professionalInfo.address, - latitude: profesionalSeleccionado! - .professionalInfo.latitude, - longitude: profesionalSeleccionado! - .professionalInfo.longitude, - day: fechaSeleccionada.toString(), - createdAt: Timestamp.now(), - description: _observationController.text, - range1Hour1: horaSeleccionada!, - range1Hour2: horaSeleccionada!.add(hour: 2), - rate: '0', - location: serviceLocationPreference!, - ), - ); - } else if (serviceLocationPreference == - ServiceLocationPreferences.delivery) { - } else { - // TODO: error inesperado - } - }, + onPressed: isLoading + ? null + : () { + if (serviceLocationPreference == + ServiceLocationPreferences.office) { + context.read().add( + CreateService( + professionalId: profesionalSeleccionado! + .professionalInfo.id, + userId: state.user!.id, + address: profesionalSeleccionado! + .professionalInfo.address, + aditionalAddress: profesionalSeleccionado! + .professionalInfo.aditionalAddress, + latitude: profesionalSeleccionado! + .professionalInfo.latitude, + longitude: profesionalSeleccionado! + .professionalInfo.longitude, + day: fechaSeleccionada.toString(), + createdAt: Timestamp.now(), + description: _observationController.text, + range1Hour1: horaSeleccionada!, + range1Hour2: horaSeleccionada!.add(hour: 2), + rate: '0', + location: serviceLocationPreference!, + ), + ); + } else if (serviceLocationPreference == + ServiceLocationPreferences.delivery) { + } else { + // TODO: error inesperado + } + }, style: FilledButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.primary, padding: const EdgeInsets.symmetric(vertical: 15), @@ -351,6 +445,9 @@ class _UserMapScreenState extends State { isClearButtonVisible = false; _observationController.text = ''; serviceLocationPreference = null; + + polylines.clear(); + markers.clear(); setState(() {}); }, style: FilledButton.styleFrom( @@ -387,6 +484,56 @@ class _UserMapScreenState extends State { ); } + Future> getPolylinePoints( + LatLng originP, LatLng destinationP) async { + List polylineCoordinates = []; + PolylinePoints polylinePoints = PolylinePoints(); + PolylineResult result = await polylinePoints.getRouteBetweenCoordinates( + GOOGLE_MAPS_API_KEY, + PointLatLng(originP.latitude, originP.longitude), + PointLatLng(destinationP.latitude, destinationP.longitude), + travelMode: TravelMode.driving, + ); + if (result.points.isNotEmpty) { + for (var point in result.points) { + polylineCoordinates.add(LatLng(point.latitude, point.longitude)); + } + } else { + print(result.errorMessage); + } + return polylineCoordinates; + } + + void getLocation() async { + try { + Position position = await _determinePosition(); + + setState(() { + _currentP = LatLng( + position.latitude, + position.longitude, + ); + }); + + _animateCameraToPosition(_currentP!); + } catch (e) { + log(e.toString()); + } + } + + void generatePolyLineFromPoints(List polylineCoordinates) async { + PolylineId id = const PolylineId("poly"); + Polyline polyline = Polyline( + polylineId: id, + color: Colors.blue, + points: polylineCoordinates, + width: 6, + ); + setState(() { + polylines[id] = polyline; + }); + } + Future getLocationName(double latitude, double longitude) async { String address; List placemarks = diff --git a/lib/screens/user/user_service_screen.dart b/lib/screens/user/user_service_screen.dart new file mode 100644 index 0000000..b96bb2e --- /dev/null +++ b/lib/screens/user/user_service_screen.dart @@ -0,0 +1,380 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injector/injector.dart'; +import 'package:intl/intl.dart'; +import 'package:professional_repository/professional_repository.dart'; +import 'package:prosappco/blocs/service_bloc/service_bloc.dart'; +import 'package:prosappco/components/general_primary_button.dart'; +import 'package:service_repository/service_repository.dart'; +import 'package:setting_repository/setting_repository.dart'; +import 'package:user_repository/user_repository.dart'; + +class UserServiceScreen extends StatefulWidget { + final String serviceId; + const UserServiceScreen({Key? key, required this.serviceId}) + : super(key: key); + + @override + State createState() => _UserServiceScreenState(); +} + +class _UserServiceScreenState extends State { + final settingRepository = Injector.appInstance.get(); + SettingEntity? settings; + + @override + void initState() { + super.initState(); + + _loadSettings(); + } + + void _loadSettings() { + settingRepository.getSettings().then( + (value) => setState(() { + settings = value; + }), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Servicio'), + ), + body: BlocBuilder( + builder: (context, state) { + if (state is ServiceLoaded) { + final service = state.service; + return FutureBuilder( + future: _getUserAndProfessionalInfo(service), + builder: (BuildContext context, + AsyncSnapshot> snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else { + if (snapshot.hasError) { + return Center( + child: Text('Error inesperado: ${snapshot.error}'), + ); + } else { + final userInfo = snapshot.data![0] as MyUser; + final professionalInfo = + snapshot.data![1] as ProfessionalEntity; + + return Column( + children: [ + ListTile( + leading: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: Colors.grey.shade300, + shape: BoxShape.circle, + image: userInfo.picture == null + ? null + : DecorationImage( + image: NetworkImage(userInfo.picture!), + fit: BoxFit.contain, + ), + ), + child: userInfo.picture == null + ? Icon( + CupertinoIcons.person, + color: Colors.grey.shade400, + size: 40, + ) + : null, + ), + title: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${userInfo.name}', + style: const TextStyle( + fontWeight: FontWeight.bold), + ), + const SizedBox(width: 5), + Text( + '${DateFormat('dd MMMM', 'es').format(DateTime.parse(service.day))} - ${ScheduleEntity.getFormatTime(service.range1Hour1)}', + style: TextStyle( + color: Colors.grey[600], + ), + ), + ], + ), + subtitle: const Text('Rating: 5.0'), + ), + Container( + margin: const EdgeInsets.only( + left: 40, right: 40, top: 20, bottom: 20), + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 15), + decoration: BoxDecoration( + color: const Color(0xFFD6F4FF), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(1, 3), + ), + ], + ), + child: Row( + children: [ + const Icon( + Icons.error_outline, + size: 27, + color: Colors.black54, + ), + const SizedBox(width: 15), + service.location == + ServiceLocationPreferences.delivery + ? const Text( + 'Servicio a su domicilio.', + style: TextStyle( + color: Colors.black, fontSize: 14), + ) + : const Text( + 'Servicio en sitio / consultorio', + style: TextStyle( + color: Colors.black, fontSize: 14), + ), + ], + ), + ), + Visibility( + visible: settings?.tarifas ?? false, + child: Column( + children: [ + Text( + formatCurrency(int.tryParse(service.rate) ?? 0), + style: const TextStyle( + fontWeight: FontWeight.w600, fontSize: 25), + ), + const Text('Tarifa de consulta'), + const SizedBox(height: 15), + const Text( + 'Metodos de pago', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + professionalInfo.paymentMethods.datafono || + professionalInfo.paymentMethods.nequi || + professionalInfo + .paymentMethods.transferencia + ? Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.center, + children: [ + Visibility( + visible: professionalInfo + .paymentMethods.datafono, + child: const Chip( + label: Text('Datafono')), + ), + Visibility( + visible: professionalInfo + .paymentMethods.nequi, + child: + const Chip(label: Text('Nequi')), + ), + Visibility( + visible: professionalInfo + .paymentMethods.transferencia, + child: const Chip( + label: Text( + 'Transferencia Bancaria')), + ), + ], + ) + : const Text( + 'No hay metodos de pago registrados', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 15, + color: Colors.black45, + ), + ), + ], + ), + ), + ListTile( + leading: const Icon(Icons.near_me), + title: Text( + service.address, + style: TextStyle( + fontSize: 15, color: Colors.grey[600]), + ), + subtitle: service.aditionalAddress.isEmpty + ? null + : Text( + service.aditionalAddress, + style: TextStyle( + fontSize: 15, color: Colors.grey[600]), + ), + ), + service.description.isEmpty + ? const SizedBox() + : SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + child: Text( + '"${service.description.trim()}"', + style: TextStyle( + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + ), + ), + Expanded( + child: customMessageStatus(service), + ), + customButton(service, context), + const SizedBox(height: 20), + ], + ); + } + } + }, + ); + } else { + BlocProvider.of(context) + .add(LoadService(widget.serviceId)); + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ), + ); + } + + Center customMessageStatus(ServiceEntity service) { + if (service.status == ServiceStatus.cancelled) { + return const Center( + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Icon( + CupertinoIcons.xmark_circle, + color: Color(0xFF35A8ED), + size: 70, + ), + ), + Text( + 'Servicio cancelado exitosamente', + style: TextStyle( + color: Color(0xFF35A8ED), + fontSize: 17, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + if (service.status == ServiceStatus.completed) { + return const Center( + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Icon( + Icons.favorite_outline_sharp, + color: Color(0xFF35A8ED), + size: 70, + ), + ), + Text( + 'Servicio terminado exitosamente', + style: TextStyle( + color: Color(0xFF35A8ED), + fontSize: 17, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + return const Center( + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Icon( + Icons.check_circle_outline_rounded, + color: Color(0xFF35A8ED), + size: 70, + ), + ), + Text( + 'Servicio solicitado exitosamente', + style: TextStyle( + color: Color(0xFF35A8ED), + fontSize: 17, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + GeneralPrimaryButton customButton( + ServiceEntity service, BuildContext context) { + if (service.status == ServiceStatus.pending) { + return GeneralPrimaryButton( + onPressed: () { + final currentState = context.read().state; + if (currentState is ServiceLoaded) { + context.read().add( + UpdateServiceStatus( + widget.serviceId, + ServiceStatus.cancelled, + ), + ); + } + }, + color: Colors.red, + label: 'Cancelar Servicio', + ); + } + + return GeneralPrimaryButton( + onPressed: () { + Navigator.pop(context); + }, + label: 'Volver', + ); + } + + String formatCurrency(int number) { + final formatter = + NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: ''); + return '\$${formatter.format(number)}'; + } + + Future> _getUserAndProfessionalInfo( + ServiceEntity service) async { + final userRepo = FirebaseUserRepository(FirebaseAuth.instance); + final userInfo = await userRepo.getMyUser(service.professionalId); + + final professionalRepo = FirebaseProfessionalRepository(); + final professionalInfo = + await professionalRepo.getProInfo(service.professionalId); + + return [userInfo, professionalInfo]; + } +} diff --git a/packages/professional_repository/lib/src/entities/schedule_entity.dart b/packages/professional_repository/lib/src/entities/schedule_entity.dart index 7e0fd19..8026bbf 100644 --- a/packages/professional_repository/lib/src/entities/schedule_entity.dart +++ b/packages/professional_repository/lib/src/entities/schedule_entity.dart @@ -91,7 +91,7 @@ class ScheduleEntity extends Equatable { return null; } - String? getFormatTime(TimeOfDay? time) { + static String? getFormatTime(TimeOfDay? time) { if (time == null) { return null; } diff --git a/packages/service_repository/lib/src/entities/service_entity.dart b/packages/service_repository/lib/src/entities/service_entity.dart index 949b933..60dcc5a 100644 --- a/packages/service_repository/lib/src/entities/service_entity.dart +++ b/packages/service_repository/lib/src/entities/service_entity.dart @@ -53,14 +53,19 @@ class ServiceEntity extends Equatable { day: doc['day'] as String, createdAt: doc['created_at'] as Timestamp, description: doc['description'] as String, - range1Hour1: doc['range1_hour1'] as TimeOfDay, - range1Hour2: doc['range1_hour2'] as TimeOfDay, + range1Hour1: parseTimeOfDay(doc['range1_hour1'] as String), + range1Hour2: parseTimeOfDay(doc['range1_hour2'] as String), status: intToEnumService(doc['status'] as int), rate: doc['rate'] as String, location: intToEnum(doc['location'] as int), ); } + static TimeOfDay parseTimeOfDay(String timeString) { + final parts = timeString.split(':'); + return TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1])); + } + Map toDocument() { return { 'professional_id': professionalId, @@ -74,14 +79,21 @@ class ServiceEntity extends Equatable { 'day': day, 'created_at': createdAt, 'description': description, - 'range1_hour1': range1Hour1, - 'range1_hour2': range1Hour2, + 'range1_hour1': formatTimeOfDay(range1Hour1), + 'range1_hour2': formatTimeOfDay(range1Hour2), 'rate': rate, 'status': enumToIntService(status), 'location': enumToInt(location), }; } + String? formatTimeOfDay(TimeOfDay? time) { + if (time != null) { + return "${time.hour.toString()}:${time.minute.toString()}"; + } + return null; + } + @override List get props => [ professionalId, diff --git a/packages/service_repository/lib/src/models/service_status.dart b/packages/service_repository/lib/src/models/service_status.dart index 4007918..db939a1 100644 --- a/packages/service_repository/lib/src/models/service_status.dart +++ b/packages/service_repository/lib/src/models/service_status.dart @@ -1,6 +1,6 @@ export 'service_status.dart'; -enum ServiceStatus { pending, active, cancelled, completed } +enum ServiceStatus { pending, acepted, active, cancelled, completed } int enumToIntService(ServiceStatus state) { return state.index; diff --git a/packages/service_repository/lib/src/repositories/firebase_service_repository.dart b/packages/service_repository/lib/src/repositories/firebase_service_repository.dart index f87867d..522efaf 100644 --- a/packages/service_repository/lib/src/repositories/firebase_service_repository.dart +++ b/packages/service_repository/lib/src/repositories/firebase_service_repository.dart @@ -1,13 +1,30 @@ -import 'dart:developer'; - import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:service_repository/service_repository.dart'; class FirebaseServiceRepository { - final serviceCollection = FirebaseFirestore.instance.collection('services'); + final serviceCollection = + FirebaseFirestore.instance.collection('services v2'); - Future createService(ServiceEntity entity) async { - log('xd -- ${entity.toString()}'); - await serviceCollection.add(entity.toDocument()); + Future createService(ServiceEntity entity) async { + DocumentReference> docRef = + await serviceCollection.add(entity.toDocument()); + return docRef.id; + } + + Stream getService(String serviceId) { + return serviceCollection.doc(serviceId).snapshots().map((snapshot) { + if (snapshot.exists) { + return ServiceEntity.fromDocument(snapshot.data()!); + } else { + throw Exception('El servicio con ID $serviceId no existe'); + } + }); + } + + Future updateServiceStatus( + String serviceId, ServiceStatus newStatus) async { + await serviceCollection + .doc(serviceId) + .update({'status': enumToIntService(newStatus)}); } }