calendario profesional

This commit is contained in:
Felipe
2024-05-23 21:41:49 -05:00
parent b419e24b90
commit 5db51bc86a
9 changed files with 389 additions and 139 deletions
@@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:professional_repository/professional_repository.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'; import 'package:user_repository/user_repository.dart';
part 'professional_event.dart'; part 'professional_event.dart';
@@ -37,7 +38,7 @@ class ProfessionalBloc extends Bloc<ProfessionalEvent, ProfessionalState> {
on<UpdateProfessionalEvent>((event, emit) async { on<UpdateProfessionalEvent>((event, emit) async {
final proInfo = _professionalRepository.lastProInfo(); final proInfo = _professionalRepository.lastProInfo();
emit(LoadedModeProState(event.isProModeActive, proInfo)); emit(LoadedModeProState(event.isProModeActive, proInfo ));
}); });
on<SendProfessionalToReviewEvent>((event, emit) async { on<SendProfessionalToReviewEvent>((event, emit) async {
@@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:professional_repository/professional_repository.dart'; import 'package:professional_repository/professional_repository.dart';
@@ -22,11 +20,13 @@ class ProfessionalListBloc
on<ProfessionalListFetch>(_onProfessionalListFetch); on<ProfessionalListFetch>(_onProfessionalListFetch);
} }
void _onProfessionalListFetch(ProfessionalListFetch event, Emitter<ProfessionalListState> emit) async { void _onProfessionalListFetch(
ProfessionalListFetch event, Emitter<ProfessionalListState> emit) async {
emit(ProfessionalListLoading()); emit(ProfessionalListLoading());
final users = await _userRepository.getUsersProfessionalActive(); 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}; final professionalInfoMap = {for (var doc in professionals) doc.id: doc};
+1 -3
View File
@@ -60,7 +60,7 @@ class ServiceBloc extends Bloc<ServiceEvent, ServiceState> {
range1Hour1: event.range1Hour1, range1Hour1: event.range1Hour1,
range1Hour2: event.range1Hour2, range1Hour2: event.range1Hour2,
rate: event.rate, rate: event.rate,
status: ServiceStatus.pending, status: event.status ?? ServiceStatus.pending,
location: event.location, location: event.location,
); );
@@ -307,8 +307,6 @@ class ServiceBloc extends Bloc<ServiceEvent, ServiceState> {
final count = await _serviceRepository final count = await _serviceRepository
.countPendingServicesForProfessional(event.professionalId); .countPendingServicesForProfessional(event.professionalId);
emit(PendingServicesCountedLoaded(count)); emit(PendingServicesCountedLoaded(count));
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
+7 -8
View File
@@ -1,6 +1,3 @@
import 'dart:developer';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.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/my_user_bloc/my_user_bloc.dart';
import 'package:prosappco/blocs/professional_bloc/professional_bloc.dart'; import 'package:prosappco/blocs/professional_bloc/professional_bloc.dart';
import 'package:prosappco/blocs/score_bloc/score_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/drawer_reputation.dart';
import 'package:prosappco/components/general_drawer_header.dart'; import 'package:prosappco/components/general_drawer_header.dart';
import 'package:prosappco/components/general_drawer_item.dart'; import 'package:prosappco/components/general_drawer_item.dart';
import 'package:prosappco/screens/configuration/configuration_screen.dart'; import 'package:prosappco/screens/configuration/configuration_screen.dart';
import 'package:prosappco/screens/configuration/configuration_support_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_score_list_screen.dart';
import 'package:prosappco/screens/lists/professional_service_history_list_screen.dart'; import 'package:prosappco/screens/lists/professional_service_history_list_screen.dart';
import 'package:prosappco/screens/lists/professional_service_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_form_screen.dart';
import 'package:prosappco/screens/professional/professional_pending_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_profile_screen.dart';
import 'package:prosappco/screens/professional/professional_services_screen.dart';
import 'package:prosappco/screens/profile/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:prosappco/screens/web/web_view_screen.dart';
import 'package:service_repository/service_repository.dart'; import 'package:service_repository/service_repository.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -140,13 +133,19 @@ class GeneralDrawer extends StatelessWidget {
shouldProModeActive(context, professionalState) shouldProModeActive(context, professionalState)
? GeneralDrawerItem( ? GeneralDrawerItem(
onTap: () { onTap: () {
if (professionalState
is LoadedModeProState) {
Navigator.push( Navigator.push(
context, context,
CupertinoPageRoute( CupertinoPageRoute(
builder: (context) => builder: (context) =>
const ProfessionalCalendarScreen(), ProfessionalCalendarScreen(
userProfessional:
professionalState.proInfo!,
),
), ),
); );
}
}, },
label: 'Calendario', label: 'Calendario',
leading: Icons.calendar_month_outlined, leading: Icons.calendar_month_outlined,
@@ -28,8 +28,7 @@ class _ProfessionalServiceListScreenState
serviceBloc = Injector.appInstance.get<ServiceBloc>(); serviceBloc = Injector.appInstance.get<ServiceBloc>();
serviceBloc.add( serviceBloc.add(LoadServicesForProfessional(FirebaseAuth.instance.currentUser!.uid));
LoadServicesForProfessional(FirebaseAuth.instance.currentUser!.uid));
} }
@override @override
@@ -1,9 +1,24 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.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: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 { class ProfessionalCalendarScreen extends StatefulWidget {
const ProfessionalCalendarScreen({super.key}); final ProfessionalEntity userProfessional;
const ProfessionalCalendarScreen({super.key, required this.userProfessional});
@override @override
State<ProfessionalCalendarScreen> createState() => State<ProfessionalCalendarScreen> createState() =>
@@ -12,47 +27,54 @@ class ProfessionalCalendarScreen extends StatefulWidget {
class _ProfessionalCalendarScreenState class _ProfessionalCalendarScreenState
extends State<ProfessionalCalendarScreen> { extends State<ProfessionalCalendarScreen> {
TextEditingController _titleController = TextEditingController(); final settingRepository = Injector.appInstance.get<SettingRepository>();
TextEditingController _descriptionController = TextEditingController(); final serviceRepository =
Injector.appInstance.get<FirebaseServiceRepository>();
SettingEntity? settings;
DateTime today = DateTime.now(); DateTime today = DateTime.now();
DateTime now = DateTime.now(); DateTime now = DateTime.now();
late int numDay;
List<ServiceEntity>? _services;
CalendarFormat _calendarFormat = CalendarFormat.month; CalendarFormat _calendarFormat = CalendarFormat.month;
TimeOfDay? _selectedTime1; bool isLoading = false;
TimeOfDay? _selectedTime2;
Future<TimeOfDay?> _selectTime1(BuildContext context) async { @override
final TimeOfDay? pickedTime1 = await showTimePicker( void initState() {
context: context, super.initState();
initialTime: TimeOfDay.now(),
today = DateTime.utc(today.year, today.month, today.day);
numDay = today.weekday;
_loadSettings();
_loadServices();
}
void _loadSettings() {
settingRepository.getSettings().then(
(value) => setState(() {
settings = value;
}),
); );
if (pickedTime1 != null) {
setState(() {
_selectedTime1 = pickedTime1;
});
}
return pickedTime1;
} }
Future<TimeOfDay?> _selectTime2(BuildContext context) async { void _loadServices() {
final TimeOfDay? pickedTime2 = await showTimePicker( serviceRepository
context: context, .getServicesForProfessionalforCalendar(widget.userProfessional.id)
initialTime: TimeOfDay.now(), .then((services) {
);
if (pickedTime2 != null) {
setState(() { setState(() {
_selectedTime2 = pickedTime2; _services = services;
});
}); });
}
return pickedTime2;
} }
void _onDaySelected(DateTime day, DateTime focusedDay) { void _onDaySelected(DateTime day, DateTime focusedDay) {
setState(() { setState(() {
today = day; today = day;
numDay = today.weekday;
}); });
} }
@@ -65,6 +87,7 @@ class _ProfessionalCalendarScreenState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
DateTime lastDay = today.add(const Duration(days: 365)); DateTime lastDay = today.add(const Duration(days: 365));
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Calendario'), title: const Text('Calendario'),
@@ -73,13 +96,25 @@ class _ProfessionalCalendarScreenState
? FloatingActionButtonLocation.startFloat ? FloatingActionButtonLocation.startFloat
: FloatingActionButtonLocation.endFloat, : FloatingActionButtonLocation.endFloat,
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
body: Column( body: BlocProvider<ServiceBloc>(
create: (context) => Injector.appInstance.get<ServiceBloc>(),
child: BlocConsumer<ServiceBloc, ServiceState>(
listener: (context, serviceState) {
if (serviceState is CreateServiceLoading) {
isLoading = true;
}
if (serviceState is CreateServiceFailure) {
isLoading = false;
}
},
builder: (context, state) {
return Column(
children: [ children: [
Container( Container(
color: const Color.fromARGB(255, 224, 247, 255), color: const Color.fromARGB(255, 224, 247, 255),
child: TableCalendar( child: TableCalendar(
locale: 'es_MX', locale: 'es_MX',
firstDay: DateTime.utc(2010, 10, 16), firstDay: DateTime.now(),
lastDay: lastDay, lastDay: lastDay,
focusedDay: today, focusedDay: today,
availableGestures: AvailableGestures.all, availableGestures: AvailableGestures.all,
@@ -87,18 +122,6 @@ class _ProfessionalCalendarScreenState
selectedDayPredicate: (day) => isSameDay(day, today), selectedDayPredicate: (day) => isSameDay(day, today),
calendarFormat: _calendarFormat, calendarFormat: _calendarFormat,
onFormatChanged: _onFormatChange, 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 { availableCalendarFormats: const {
CalendarFormat.month: 'Mes', CalendarFormat.month: 'Mes',
CalendarFormat.week: 'Semana', CalendarFormat.week: 'Semana',
@@ -106,18 +129,21 @@ class _ProfessionalCalendarScreenState
}, },
), ),
), ),
// SizedBox( SizedBox(
// width: double.infinity, width: double.infinity,
// child: Padding( child: Padding(
// padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), padding:
// child: Text(DateFormat('dd MMMM yyyy', 'es').format(today), const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
// style: const TextStyle( child: Text(
// color: Colors.black, DateFormat('dd MMMM yyyy', 'es').format(today),
// fontSize: 16, style: const TextStyle(
// fontWeight: FontWeight.w600, color: Colors.black,
// )), fontSize: 16,
// ), fontWeight: FontWeight.w600,
// ), ),
),
),
),
const Divider( const Divider(
height: 0, height: 0,
), ),
@@ -125,17 +151,260 @@ class _ProfessionalCalendarScreenState
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 15), padding: const EdgeInsets.only(bottom: 15),
child: Column( child: Column(
children: const [], children: [
), ..._rangesItems(
), _getScheduleFromNumDay(numDay), context),
),
], ],
), ),
floatingActionButton: FloatingActionButton( ),
onPressed: () {}, )
// onPressed: _showDialog, ],
child: const Icon(Icons.add), );
},
),
), ),
); );
} }
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<Widget> _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<TimeOfDay> ranges = TimeOfDayUtils.genRanges(
schedule.range1Hour1!,
schedule.range2Hour2!,
);
return rangesItemList(ranges, _services, today, context);
}
List<TimeOfDay> ranges1 = TimeOfDayUtils.genRanges(
schedule.range1Hour1!,
schedule.range1Hour2!,
);
List<TimeOfDay> ranges2 = TimeOfDayUtils.genRanges(
schedule.range2Hour1!,
schedule.range2Hour2!,
);
return [
...rangesItemList(ranges1, _services, today, context),
...rangesItemList(ranges2, _services, today, context),
];
}
bool _isHora1Ocupada(
TimeOfDay hora1, List<ServiceEntity>? 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<Widget> rangesItemList(List<TimeOfDay> ranges,
List<ServiceEntity>? 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<ServiceBloc>().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();
}
} }
@@ -121,23 +121,6 @@ class ServiceEntity extends Equatable {
@override @override
String toString() { String toString() {
return '''ServiceEntity{{ 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 }}''';
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
}}''';
} }
} }
@@ -7,6 +7,7 @@ enum ServiceStatus {
active, // 3 active, // 3
cancelled, // 4 cancelled, // 4
completed, // 5 completed, // 5
selfBooked // 6
} }
int enumToIntService(ServiceStatus state) { int enumToIntService(ServiceStatus state) {
@@ -41,7 +41,7 @@ class FirebaseServiceRepository {
String professionalId) { String professionalId) {
return serviceCollection return serviceCollection
.where('professional_id', isEqualTo: professionalId) .where('professional_id', isEqualTo: professionalId)
.where('status', whereIn: [0, 1, 2, 3]) .where('status', whereIn: [1, 2, 3])
.snapshots() .snapshots()
.map((querySnapshot) => querySnapshot.docs .map((querySnapshot) => querySnapshot.docs
.map((doc) => ServiceEntity.fromDocument(doc.data(), doc.id)) .map((doc) => ServiceEntity.fromDocument(doc.data(), doc.id))
@@ -52,7 +52,7 @@ class FirebaseServiceRepository {
String professionalId) async { String professionalId) async {
QuerySnapshot<Map<String, dynamic>> query = await serviceCollection QuerySnapshot<Map<String, dynamic>> query = await serviceCollection
.where('professional_id', isEqualTo: professionalId) .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 return query.docs
.map((doc) => ServiceEntity.fromDocument(doc.data(), doc.id)) .map((doc) => ServiceEntity.fromDocument(doc.data(), doc.id))