From 5db51bc86a9934be15eabd345f76284ad6df1587 Mon Sep 17 00:00:00 2001 From: Felipe Date: Thu, 23 May 2024 21:41:49 -0500 Subject: [PATCH] calendario profesional --- .../professional_bloc/professional_bloc.dart | 3 +- .../professional_list_bloc.dart | 8 +- lib/blocs/service_bloc/service_bloc.dart | 4 +- lib/components/general_drawer.dart | 27 +- .../professional_service_list_screen.dart | 3 +- .../professional_calendar_screen.dart | 447 ++++++++++++++---- .../lib/src/entities/service_entity.dart | 19 +- .../lib/src/models/service_status.dart | 13 +- .../firebase_service_repository.dart | 4 +- 9 files changed, 389 insertions(+), 139 deletions(-) diff --git a/lib/blocs/professional_bloc/professional_bloc.dart b/lib/blocs/professional_bloc/professional_bloc.dart index 025822f..413d1d1 100644 --- a/lib/blocs/professional_bloc/professional_bloc.dart +++ b/lib/blocs/professional_bloc/professional_bloc.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:professional_repository/professional_repository.dart'; +import 'package:prosappco/blocs/professional_list_bloc/professional_list_bloc.dart'; import 'package:user_repository/user_repository.dart'; part 'professional_event.dart'; @@ -37,7 +38,7 @@ class ProfessionalBloc extends Bloc { on((event, emit) async { final proInfo = _professionalRepository.lastProInfo(); - emit(LoadedModeProState(event.isProModeActive, proInfo)); + emit(LoadedModeProState(event.isProModeActive, proInfo )); }); on((event, emit) async { diff --git a/lib/blocs/professional_list_bloc/professional_list_bloc.dart b/lib/blocs/professional_list_bloc/professional_list_bloc.dart index 1cf7003..8eb9bea 100644 --- a/lib/blocs/professional_list_bloc/professional_list_bloc.dart +++ b/lib/blocs/professional_list_bloc/professional_list_bloc.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:professional_repository/professional_repository.dart'; @@ -22,11 +20,13 @@ class ProfessionalListBloc on(_onProfessionalListFetch); } - void _onProfessionalListFetch(ProfessionalListFetch event, Emitter emit) async { + void _onProfessionalListFetch( + ProfessionalListFetch event, Emitter emit) async { emit(ProfessionalListLoading()); final users = await _userRepository.getUsersProfessionalActive(); - final professionals = await _firebaseProfessionalRepository.getProfessionalInfo(); + final professionals = + await _firebaseProfessionalRepository.getProfessionalInfo(); final professionalInfoMap = {for (var doc in professionals) doc.id: doc}; diff --git a/lib/blocs/service_bloc/service_bloc.dart b/lib/blocs/service_bloc/service_bloc.dart index e909860..dda31cf 100644 --- a/lib/blocs/service_bloc/service_bloc.dart +++ b/lib/blocs/service_bloc/service_bloc.dart @@ -60,7 +60,7 @@ class ServiceBloc extends Bloc { range1Hour1: event.range1Hour1, range1Hour2: event.range1Hour2, rate: event.rate, - status: ServiceStatus.pending, + status: event.status ?? ServiceStatus.pending, location: event.location, ); @@ -307,8 +307,6 @@ class ServiceBloc extends Bloc { final count = await _serviceRepository .countPendingServicesForProfessional(event.professionalId); - - emit(PendingServicesCountedLoaded(count)); } catch (e) { log(e.toString()); diff --git a/lib/components/general_drawer.dart b/lib/components/general_drawer.dart index 1e7fc2d..2e40c49 100644 --- a/lib/components/general_drawer.dart +++ b/lib/components/general_drawer.dart @@ -1,6 +1,3 @@ -import 'dart:developer'; - -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -11,13 +8,11 @@ import 'package:injector/injector.dart'; import 'package:prosappco/blocs/my_user_bloc/my_user_bloc.dart'; import 'package:prosappco/blocs/professional_bloc/professional_bloc.dart'; import 'package:prosappco/blocs/score_bloc/score_bloc.dart'; -import 'package:prosappco/blocs/service_bloc/service_bloc.dart'; import 'package:prosappco/components/drawer_reputation.dart'; import 'package:prosappco/components/general_drawer_header.dart'; 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/professional_score_list_screen.dart'; import 'package:prosappco/screens/lists/professional_service_history_list_screen.dart'; import 'package:prosappco/screens/lists/professional_service_list_screen.dart'; @@ -29,9 +24,7 @@ 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/professional/professional_services_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:service_repository/service_repository.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -140,13 +133,19 @@ class GeneralDrawer extends StatelessWidget { shouldProModeActive(context, professionalState) ? GeneralDrawerItem( onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - const ProfessionalCalendarScreen(), - ), - ); + if (professionalState + is LoadedModeProState) { + Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => + ProfessionalCalendarScreen( + userProfessional: + professionalState.proInfo!, + ), + ), + ); + } }, label: 'Calendario', leading: Icons.calendar_month_outlined, diff --git a/lib/screens/lists/professional_service_list_screen.dart b/lib/screens/lists/professional_service_list_screen.dart index b418fa3..4f18ad1 100644 --- a/lib/screens/lists/professional_service_list_screen.dart +++ b/lib/screens/lists/professional_service_list_screen.dart @@ -28,8 +28,7 @@ class _ProfessionalServiceListScreenState serviceBloc = Injector.appInstance.get(); - serviceBloc.add( - LoadServicesForProfessional(FirebaseAuth.instance.currentUser!.uid)); + serviceBloc.add(LoadServicesForProfessional(FirebaseAuth.instance.currentUser!.uid)); } @override diff --git a/lib/screens/professional/professional_calendar_screen.dart b/lib/screens/professional/professional_calendar_screen.dart index a5c122c..5f03400 100644 --- a/lib/screens/professional/professional_calendar_screen.dart +++ b/lib/screens/professional/professional_calendar_screen.dart @@ -1,9 +1,24 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:prosappco/blocs/service_bloc/service_bloc.dart'; +import 'package:prosappco/screens/service/professional_service_screen.dart'; +import 'package:prosappco/utils/time_of_day_extension.dart'; +import 'package:provider/provider.dart'; import 'package:table_calendar/table_calendar.dart'; +import 'package:injector/injector.dart'; +import 'package:professional_repository/professional_repository.dart'; +import 'package:intl/intl.dart'; +import 'package:prosappco/utils/time_of_day_utils.dart'; +import 'package:service_repository/service_repository.dart'; +import 'package:setting_repository/setting_repository.dart'; class ProfessionalCalendarScreen extends StatefulWidget { - const ProfessionalCalendarScreen({super.key}); + final ProfessionalEntity userProfessional; + + const ProfessionalCalendarScreen({super.key, required this.userProfessional}); @override State createState() => @@ -12,47 +27,54 @@ class ProfessionalCalendarScreen extends StatefulWidget { class _ProfessionalCalendarScreenState extends State { - TextEditingController _titleController = TextEditingController(); - TextEditingController _descriptionController = TextEditingController(); + final settingRepository = Injector.appInstance.get(); + final serviceRepository = + Injector.appInstance.get(); + SettingEntity? settings; DateTime today = DateTime.now(); DateTime now = DateTime.now(); + late int numDay; + + List? _services; CalendarFormat _calendarFormat = CalendarFormat.month; - TimeOfDay? _selectedTime1; - TimeOfDay? _selectedTime2; + bool isLoading = false; - Future _selectTime1(BuildContext context) async { - final TimeOfDay? pickedTime1 = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (pickedTime1 != null) { - setState(() { - _selectedTime1 = pickedTime1; - }); - } - return pickedTime1; + @override + void initState() { + super.initState(); + + today = DateTime.utc(today.year, today.month, today.day); + numDay = today.weekday; + + _loadSettings(); + _loadServices(); } - Future _selectTime2(BuildContext context) async { - final TimeOfDay? pickedTime2 = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (pickedTime2 != null) { - setState(() { - _selectedTime2 = pickedTime2; - }); - } + void _loadSettings() { + settingRepository.getSettings().then( + (value) => setState(() { + settings = value; + }), + ); + } - return pickedTime2; + void _loadServices() { + serviceRepository + .getServicesForProfessionalforCalendar(widget.userProfessional.id) + .then((services) { + setState(() { + _services = services; + }); + }); } void _onDaySelected(DateTime day, DateTime focusedDay) { setState(() { today = day; + numDay = today.weekday; }); } @@ -65,6 +87,7 @@ class _ProfessionalCalendarScreenState @override Widget build(BuildContext context) { DateTime lastDay = today.add(const Duration(days: 365)); + return Scaffold( appBar: AppBar( title: const Text('Calendario'), @@ -73,69 +96,315 @@ class _ProfessionalCalendarScreenState ? FloatingActionButtonLocation.startFloat : FloatingActionButtonLocation.endFloat, resizeToAvoidBottomInset: false, - body: Column( - children: [ - Container( - color: const Color.fromARGB(255, 224, 247, 255), - child: TableCalendar( - locale: 'es_MX', - firstDay: DateTime.utc(2010, 10, 16), - lastDay: lastDay, - focusedDay: today, - availableGestures: AvailableGestures.all, - onDaySelected: _onDaySelected, - selectedDayPredicate: (day) => isSameDay(day, today), - calendarFormat: _calendarFormat, - onFormatChanged: _onFormatChange, - eventLoader: (date) { - return []; - // return events - // .where((element) { - // DateTime day = DateTime.parse(element.day); - // return (date.year == day.year && - // date.month == day.month && - // date.day == day.day); - // }) - // .map((e) => e.description) - // .toList(); - }, - availableCalendarFormats: const { - CalendarFormat.month: 'Mes', - CalendarFormat.week: 'Semana', - CalendarFormat.twoWeeks: '2 Semanas', - }, - ), - ), - // SizedBox( - // width: double.infinity, - // child: Padding( - // padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), - // child: Text(DateFormat('dd MMMM yyyy', 'es').format(today), - // style: const TextStyle( - // color: Colors.black, - // fontSize: 16, - // fontWeight: FontWeight.w600, - // )), - // ), - // ), - const Divider( - height: 0, - ), - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 15), - child: Column( - children: const [], - ), - ), - ), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: () {}, - // onPressed: _showDialog, - child: const Icon(Icons.add), + body: BlocProvider( + create: (context) => Injector.appInstance.get(), + child: BlocConsumer( + listener: (context, serviceState) { + if (serviceState is CreateServiceLoading) { + isLoading = true; + } + if (serviceState is CreateServiceFailure) { + isLoading = false; + } + }, + builder: (context, state) { + return Column( + children: [ + Container( + color: const Color.fromARGB(255, 224, 247, 255), + child: TableCalendar( + locale: 'es_MX', + firstDay: DateTime.now(), + lastDay: lastDay, + focusedDay: today, + availableGestures: AvailableGestures.all, + onDaySelected: _onDaySelected, + selectedDayPredicate: (day) => isSameDay(day, today), + calendarFormat: _calendarFormat, + onFormatChanged: _onFormatChange, + availableCalendarFormats: const { + CalendarFormat.month: 'Mes', + CalendarFormat.week: 'Semana', + CalendarFormat.twoWeeks: '2 Semanas', + }, + ), + ), + SizedBox( + width: double.infinity, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Text( + DateFormat('dd MMMM yyyy', 'es').format(today), + style: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const Divider( + height: 0, + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 15), + child: Column( + children: [ + ..._rangesItems( + _getScheduleFromNumDay(numDay), context), + ], + ), + ), + ) + ], + ); + }, + ), ), ); } + + ScheduleEntity? _getScheduleFromNumDay(int numDay) { + switch (numDay) { + case 1: + return widget.userProfessional.schedules.monday; + case 2: + return widget.userProfessional.schedules.tuesday; + case 3: + return widget.userProfessional.schedules.wednesday; + case 4: + return widget.userProfessional.schedules.thursday; + case 5: + return widget.userProfessional.schedules.friday; + case 6: + return widget.userProfessional.schedules.saturday; + case 7: + return widget.userProfessional.schedules.sunday; + default: + return null; + } + } + + List _rangesItems(ScheduleEntity? schedule, BuildContext context) { + if (schedule == null || + schedule.enabled == false || + schedule.range1Hour1 == null || + schedule.range2Hour2 == null) { + return [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 25), + child: Text("No hay horarios disponibles"), + ) + ]; + } + + if (schedule.continuousDay) { + List ranges = TimeOfDayUtils.genRanges( + schedule.range1Hour1!, + schedule.range2Hour2!, + ); + + return rangesItemList(ranges, _services, today, context); + } + + List ranges1 = TimeOfDayUtils.genRanges( + schedule.range1Hour1!, + schedule.range1Hour2!, + ); + List ranges2 = TimeOfDayUtils.genRanges( + schedule.range2Hour1!, + schedule.range2Hour2!, + ); + + return [ + ...rangesItemList(ranges1, _services, today, context), + ...rangesItemList(ranges2, _services, today, context), + ]; + } + + bool _isHora1Ocupada( + TimeOfDay hora1, List? events, DateTime selectedDay) { + if (events != null) { + for (ServiceEntity event in events) { + if (selectedDay.toString() == event.day) { + if (hora1 == event.range1Hour1) { + return true; + } + } + } + } + return false; + } + + List rangesItemList(List ranges, + List? events, DateTime selectedDay, BuildContext context) { + return ranges.map((time) { + if (_isHora1Ocupada(time, events, selectedDay)) { + return Card( + elevation: 4, + margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: ListTile( + onTap: () { + if (events != null) { + for (ServiceEntity event in events) { + if (selectedDay.toString() == event.day) { + if (time == event.range1Hour1) { + if (event.userId == event.professionalId) { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar( + content: Text('Horario ocupado por ti'), + )); + } else { + Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => ProfessionalServiceScreen( + serviceId: event.id!, + ), + ), + ); + } + } + } + } + } + }, + contentPadding: const EdgeInsets.all(16), + leading: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.yellow, Colors.red, Colors.red], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.access_time, + color: Colors.white, + ), + ), + ), + title: Text( + ScheduleEntity.getFormatTime(time) ?? '', + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ), + subtitle: const Text( + 'Ocupado', + style: TextStyle( + color: Colors.red, fontSize: 13, fontWeight: FontWeight.bold), + ), + ), + ); + } else { + return Card( + elevation: 4, + margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: ListTile( + onTap: () { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Text('Reservar hora'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '¿Estás seguro de que deseas reservar a las ${ScheduleEntity.getFormatTime(time)} del ${DateFormat('dd-MM-yyyy').format(today)}?', + ), + const SizedBox(height: 10), + const Text( + '⚠️ Esta accion no se puede deshacer ⚠️', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(dialogContext); + }, + child: const Text('No, cancelar'), + ), + TextButton( + onPressed: () { + Navigator.pop(dialogContext); + + Navigator.pop(context); + + context.read().add( + CreateService( + professionalId: widget.userProfessional.id, + userId: widget.userProfessional.id, + address: widget.userProfessional.address, + aditionalAddress: '', + latitude: 0, + longitude: 0, + day: today.toString(), + createdAt: Timestamp.now(), + description: '', + range1Hour1: time, + range1Hour2: time.add(hour: 2), + rate: '0', + location: ServiceLocationPreferences.office, + status: ServiceStatus.selfBooked, + ), + ); + }, + child: const Text('Si, reservar'), + ), + ], + ); + }, + ); + }, + contentPadding: const EdgeInsets.all(16), + leading: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue, Colors.green], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.access_time, + color: Colors.white, + ), + ), + ), + title: Text( + ScheduleEntity.getFormatTime(time) ?? '', + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ), + subtitle: const Text( + 'Disponible', + style: TextStyle( + color: Colors.green, + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + }).toList(); + } } diff --git a/packages/service_repository/lib/src/entities/service_entity.dart b/packages/service_repository/lib/src/entities/service_entity.dart index 3ef3d9b..a1bd888 100644 --- a/packages/service_repository/lib/src/entities/service_entity.dart +++ b/packages/service_repository/lib/src/entities/service_entity.dart @@ -121,23 +121,6 @@ class ServiceEntity extends Equatable { @override String toString() { - return '''ServiceEntity{{ - professionalId: $professionalId, - professionalScored: $professionalScored, - userId: $userId, - userScored: $userScored, - address: $address, - aditionalAddress: $aditionalAddress, - latitude: $latitude, - longitude: $longitude, - day: $day, - createdAt: $createdAt, - description: $description, - range1Hour1: $range1Hour1, - range1Hour2: $range1Hour2, - rate: $rate, - status: $status, - location: $location - }}'''; + return '''ServiceEntity{{ professionalId: $professionalId, professionalScored: $professionalScored, userId: $userId, userScored: $userScored, address: $address, aditionalAddress: $aditionalAddress, latitude: $latitude, longitude: $longitude, day: $day, createdAt: $createdAt, description: $description, range1Hour1: $range1Hour1, range1Hour2: $range1Hour2, rate: $rate, status: $status, location: $location }}'''; } } diff --git a/packages/service_repository/lib/src/models/service_status.dart b/packages/service_repository/lib/src/models/service_status.dart index d022e3d..73b13cc 100644 --- a/packages/service_repository/lib/src/models/service_status.dart +++ b/packages/service_repository/lib/src/models/service_status.dart @@ -1,12 +1,13 @@ export 'service_status.dart'; enum ServiceStatus { - pending, // 0 - acepted, // 1 - denied, // 2 - active, // 3 - cancelled, // 4 - completed, // 5 + pending, // 0 + acepted, // 1 + denied, // 2 + active, // 3 + cancelled, // 4 + completed, // 5 + selfBooked // 6 } int enumToIntService(ServiceStatus state) { 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 d0a5cf9..a07d916 100644 --- a/packages/service_repository/lib/src/repositories/firebase_service_repository.dart +++ b/packages/service_repository/lib/src/repositories/firebase_service_repository.dart @@ -41,7 +41,7 @@ class FirebaseServiceRepository { String professionalId) { return serviceCollection .where('professional_id', isEqualTo: professionalId) - .where('status', whereIn: [0, 1, 2, 3]) + .where('status', whereIn: [1, 2, 3]) .snapshots() .map((querySnapshot) => querySnapshot.docs .map((doc) => ServiceEntity.fromDocument(doc.data(), doc.id)) @@ -52,7 +52,7 @@ class FirebaseServiceRepository { String professionalId) async { QuerySnapshot> query = await serviceCollection .where('professional_id', isEqualTo: professionalId) - .where('status', whereIn: [0, 1, 2, 3]).get(); + .where('status', whereIn: [0, 1, 2, 3, 6]).get(); return query.docs .map((doc) => ServiceEntity.fromDocument(doc.data(), doc.id))