replace: swap prosappweb content for prosapp_web_app (more complete version)
prosapp_web_app has chat, dashboard, calendar, support, 13 providers and Fluro URL routing. Keep Dockerfile + nginx.conf from previous prosappweb. Upgrade google_fonts 6.2.1 → 8.1.0 (Dart 3.12 compat fix). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
74a4f41902
commit
15175c1b91
@@ -0,0 +1,24 @@
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BlankView extends StatelessWidget {
|
||||
const BlankView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
Text('Blank view', style: CustomLabels.h1),
|
||||
SizedBox(height: 10),
|
||||
WhiteCard(
|
||||
title: 'Blank',
|
||||
child: Text('Blank View'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:prosapp_web_app/models/profesional.dart';
|
||||
import 'package:prosapp_web_app/models/schedules_entity.dart';
|
||||
import 'package:prosapp_web_app/models/service.dart';
|
||||
import 'package:prosapp_web_app/providers/professional_form_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/professional_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/calendar_services_provider.dart';
|
||||
import 'package:prosapp_web_app/services/notifications_service.dart';
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
import 'package:prosapp_web_app/utils/time_of_day_extension.dart';
|
||||
import 'package:prosapp_web_app/utils/time_of_day_utils.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
class CalendarView extends StatefulWidget {
|
||||
final String professionalId;
|
||||
|
||||
const CalendarView({super.key, required this.professionalId});
|
||||
|
||||
@override
|
||||
State<CalendarView> createState() => _CalendarViewState();
|
||||
}
|
||||
|
||||
class _CalendarViewState extends State<CalendarView> {
|
||||
List<Service>? _services;
|
||||
DateTime today = DateTime.now();
|
||||
late int numDay;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
today = DateTime.utc(today.year, today.month, today.day);
|
||||
numDay = today.weekday;
|
||||
|
||||
_fetchProfessionalAndServices();
|
||||
}
|
||||
|
||||
void _fetchProfessionalAndServices() async {
|
||||
final professionalFormProvider =
|
||||
Provider.of<ProfessionalFormProvider>(context, listen: false);
|
||||
final servicesProvider =
|
||||
Provider.of<CalendarServicesProvider>(context, listen: false);
|
||||
final proProvider =
|
||||
Provider.of<ProfessionalProvider>(context, listen: false);
|
||||
|
||||
final professional =
|
||||
await proProvider.getProfessional(widget.professionalId);
|
||||
professionalFormProvider.setProfesional(professional);
|
||||
|
||||
final services =
|
||||
await servicesProvider.getServicesForProfessional(professional.id);
|
||||
setState(() {
|
||||
_services = services;
|
||||
});
|
||||
}
|
||||
|
||||
void _onDaySelected(DateTime day, DateTime focusedDay) {
|
||||
setState(() {
|
||||
today = day;
|
||||
numDay = today.weekday;
|
||||
});
|
||||
_fetchProfessionalAndServices();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<ProfessionalFormProvider>(
|
||||
builder: (context, professionalFormProvider, child) {
|
||||
if (professionalFormProvider.profesional == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
final profesional = professionalFormProvider.profesional!;
|
||||
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: WhiteCard(
|
||||
title: 'Calendario',
|
||||
child: Column(
|
||||
children: [
|
||||
TableCalendar(
|
||||
locale: 'es_CO',
|
||||
firstDay: DateTime.now(),
|
||||
lastDay: DateTime.now().add(const Duration(days: 180)),
|
||||
focusedDay: today,
|
||||
availableGestures: AvailableGestures.all,
|
||||
onDaySelected: _onDaySelected,
|
||||
selectedDayPredicate: (day) => isSameDay(day, today),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
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),
|
||||
Column(
|
||||
children: [
|
||||
..._rangesItems(
|
||||
_getScheduleFromNumDay(numDay, profesional),
|
||||
context),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
ScheduleEntity? _getScheduleFromNumDay(
|
||||
int numDay, Profesional userProfessional) {
|
||||
switch (numDay) {
|
||||
case 1:
|
||||
return userProfessional.schedules.monday;
|
||||
case 2:
|
||||
return userProfessional.schedules.tuesday;
|
||||
case 3:
|
||||
return userProfessional.schedules.wednesday;
|
||||
case 4:
|
||||
return userProfessional.schedules.thursday;
|
||||
case 5:
|
||||
return userProfessional.schedules.friday;
|
||||
case 6:
|
||||
return userProfessional.schedules.saturday;
|
||||
case 7:
|
||||
return userProfessional.schedules.sunday;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _rangesItems(ScheduleEntity? schedule, BuildContext context) {
|
||||
if (schedule == null) {
|
||||
return [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Text("No hay horarios disponibles"),
|
||||
)
|
||||
];
|
||||
}
|
||||
if (!schedule.enabled) {
|
||||
return [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Text("No hay horarios disponibles"),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
if (schedule.continuousDay) {
|
||||
if (schedule.range1Hour1 == null || schedule.range2Hour2 == null || schedule.range1Hour1!.compareTo(schedule.range2Hour2!) >= 0) { return [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Text("No hay horarios disponibles"),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
List<TimeOfDay> ranges = TimeOfDayUtils.genRanges(
|
||||
schedule.range1Hour1!,
|
||||
schedule.range2Hour2!,
|
||||
);
|
||||
|
||||
return rangesItemList(ranges, _services, today, context);
|
||||
} else {
|
||||
if (schedule.range1Hour1 == null || schedule.range1Hour2 == null || schedule.range2Hour1 == null || schedule.range2Hour2 == null || schedule.range1Hour1!.compareTo(schedule.range1Hour2!) >= 0 || schedule.range2Hour1!.compareTo(schedule.range2Hour2!) >= 0) {
|
||||
return [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Text("No hay horarios disponibles"),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
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<Service>? events, DateTime selectedDay) {
|
||||
if (events != null) {
|
||||
for (Service event in events) {
|
||||
if (selectedDay.toIso8601String().split('T').first == event.day) {
|
||||
if (hora1 == event.range1Hour1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Widget> rangesItemList(List<TimeOfDay> ranges, List<Service>? events,DateTime selectedDay, BuildContext context) {
|
||||
final currentDateTime = DateTime.now();
|
||||
|
||||
return ranges.map((time) {
|
||||
final selectedDateTime = DateTime(
|
||||
selectedDay.year,
|
||||
selectedDay.month,
|
||||
selectedDay.day,
|
||||
time.hour,
|
||||
time.minute,
|
||||
);
|
||||
|
||||
if (selectedDateTime
|
||||
.isBefore(currentDateTime.add(const Duration(hours: 3)))) {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
margin: const EdgeInsets.only(top: 15, left: 10, right: 10),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.grey,
|
||||
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,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
subtitle: const Text(
|
||||
'No disponible',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_isHora1Ocupada(time, events, selectedDay)) {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
margin: const EdgeInsets.only(top: 15, left: 10, right: 10),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: ListTile(
|
||||
onTap: () {
|
||||
if (events != null) {
|
||||
for (Service event in events) {
|
||||
if (selectedDay.toIso8601String().split('T').first ==
|
||||
event.day) {
|
||||
if (time == event.range1Hour1) {
|
||||
NotificationsService.showSnackbar('Ocupado');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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.only(top: 15, left: 10, right: 10),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: ListTile(
|
||||
onTap: () {
|
||||
Navigator.pop(context, [selectedDay, time]);
|
||||
},
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:prosapp_web_app/models/message_entity.dart';
|
||||
import 'package:prosapp_web_app/providers/chat_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/score_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/services_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/sidemenu_provider.dart';
|
||||
import 'package:prosapp_web_app/services/navigation_service.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ChatView extends StatelessWidget {
|
||||
final String type;
|
||||
final String serviceId;
|
||||
final String professionalId;
|
||||
|
||||
const ChatView({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.serviceId,
|
||||
required this.professionalId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final chatProvider = Provider.of<ChatProvider>(context);
|
||||
final servicesProvider =
|
||||
Provider.of<ServicesProvider>(context, listen: false);
|
||||
final scoreProvider = Provider.of<ScoreProvider>(context, listen: false);
|
||||
|
||||
scoreProvider.loadReputation(professionalId);
|
||||
|
||||
return StreamBuilder(
|
||||
stream: chatProvider.getChat(serviceId),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final chat = snapshot.data;
|
||||
|
||||
if (chat == null) {
|
||||
return const Center(
|
||||
child: Text('Chat no encontrado'),
|
||||
);
|
||||
}
|
||||
|
||||
final service = servicesProvider.service?.service;
|
||||
final user = servicesProvider.service?.user;
|
||||
final reputation = scoreProvider.reputation;
|
||||
|
||||
if (service == null || user == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final image = (user.picture == '' || user.picture == null)
|
||||
? const Image(image: AssetImage('no-image.jpg'))
|
||||
: FadeInImage.assetNetwork(
|
||||
placeholder: 'loader.gif',
|
||||
fit: BoxFit.cover,
|
||||
image: user.picture!,
|
||||
);
|
||||
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
color: const Color(0xFFD6F4FF),
|
||||
alignment: Alignment.topCenter,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 20),
|
||||
SizedBox(
|
||||
width: 85,
|
||||
height: 85,
|
||||
child: ClipOval(child: image),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
user.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
RatingBar.builder(
|
||||
initialRating: type == 'user'
|
||||
? calculoRating(reputation.averagePro)
|
||||
: calculoRating(reputation.average),
|
||||
minRating: 1,
|
||||
direction: Axis.horizontal,
|
||||
allowHalfRating: true,
|
||||
itemCount: 5,
|
||||
itemSize: 25,
|
||||
maxRating: 5,
|
||||
itemBuilder: (context, _) => const Icon(
|
||||
Icons.star,
|
||||
color: Color(0xFF2BA4EC),
|
||||
),
|
||||
onRatingUpdate: (rating) {},
|
||||
ignoreGestures: true,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
if (type == 'user')
|
||||
Text(
|
||||
'${reputation.averagePro.toStringAsFixed(1)} (${reputation.totalPro.toString()})',
|
||||
),
|
||||
if (type == 'professional')
|
||||
Text(
|
||||
'${reputation.average.toStringAsFixed(1)} (${reputation.total.toString()})',
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
reverse: true,
|
||||
child: Column(
|
||||
children: _messagesList(chat.messages),
|
||||
),
|
||||
),
|
||||
),
|
||||
_MessageInput(
|
||||
serviceId: serviceId,
|
||||
userId: FirebaseAuth.instance.currentUser!.uid),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double calculoRating(double average) {
|
||||
String numeroString = average.toString();
|
||||
List<String> partes = numeroString.split('.');
|
||||
int parteEntera = int.parse(partes[0]);
|
||||
int parteFraccionaria = partes.length > 1 ? int.parse(partes[1]) : 0;
|
||||
|
||||
if (parteFraccionaria >= 3) {
|
||||
parteFraccionaria = 5;
|
||||
} else {
|
||||
parteFraccionaria = 0;
|
||||
}
|
||||
|
||||
// Unir la parte entera y fraccionaria y convertirlo nuevamente a double
|
||||
double resultado = double.parse('$parteEntera.$parteFraccionaria');
|
||||
return resultado;
|
||||
}
|
||||
|
||||
List<Widget> _messagesList(List<MessageEntity> messages) {
|
||||
return messages
|
||||
.map(
|
||||
(e) => e.ownerId != FirebaseAuth.instance.currentUser!.uid
|
||||
? ListTile(
|
||||
title: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 60),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(20),
|
||||
bottomLeft: Radius.circular(20),
|
||||
bottomRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
e.content,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Text(
|
||||
_getFechaHoraFormateada(e.createdAt),
|
||||
style:
|
||||
const TextStyle(color: Colors.grey, fontSize: 11),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListTile(
|
||||
title: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 60),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 16,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFD5EFFF),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
bottomLeft: Radius.circular(20),
|
||||
bottomRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
e.content,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Text(
|
||||
_getFechaHoraFormateada(e.createdAt),
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
String _getFechaHoraFormateada(DateTime fecha) {
|
||||
final now = DateTime.now();
|
||||
final yesterday = now.subtract(const Duration(days: 1));
|
||||
final formatter = DateFormat('h:mm a');
|
||||
|
||||
if (fecha.day == now.day &&
|
||||
fecha.month == now.month &&
|
||||
fecha.year == now.year) {
|
||||
return 'Hoy - ${formatter.format(fecha)}';
|
||||
} else if (fecha.day == yesterday.day &&
|
||||
fecha.month == yesterday.month &&
|
||||
fecha.year == yesterday.year) {
|
||||
return 'Ayer - ${formatter.format(fecha)}';
|
||||
} else {
|
||||
return '${DateFormat('dd/MM/yyyy').format(fecha)} - ${formatter.format(fecha)}';
|
||||
}
|
||||
}
|
||||
|
||||
void navigateTo(String routeName) {
|
||||
NavigationService.replaceTo(routeName);
|
||||
SideMenuProvider.closeMenu();
|
||||
}
|
||||
}
|
||||
|
||||
class _MessageInput extends StatefulWidget {
|
||||
final String serviceId;
|
||||
final String userId;
|
||||
|
||||
const _MessageInput({
|
||||
required this.serviceId,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_MessageInput> createState() => _MessageInputState();
|
||||
}
|
||||
|
||||
class _MessageInputState extends State<_MessageInput> {
|
||||
final _controller = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final chatProvider = Provider.of<ChatProvider>(context, listen: false);
|
||||
|
||||
void sendMessage() {
|
||||
if (_controller.text.trim().isNotEmpty) {
|
||||
final message = MessageEntity(
|
||||
ownerId: widget.userId,
|
||||
content: _controller.text.trim(),
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
chatProvider.sendMessage(widget.serviceId, message);
|
||||
_controller.clear();
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Mensaje',
|
||||
hintStyle: TextStyle(color: Colors.grey[600], fontSize: 16),
|
||||
border: OutlineInputBorder(
|
||||
borderSide:
|
||||
const BorderSide(color: Colors.grey, width: 1.0),
|
||||
borderRadius: BorderRadius.circular(50)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
const BorderSide(color: Colors.grey, width: 1.0),
|
||||
borderRadius: BorderRadius.circular(50)),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[200],
|
||||
),
|
||||
onSubmitted: (value) => sendMessage(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: () => sendMessage(),
|
||||
child: Container(
|
||||
height: 50,
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2BA4EC),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.send,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:prosapp_web_app/models/schedules_entity.dart';
|
||||
import 'package:prosapp_web_app/models/service.dart';
|
||||
import 'package:prosapp_web_app/models/service_location_preferences.dart';
|
||||
import 'package:prosapp_web_app/models/service_status.dart';
|
||||
import 'package:prosapp_web_app/models/usuario.dart';
|
||||
import 'package:prosapp_web_app/models/usuario_profesional.dart';
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/cities_provider.dart';
|
||||
import 'package:prosapp_web_app/router/router.dart';
|
||||
import 'package:prosapp_web_app/services/navigation_service.dart';
|
||||
import 'package:prosapp_web_app/services/notifications_service.dart';
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:prosapp_web_app/utils/local_notifications.dart';
|
||||
import 'package:prosapp_web_app/utils/network_utility.dart';
|
||||
import 'package:prosapp_web_app/utils/time_of_day_extension.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DashboardView extends StatefulWidget {
|
||||
const DashboardView({super.key});
|
||||
|
||||
@override
|
||||
State<DashboardView> createState() => _DashboardViewState();
|
||||
}
|
||||
|
||||
class _DashboardViewState extends State<DashboardView> {
|
||||
Usuario? user;
|
||||
List<dynamic> _placesList = [];
|
||||
final TextEditingController _addressController = TextEditingController();
|
||||
Timer? _debounce;
|
||||
UsuarioProfesional? selectedProfessional;
|
||||
String? selectedProfessionalName;
|
||||
DateTime? selectedDay;
|
||||
TimeOfDay? selectedHour;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
setState(() {
|
||||
user = authProvider.user;
|
||||
});
|
||||
}
|
||||
|
||||
void placeAutoComplete(String query, String _coords) async {
|
||||
Uri uri = Uri.https("admin.prosapp.co", "/autocomplete", {
|
||||
"input": query,
|
||||
"location": _coords,
|
||||
});
|
||||
|
||||
String? response = await NetworkUtility.fetchUrl(uri);
|
||||
|
||||
if (response != null) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_placesList = jsonDecode(response.toString())['results'];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectProfessional(BuildContext context) async {
|
||||
try {
|
||||
List<dynamic> result = await NavigationService.navigateToFuture(
|
||||
Flurorouter.professionalsRoute);
|
||||
|
||||
final UsuarioProfesional selectedProfessional = result[0];
|
||||
|
||||
print('selectedProfessional: $selectedProfessional');
|
||||
|
||||
setState(() {
|
||||
selectedDay = result[1];
|
||||
selectedHour = result[2];
|
||||
selectedProfessionalName = selectedProfessional.user.name;
|
||||
this.selectedProfessional = selectedProfessional;
|
||||
_addressController.text = selectedProfessional.professionalInfo.address;
|
||||
});
|
||||
} catch (e) {
|
||||
print('debugeando $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounce?.cancel();
|
||||
_addressController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (user == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
bool isUserComplete() {
|
||||
return user!.name != '' &&
|
||||
user!.email != '' &&
|
||||
user!.phone != '' &&
|
||||
user!.city != '';
|
||||
}
|
||||
|
||||
final citiesProvider = Provider.of<CitiesProvider>(context);
|
||||
|
||||
if (citiesProvider.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
final _coords = citiesProvider.getCoordsOfCity(user!.city!);
|
||||
|
||||
_createService() async {
|
||||
print('debug ${selectedProfessional?.user.id ?? 'user.id'}');
|
||||
print('debug ${user!.id ?? 'user.id'}');
|
||||
print('debug ${_addressController.text ?? 'user.id'}');
|
||||
|
||||
try {
|
||||
double latitude = 0.0;
|
||||
double longitude = 0.0;
|
||||
|
||||
print('debug 1');
|
||||
|
||||
Service service = Service(
|
||||
id: null,
|
||||
professionalId: selectedProfessional!.user.id,
|
||||
professionalScored: false,
|
||||
userId: user!.id,
|
||||
userScored: false,
|
||||
address: _addressController.text,
|
||||
aditionalAddress: '',
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
day: selectedDay.toString(),
|
||||
createdAt: Timestamp.now(),
|
||||
description: '',
|
||||
range1Hour1: selectedHour!,
|
||||
range1Hour2: selectedHour!.add(hour: 2),
|
||||
rate: '',
|
||||
status: ServiceStatus.pending,
|
||||
location: ServiceLocationPreferences.delivery,
|
||||
);
|
||||
|
||||
print('debug 1');
|
||||
|
||||
await FirebaseFirestore.instance
|
||||
.collection('services')
|
||||
.add(service.toDocument());
|
||||
|
||||
print('debug 2');
|
||||
|
||||
NotificationsService.showSnackbar('Servicio solicitado exitosamente');
|
||||
|
||||
if (selectedProfessional != null) {
|
||||
if (selectedProfessional!.user.token != null) {
|
||||
LocalNotifications.sendPushNotification(
|
||||
selectedProfessional!.user.token!,
|
||||
'Nuevo servicio',
|
||||
'Tienes una nueva solicitud de servicio pendiente',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_addressController.clear();
|
||||
setState(() {
|
||||
selectedProfessional = null;
|
||||
selectedProfessionalName = null;
|
||||
selectedDay = null;
|
||||
selectedHour = null;
|
||||
});
|
||||
} catch (e) {
|
||||
NotificationsService.showSnackBarError(
|
||||
'$e Error al solicitar el servicio, intenta de nuevo');
|
||||
}
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: Stack(
|
||||
children: [
|
||||
ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
WhiteCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
isUserComplete()
|
||||
? TextFormField(
|
||||
controller: _addressController,
|
||||
onChanged: (value) {
|
||||
if (_debounce?.isActive ?? false)
|
||||
_debounce?.cancel();
|
||||
|
||||
_debounce = Timer(
|
||||
const Duration(milliseconds: 500), () {
|
||||
String modifiedValue =
|
||||
value.replaceAll(' ', '_');
|
||||
placeAutoComplete(modifiedValue, _coords);
|
||||
});
|
||||
},
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Ingresa tu dirección',
|
||||
label: 'Dirección',
|
||||
icon: Icons.location_on,
|
||||
),
|
||||
)
|
||||
: GestureDetector(
|
||||
onTap: () => NotificationsService.showSnackBarError(
|
||||
'Completa tu perfil para solicitar un servicio'),
|
||||
child: AbsorbPointer(
|
||||
child: TextFormField(
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Ingresa tu dirección',
|
||||
label: 'Dirección',
|
||||
icon: Icons.location_on,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
GestureDetector(
|
||||
onTap: () => isUserComplete()
|
||||
? _selectProfessional(context)
|
||||
: NotificationsService.showSnackBarError(
|
||||
'Completa tu perfil para solicitar un servicio'),
|
||||
child: AbsorbPointer(
|
||||
child: TextFormField(
|
||||
controller: TextEditingController(
|
||||
text: selectedProfessionalName,
|
||||
),
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Selecciona un Profesional',
|
||||
label: 'Profesional',
|
||||
icon: Icons.person_rounded,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (selectedDay != null && selectedHour != null) ...[
|
||||
TextFormField(
|
||||
readOnly: true,
|
||||
controller: TextEditingController(
|
||||
text: selectedDay == null
|
||||
? ''
|
||||
: DateFormat('dd/MM/yyyy').format(selectedDay!),
|
||||
),
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Fecha',
|
||||
label: 'Fecha',
|
||||
icon: Icons.calendar_month,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
readOnly: true,
|
||||
controller: TextEditingController(
|
||||
text: selectedHour == null
|
||||
? ''
|
||||
: ScheduleEntity.getFormatTime(selectedHour),
|
||||
),
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Hora',
|
||||
label: 'Hora',
|
||||
icon: Icons.watch_later_outlined,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 230),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (!isUserComplete()) {
|
||||
NotificationsService.showSnackBarError(
|
||||
'Completa tu perfil para solicitar un servicio');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_createService();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.blue.shade400),
|
||||
shape: WidgetStateProperty.all(
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(
|
||||
Colors.transparent),
|
||||
),
|
||||
child: const Text(
|
||||
'Solicitar cita',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
if (selectedDay != null &&
|
||||
selectedHour != null &&
|
||||
selectedProfessional != null) ...[
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
_addressController.clear();
|
||||
selectedProfessional = null;
|
||||
selectedDay = null;
|
||||
selectedHour = null;
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.red,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5)))),
|
||||
shadowColor: WidgetStateProperty.all(
|
||||
Colors.transparent)),
|
||||
child: const Icon(
|
||||
Icons.close_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 80,
|
||||
child: _placesList.isNotEmpty
|
||||
? Material(
|
||||
elevation: 5.0,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _placesList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
title:
|
||||
Text(_placesList[index]['formatted_address']),
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.compact,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_addressController.text =
|
||||
_placesList[index]['formatted_address'];
|
||||
_placesList = [];
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/email_form_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/phone_form_provider.dart';
|
||||
import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart';
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class EmailView extends StatelessWidget {
|
||||
const EmailView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authProvider = Provider.of<AuthProvider>(context);
|
||||
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
final TextEditingController _confirmPasswordController =
|
||||
TextEditingController();
|
||||
|
||||
final RegExp emailRegex =
|
||||
RegExp(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
|
||||
|
||||
RegExp passwordRegex = RegExp(r'^(?=.*?[0-9])');
|
||||
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => EmailFormProvider(),
|
||||
child: Builder(builder: (context) {
|
||||
final emailFormProvider =
|
||||
Provider.of<EmailFormProvider>(context, listen: false);
|
||||
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: WhiteCard(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 40),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 370),
|
||||
child: Form(
|
||||
autovalidateMode:
|
||||
AutovalidateMode.onUserInteraction,
|
||||
key: emailFormProvider.formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Ingresa un email';
|
||||
}
|
||||
|
||||
if (!emailRegex.hasMatch(value)) {
|
||||
return 'Ingresa un email válido';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (email) =>
|
||||
emailFormProvider.email = email,
|
||||
decoration:
|
||||
CustomInputs.loginInputDecoration(
|
||||
hint: 'Ingresa tu email',
|
||||
label: 'Email',
|
||||
icon: Icons.email_outlined,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
onChanged: (value) =>
|
||||
emailFormProvider.password = value,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Ingresa una contraseña';
|
||||
}
|
||||
|
||||
if (value.length < 6) {
|
||||
return 'La contraseña debe tener al menos 6 caracteres';
|
||||
}
|
||||
|
||||
if (!passwordRegex.hasMatch(value)) {
|
||||
return 'La contraseña debe tener al menos un número';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration:
|
||||
CustomInputs.loginInputDecoration(
|
||||
hint: 'Ingresa tu contraseña',
|
||||
label: 'Contraseña',
|
||||
icon: Icons.lock_outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
obscureText: true,
|
||||
controller: _confirmPasswordController,
|
||||
onChanged: (value) =>
|
||||
emailFormProvider.password = value,
|
||||
validator: (value) {
|
||||
if (value != _passwordController.text) {
|
||||
return 'Las contraseñas no coinciden';
|
||||
}
|
||||
if (value!.isEmpty) {
|
||||
return 'La contraseña es obligatoria';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration:
|
||||
CustomInputs.loginInputDecoration(
|
||||
hint: 'Confirma tu contraseña',
|
||||
label: 'Confirmar contraseña',
|
||||
icon: Icons.lock_outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final isValid =
|
||||
emailFormProvider.validateForm();
|
||||
if (isValid) {
|
||||
await authProvider.addEmailAndPassword(
|
||||
emailFormProvider.email,
|
||||
emailFormProvider.password);
|
||||
}
|
||||
},
|
||||
text: "Guardar",
|
||||
color: Colors.blue,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/login_form_provider.dart';
|
||||
import 'package:prosapp_web_app/router/router.dart';
|
||||
import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart';
|
||||
import 'package:prosapp_web_app/ui/buttons/link_text.dart';
|
||||
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
|
||||
import 'package:email_validator/email_validator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LoginView extends StatelessWidget {
|
||||
const LoginView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authProvider = Provider.of<AuthProvider>(context);
|
||||
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => LoginFormProvider(),
|
||||
child: Builder(builder: (context) {
|
||||
final loginFormProvider =
|
||||
Provider.of<LoginFormProvider>(context, listen: false);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 100),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 370),
|
||||
child: Form(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
key: loginFormProvider.formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
// Email
|
||||
TextFormField(
|
||||
onFieldSubmitted: (_) =>
|
||||
onFormSubmit(loginFormProvider, authProvider),
|
||||
validator: (value) {
|
||||
if (!EmailValidator.validate(value ?? '')) {
|
||||
return 'Email no válido';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) => loginFormProvider.email = value,
|
||||
decoration: CustomInputs.loginInputDecoration(
|
||||
hint: 'Ingresa tu correo',
|
||||
label: 'Email',
|
||||
icon: Icons.email_outlined,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Password
|
||||
TextFormField(
|
||||
onFieldSubmitted: (_) =>
|
||||
onFormSubmit(loginFormProvider, authProvider),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Ingresa tu contraseña";
|
||||
}
|
||||
|
||||
if (value.length < 8) {
|
||||
return "La contraseña debe tener al menos 8 caracteres";
|
||||
}
|
||||
|
||||
return null; // Válido
|
||||
},
|
||||
onChanged: (value) => loginFormProvider.password = value,
|
||||
obscureText: true,
|
||||
decoration: CustomInputs.loginInputDecoration(
|
||||
hint: 'Ingresa tu contraseña',
|
||||
label: 'Contraseña',
|
||||
icon: Icons.lock_outline,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
CustomOutlinedButton(
|
||||
onPressed: () =>
|
||||
onFormSubmit(loginFormProvider, authProvider),
|
||||
text: "Ingresar",
|
||||
color: Colors.blue,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
LinkText(
|
||||
text: "Nueva cuenta",
|
||||
onPressed: () {
|
||||
Navigator.pushReplacementNamed(
|
||||
context, Flurorouter.registerRoute);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
|
||||
LinkText(
|
||||
text: "Entrar con celular",
|
||||
onPressed: () {
|
||||
Navigator.pushReplacementNamed(
|
||||
context, Flurorouter.phoneLoginRoute);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
void onFormSubmit(
|
||||
LoginFormProvider loginFormProvider, AuthProvider authProvider) async {
|
||||
final isValid = loginFormProvider.validateForm();
|
||||
if (isValid) {
|
||||
await authProvider.login(
|
||||
loginFormProvider.email,
|
||||
loginFormProvider.password,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class NoPageFoundView extends StatelessWidget {
|
||||
const NoPageFoundView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'404 - No Page Found',
|
||||
style: GoogleFonts.montserratAlternates(
|
||||
fontSize: 50, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/phone_form_provider.dart';
|
||||
import 'package:prosapp_web_app/router/router.dart';
|
||||
import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart';
|
||||
import 'package:prosapp_web_app/ui/buttons/link_text.dart';
|
||||
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class PhoneLoginView extends StatelessWidget {
|
||||
const PhoneLoginView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authProvider = Provider.of<AuthProvider>(context);
|
||||
|
||||
final TextEditingController _phoneController = TextEditingController();
|
||||
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => PhoneFormProvider(),
|
||||
child: Builder(builder: (context) {
|
||||
final phoneFormProvider =
|
||||
Provider.of<PhoneFormProvider>(context, listen: false);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 370),
|
||||
child: Form(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
key: phoneFormProvider.formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _phoneController,
|
||||
validator: (value) {
|
||||
if (value == null ||
|
||||
value.isEmpty ||
|
||||
value.length < 10) {
|
||||
return 'Ingresa un número de teléfono válido';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
phoneFormProvider.phone = '+57${value.trim()}';
|
||||
},
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: CustomInputs.loginInputDecoration(
|
||||
hint: 'Ingresa tu número de teléfono',
|
||||
label: 'Número de Teléfono',
|
||||
icon: Icons.phone_android_outlined,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final isValid = phoneFormProvider.validateForm();
|
||||
if (isValid) {
|
||||
await authProvider
|
||||
.verifyPhoneNumber(phoneFormProvider.phone);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
_buildOtpModal(context, authProvider),
|
||||
);
|
||||
}
|
||||
},
|
||||
text: "Enviar código",
|
||||
color: Colors.blue,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
LinkText(
|
||||
text: "Entrar con correo",
|
||||
onPressed: () {
|
||||
Navigator.pushReplacementNamed(
|
||||
context, Flurorouter.loginRoute);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOtpModal(BuildContext context, AuthProvider authProvider) {
|
||||
final _otpController = TextEditingController();
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text('Ingresar código OTP'),
|
||||
content: TextField(
|
||||
controller: _otpController,
|
||||
decoration: const InputDecoration(labelText: 'Código OTP'),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final otp = _otpController.text.trim();
|
||||
|
||||
if (otp.isNotEmpty) {
|
||||
await authProvider.signInWithOTP(otp);
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Por favor ingresa el código OTP')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Verificar OTP'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/phone_form_provider.dart';
|
||||
import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart';
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class PhoneView extends StatelessWidget {
|
||||
const PhoneView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authProvider = Provider.of<AuthProvider>(context);
|
||||
|
||||
final TextEditingController _phoneController = TextEditingController();
|
||||
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => PhoneFormProvider(),
|
||||
child: Builder(builder: (context) {
|
||||
final phoneFormProvider =
|
||||
Provider.of<PhoneFormProvider>(context, listen: false);
|
||||
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: WhiteCard(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 40),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 370),
|
||||
child: Form(
|
||||
autovalidateMode:
|
||||
AutovalidateMode.onUserInteraction,
|
||||
key: phoneFormProvider.formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _phoneController,
|
||||
validator: (value) {
|
||||
if (value == null ||
|
||||
value.isEmpty ||
|
||||
value.length < 10) {
|
||||
return 'Ingresa un número de teléfono válido';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
phoneFormProvider.phone =
|
||||
'+57${value.trim()}';
|
||||
},
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration:
|
||||
CustomInputs.loginInputDecoration(
|
||||
hint: 'Ingresa tu número de teléfono',
|
||||
label: 'Número de Teléfono',
|
||||
icon: Icons.phone_android_outlined,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final isValid =
|
||||
phoneFormProvider.validateForm();
|
||||
if (isValid) {
|
||||
await authProvider
|
||||
.verifyPhoneNumberForLink(
|
||||
phoneFormProvider.phone);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _buildOtpModal(
|
||||
context, authProvider),
|
||||
);
|
||||
}
|
||||
},
|
||||
text: "Enviar código",
|
||||
color: Colors.blue,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOtpModal(BuildContext context, AuthProvider authProvider) {
|
||||
final _otpController = TextEditingController();
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text('Ingresar código OTP'),
|
||||
content: TextField(
|
||||
controller: _otpController,
|
||||
decoration: const InputDecoration(labelText: 'Código OTP'),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final otp = _otpController.text.trim();
|
||||
|
||||
if (otp.isNotEmpty) {
|
||||
await authProvider.linkPhoneWithOTP(otp);
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Por favor ingresa el código OTP')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Verificar OTP'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:prosapp_web_app/models/profesional.dart';
|
||||
import 'package:prosapp_web_app/models/schedules_entity.dart';
|
||||
import 'package:prosapp_web_app/models/service.dart';
|
||||
import 'package:prosapp_web_app/models/usuario.dart';
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/professional_form_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/professional_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/services_provider.dart';
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
import 'package:prosapp_web_app/utils/time_of_day_utils.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProfessionalCalendarView extends StatefulWidget {
|
||||
const ProfessionalCalendarView({super.key});
|
||||
|
||||
@override
|
||||
State<ProfessionalCalendarView> createState() =>
|
||||
_ProfessionalCalendarViewState();
|
||||
}
|
||||
|
||||
class _ProfessionalCalendarViewState extends State<ProfessionalCalendarView> {
|
||||
DateTime today = DateTime.now();
|
||||
late int numDay;
|
||||
|
||||
List<Service>? _services;
|
||||
|
||||
Usuario? user;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
numDay = today.weekday;
|
||||
_fetchProfessionalAndServices();
|
||||
}
|
||||
|
||||
void _fetchProfessionalAndServices() async {
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
final professionalFormProvider = Provider.of<ProfessionalFormProvider>(context, listen: false);
|
||||
final servicesProvider = Provider.of<ServicesProvider>(context, listen: false);
|
||||
|
||||
|
||||
|
||||
final proProvider = Provider.of<ProfessionalProvider>(context, listen: false);
|
||||
|
||||
final professional = await proProvider.getProfessional(authProvider.user!.id);
|
||||
professionalFormProvider.setProfesional(professional);
|
||||
|
||||
final services = await servicesProvider.getServicesForProfessional(professional.id);
|
||||
setState(() {
|
||||
_services = services;
|
||||
user = authProvider.user;
|
||||
});
|
||||
}
|
||||
|
||||
void _onDaySelected(DateTime day, DateTime focusedDay) {
|
||||
setState(() {
|
||||
today = day;
|
||||
numDay = today.weekday;
|
||||
});
|
||||
_fetchProfessionalAndServices();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<ProfessionalFormProvider>(
|
||||
builder: (context, professionalFormProvider, child) {
|
||||
if (professionalFormProvider.profesional == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
final profesional = professionalFormProvider.profesional!;
|
||||
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: WhiteCard(
|
||||
title: 'Calendario',
|
||||
child: Column(
|
||||
children: [
|
||||
TableCalendar(
|
||||
locale: 'es_CO',
|
||||
firstDay: DateTime.now(),
|
||||
lastDay: DateTime.utc(2030, 3, 14),
|
||||
focusedDay: today,
|
||||
availableGestures: AvailableGestures.all,
|
||||
onDaySelected: _onDaySelected,
|
||||
selectedDayPredicate: (day) => isSameDay(day, today),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
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),
|
||||
Column(
|
||||
children: [
|
||||
..._rangesItems(_getScheduleFromNumDay(numDay, profesional),context),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ScheduleEntity? _getScheduleFromNumDay(int numDay, Profesional userProfessional) {
|
||||
switch (numDay) {
|
||||
case 1:
|
||||
return userProfessional.schedules.monday;
|
||||
case 2:
|
||||
return userProfessional.schedules.tuesday;
|
||||
case 3:
|
||||
return userProfessional.schedules.wednesday;
|
||||
case 4:
|
||||
return userProfessional.schedules.thursday;
|
||||
case 5:
|
||||
return userProfessional.schedules.friday;
|
||||
case 6:
|
||||
return userProfessional.schedules.saturday;
|
||||
case 7:
|
||||
return userProfessional.schedules.sunday;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _rangesItems(ScheduleEntity? schedule, BuildContext context) {
|
||||
if (schedule == null ||
|
||||
!schedule.enabled ||
|
||||
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<Service>? events, DateTime selectedDay) {
|
||||
if (events != null) {
|
||||
for (Service event in events) {
|
||||
if (selectedDay.toIso8601String().split('T').first == event.day) {
|
||||
if (hora1 == event.range1Hour1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Widget> rangesItemList(List<TimeOfDay> ranges, List<Service>? 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 (Service event in events) {
|
||||
if (selectedDay.toIso8601String().split('T').first ==
|
||||
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 acción 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);
|
||||
},
|
||||
child: const Text('Sí, 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:prosapp_web_app/models/location_preferences.dart';
|
||||
import 'package:prosapp_web_app/models/schedules_entity.dart';
|
||||
import 'package:prosapp_web_app/models/usuario.dart';
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/professional_form_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/professional_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/profile_form_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/settings_provider.dart';
|
||||
import 'package:prosapp_web_app/router/router.dart';
|
||||
import 'package:prosapp_web_app/services/navigation_service.dart';
|
||||
import 'package:prosapp_web_app/services/notifications_service.dart';
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
|
||||
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ProfessionalProfileView extends StatefulWidget {
|
||||
const ProfessionalProfileView({super.key});
|
||||
|
||||
@override
|
||||
State<ProfessionalProfileView> createState() =>
|
||||
_ProfessionalProfileViewState();
|
||||
}
|
||||
|
||||
class _ProfessionalProfileViewState extends State<ProfessionalProfileView> {
|
||||
Usuario? user;
|
||||
late ProfessionalFormProvider professionalFormProvider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
final profileFormProvider =
|
||||
Provider.of<ProfileFormProvider>(context, listen: false);
|
||||
professionalFormProvider =
|
||||
Provider.of<ProfessionalFormProvider>(context, listen: false);
|
||||
|
||||
final proProvider =
|
||||
Provider.of<ProfessionalProvider>(context, listen: false);
|
||||
|
||||
proProvider.getProfessional(authProvider.user!.id).then((value) {
|
||||
professionalFormProvider.setProfesional(value);
|
||||
});
|
||||
|
||||
profileFormProvider.user = authProvider.user;
|
||||
setState(() {
|
||||
user = authProvider.user;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 700) {
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: const [SizedBox(height: 10), _ProfileViewBody()],
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
child: ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: const [SizedBox(height: 10), _ProfileViewBody()],
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _ProfileViewBody extends StatelessWidget {
|
||||
const _ProfileViewBody();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: const Column(
|
||||
children: [
|
||||
_AvatarContainer(containerFull: true),
|
||||
_ProfileViewForm(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProfileViewForm extends StatefulWidget {
|
||||
const _ProfileViewForm();
|
||||
|
||||
@override
|
||||
State<_ProfileViewForm> createState() => _ProfileViewFormState();
|
||||
}
|
||||
|
||||
class _ProfileViewFormState extends State<_ProfileViewForm> {
|
||||
bool _serviceDeliverySwitchValue = false;
|
||||
bool _serviceSiteSwitchValue = false;
|
||||
bool _serviceRateSwitchValue = false;
|
||||
|
||||
bool first = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profileFormProvider = Provider.of<ProfileFormProvider>(context);
|
||||
final settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
final user = profileFormProvider.user!;
|
||||
|
||||
return Consumer<ProfessionalFormProvider>(
|
||||
builder: (context, professionalFormProvider, child) {
|
||||
if (professionalFormProvider.profesional == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (settingsProvider.settings == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
final settings = settingsProvider.settings!;
|
||||
final profesional = professionalFormProvider.profesional!;
|
||||
if (first) {
|
||||
first = false;
|
||||
|
||||
if (settings.tarifas) {
|
||||
_serviceRateSwitchValue = profesional.ratePreferences;
|
||||
} else {
|
||||
_serviceRateSwitchValue = false;
|
||||
}
|
||||
|
||||
if (profesional.locationPreferences == LocationPreferences.both) {
|
||||
_serviceSiteSwitchValue = true;
|
||||
_serviceDeliverySwitchValue = true;
|
||||
} else if (profesional.locationPreferences ==
|
||||
LocationPreferences.office) {
|
||||
_serviceSiteSwitchValue = true;
|
||||
_serviceDeliverySwitchValue = false;
|
||||
} else if (profesional.locationPreferences ==
|
||||
LocationPreferences.delivery) {
|
||||
_serviceSiteSwitchValue = false;
|
||||
_serviceDeliverySwitchValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
double columnWidth = constraints.maxWidth < 600 ? 95 : 150;
|
||||
|
||||
return WhiteCard(
|
||||
title: 'Información profesional',
|
||||
child: Form(
|
||||
key: professionalFormProvider.profileFormKey,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
SwitchListTile(
|
||||
title: const Text('Servicio a domicilio'),
|
||||
value: _serviceDeliverySwitchValue,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_serviceDeliverySwitchValue = value;
|
||||
|
||||
if (settings.domicilios == true) {
|
||||
_serviceDeliverySwitchValue = value;
|
||||
if (!_serviceDeliverySwitchValue) {
|
||||
_serviceSiteSwitchValue = true;
|
||||
}
|
||||
} else {
|
||||
_serviceDeliverySwitchValue = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SwitchListTile(
|
||||
title: const Text('Servicio en sitio / consultorio'),
|
||||
value: _serviceSiteSwitchValue,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (settings.domicilios == true) {
|
||||
_serviceSiteSwitchValue = value;
|
||||
if (!_serviceSiteSwitchValue) {
|
||||
_serviceDeliverySwitchValue = true;
|
||||
}
|
||||
} else {
|
||||
_serviceSiteSwitchValue = true;
|
||||
_serviceDeliverySwitchValue = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_serviceSiteSwitchValue)
|
||||
Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
initialValue: profesional.address,
|
||||
// validator: (value) {
|
||||
// if (value == null || value.isEmpty) {
|
||||
// return 'La dirección es obligatoria';
|
||||
// }
|
||||
// return null;
|
||||
// },
|
||||
onChanged: (value) {},
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Ingresa tu dirección',
|
||||
label: 'Dirección',
|
||||
icon: Icons.location_on_outlined,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
initialValue: profesional.aditionalAddress,
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Ingresa tu piso / apartamento / conjunto',
|
||||
label: 'Piso / Apartamento / Conjunto',
|
||||
icon: Icons.email_outlined,
|
||||
),
|
||||
onChanged: (value) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
aditionalAddress: value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (settings.tarifas == true) ...[
|
||||
const SizedBox(height: 20),
|
||||
SwitchListTile(
|
||||
title: const Text('Tarifa'),
|
||||
value: _serviceRateSwitchValue,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_serviceRateSwitchValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_serviceRateSwitchValue)
|
||||
Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.number,
|
||||
initialValue: profesional.rate,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'La tarifa es obligatoria';
|
||||
}
|
||||
final numericRegex = RegExp(r'^[0-9]+$');
|
||||
if (!numericRegex.hasMatch(value)) {
|
||||
return 'Por favor, ingresa solo números';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
rate: value);
|
||||
},
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Ingresa la tarifa del servicio',
|
||||
label: 'Tarifa',
|
||||
icon: Icons.attach_money,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
Text('Metodos de pago', style: CustomLabels.h3),
|
||||
CheckboxListTile(
|
||||
title: const Text('Datafono'),
|
||||
value: profesional.paymentMethods.datafono,
|
||||
onChanged: (value) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
paymentMethods: profesional.paymentMethods
|
||||
.copyWith(datafono: value));
|
||||
}),
|
||||
const SizedBox(height: 10),
|
||||
CheckboxListTile(
|
||||
title: const Text('Nequi'),
|
||||
value: profesional.paymentMethods.nequi,
|
||||
onChanged: (value) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
paymentMethods: profesional.paymentMethods
|
||||
.copyWith(nequi: value));
|
||||
}),
|
||||
const SizedBox(height: 10),
|
||||
CheckboxListTile(
|
||||
title: const Text('Transferencia bancaria'),
|
||||
value: profesional.paymentMethods.transferencia,
|
||||
onChanged: (value) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
paymentMethods: profesional.paymentMethods
|
||||
.copyWith(transferencia: value));
|
||||
}),
|
||||
const SizedBox(height: 20),
|
||||
Text('Horarios de atenciòn', style: CustomLabels.h3),
|
||||
const SizedBox(height: 10),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
NavigationService.replaceTo(
|
||||
Flurorouter.professionalScheduleRoute);
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Table(
|
||||
columnWidths: {
|
||||
0: FixedColumnWidth(columnWidth),
|
||||
},
|
||||
border:
|
||||
TableBorder.all(color: Colors.grey.withOpacity(0.3)),
|
||||
children: [
|
||||
_buildTableRow(
|
||||
'Lunes', profesional.schedules.monday, context),
|
||||
_buildTableRow(
|
||||
'Martes', profesional.schedules.tuesday, context),
|
||||
_buildTableRow('Miercoles',
|
||||
profesional.schedules.wednesday, context),
|
||||
_buildTableRow(
|
||||
'Jueves', profesional.schedules.thursday, context),
|
||||
_buildTableRow(
|
||||
'Viernes', profesional.schedules.friday, context),
|
||||
_buildTableRow(
|
||||
'Sabado', profesional.schedules.saturday, context),
|
||||
_buildTableRow(
|
||||
'Domingo', profesional.schedules.sunday, context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 130),
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
LocationPreferences locationPreferences;
|
||||
|
||||
if (_serviceDeliverySwitchValue &&
|
||||
_serviceSiteSwitchValue) {
|
||||
locationPreferences = LocationPreferences.both;
|
||||
} else if (_serviceDeliverySwitchValue) {
|
||||
locationPreferences = LocationPreferences.delivery;
|
||||
} else if (_serviceSiteSwitchValue) {
|
||||
locationPreferences = LocationPreferences.office;
|
||||
} else {
|
||||
locationPreferences = LocationPreferences.office;
|
||||
}
|
||||
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
locationPreferences: locationPreferences,
|
||||
ratePreferences: _serviceRateSwitchValue,
|
||||
);
|
||||
|
||||
await professionalFormProvider
|
||||
.updateProfesionalProfileInfo(user.id);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.blue.shade400,
|
||||
|
||||
),
|
||||
shape:
|
||||
WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text(
|
||||
'Guardar',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
TableRow _buildTableRow(
|
||||
String day, ScheduleEntity schedules, BuildContext context) {
|
||||
return TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.blueAccent,
|
||||
),
|
||||
child: Text(
|
||||
day,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TableCell(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
timeList(schedules, context),
|
||||
style: const TextStyle(
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String timeList(ScheduleEntity? schedule, BuildContext context) {
|
||||
if (schedule == null) {
|
||||
return 'No hay horarios';
|
||||
}
|
||||
if (!schedule.enabled) {
|
||||
return 'No hay horarios';
|
||||
}
|
||||
if (schedule.range1Hour1 == null || schedule.range2Hour2 == null) {
|
||||
return 'No hay horarios';
|
||||
}
|
||||
if (schedule.continuousDay) {
|
||||
return '${ScheduleEntity.getFormatTime(schedule.range1Hour1)} a ${ScheduleEntity.getFormatTime(schedule.range2Hour2)}';
|
||||
} else {
|
||||
if (schedule.range1Hour2 == null || schedule.range2Hour1 == null) {
|
||||
return 'No hay horarios';
|
||||
}
|
||||
|
||||
return '${ScheduleEntity.getFormatTime(schedule.range1Hour1)} a ${ScheduleEntity.getFormatTime(schedule.range1Hour2)} - ${ScheduleEntity.getFormatTime(schedule.range2Hour1)} a ${ScheduleEntity.getFormatTime(schedule.range2Hour2)}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _AvatarContainer extends StatelessWidget {
|
||||
final bool containerFull;
|
||||
|
||||
const _AvatarContainer({required this.containerFull});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = Provider.of<AuthProvider>(context).user!;
|
||||
final profileFormProvider = Provider.of<ProfileFormProvider>(context);
|
||||
|
||||
final image = (profileFormProvider.user!.picture == '' ||
|
||||
profileFormProvider.user!.picture == null)
|
||||
? const Image(image: AssetImage('no-image.jpg'))
|
||||
: FadeInImage.assetNetwork(
|
||||
placeholder: 'loader.gif',
|
||||
fit: BoxFit.cover,
|
||||
image: profileFormProvider.user!.picture!,
|
||||
);
|
||||
|
||||
return WhiteCard(
|
||||
width: containerFull ? null : 250,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(user.name, style: CustomLabels.h2),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
height: 160,
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
height: 200,
|
||||
child: ClipOval(child: image),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 5,
|
||||
right: 5,
|
||||
child: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
border: Border.all(color: Colors.white, width: 5),
|
||||
),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () async {
|
||||
FilePickerResult? result =
|
||||
await FilePicker.platform.pickFiles(
|
||||
withData: true,
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
PlatformFile file = result.files.first;
|
||||
Uint8List? fileBytes = file.bytes;
|
||||
|
||||
if (fileBytes != null) {
|
||||
NotificationsService.showBusyIndicator(context);
|
||||
await profileFormProvider
|
||||
.uploadPicture(fileBytes);
|
||||
Provider.of<AuthProvider>(context, listen: false)
|
||||
.refreshUser();
|
||||
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} else {}
|
||||
},
|
||||
backgroundColor: Colors.indigo,
|
||||
elevation: 0,
|
||||
child: const Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
size: 20,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:prosapp_web_app/models/usuario_profesional.dart';
|
||||
import 'package:prosapp_web_app/services/navigation_service.dart';
|
||||
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:prosapp_web_app/providers/professionals_provider.dart';
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
|
||||
class ProfessionalsView extends StatelessWidget {
|
||||
const ProfessionalsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final professionalsProvider = Provider.of<ProfessionalsProvider>(context);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
int crossAxisCount;
|
||||
double childAspectRatio;
|
||||
|
||||
if (constraints.maxWidth >= 1540) {
|
||||
crossAxisCount = 5;
|
||||
childAspectRatio = 0.75;
|
||||
} else if (constraints.maxWidth >= 1000) {
|
||||
crossAxisCount = 4;
|
||||
childAspectRatio = 0.65;
|
||||
} else if (constraints.maxWidth >= 650) {
|
||||
crossAxisCount = 3;
|
||||
childAspectRatio = 0.55;
|
||||
} else if (constraints.maxWidth >= 400) {
|
||||
crossAxisCount = 2;
|
||||
childAspectRatio = 0.55;
|
||||
} else {
|
||||
crossAxisCount = 1;
|
||||
childAspectRatio = 0.7;
|
||||
}
|
||||
|
||||
if (professionalsProvider.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
childAspectRatio: childAspectRatio,
|
||||
),
|
||||
itemCount: professionalsProvider.professionals.length,
|
||||
itemBuilder: (context, index) {
|
||||
UsuarioProfesional data = professionalsProvider.professionals[index];
|
||||
|
||||
final image = (data.user.picture == '' || data.user.picture == null)
|
||||
? const Image(image: AssetImage('no-image.jpg'))
|
||||
: FadeInImage.assetNetwork(
|
||||
placeholder: 'loader.gif',
|
||||
fit: BoxFit.cover,
|
||||
image: data.user.picture!,
|
||||
);
|
||||
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final List<dynamic> result = await NavigationService.navigateToFuture('/dashboard/calendar/${data.user.id}');
|
||||
|
||||
DateTime day = result[0];
|
||||
TimeOfDay time = result[1];
|
||||
|
||||
Navigator.pop(context, [data, day, time]);
|
||||
},
|
||||
child: WhiteCard(
|
||||
title: data.user.name.toUpperCase(),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.star,
|
||||
size: 15,
|
||||
color: Colors.yellow,
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
data.averageScore.toString(),
|
||||
style: CustomLabels.h4,
|
||||
),
|
||||
],
|
||||
),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: ClipOval(
|
||||
child: image,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.location_on_outlined,
|
||||
size: 15),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
data.user.city ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline, size: 15),
|
||||
const SizedBox(width: 5),
|
||||
Text(data.professionalInfo.profession),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
if (data.professionalInfo.paymentMethods.datafono ||
|
||||
data.professionalInfo.paymentMethods.nequi ||
|
||||
data.professionalInfo.paymentMethods
|
||||
.transferencia) ...[
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.payment_outlined, size: 15),
|
||||
SizedBox(width: 5),
|
||||
Text('Metodos de pago'),
|
||||
],
|
||||
),
|
||||
Visibility(
|
||||
visible: data
|
||||
.professionalInfo.paymentMethods.datafono,
|
||||
child: const Text('- Datafono'),
|
||||
),
|
||||
Visibility(
|
||||
visible:
|
||||
data.professionalInfo.paymentMethods.nequi,
|
||||
child: const Text('- Nequi'),
|
||||
),
|
||||
Visibility(
|
||||
visible: data.professionalInfo.paymentMethods
|
||||
.transferencia,
|
||||
child: const Text('- Transferencia'),
|
||||
),
|
||||
] else
|
||||
const Text(
|
||||
'No hay metodos de pago registrados',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black45,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:prosapp_web_app/models/city.dart';
|
||||
import 'package:prosapp_web_app/models/usuario.dart';
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/cities_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/profile_form_provider.dart';
|
||||
import 'package:prosapp_web_app/router/router.dart';
|
||||
import 'package:prosapp_web_app/services/navigation_service.dart';
|
||||
import 'package:prosapp_web_app/services/notifications_service.dart';
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
|
||||
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ProfileView extends StatefulWidget {
|
||||
const ProfileView({super.key});
|
||||
|
||||
@override
|
||||
State<ProfileView> createState() => _ProfileViewState();
|
||||
}
|
||||
|
||||
class _ProfileViewState extends State<ProfileView> {
|
||||
Usuario? user;
|
||||
List<City> cities = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
final profileFormProvider =
|
||||
Provider.of<ProfileFormProvider>(context, listen: false);
|
||||
final citiesProvider = Provider.of<CitiesProvider>(context, listen: false);
|
||||
|
||||
profileFormProvider.user = authProvider.user;
|
||||
setState(() {
|
||||
cities = citiesProvider.cities;
|
||||
user = authProvider.user;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 700) {
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: const [SizedBox(height: 10), _ProfileViewBody()],
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
child: ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: const [SizedBox(height: 10), _ProfileViewBody()],
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _ProfileViewBody extends StatelessWidget {
|
||||
const _ProfileViewBody();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 700) {
|
||||
return const Column(
|
||||
children: [
|
||||
_AvatarContainer(containerFull: true),
|
||||
_ProfileViewForm(),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: Table(
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(250),
|
||||
},
|
||||
children: const [
|
||||
TableRow(
|
||||
children: [
|
||||
_AvatarContainer(containerFull: true),
|
||||
_ProfileViewForm(),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _ProfileViewForm extends StatelessWidget {
|
||||
const _ProfileViewForm();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profileFormProvider = Provider.of<ProfileFormProvider>(context);
|
||||
final citiesProvider = Provider.of<CitiesProvider>(context);
|
||||
final cities = citiesProvider.cities;
|
||||
final user = profileFormProvider.user!;
|
||||
|
||||
return WhiteCard(
|
||||
title: 'Información general',
|
||||
child: Form(
|
||||
key: profileFormProvider.formKey,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
initialValue: user.name,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'El nombre es obligatorio';
|
||||
}
|
||||
if (value.trim().length < 4) {
|
||||
return 'El nombre debe tener al menos 4 caracteres';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
profileFormProvider.copyUserWith(name: value);
|
||||
},
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Nombre de usuario',
|
||||
label: 'Nombre',
|
||||
icon: Icons.person_outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
readOnly: true,
|
||||
onTap: user.phone == null || user.phone!.isEmpty
|
||||
? () {
|
||||
NavigationService.navigateTo(Flurorouter.phoneRoute);
|
||||
}
|
||||
: null,
|
||||
initialValue: user.phone ?? '',
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Número de teléfono',
|
||||
label: 'Teléfono',
|
||||
icon: Icons.phone,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
readOnly: true,
|
||||
onTap: user.email == null || user.email!.isEmpty
|
||||
? () {
|
||||
NavigationService.navigateTo(Flurorouter.emailRoute);
|
||||
}
|
||||
: null,
|
||||
initialValue: user.email ?? '',
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Correo del usuario',
|
||||
label: 'Correo',
|
||||
icon: Icons.email_outlined,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
DropdownButtonFormField(
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return 'La ciudad es obligatoria';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
value: user.city == '' ? null : user.city,
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Selecciona tu ciudad',
|
||||
label: 'Ciudad',
|
||||
icon: Icons.location_city_outlined,
|
||||
),
|
||||
items: cities.map((City ciudad) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: ciudad.cityName,
|
||||
child: Text('${ciudad.cityName} - ${ciudad.stateOfCity}',
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.normal,
|
||||
)),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
profileFormProvider.copyUserWith(city: value!);
|
||||
}),
|
||||
const SizedBox(height: 20),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 130),
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await profileFormProvider.updateUserInfo();
|
||||
|
||||
Provider.of<AuthProvider>(context, listen: false)
|
||||
.refreshUser();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.blue.shade400,
|
||||
|
||||
),
|
||||
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text('Guardar',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AvatarContainer extends StatelessWidget {
|
||||
final bool containerFull;
|
||||
|
||||
const _AvatarContainer({required this.containerFull});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = Provider.of<AuthProvider>(context).user!;
|
||||
final profileFormProvider = Provider.of<ProfileFormProvider>(context);
|
||||
|
||||
final image = (profileFormProvider.user!.picture == '' ||
|
||||
profileFormProvider.user!.picture == null)
|
||||
? const Image(image: AssetImage('no-image.jpg'))
|
||||
: FadeInImage.assetNetwork(
|
||||
placeholder: 'loader.gif',
|
||||
fit: BoxFit.cover,
|
||||
image: profileFormProvider.user!.picture!,
|
||||
);
|
||||
|
||||
return WhiteCard(
|
||||
width: containerFull ? null : 250,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
user.name,
|
||||
style: CustomLabels.h2,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
height: 160,
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
height: 200,
|
||||
child: ClipOval(child: image),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 5,
|
||||
right: 5,
|
||||
child: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
border: Border.all(color: Colors.white, width: 5),
|
||||
),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () async {
|
||||
FilePickerResult? result =
|
||||
await FilePicker.platform.pickFiles(
|
||||
withData: true,
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
PlatformFile file = result.files.first;
|
||||
Uint8List? fileBytes = file.bytes;
|
||||
|
||||
if (fileBytes != null) {
|
||||
NotificationsService.showBusyIndicator(context);
|
||||
await profileFormProvider
|
||||
.uploadPicture(fileBytes);
|
||||
Provider.of<AuthProvider>(context, listen: false)
|
||||
.refreshUser();
|
||||
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} else {}
|
||||
},
|
||||
backgroundColor: Colors.indigo,
|
||||
elevation: 0,
|
||||
child: const Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
size: 20,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:prosapp_web_app/models/comment_entity.dart';
|
||||
import 'package:prosapp_web_app/providers/score_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/services_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/sidemenu_provider.dart';
|
||||
import 'package:prosapp_web_app/router/router.dart';
|
||||
import 'package:prosapp_web_app/services/navigation_service.dart';
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
||||
|
||||
class RatingView extends StatefulWidget {
|
||||
final String type;
|
||||
final String serviceId;
|
||||
final String professionalId;
|
||||
|
||||
const RatingView({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.serviceId,
|
||||
required this.professionalId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RatingView> createState() => _RatingViewState();
|
||||
}
|
||||
|
||||
class _RatingViewState extends State<RatingView> {
|
||||
double _rating = 1.0;
|
||||
TextEditingController commentController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final servicesProvider =
|
||||
Provider.of<ServicesProvider>(context, listen: false);
|
||||
final scoreProvider = Provider.of<ScoreProvider>(context);
|
||||
|
||||
if (widget.type == 'user') {
|
||||
servicesProvider.getServiceForUser(widget.serviceId);
|
||||
}
|
||||
|
||||
if (widget.type == 'professional') {
|
||||
servicesProvider.getServiceForProfessional(widget.serviceId);
|
||||
}
|
||||
|
||||
return Consumer<ServicesProvider>(
|
||||
builder: (context, servicesProvider, child) {
|
||||
if (servicesProvider.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final service = servicesProvider.service!.service;
|
||||
final user = servicesProvider.service!.user;
|
||||
|
||||
if (widget.type == 'user' && service.professionalScored == true) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
navigateTo(Flurorouter.dashboardRoute);
|
||||
});
|
||||
}
|
||||
|
||||
if (widget.type == 'professional' && service.userScored == true) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
navigateTo(Flurorouter.dashboardRoute);
|
||||
});
|
||||
}
|
||||
|
||||
final image = (user.picture == '' || user.picture == null)
|
||||
? const Image(image: AssetImage('no-image.jpg'))
|
||||
: FadeInImage.assetNetwork(
|
||||
placeholder: 'loader.gif',
|
||||
fit: BoxFit.cover,
|
||||
image: user.picture!,
|
||||
);
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: WhiteCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: ClipOval(child: image),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
user.name.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Califica el servicio',
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
RatingBar.builder(
|
||||
initialRating: _rating,
|
||||
minRating: 1,
|
||||
direction: Axis.horizontal,
|
||||
allowHalfRating: true,
|
||||
itemCount: 5,
|
||||
itemSize: 35,
|
||||
glow: false,
|
||||
maxRating: 5,
|
||||
itemPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 5),
|
||||
itemBuilder: (context, _) => const Icon(
|
||||
Icons.star,
|
||||
color: Color(0xFF2BA4EC),
|
||||
),
|
||||
onRatingUpdate: (rating) {
|
||||
setState(() {
|
||||
_rating = rating;
|
||||
});
|
||||
},
|
||||
ignoreGestures: false,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
customMessage(_rating),
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
color: Color(0xFF2BA4EC),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 80, vertical: 40),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Comentario',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
maxLines: null,
|
||||
maxLength: 500,
|
||||
keyboardType: TextInputType.multiline,
|
||||
controller: commentController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 180),
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (widget.type == 'user') {
|
||||
final newComment = CommentEntity(
|
||||
content: commentController.text,
|
||||
score: _rating,
|
||||
isFromUser: true,
|
||||
serviceId: widget.serviceId,
|
||||
createdAt: Timestamp.now(),
|
||||
authorId: service.userId,
|
||||
destinationId: service.professionalId,
|
||||
);
|
||||
|
||||
await scoreProvider.addComment(
|
||||
newComment,
|
||||
);
|
||||
|
||||
servicesProvider.changeProfessionalScored(
|
||||
widget.serviceId);
|
||||
}
|
||||
|
||||
if (widget.type == 'professional') {
|
||||
final newComment = CommentEntity(
|
||||
content: commentController.text,
|
||||
score: _rating,
|
||||
isFromUser: false,
|
||||
serviceId: widget.serviceId,
|
||||
createdAt: Timestamp.now(),
|
||||
authorId: service.professionalId,
|
||||
destinationId: service.userId,
|
||||
);
|
||||
|
||||
await scoreProvider.addComment(
|
||||
newComment,
|
||||
);
|
||||
|
||||
servicesProvider
|
||||
.changeUserScored(widget.serviceId);
|
||||
}
|
||||
|
||||
navigateTo(Flurorouter.dashboardRoute);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.blue.shade400,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(
|
||||
Colors.transparent),
|
||||
),
|
||||
child: const Text(
|
||||
'Enviar calificación',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void navigateTo(String routeName) {
|
||||
NavigationService.replaceTo(routeName);
|
||||
SideMenuProvider.closeMenu();
|
||||
}
|
||||
|
||||
String customMessage(double rating) {
|
||||
if (rating > 4.0) {
|
||||
return '¡Excelente! 👏🌟';
|
||||
}
|
||||
|
||||
if (rating < 2.0) {
|
||||
return '¡Malo! 😔❌';
|
||||
}
|
||||
|
||||
if (rating <= 4.0 && rating >= 3.0) {
|
||||
return '¡Bueno! 👍😊';
|
||||
}
|
||||
|
||||
if (rating < 3.0 && rating >= 2.0) {
|
||||
return '¡Regular! 😐🔄';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/register_form_provider.dart';
|
||||
import 'package:prosapp_web_app/router/router.dart';
|
||||
import 'package:prosapp_web_app/ui/buttons/custom_outlined_button.dart';
|
||||
import 'package:prosapp_web_app/ui/buttons/link_text.dart';
|
||||
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
|
||||
import 'package:email_validator/email_validator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RegisterView extends StatelessWidget {
|
||||
const RegisterView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authProvider = Provider.of<AuthProvider>(context);
|
||||
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => RegisterFormProvider(),
|
||||
child: Builder(builder: (context) {
|
||||
final registerFormProvider =
|
||||
Provider.of<RegisterFormProvider>(context, listen: false);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 40),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 370),
|
||||
child: Form(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
key: registerFormProvider.formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
// Name
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (value == null ||
|
||||
value.isEmpty ||
|
||||
value.trim().isEmpty) {
|
||||
return 'El nombre es obligatorio';
|
||||
}
|
||||
|
||||
if (value.trim().length < 3) {
|
||||
return 'El nombre debe tener al menos 3 caracteres';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) => registerFormProvider.name = value,
|
||||
decoration: CustomInputs.loginInputDecoration(
|
||||
hint: 'Ingrese su nombre',
|
||||
label: 'Nombre',
|
||||
icon: Icons.person_outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Email
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (!EmailValidator.validate(value ?? '')) {
|
||||
return 'Email no válido';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) => registerFormProvider.email = value,
|
||||
decoration: CustomInputs.loginInputDecoration(
|
||||
hint: 'Ingresa tu correo',
|
||||
label: 'Email',
|
||||
icon: Icons.email_outlined,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Password
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (value == null || value.length < 8) {
|
||||
return "La contraseña debe tener al menos 8 caracteres";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) =>
|
||||
registerFormProvider.password = value,
|
||||
obscureText: true,
|
||||
decoration: CustomInputs.loginInputDecoration(
|
||||
hint: 'Ingresa tu contraseña',
|
||||
label: 'Contraseña',
|
||||
icon: Icons.lock_outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final validForm = registerFormProvider.validateForm();
|
||||
|
||||
if (!validForm) return;
|
||||
|
||||
await authProvider.register(
|
||||
registerFormProvider.email,
|
||||
registerFormProvider.password,
|
||||
registerFormProvider.name,
|
||||
);
|
||||
},
|
||||
text: "Crear cuenta",
|
||||
color: Colors.blue,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
LinkText(
|
||||
text: "Iniciar sesión",
|
||||
onPressed: () {
|
||||
Navigator.pushReplacementNamed(
|
||||
context, Flurorouter.loginRoute);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
LinkText(
|
||||
text: "Entrar con celular",
|
||||
onPressed: () {
|
||||
Navigator.pushReplacementNamed(
|
||||
context, Flurorouter.phoneLoginRoute);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:prosapp_web_app/models/pro_state.dart';
|
||||
import 'package:prosapp_web_app/models/profession.dart';
|
||||
import 'package:prosapp_web_app/models/usuario.dart';
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/professional_form_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/professional_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/professions_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/profile_form_provider.dart';
|
||||
import 'package:prosapp_web_app/services/notifications_service.dart';
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:prosapp_web_app/ui/views/no_page_found_view.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RequestProfessionalView extends StatefulWidget {
|
||||
const RequestProfessionalView({super.key});
|
||||
|
||||
@override
|
||||
State<RequestProfessionalView> createState() =>
|
||||
_RequestProfessionalViewState();
|
||||
}
|
||||
|
||||
class _RequestProfessionalViewState extends State<RequestProfessionalView> {
|
||||
Usuario? user;
|
||||
List<Profession> professions = [];
|
||||
late ProfessionalFormProvider professionalFormProvider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
final profileFormProvider =
|
||||
Provider.of<ProfileFormProvider>(context, listen: false);
|
||||
professionalFormProvider =
|
||||
Provider.of<ProfessionalFormProvider>(context, listen: false);
|
||||
final professionsProvider =
|
||||
Provider.of<ProfessionsProvider>(context, listen: false);
|
||||
final proProvider =
|
||||
Provider.of<ProfessionalProvider>(context, listen: false);
|
||||
|
||||
profileFormProvider.user = authProvider.user;
|
||||
|
||||
proProvider.getProfessional(authProvider.user!.id).then((value) {
|
||||
professionalFormProvider.setProfesional(value);
|
||||
});
|
||||
|
||||
setState(() {
|
||||
professions = professionsProvider.professions;
|
||||
user = authProvider.user;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 900) {
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: const [SizedBox(height: 10), _ProfileViewForm()],
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
child: ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: const [SizedBox(height: 10), _ProfileViewForm()],
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _ProfileViewForm extends StatefulWidget {
|
||||
const _ProfileViewForm();
|
||||
|
||||
@override
|
||||
State<_ProfileViewForm> createState() => _ProfileViewFormState();
|
||||
}
|
||||
|
||||
class _ProfileViewFormState extends State<_ProfileViewForm> {
|
||||
final TextEditingController _specialityController = TextEditingController();
|
||||
|
||||
List<String> specializations = [];
|
||||
|
||||
void _addItemToList() {
|
||||
setState(() {
|
||||
String newItem = _specialityController.text.trim();
|
||||
if (newItem.isNotEmpty) {
|
||||
specializations.add(newItem);
|
||||
_specialityController.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _removeItemFromList(String item) {
|
||||
setState(() {
|
||||
specializations.remove(item);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authProvider = Provider.of<AuthProvider>(context);
|
||||
final professionsProvider = Provider.of<ProfessionsProvider>(context);
|
||||
final professions = professionsProvider.professions;
|
||||
final user = authProvider.user!;
|
||||
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: Consumer<ProfessionalFormProvider>(
|
||||
builder: (context, professionalFormProvider, child) {
|
||||
if (professionalFormProvider.profesional == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
final professional = professionalFormProvider.profesional;
|
||||
|
||||
switch (enumToInt(user.proState)) {
|
||||
case 0:
|
||||
return WhiteCard(
|
||||
title: 'Información profesional',
|
||||
child: Form(
|
||||
key: professionalFormProvider.formKey,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
initialValue: professional!.identification,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'La cedula es obligatoria';
|
||||
}
|
||||
if (value.trim().length < 6) {
|
||||
return 'La cedula debe tener al menos 6 caracteres';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
identification: value);
|
||||
},
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Ingresa tu cedula',
|
||||
label: 'Cedula',
|
||||
icon: Icons.badge_outlined,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
try {
|
||||
FilePickerResult? result =
|
||||
await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['pdf'],
|
||||
withData: true,
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
PlatformFile file = result.files.first;
|
||||
Uint8List? fileBytes = file.bytes;
|
||||
|
||||
if (fileBytes != null) {
|
||||
NotificationsService.showBusyIndicator(context);
|
||||
|
||||
final provider =
|
||||
Provider.of<ProfessionalFormProvider>(
|
||||
context,
|
||||
listen: false);
|
||||
await provider.uploadPdfIdentification(
|
||||
fileBytes, user.id);
|
||||
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('debugeando $e');
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.upload_file),
|
||||
label: const Text('Cargar pdf de la cedula'),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
DropdownButtonFormField(
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return 'La profesión es obligatoria';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
value: professional.profession == ''
|
||||
? null
|
||||
: professional.profession,
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Selecciona tu profesión',
|
||||
label: 'Profesión',
|
||||
icon: Icons.work_outline_outlined,
|
||||
),
|
||||
items: professions.map((Profession profession) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: profession.name,
|
||||
child: Text(
|
||||
profession.name,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
profession: value);
|
||||
}),
|
||||
const SizedBox(height: 10),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
try {
|
||||
FilePickerResult? result =
|
||||
await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['pdf'],
|
||||
withData: true,
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
PlatformFile file = result.files.first;
|
||||
Uint8List? fileBytes = file.bytes;
|
||||
|
||||
if (fileBytes != null) {
|
||||
NotificationsService.showBusyIndicator(context);
|
||||
|
||||
final provider =
|
||||
Provider.of<ProfessionalFormProvider>(
|
||||
context,
|
||||
listen: false);
|
||||
await provider.uploadPdfCertificate(
|
||||
fileBytes, user.id);
|
||||
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('debugeando $e');
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.upload_file),
|
||||
label: const Text('Cargar pdf del certificado'),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (RegExp(r'\s{2,}').hasMatch(value!)) {
|
||||
return 'La especialización no es valida';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onFieldSubmitted: (_) {
|
||||
_addItemToList();
|
||||
},
|
||||
controller: _specialityController,
|
||||
decoration: CustomInputs.formInputDecoration(
|
||||
hint: 'Ingresa tus especializaciones y agregalas (+)',
|
||||
label: 'Especializaciones',
|
||||
icon: Icons.assignment_outlined,
|
||||
iconButton: IconButton(
|
||||
onPressed: () {
|
||||
_addItemToList();
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
try {
|
||||
FilePickerResult? result =
|
||||
await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowMultiple: true,
|
||||
allowedExtensions: ['pdf'],
|
||||
withData: true,
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
List<Uint8List> filesBytes = result.files
|
||||
.where((file) => file.bytes != null)
|
||||
.map((file) => file.bytes!)
|
||||
.toList();
|
||||
|
||||
if (filesBytes.isNotEmpty) {
|
||||
NotificationsService.showBusyIndicator(context);
|
||||
|
||||
final provider =
|
||||
Provider.of<ProfessionalFormProvider>(
|
||||
context,
|
||||
listen: false);
|
||||
await provider.uploadPdfSpecializations(
|
||||
filesBytes, user.id);
|
||||
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('debugeando $e');
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.upload_file),
|
||||
label:
|
||||
const Text('Cargar pdfs de las especializaciones'),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 4.0,
|
||||
children: specializations
|
||||
.map((item) => Chip(
|
||||
label: Text(item),
|
||||
backgroundColor: Colors.blue.withOpacity(0.3),
|
||||
labelStyle:
|
||||
const TextStyle(color: Colors.blue),
|
||||
deleteIconColor: Colors.blue,
|
||||
onDeleted: () {
|
||||
_removeItemFromList(item);
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
side: const BorderSide(
|
||||
color: Colors.blue, width: 0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 180),
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
specializations: specializations);
|
||||
final res = await professionalFormProvider
|
||||
.updateProfesionalInfo(user.id);
|
||||
|
||||
if (res) {
|
||||
Provider.of<AuthProvider>(context,
|
||||
listen: false)
|
||||
.refreshUser();
|
||||
|
||||
final profileFormProvider =
|
||||
Provider.of<ProfileFormProvider>(context,
|
||||
listen: false);
|
||||
profileFormProvider.copyUserWith(
|
||||
proState: ProState.pending);
|
||||
profileFormProvider.updateUserInfoNoValid();
|
||||
}
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.blue.shade400,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(5)),
|
||||
),
|
||||
),
|
||||
shadowColor:
|
||||
WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text(
|
||||
'Enviar a revisión',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
case 1:
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double screenWidth = constraints.maxWidth;
|
||||
double baseFontSize = 18;
|
||||
double responsiveFontSize =
|
||||
screenWidth < 600 ? baseFontSize * 0.8 : baseFontSize;
|
||||
|
||||
return WhiteCard(
|
||||
child: Column(
|
||||
children: [
|
||||
const Center(
|
||||
child: Image(
|
||||
image: AssetImage('checklist.gif'),
|
||||
width: 320,
|
||||
),
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1020),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
'Gracias por proporcionar tu información. Actualmente, estamos revisando tus datos y una vez aprobados, podrás acceder al perfil profesional sin problemas. Te notificaremos tan pronto como tu cuenta esté lista.',
|
||||
style: TextStyle(fontSize: responsiveFontSize),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'¡Gracias por tu paciencia!',
|
||||
style: TextStyle(fontSize: responsiveFontSize),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
case 3:
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double screenWidth = constraints.maxWidth;
|
||||
double baseFontSize = 18;
|
||||
double responsiveFontSize =
|
||||
screenWidth < 600 ? baseFontSize * 0.8 : baseFontSize;
|
||||
|
||||
return WhiteCard(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 25),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.sentiment_dissatisfied_outlined,
|
||||
size: 100,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1020),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8, right: 8, bottom: 10),
|
||||
child: Text(
|
||||
'Lamentablemente, tu solicitud no ha sido aceptada en esta ocasión. Por favor, revisa tus datos y vuelve a intentarlo más tarde.',
|
||||
style: TextStyle(fontSize: responsiveFontSize),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'¡Gracias por tu paciencia!',
|
||||
style: TextStyle(fontSize: responsiveFontSize),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Center(child: Text('No se encontró la sección.'));
|
||||
// return const NoPageFoundView();
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
import 'package:prosapp_web_app/models/usuario.dart';
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/professional_form_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/professional_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/profile_form_provider.dart';
|
||||
import 'package:prosapp_web_app/router/router.dart';
|
||||
import 'package:prosapp_web_app/services/navigation_service.dart';
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:prosapp_web_app/ui/shared/widgets/schedule_day_tile.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ScheduleView extends StatefulWidget {
|
||||
const ScheduleView({super.key});
|
||||
|
||||
@override
|
||||
State<ScheduleView> createState() => _ScheduleViewState();
|
||||
}
|
||||
|
||||
class _ScheduleViewState extends State<ScheduleView> {
|
||||
Usuario? user;
|
||||
late ProfessionalFormProvider professionalFormProvider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
final profileFormProvider =
|
||||
Provider.of<ProfileFormProvider>(context, listen: false);
|
||||
professionalFormProvider =
|
||||
Provider.of<ProfessionalFormProvider>(context, listen: false);
|
||||
|
||||
final proProvider =
|
||||
Provider.of<ProfessionalProvider>(context, listen: false);
|
||||
|
||||
profileFormProvider.user = authProvider.user;
|
||||
|
||||
proProvider.getProfessional(authProvider.user!.id).then((value) {
|
||||
professionalFormProvider.setProfesional(value);
|
||||
});
|
||||
|
||||
profileFormProvider.user = authProvider.user;
|
||||
setState(() {
|
||||
user = authProvider.user;
|
||||
});
|
||||
}
|
||||
|
||||
bool isTimeRangeValid(TimeOfDay startTime, TimeOfDay endTime) {
|
||||
final start = DateTime(2024, 1, 1, startTime.hour, startTime.minute);
|
||||
final end = DateTime(2024, 1, 1, endTime.hour, endTime.minute);
|
||||
return start.isBefore(end);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profileFormProvider = Provider.of<ProfileFormProvider>(context);
|
||||
final user = profileFormProvider.user!;
|
||||
|
||||
return Consumer<ProfessionalFormProvider>(
|
||||
builder: (context, professionalFormProvider, child) {
|
||||
if (professionalFormProvider.profesional == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
final profesional = professionalFormProvider.profesional!;
|
||||
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: WhiteCard(
|
||||
title: 'Horario',
|
||||
child: Column(
|
||||
children: [
|
||||
ScheduleDayTile(
|
||||
day: 'Lunes',
|
||||
schedule: profesional.schedules.monday,
|
||||
onEnableChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
monday: profesional.schedules.monday.copyWith(
|
||||
enabled: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onContinuousDayChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
monday: profesional.schedules.monday.copyWith(
|
||||
continuousDay: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onRange1Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
monday: profesional.schedules.monday.copyWith(
|
||||
range1Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange1Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
monday: profesional.schedules.monday.copyWith(
|
||||
range1Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
monday: profesional.schedules.monday.copyWith(
|
||||
range2Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
monday: profesional.schedules.monday.copyWith(
|
||||
range2Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
height: 20,
|
||||
),
|
||||
ScheduleDayTile(
|
||||
day: 'Martes',
|
||||
schedule: profesional.schedules.tuesday,
|
||||
onEnableChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
tuesday: profesional.schedules.tuesday.copyWith(
|
||||
enabled: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onContinuousDayChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
tuesday: profesional.schedules.tuesday.copyWith(
|
||||
continuousDay: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onRange1Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
tuesday: profesional.schedules.tuesday.copyWith(
|
||||
range1Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange1Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
tuesday: profesional.schedules.tuesday.copyWith(
|
||||
range1Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
tuesday: profesional.schedules.tuesday.copyWith(
|
||||
range2Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
tuesday: profesional.schedules.tuesday.copyWith(
|
||||
range2Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
height: 20,
|
||||
),
|
||||
ScheduleDayTile(
|
||||
day: 'Miercoles',
|
||||
schedule: profesional.schedules.wednesday,
|
||||
onEnableChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
wednesday:
|
||||
profesional.schedules.wednesday.copyWith(
|
||||
enabled: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onContinuousDayChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
wednesday:
|
||||
profesional.schedules.wednesday.copyWith(
|
||||
continuousDay: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onRange1Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
wednesday: profesional.schedules.wednesday.copyWith(
|
||||
range1Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange1Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
wednesday: profesional.schedules.wednesday.copyWith(
|
||||
range1Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
wednesday: profesional.schedules.wednesday.copyWith(
|
||||
range2Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
wednesday: profesional.schedules.wednesday.copyWith(
|
||||
range2Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
height: 20,
|
||||
),
|
||||
ScheduleDayTile(
|
||||
day: 'Jueves',
|
||||
schedule: profesional.schedules.thursday,
|
||||
onEnableChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
thursday: profesional.schedules.thursday.copyWith(
|
||||
enabled: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onContinuousDayChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
thursday: profesional.schedules.thursday.copyWith(
|
||||
continuousDay: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onRange1Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
thursday: profesional.schedules.thursday.copyWith(
|
||||
range1Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange1Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
thursday: profesional.schedules.thursday.copyWith(
|
||||
range1Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
thursday: profesional.schedules.thursday.copyWith(
|
||||
range2Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
thursday: profesional.schedules.thursday.copyWith(
|
||||
range2Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
height: 20,
|
||||
),
|
||||
ScheduleDayTile(
|
||||
day: 'Viernes',
|
||||
schedule: profesional.schedules.friday,
|
||||
onEnableChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
friday: profesional.schedules.friday.copyWith(
|
||||
enabled: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onContinuousDayChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
friday: profesional.schedules.friday.copyWith(
|
||||
continuousDay: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onRange1Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
friday: profesional.schedules.friday.copyWith(
|
||||
range1Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange1Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
friday: profesional.schedules.friday.copyWith(
|
||||
range1Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
friday: profesional.schedules.friday.copyWith(
|
||||
range2Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
friday: profesional.schedules.friday.copyWith(
|
||||
range2Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
height: 20,
|
||||
),
|
||||
ScheduleDayTile(
|
||||
day: 'Sabado',
|
||||
schedule: profesional.schedules.saturday,
|
||||
onEnableChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
saturday: profesional.schedules.saturday.copyWith(
|
||||
enabled: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onContinuousDayChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
saturday: profesional.schedules.saturday.copyWith(
|
||||
continuousDay: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onRange1Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
saturday: profesional.schedules.saturday.copyWith(
|
||||
range1Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange1Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
saturday: profesional.schedules.saturday.copyWith(
|
||||
range1Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
saturday: profesional.schedules.saturday.copyWith(
|
||||
range2Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
saturday: profesional.schedules.saturday.copyWith(
|
||||
range2Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
height: 20,
|
||||
),
|
||||
ScheduleDayTile(
|
||||
day: 'Domingo',
|
||||
schedule: profesional.schedules.sunday,
|
||||
onEnableChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
sunday: profesional.schedules.sunday.copyWith(
|
||||
enabled: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onContinuousDayChanged: (value) {
|
||||
setState(() {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
sunday: profesional.schedules.sunday.copyWith(
|
||||
continuousDay: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onRange1Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
sunday: profesional.schedules.sunday.copyWith(
|
||||
range1Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange1Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
sunday: profesional.schedules.sunday.copyWith(
|
||||
range1Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour1Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
sunday: profesional.schedules.sunday.copyWith(
|
||||
range2Hour1: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onRange2Hour2Pick: (pickedTime) {
|
||||
professionalFormProvider.copyProfesionalWith(
|
||||
schedules: profesional.schedules.copyWith(
|
||||
sunday: profesional.schedules.sunday.copyWith(
|
||||
range2Hour2: pickedTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
height: 20,
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 130),
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
|
||||
|
||||
await professionalFormProvider.updateProfesionalProfileScheduleInfo(user.id);
|
||||
|
||||
NavigationService.replaceTo(Flurorouter.professionalProfileRoute);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.blue.shade400,
|
||||
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor:
|
||||
WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text('Guardar',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,967 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:prosapp_web_app/models/schedules_entity.dart';
|
||||
import 'package:prosapp_web_app/models/service.dart';
|
||||
import 'package:prosapp_web_app/models/service_location_preferences.dart';
|
||||
import 'package:prosapp_web_app/models/service_status.dart';
|
||||
import 'package:prosapp_web_app/models/usuario.dart';
|
||||
import 'package:prosapp_web_app/providers/services_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/settings_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/sidemenu_provider.dart';
|
||||
import 'package:prosapp_web_app/router/router.dart';
|
||||
import 'package:prosapp_web_app/services/navigation_service.dart';
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:prosapp_web_app/ui/views/no_page_found_view.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:prosapp_web_app/utils/local_notifications.dart';
|
||||
|
||||
class ServiceView extends StatelessWidget {
|
||||
final String type;
|
||||
final String serviceId;
|
||||
|
||||
const ServiceView({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.serviceId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
final servicesProvider =
|
||||
Provider.of<ServicesProvider>(context, listen: false);
|
||||
|
||||
if (type == 'user') {
|
||||
servicesProvider.getServiceForUser(serviceId);
|
||||
}
|
||||
|
||||
if (type == 'professional') {
|
||||
servicesProvider.getServiceForProfessional(serviceId);
|
||||
}
|
||||
|
||||
return Consumer<ServicesProvider>(
|
||||
builder: (context, servicesProvider, child) {
|
||||
if (servicesProvider.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (servicesProvider.service == null) {
|
||||
// return const NoPageFoundView();
|
||||
return Center(child: Text('No se encontró el servicio.'));
|
||||
}
|
||||
|
||||
final service = servicesProvider.service!.service;
|
||||
final user = servicesProvider.service!.user;
|
||||
|
||||
final image = (user.picture == '' || user.picture == null)
|
||||
? const Image(image: AssetImage('no-image.jpg'))
|
||||
: FadeInImage.assetNetwork(
|
||||
placeholder: 'loader.gif',
|
||||
fit: BoxFit.cover,
|
||||
image: user.picture!,
|
||||
);
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: WhiteCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: ClipOval(child: image),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
user.name.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Text(
|
||||
'${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(service.day))} - ${ScheduleEntity.getFormatTime(service.range1Hour1)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 15,
|
||||
vertical: 5,
|
||||
),
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 20,
|
||||
color: Colors.black54,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
_CustomServiceLocation(service.location),
|
||||
style: const TextStyle(
|
||||
fontSize: 16, color: Colors.black54),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (service.description.isNotEmpty) ...[
|
||||
const SizedBox(height: 12.0),
|
||||
Text(
|
||||
'"${service.description.trim()}"',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20.0),
|
||||
customActionButtons(service, user),
|
||||
const SizedBox(height: 40.0),
|
||||
customMessageStatus(service, user),
|
||||
const SizedBox(height: 20.0),
|
||||
customButton(
|
||||
service, context, user, servicesProvider),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void navigateTo(String routeName) {
|
||||
NavigationService.replaceTo(routeName);
|
||||
SideMenuProvider.closeMenu();
|
||||
}
|
||||
|
||||
Widget customActionButtons(Service service, Usuario user) {
|
||||
DateTime serviceDate = DateTime.parse(service.day);
|
||||
DateTime now = DateTime.now();
|
||||
DateTime serviceDateTime = DateTime(
|
||||
serviceDate.year,
|
||||
serviceDate.month,
|
||||
serviceDate.day,
|
||||
service.range1Hour2.hour,
|
||||
service.range1Hour2.minute,
|
||||
);
|
||||
|
||||
if (service.status == ServiceStatus.acepted ||
|
||||
service.status == ServiceStatus.active) {
|
||||
if (now.isAfter(serviceDateTime)) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 15,
|
||||
children: [
|
||||
Visibility(
|
||||
visible: user.phone == null || user.phone == '' ? false : true,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => launch("tel:${user.phone}"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF2BA4EC),
|
||||
backgroundColor: Colors.white,
|
||||
shape: const CircleBorder(
|
||||
side: BorderSide(
|
||||
color: Color(0xFF2BA4EC),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 20, horizontal: 0),
|
||||
child: Icon(
|
||||
Icons.phone_android,
|
||||
size: 30,
|
||||
color: Color(0xFF2BA4EC),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (type == 'user') {
|
||||
NavigationService.navigateTo(
|
||||
'/dashboard/user/service/${service.id}/chat/${user.id}');
|
||||
}
|
||||
|
||||
if (type == 'professional') {
|
||||
NavigationService.navigateTo(
|
||||
'/dashboard/professional/service/${service.id}/chat/${user.id}');
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF2BA4EC),
|
||||
backgroundColor: Colors.white,
|
||||
shape: const CircleBorder(
|
||||
side: BorderSide(
|
||||
color: Color(0xFF2BA4EC),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 20, horizontal: 0),
|
||||
child: Icon(
|
||||
Icons.message,
|
||||
size: 30,
|
||||
color: Color(0xFF2BA4EC),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
Widget customMessageStatus(Service service, Usuario user) {
|
||||
DateTime serviceDate = DateTime.parse(service.day);
|
||||
DateTime now = DateTime.now();
|
||||
DateTime serviceDateTime = DateTime(
|
||||
serviceDate.year,
|
||||
serviceDate.month,
|
||||
serviceDate.day,
|
||||
service.range1Hour2.hour,
|
||||
service.range1Hour2.minute,
|
||||
);
|
||||
|
||||
if (type == 'user') {
|
||||
if (service.status == ServiceStatus.completed &&
|
||||
service.professionalScored == false) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Card(
|
||||
color: Colors.green.shade50,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(32, 56, 32, 32),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Completado',
|
||||
style: TextStyle(fontSize: 32, color: Colors.green),
|
||||
),
|
||||
const Text(
|
||||
'Califica el servicio',
|
||||
style: TextStyle(fontSize: 20, color: Colors.green),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
NavigationService.navigateTo(
|
||||
'/dashboard/user/service/${service.id}/rating/${user.id}');
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
'Calificar',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: -40,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.green.shade50, width: 4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.star,
|
||||
color: Colors.green,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (service.status == ServiceStatus.completed &&
|
||||
service.professionalScored == true) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Card(
|
||||
color: Colors.green.shade50,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Completado',
|
||||
style: TextStyle(fontSize: 32, color: Colors.green),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: -40,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.green.shade50, width: 4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.star,
|
||||
color: Colors.green,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (service.status == ServiceStatus.cancelled) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Card(
|
||||
color: Colors.red.shade100,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
|
||||
child: Text(
|
||||
'Cancelado',
|
||||
style: TextStyle(fontSize: 32, color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: -40,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.red.shade100, width: 4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close_rounded,
|
||||
color: Colors.red, size: 48),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (service.status == ServiceStatus.denied) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Card(
|
||||
color: Colors.red.shade100,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
|
||||
child: Text(
|
||||
'Rechazado',
|
||||
style: TextStyle(fontSize: 32, color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: -40,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.red.shade100, width: 4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close_rounded,
|
||||
color: Colors.red, size: 48),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (now.isAfter(serviceDateTime)) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Card(
|
||||
color: Colors.red.shade100,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Caducado',
|
||||
style: TextStyle(fontSize: 32, color: Colors.red),
|
||||
),
|
||||
Text(
|
||||
'Tu servicio ha excedido \n el tiempo de espera',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 18, color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: -40,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.red.shade100, width: 4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close_rounded,
|
||||
color: Colors.red, size: 48),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (type == 'professional') {
|
||||
if (service.status == ServiceStatus.completed &&
|
||||
service.userScored == false) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Card(
|
||||
color: Colors.green.shade50,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(32, 56, 32, 32),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Completado',
|
||||
style: TextStyle(fontSize: 32, color: Colors.green),
|
||||
),
|
||||
const Text(
|
||||
'Califica el servicio',
|
||||
style: TextStyle(fontSize: 20, color: Colors.green),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
NavigationService.navigateTo(
|
||||
'/dashboard/professional/service/${service.id}/rating/${user.id}');
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
'Calificar',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: -40,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.green.shade50, width: 4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.star,
|
||||
color: Colors.green,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (service.status == ServiceStatus.completed &&
|
||||
service.userScored == true) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Card(
|
||||
color: Colors.green.shade50,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Completado',
|
||||
style: TextStyle(fontSize: 32, color: Colors.green),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: -40,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.green.shade50, width: 4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.star,
|
||||
color: Colors.green,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (service.status == ServiceStatus.cancelled) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Card(
|
||||
color: Colors.red.shade100,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
|
||||
child: Text(
|
||||
'Cancelado',
|
||||
style: TextStyle(fontSize: 32, color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: -40,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.red.shade100, width: 4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close_rounded,
|
||||
color: Colors.red, size: 48),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
if (service.status == ServiceStatus.denied) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Card(
|
||||
color: Colors.red.shade100,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
|
||||
child: Text(
|
||||
'Rechazado',
|
||||
style: TextStyle(fontSize: 32, color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: -40,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.red.shade100, width: 4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close_rounded,
|
||||
color: Colors.red, size: 48),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (now.isAfter(serviceDateTime)) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Card(
|
||||
color: Colors.red.shade100,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.fromLTRB(32, 56, 32, 32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Caducado',
|
||||
style: TextStyle(fontSize: 32, color: Colors.red),
|
||||
),
|
||||
Text(
|
||||
'Tu servicio ha excedido \n el tiempo de espera',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 18, color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: -40,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.red.shade100, width: 4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close_rounded,
|
||||
color: Colors.red, size: 48),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
Widget customButton(Service service, BuildContext context, Usuario userInfo,
|
||||
ServicesProvider servicesProvider) {
|
||||
DateTime serviceDate = DateTime.parse(service.day);
|
||||
DateTime now = DateTime.now();
|
||||
DateTime serviceDateTime = DateTime(
|
||||
serviceDate.year,
|
||||
serviceDate.month,
|
||||
serviceDate.day,
|
||||
service.range1Hour2.hour,
|
||||
service.range1Hour2.minute,
|
||||
);
|
||||
|
||||
if (now.isAfter(serviceDateTime)) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 130),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
navigateTo(Flurorouter.dashboardRoute);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.blue.shade400,
|
||||
),
|
||||
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text('Volver', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (type == 'professional') {
|
||||
if (service.status == ServiceStatus.pending) {
|
||||
return Column(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 160),
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await servicesProvider.changeServiceStatus(
|
||||
service.id!, ServiceStatus.denied);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.red.shade400,
|
||||
),
|
||||
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text('Rechazar servicio',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 160),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
servicesProvider.changeServiceStatus(
|
||||
service.id!, ServiceStatus.acepted);
|
||||
|
||||
navigateTo(Flurorouter.dashboardRoute);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.blue.shade400,
|
||||
),
|
||||
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text('Aceptar servicio',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (service.status == ServiceStatus.acepted) {
|
||||
if (serviceDateTime.difference(now).inDays > 1) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 130),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
servicesProvider.changeServiceStatus(
|
||||
service.id!, ServiceStatus.denied);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.red.shade400,
|
||||
),
|
||||
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text('Cancelar servicio',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 160),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
servicesProvider.changeServiceStatus(
|
||||
service.id!, ServiceStatus.active);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.blue.shade400,
|
||||
),
|
||||
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text('Iniciar servicio',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (service.status == ServiceStatus.active) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 160),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
servicesProvider.changeServiceStatus(
|
||||
service.id!, ServiceStatus.completed);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.blue.shade400,
|
||||
),
|
||||
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text('Terminar servicio',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (type == 'user') {
|
||||
if (service.status == ServiceStatus.pending) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 130),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.red.shade400,
|
||||
),
|
||||
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text('Cancelar servicio',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (service.status == ServiceStatus.acepted) {
|
||||
if (serviceDateTime.difference(now).inDays > 1) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 130),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.red.shade400,
|
||||
),
|
||||
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text('Cancelar servicio',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (service.status == ServiceStatus.active) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 130),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.blue.shade400,
|
||||
),
|
||||
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text('Terminar servicio',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
|
||||
// GeneralSecondaryButton(
|
||||
// label: 'Terminar servicio',
|
||||
// onPressed: () {
|
||||
// // final currentState = context.read<ServiceBloc>().state;
|
||||
// // if (currentState is ServiceLoaded) {
|
||||
// // context.read<ServiceBloc>().add(UpdateServiceStatus(
|
||||
// // widget.serviceId, ServiceStatus.completed));
|
||||
// // }
|
||||
// });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 130),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (type == 'professional') {
|
||||
navigateTo(Flurorouter.professionalServicesRequestsRoute);
|
||||
}
|
||||
if (type == 'user') {
|
||||
navigateTo(Flurorouter.dashboardRoute);
|
||||
}
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Colors.blue.shade400,
|
||||
),
|
||||
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
)),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: const Text('Volver', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _CustomServiceLocation(ServiceLocationPreferences location) {
|
||||
if (type == 'user') {
|
||||
if (location == ServiceLocationPreferences.office) {
|
||||
return 'Servicio en sitio / consultorio';
|
||||
}
|
||||
}
|
||||
|
||||
if (type == 'professional') {
|
||||
if (location == ServiceLocationPreferences.office) {
|
||||
return 'Servicio en tu consultorio';
|
||||
}
|
||||
}
|
||||
if (location == ServiceLocationPreferences.delivery) {
|
||||
return 'Servicio a domicilio';
|
||||
}
|
||||
|
||||
return 'Error al cargar el servicio.';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:prosapp_web_app/models/schedules_entity.dart';
|
||||
import 'package:prosapp_web_app/models/service.dart';
|
||||
import 'package:prosapp_web_app/models/service_status.dart';
|
||||
import 'package:prosapp_web_app/services/navigation_service.dart';
|
||||
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
|
||||
import 'package:prosapp_web_app/ui/shared/widgets/status_item.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/services_provider.dart';
|
||||
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
|
||||
class ServicesHistoryView extends StatelessWidget {
|
||||
final String type;
|
||||
|
||||
const ServicesHistoryView({super.key, required this.type});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final servicesProvider =
|
||||
Provider.of<ServicesProvider>(context, listen: false);
|
||||
|
||||
if (type == 'user') {
|
||||
servicesProvider.getServicesHistoryForUser(
|
||||
Provider.of<AuthProvider>(context, listen: false).user!.id);
|
||||
}
|
||||
if (type == 'professional') {
|
||||
servicesProvider.getServicesHistoryForProfessional(
|
||||
Provider.of<AuthProvider>(context, listen: false).user!.id);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: Consumer<ServicesProvider>(
|
||||
builder: (context, servicesProvider, child) {
|
||||
if (servicesProvider.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (servicesProvider.services.isEmpty) {
|
||||
return ListView(
|
||||
children: const [
|
||||
WhiteCard(
|
||||
child: Center(child: Text('No hay servicios disponibles.')),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: servicesProvider.services.length,
|
||||
itemBuilder: (context, index) {
|
||||
final data = servicesProvider.services[index];
|
||||
|
||||
final image =
|
||||
(data.user.picture == '' || data.user.picture == null)
|
||||
? const Image(image: AssetImage('no-image.jpg'))
|
||||
: FadeInImage.assetNetwork(
|
||||
placeholder: 'loader.gif',
|
||||
fit: BoxFit.cover,
|
||||
image: data.user.picture!,
|
||||
);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
NavigationService.replaceTo(
|
||||
'/dashboard/$type/service/${data.service.id}');
|
||||
},
|
||||
child: WhiteCard(
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: ClipOval(
|
||||
child: image,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data.user.name,
|
||||
style: CustomLabels.h2,
|
||||
),
|
||||
if (data.service.description != '')
|
||||
Text(
|
||||
'"${data.service.description}"',
|
||||
style: CustomLabels.h5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${ScheduleEntity.getFormatTime(data.service.range1Hour1)} - ${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(data.service.day))}',
|
||||
style: const TextStyle(
|
||||
color: Colors.black54, fontSize: 16),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10),
|
||||
child: customStatus(data.service),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget customStatus(Service service) {
|
||||
if (service.status == ServiceStatus.completed) {
|
||||
return const StatusItem(text: 'Completado', color: Colors.blueAccent);
|
||||
}
|
||||
if (service.status == ServiceStatus.cancelled) {
|
||||
return const StatusItem(text: 'Cancelado', color: Colors.red);
|
||||
}
|
||||
if (service.status == ServiceStatus.denied) {
|
||||
return const StatusItem(text: 'Rechazado', color: Colors.red);
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:prosapp_web_app/models/schedules_entity.dart';
|
||||
import 'package:prosapp_web_app/models/service.dart';
|
||||
import 'package:prosapp_web_app/models/service_status.dart';
|
||||
import 'package:prosapp_web_app/services/navigation_service.dart';
|
||||
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
|
||||
import 'package:prosapp_web_app/ui/shared/widgets/status_item.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/services_provider.dart';
|
||||
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
|
||||
class ServicesRequestsView extends StatelessWidget {
|
||||
const ServicesRequestsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final servicesProvider =
|
||||
Provider.of<ServicesProvider>(context, listen: false);
|
||||
|
||||
servicesProvider.getServicesRequestsForProfessional(
|
||||
Provider.of<AuthProvider>(context, listen: false).user!.id);
|
||||
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: Consumer<ServicesProvider>(
|
||||
builder: (context, servicesProvider, child) {
|
||||
if (servicesProvider.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (servicesProvider.services.isEmpty) {
|
||||
return ListView(
|
||||
children: const [
|
||||
WhiteCard(
|
||||
child: Center(child: Text('No hay servicios disponibles.')),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: servicesProvider.services.length,
|
||||
itemBuilder: (context, index) {
|
||||
final data = servicesProvider.services[index];
|
||||
|
||||
final image =
|
||||
(data.user.picture == '' || data.user.picture == null)
|
||||
? const Image(image: AssetImage('no-image.jpg'))
|
||||
: FadeInImage.assetNetwork(
|
||||
placeholder: 'loader.gif',
|
||||
fit: BoxFit.cover,
|
||||
image: data.user.picture!,
|
||||
);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
NavigationService.replaceTo(
|
||||
'/dashboard/professional/service/${data.service.id}');
|
||||
},
|
||||
child: WhiteCard(
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: ClipOval(
|
||||
child: image,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data.user.name,
|
||||
style: CustomLabels.h2,
|
||||
),
|
||||
if (data.service.description != '')
|
||||
Text(
|
||||
'"${data.service.description}"',
|
||||
style: CustomLabels.h5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${ScheduleEntity.getFormatTime(data.service.range1Hour1)} - ${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(data.service.day))}',
|
||||
style: const TextStyle(
|
||||
color: Colors.black54, fontSize: 16),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10),
|
||||
child: customStatus(data.service),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget customStatus(Service service) {
|
||||
if (service.status == ServiceStatus.pending) {
|
||||
return const StatusItem(text: 'Solicitud', color: Colors.black45);
|
||||
}
|
||||
return const SizedBox();
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:prosapp_web_app/models/schedules_entity.dart';
|
||||
import 'package:prosapp_web_app/models/service.dart';
|
||||
import 'package:prosapp_web_app/models/service_status.dart';
|
||||
import 'package:prosapp_web_app/services/navigation_service.dart';
|
||||
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
|
||||
import 'package:prosapp_web_app/ui/shared/widgets/status_item.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/services_provider.dart';
|
||||
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
|
||||
class ServicesView extends StatelessWidget {
|
||||
final String type;
|
||||
|
||||
const ServicesView({super.key, required this.type});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final servicesProvider =
|
||||
Provider.of<ServicesProvider>(context, listen: false);
|
||||
|
||||
if (type == 'user') {
|
||||
servicesProvider.getServicesForUser(
|
||||
Provider.of<AuthProvider>(context, listen: false).user!.id);
|
||||
}
|
||||
if (type == 'professional') {
|
||||
servicesProvider.getServicesForProfessional(
|
||||
Provider.of<AuthProvider>(context, listen: false).user!.id);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: Consumer<ServicesProvider>(
|
||||
builder: (context, servicesProvider, child) {
|
||||
if (servicesProvider.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (servicesProvider.services.isEmpty) {
|
||||
return ListView(
|
||||
children: const [
|
||||
WhiteCard(
|
||||
child: Center(child: Text('No hay servicios disponibles.')),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: servicesProvider.services.length,
|
||||
itemBuilder: (context, index) {
|
||||
final data = servicesProvider.services[index];
|
||||
|
||||
final image =
|
||||
(data.user.picture == '' || data.user.picture == null)
|
||||
? const Image(image: AssetImage('no-image.jpg'))
|
||||
: FadeInImage.assetNetwork(
|
||||
placeholder: 'loader.gif',
|
||||
fit: BoxFit.cover,
|
||||
image: data.user.picture!,
|
||||
);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
NavigationService.replaceTo(
|
||||
'/dashboard/$type/service/${data.service.id}');
|
||||
},
|
||||
child: WhiteCard(
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: ClipOval(
|
||||
child: image,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data.user.name,
|
||||
style: CustomLabels.h2,
|
||||
),
|
||||
if (data.service.description != '')
|
||||
Text(
|
||||
'"${data.service.description}"',
|
||||
style: CustomLabels.h5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${ScheduleEntity.getFormatTime(data.service.range1Hour1)} - ${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(data.service.day))}',
|
||||
style: const TextStyle(
|
||||
color: Colors.black54, fontSize: 16),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10),
|
||||
child: customStatus(data.service),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget customStatus(Service service) {
|
||||
if (service.status == ServiceStatus.pending) {
|
||||
return const StatusItem(text: 'Pendiente', color: Colors.black54);
|
||||
}
|
||||
if (service.status == ServiceStatus.acepted) {
|
||||
return const StatusItem(text: 'Aceptado', color: Colors.green);
|
||||
}
|
||||
if (service.status == ServiceStatus.active) {
|
||||
return const StatusItem(text: 'Activo', color: Colors.blueAccent);
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import 'package:prosapp_web_app/providers/settings_provider.dart';
|
||||
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SupportView extends StatelessWidget {
|
||||
const SupportView({super.key});
|
||||
|
||||
Future<void> _launchURL(String url) async {
|
||||
final Uri _url = Uri.parse(url);
|
||||
|
||||
if (await canLaunchUrl(_url)) {
|
||||
await launchUrl(
|
||||
_url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
webOnlyWindowName: '_blank', // Para abrir en una nueva pestaña en web
|
||||
);
|
||||
} else {
|
||||
throw 'No se pudo abrir la URL $url';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _goWhatssApp(String number) async {
|
||||
final url = 'https://api.whatsapp.com/send?phone=$number&text=Hola%21+soy+usuario+de+Prosapp+y+quisiera+conocer+mas+sobre+esta+app+%F0%9F%98%81';
|
||||
await _launchURL(url);
|
||||
}
|
||||
|
||||
Future<void> _goEmail(String email) async {
|
||||
final url = 'mailto:$email?subject=${Uri.encodeComponent(email)}';
|
||||
|
||||
await _launchURL(url);
|
||||
}
|
||||
|
||||
Future<void> _goSugerencias() async {
|
||||
const url = 'https://admin.prosapp.co/sugerencias';
|
||||
await _launchURL(url);
|
||||
}
|
||||
|
||||
Future<void> _goPoliticas(String urlText) async {
|
||||
final url = urlText;
|
||||
await _launchURL(url);
|
||||
}
|
||||
|
||||
Future<void> _goTerminos(String urlText) async {
|
||||
final url = urlText;
|
||||
await _launchURL(url);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
|
||||
return Consumer<SettingsProvider>(
|
||||
builder: (context, settingsProvider, child) {
|
||||
if (settingsProvider.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final settings = settingsProvider.settings!;
|
||||
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
child: WhiteCard(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 30),
|
||||
child: Center(
|
||||
child: Text(
|
||||
settings.tituloSoporte,
|
||||
style: TextStyle(
|
||||
fontSize: size.width > 500 ? 30 : 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: size.width > 500 ? 30 : 0,
|
||||
right: size.width > 500 ? 30 : 0,
|
||||
bottom: 30,
|
||||
),
|
||||
child: Text(
|
||||
settings.parrafoSoporte,
|
||||
style: CustomLabels.h3,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Horario de atención:',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(settings.diasSoporte, style: CustomLabels.h3),
|
||||
Text(settings.horasSoporte, style: CustomLabels.h3),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: size.width > 500 ? 30 : 0,
|
||||
right: size.width > 500 ? 30 : 0,
|
||||
bottom: 30,
|
||||
),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 15,
|
||||
runSpacing: 15,
|
||||
children: [
|
||||
_buildGridButton(
|
||||
context,
|
||||
icon: Icons.phone,
|
||||
text: 'WhatsApp',
|
||||
onPressed: () =>
|
||||
_goWhatssApp(settings.numeroSoporte),
|
||||
),
|
||||
_buildGridButton(
|
||||
context,
|
||||
icon: Icons.email,
|
||||
text: 'Email',
|
||||
onPressed: () => _goEmail(settings.emailSoporte),
|
||||
),
|
||||
_buildGridButton(
|
||||
context,
|
||||
icon: Icons.feedback,
|
||||
text: 'Sugerencias',
|
||||
onPressed: _goSugerencias,
|
||||
),
|
||||
_buildGridButton(
|
||||
context,
|
||||
icon: Icons.privacy_tip,
|
||||
text: 'Políticas de Privacidad',
|
||||
onPressed: () =>
|
||||
_goPoliticas(settings.politicasPrivacidad),
|
||||
),
|
||||
_buildGridButton(
|
||||
context,
|
||||
icon: Icons.article,
|
||||
text: 'Términos y Condiciones',
|
||||
onPressed: () =>
|
||||
_goTerminos(settings.terminosCondiciones),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGridButton(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String text,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: Container(
|
||||
height: 110,
|
||||
width: 180,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[600],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 40, color: Colors.white),
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: Text(
|
||||
textAlign: TextAlign.center,
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user