This commit is contained in:
Felipe
2023-11-09 11:21:54 -05:00
parent db2f965e4c
commit b3d1740649
51 changed files with 397 additions and 274 deletions
+101
View File
@@ -0,0 +1,101 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/presentation/screens/web_view.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutScreen extends StatefulWidget {
const AboutScreen({super.key});
@override
State<AboutScreen> createState() => _AboutScreenState();
}
class _AboutScreenState extends State<AboutScreen> {
SettingModel? settings;
@override
void initState() {
super.initState();
if (settings == null) {
SettingModel.getSettings().then(
(SettingModel value) => setState(() {
settings = value;
}),
);
}
}
void _launchURL(String url) async {
if (await canLaunch(url)) {
await launch(url, forceSafariVC: false, forceWebView: false);
} else {
throw 'No se pudo abrir el enlace $url';
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Acerca de la aplicación'),
body: ListView(
children: [
ListTile(
onTap: () {
if (kIsWeb) {
_launchURL(settings?.proliticasPrivacidad ?? '');
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return WebViewScreen(
label: 'Políticas de privacidad',
link: settings?.proliticasPrivacidad ?? '',
);
},
),
);
}
},
title: const Text('Políticas de privacidad'),
trailing:
const Icon(Icons.keyboard_arrow_right, color: Colors.black),
),
ListTile(
onTap: () {
if (kIsWeb) {
_launchURL(settings?.terminosCondiciones ?? '');
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return WebViewScreen(
label: 'Términos y condiciones',
link: settings?.terminosCondiciones ?? '',
);
},
),
);
}
},
title: const Text('Términos y condiciones'),
trailing:
const Icon(Icons.keyboard_arrow_right, color: Colors.black),
),
ListTile(
title: const Text('Versión de la aplicación'),
subtitle: Text(settings?.version ?? ''),
),
],
),
);
}
}
+453
View File
@@ -0,0 +1,453 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/components/primary_btn.dart';
import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/presentation/screens/cita.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:intl/intl.dart';
class CalendarScreen extends StatefulWidget {
const CalendarScreen({super.key});
@override
State<CalendarScreen> createState() => _CalendarScreenState();
}
class _CalendarScreenState extends State<CalendarScreen> {
final uid = AuthenticationRepository.instance.getCurrentUserUid();
List<Event>? _events;
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
EventoService eventoService = EventoService();
CalendarFormat _calendarFormat = CalendarFormat.month;
DateTime today = DateTime.now();
DateTime now = DateTime.now();
TimeOfDay? _selectedTime1;
TimeOfDay? _selectedTime2;
Future<TimeOfDay?> _selectTime1(BuildContext context) async {
final TimeOfDay? pickedTime1 = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (pickedTime1 != null) {
setState(() {
_selectedTime1 = pickedTime1;
});
}
return pickedTime1;
}
Future<TimeOfDay?> _selectTime2(BuildContext context) async {
final TimeOfDay? pickedTime2 = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (pickedTime2 != null) {
setState(() {
_selectedTime2 = pickedTime2;
});
}
return pickedTime2;
}
@override
void initState() {
super.initState();
if (_events == null) {
Event.getEventsAllByIdStatus(uid ?? "", 'aprobado')
.then((value) => setState(() {
_events = value;
}));
}
}
void _onDaySelected(DateTime day, DateTime focusedDay) {
setState(() {
today = day;
});
}
void _onFormatChange(CalendarFormat format) {
setState(() {
_calendarFormat = format;
});
}
@override
Widget build(BuildContext context) {
if (_events == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
var events = _events!;
// DateTime firstDay = today.subtract(Duration(days: 365));
DateTime lastDay = today.add(const Duration(days: 365));
return Scaffold(
floatingActionButtonLocation: kIsWeb
? FloatingActionButtonLocation.startFloat
: FloatingActionButtonLocation.endFloat,
resizeToAvoidBottomInset: false,
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Calendario',
),
body: Column(
children: [
Container(
color: const Color.fromARGB(255, 224, 247, 255),
child: TableCalendar(
locale: 'es_MX',
firstDay: DateTime.utc(2010, 10, 16),
lastDay: lastDay,
focusedDay: today,
availableGestures: AvailableGestures.all,
onDaySelected: _onDaySelected,
selectedDayPredicate: (day) => isSameDay(day, today),
calendarFormat: _calendarFormat,
onFormatChanged: _onFormatChange,
eventLoader: (date) {
return events
.where((element) {
DateTime day = DateTime.parse(element.day);
return (date.year == day.year &&
date.month == day.month &&
date.day == day.day);
})
.map((e) => e.description)
.toList();
},
availableCalendarFormats: const {
CalendarFormat.month: 'Mes',
CalendarFormat.week: 'Semana',
CalendarFormat.twoWeeks: '2 Semanas',
},
),
),
SizedBox(
width: double.infinity,
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
child: Text(DateFormat('dd MMMM yyyy', 'es').format(today)),
)),
const Divider(
height: 0,
),
_eventList()
],
),
floatingActionButton: FloatingActionButton(
onPressed: _showDialog,
child: const Icon(Icons.add),
),
);
}
void _showDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setStateDialog) {
return SizedBox(
height: 800,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 0, vertical: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
_selectedTime1 == null
? ''
: _selectedTime1!.format(context),
style: TextStyle(
color: Colors.grey[500],
fontSize: 12,
),
),
_selectedTime2 != null && _selectedTime1 != null
? Text(
' - ',
style: TextStyle(
color: Colors.grey[500],
fontSize: 12,
),
)
: const SizedBox(),
Text(
_selectedTime2 == null
? ''
: _selectedTime2!.format(context),
style: TextStyle(
color: Colors.grey[500],
fontSize: 12,
),
),
Text(
' | ',
style: TextStyle(
color: Colors.grey[500],
),
),
Text(
DateFormat('dd MMMM yyyy', 'es').format(today),
style: const TextStyle(
fontSize: 13,
),
)
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextFormField(
controller: _titleController,
decoration: const InputDecoration(hintText: 'Titulo'),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 20),
child: TextFormField(
controller: _descriptionController,
decoration:
const InputDecoration(hintText: 'Descripción'),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 40),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 80,
child: TextFormField(
textAlign: TextAlign.center,
onTap: () async {
var value = await _selectTime1(context);
setStateDialog(() {
_selectedTime1 = value;
});
},
readOnly: true,
decoration: const InputDecoration(
hintText: 'Hora',
),
controller: TextEditingController(
text: _selectedTime1 == null
? ''
: ' ${_selectedTime1!.format(context)}'),
style: const TextStyle(fontSize: 15),
),
),
const Text(' - '),
SizedBox(
width: 80,
child: TextFormField(
textAlign: TextAlign.center,
onTap: () async {
var value = await _selectTime2(context);
setStateDialog(() {
_selectedTime2 = value;
});
},
readOnly: true,
decoration: const InputDecoration(
hintText: 'Hora',
),
controller: TextEditingController(
text: _selectedTime2 == null
? ''
: ' ${_selectedTime2!.format(context)}'),
style: const TextStyle(fontSize: 15),
),
),
],
),
),
PrimaryButtom(
onPressed: () async {
final DateTime combinedDate1 = DateTime(
today.year,
today.month,
today.day,
_selectedTime1!.hour,
_selectedTime1!.minute,
);
final DateTime combinedDate2 = DateTime(
today.year,
today.month,
today.day,
_selectedTime2!.hour,
_selectedTime2!.minute,
);
await eventoService
.createEvent(
_titleController.text,
_descriptionController.text,
DateFormat('yyyy-MM-dd HH:mm:ss.SSS').format(today),
'$combinedDate1',
'$combinedDate2',
uid.toString(),
'sitio',
'',
0,
0,
'aprobado',
0,
false,
false,
)
.then((value) {
Navigator.pop(context);
_titleController.text = '';
_descriptionController.text = '';
});
Event.getEventsAllByIdStatus(uid ?? "", 'aprobado')
.then(
(value) => setState(() {
_events = value;
}),
);
},
label: 'Añadir evento',
),
],
),
);
},
),
);
},
);
}
Widget _eventList() {
return FutureBuilder(
future: getByProId(DateFormat("yyyy-MM-dd 00:00:00.000").format(today)),
builder: (BuildContext context, AsyncSnapshot<List<Event>> snapshot) {
if (!snapshot.hasData) {
return const Text('No tienes citas');
}
List<Event> eventos = [];
try {
eventos.addAll(snapshot.data!);
} catch (e) {
print("Error" + e.toString());
}
if (eventos.isEmpty) {
return const Center(child: Text('No tienes citas'));
}
return Column(
children: [
...eventos.map(
(e) => ListTile(
onTap: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return CitaScreen(evento: e);
},
),
);
},
leading: Text(
TimeOfDay.fromDateTime(DateTime.parse(e.range1Hour1))
.format(context)),
title: RichText(
text: TextSpan(
children: [
TextSpan(
text: '${e.title}, ',
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
TextSpan(
text: DateFormat('dd MMM', 'es')
.format(DateTime.parse(e.day)),
style: const TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
],
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
e.professionalId == e.userId
? const SizedBox()
: RatingBar.builder(
initialRating: e.scoresModel?.average ?? 0,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 25,
maxRating: 5,
itemPadding:
const EdgeInsets.symmetric(horizontal: 0),
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC),
),
onRatingUpdate: (rating) {},
ignoreGestures: true,
),
const SizedBox(width: 5),
e.professionalId == e.userId
? const SizedBox()
: Text(
'(${e.scoresModel?.total.toString()}) ${e.scoresModel?.average.toStringAsFixed(1)}'),
],
),
Text(
'" ${e.description} "',
style: const TextStyle(fontStyle: FontStyle.italic),
),
],
),
trailing: const Icon(Icons.keyboard_arrow_right),
),
),
],
);
},
);
}
}
+397
View File
@@ -0,0 +1,397 @@
import 'dart:convert';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/photo_view.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/chat_model.dart';
import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/models/user_model.dart';
import 'package:prosappco/src/presentation/screens/professional.dart';
import 'package:prosappco/src/presentation/screens/professional_info.dart';
import 'package:http/http.dart' as http;
class ChatScreen extends StatefulWidget {
final String? eventoId;
const ChatScreen({super.key, this.eventoId});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final _textController = TextEditingController();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
UserModel? user;
Professional? professional;
bool pro = false;
Future<void> sendPushNotification(String token) async {
try {
http.Response response = await http.post(
Uri.parse('https://fcm.googleapis.com/fcm/send'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
'Authorization':
'key=AAAAORdR-xU:APA91bF_wblg86jHAC-uexrXPHavYRlk5wge1Gf46m56V4J2D2L37Cp_hf46JZUzpvsWPSpqc5ewHelKI9LTifUG_s2mciMI6e5VLKo7E1R8btbNo7iaM9do2ctoyHKUm1atlZBdKaN2',
},
body: jsonEncode(
<String, dynamic>{
'notification': <String, dynamic>{
'body': 'Tienes un nuevo mensaje',
'title': 'Nuevo mensaje',
},
'priority': 'high',
'data': <String, dynamic>{
'click_action': 'FLUTTER_NOTIFICATION_CLICK',
'id': '1',
'status': 'done'
},
'to': token,
},
),
);
response;
} catch (e) {
print('error al enviar notificacion $e');
}
}
@override
void initState() {
super.initState();
Event.getEventById(widget.eventoId!).then((event) {
if (user == null) {
if (uid != event.userId) {
UserModel.getUser(event.userId).then(
(UserModel s) => setState(() => user = s),
);
} else {
Professional.getProfessional(event.professionalId)
.then((value) => {professional = value});
UserModel.getUser(event.professionalId).then(
(UserModel s) => setState(() => {user = s, pro = true}),
);
}
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Chat'),
body: Column(
children: [
Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.3),
spreadRadius: 2,
blurRadius: 3,
offset: const Offset(0, 2),
),
],
),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
color: const Color(0xFFD6F4FF),
alignment: Alignment.topCenter,
child: ListTile(
leading: GestureDetector(
onTap: () {
if (pro) {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (BuildContext context) {
return ProfessionalInfoScreen(
professional: professional!,
);
},
),
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: ReferencePhoto(
ref: user?.photo,
size: 55,
sizeCircle: 60,
),
),
),
title: Text(
'${user?.name}',
style: const TextStyle(
color: Colors.black, fontWeight: FontWeight.w600),
),
subtitle: Text(user?.profession ?? ''),
trailing: const Icon(Icons.keyboard_arrow_right),
),
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.only(top: 10),
reverse: true,
child: streamB(uid!),
),
),
Container(
alignment: Alignment.bottomCenter,
width: MediaQuery.of(context).size.width,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
width: MediaQuery.of(context).size.width,
child: Row(
children: [
Expanded(
child: TextFormField(
controller: _textController,
style: const TextStyle(color: Colors.black),
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],
),
onFieldSubmitted: (value) async {
String muestra = _textController.text.trim();
if (muestra.isNotEmpty) {
final nuevoMensaje = MessageModel(
user: uid!,
content:
_textController.text.trimLeft().trimRight(),
timestamp: DateTime.now());
final nuevoMensajeMap = {
'user': nuevoMensaje.user,
'content': nuevoMensaje.content,
'timestamp': nuevoMensaje.timestamp,
};
FirebaseFirestore.instance
.collection('chats')
.doc(widget.eventoId)
.update({
'message': FieldValue.arrayUnion([nuevoMensajeMap])
});
if (user?.token != '') {
sendPushNotification(user!.token!);
}
// if (user?.token != '') {
// final mensajesQuerySnapshot =
// await FirebaseFirestore.instance
// .collection('chats')
// .doc(widget.eventoId)
// .get();
// final mensajes =
// mensajesQuerySnapshot.data()?['message'];
// if (mensajes != null && mensajes.isNotEmpty) {
// final ultimoMensaje = mensajes.last;
// final ultimoMensajeUser = ultimoMensaje['user'];
// if (ultimoMensajeUser == uid) {
// // El último mensaje fue enviado por ti, no se envía la notificación
// } else {
// sendPushNotification(user!.token!);
// }
// } else {
// sendPushNotification(user!.token!);
// }
// }
_textController.clear();
}
},
),
),
const SizedBox(width: 12),
GestureDetector(
onTap: () async {
String muestra = _textController.text.trim();
if (muestra.isNotEmpty) {
final nuevoMensaje = MessageModel(
user: uid!,
content:
_textController.text.trimLeft().trimRight(),
timestamp: DateTime.now());
final nuevoMensajeMap = {
'user': nuevoMensaje.user,
'content': nuevoMensaje.content,
'timestamp': nuevoMensaje.timestamp,
};
FirebaseFirestore.instance
.collection('chats')
.doc(widget.eventoId)
.update({
'message': FieldValue.arrayUnion([nuevoMensajeMap])
});
if (user?.token != '') {
final mensajesQuerySnapshot = await FirebaseFirestore
.instance
.collection('chats')
.doc(widget.eventoId)
.get();
final mensajes =
mensajesQuerySnapshot.data()?['message'];
if (mensajes != null && mensajes.isNotEmpty) {
final ultimoMensaje = mensajes.last;
final ultimoMensajeUser = ultimoMensaje['user'];
if (ultimoMensajeUser == uid) {
// El último mensaje fue enviado por ti, no se envía la notificación
} else {
sendPushNotification(user!.token!);
}
} else {
sendPushNotification(user!.token!);
}
}
_textController.clear();
}
},
child: Container(
height: 50,
width: 50,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
child: const Center(
child: Icon(
Icons.send,
color: Colors.white,
),
),
),
)
],
),
),
),
],
),
);
}
StreamBuilder<DocumentSnapshot<Map<String, dynamic>>> streamB(String uid) {
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection('chats')
.doc(widget.eventoId)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final data = snapshot.data!;
final chat = ChatModel.fromDocumentSnapshot(data);
return Column(
children: [
...chat.messages.map(
(e) => uid != e.user
? 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.shade200,
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(
DateFormat('h:mm a').format(e.timestamp),
style: const TextStyle(
color: Colors.grey, fontSize: 12),
),
),
],
),
)
: 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(
DateFormat('h:mm a').format(e.timestamp),
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
),
],
),
),
)
],
);
},
);
}
}
+799
View File
@@ -0,0 +1,799 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/photo_view.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/models/scores_model.dart';
import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/models/user_model.dart';
import 'package:prosappco/src/presentation/screens/chat.dart';
import 'package:prosappco/src/presentation/screens/score.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:community_material_icon/community_material_icon.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class CitaScreen extends StatefulWidget {
final Event evento;
const CitaScreen({super.key, required this.evento});
@override
State<CitaScreen> createState() => _CitaScreenState();
}
class _CitaScreenState extends State<CitaScreen> {
final uid = AuthenticationRepository.instance.getCurrentUserUid();
UserModel? user;
DateTime today = DateTime.now();
String nombre = '';
String userToken = '';
String numberPhone = '';
int tarifa = 0;
Reference? ref_photo;
ScoresModel? scoresModel;
bool? ver = true;
bool? pro;
String proName = '';
late final FirebaseAuth _auth;
String formatCurrency(int number) {
final formatter =
NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: '');
return '\$${formatter.format(number)}';
}
Future<void> sendPushNotification(
String user, String accion, String proName) async {
try {
http.Response response = await http.post(
Uri.parse('https://fcm.googleapis.com/fcm/send'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
'Authorization':
'key=AAAAORdR-xU:APA91bF_wblg86jHAC-uexrXPHavYRlk5wge1Gf46m56V4J2D2L37Cp_hf46JZUzpvsWPSpqc5ewHelKI9LTifUG_s2mciMI6e5VLKo7E1R8btbNo7iaM9do2ctoyHKUm1atlZBdKaN2',
},
body: jsonEncode(
<String, dynamic>{
'notification': <String, dynamic>{
'body': accion == 'rechazo'
? '$proName a rechazado tu solicitud de servicio'
: '$proName a aprobado tu solicitud de servicio',
'title': '$proName $accion',
},
'priority': 'high',
'data': <String, dynamic>{
'click_action': 'FLUTTER_NOTIFICATION_CLICK',
'id': '1',
'status': 'done',
'screen': 'misservicios',
},
'to': user
},
),
);
response;
} catch (e) {
print('error al enviar notificacion $e');
}
}
Future<void> _openMap(double lat, double lng) async {
final Uri _url =
Uri.parse('https://www.google.com/maps/search/?api=1&query=$lat,$lng');
if (!await launchUrl(_url)) {
throw Exception('Could not launch $_url');
}
}
Future<void> _sendWhatsapp(String phoneNumber) async {
final whatsappUrl =
'https://wa.me/$phoneNumber?text=${Uri.parse('Hola! me contactaste por Prossapp')}';
if (!await launch(whatsappUrl)) {
throw Exception('Could not launch $whatsappUrl');
}
}
SettingModel? settings;
@override
void initState() {
super.initState();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
_auth = FirebaseAuth.instance;
final currentUser = _auth.currentUser;
if (currentUser != null && currentUser.displayName != null) {
proName = currentUser.displayName!;
}
if (settings == null) {
SettingModel.getSettings().then(
(SettingModel value) => setState(() {
settings = value;
}),
);
}
if (scoresModel == null) {
if (uid != widget.evento.userId) {
ScoresModel.scoreTo(widget.evento.userId, false, false).then(
(ScoresModel s) => setState(() {
scoresModel = s;
pro = true;
}),
);
} else {
ScoresModel.scoreTo(widget.evento.professionalId, true, false).then(
(ScoresModel s) => setState(() {
scoresModel = s;
pro = false;
}),
);
}
}
}
@override
Widget build(BuildContext context) {
today.difference(DateTime.parse(widget.evento.range1Hour1));
final eventDate = DateFormat('yyyy-MM-dd').parse(widget.evento.day);
if (nombre == '') {
if (uid != widget.evento.userId) {
UserModel.getUser(widget.evento.userId).then((value) {
UserModel.getUser(uid.toString()).then((me) {
setState(() {
nombre = value.name;
ref_photo = value.photo;
userToken = value.token ?? '';
numberPhone = value.phoneNumber ?? '';
});
});
});
} else {
UserModel.getUser(widget.evento.professionalId).then((value) {
setState(() {
nombre = value.name;
ref_photo = value.photo;
numberPhone = value.phoneNumber ?? '';
});
});
}
}
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Servicio'),
body: Column(
children: [
Expanded(
child: Column(
children: [
ListTile(
leading: ReferencePhoto(
ref: ref_photo,
size: 50,
sizeCircle: 50,
sizeIcon: 35,
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
nombre,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'${DateFormat('dd MMMM', 'es').format(DateTime.parse(widget.evento.day))} ${DateFormat('h:mm a').format(DateTime.parse(widget.evento.range1Hour1))}',
style: const TextStyle(
color: Colors.grey,
fontSize: 16,
),
)
],
),
subtitle: Row(
children: [
RatingBar.builder(
initialRating: scoresModel?.average ?? 0,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 25,
maxRating: 5,
itemPadding: const EdgeInsets.symmetric(horizontal: 0),
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC),
),
onRatingUpdate: (rating) {},
ignoreGestures: true,
),
const SizedBox(width: 5),
Text(
'(${scoresModel?.total.toString()}) ${scoresModel?.average.toStringAsFixed(1)}'),
],
),
),
widget.evento.userId == widget.evento.professionalId
? const SizedBox()
: Container(
margin: const EdgeInsets.only(
left: 40, right: 40, top: 20, bottom: 20),
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 15),
decoration: BoxDecoration(
color: const Color(0xFFD6F4FF),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(1, 3),
),
],
),
child: Row(
children: [
const Icon(
Icons.error_outline,
size: 27,
color: Colors.black54,
),
const SizedBox(width: 15),
widget.evento.ubicacion != 'sitio'
? const Text(
'Servicio a domicilio.',
style: TextStyle(
color: Colors.black, fontSize: 14),
)
: const Text(
'Servicio en su sitio / consultorio',
style: TextStyle(
color: Colors.black, fontSize: 14),
),
],
),
),
settings?.tarifas == true && widget.evento.tarifa != 0
? Column(
children: [
Text(
formatCurrency(widget.evento.tarifa ?? 0),
style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 25),
),
const Text('Tarifa consulta',
style: TextStyle(fontSize: 15)),
],
)
: const SizedBox(),
const SizedBox(height: 15),
// Text('${widget.evento.range1Hour1} - ${DateTime.now()}'),
Text(
textAlign: TextAlign.center,
'"${widget.evento.description?.trim()}"',
style: const TextStyle(
color: Colors.grey, fontStyle: FontStyle.italic),
),
widget.evento.userId == widget.evento.professionalId
? const SizedBox()
: widget.evento.status == 'aprobado' ||
widget.evento.status == 'iniciado'
? const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Text(
'Medios de comunicación con el usuario.',
style: TextStyle(color: Color(0xFF2BA4EC)),
),
)
: const SizedBox(height: 10),
widget.evento.userId == widget.evento.professionalId
? const SizedBox()
: widget.evento.status == 'terminado'
? pro == true
? widget.evento.professionalScored == true
? const SizedBox()
: Column(
children: [
const SizedBox(height: 120),
ElevatedButton(
onPressed: () {
Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return ScoreScreen(
evento: widget.evento,
pro: pro!,
);
},
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor:
const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(230, 60),
),
child: const Text(
'Puntuar servicio',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
],
)
: widget.evento.userScored == true
? const SizedBox()
: Column(
children: [
const SizedBox(height: 120),
ElevatedButton(
onPressed: () {
Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return ScoreScreen(
evento: widget.evento,
pro: pro!,
);
},
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor:
const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(230, 60),
),
child: const Text(
'Puntuar servicio',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
],
)
: widget.evento.status == 'aprobado' ||
widget.evento.status == 'iniciado'
? Row(
children: [
const Expanded(child: SizedBox()),
ElevatedButton(
onPressed: () => launch("tel:$numberPhone"),
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFF2BA4EC),
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
side: const BorderSide(
color: Color(0xFF2BA4EC),
width: 2,
),
),
),
child: const Padding(
padding: EdgeInsets.symmetric(
vertical: 18, horizontal: 0),
child: Icon(
Icons.phone_android,
size: 30,
color: Color(0xFF2BA4EC),
),
),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: () async {
if (widget.evento.status != 'pendiente') {
final chatDoc = FirebaseFirestore
.instance
.collection('chats')
.doc(widget.evento.id);
final chatSnapshot =
await chatDoc.get();
if (!chatSnapshot.exists ||
chatSnapshot.data()!['message'] ==
null) {
await chatDoc.set(
{
'professional_id':
widget.evento.professionalId,
'user_id': widget.evento.userId,
'message': [],
},
SetOptions(merge: true),
).catchError((error) => print(
'Error al crear el documento: $error'));
}
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return ChatScreen(
eventoId: widget.evento.id);
},
),
);
}
// if (widget.evento.status != 'pendiente') {
// await FirebaseFirestore.instance
// .collection('chats')
// .doc(widget.evento.id)
// .set(
// {
// 'professional_id':
// widget.evento.professionalId,
// 'user_id': widget.evento.userId,
// 'message': [],
// },
// SetOptions(
// merge:
// true)).catchError((error) => print(
// 'Error al crear el documento: $error'));
// Navigator.push(
// context,
// CupertinoPageRoute(
// builder: (BuildContext context) {
// return ChatScreen(
// eventoId: widget.evento.id);
// },
// ),
// );
// }
else {
Get.snackbar(
'El profesional aun no ha aceptado tu solicitud',
'Debes esperar a que el profesional acepte tu solicitud para poder iniciar un chat.',
snackPosition: SnackPosition.BOTTOM,
);
}
},
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFF2BA4EC),
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
side: const BorderSide(
color: Color(0xFF2BA4EC),
width: 2,
),
),
),
child: const Padding(
padding: EdgeInsets.symmetric(
vertical: 18, horizontal: 0),
child: Icon(
Icons.message,
size: 30,
color: Color(0xFF2BA4EC),
),
),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: () {
_sendWhatsapp(numberPhone);
},
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFF2BA4EC),
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
side: const BorderSide(
color: Color(0xFF2BA4EC),
width: 2,
),
),
),
child: const Padding(
padding: EdgeInsets.symmetric(
vertical: 18, horizontal: 0),
child: Icon(
CommunityMaterialIcons.whatsapp,
size: 30,
color: Color(0xFF2BA4EC),
),
),
),
const Expanded(child: SizedBox()),
],
)
: SizedBox(),
widget.evento.ubicacion == 'sitio'
? const SizedBox()
: Padding(
padding: const EdgeInsets.only(top: 40),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
_openMap(widget.evento.latitud!,
widget.evento.longitud!);
},
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFF2BA4EC),
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
side: const BorderSide(
color: Color(0xFF2BA4EC),
width: 2,
),
),
),
child: const Padding(
padding: EdgeInsets.symmetric(
vertical: 18, horizontal: 0),
child: Icon(
Icons.near_me,
size: 30,
color: Color(0xFFFFFFFF),
),
),
),
const SizedBox(width: 20),
SizedBox(
width: 200,
child: Text('${widget.evento.address}'),
)
],
),
),
],
),
),
widget.evento.status == 'aprobado'
? Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: eventDate.year == today.year &&
eventDate.month == today.month &&
eventDate.day == today.day
? (DateTime.now()
.difference(DateTime.parse(
widget.evento.range1Hour1))
.abs() <=
const Duration(minutes: 30) &&
ver == true)
? ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "iniciado"}).then(
(value) {
setState(() {
ver = false;
});
});
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(230, 60),
),
child: const Text(
'Iniciar servicio',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
)
: const SizedBox()
: const SizedBox(),
),
widget.evento.professionalId == uid
? ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "denegado"}).then(
(value) {
if (userToken != '') {
sendPushNotification(
userToken, 'rechazo', proName);
}
Navigator.pushReplacementNamed(
context, '/solicitud');
});
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFEC2B2B),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(230, 60),
),
child: const Text(
'Cancelar servicio',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
)
: const SizedBox(),
],
),
)
: const SizedBox(),
widget.evento.status == 'pendiente'
? Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: widget.evento.professionalId == uid
? ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "aprobado"}).then(
(value) {
if (userToken != '') {
sendPushNotification(
userToken, 'acepto', proName);
}
Navigator.pushReplacementNamed(
context, '/solicitud');
});
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(230, 60),
),
child: const Text(
'Aceptar',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
)
: const SizedBox(),
),
ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "denegado"}).then((value) {
if (userToken != '') {
sendPushNotification(
userToken, 'rechazo', proName);
}
Navigator.pushReplacementNamed(
context, '/solicitud');
});
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFEC2B2B),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(230, 60),
),
child: const Text(
'Cancelar servicio',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
],
),
)
: const SizedBox(),
widget.evento.status == 'iniciado' || ver == false
? Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Column(
children: [
ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "terminado"}).then((value) {
Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return ScoreScreen(
evento: widget.evento,
pro: pro!,
);
},
),
);
});
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(230, 60),
),
child: const Text(
'Terminar servicio',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
],
),
)
: const SizedBox(),
],
),
);
}
}
+206
View File
@@ -0,0 +1,206 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:diacritic/diacritic.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
class CityScreen extends StatefulWidget {
const CityScreen({super.key});
@override
State<CityScreen> createState() => _CityScreenState();
}
final CollectionReference countriesCollection =
FirebaseFirestore.instance.collection('countries');
class City {
String? cityName;
String? coordsOfCity;
String? stateOfCity;
String? countryOfCity;
City({
this.cityName,
this.coordsOfCity,
this.stateOfCity,
this.countryOfCity,
});
@override
String toString() {
return "${cityName ?? ""}, ${coordsOfCity ?? ""}, ${stateOfCity ?? ""}, ${countryOfCity ?? ""}";
}
}
Future<List<City>> getCountries() async {
List<City> citys = [];
try {
QuerySnapshot countries = await countriesCollection.get();
for (DocumentSnapshot country in countries.docs) {
String countryName = country.id;
Map<String, dynamic> data = country.data() as Map<String, dynamic>;
Map<String, Map<String, String>> states = {};
for (var entry in data.entries) {
String key = entry.key;
Map<String, String> cityData = Map<String, String>.from(entry.value);
states[key] = cityData;
}
for (var state in states.entries) {
var citysState = state.value.entries.map((city) => City(
cityName: city.key,
coordsOfCity: city.value,
stateOfCity: state.key,
countryOfCity: countryName,
));
citys.addAll(citysState);
}
}
} catch (e) {
print('$e');
}
return citys;
}
class _CityScreenState extends State<CityScreen> {
List<City>? filteredCities;
TextEditingController searchController = TextEditingController();
final User? user = FirebaseAuth.instance.currentUser;
final uid = AuthenticationRepository.instance.getCurrentUserUid();
List<City>? _cities;
@override
void initState() {
super.initState();
searchController.addListener(() {
setState(() {
if (_cities != null) {
if (searchController.text.isEmpty) {
filteredCities = _cities!;
} else {
filteredCities = _cities!
.where((city) => removeDiacritics(city.cityName!)
.toLowerCase()
.contains(
removeDiacritics(searchController.text.toLowerCase())))
.toList();
}
}
});
});
if (_cities == null) {
getCountries().then((List<City> element) => setState(() {
_cities = element;
filteredCities = element;
}));
}
}
Future<void> updateCity(String cityName, String coordsCity) async {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'city': cityName});
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'coordsOfCity': coordsCity});
} catch (e) {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.set({'city': cityName});
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.set({'coordsOfCity': coordsCity});
} catch (e) {
print('Error al agregar la ciudad: $e');
}
print('Error al actualizar la ciudad: $e');
}
}
@override
Widget build(BuildContext context) {
if (filteredCities == null) {
return const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF2BA4EC)),
),
);
}
var citys = filteredCities!;
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Selecciona tu ciudad'),
body: Column(
children: [
Padding(
padding: const EdgeInsets.only(left: 10, right: 10, top: 10),
child: TextField(
controller: searchController,
decoration: const InputDecoration(
hintText: 'Busca una ciudad',
prefixIcon: Icon(Icons.near_me),
),
),
),
Expanded(
child: ListView.builder(
itemCount: citys.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 18.0,
color: Colors.black,
),
children: [
TextSpan(
text: '${citys[index].cityName ?? ""}, ',
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(
text:
"${citys[index].stateOfCity ?? ""}, ${citys[index].countryOfCity ?? ""}",
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
onTap: () {
updateCity(citys[index].cityName ?? "",
citys[index].coordsOfCity ?? "");
Navigator.pop(context, citys[index].cityName ?? "");
},
);
},
),
),
],
),
),
);
}
}
@@ -0,0 +1,274 @@
import 'package:flutter/material.dart';
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
import 'package:get/get.dart';
import 'package:prosappco/src/components/bottom_sheet.dart';
import 'package:prosappco/src/components/column_padding.dart';
import 'package:prosappco/src/components/primary_btn.dart';
import 'package:prosappco/src/controllers/otp_controller.dart';
import 'package:prosappco/src/controllers/phone_auth_controller.dart';
import 'package:responsive_builder/responsive_builder.dart';
class CodeValidationScreen extends StatelessWidget {
CodeValidationScreen({super.key, this.phoneNumber});
String? phoneNumber;
var otp;
final controller = Get.put(OTPController());
@override
Widget build(BuildContext context) {
return ScreenTypeLayout.builder(
mobile: (BuildContext context) => _mobileView(context),
tablet: (BuildContext context) => _mobileView(context),
desktop: (BuildContext context) => _desktopView(context),
);
}
Widget _mobileView(BuildContext context) {
return BottomSheetExpanded(
horizontalPadding: 10,
children: [
Row(
children: [
IconButton(
icon: const Icon(
Icons.arrow_back,
size: 30,
),
onPressed: () {
Navigator.pop(context);
},
),
const Text(
'Valida el código',
style: TextStyle(
color: Color(0xFF262626),
fontSize: 38.0,
fontWeight: FontWeight.bold,
),
),
],
),
ColumnPadding(
alineacion: MainAxisAlignment.start,
padding: const EdgeInsets.symmetric(horizontal: 25),
children: [
const SizedBox(height: 10),
const SizedBox(
width: double.infinity,
child: Text(
'Numero de celular',
style: TextStyle(
fontSize: 18.0,
color: Color(0xFF65676B),
),
),
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: TextField(
onChanged: (value) {
phoneNumber = value;
},
controller: TextEditingController(text: phoneNumber ?? ''),
decoration: const InputDecoration(
border: InputBorder.none,
hintText: '',
suffixIcon: Icon(Icons.edit),
),
),
),
TextButton(
child: const Text('Reenviar código'),
onPressed: () {
if (phoneNumber!.isNotEmpty) {
PhoneAuthController.instance.phoneAuthentication(
phoneNumber!,
);
}
},
),
],
),
const SizedBox(height: 10),
const SizedBox(
width: double.infinity,
child: Text(
'Codigo',
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 18.0,
color: Color(0xFF65676B),
),
),
),
const SizedBox(height: 10),
OtpTextField(
numberOfFields: 6,
focusedBorderColor: Colors.blue,
fillColor: Colors.black.withOpacity(0.1),
filled: true,
keyboardType: TextInputType.number,
onSubmit: (code) {
otp = code;
OTPController.instance.verifyOTP(otp);
},
),
const SizedBox(height: 40),
PrimaryButtom(
onPressed: () {
OTPController.instance.verifyOTP(otp);
},
label: 'Valida el código',
),
const SizedBox(height: 30),
],
),
],
);
}
Widget _desktopView(BuildContext context) {
double height = MediaQuery.of(context).size.height;
double width = MediaQuery.of(context).size.width;
return Scaffold(
backgroundColor: const Color(0xFFD6F4FF),
body: SizedBox(
height: height,
width: width,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: SizedBox(
height: height,
child: const Center(
child: Image(
image: AssetImage('images/logo_prosapp.png'),
),
),
),
),
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: width * 0.07),
color: Colors.white,
height: height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
IconButton(
icon: const Icon(
Icons.arrow_back,
size: 30,
),
onPressed: () {
Navigator.pop(context);
},
),
SizedBox(width: width * 0.01),
const Text(
'Validar código',
style: TextStyle(
color: Color(0xFF262626),
fontSize: 38.0,
fontWeight: FontWeight.bold,
),
),
],
),
ColumnPadding(
alineacion: MainAxisAlignment.start,
padding: const EdgeInsets.symmetric(horizontal: 25),
children: [
const SizedBox(height: 10),
const SizedBox(
width: double.infinity,
child: Text(
'Numero de celular',
style: TextStyle(
fontSize: 18.0,
color: Color(0xFF65676B),
),
),
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: TextField(
onChanged: (value) {
phoneNumber = value;
},
controller: TextEditingController(
text: phoneNumber ?? ''),
decoration: const InputDecoration(
border: InputBorder.none,
hintText: '',
suffixIcon: Icon(Icons.edit),
),
),
),
TextButton(
child: const Text('Reenviar código'),
onPressed: () {
if (phoneNumber!.isNotEmpty) {
PhoneAuthController.instance
.phoneAuthentication(
phoneNumber!,
);
}
},
),
],
),
const SizedBox(height: 10),
const SizedBox(
width: double.infinity,
child: Text(
'Codigo',
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 18.0,
color: Color(0xFF65676B),
),
),
),
const SizedBox(height: 10),
OtpTextField(
numberOfFields: 6,
focusedBorderColor: Colors.blue,
fillColor: Colors.black.withOpacity(0.1),
filled: true,
keyboardType: TextInputType.number,
onSubmit: (code) {
otp = code;
OTPController.instance.verifyOTP(otp);
},
),
const SizedBox(height: 40),
PrimaryButtom(
onPressed: () {
OTPController.instance.verifyOTP(otp);
},
label: 'Valida el código',
),
const SizedBox(height: 30),
],
),
],
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,126 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/presentation/screens/about.dart';
class ConfiguracionScreen extends StatefulWidget {
const ConfiguracionScreen({super.key});
@override
State<ConfiguracionScreen> createState() => _ConfiguracionScreenState();
}
class _ConfiguracionScreenState extends State<ConfiguracionScreen> {
late final FirebaseAuth _auth;
@override
void initState() {
super.initState();
_auth = FirebaseAuth.instance;
}
Future<void> deleteAccount() async {
try {
final currentUser = _auth.currentUser;
if (currentUser != null) {
final uid = currentUser.uid;
await FirebaseFirestore.instance.collection('users').doc(uid).delete();
await currentUser.delete();
await _auth.signOut();
Get.snackbar(
'Cuenta Eliminada',
'Tu cuenta ha sido eliminada con éxito.',
snackPosition: SnackPosition.BOTTOM,
);
}
} catch (e) {
Get.snackbar(
'Error al Eliminar Cuenta',
'Hubo un error al eliminar tu cuenta. Por favor, inténtalo de nuevo más tarde.',
snackPosition: SnackPosition.BOTTOM,
);
}
}
Future<void> _showDeleteAccountConfirmationDialog(
BuildContext context) async {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Eliminar Cuenta'),
content: const Text(
'¿Estás seguro de que deseas eliminar tu cuenta? Esta acción no se puede deshacer.'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancelar'),
),
TextButton(
onPressed: () {
deleteAccount();
Navigator.of(context).pop();
},
child: const Text(
'Eliminar',
style:
TextStyle(color: Colors.red, fontWeight: FontWeight.w600),
),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Configuración'),
body: ListView(
children: [
ListTile(
onTap: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const AboutScreen();
},
),
);
},
title: const Text('Acerca de la aplicación'),
trailing: const Icon(
Icons.keyboard_arrow_right,
color: Colors.black,
),
),
ListTile(
onTap: () {
_showDeleteAccountConfirmationDialog(context);
},
title: const Text(
'Eliminar cuenta',
style: TextStyle(color: Colors.red),
),
),
],
),
);
}
}
+119
View File
@@ -0,0 +1,119 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/components/primary_btn.dart';
import '../../components/schedule_picker.dart';
class HorarioScreen extends StatelessWidget {
Map<String, Schedule> horarios;
HorarioScreen({super.key, required this.horarios});
final uid = AuthenticationRepository.instance.getCurrentUserUid();
bool lunesValue = false;
bool martesValue = false;
bool miercolesValue = false;
bool juevesValue = false;
bool viernesValue = false;
bool sabadoValue = false;
bool domingoValue = false;
bool jornadaContinuaLunes = false;
Future<void> updateHorario(BuildContext context) async {
try {
Map<String, dynamic> horariosMap = {};
horarios.forEach((key, value) {
horariosMap[key] = {
'habilitado': value.habilitado,
'jornadaContinua': value.jornadaContinua,
'range1Hour1': value.range1Hour1 != null
? value.range1Hour1!.format(context).toString()
: null,
'range1Hour2': value.range1Hour2 != null
? value.range1Hour2!.format(context).toString()
: null,
'range2Hour1': value.range2Hour1 != null
? value.range2Hour1!.format(context).toString()
: null,
'range2Hour2': value.range2Hour2 != null
? value.range2Hour2!.format(context).toString()
: null,
};
});
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'horario': horariosMap,
});
} catch (e) {
print('Error al actualizar el horario: $e');
}
}
int _dayOfWeekToInt(String dayOfWeek) {
switch (dayOfWeek) {
case 'Lunes':
return 1;
case 'Martes':
return 2;
case 'Miercoles':
return 3;
case 'Jueves':
return 4;
case 'Viernes':
return 5;
case 'Sabado':
return 6;
case 'Domingo':
return 7;
default:
throw ArgumentError('Invalid day of week: $dayOfWeek');
}
}
@override
Widget build(BuildContext context) {
final sortedHorarios = Map.fromEntries(
horarios.entries.toList()
..sort(
(a, b) => _dayOfWeekToInt(a.key).compareTo(_dayOfWeekToInt(b.key))),
);
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Horario'),
body: SingleChildScrollView(
child: Column(
children: [
const Divider(
height: 5,
),
Column(
children: sortedHorarios.entries.map<Widget>(
(entry) {
return SchedulePicker(
name: entry.key,
schedule: entry.value,
);
},
).toList(),
),
Padding(
padding: const EdgeInsets.only(top: 30, bottom: 30),
child: PrimaryButtom(
onPressed: () async {
await updateHorario(context);
Navigator.pop(context);
},
label: 'Guardar',
),
)
],
),
),
),
);
}
}
@@ -0,0 +1,447 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:intl_phone_field/intl_phone_field.dart';
import 'package:prosappco/src/components/bottom_sheet.dart';
import 'package:prosappco/src/components/primary_btn.dart';
import 'package:prosappco/src/controllers/phone_auth_controller.dart';
import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/providers/user_provider.dart';
import 'package:prosappco/src/presentation/screens/code_validation.dart';
import 'package:prosappco/src/presentation/screens/web_view.dart';
import 'package:provider/provider.dart';
import 'package:responsive_builder/responsive_builder.dart';
import 'package:url_launcher/url_launcher.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final controller = Get.put(PhoneAuthController());
final _formKey = GlobalKey<FormState>();
String completePhoneNumber = '';
bool _isChecked = false;
SettingModel? settings;
void _clearPhoneNumber() {
setState(() {
controller.phoneNo.text = '';
});
}
void _launchURL(String url) async {
if (await canLaunch(url)) {
await launch(url, forceSafariVC: false, forceWebView: false);
} else {
throw 'No se pudo abrir el enlace $url';
}
}
@override
void initState() {
super.initState();
if (settings == null) {
SettingModel.getSettings().then(
(SettingModel value) => setState(() {
settings = value;
}),
);
}
}
@override
Widget build(BuildContext context) {
return ScreenTypeLayout.builder(
mobile: (BuildContext context) => _mobileView(context),
tablet: (BuildContext context) => _mobileView(context),
desktop: (BuildContext context) => _desktopView(context),
);
}
Widget _mobileView(BuildContext context) {
return BottomSheetExpanded(
children: [
const SizedBox(
width: double.infinity,
child: Text(
'Iniciar sesión',
style: TextStyle(
color: Color(0xFF262626),
fontSize: 38.0,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 10),
const SizedBox(
width: double.infinity,
child: Text(
'Numero de celular',
style: TextStyle(
fontSize: 18.0,
color: Color(0xFF65676B),
),
),
),
Form(
key: _formKey,
child: IntlPhoneField(
controller: controller.phoneNo,
initialCountryCode: 'CO',
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (phoneNo) {
completePhoneNumber = phoneNo.completeNumber;
},
decoration: const InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color.fromARGB(255, 184, 0, 0)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
fillColor: Color.fromARGB(255, 239, 239, 239),
filled: true,
),
),
),
const Text(
'Un código será enviado a este numero de celular.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13.0,
color: Color(0xFF65676B),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Checkbox(
value: _isChecked,
onChanged: (value) {
setState(() {
_isChecked = value!;
});
},
),
GestureDetector(
onTap: () {
if (kIsWeb) {
_launchURL(settings?.terminosCondiciones ?? '');
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return WebViewScreen(
label: 'Términos y condiciones',
link: settings?.terminosCondiciones ?? '',
);
},
),
);
}
},
child: const Text(
'Acepto los términos y condiciones.',
style: TextStyle(
fontSize: 13.0,
color: Color(0xFF65676B),
decoration: TextDecoration.underline,
),
),
),
],
),
),
PrimaryButtom(
onPressed: () async {
if (_formKey.currentState!.validate()) {
PhoneAuthController.instance.phoneAuthentication(
completePhoneNumber.trim(),
);
await Get.to(
() => CodeValidationScreen(
phoneNumber: completePhoneNumber.trim(),
),
);
_clearPhoneNumber();
Provider.of<UserProvider>(context, listen: false)
.initUserProvider();
}
},
isEnabled: _isChecked,
label: 'Enviar código',
),
const SizedBox(height: 20),
GestureBottom(clearPhoneNumber: _clearPhoneNumber),
const SizedBox(height: 20),
const RichTxTBottom(),
const SizedBox(height: 20),
],
);
}
Widget _desktopView(BuildContext context) {
double height = MediaQuery.of(context).size.height;
double width = MediaQuery.of(context).size.width;
return Scaffold(
backgroundColor: const Color(0xFFD6F4FF),
body: SizedBox(
height: height,
width: width,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: SizedBox(
height: height,
child: const Center(
child: Image(
image: AssetImage('images/logo_prosapp.png'),
),
),
),
),
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: width * 0.1),
color: Colors.white,
height: height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
width: double.infinity,
child: Text(
'Iniciar sesión',
style: TextStyle(
color: Color(0xFF262626),
fontSize: 38.0,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 20),
const SizedBox(
width: double.infinity,
child: Text(
'Numero de celular',
style: TextStyle(
fontSize: 18.0,
color: Color(0xFF65676B),
),
),
),
Form(
key: _formKey,
child: IntlPhoneField(
controller: controller.phoneNo,
initialCountryCode: 'CO',
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
],
onChanged: (phoneNo) {
completePhoneNumber = phoneNo.completeNumber;
},
decoration: const InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color.fromARGB(255, 184, 0, 0)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
fillColor: Color.fromARGB(255, 239, 239, 239),
filled: true,
),
),
),
const Text(
'Se enviará un código a este número de celular.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13.0,
color: Color(0xFF65676B),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Checkbox(
value: _isChecked,
onChanged: (value) {
setState(() {
_isChecked = value!;
});
},
),
GestureDetector(
onTap: () {
if (kIsWeb) {
_launchURL(settings?.terminosCondiciones ?? '');
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return WebViewScreen(
label: 'Términos y condiciones',
link:
settings?.terminosCondiciones ?? '',
);
},
),
);
}
},
child: const Text(
'Acepto los términos y condiciones.',
style: TextStyle(
fontSize: 13.0,
color: Color(0xFF65676B),
decoration: TextDecoration
.underline, // Add underline style
),
),
),
],
),
),
PrimaryButtom(
onPressed: () async {
if (_formKey.currentState!.validate()) {
PhoneAuthController.instance.phoneAuthentication(
completePhoneNumber.trim(),
);
await Get.to(
() => CodeValidationScreen(
phoneNumber: completePhoneNumber.trim(),
),
);
_clearPhoneNumber();
Provider.of<UserProvider>(context, listen: false)
.initUserProvider();
}
},
isEnabled: _isChecked,
label: 'Enviar código',
),
const SizedBox(height: 20),
GestureBottom(clearPhoneNumber: _clearPhoneNumber),
const SizedBox(height: 20),
const RichTxTBottom(),
],
),
),
),
],
),
),
);
}
}
class GestureBottom extends StatelessWidget {
final VoidCallback clearPhoneNumber;
GestureBottom({
super.key,
required this.clearPhoneNumber,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
clearPhoneNumber();
Navigator.pushNamed(context, '/login');
},
child: const Text(
'Inicia sesión con tu correo electrónico',
style: TextStyle(
fontSize: 15.0, color: Color(0xFF65676B),
decoration: TextDecoration.underline, // Subrayado
),
),
);
}
}
class RichTxTBottom extends StatelessWidget {
const RichTxTBottom({
super.key,
});
@override
Widget build(BuildContext context) {
return RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 16.0,
color: Color(0xFF65676B),
fontFamily: 'Poppins',
),
children: [
const TextSpan(text: '¿No estás registrado? '),
WidgetSpan(
child: GestureDetector(
onTap: () {
Navigator.pushNamed(context, '/register');
},
child: const Text(
'Regístrate',
style: TextStyle(
fontSize: 16.0,
color: Color(0xFF2BA4EC),
fontWeight: FontWeight.w600,
),
),
),
),
],
),
);
}
}
@@ -0,0 +1,577 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/primary_btn.dart';
import 'package:prosappco/src/controllers/login_email_controller.dart';
import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/providers/user_provider.dart';
import 'package:provider/provider.dart';
import 'package:responsive_builder/responsive_builder.dart';
class LoginEmailScreen extends StatefulWidget {
const LoginEmailScreen({super.key});
@override
State<LoginEmailScreen> createState() => _LoginEmailScreenState();
}
class _LoginEmailScreenState extends State<LoginEmailScreen> {
bool _obscureText = true;
final controller = Get.put(LoginEmailController());
final _formKey = GlobalKey<FormState>();
SettingModel? settings;
@override
void initState() {
super.initState();
if (settings == null) {
SettingModel.getSettings().then(
(SettingModel value) => setState(() {
settings = value;
}),
);
}
}
@override
Widget build(BuildContext context) {
return ScreenTypeLayout.builder(
mobile: (BuildContext context) => _mobileView(context),
tablet: (BuildContext context) => _mobileView(context),
desktop: (BuildContext context) => _desktopView(context),
);
}
Widget _mobileView(BuildContext context) {
bool isIOS = Theme.of(context).platform == TargetPlatform.iOS;
return SafeArea(
child: Scaffold(
backgroundColor: const Color(0xFFD6F4FF),
body: SingleChildScrollView(
reverse: true,
child: Stack(
children: [
Container(
margin: const EdgeInsets.only(top: 130),
width: double.infinity,
height: 700,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topRight: Radius.circular(50),
topLeft: Radius.circular(50))),
),
Container(
margin: const EdgeInsets.only(top: 20, left: 30),
child: const Image(
image: AssetImage('images/logo_prosapp.png'),
width: 180,
height: 100,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 0, vertical: 20),
margin: const EdgeInsets.only(
top: 125,
left: 25,
),
child: Row(
children: <Widget>[
IconButton(
icon: const Icon(
Icons.arrow_back,
size: 30,
),
onPressed: () {
Navigator.pop(context);
},
),
const Text(
'Iniciar sesión',
style: TextStyle(
color: Color(0xFF262626),
fontSize: 38.0,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.right,
),
],
),
),
Form(
key: _formKey,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 0, vertical: 20),
margin: const EdgeInsets.only(top: 210, left: 50, right: 50),
child: Column(
children: [
!isIOS && !kIsWeb && settings?.google == true
? Padding(
padding: const EdgeInsets.only(bottom: 20),
child: ElevatedButton(
onPressed: () async {
await AuthenticationRepository.instance
.signInWithGoogle()
.then((value) => {
Provider.of<UserProvider>(context,
listen: false)
.initUserProvider()
});
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(230, 60),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Entra con Google ',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
SizedBox(width: 5),
FaIcon(FontAwesomeIcons.google),
],
)),
)
: const SizedBox(),
!isIOS && !kIsWeb && settings?.google == true
? const Padding(
padding: EdgeInsets.symmetric(vertical: 5),
child: Row(
children: [
Expanded(
child: Divider(
color: Colors.black38,
thickness: 1,
),
),
Padding(
padding:
EdgeInsets.symmetric(horizontal: 10),
child: Text("ó"),
),
Expanded(
child: Divider(
color: Colors.black38,
thickness: 1,
),
),
],
),
)
: const SizedBox(),
const Padding(
padding: EdgeInsets.only(bottom: 5),
child: Align(
alignment: Alignment.topLeft,
child: Text('Email',
style: TextStyle(
fontSize: 18.0, color: Color(0xFF65676B))),
),
),
FormEmail(controller: controller),
const Padding(
padding: EdgeInsets.only(bottom: 5),
child: Align(
alignment: Alignment.topLeft,
child: Text('Password',
style: TextStyle(
fontSize: 18.0, color: Color(0xFF65676B))),
),
),
TextFormField(
controller: controller.password,
obscureText: _obscureText,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Por favor, ingresa una contraseña';
}
return null;
},
decoration: InputDecoration(
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
border: const OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
errorBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Color.fromARGB(255, 184, 0, 0)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
hintText: 'Contraseña',
fillColor: const Color.fromARGB(255, 239, 239, 239),
filled: true,
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscureText
? Icons.visibility
: Icons.visibility_off,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
hintStyle: const TextStyle(
color: Colors.grey,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: TextButton(
onPressed: () {
Navigator.pushNamed(context, '/resetpassword');
},
child: const Text(
'Olvidé la contraseña',
style: TextStyle(
color: Colors.blue,
),
),
),
),
PaddingButtomBottom(
formKey: _formKey, controller: controller),
const RichTxtBottom()
],
),
),
),
],
),
),
),
);
}
Widget _desktopView(BuildContext context) {
double height = MediaQuery.of(context).size.height;
double width = MediaQuery.of(context).size.width;
return Scaffold(
backgroundColor: const Color(0xFFD6F4FF),
body: SizedBox(
height: height,
width: width,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: SizedBox(
height: height,
child: const Center(
child: Image(
image: AssetImage('images/logo_prosapp.png'),
),
),
),
),
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: width * 0.07),
color: Colors.white,
height: height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: <Widget>[
IconButton(
icon: const Icon(
Icons.arrow_back,
size: 30,
),
onPressed: () {
Navigator.pop(context);
},
),
SizedBox(width: width * 0.01),
const Text(
'Iniciar sesión',
style: TextStyle(
color: Color(0xFF262626),
fontSize: 38.0,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.right,
),
],
),
Form(
key: _formKey,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 0, vertical: 20),
child: Column(children: [
const Padding(
padding: EdgeInsets.only(bottom: 5),
child: Align(
alignment: Alignment.topLeft,
child: Text('Email',
style: TextStyle(
fontSize: 18.0,
color: Color(0xFF65676B))),
)),
FormEmail(controller: controller),
const Padding(
padding: EdgeInsets.only(bottom: 5),
child: Align(
alignment: Alignment.topLeft,
child: Text('Password',
style: TextStyle(
fontSize: 18.0,
color: Color(0xFF65676B))),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 50),
child: TextFormField(
controller: controller.password,
obscureText: _obscureText,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Por favor, ingresa una contraseña';
}
return null;
},
decoration: InputDecoration(
enabledBorder: const OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
focusedBorder: const OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
border: const OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
errorBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Color.fromARGB(255, 184, 0, 0)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
hintText: 'Contraseña',
hintStyle: const TextStyle(
color: Colors.grey,
),
fillColor:
const Color.fromARGB(255, 239, 239, 239),
filled: true,
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscureText
? Icons.visibility
: Icons.visibility_off,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: TextButton(
onPressed: () {
Navigator.pushNamed(context, '/resetpassword');
},
child: const Text(
'Olvidé la contraseña',
style: TextStyle(
color: Colors.blue,
),
),
),
),
PaddingButtomBottom(
formKey: _formKey, controller: controller),
const RichTxtBottom()
]),
),
),
],
),
),
),
],
),
),
);
}
}
class FormEmail extends StatelessWidget {
const FormEmail({
super.key,
required this.controller,
});
final LoginEmailController controller;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: TextFormField(
controller: controller.email,
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Por favor, ingresa un Email';
}
final RegExp emailRegExp =
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegExp.hasMatch(value)) {
return 'Por favor, ingresa un Email válido';
}
return null;
},
decoration: const InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color.fromARGB(255, 184, 0, 0)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
hintText: 'Hello@gmail.com',
fillColor: Color.fromARGB(255, 239, 239, 239),
filled: true,
prefixIcon: Icon(Icons.email_outlined),
hintStyle: TextStyle(
color: Colors.grey,
),
),
),
);
}
}
class PaddingButtomBottom extends StatelessWidget {
const PaddingButtomBottom({
super.key,
required GlobalKey<FormState> formKey,
required this.controller,
}) : _formKey = formKey;
final GlobalKey<FormState> _formKey;
final LoginEmailController controller;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 40),
child: Center(
child: PrimaryButtom(
onPressed: () {
if (_formKey.currentState!.validate()) {
LoginEmailController.instance
.loginUser(
controller.email.text.trim(),
controller.password.text.trim(),
)
.then((value) {
Provider.of<UserProvider>(context, listen: false)
.initUserProvider();
});
}
},
label: 'Iniciar',
)),
);
}
}
class RichTxtBottom extends StatelessWidget {
const RichTxtBottom({
super.key,
});
@override
Widget build(BuildContext context) {
return RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 16.0,
color: Color(0xFF65676B),
fontFamily: 'Poppins',
),
children: [
const TextSpan(text: '¿No estás registrado? '),
WidgetSpan(
child: GestureDetector(
onTap: () {
Navigator.pushReplacementNamed(context, '/register');
},
child: const Text(
'Registrarse',
style: TextStyle(
fontSize: 16.0,
color: Color(0xFF2BA4EC),
fontWeight: FontWeight.w600,
),
),
),
),
],
),
);
}
}
+104
View File
@@ -0,0 +1,104 @@
import 'package:community_material_icon/community_material_icon.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/photo_view.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/chat_model.dart';
import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/presentation/screens/chat.dart';
class MessagesScreen extends StatefulWidget {
const MessagesScreen({super.key});
@override
State<MessagesScreen> createState() => _MessagesScreenState();
}
class _MessagesScreenState extends State<MessagesScreen> {
final uid = AuthenticationRepository.instance.getCurrentUserUid();
List<ChatModel> list = [];
@override
void initState() {
super.initState();
ChatModel.getChatsByProId(uid!).then(
(List<ChatModel> s) => setState(() {
list = s;
}),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Mensajes'),
body: Column(
children: [
const Padding(
padding: EdgeInsets.all(10),
child: TextField(
// controller: searchController,
decoration: InputDecoration(
hintText: 'Escribe un nombre',
prefixIcon: Icon(CommunityMaterialIcons.stethoscope),
),
),
),
Expanded(
child: ListView.builder(
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
if (list[index].messages.isNotEmpty) {
final user = list[index].user;
final lastMsg = list[index].messages.last;
return ListTile(
title: Text(
(user?.name ?? ''),
),
subtitle: Text(
'" ${lastMsg.content} "',
style: const TextStyle(fontStyle: FontStyle.italic),
),
leading: ReferencePhoto(
ref: user?.photo,
size: 55,
sizeCircle: 60,
),
trailing: const Column(
children: [
SizedBox(height: 8),
Icon(
Icons.keyboard_arrow_right,
color: Colors.black,
),
],
),
onTap: () {
Event.getEventById(list[index].id).then((value) {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return ChatScreen(eventoId: list[index].id);
},
),
);
});
},
);
} else {
return const SizedBox();
}
},
),
),
],
),
);
}
}
@@ -0,0 +1,113 @@
import 'package:community_material_icon/community_material_icon.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/photo_view.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/chat_model.dart';
import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/presentation/screens/chat.dart';
class MessagesUserScreen extends StatefulWidget {
const MessagesUserScreen({super.key});
@override
State<MessagesUserScreen> createState() => _MessagesUserScreenState();
}
class _MessagesUserScreenState extends State<MessagesUserScreen> {
final uid = AuthenticationRepository.instance.getCurrentUserUid();
List<ChatModel> list = [];
@override
void initState() {
super.initState();
ChatModel.getChatsByUserId(uid!).then(
(List<ChatModel> s) => setState(() {
list = s;
}),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Mensajes'),
body: Column(
children: [
const Padding(
padding: EdgeInsets.all(10),
child: TextField(
// controller: searchController,
decoration: InputDecoration(
hintText: 'Escribe un nombre',
prefixIcon: Icon(CommunityMaterialIcons.stethoscope),
),
),
),
Expanded(
child: ListView.builder(
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
if (list[index].messages.isNotEmpty) {
final pro = list[index].professional;
final lastMsg = list[index].messages.last;
return ListTile(
title: Text(
(pro?.name ?? ''),
),
subtitle: Text(
'" ${lastMsg.content} "',
style: const TextStyle(fontStyle: FontStyle.italic),
),
leading: ReferencePhoto(
ref: pro?.photo,
size: 55,
sizeCircle: 60,
),
trailing: Column(
children: [
SizedBox(height: 8),
const Icon(
Icons.keyboard_arrow_right,
color: Colors.black,
),
Text(
lastMsg.timestamp.day >= DateTime.now().day
? DateFormat('h:mm a').format(lastMsg.timestamp)
: DateFormat('dd/MM/yyyy', 'es')
.format(lastMsg.timestamp),
style:
const TextStyle(color: Colors.grey, fontSize: 12),
)
],
),
onTap: () {
Event.getEventById(list[index].id).then((value) {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return ChatScreen(eventoId: list[index].id);
},
),
);
});
},
);
} else {
return const SizedBox();
}
},
),
),
],
),
);
}
}
@@ -0,0 +1,273 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:intl/intl.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/drawer_professional.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/models/scores_model.dart';
import 'package:prosappco/src/presentation/screens/cita.dart';
class MyServicesScreen extends StatelessWidget {
MyServicesScreen({super.key});
DateTime today = DateTime.now();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Mis servicios'),
drawer: DrawerProfessional(),
body: SingleChildScrollView(
child: Column(
children: [
_eventList(),
],
),
),
),
);
}
Widget _eventList() {
return StreamBuilder<List<Event>>(
stream: FirebaseFirestore.instance
.collection('services')
.where('user_id', isEqualTo: uid)
.where('status', whereIn: [
'aprobado',
'denegado',
'iniciado',
'terminado',
'pendiente'
])
.snapshots()
.asyncMap((snapshot) async {
try {
List<Event> eventos = [];
for (var element in snapshot.docs) {
final event = Event.fromJson(element.data());
event.scoresModel =
await ScoresModel.scoreTo(event.userId, false, false);
event.id = element.id;
eventos.add(event);
}
return eventos;
} catch (e) {
print('Error getByProId $e');
return [];
}
}),
builder: (BuildContext context, AsyncSnapshot<List<Event>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
List<Event> eventos = [];
try {
eventos.addAll(snapshot.data!);
eventos.sort((a, b) => a.timeStamp!.compareTo(b.timeStamp!));
} catch (e) {
print("Error" + e.toString());
}
if (eventos.isEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 50),
child: Center(child: Text('No tienes citas')),
);
}
return Column(
children: [
...eventos
.where((event) => event.professionalId != event.userId)
.map(
(event) => FutureBuilder<DocumentSnapshot>(
future: FirebaseFirestore.instance
.collection('users')
.doc(event.professionalId)
.get(),
builder: (BuildContext context,
AsyncSnapshot<DocumentSnapshot> profSnapshot) {
if (profSnapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (profSnapshot.hasError) {
return const Text(
'Error al obtener los datos del profesional',
);
}
final professionalData = profSnapshot.data;
final professionalName =
professionalData?['name'] ?? 'N/D';
return ListTile(
tileColor: event.status == 'denegado'
? Colors.red[100]
: Colors.blue[100],
onTap: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return CitaScreen(evento: event);
},
),
);
},
leading: event.status == 'aprobado'
? const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check,
color: Colors.blue,
size: 30,
),
Text('Aceptado',
style: TextStyle(fontSize: 12)),
],
)
: event.status == 'iniciado'
? const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.access_time,
color: Colors.blue,
size: 30,
),
Text('Iniciado',
style: TextStyle(fontSize: 12)),
],
)
: event.status == 'pendiente'
? const Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
Icons.access_time_outlined,
color: Colors.blue,
size: 30,
),
Text('Pendiente',
style: TextStyle(fontSize: 12)),
],
)
: event.status == 'terminado'
? const Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
Icons.rocket_launch,
color: Colors.blue,
size: 30,
),
Text('Finalizado',
style:
TextStyle(fontSize: 12)),
],
)
: const Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
Icons.close,
color: Colors.red,
size: 30,
),
Text('Cancelado',
style:
TextStyle(fontSize: 12)),
],
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$professionalName',
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'${DateFormat('dd MMMM', 'es').format(DateTime.parse(event.day))} - ${DateFormat('h:mm a').format(DateTime.parse(event.range1Hour1))}',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
RatingBar.builder(
initialRating:
event.scoresModel?.average ?? 0,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 25,
maxRating: 5,
itemPadding:
const EdgeInsets.symmetric(horizontal: 0),
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC),
),
onRatingUpdate: (rating) {},
ignoreGestures: true,
),
const SizedBox(width: 5),
Text(
'(${event.scoresModel?.total.toString()}) ${event.scoresModel?.average.toStringAsFixed(1)}'),
],
),
Text(
'"${event.description}"',
style:
const TextStyle(fontStyle: FontStyle.italic),
),
],
),
trailing: const Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(Icons.keyboard_arrow_right),
],
),
);
},
),
),
],
);
},
);
}
}
@@ -0,0 +1,225 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:intl/intl.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/drawer_professional.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/models/scores_model.dart';
import 'package:prosappco/src/presentation/screens/cita.dart';
class MyServicesProScreen extends StatelessWidget {
MyServicesProScreen({super.key});
DateTime today = DateTime.now();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Mis servicios',
),
drawer: DrawerProfessional(),
body: SingleChildScrollView(
child: Column(
children: [
_eventList(),
],
),
),
),
);
}
Widget _eventList() {
return StreamBuilder<List<Event>>(
stream: FirebaseFirestore.instance
.collection('services')
.where('professional_id', isEqualTo: uid)
.where('status', whereIn: [
'aprobado',
'denegado',
'iniciado',
'terminado',
])
.snapshots()
.asyncMap((snapshot) async {
try {
List<Event> eventos = [];
for (var element in snapshot.docs) {
final event = Event.fromJson(element.data());
event.scoresModel =
await ScoresModel.scoreTo(event.userId, false, false);
event.id = element.id;
eventos.add(event);
}
return eventos;
} catch (e) {
print('Error getByProId $e');
return [];
}
}),
builder: (BuildContext context, AsyncSnapshot<List<Event>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
List<Event> eventos = [];
try {
eventos.addAll(snapshot.data!);
eventos.sort((a, b) => a.timeStamp!.compareTo(b.timeStamp!));
} catch (e) {
print("Error" + e.toString());
}
if (eventos.isEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 50),
child: Center(child: Text('No tienes citas')),
);
}
return Column(
children: [
...eventos.map(
(event) => ListTile(
tileColor: event.status == 'denegado'
? Colors.red[100]
: Colors.blue[100],
onTap: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return CitaScreen(evento: event);
},
),
);
},
leading: event.status == 'aprobado'
? const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check,
color: Colors.blue,
size: 30,
),
Text('Aceptado', style: TextStyle(fontSize: 12)),
],
)
: event.status == 'iniciado'
? const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.access_time,
color: Colors.blue,
size: 30,
),
Text('Iniciado', style: TextStyle(fontSize: 12)),
],
)
: event.status == 'terminado'
? const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.rocket_launch,
color: Colors.blue,
size: 30,
),
Text('Finalizado',
style: TextStyle(fontSize: 12)),
],
)
: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.close,
color: Colors.red,
size: 30,
),
Text('Cancelado',
style: TextStyle(fontSize: 12)),
],
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.title,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'${DateFormat('dd MMMM', 'es').format(DateTime.parse(event.day))} - ${DateFormat('h:mm a').format(DateTime.parse(event.range1Hour1))}',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
RatingBar.builder(
initialRating: event.scoresModel?.average ?? 0,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 25,
maxRating: 5,
itemPadding:
const EdgeInsets.symmetric(horizontal: 0),
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC),
),
onRatingUpdate: (rating) {},
ignoreGestures: true,
),
const SizedBox(width: 5),
Text(
'(${event.scoresModel?.total.toString()}) ${event.scoresModel?.average.toStringAsFixed(1)}'),
],
),
Text(
'"${event.description}"',
style: const TextStyle(fontStyle: FontStyle.italic),
),
],
),
trailing: const Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(Icons.keyboard_arrow_right),
],
),
),
),
],
);
},
);
}
}
@@ -0,0 +1,275 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl_phone_field/intl_phone_field.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/components/primary_btn.dart';
import 'package:prosappco/src/controllers/new_phone_controller.dart';
class NewNumberScreen extends StatefulWidget {
const NewNumberScreen({super.key});
@override
State<NewNumberScreen> createState() => _NewNumberScreenState();
}
// Actualizar número de teléfono en Firebase
Future<void> updatePhoneNumber(String verificationId, String smsCode) async {
try {
PhoneAuthCredential credential = PhoneAuthProvider.credential(
verificationId: verificationId, smsCode: smsCode);
await FirebaseAuth.instance.currentUser!.updatePhoneNumber(credential);
print("Phone number updated successfully");
} catch (e) {
print("Error updating phone number: $e");
}
}
class _NewNumberScreenState extends State<NewNumberScreen> {
final controller = Get.put(NewPhoneController());
String completePhoneNumber = '';
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
resizeToAvoidBottomInset: false,
appBar: PopAppbar(
onPressed: () {
_formKey.currentState!.reset();
Navigator.pop(context);
},
label: 'Añadir numero',
),
body: !kIsWeb
? Container(
padding:
const EdgeInsets.symmetric(horizontal: 0, vertical: 20),
margin: const EdgeInsets.only(top: 30, left: 50, right: 50),
child: Column(
children: [
const Padding(
padding: EdgeInsets.only(bottom: 5),
child: Align(
alignment: Alignment.topLeft,
child: Text('Numero de celular',
style: TextStyle(
fontSize: 18.0, color: Color(0xFF65676B))),
)),
Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.only(bottom: 5),
child: IntlPhoneField(
controller: controller.newPhoneNo,
initialCountryCode: 'CO',
onChanged: (newPhoneNo) {
completePhoneNumber = newPhoneNo.completeNumber;
},
decoration: const InputDecoration(
border: OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color.fromARGB(255, 184, 0, 0)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
enabledBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
focusedBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
fillColor: Color.fromARGB(255, 239, 239, 239),
filled: true,
),
),
),
),
const Padding(
padding: EdgeInsets.only(bottom: 30),
child: Text(
'Se enviará un código a este número de celular',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13.0, color: Color(0xFF65676B))),
),
Container(
margin: const EdgeInsets.only(top: 10, bottom: 30),
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 15),
decoration: BoxDecoration(
color: const Color(0xFFD6F4FF),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(1, 3),
),
],
),
child: const Row(
children: [
Icon(
Icons.error_outline,
size: 27,
color: Colors.black54,
),
SizedBox(width: 15),
Expanded(
child: Text(
'¡Al actualizar tu número, se cerrará la sesión para confirmar que eres tú!.',
style:
TextStyle(color: Colors.black, fontSize: 14),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Center(
child: PrimaryButtom(
onPressed: () {
controller.updatePhoneNumber(
completePhoneNumber.toString());
},
label: 'Enviar código'),
),
),
],
),
)
: Center(
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 0, vertical: 20),
width: 400,
margin: const EdgeInsets.only(top: 30, left: 50, right: 50),
child: Column(
children: [
const Padding(
padding: EdgeInsets.only(bottom: 5),
child: Align(
alignment: Alignment.topLeft,
child: Text('Numero de celular',
style: TextStyle(
fontSize: 18.0, color: Color(0xFF65676B))),
)),
Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.only(bottom: 5),
child: IntlPhoneField(
controller: controller.newPhoneNo,
initialCountryCode: 'CO',
onChanged: (newPhoneNo) {
completePhoneNumber = newPhoneNo.completeNumber;
},
decoration: const InputDecoration(
border: OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color.fromARGB(255, 184, 0, 0)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
enabledBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
focusedBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
fillColor: Color.fromARGB(255, 239, 239, 239),
filled: true,
),
),
),
),
const Padding(
padding: EdgeInsets.only(bottom: 30),
child: Text(
'Se enviará un código a este número de celular',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13.0, color: Color(0xFF65676B))),
),
Container(
margin: const EdgeInsets.only(top: 10, bottom: 30),
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 15),
decoration: BoxDecoration(
color: const Color(0xFFD6F4FF),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(1, 3),
),
],
),
child: const Row(
children: [
Icon(
Icons.error_outline,
size: 27,
color: Colors.black54,
),
SizedBox(width: 15),
Expanded(
child: Text(
'¡Al actualizar tu número, se cerrará la sesión para confirmar que eres tú!.',
style: TextStyle(
color: Colors.black, fontSize: 14),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Center(
child: PrimaryButtom(
onPressed: () {
controller.updatePhoneNumber(
completePhoneNumber.toString());
},
label: 'Enviar código'),
),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,153 @@
import 'package:flutter/material.dart';
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
import 'package:prosappco/src/controllers/otp_controller.dart';
class NewNumberValidationScreen extends StatefulWidget {
const NewNumberValidationScreen({super.key});
@override
State<NewNumberValidationScreen> createState() =>
_NewNumberValidationScreenState();
}
class _NewNumberValidationScreenState extends State<NewNumberValidationScreen> {
dynamic otp;
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: const Color(0xFFD6F4FF),
body: Stack(
children: [
Container(
margin: const EdgeInsets.only(top: 280),
width: double.infinity,
height: 600,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topRight: Radius.circular(50),
topLeft: Radius.circular(50))),
),
Container(
margin: const EdgeInsets.only(top: 120, left: 70, right: 70),
child: const Image(image: AssetImage('images/logo_prosapp.png')),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 20),
margin: const EdgeInsets.only(top: 280, left: 25),
child: Row(
children: <Widget>[
IconButton(
icon: const Icon(
Icons.arrow_back,
size: 30,
),
onPressed: () {
Navigator.pop(context);
},
),
const Text(
'Valida el código',
style: TextStyle(
color: Color(0xFF262626),
fontSize: 38.0,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.right,
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 20),
margin: const EdgeInsets.only(top: 350, left: 50, right: 50),
child: Column(children: [
const Padding(
padding: EdgeInsets.only(bottom: 5),
child: Align(
alignment: Alignment.topLeft,
child: Text('Numero de celular',
style: TextStyle(
fontSize: 18.0, color: Color(0xFF65676B))),
)),
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Row(
children: [
const Expanded(
child: TextField(
decoration: InputDecoration(
border: InputBorder.none,
hintText: '+57',
suffixIcon: Icon(Icons.edit),
),
),
),
TextButton(
onPressed: () {
// Acción a realizar cuando se hace clic en el texto
},
child: const Text('Reenviar código'),
),
],
),
),
const Align(
alignment: Alignment.topLeft,
child: Padding(
padding: EdgeInsets.only(bottom: 5),
child: Text('Código',
textAlign: TextAlign.left,
style:
TextStyle(fontSize: 18.0, color: Color(0xFF65676B))),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: OtpTextField(
numberOfFields: 6,
focusedBorderColor: Colors.blue,
fillColor: Colors.black.withOpacity(0.1),
filled: true,
keyboardType: TextInputType.number,
onSubmit: (code) {
otp = code;
OTPController.instance.verifyOTP(otp);
},
),
),
Padding(
padding: const EdgeInsets.only(bottom: 40),
child: Center(
child: ElevatedButton(
onPressed: () {
OTPController.instance.verifyOTP(otp);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC), // Color del botón
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(50), // Bordes redondeados
),
elevation: 0,
minimumSize: const Size(230, 60), // Tamaño mínimo del botón
),
child: const Text(
'Valida el código',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
)),
),
]),
),
],
),
));
}
}
@@ -0,0 +1,160 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/components/primary_btn.dart';
class NewPasswordScreen extends StatefulWidget {
NewPasswordScreen({super.key});
@override
State<NewPasswordScreen> createState() => _NewPasswordScreenState();
}
class _NewPasswordScreenState extends State<NewPasswordScreen> {
final _currentPasswordController = TextEditingController();
final _newPasswordController = TextEditingController();
bool _obscureText = true;
bool _obscureText2 = true;
final FirebaseAuth _auth = FirebaseAuth.instance;
Future<void> updatePassword(
String currentPassword, String newPassword) async {
final User user = _auth.currentUser!;
final credential = EmailAuthProvider.credential(
email: user.email!,
password: currentPassword,
);
try {
if (newPassword == currentPassword) {
Get.snackbar(
'Misma contraseña',
'La nueva contraseña debe ser distinta a la contraseña actual.',
snackPosition: SnackPosition.BOTTOM,
);
return;
} else if (newPassword.isEmpty) {
Get.snackbar(
'Ingrese una contraseña valida',
'La nueva contraseña no puede estar vacia.',
snackPosition: SnackPosition.BOTTOM,
);
return;
} else {
await user.reauthenticateWithCredential(credential);
try {
await user.updatePassword(newPassword);
// Muestra un mensaje de éxito
Get.snackbar(
'Contraseña actualizada',
'Tu contraseña ha sido cambiada con éxito.',
snackPosition: SnackPosition.BOTTOM,
);
} catch (e) {
print("Error al verificar la contraseña actual: $e");
}
}
} catch (e) {
Get.snackbar(
'Contraseña incorrecta',
'Ha ocurrido un error al actualizar la contraseña. Asegúrate de ingresar correctamente la contraseña actual.',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
}
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: ' Cambia tu contraseña',
),
body: Center(
child: SizedBox(
width: 300,
child: Padding(
padding: const EdgeInsets.only(top: 20),
child: Column(
children: [
const Text(
"Ten en cuenta que al cambiar tu contraseña, se cerrará automáticamente tu sesión.",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 40),
TextFormField(
controller: _currentPasswordController,
obscureText: _obscureText,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscureText
? Icons.visibility
: Icons.visibility_off,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
hintText: 'Contraseña (Actual)'),
),
const SizedBox(height: 40),
TextFormField(
controller: _newPasswordController,
obscureText: _obscureText2,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscureText2
? Icons.visibility
: Icons.visibility_off,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscureText2 = !_obscureText2;
});
},
),
hintText: 'Contraseña (Nueva)'),
),
const SizedBox(height: 80),
PrimaryButtom(
onPressed: () {
updatePassword(_currentPasswordController.text.trim(),
_newPasswordController.text.trim());
},
label: 'Actualizar contraseña'),
const SizedBox(height: 30),
const Text(
"Esta contraseña es valida si el inicio de sesión es por email.",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
],
),
),
),
),
),
);
}
}
@@ -0,0 +1,249 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:diacritic/diacritic.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/presentation/screens/support.dart';
final CollectionReference professionsCollection =
FirebaseFirestore.instance.collection('professions');
Future<List<String>> getProfessions() async {
try {
DocumentSnapshot<Object?> profession =
await professionsCollection.doc('professions').get();
Map<String, dynamic> data = profession.data() as Map<String, dynamic>;
var professionsList = (data['professions'] as List<dynamic>)
.map((e) => e.toString())
.toList();
return professionsList;
} catch (e) {
print('$e');
}
return [];
}
class ProfessionScreen extends StatefulWidget {
const ProfessionScreen({super.key});
@override
State<ProfessionScreen> createState() => _ProfessionScreenState();
}
class _ProfessionScreenState extends State<ProfessionScreen> {
List<String>? filteredProfessions;
TextEditingController searchController = TextEditingController();
final User? user = FirebaseAuth.instance.currentUser;
List<String>? _professions;
final ScrollController _scrollController = ScrollController();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
bool isNewProfessionAdded = false;
@override
void initState() {
super.initState();
searchController.addListener(() {
setState(() {
if (_professions != null) {
if (searchController.text.isEmpty) {
filteredProfessions = _professions!;
} else {
filteredProfessions = _professions!
.where((profession) => removeDiacritics(profession)
.toLowerCase()
.contains(
removeDiacritics(searchController.text.toLowerCase())))
.toList();
}
}
});
});
if (_professions == null) {
getProfessions().then((List<String> element) => setState(() {
_professions = element;
filteredProfessions = element;
}));
}
}
Future<void> updateProfession(String profession) async {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'profesion': profession});
} catch (e) {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.set({'profesion': profession});
} catch (e) {
print('Error al agregar la profesion: $e');
}
print('Error al actualizar la profesion: $e');
}
}
Future<void> saveProfession(String newProfession) async {
final DocumentReference professionsDocRef =
professionsCollection.doc('professions');
try {
final DocumentSnapshot<Object?> profession =
await professionsDocRef.get();
Map<String, dynamic> data = profession.data() as Map<String, dynamic>;
List<String> professions = [];
if (data['professions'] != null) {
professions = List<String>.from(data['professions']);
}
professions.add(newProfession);
await professionsDocRef.set({
'professions': professions,
}, SetOptions(merge: true));
setState(() {
getProfessions().then((List<String> element) => setState(() {
_professions = element;
filteredProfessions = element;
int newIndex = professions.indexOf(newProfession);
if (newIndex != -1) {
_scrollController.animateTo(
newIndex * 50.0,
duration: const Duration(milliseconds: 600),
curve: Curves.easeIn,
);
}
isNewProfessionAdded = true;
Future.delayed(const Duration(seconds: 2), () {
setState(() {
isNewProfessionAdded = false;
});
});
}));
});
} catch (e) {
print('Error al guardar la profesión: $e');
}
}
@override
Widget build(BuildContext context) {
if (filteredProfessions == null) {
return const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF2BA4EC)),
),
);
}
var professions = filteredProfessions!;
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Seleccione su profesión',
),
body: Column(
children: [
GestureDetector(
onTap: () {
String newProfession = "";
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Agregar una profesión"),
content: TextField(
controller: TextEditingController(),
onChanged: (value) {
newProfession = value;
},
),
actions: [
ElevatedButton(
onPressed: () {
if (newProfession.isNotEmpty) {
saveProfession(newProfession);
Navigator.pop(context);
}
},
child: const Text("Guardar"),
),
],
);
},
);
},
child: Container(
padding: const EdgeInsets.all(10),
child: const Text(
'Si no vez tu profesion, presiona aquí',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.blue,
),
textAlign: TextAlign.center,
),
),
),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10, top: 0),
child: TextField(
controller: searchController,
decoration: const InputDecoration(
hintText: 'Busca tu profesión',
prefixIcon: Icon(Icons.assignment_ind_rounded),
),
),
),
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: professions.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
professions[index],
style: TextStyle(
fontSize: 18.0,
color: isNewProfessionAdded &&
index == professions.length - 1
? Colors.white
: Colors.black,
),
),
tileColor:
isNewProfessionAdded && index == professions.length - 1
? Colors.blue
: null,
onTap: () {
updateProfession(professions[index]);
Navigator.pop(context, professions[index]);
},
);
},
),
),
],
),
),
);
}
}
@@ -0,0 +1,429 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:diacritic/diacritic.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/photo_view.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/models/user_model.dart';
import 'package:prosappco/src/presentation/screens/professional_info.dart';
import '../../models/scores_model.dart';
class ProfessionalScreen extends StatefulWidget {
final String profession;
const ProfessionalScreen({super.key, required this.profession});
@override
State<ProfessionalScreen> createState() => _ProfessionalScreenState();
}
final FirebaseStorage storage = FirebaseStorage.instance;
final CollectionReference usersCollection =
FirebaseFirestore.instance.collection('users');
var _photo = '...';
class Professional {
final String id;
final Reference professionalRef;
final String name;
final String professionName;
final String cityName;
final String ubicacion;
final String realAddress;
final double latitude;
final double longitude;
final List<String> professionalEspecializado;
final ScoresModel scores;
final int? tarifa;
final String? token;
Professional({
required this.id,
required this.professionalRef,
required this.name,
required this.professionName,
required this.cityName,
required this.ubicacion,
required this.professionalEspecializado,
required this.scores,
required this.realAddress,
required this.latitude,
required this.longitude,
this.tarifa,
this.token,
});
String getEspecializaciones() {
return professionalEspecializado.join(',\n');
}
@override
String toString() {
return 'Professional {\n'
' professionalRef: $professionalRef,\n'
' name: $name,\n'
' professionName: $professionName,\n'
' cityName: $cityName,\n'
' ubicacion: $ubicacion,\n'
' realAddress: $realAddress,\n'
' latitude: $latitude,\n'
' longitude: $longitude,\n'
' professionalEspecializado: ${getEspecializaciones()}\n'
' tarifa: $tarifa,\n'
' token: $token,\n'
'}';
}
static Future<Professional?> getProfessional(String uid) async {
var photo = '...';
final FirebaseStorage storage = FirebaseStorage.instance;
try {
DocumentSnapshot user =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
Map<String, dynamic> data = user.data() as Map<String, dynamic>;
photo = await AuthenticationRepository.instance.getPhoto(user.id);
if (data['estado'] == 'activo') {
List<String> especializaciones;
especializaciones = (data['especializaciones'] as List<dynamic>)
.map((e) => e.toString())
.toList();
Professional professional = Professional(
id: user.id,
name: data['name'],
professionName: data['profesion'],
cityName: data['city'],
professionalRef: storage.ref().child(photo),
professionalEspecializado: especializaciones,
ubicacion: data['ubicacion'] ?? '',
realAddress: data['address'] ?? '',
latitude: data['latitude'] ?? 0,
longitude: data['longitude'] ?? 0,
scores: await ScoresModel.scoreFrom(uid, true, true),
tarifa: data['tarifa'] ?? 0,
token: data['token'] ?? '',
);
return professional;
}
} catch (e) {
print('Error al obtener profesionales: $e');
}
return null;
}
}
class _ProfessionalScreenState extends State<ProfessionalScreen> {
UserModel? userme;
SettingModel? settings;
@override
void initState() {
super.initState();
if (userme == null) {
UserModel.getUser(uid.toString()).then(
(UserModel s) => setState(() => userme = s),
);
}
if (settings == null) {
SettingModel.getSettings().then(
(SettingModel value) => setState(() {
settings = value;
}),
);
}
searchController.addListener(() {
setState(() {
if (_professionals != null) {
if (searchController.text.isEmpty) {
filteredProfessionals = _professionals!
.where((professional) => professional.id != uid)
.toList();
} else {
filteredProfessionals = _professionals!
.where((professional) =>
removeDiacritics(professional.name).toLowerCase().contains(
removeDiacritics(
searchController.text.toLowerCase())) &&
professional.id != uid)
.toList();
}
}
});
});
if (_professionals == null) {
getProfessionals().then((List<Professional> element) => setState(() {
_professionals = element;
filteredProfessionals = element;
}));
}
}
String formatCurrency(int number) {
final formatter =
NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: '');
return '\$${formatter.format(number)}';
}
Future<String?> _showChoiceDialog(BuildContext context) async {
String? selectedOption = await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: SingleChildScrollView(
child: ListBody(
children: [
GestureDetector(
child: const Text(
textAlign: TextAlign.center,
"A domicilio",
style: TextStyle(color: Color(0xFF2BA4EC)),
),
onTap: () {
Navigator.of(context).pop("domicilio");
},
),
const Divider(color: Colors.black54),
GestureDetector(
child: const Text(
textAlign: TextAlign.center,
"En sitio",
style: TextStyle(color: Color(0xFF2BA4EC)),
),
onTap: () {
Navigator.of(context).pop("sitio");
},
),
],
),
),
);
},
);
return selectedOption;
}
List<Professional>? filteredProfessionals;
TextEditingController searchController = TextEditingController();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
List<Professional>? _professionals;
Future<List<Professional>> getProfessionals() async {
List<Professional> professionals = [];
try {
QuerySnapshot users = await usersCollection.get();
for (DocumentSnapshot user in users.docs) {
Map<String, dynamic> data = user.data() as Map<String, dynamic>;
_photo = await AuthenticationRepository.instance.getPhoto(user.id);
if (data['estado'] == 'activo') {
if (user.id != uid) {
List<String> especializaciones;
especializaciones = (data['especializaciones'] as List<dynamic>)
.map((e) => e.toString())
.toList();
if (widget.profession == data['profesion'] ||
widget.profession == '' &&
userme?.city == data['city'] &&
data['ubicacion'] != null) {
Professional professional = Professional(
id: user.id,
name: data['name'],
professionName: data['profesion'],
cityName: data['city'],
professionalRef: storage.ref().child(_photo),
professionalEspecializado: especializaciones,
ubicacion: data['ubicacion'] ?? '',
realAddress: data['address'] ?? '',
latitude: data['latitude'] ?? 0,
longitude: data['longitude'] ?? 0,
scores: await ScoresModel.scoreTo(user.id, true, true),
tarifa: data['tarifa'] ?? 0,
token: data['token'] ?? '',
);
professionals.add(professional);
}
}
}
}
} catch (e) {
print('Error al obtener profesionales: $e');
}
return professionals;
}
@override
Widget build(BuildContext context) {
if (filteredProfessionals == null) {
return const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF2BA4EC)),
),
);
}
var professionals = filteredProfessionals!;
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Seleccione un profesional'),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: TextField(
controller: searchController,
decoration: const InputDecoration(
hintText: 'Escriba un nombre',
prefixIcon: Icon(Icons.assignment_ind_rounded),
),
),
),
Expanded(
child: ListView.builder(
itemCount: professionals.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: GestureDetector(
onTap: () {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (BuildContext context) {
return ProfessionalInfoScreen(
professional: professionals[index],
);
},
),
);
},
child: ReferencePhoto(
ref: professionals[index].professionalRef,
sizeCircle: 50,
size: 50,
sizeIcon: 35,
),
),
trailing: GestureDetector(
child: const Icon(Icons.keyboard_arrow_right),
onTap: () {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (BuildContext context) {
return ProfessionalInfoScreen(
professional: professionals[index],
);
},
),
);
},
),
title: RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 15.0,
color: Colors.black,
),
children: <TextSpan>[
TextSpan(
text: '${professionals[index].name}, ',
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(
text:
"${professionals[index].professionName}, ${professionals[index].cityName}",
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
settings?.tarifas == true
? professionals[index].tarifa == 0
? const SizedBox()
: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
color: const Color(0xFFD6F4FF),
),
child: Text(
formatCurrency(
professionals[index].tarifa ?? 0),
style: TextStyle(
color: Colors.grey[850],
fontWeight: FontWeight.w600,
),
),
)
: const SizedBox(),
professionals[index].ubicacion == 'ambos'
? Text(
'Disponibilidad a domicilio y en sitio ${professionals[index].tarifa}',
style: const TextStyle(
color: Colors.blue,
),
)
: professionals[index].ubicacion == 'sitio'
? Text(
'Disponibilidad en ${professionals[index].ubicacion} ',
style: const TextStyle(
color: Colors.blue,
),
)
: Text(
'Disponibilidad a ${professionals[index].ubicacion}',
style: const TextStyle(
color: Colors.blue,
),
),
],
),
onTap: () {
if (professionals[index].ubicacion == 'ambos') {
_showChoiceDialog(context).then((String? value) {
if (value != null) {
Navigator.pop(
context, [professionals[index], value]);
}
});
} else if (professionals[index].ubicacion == 'sitio') {
Navigator.pop(context, [professionals[index], 'sitio']);
} else {
Navigator.pop(
context, [professionals[index], 'domicilio']);
}
},
);
},
),
),
],
),
),
);
}
}
@@ -0,0 +1,255 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/components/primary_btn.dart';
class ProfessionalDireccionScreen extends StatefulWidget {
const ProfessionalDireccionScreen({super.key});
@override
State<ProfessionalDireccionScreen> createState() =>
_ProfessionalDireccionScreenState();
}
class _ProfessionalDireccionScreenState
extends State<ProfessionalDireccionScreen> {
final TextEditingController _locationController = TextEditingController();
final String _locationPosition = '';
final uid = AuthenticationRepository.instance.getCurrentUserUid();
late GoogleMapController googleMapController;
static const CameraPosition initialCameraPosition = CameraPosition(
target: LatLng(7.8939100, -72.5078200),
zoom: 14.4746,
);
Set<Marker> markers = {};
Future<Position> _determinePosition() async {
bool serviceEnabled;
LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return Future.error('Location services are disabled');
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return Future.error('Location permission denied');
}
}
if (permission == LocationPermission.deniedForever) {
return Future.error('Location permissions are permanently denied');
}
Position position = await Geolocator.getCurrentPosition();
return position;
}
@override
void initState() {
super.initState();
}
late String lat;
late String long;
var coordinates;
Future<String> getLocationName(double latitude, double longitude) async {
String address;
List<Placemark> placemarks =
await placemarkFromCoordinates(latitude, longitude);
Placemark place = placemarks[0];
if (place.thoroughfare != '' || place.subThoroughfare != '') {
address =
"${place.thoroughfare} ${place.subThoroughfare} ${place.subLocality}, ${place.locality}, ${place.administrativeArea}";
} else {
address = '';
}
return address;
}
Future<void> updateAddress(
String addressName, double latitude, double longitude) async {
try {
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'address': addressName,
'latitude': latitude,
'longitude': longitude,
});
} catch (e) {
try {
await FirebaseFirestore.instance.collection('users').doc(uid).set({
'address': addressName,
'latitude': latitude,
'longitude': longitude,
});
} catch (e) {
print('Error al agregar la ciudad: $e');
}
print('Error al actualizar la ciudad: $e');
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Ubicación'),
backgroundColor: const Color(0xFFD6F4FF),
body: Stack(
children: [
GoogleMap(
mapType: MapType.normal,
initialCameraPosition: initialCameraPosition,
markers: markers,
zoomControlsEnabled: false,
onMapCreated: (GoogleMapController controller) {
googleMapController = controller;
},
onCameraIdle: () {
if (coordinates != null) {
getLocationName(coordinates.latitude, coordinates.longitude)
.then((locationName) {
setState(() {
_locationController.text = locationName;
});
});
}
},
onCameraMove: (position) {
setState(() {
coordinates = position.target;
});
},
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<OneSequenceGestureRecognizer>(
() => EagerGestureRecognizer(),
),
},
),
Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: Padding(
padding: const EdgeInsets.only(
left: 35, right: 35, bottom: 15, top: 5),
child: TextFormField(
controller: _locationController,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.near_me),
hintText: 'Dirección',
),
),
),
),
const Positioned(
bottom: 10,
right: 0,
left: 0,
top: 0,
child: Icon(
Icons.location_on,
size: 40,
color: Colors.red,
),
),
Positioned(
bottom: 130,
right: 20,
child: FloatingActionButton(
onPressed: () async {
try {
Position position = await _determinePosition();
googleMapController.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(
position.latitude,
position.longitude,
),
zoom: 17),
),
);
setState(() {});
} catch (e) {
Get.snackbar(
'Ubicación desactivada',
'Por favor activa la ubicacion de tu telefono.',
snackPosition: SnackPosition.TOP,
);
}
// markers.clear();
// markers.add(Marker(
// markerId: const MarkerId('currentLocation'),
// position:
// LatLng(position.latitude, position.longitude)));
},
elevation: 0,
child: const Icon(
Icons.gps_fixed,
size: 30,
),
),
),
Positioned(
bottom: 30,
left: 0,
right: 0,
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Align(
alignment: Alignment.center,
child: PrimaryButtom(
onPressed: () {
updateAddress(
_locationController.text,
coordinates.latitude,
coordinates.longitude,
);
Navigator.pop(context, _locationController.text);
},
label: 'Guardar'),
),
),
)
],
),
),
);
}
}
@@ -0,0 +1,302 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:intl/intl.dart';
import 'package:prosappco/src/components/photo_view.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/presentation/screens/professional.dart';
import 'package:prosappco/src/presentation/screens/reputation.dart';
import '../../models/scores_model.dart';
class ProfessionalInfoScreen extends StatefulWidget {
Professional professional;
ProfessionalInfoScreen({super.key, required this.professional});
@override
State<ProfessionalInfoScreen> createState() => _ProfessionalInfoScreenState();
}
class _ProfessionalInfoScreenState extends State<ProfessionalInfoScreen> {
SettingModel? settings;
String formatCurrency(int number) {
final formatter =
NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: '');
return '\$${formatter.format(number)}';
}
@override
void initState() {
super.initState();
if (settings == null) {
SettingModel.getSettings().then(
(SettingModel value) => setState(() {
settings = value;
}),
);
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: widget.professional.name),
body: SingleChildScrollView(
child: Stack(
children: [
Column(
children: [
Container(
width: double.infinity,
height: 140,
decoration: BoxDecoration(
color: const Color(0xFFD6F4FF),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 7,
offset: const Offset(0, 2),
),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 50),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 5,
),
Text(
widget.professional.name,
style: const TextStyle(fontWeight: FontWeight.w500),
),
Text(
widget.professional.professionName,
style: const TextStyle(color: Color(0xFF1688C9)),
),
widget.professional.getEspecializaciones().isEmpty
? const SizedBox()
: Text(
'Especializado/a en ${widget.professional.getEspecializaciones()}',
style: const TextStyle(color: Colors.black54),
),
Text(
widget.professional.cityName,
),
],
),
),
widget.professional.ubicacion == 'domicilio'
? Container(
margin: const EdgeInsets.only(
left: 40, right: 40, top: 20, bottom: 20),
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 15),
decoration: BoxDecoration(
color: const Color(0xFFD6F4FF),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(1, 3),
),
],
),
child: const Row(
children: [
Icon(
Icons.error_outline,
size: 27,
color: Colors.black54,
),
SizedBox(width: 15),
Text(
'Este profesional solo atiende en\nsu dirección de trabajo.',
style: TextStyle(
color: Colors.black, fontSize: 13),
),
],
),
)
: const SizedBox(),
const SizedBox(height: 10),
settings?.tarifas == true
? RichText(
text: TextSpan(
children: [
const TextSpan(
text: 'Tarifa consulta ',
style: TextStyle(
color: Colors.black,
),
),
TextSpan(
text: formatCurrency(
widget.professional.tarifa ?? 0),
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
)
: const SizedBox(),
const SizedBox(height: 10),
const Text(
'Se unió el 02 de abril del 2023',
style: TextStyle(fontSize: 12),
),
const SizedBox(height: 15),
Container(
decoration: BoxDecoration(
color: const Color(0xFFD6F4FF),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 3,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
),
child: ListTile(
onTap: () {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (BuildContext context) {
return const ReputationScreen();
},
),
);
},
trailing: const Icon(Icons.keyboard_arrow_right,
color: Colors.black),
title: const Text(
'Reputación',
style: TextStyle(color: Colors.black),
),
subtitle: Row(
children: [
RatingBar.builder(
initialRating: widget.professional.scores.average,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 25,
maxRating: 5,
itemPadding:
const EdgeInsets.symmetric(horizontal: 0),
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC),
),
onRatingUpdate: (rating) {},
ignoreGestures: true,
),
const SizedBox(width: 5),
Text(
'(${widget.professional.scores.total.toString()}) ${widget.professional.scores.average.toStringAsFixed(1)}'),
],
),
),
),
..._scoresList(widget.professional.scores.details),
const SizedBox(
height: 10,
),
],
),
Container(
padding: const EdgeInsets.only(
top: 110,
left: 10,
),
child: ReferencePhoto(
ref: widget.professional.professionalRef,
size: 100,
sizeCircle: 100,
sizeIcon: 50,
),
)
],
),
),
),
);
}
List<Widget> _scoresList(List<ScoreDetailModel> list) {
return list.map((e) => _scoreItem(e)).toList();
}
Widget _scoreItem(ScoreDetailModel scoreDetails) {
return ListTile(
onTap: () {},
leading: ReferencePhoto(
ref: scoreDetails.avatar,
size: 50,
sizeCircle: 50,
sizeIcon: 35,
),
title: Row(
children: [
RatingBar.builder(
initialRating: scoreDetails.score,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 22,
maxRating: 5,
itemPadding: const EdgeInsets.symmetric(horizontal: 0),
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC),
),
onRatingUpdate: (rating) {},
ignoreGestures: true,
),
Text(
' (${scoreDetails.score})',
style: const TextStyle(color: Colors.black54, fontSize: 13),
)
],
),
subtitle: Row(
children: [
Expanded(
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: '${scoreDetails.name}, ',
style: const TextStyle(fontSize: 15, color: Colors.black),
),
TextSpan(
text: '"${scoreDetails.comment}"',
style: const TextStyle(fontSize: 15, color: Colors.grey),
),
],
),
),
),
],
));
}
}
@@ -0,0 +1,828 @@
import 'dart:io';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/photo_view.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/controllers/info_%20professional.dart';
import 'package:prosappco/src/services/select_image_profile.dart';
import 'package:file_picker/file_picker.dart';
class ProfessionalProfileScreen extends StatefulWidget {
const ProfessionalProfileScreen({super.key});
@override
State<ProfessionalProfileScreen> createState() =>
ProfessionalProfileScreenState();
}
class ProfessionalProfileScreenState extends State<ProfessionalProfileScreen> {
File? imagen_to_upload;
File? image_cedula;
File? image_certificado;
late final FirebaseAuth _auth;
final FirebaseStorage storage = FirebaseStorage.instance;
final uid = AuthenticationRepository.instance.getCurrentUserUid();
final _formKey = GlobalKey<FormState>();
final controller = Get.put(InforProfessionalController());
final _cedulaController = TextEditingController();
final _especializacionController = TextEditingController();
List<File> images_especializacion = [];
var _profession = '...';
var photoTemp = '';
var photoCedulaTemp = '';
var photoCertificadoTemp = '';
var _photo = '...';
@override
void initState() {
super.initState();
_auth = FirebaseAuth.instance;
if (_photo == '...') {
AuthenticationRepository.instance
.getPhoto(uid.toString())
.then((String s) => setState(() {
_photo = s;
}));
}
if (_profession == '...') {
AuthenticationRepository.instance
.getProfession(uid.toString())
.then((String s) => setState(() {
_profession = s;
}));
}
}
Future<File?> getPdf() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf'],
);
if (result != null) {
File file = File(result.files.single.path!);
return file;
} else {
return null;
}
}
Future<void> _showChoiceDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: SingleChildScrollView(
child: ListBody(
children: [
GestureDetector(
child: const Text(
textAlign: TextAlign.center,
"Tomar foto",
style: TextStyle(color: Color(0xFF2BA4EC)),
),
onTap: () async {
final imagen = await getImage(1);
setState(() {
imagen_to_upload = File(imagen[0]!.path);
});
Navigator.of(context).pop();
},
),
const Divider(color: Colors.black54),
GestureDetector(
child: const Text(
textAlign: TextAlign.center,
"Abrir Galería",
style: TextStyle(color: Color(0xFF2BA4EC)),
),
onTap: () async {
final imagen = await getImage(2);
setState(() {
imagen_to_upload = File(imagen[0]!.path);
});
Navigator.of(context).pop();
},
),
],
),
),
);
},
);
}
Future<void> _showChoiceDialogCedula(BuildContext context) async {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: SingleChildScrollView(
child: ListBody(
children: [
GestureDetector(
child: const Text(
textAlign: TextAlign.center,
"Abrir Galería",
style: TextStyle(color: Color(0xFF2BA4EC)),
),
onTap: () async {
final imagen = await getPdf();
setState(() {
image_cedula = File(imagen!.path);
});
Navigator.of(context).pop();
},
),
],
),
),
);
},
);
}
Future<void> _showChoiceDialogCertificado(BuildContext context) async {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: SingleChildScrollView(
child: ListBody(
children: [
GestureDetector(
child: const Text(
textAlign: TextAlign.center,
"Abrir Galería",
style: TextStyle(color: Color(0xFF2BA4EC)),
),
onTap: () async {
final imagen = await getPdf();
setState(() {
image_certificado = File(imagen!.path);
});
Navigator.of(context).pop();
},
),
],
),
),
);
},
);
}
Future<bool> uploadCedula(File image) async {
final now = DateTime.now();
final formattedDate = DateFormat('HHmmssddMMyyyy').format(now);
final milliseconds = (now.microsecondsSinceEpoch / 1000).round();
final random = 'c$formattedDate$milliseconds';
Reference ref =
storage.ref().child('users').child(uid!).child('cedula').child(random);
final UploadTask uploadTask = ref.putFile(image);
final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true);
photoCedulaTemp = ref.fullPath;
if (snapshot.state == TaskState.success) {
updateImageCedula(photoCedulaTemp);
return true;
} else {
return false;
}
}
Future<void> updateImageCedula(image) async {
try {
final userRef = FirebaseFirestore.instance.collection('users').doc(uid);
final userSnapshot = await userRef.get();
if (userSnapshot.exists) {
await userRef.update({'imgCedula': image});
} else {
await userRef.set({'imgCedula': image});
}
} catch (e) {
print('Error al agregar o actualizar la imagen de cédula: $e');
}
}
Future<bool> uploadCertificado(File image) async {
final now = DateTime.now();
final formattedDate = DateFormat('HHmmssddMMyyyy').format(now);
final milliseconds = (now.microsecondsSinceEpoch / 1000).round();
final random = 'f$formattedDate$milliseconds';
Reference ref = storage
.ref()
.child('users')
.child(uid!)
.child('certificado_profesional')
.child(random);
final UploadTask uploadTask = ref.putFile(image);
final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true);
photoCertificadoTemp = ref.fullPath;
if (snapshot.state == TaskState.success) {
updateImageCertificado(photoCertificadoTemp);
return true;
} else {
return false;
}
}
Future<void> updateImageCertificado(image) async {
try {
final userRef = FirebaseFirestore.instance.collection('users').doc(uid);
final userSnapshot = await userRef.get();
if (userSnapshot.exists) {
await userRef.update({'imgCertificado': image});
} else {
await userRef.set({'imgCertificado': image});
}
} catch (e) {
print('Error al agregar o actualizar la imagen de certificado: $e');
}
}
Future<void> updateImage(image) async {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'photo': image});
} catch (e) {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.set({'photo': image});
} catch (e) {
print('Error al agregar la imagen de perfil: $e');
}
print('Error al actualizar la imagen de perfil: $e');
}
}
Future<bool> uploadImage(File image) async {
final String namefile = image.path.split('/').last;
Reference ref = storage
.ref()
.child('users')
.child(uid!)
.child('profile')
.child(namefile);
final UploadTask uploadTask = ref.putFile(image);
final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true);
photoTemp = ref.fullPath;
if (snapshot.state == TaskState.success) {
return true;
} else {
return false;
}
}
Future<void> _showChoiceDialogEspecializaciones(BuildContext context) async {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: SingleChildScrollView(
child: ListBody(
children: [
GestureDetector(
child: const Text(
textAlign: TextAlign.center,
"Abrir Galería",
style: TextStyle(color: Color(0xFF2BA4EC)),
),
onTap: () async {
final List<File>? images = await getPdfs();
if (images != null) {
setState(() {
images_especializacion = images;
});
}
Navigator.of(context).pop();
},
),
],
),
),
);
},
);
}
Future<List<File>?> getPdfs() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf'],
allowMultiple: true,
);
if (result != null) {
List<File> files = result.files.map((file) => File(file.path!)).toList();
return files;
} else {
return null;
}
}
Future<List<String>> uploadEspecializaciones(List<File> images) async {
List<String> photoPaths = [];
for (File image in images) {
Reference ref = storage
.ref()
.child('users')
.child(uid!)
.child('especializaciones')
.child('e${DateTime.now().millisecondsSinceEpoch}.pdf');
final UploadTask uploadTask = ref.putFile(image);
final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true);
if (snapshot.state == TaskState.success) {
photoPaths.add(ref.fullPath);
} else {
updateImagesEspecializaciones(photoPaths);
return [];
}
}
updateImagesEspecializaciones(photoPaths);
return photoPaths;
}
Future<void> updateImagesEspecializaciones(List<String> photoPaths) async {
try {
final userRef = FirebaseFirestore.instance.collection('users').doc(uid);
final userSnapshot = await userRef.get();
if (userSnapshot.exists) {
await userRef.update({'imgEspecializaciones': photoPaths});
} else {
await userRef.set({'imgEspecializaciones': photoPaths});
}
} catch (e) {
print(
'Error al agregar o actualizar las imágenes de especializaciones: $e');
}
}
void _showCustomSnackBar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Container(
height: 50,
child: Center(
child: Text(
message,
style: TextStyle(fontSize: 18),
),
),
),
duration: Duration(seconds: 3),
backgroundColor: Colors.red, // Personaliza el color de fondo
behavior: SnackBarBehavior.floating,
),
);
}
Future<void> sendInfo() async {
final String cedula = _cedulaController.text.trim();
final String especializacion = _especializacionController.text.trim();
final List<String> especializaciones =
especializacion.split(',').map((e) => e.trim()).toList();
if (cedula.isEmpty) {
_showCustomSnackBar(
context, 'Por favor, adjunta el documento PDF de tu cédula.');
return;
}
if (image_cedula == null) {
_showCustomSnackBar(
context, 'Por favor, adjunte el documento PDF de su cédula.');
return;
}
if (image_certificado == null) {
_showCustomSnackBar(context,
'Por favor, adjunta el documento PDF de tu certificado. ¡Gracias por tu colaboración!');
return;
}
// Actualiza los datos del usuario en Firestore
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'cedula': cedula,
'estado': 'revision',
'especializaciones': especializaciones
});
// Sube las imágenes al storage de Firebase
uploadCedula(image_cedula!);
uploadCertificado(image_certificado!);
uploadEspecializaciones(images_especializacion);
// Actualiza la imagen de perfil si hay cambios
if (imagen_to_upload != null) {
updateImage(photoTemp);
}
// Navega a la siguiente pantalla
Navigator.pushReplacementNamed(context, '/solicitudEnviada');
}
void showSnackBar(String message) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)));
}
Future<Widget> downloadImage(Reference ref) async {
try {
if (_photo == '...' || _photo.isEmpty) {
return GestureDetector(
onTap: () {
_showChoiceDialog(context);
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 50),
width: 100,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF2BA4EC),
borderRadius: BorderRadius.circular(50),
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 90,
),
),
);
} else {
final imageData = await ref.getData();
if (imageData != null) {
// final widgetImage = Image.memory(imageData);
final widgetImage = GestureDetector(
onTap: () {
_showChoiceDialog(context);
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 50),
child: ClipOval(
child: Image.memory(
imageData,
width: 60,
height: 60,
fit: BoxFit.cover,
),
),
),
);
return widgetImage;
} else {
return GestureDetector(
onTap: () {
_showChoiceDialog(context);
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 50),
width: 100,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF2BA4EC),
borderRadius: BorderRadius.circular(50),
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 90,
),
),
);
}
}
} catch (e) {
print('$e');
return GestureDetector(
onTap: () {
_showChoiceDialog(context);
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 50),
width: 100,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF2BA4EC),
borderRadius: BorderRadius.circular(50),
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 90,
),
),
);
}
}
@override
Widget build(BuildContext context) {
String profession = _profession.toString();
double _space = 10;
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Perfil profesional',
),
body: SingleChildScrollView(
reverse: true,
child: Center(
child: Column(
children: [
GestureDetector(
onTap: () {
_showChoiceDialog(context);
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 25),
child: (imagen_to_upload != null)
? LocalPhoto(
file: imagen_to_upload!,
)
: ReferencePhoto(
ref: storage.ref().child(_photo),
size: 100,
sizeCircle: 100,
),
),
),
Container(
width: 300,
padding: const EdgeInsets.only(top: 0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
keyboardType: TextInputType.number,
controller: _cedulaController,
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Ingrese una cedula válida';
}
return null;
},
decoration: const InputDecoration(
prefixIcon: Icon(Icons.person_outline),
hintText: 'Cedula (Obligatorio)'),
),
SizedBox(height: _space),
ElevatedButton(
onPressed: () {
_showChoiceDialogCedula(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFD6F4FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(250, 50),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Cedula',
style: TextStyle(
color: Color(0xFF2BA4EC),
fontSize: 17,
),
),
const SizedBox(width: 15),
Icon(
image_cedula != null
? Icons.check
: Icons.file_upload_outlined,
color: const Color(0xFF2BA4EC),
size: 30,
),
],
),
),
SizedBox(height: _space),
TextFormField(
readOnly: true,
onTap: () async {
final String? profesion = (await Navigator.pushNamed(
context, '/profession')) as String?;
if (profesion != null) {
setState(() {
_profession = profesion;
});
}
},
decoration: InputDecoration(
prefixIcon:
const Icon(Icons.assignment_ind_rounded),
suffixIcon: const Icon(Icons.arrow_drop_down),
hintStyle: profession == ''
? const TextStyle()
: const TextStyle(color: Colors.black87),
hintText: profession == ''
? 'Profesión (Obligatorio)'
: profession),
),
SizedBox(height: _space),
ElevatedButton(
onPressed: () {
_showChoiceDialogCertificado(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFD6F4FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(250, 50),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Certificado profesional',
style: TextStyle(
color: Color(0xFF2BA4EC),
fontSize: 17,
),
),
const SizedBox(width: 15),
Icon(
image_certificado != null
? Icons.check
: Icons.file_upload_outlined,
color: const Color(0xFF2BA4EC),
size: 30,
),
],
),
),
SizedBox(height: _space),
TextFormField(
controller: _especializacionController,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.assignment_ind_rounded),
hintText: 'Especialización'),
),
SizedBox(height: _space),
ElevatedButton(
onPressed: () async {
_showChoiceDialogEspecializaciones(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFD6F4FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(250, 50),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Especialización',
style: TextStyle(
color: Color(0xFF2BA4EC),
fontSize: 17,
),
),
const SizedBox(width: 15),
Icon(
images_especializacion.isEmpty
? Icons.file_upload_outlined
: Icons.check,
color: const Color(0xFF2BA4EC),
size: 30,
),
],
),
),
],
),
),
),
Container(
margin: const EdgeInsets.only(
left: 40, right: 40, top: 40, bottom: 0),
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
decoration: BoxDecoration(
color: const Color(0xFFD6F4FF),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(1, 3),
),
],
),
child: const Row(
children: [
Icon(
Icons.error_outline,
size: 27,
color: Colors.black54,
),
SizedBox(width: 15),
Expanded(
child: Text(
'Si tienes más de una especialidad, por favor, adjunta un archivo con el diploma de respaldo para cada una de ellas y sepáralos por comas. ¡Gracias!',
style: TextStyle(color: Colors.black, fontSize: 14),
),
)
],
),
),
Container(
alignment: Alignment.bottomCenter,
margin: const EdgeInsets.only(
top: 80, right: 20, left: 20, bottom: 30),
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
sendInfo();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(250, 50),
maximumSize: const Size(350, 50),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Enviar información',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 17,
),
),
SizedBox(width: 15),
Icon(
Icons.send,
color: Colors.white,
size: 20,
),
],
),
),
),
],
),
),
),
));
}
}
@@ -0,0 +1,705 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:file_picker/file_picker.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/photo_view_web.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:intl/intl.dart';
import 'dart:io';
class ProfessionalProfileWebScreen extends StatefulWidget {
const ProfessionalProfileWebScreen({super.key});
@override
State<ProfessionalProfileWebScreen> createState() =>
_ProfessionalProfileWebScreenState();
}
class _ProfessionalProfileWebScreenState
extends State<ProfessionalProfileWebScreen> {
final uid = AuthenticationRepository.instance.getCurrentUserUid();
final FirebaseStorage storage = FirebaseStorage.instance;
// variables imagen
String selectedImage = '';
String selectedCedulaImage = '';
String selectedCertificadoImage = '';
List selectedEspecializacionImages = [];
List<Uint8List> imagesEspecializacionsBytes = [];
XFile? file;
Uint8List? selectedImagInBytes;
XFile? image_cedula;
Uint8List? imageCedulaBytes;
XFile? image_certificado;
Uint8List? imageCertificadoBytes;
XFile? image_especializaciones;
Uint8List? imageEspecializacionesBytes;
// controllers
final _formKey = GlobalKey<FormState>();
final TextEditingController _cedulaController = TextEditingController();
final TextEditingController _especializacionController =
TextEditingController();
// variables
bool _isLoading = false;
String _profession = '...';
String photoTemp = '';
String photoCedulaTemp = '';
String photoCertificadoTemp = '';
String _photo = '...';
@override
void initState() {
super.initState();
if (_photo == '...') {
AuthenticationRepository.instance.getPhoto(uid.toString()).then(
(String s) => setState(() {
_photo = s;
}),
);
}
if (_profession == '...') {
AuthenticationRepository.instance.getProfession(uid.toString()).then(
(String s) => setState(() {
_profession = s;
}),
);
}
}
_selectFile(bool imageFrom) async {
FilePickerResult? fileResult = await FilePicker.platform.pickFiles();
if (fileResult != null) {
setState(() {
selectedImage = fileResult.files.first.name;
selectedImagInBytes = fileResult.files.first.bytes;
});
}
}
_selectFileCedula(bool imageFrom) async {
FilePickerResult? fileResult = await FilePicker.platform.pickFiles();
if (fileResult != null) {
setState(() {
selectedCedulaImage = fileResult.files.first.name;
imageCedulaBytes = fileResult.files.first.bytes;
});
}
}
_selectFileCertificado(bool imageFrom) async {
FilePickerResult? fileResult = await FilePicker.platform.pickFiles();
if (fileResult != null) {
setState(() {
selectedCertificadoImage = fileResult.files.first.name;
imageCertificadoBytes = fileResult.files.first.bytes;
});
}
}
_selectFilesEspecializaciones(bool imageFrom) async {
FilePickerResult? fileResult =
await FilePicker.platform.pickFiles(allowMultiple: true);
try {
if (fileResult != null) {
List<Uint8List> selectedFileBytes = [];
for (var file in fileResult.files) {
Uint8List? bytes = file.bytes;
if (bytes != null) {
selectedFileBytes.add(bytes);
}
}
setState(() {
imagesEspecializacionsBytes = selectedFileBytes;
});
}
} catch (e) {
print('$e');
}
}
Future<void> sendInfo() async {
setState(() {
_isLoading = true;
});
final String cedula = _cedulaController.text.trim();
final String especializacion = _especializacionController.text.trim();
final List<String> especializaciones =
especializacion.split(',').map((e) => e.trim()).toList();
if (cedula.isEmpty) {
showSnackBar('Cedula invalida', 'Ingrese una cedula válida');
return;
}
if (imageCedulaBytes == null) {
showSnackBar('Cedula', 'Ingrese una imagen de su cedula');
return;
}
if (imageCertificadoBytes == null) {
showSnackBar('Certificado', 'Ingrese una imagen de su certificado');
return;
}
// Actualiza los datos del usuario en Firestore
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'cedula': cedula,
'estado': 'revision',
'especializaciones': especializaciones
});
// Sube las imágenes al storage de Firebase
await uploadCedula();
await uploadCertificado();
List<String> uploadedPhotoPaths =
await uploadEspecializaciones(imagesEspecializacionsBytes);
if (uploadedPhotoPaths.isNotEmpty) {
// Las imágenes se cargaron correctamente
// Actualiza las imágenes en Firestore
await updateFilesEspecializaciones(uploadedPhotoPaths);
// Navega a la siguiente pantalla
Navigator.pushReplacementNamed(context, '/solicitudEnviada');
} else {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Error'),
content: const Text('Ocurrió un error al cargar las imágenes.'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Aceptar'),
),
],
);
},
);
}
// Actualiza la imagen de perfil si hay cambios
if (selectedImagInBytes != null) {
await uploadFile();
await updateImage(photoTemp);
}
setState(() {
_isLoading = false;
});
// Navega a la siguiente pantalla
Navigator.pushReplacementNamed(context, '/solicitudEnviada');
}
uploadFile() async {
try {
final now = DateTime.now();
final formattedDate = DateFormat('HHmmssddMMyyyy').format(now);
final milliseconds = (now.microsecondsSinceEpoch / 1000).round();
final random = '$formattedDate$milliseconds';
final Reference ref = FirebaseStorage.instance
.ref()
.child('users')
.child(uid!)
.child('profile')
.child(random);
final metaData = SettableMetadata(contentType: 'image/jpeg');
final UploadTask uploadTask = ref.putData(selectedImagInBytes!, metaData);
final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true);
photoTemp = ref.fullPath;
if (snapshot.state == TaskState.success) {
return true;
} else {
return false;
}
} catch (e) {
print('web image error - $e');
}
}
uploadCedula() async {
try {
final now = DateTime.now();
final formattedDate = DateFormat('HHmmssddMMyyyy').format(now);
final milliseconds = (now.microsecondsSinceEpoch / 1000).round();
final random = 'c$formattedDate$milliseconds';
final Reference ref = FirebaseStorage.instance
.ref()
.child('users')
.child(uid!)
.child('cedula')
.child(random);
final metaData = SettableMetadata(contentType: 'application/pdf');
final UploadTask uploadTask = ref.putData(imageCedulaBytes!, metaData);
final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true);
photoCedulaTemp = ref.fullPath;
if (snapshot.state == TaskState.success) {
updateImageCedula(photoCedulaTemp);
return true;
} else {
return false;
}
} catch (e) {
print('web image cedula error - $e');
}
}
uploadCertificado() async {
try {
final now = DateTime.now();
final formattedDate = DateFormat('HHmmssddMMyyyy').format(now);
final milliseconds = (now.microsecondsSinceEpoch / 1000).round();
final random = 'f$formattedDate$milliseconds';
final Reference ref = FirebaseStorage.instance
.ref()
.child('users')
.child(uid!)
.child('certificado_profesional')
.child(random);
final metaData = SettableMetadata(contentType: 'application/pdf');
final UploadTask uploadTask =
ref.putData(imageCertificadoBytes!, metaData);
final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true);
photoCertificadoTemp = ref.fullPath;
if (snapshot.state == TaskState.success) {
updateImageCertificado(photoCertificadoTemp);
return true;
} else {
return false;
}
} catch (e) {
print('web image certificado error - $e');
}
}
Future<List<String>> uploadEspecializaciones(List<Uint8List> files) async {
List<String> filePaths = [];
for (Uint8List fileBytes in files) {
final Reference ref = FirebaseStorage.instance
.ref()
.child('users')
.child(uid!)
.child('especializaciones')
.child('e${DateTime.now().millisecondsSinceEpoch}.pdf');
final SettableMetadata metaData =
SettableMetadata(contentType: 'application/pdf');
final UploadTask uploadTask = ref.putData(fileBytes, metaData);
final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true);
if (snapshot.state == TaskState.success) {
filePaths.add(ref.fullPath);
} else {
await updateFilesEspecializaciones(filePaths);
return [];
}
}
await updateFilesEspecializaciones(filePaths);
return filePaths;
}
Future<void> updateImageCedula(image) async {
try {
final userRef = FirebaseFirestore.instance.collection('users').doc(uid);
final userSnapshot = await userRef.get();
if (userSnapshot.exists) {
await userRef.update({'imgCedula': image});
} else {
await userRef.set({'imgCedula': image});
}
} catch (e) {
print('Error al agregar o actualizar la imagen de cédula: $e');
}
}
Future<void> updateImageCertificado(image) async {
try {
final userRef = FirebaseFirestore.instance.collection('users').doc(uid);
final userSnapshot = await userRef.get();
if (userSnapshot.exists) {
await userRef.update({'imgCertificado': image});
} else {
await userRef.set({'imgCertificado': image});
}
} catch (e) {
print('Error al agregar o actualizar la imagen de certificado: $e');
}
}
Future<void> updateImage(image) async {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'photo': image});
} catch (e) {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.set({'photo': image});
} catch (e) {
print('Error al agregar la imagen de perfil: $e');
}
print('Error al actualizar la imagen de perfil: $e');
}
}
Future<void> updateFilesEspecializaciones(List<String> filePaths) async {
try {
final userRef = FirebaseFirestore.instance.collection('users').doc(uid);
final userSnapshot = await userRef.get();
if (userSnapshot.exists) {
await userRef.update({'imgEspecializaciones': filePaths});
} else {
await userRef.set({'imgEspecializaciones': filePaths});
}
} catch (e) {
print('Error al actualizar los archivos de especializaciones: $e');
}
}
void showSnackBar(String title, String message) {
Get.snackbar(
title,
message,
snackPosition: SnackPosition.TOP,
);
}
@override
Widget build(BuildContext context) {
String profession = _profession.toString();
double _space = 10;
return Scaffold(
backgroundColor: const Color(0xFFD6F4FF),
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Perfil profesional'),
body: SingleChildScrollView(
child: Center(
child: SizedBox(
width: 350,
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
color: Colors.white,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: _isLoading
? const Padding(
padding: EdgeInsets.symmetric(vertical: 30),
child: CircularProgressIndicator(),
)
: Column(
children: [
Container(
padding: const EdgeInsets.only(top: 20),
child: (selectedImagInBytes != null)
? LocalPhotoWeb(file: selectedImagInBytes)
: ReferencePhotoWeb(
ref: storage.ref().child(_photo)),
),
SizedBox(
width: 300,
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
keyboardType: TextInputType.number,
controller: _cedulaController,
inputFormatters: [
FilteringTextInputFormatter
.digitsOnly // Solo permite caracteres numéricos
],
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Ingrese una cedula válida';
}
return null;
},
decoration: const InputDecoration(
prefixIcon: Icon(Icons.person_outline),
hintText: 'Cedula (Obligatorio)',
),
),
SizedBox(height: _space),
ElevatedButton(
onPressed: () {
_selectFileCedula(true);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFD6F4FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(250, 50),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Text(
'Cedula',
style: TextStyle(
color: Color(0xFF2BA4EC),
fontSize: 17,
),
),
const SizedBox(width: 15),
Icon(
imageCedulaBytes != null
? Icons.check
: Icons.file_upload_outlined,
color: const Color(0xFF2BA4EC),
size: 30,
),
],
),
),
SizedBox(height: _space),
TextFormField(
readOnly: true,
onTap: () async {
final String? profesion =
(await Navigator.pushNamed(
context, '/profession'))
as String?;
if (profesion != null) {
setState(() {
_profession = profesion;
});
}
},
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.assignment_ind_rounded),
suffixIcon:
const Icon(Icons.arrow_drop_down),
hintStyle: profession == ''
? const TextStyle()
: const TextStyle(
color: Colors.black87),
hintText: profession == ''
? 'Profesión (Obligatorio)'
: profession),
),
SizedBox(height: _space),
ElevatedButton(
onPressed: () {
_selectFileCertificado(true);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFD6F4FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(250, 50),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Text(
'Certificado profesional',
style: TextStyle(
color: Color(0xFF2BA4EC),
fontSize: 17,
),
),
const SizedBox(width: 15),
Icon(
imageCertificadoBytes != null
? Icons.check
: Icons.file_upload_outlined,
color: const Color(0xFF2BA4EC),
size: 30,
),
],
),
),
SizedBox(height: _space),
TextFormField(
controller: _especializacionController,
decoration: const InputDecoration(
prefixIcon:
Icon(Icons.assignment_ind_rounded),
hintText: 'Especialización'),
),
SizedBox(height: _space),
ElevatedButton(
onPressed: () async {
_selectFilesEspecializaciones(true);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFD6F4FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(250, 50),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Text(
'Especialización',
style: TextStyle(
color: Color(0xFF2BA4EC),
fontSize: 17,
),
),
const SizedBox(width: 15),
Icon(
imagesEspecializacionsBytes.isEmpty
? Icons.file_upload_outlined
: Icons.check,
color: const Color(0xFF2BA4EC),
size: 30,
),
],
),
),
],
),
),
),
Container(
margin: const EdgeInsets.only(top: 40),
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 15),
decoration: BoxDecoration(
color: const Color(0xFFD6F4FF),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(1, 3),
),
],
),
child: const Row(
children: [
Icon(
Icons.error_outline,
size: 27,
color: Colors.black54,
),
SizedBox(width: 15),
Expanded(
child: Text(
'Si tienes más de una especialidad, por favor, adjunta un archivo con el diploma de respaldo para cada una de ellas y sepáralos por comas. ¡Gracias!',
style: TextStyle(
color: Colors.black, fontSize: 14),
),
),
],
),
),
Container(
alignment: Alignment.bottomCenter,
margin: const EdgeInsets.only(
top: 30, right: 20, left: 20, bottom: 30),
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
sendInfo();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(250, 50),
maximumSize: const Size(350, 50),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Enviar información',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 17,
),
),
SizedBox(width: 15),
Icon(
Icons.send,
color: Colors.white,
size: 20,
),
],
),
),
),
],
),
),
),
),
),
),
);
}
}
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
class ProfessionalRevisionScreen extends StatefulWidget {
const ProfessionalRevisionScreen({super.key});
@override
State<ProfessionalRevisionScreen> createState() =>
_ProfessionalRevisionScreenState();
}
class _ProfessionalRevisionScreenState
extends State<ProfessionalRevisionScreen> {
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Perfil profesional',
),
body: Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.only(top: 40),
child: Column(
children: [
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.access_time,
color: Color(0xFF2BA4EC),
),
SizedBox(width: 8),
Text('Información en revisión.',
style: TextStyle(
color: Color(0xFF2BA4EC),
fontSize: 17,
fontWeight: FontWeight.w500)),
],
),
const Image(
image: AssetImage('images/checklist.gif'),
width: 300,
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 40),
decoration: BoxDecoration(
color: const Color(0xFFD6F4FF),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: const Offset(0, 3), // changes position of shadow
),
],
),
padding:
const EdgeInsets.symmetric(vertical: 15, horizontal: 25),
child: const Wrap(
alignment: WrapAlignment.start, // Centra el contenido
children: [
SizedBox(
width: 350,
child: Row(
children: [
Expanded(
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. ¡Gracias por tu paciencia!',
style: TextStyle(
fontSize: 14,
),
),
),
],
),
),
],
),
),
],
),
),
),
));
}
}
@@ -0,0 +1,712 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:io';
import 'package:intl/intl.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/controllers/add_name_email_city.dart';
import 'package:prosappco/src/presentation/widgets/profile/birth_date_picker.dart';
import 'package:prosappco/src/presentation/screens/city.dart';
import 'package:prosappco/src/presentation/screens/new_number.dart';
import 'package:prosappco/src/presentation/screens/new_password.dart';
import 'package:prosappco/src/services/select_image_profile.dart';
import 'package:prosappco/src/presentation/widgets/profile/gender_dropdown.dart';
import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart';
import '../../../components/photo_view.dart';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({Key? key}) : super(key: key);
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
File? imagen_to_upload;
final DateFormat formatter = DateFormat('dd/MM/yyyy');
final uid = AuthenticationRepository.instance.getCurrentUserUid();
bool _obscureText = true;
final _formKey = GlobalKey<FormState>();
final controller = Get.put(NameEmailCityController());
final _phoneNumberController = TextEditingController();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
late final FirebaseAuth _auth;
final FirebaseStorage storage = FirebaseStorage.instance;
var photoTemp = '';
var _ciudad = '...';
var _photo = '...';
String? _email = '';
String gender = '';
DateTime? birthDate;
@override
void initState() {
super.initState();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
_auth = FirebaseAuth.instance;
final currentUser = _auth.currentUser;
if (currentUser != null && currentUser.phoneNumber != null) {
_phoneNumberController.text = currentUser.phoneNumber!;
}
if (currentUser != null && currentUser.displayName != null) {
_nameController.text = currentUser.displayName!;
}
if (currentUser != null && currentUser.email != null) {
_emailController.text = currentUser.email!;
}
_email = currentUser?.email;
if (_ciudad == '...') {
AuthenticationRepository.instance
.getCity(uid.toString())
.then((String s) => setState(() {
_ciudad = s;
}));
}
if (_photo == '...') {
AuthenticationRepository.instance
.getPhoto(uid.toString())
.then((String s) => setState(() {
_photo = s;
}));
}
}
Future<void> updateImage(image) async {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'photo': image});
} catch (e) {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.set({'photo': image});
} catch (e) {
print('Error al agregar la imagen de perfil: $e');
}
print('Error al actualizar la imagen de perfil: $e');
}
}
Future<bool> uploadImage(File image) async {
final now = DateTime.now();
final formattedDate = DateFormat('HHmmssddMMyyyy').format(now);
final milliseconds = (now.microsecondsSinceEpoch / 1000).round();
final random = '$formattedDate$milliseconds';
Reference ref =
storage.ref().child('users').child(uid!).child('profile').child(random);
final UploadTask uploadTask = ref.putFile(image);
final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true);
photoTemp = ref.fullPath;
if (snapshot.state == TaskState.success) {
return true;
} else {
return false;
}
}
Future<Widget> downloadImage(Reference ref) async {
try {
if (_photo == '...' || _photo.isEmpty) {
return GestureDetector(
onTap: () {
_showChoiceDialog(context);
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 50),
width: 100,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF2BA4EC),
borderRadius: BorderRadius.circular(50),
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 90,
),
),
);
} else {
final imageData = await ref.getData();
if (imageData != null) {
final widgetImage = GestureDetector(
onTap: () {
_showChoiceDialog(context);
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 50),
child: ClipOval(
child: Image.memory(
imageData,
width: 60,
height: 60,
fit: BoxFit.cover,
),
),
),
);
return widgetImage;
} else {
return GestureDetector(
onTap: () {
_showChoiceDialog(context);
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 50),
width: 100,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF2BA4EC),
borderRadius: BorderRadius.circular(50),
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 90,
),
),
);
}
}
} catch (e) {
return GestureDetector(
onTap: () {
_showChoiceDialog(context);
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 50),
width: 100,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF2BA4EC),
borderRadius: BorderRadius.circular(50),
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 90,
),
),
);
}
}
Future<void> updateInfo() async {
final currentUser = _auth.currentUser;
final currentPhoneNumber = _auth.currentUser!.phoneNumber;
String newName = _nameController.text.trim();
String newEmail = _emailController.text.trim();
String newPassword = _passwordController.text.trim();
if (newName.isEmpty) {
Get.snackbar(
'Nombre Invalido',
'Ingresa un nombre válido.',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
if (newEmail.isEmpty) {
Get.snackbar(
'Correo Invalido',
'Ingresa un email válido.',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
if (currentUser?.displayName != newName) {
try {
await FirebaseAuth.instance.currentUser!.updateDisplayName(newName);
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'name': newName,
'lowerName': newName.toLowerCase(),
});
} catch (e) {
print('Error al actualizar el nombre: $e');
}
}
if (currentUser?.email != newEmail) {
if (newPassword.isNotEmpty) {
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'email': newEmail,
});
updateEmailAndPassword(newEmail, newPassword);
} else {
Get.snackbar(
'Contraseña Invalida',
'Porfavor ingresa una contraseña.',
snackPosition: SnackPosition.BOTTOM,
);
}
}
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'phoneNumber': currentPhoneNumber});
} catch (e) {
print('e');
}
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'email': newEmail});
} catch (e) {
print('e');
}
try {
if (imagen_to_upload == null) {
return;
} else {
final uploaded = await uploadImage(imagen_to_upload!);
updateImage(photoTemp);
//image
}
} catch (e) {
print('Error al actualizar la imagen de perfil $e');
}
// notifyListeners();
}
Future<void> _updateEmailAndPassword(
String newEmail, String currentPassword) async {
final user = _auth.currentUser;
if (user!.email! == newEmail) {
Get.snackbar(
'No se puede actualizar',
'El correo actual no puede ser actualizado.',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
if (!newEmail.contains('@') || !newEmail.contains('.')) {
Get.snackbar(
'No se puede actualizar',
'Ingresa un correo electrónico valido.',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
final emailExistsQuery = await FirebaseFirestore.instance
.collection('users')
.where('email', isEqualTo: newEmail)
.get();
if (emailExistsQuery.docs.isNotEmpty) {
Get.snackbar(
'No se puede actualizar',
'El nuevo correo electrónico ya está en uso.',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
try {
final credential = EmailAuthProvider.credential(
email: user.email!, password: currentPassword);
await user.reauthenticateWithCredential(credential);
await user.updateEmail(newEmail);
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'email': newEmail});
setState(() {
_email = newEmail;
});
Get.snackbar(
'Éxito',
'Correo electrónico actualizado correctamente.',
snackPosition: SnackPosition.BOTTOM,
);
Navigator.of(context).pop();
} catch (e) {
Get.snackbar(
'No se pudo actualizar el correo',
'Verifica tu contraseña actual y asegúrate de que el nuevo correo electrónico no se haya utilizado previamente.',
snackPosition: SnackPosition.BOTTOM,
);
print('Error al actualizar el correo electrónico: $e');
}
}
Future<void> _showEmailUpdateDialog(BuildContext context) async {
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Actualizar Email'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: emailController,
decoration: const InputDecoration(labelText: 'Nuevo Email'),
),
TextField(
controller: passwordController,
decoration:
const InputDecoration(labelText: 'Contraseña Actual'),
obscureText: true,
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text(
'Cancelar',
style: TextStyle(color: Colors.grey),
),
),
TextButton(
onPressed: () {
String newEmail = emailController.text.trim();
String currentPassword = passwordController.text.trim();
if (newEmail.isNotEmpty && currentPassword.isNotEmpty) {
_updateEmailAndPassword(newEmail, currentPassword);
}
},
child: const Text(
'Guardar',
style:
TextStyle(color: Colors.blue, fontWeight: FontWeight.w600),
),
),
],
);
},
);
}
Future<void> updateEmailAndPassword(String email, String password) async {
final User? user = FirebaseAuth.instance.currentUser;
if (user != null) {
try {
await user.updateEmail(email);
await user.updatePassword(password);
} catch (e) {
Get.snackbar(
'Agregar correo',
'Inicia sesión para asegurarnos de que seas tú.',
snackPosition: SnackPosition.BOTTOM,
);
AuthenticationRepository.instance.logout(uid!);
}
}
}
Future<void> _showChoiceDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: SingleChildScrollView(
child: ListBody(
children: [
GestureDetector(
child: const Text(
textAlign: TextAlign.center,
"Tomar foto",
style: TextStyle(color: Color(0xFF2BA4EC)),
),
onTap: () async {
final imagen = await getImage(1);
setState(() {
imagen_to_upload = File(imagen[0]!.path);
});
Navigator.of(context).pop();
},
),
const Divider(color: Colors.black54),
GestureDetector(
child: const Text(
textAlign: TextAlign.center,
"Abrir Galería",
style: TextStyle(color: Color(0xFF2BA4EC)),
),
onTap: () async {
final imagen = await getImage(2);
setState(() {
imagen_to_upload = File(imagen[0]!.path);
});
Navigator.of(context).pop();
},
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
String city = _ciudad.toString();
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Perfil'),
body: SingleChildScrollView(
reverse: true,
child: Center(
child: Column(
children: [
GestureDetector(
onTap: () {
_showChoiceDialog(context);
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 20),
child: (imagen_to_upload != null)
? LocalPhoto(file: imagen_to_upload!)
: ReferencePhoto(ref: storage.ref().child(_photo))),
),
Container(
width: 300,
padding: const EdgeInsets.only(top: 0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController,
maxLength: 50,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Porfavor ingrese un nombre.';
}
if (value.length < 5) {
return 'Debe tener al menos 5 caracteres.';
}
return null;
},
decoration: const InputDecoration(
prefixIcon: Icon(Icons.person_outline),
hintText: 'Nombre (Obligatorio)'),
),
const SizedBox(height: 0),
_email != null
? TextFormField(
onTap: () {
_showEmailUpdateDialog(context);
},
readOnly: true,
controller: _emailController,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.email_outlined),
hintText: 'Email (Obligatorio)'),
)
: TextFormField(
controller: _emailController,
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Por favor ingrese un email';
}
final RegExp emailRegExp =
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegExp.hasMatch(value)) {
return 'Por favor ingrese un email válido';
}
return null;
},
decoration: const InputDecoration(
prefixIcon: Icon(Icons.email_outlined),
hintText: 'Email (Obligatorio)'),
),
const SizedBox(height: 20.0),
_email != null
? const SizedBox.shrink()
: TextFormField(
controller: _passwordController,
obscureText: _obscureText,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Porfavor ingrese una contraseña.';
}
if (value.length < 5) {
return 'Debe tener al menos 5 caracteres.';
}
return null;
},
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscureText
? Icons.visibility
: Icons.visibility_off,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
hintText: 'Contraseña (Obligatorio)'),
),
_email != null
? const SizedBox.shrink()
: const SizedBox(height: 20.0),
TextFormField(
readOnly: true,
onTap: () async {
final String? ciudad = await Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const CityScreen();
},
),
) as String?;
if (ciudad != null) {
setState(() {
_ciudad = ciudad;
});
}
},
decoration: InputDecoration(
prefixIcon: const Icon(Icons.near_me),
suffixIcon: const Icon(Icons.arrow_drop_down),
hintStyle: city == ''
? const TextStyle()
: const TextStyle(color: Colors.black87),
hintText: city == '' ? 'Ciudad' : city,
),
),
const SizedBox(height: 20.0),
TextFormField(
controller: _phoneNumberController,
readOnly: true,
onTap: () {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (BuildContext context) {
return const NewNumberScreen();
},
),
);
},
decoration: const InputDecoration(
prefixIcon: Icon(Icons.phone_android),
suffixIcon: Icon(Icons.edit_outlined),
hintText: '+57',
),
),
const SizedBox(height: 20.0),
GenderDropdown(
onChanged: (selectedGender) {
setState(() {
gender = selectedGender;
});
},
),
const SizedBox(height: 20.0),
BirthDatePicker(
onDateSelected: (birthDay) {
setState(() {
birthDate = birthDay;
});
},
controller: TextEditingController(
text: birthDate == null
? ''
: formatter.format(birthDate!),
),
),
],
),
),
),
Container(
alignment: Alignment.bottomCenter,
margin: const EdgeInsets.only(top: 35),
padding: const EdgeInsets.only(bottom: 30),
child: Column(
children: [
_email != null
? PrimaryButton(
onPressed: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return NewPasswordScreen();
},
),
);
},
text: 'Cambiar Contraseña',
minWidth: 300,
minHeight: 45,
)
: const SizedBox(height: 20),
const SizedBox(height: 60),
PrimaryButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
await updateInfo();
}
},
text: 'Guardar',
),
],
),
),
],
),
),
),
);
}
}
@@ -0,0 +1,727 @@
import 'dart:io';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/banner_photo.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/components/primary_btn.dart';
import 'package:prosappco/src/components/schedule_picker.dart';
import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/presentation/screens/horario.dart';
import 'package:prosappco/src/presentation/screens/professional.dart';
import 'package:prosappco/src/presentation/screens/professional_direccion.dart';
import 'package:prosappco/src/services/select_image_profile.dart';
class ProfileProScreen extends StatefulWidget {
const ProfileProScreen({super.key});
@override
State<ProfileProScreen> createState() => _ProfileProScreenState();
}
class _ProfileProScreenState extends State<ProfileProScreen> {
final uid = AuthenticationRepository.instance.getCurrentUserUid();
final TextEditingController _opcionalAddressController =
TextEditingController();
final TextEditingController _tarifaController = TextEditingController();
File? image_portada;
File? imagen_to_upload;
bool tarifaValue = false;
bool domicilioValue = true;
bool sitioValue = false;
var photoTemp = '';
var _photo = '...';
var _direccion = '...';
var _ubicacion = '...';
var _opcionalAddress = '...';
int _tarifa = 0;
SettingModel? settings;
Map<String, Schedule>? _horarios;
@override
void initState() {
super.initState();
if (settings == null) {
SettingModel.getSettings().then(
(SettingModel value) => setState(() => settings = value),
);
}
final uid = AuthenticationRepository.instance.getCurrentUserUid();
if (_photo == '...') {
AuthenticationRepository.instance.getBanner(uid.toString()).then(
(String s) => setState(
() {
_photo = s;
},
),
);
}
if (_horarios == null) {
Schedule.getHorarios(uid.toString()).then(
(Map<String, Schedule> data) {
setState(() {
_horarios = data;
});
},
);
}
if (_direccion == '...') {
AuthenticationRepository.instance
.getAddress(uid.toString())
.then((String s) => setState(() {
_direccion = s;
}));
}
if (_ubicacion == '...') {
AuthenticationRepository.instance.getUbicacion(uid.toString()).then(
(String s) => setState(
() {
_ubicacion = s;
if (_ubicacion == 'ambos') {
sitioValue = true;
domicilioValue = true;
} else if (_ubicacion == 'sitio') {
sitioValue = true;
} else if (_ubicacion == 'domicilio') {
domicilioValue = true;
}
},
),
);
}
if (_opcionalAddress == '...') {
AuthenticationRepository.instance.getOpcionalAddress(uid.toString()).then(
(String s) => setState(
() {
_opcionalAddress = s;
if (_opcionalAddress != '...') {
_opcionalAddressController.text = _opcionalAddress;
}
},
),
);
}
if (_tarifa == 0) {
AuthenticationRepository.instance.getTarifa(uid.toString()).then(
(s) => setState(
() {
_tarifa = s;
if (_tarifa != 0) {
tarifaValue = true;
_tarifaController.text = _tarifa.toString();
}
},
),
);
}
}
void createSchedules() async {
if (_horarios == null || _horarios!.isEmpty) {
final defaultSchedule = {
'Lunes': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
},
'Martes': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
},
'Miercoles': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
},
'Jueves': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
},
'Viernes': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
},
'Sabado': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
},
'Domingo': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
}
};
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'horario': defaultSchedule});
Navigator.pop(context);
} catch (e) {
print(e);
}
} else {
Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return HorarioScreen(
horarios: _horarios!,
);
},
),
);
}
}
Future<void> updateInfo() async {
if (_opcionalAddressController.text.isNotEmpty) {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'opcional_address': _opcionalAddressController.text});
}
if (tarifaValue && _tarifaController.text.isNotEmpty) {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'tarifa': int.parse(_tarifaController.text)});
} else {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'tarifa': 0});
}
if (domicilioValue && sitioValue) {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'ubicacion': 'ambos'});
} else if (domicilioValue) {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'ubicacion': 'domicilio'});
} else if (sitioValue) {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'ubicacion': 'sitio'});
} else {
Get.snackbar(
'Elige como vas a dar tu servicio',
'Selecciona si tu servicio es a domicilio o en tu consultorio.',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.black.withOpacity(0.2),
messageText: const Text(
'Selecciona si tu servicio es a domicilio o en tu consultorio.',
style: TextStyle(color: Colors.white),
),
);
return;
}
try {
if (imagen_to_upload == null) {
} else {
updateImage(photoTemp);
//image
}
} catch (e) {
print('Error al actualizar la imagen de perfil $e');
}
Get.snackbar(
'Información actualizada',
'Tu información ha sido actualizada con éxito.',
snackPosition: SnackPosition.TOP,
);
Navigator.pop(context);
return;
}
void name() {
_horarios!.forEach((dia, horario) {
print('Día: $dia');
print('Habilitado: ${horario.habilitado}');
print('Jornada Continua: ${horario.jornadaContinua}');
print('Rango 1 Hora 1: ${horario.range1Hour1}');
print('Rango 1 Hora 2: ${horario.range1Hour2}');
print('Rango 2 Hora 1: ${horario.range2Hour1}');
print('Rango 2 Hora 2: ${horario.range2Hour2}');
print('-----------------------------------');
});
}
Future<void> updateImage(image) async {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'banner': image});
} catch (e) {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.set({'banner': image});
} catch (e) {
print('Error al agregar la imagen de perfil: $e');
}
print('Error al actualizar la imagen de perfil: $e');
}
}
Future<bool> uploadImage(File image) async {
final String namefile = image.path.split('/').last;
Reference ref = storage
.ref()
.child('users')
.child(uid!)
.child('profile')
.child(namefile);
final UploadTask uploadTask = ref.putFile(image);
final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true);
photoTemp = ref.fullPath;
if (snapshot.state == TaskState.success) {
return true;
} else {
return false;
}
}
Future<void> _showChoiceDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: SingleChildScrollView(
child: ListBody(
children: [
GestureDetector(
child: const Text(
textAlign: TextAlign.center,
"Tomar foto",
style: TextStyle(color: Color(0xFF2BA4EC)),
),
onTap: () async {
final imagen = await getImage(1);
setState(() {
imagen_to_upload = File(imagen[0]!.path);
});
Navigator.of(context).pop();
},
),
const Divider(color: Colors.black54),
GestureDetector(
child: const Text(
textAlign: TextAlign.center,
"Abrir Galería",
style: TextStyle(color: Color(0xFF2BA4EC)),
),
onTap: () async {
final imagen = await getImage(2);
setState(() {
imagen_to_upload = File(imagen[0]!.path);
});
Navigator.of(context).pop();
},
),
],
),
),
);
},
);
}
void toggleDomicilio(bool newValue) {
setState(() {
domicilioValue = newValue;
if (newValue == false && sitioValue == false) {
sitioValue = true;
}
});
}
void toggleSitio(bool newValue) {
setState(() {
sitioValue = newValue;
if (newValue == false && domicilioValue == false) {
domicilioValue = true;
}
});
}
@override
Widget build(BuildContext context) {
String address = _direccion.toString();
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Perfil profesional'),
body: SingleChildScrollView(
reverse: true,
child: Column(
children: [
GestureDetector(
onTap: () {
_showChoiceDialog(context);
},
child: Container(
child: (imagen_to_upload != null)
? LocalPhoto(
file: imagen_to_upload!,
)
: ReferenceBannerPhoto(
ref: storage.ref().child(_photo),
),
),
),
if (settings?.tarifas == true)
const Divider(
color: Colors.white,
height: 12,
),
if (settings?.tarifas == true)
customSwitch(
'Tarifa',
tarifaValue,
(value) {
tarifaValue = value;
},
),
tarifaValue
? Padding(
padding:
const EdgeInsets.only(left: 40, right: 40, bottom: 15),
child: Column(
children: [
TextFormField(
controller: _tarifaController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.attach_money),
hintText: 'COP'),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
),
],
)
.animate()
.moveY(duration: const Duration(milliseconds: 100)),
)
: const SizedBox(),
if (settings?.domicilios == true)
const Divider(
color: Colors.white,
height: 12,
),
if (settings?.domicilios == true)
customSwitch(
'Servicio a domicilio', domicilioValue, toggleDomicilio),
const Divider(),
customSwitch('Servicio en sitio', sitioValue, toggleSitio),
sitioValue
? Padding(
padding:
const EdgeInsets.only(left: 40, right: 40, bottom: 15),
child: Column(
children: [
TextFormField(
readOnly: true,
onTap: () async {
final String? direccion = await Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const ProfessionalDireccionScreen();
},
),
) as String?;
if (direccion != null) {
setState(() {
_direccion = direccion;
});
}
},
decoration: InputDecoration(
prefixIcon: const Icon(Icons.near_me),
hintStyle: address == ''
? const TextStyle()
: const TextStyle(color: Colors.black87),
hintText: address == '' ? 'Dirección' : address),
),
const SizedBox(height: 10),
TextFormField(
controller: _opcionalAddressController,
decoration: const InputDecoration(
hintText: 'Oficina / Piso / Conjunto'),
),
],
)
.animate()
.moveY(duration: const Duration(milliseconds: 100)),
)
: const SizedBox(),
const Divider(),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 30),
child: SizedBox(
width: double.infinity,
child: Text(
'Horario estandar',
style: TextStyle(
color: Colors.black,
fontSize: 15,
),
),
),
),
GestureDetector(
onTap: () {
createSchedules();
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
children: [
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Lunes'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: Text(timeList(_horarios?['Lunes'], context)),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Martes'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child:
Text(timeList(_horarios?['Martes'], context)),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Miercoles'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: Text(
timeList(_horarios?['Miercoles'], context)),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Jueves'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child:
Text(timeList(_horarios?['Jueves'], context)),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Viernes'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child:
Text(timeList(_horarios?['Viernes'], context)),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Sabado'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child:
Text(timeList(_horarios?['Sabado'], context)),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Domingo'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child:
Text(timeList(_horarios?['Domingo'], context)),
),
),
],
),
],
),
),
),
PrimaryButtom(
onPressed: () {
updateInfo();
// name();
},
label: 'Guardar'),
const SizedBox(
height: 20,
)
],
),
),
);
}
Widget customSwitch(
String text, bool switchValue, ValueChanged<bool> onChanged) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: SizedBox(
height: 40,
child: Row(
children: [
Expanded(
child: Text(
text,
style: const TextStyle(
fontSize: 15,
color: Colors.black,
),
),
),
Transform.scale(
scale: 1.2,
child: Switch(
value: switchValue,
onChanged: onChanged,
),
),
],
),
),
);
}
String timeList(Schedule? schedule, BuildContext context) {
if (schedule == null) {
return 'N/A';
}
if (!schedule.habilitado) {
return 'N/A';
}
if (schedule.jornadaContinua) {
return '${schedule.range1Hour1?.format(context).toString()} - ${schedule.range2Hour2?.format(context).toString()}';
} else {
return '${schedule.range1Hour1?.format(context).toString()} - ${schedule.range1Hour2?.format(context).toString()}; ${schedule.range2Hour1?.format(context).toString()} - ${schedule.range2Hour2?.format(context).toString()}';
}
}
}
@@ -0,0 +1,642 @@
import 'dart:io';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/components/primary_btn.dart';
import 'package:prosappco/src/components/schedule_picker.dart';
import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/presentation/screens/horario.dart';
import 'package:prosappco/src/presentation/screens/professional.dart';
import 'package:prosappco/src/presentation/screens/ubicacion.dart';
class ProfileProWebScreen extends StatefulWidget {
const ProfileProWebScreen({super.key});
@override
State<ProfileProWebScreen> createState() => _ProfileProWebScreenState();
}
class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
final uid = AuthenticationRepository.instance.getCurrentUserUid();
final TextEditingController _opcionalAddressController =
TextEditingController();
final TextEditingController _tarifaController = TextEditingController();
final TextEditingController _ubicationController = TextEditingController();
double latUser = 0.0;
double lngUser = 0.0;
bool domicilioValue = true;
bool tarifaValue = false;
bool sitioValue = false;
var photoTemp = '';
var _direccion = '...';
var _ubicacion = '...';
var _opcionalAddress = '...';
int _tarifa = 0;
SettingModel? settings;
Map<String, Schedule>? _horarios;
@override
void initState() {
super.initState();
if (settings == null) {
SettingModel.getSettings().then(
(SettingModel value) => setState(() => settings = value),
);
}
final uid = AuthenticationRepository.instance.getCurrentUserUid();
if (_horarios == null) {
Schedule.getHorarios(uid.toString()).then(
(Map<String, Schedule> data) {
setState(() {
_horarios = data;
});
},
);
}
if (_direccion == '...') {
AuthenticationRepository.instance
.getAddress(uid.toString())
.then((String s) => setState(() {
_direccion = s;
_ubicationController.text = _direccion;
}));
}
if (_ubicacion == '...') {
AuthenticationRepository.instance.getUbicacion(uid.toString()).then(
(String s) => setState(
() {
_ubicacion = s;
if (_ubicacion == 'ambos') {
sitioValue = true;
domicilioValue = true;
} else if (_ubicacion == 'sitio') {
sitioValue = true;
} else if (_ubicacion == 'domicilio') {
domicilioValue = true;
}
},
),
);
}
if (_opcionalAddress == '...') {
AuthenticationRepository.instance.getOpcionalAddress(uid.toString()).then(
(String s) => setState(
() {
_opcionalAddress = s;
if (_opcionalAddress != '...') {
_opcionalAddressController.text = _opcionalAddress;
}
},
),
);
}
if (_tarifa == 0) {
AuthenticationRepository.instance.getTarifa(uid.toString()).then(
(s) => setState(
() {
_tarifa = s;
if (_tarifa != 0) {
tarifaValue = true;
_tarifaController.text = _tarifa.toString();
}
},
),
);
}
}
Future<void> updateInfo() async {
if (_ubicationController.text.isNotEmpty) {
FirebaseFirestore.instance.collection('users').doc(uid).update({
'address': _ubicationController.text,
if (latUser != 0.0) 'latitude': latUser,
if (latUser != 0.0) 'longitude': lngUser,
});
}
if (_opcionalAddressController.text.isNotEmpty) {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'opcional_address': _opcionalAddressController.text});
}
if (tarifaValue && _tarifaController.text.isNotEmpty) {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'tarifa': int.parse(_tarifaController.text)});
} else {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'tarifa': 0});
}
if (domicilioValue && sitioValue) {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'ubicacion': 'ambos'});
} else if (domicilioValue) {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'ubicacion': 'domicilio'});
} else if (sitioValue) {
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'ubicacion': 'sitio'});
} else {
Get.snackbar(
'Elige como vas a dar tu servicio',
'Selecciona si tu servicio es a domicilio o en tu consultorio.',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.black.withOpacity(0.2),
messageText: const Text(
'Selecciona si tu servicio es a domicilio o en tu consultorio.',
style: TextStyle(color: Colors.white),
),
);
return;
}
Get.snackbar(
'Información actualizada',
'Tu información a sido actualizada con exito.',
snackPosition: SnackPosition.TOP,
);
Navigator.pop(context);
return;
}
Future<void> updateImage(image) async {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'banner': image});
} catch (e) {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.set({'banner': image});
} catch (e) {
print('Error al agregar la imagen de perfil: $e');
}
print('Error al actualizar la imagen de perfil: $e');
}
}
void createSchedules() async {
if (_horarios == null || _horarios!.isEmpty) {
final defaultSchedule = {
'Lunes': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
},
'Martes': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
},
'Miercoles': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
},
'Jueves': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
},
'Viernes': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
},
'Sabado': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
},
'Domingo': {
'habilitado': false,
'jornadaContinua': false,
'range1Hour1': null,
'range1Hour2': null,
'range2Hour1': null,
'range2Hour2': null
}
};
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'horario': defaultSchedule});
Navigator.pop(context);
} catch (e) {
print(e);
}
} else {
Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return HorarioScreen(
horarios: _horarios!,
);
},
),
);
}
}
Future<bool> uploadImage(File image) async {
final String namefile = image.path.split('/').last;
Reference ref = storage
.ref()
.child('users')
.child(uid!)
.child('profile')
.child(namefile);
final UploadTask uploadTask = ref.putFile(image);
final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true);
photoTemp = ref.fullPath;
if (snapshot.state == TaskState.success) {
return true;
} else {
return false;
}
}
void toggleDomicilio(bool newValue) {
setState(() {
domicilioValue = newValue;
if (newValue == false && sitioValue == false) {
sitioValue = true;
}
});
}
void toggleSitio(bool newValue) {
setState(() {
sitioValue = newValue;
if (newValue == false && domicilioValue == false) {
domicilioValue = true;
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Perfil profesional'),
body: SingleChildScrollView(
reverse: true,
child: Column(
children: [
if (settings?.tarifas == true)
const Divider(
color: Colors.white,
height: 12,
),
if (settings?.tarifas == true)
customSwitch(
'Tarifa',
tarifaValue,
(value) {
tarifaValue = value;
},
),
tarifaValue
? Padding(
padding:
const EdgeInsets.only(left: 40, right: 40, bottom: 15),
child: Column(
children: [
TextFormField(
controller: _tarifaController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.attach_money),
hintText: 'COP'),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
),
],
)
.animate()
.moveY(duration: const Duration(milliseconds: 100)),
)
: const SizedBox(),
if (settings?.domicilios == true)
const Divider(
color: Colors.white,
height: 12,
),
if (settings?.domicilios == true)
customSwitch(
'Servicio a domicilio', domicilioValue, toggleDomicilio),
const Divider(),
customSwitch('Servicio en sitio', sitioValue, toggleSitio),
sitioValue
? Padding(
padding:
const EdgeInsets.only(left: 40, right: 40, bottom: 15),
child: Column(
children: [
TextFormField(
controller: _ubicationController,
readOnly: true,
onTap: () async {
final List<dynamic> datos = await Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const UbicacionScreen();
},
),
);
if (datos.length == 3) {
final formattedAddress = datos[0];
final lat = datos[1];
final lng = datos[2];
setState(() {
_ubicationController.text = formattedAddress;
latUser = lat;
lngUser = lng;
});
}
},
decoration: const InputDecoration(
hintText: 'Escribe tu ubicación',
prefixIcon: Icon(Icons.near_me),
),
),
const SizedBox(height: 10),
TextFormField(
controller: _opcionalAddressController,
decoration: const InputDecoration(
hintText: 'Oficina / Piso / Conjunto'),
),
],
)
.animate()
.moveY(duration: const Duration(milliseconds: 100)),
)
: const SizedBox(),
const Divider(),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 30),
child: SizedBox(
width: double.infinity,
child: Text(
'Horario estandar',
style: TextStyle(
color: Colors.black,
fontSize: 15,
),
),
),
),
GestureDetector(
onTap: () {
createSchedules();
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
children: [
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Lunes'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: Text(timeList(_horarios?['Lunes'], context)),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Martes'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child:
Text(timeList(_horarios?['Martes'], context)),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Miercoles'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: Text(
timeList(_horarios?['Miercoles'], context)),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Jueves'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child:
Text(timeList(_horarios?['Jueves'], context)),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Viernes'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child:
Text(timeList(_horarios?['Viernes'], context)),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Sabado'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child:
Text(timeList(_horarios?['Sabado'], context)),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child: const Text('Domingo'),
),
),
TableCell(
child: Container(
padding: const EdgeInsets.all(8.0),
child:
Text(timeList(_horarios?['Domingo'], context)),
),
),
],
),
],
),
),
),
PrimaryButtom(
onPressed: () {
updateInfo();
},
label: 'Guardar'),
const SizedBox(
height: 20,
)
],
),
),
);
}
Widget customSwitch(
String text, bool switchValue, ValueChanged<bool> onChanged) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: SizedBox(
height: 40,
child: Row(
children: [
Expanded(
child: Text(
text,
style: const TextStyle(
fontSize: 15,
color: Colors.black,
),
),
),
Transform.scale(
scale: 1.2,
child: Switch(
value: switchValue,
onChanged: onChanged,
),
),
],
),
),
);
}
String timeList(Schedule? schedule, BuildContext context) {
if (schedule == null) {
return 'N/A';
}
if (!schedule.habilitado) {
return 'N/A';
}
if (schedule.jornadaContinua) {
return '${schedule.range1Hour1?.format(context).toString()} - ${schedule.range2Hour2?.format(context).toString()}';
} else {
return '${schedule.range1Hour1?.format(context).toString()} - ${schedule.range1Hour2?.format(context).toString()}; ${schedule.range2Hour1?.format(context).toString()} - ${schedule.range2Hour2?.format(context).toString()}';
}
}
}
@@ -0,0 +1,628 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:image_picker/image_picker.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/photo_view_web.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/presentation/screens/city.dart';
import 'package:prosappco/src/presentation/screens/new_number.dart';
import 'package:prosappco/src/presentation/screens/new_password.dart';
import 'package:universal_html/html.dart' as html;
class ProfileWebScreen extends StatefulWidget {
const ProfileWebScreen({super.key});
@override
State<ProfileWebScreen> createState() => _ProfileWebScreenState();
}
class _ProfileWebScreenState extends State<ProfileWebScreen> {
final uid = AuthenticationRepository.instance.getCurrentUserUid();
final FirebaseStorage storage = FirebaseStorage.instance;
late final FirebaseAuth _auth;
List<City>? filteredCities;
// variables imagen
String selectedImage = '';
XFile? file;
Uint8List? selectedImagInBytes;
String photoTemp = '';
// controladores
final _formKey = GlobalKey<FormState>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _phoneNumberController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
bool _obscureText = true;
// variables
String _ciudad = '...';
String _photo = '...';
String? _email = '';
@override
void initState() {
super.initState();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
_auth = FirebaseAuth.instance;
final currentUser = _auth.currentUser;
if (currentUser != null && currentUser.phoneNumber != null) {
_phoneNumberController.text = currentUser.phoneNumber!;
}
if (currentUser != null && currentUser.displayName != null) {
_nameController.text = currentUser.displayName!;
}
if (currentUser != null && currentUser.email != null) {
_emailController.text = currentUser.email!;
}
_email = currentUser?.email;
if (_ciudad == '...') {
AuthenticationRepository.instance
.getCity(uid.toString())
.then((String s) => setState(() {
_ciudad = s;
}));
}
if (_photo == '...') {
AuthenticationRepository.instance
.getPhoto(uid.toString())
.then((String s) => setState(() {
_photo = s;
}));
}
}
_selectFile(bool imageFrom) async {
FilePickerResult? fileResult = await FilePicker.platform.pickFiles();
if (fileResult != null) {
setState(() {
selectedImage = fileResult.files.first.name;
selectedImagInBytes = fileResult.files.first.bytes;
});
}
}
_uploadFile() async {
try {
final now = DateTime.now();
final formattedDate = DateFormat('HHmmssddMMyyyy').format(now);
final milliseconds = (now.microsecondsSinceEpoch / 1000).round();
final random = '$formattedDate$milliseconds';
final Reference ref = FirebaseStorage.instance
.ref()
.child('users')
.child(uid!)
.child('profile')
.child(random);
final metaData = SettableMetadata(contentType: 'image/jpeg');
final UploadTask uploadTask = ref.putData(selectedImagInBytes!, metaData);
final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true);
photoTemp = ref.fullPath;
if (snapshot.state == TaskState.success) {
return true;
} else {
return false;
}
} catch (e) {
print('web image error - $e');
}
}
Future<void> updateInfo() async {
final currentUser = _auth.currentUser;
final currentPhoneNumber = _auth.currentUser!.phoneNumber;
String newName = _nameController.text.trim();
String newEmail = _emailController.text.trim();
String newPassword = _passwordController.text.trim();
if (newName.isEmpty) {
// Si el nuevo nombre está vacío, no lo actualizamos y mostramos un mensaje al usuario
Get.snackbar(
'Nombre Invalido',
'Ingresa un nombre válido.',
snackPosition: SnackPosition.BOTTOM,
);
return;
} else {
try {
await FirebaseAuth.instance.currentUser!.updateDisplayName(newName);
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'name': newName,
'lowerName': newName.toLowerCase(),
});
} catch (e) {
print('Error al actualizar el nombre: $e');
}
}
if (newEmail.isEmpty) {
// Si el nuevo nombre está vacío, no lo actualizamos y mostramos un mensaje al usuario
Get.snackbar(
'Correo Invalido',
'Ingresa un email válido.',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
try {
await FirebaseAuth.instance.currentUser!.updateDisplayName(newName);
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'name': newName,
'lowerName': newName.toLowerCase(),
});
} catch (e) {
print('Error al actualizar el nombre: $e');
}
if (currentUser?.email != newEmail) {
if (newPassword.isNotEmpty) {
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'email': newEmail,
});
updateEmailAndPassword(newEmail, newPassword);
} else {
Get.snackbar(
'Contraseña Invalida',
'Por favor, ingresa una contraseña.',
snackPosition: SnackPosition.BOTTOM,
);
}
}
String? selectedCity = _ciudad;
// Guardar la ciudad seleccionada en el documento del usuario
try {
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'city': selectedCity,
});
} catch (e) {
print('Error al actualizar la ciudad: $e');
}
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'phoneNumber': currentPhoneNumber});
} catch (e) {
print('e');
}
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'email': newEmail});
} catch (e) {
print('e');
}
try {
if (selectedImagInBytes == null) {
return;
} else {
await _uploadFile();
updateImage(photoTemp);
}
} catch (e) {
print('Error al actualizar la imagen de perfil $e');
}
}
Future<void> updateImage(image) async {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'photo': image});
} catch (e) {
try {
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.set({'photo': image});
} catch (e) {
print('Error al agregar la imagen de perfil: $e');
}
print('Error al actualizar la imagen de perfil: $e');
}
}
Future<void> updateEmailAndPassword(String email, String password) async {
final User? user = FirebaseAuth.instance.currentUser;
if (user != null) {
try {
await user.updateEmail(email);
await user.updatePassword(password);
} catch (e) {
Get.snackbar(
'Agregar correo',
'Inicia sesión para asegurarnos de que seas tú.',
snackPosition: SnackPosition.BOTTOM,
);
AuthenticationRepository.instance.logout(uid!);
}
}
}
Future<List<City>> _getCities() async {
List<City> citys = [];
try {
QuerySnapshot countries = await countriesCollection.get();
for (DocumentSnapshot country in countries.docs) {
String countryName = country.id;
Map<String, dynamic> data = country.data() as Map<String, dynamic>;
Map<String, Map<String, String>> states = {};
for (var entry in data.entries) {
String key = entry.key;
Map<String, String> cityData = Map<String, String>.from(entry.value);
states[key] = cityData;
}
for (var state in states.entries) {
var citysState = state.value.entries.map((city) => City(
cityName: city.key,
coordsOfCity: city.value,
stateOfCity: state.key,
countryOfCity: countryName,
));
citys.addAll(citysState);
}
}
} catch (e) {
print('Error obteniendo las ciudades: $e');
}
return citys;
}
Future<void> _showEmailUpdateDialog(BuildContext context) async {
TextEditingController emailController = TextEditingController();
String currentEmail = FirebaseAuth.instance.currentUser?.email ?? '';
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Actualizar Correo Electrónico'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextFormField(
controller: emailController,
decoration: const InputDecoration(
labelText: 'Nuevo Correo Electrónico',
),
),
],
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancelar'),
),
ElevatedButton(
onPressed: () async {
String newEmail = emailController.text.trim();
if (newEmail.isNotEmpty && newEmail != currentEmail) {
// Actualiza el correo electrónico aquí
try {
await FirebaseAuth.instance.currentUser
?.updateEmail(newEmail);
await FirebaseFirestore.instance
.collection('users')
.doc(FirebaseAuth.instance.currentUser?.uid)
.update({'email': newEmail});
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Correo Electrónico actualizado con éxito'),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Error al actualizar el correo electrónico'),
),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Ingresa un nuevo correo electrónico válido'),
),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
),
child: const Text('Guardar'),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFD6F4FF),
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Perfil'),
body: Center(
child: SizedBox(
width: 300,
height: 680,
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
color: Colors.white,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.only(top: 20, bottom: 20),
child: (selectedImagInBytes != null)
? LocalPhotoWeb(file: selectedImagInBytes)
: ReferencePhotoWeb(ref: storage.ref().child(_photo)),
),
TextFormField(
controller: _nameController,
maxLength: 50,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Porfavor ingrese un nombre.';
}
if (value.length < 5) {
return 'Debe tener al menos 5 caracteres.';
}
return null;
},
decoration: const InputDecoration(
prefixIcon: Icon(Icons.person_outline),
hintText: 'Nombre (Obligatorio)',
),
),
_email != null
? TextFormField(
onTap: () {
_showEmailUpdateDialog(context);
},
readOnly: true,
controller: _emailController,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.email_outlined),
hintText: 'Email (Obligatorio)',
),
)
: TextFormField(
controller: _emailController,
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Por favor ingrese un email';
}
final RegExp emailRegExp =
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegExp.hasMatch(value)) {
return 'Por favor ingrese un email válido';
}
return null;
},
decoration: const InputDecoration(
prefixIcon: Icon(Icons.email_outlined),
hintText: 'Email (Obligatorio)',
),
),
const SizedBox(height: 20.0),
_email != null
? const SizedBox.shrink()
: TextFormField(
controller: _passwordController,
obscureText: _obscureText,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Porfavor ingrese una contraseña.';
}
if (value.length < 5) {
return 'Debe tener al menos 5 caracteres.';
}
return null;
},
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscureText
? Icons.visibility
: Icons.visibility_off,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
hintText: 'Contraseña (Obligatorio)',
),
),
_email != null
? const SizedBox.shrink()
: const SizedBox(height: 20.0),
FutureBuilder<List<City>>(
future: _getCities(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return const Center(
child: Text('Error al obtener las ciudades'),
);
} else {
List<City> filteredCities = snapshot.data!;
return DropdownButtonFormField<String>(
value: _ciudad,
onChanged: (String? newValue) {
setState(() {
_ciudad = newValue!;
});
},
items: filteredCities.map((City city) {
return DropdownMenuItem<String>(
value: city.cityName,
child: Text(city.cityName ?? ''),
);
}).toList(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.near_me),
hintText: _ciudad,
),
);
}
},
),
const SizedBox(height: 20.0),
TextFormField(
controller: _phoneNumberController,
readOnly: true,
onTap: () {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (BuildContext context) {
return const NewNumberScreen();
},
),
);
},
decoration: const InputDecoration(
prefixIcon: Icon(Icons.phone_android),
suffixIcon: Icon(Icons.edit_outlined),
hintText: '+57'),
),
Container(
alignment: Alignment.bottomCenter,
margin: const EdgeInsets.only(top: 30),
padding: const EdgeInsets.only(bottom: 30),
child: Column(
children: [
_email != null
? ElevatedButton(
onPressed: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return NewPasswordScreen();
},
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(300, 45),
),
child: const Text(
'Cambiar Contraseña',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 17,
),
),
)
: const SizedBox(height: 20),
const SizedBox(height: 60),
ElevatedButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
await updateInfo().whenComplete(() {
html.window.location.reload();
Get.snackbar(
'Perfil',
'Información actualizada correctamente.',
snackPosition: SnackPosition.TOP,
);
});
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(200, 50),
),
child: const Text(
'Guardar',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 17,
),
),
),
],
),
),
],
),
),
),
),
),
),
);
}
}
@@ -0,0 +1,746 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:prosappco/main.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/primary_btn.dart';
import 'package:prosappco/src/controllers/register_controller.dart';
import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/providers/user_provider.dart';
import 'package:prosappco/src/presentation/screens/login/login.dart';
import 'package:prosappco/src/presentation/screens/web_view.dart';
import 'package:provider/provider.dart';
import 'package:responsive_builder/responsive_builder.dart';
import 'package:url_launcher/url_launcher.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
bool _obscureText = true;
final controller = Get.put(RegisterController());
final _formKey = GlobalKey<FormState>();
bool _isChecked = false;
SettingModel? settings;
void _launchURL(String url) async {
if (await canLaunch(url)) {
await launch(url, forceSafariVC: false, forceWebView: false);
} else {
throw 'No se pudo abrir el enlace $url';
}
}
@override
void initState() {
super.initState();
if (settings == null) {
SettingModel.getSettings().then(
(SettingModel value) => setState(() {
settings = value;
}),
);
}
}
@override
Widget build(BuildContext context) {
return ScreenTypeLayout.builder(
mobile: (BuildContext context) => _mobileView(context),
tablet: (BuildContext context) => _mobileView(context),
desktop: (BuildContext context) => _desktopView(context),
);
}
Widget _mobileView(BuildContext context) {
bool isIOS = Theme.of(context).platform == TargetPlatform.iOS;
return SafeArea(
child: Scaffold(
backgroundColor: const Color(0xFFD6F4FF),
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SingleChildScrollView(
reverse: true,
child: Stack(
children: [
Container(
margin: const EdgeInsets.only(top: 130),
width: double.infinity,
height: 700,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topRight: Radius.circular(50),
topLeft: Radius.circular(50))),
),
Container(
margin: const EdgeInsets.only(top: 20, left: 30),
child: const Image(
image: AssetImage('images/logo_prosapp.png'),
width: 180,
height: 100,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 0, vertical: 20),
margin: const EdgeInsets.only(
top: 125,
left: 25,
),
child: Row(
children: <Widget>[
IconButton(
icon: const Icon(
Icons.arrow_back,
size: 30,
),
onPressed: () {
Navigator.pop(context);
},
),
const Text(
'Registro',
style: TextStyle(
color: Color(0xFF262626),
fontSize: 38.0,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.right,
),
],
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 0, vertical: 20),
margin: const EdgeInsets.only(top: 200, left: 50, right: 50),
child: Column(
children: [
!isIOS && !kIsWeb && settings?.google == true
? Padding(
padding: const EdgeInsets.only(bottom: 20),
child: ElevatedButton(
onPressed: () async {
await AuthenticationRepository.instance
.signInWithGoogle();
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(230, 60),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Entrar con Google ',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
SizedBox(width: 5),
FaIcon(FontAwesomeIcons.google),
],
)),
)
: const SizedBox(),
!isIOS && !kIsWeb && settings?.google == true
? const Padding(
padding: EdgeInsets.symmetric(vertical: 0),
child: Row(
children: <Widget>[
Expanded(
child: Divider(
color: Colors.black38,
thickness: 1,
),
),
Padding(
padding:
EdgeInsets.symmetric(horizontal: 10),
child: Text("ó"),
),
Expanded(
child: Divider(
color: Colors.black38,
thickness: 1,
),
),
],
),
)
: const SizedBox(),
Form(
key: _formKey,
child: Column(
children: [
const Padding(
padding: EdgeInsets.only(bottom: 5),
child: Align(
alignment: Alignment.topLeft,
child: Text('Email',
style: TextStyle(
fontSize: 18.0,
color: Color(0xFF65676B))),
)),
Padding(
padding: const EdgeInsets.only(bottom: 18),
child: TextFormField(
controller: controller.email,
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Por favor ingresa un email';
}
final RegExp emailRegExp = RegExp(
r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegExp.hasMatch(value)) {
return 'Por favor ingresa un email válido';
}
return null;
},
decoration: const InputDecoration(
border: OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
enabledBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
focusedBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color:
Color.fromARGB(255, 184, 0, 0)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
hintText: 'Hello@gmail.com',
fillColor: Color.fromARGB(255, 239, 239, 239),
filled: true,
prefixIcon: Icon(Icons.email_outlined),
hintStyle: TextStyle(
color: Colors.grey,
),
),
),
),
const Padding(
padding: EdgeInsets.only(bottom: 5),
child: Align(
alignment: Alignment.topLeft,
child: Text('Password',
style: TextStyle(
fontSize: 18.0,
color: Color(0xFF65676B))),
)),
Padding(
padding: const EdgeInsets.only(bottom: 25),
child: TextFormField(
controller: controller.password,
obscureText: _obscureText,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Por favor ingresa una contraseña';
}
if (value.length <= 6) {
return 'Contraseña muy corta';
}
return null;
},
decoration: InputDecoration(
errorBorder: const OutlineInputBorder(
borderSide: BorderSide(
color:
Color.fromARGB(255, 184, 0, 0)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
enabledBorder: const OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
border: const OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
focusedBorder: const OutlineInputBorder(
borderSide:
BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
hintText: 'Contraseña',
hintStyle: const TextStyle(
color: Colors.grey,
),
fillColor:
const Color.fromARGB(255, 239, 239, 239),
filled: true,
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscureText
? Icons.visibility
: Icons.visibility_off,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment
.center, // Centra horizontalmente
children: <Widget>[
Checkbox(
value: _isChecked,
onChanged: (value) {
setState(() {
_isChecked = value!;
});
},
),
GestureDetector(
onTap: () {
if (kIsWeb) {
_launchURL(
settings?.terminosCondiciones ?? '');
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return WebViewScreen(
label: 'Políticas de privacidad',
link: settings?.terminosCondiciones ??
'',
);
},
),
);
}
},
child: const Text(
'Acepto los términos y condiciones.',
style: TextStyle(
color: Color(0xFF65676B),
decoration: TextDecoration.underline,
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 25),
child: Center(
child: PrimaryButtom(
onPressed: () {
if (_formKey.currentState!.validate()) {
RegisterController.instance
.registerUser(
controller.email.text.trim(),
controller.password.text.trim(),
)
.then((value) => Provider.of<UserProvider>(
context,
listen: false)
.initUserProvider());
}
},
label: 'Registrarme',
isEnabled: _isChecked,
),
),
),
RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 16.0,
color: Color(0xFF65676B),
fontFamily: 'Poppins',
),
children: [
const TextSpan(text: 'Ya estas registrado? '),
WidgetSpan(
child: GestureDetector(
onTap: () {
Navigator.pushReplacementNamed(
context, '/login');
},
child: const Text(
'Iniciar Sesión',
style: TextStyle(
fontSize: 16.0,
color: Color(0xFF2BA4EC),
fontWeight: FontWeight.w600,
),
),
),
),
],
),
)
],
),
),
],
),
),
),
),
);
}
Widget _desktopView(BuildContext context) {
double height = MediaQuery.of(context).size.height;
double width = MediaQuery.of(context).size.width;
return Scaffold(
backgroundColor: const Color(0xFFD6F4FF),
body: SizedBox(
height: height,
width: width,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: SizedBox(
height: height,
child: const Center(
child: Image(
image: AssetImage('images/logo_prosapp.png'),
),
),
),
),
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: width * 0.07),
color: Colors.white,
height: height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 0, vertical: 20),
child: Row(
children: <Widget>[
IconButton(
icon: const Icon(
Icons.arrow_back,
size: 30,
),
onPressed: () {
Navigator.pop(context);
},
),
SizedBox(width: width * 0.01),
const Text(
'Registro',
style: TextStyle(
color: Color(0xFF262626),
fontSize: 38.0,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.right,
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 0, vertical: 20),
child: Column(
children: [
Form(
key: _formKey,
child: Column(
children: [
const Padding(
padding: EdgeInsets.only(bottom: 5),
child: Align(
alignment: Alignment.topLeft,
child: Text('Email',
style: TextStyle(
fontSize: 18.0,
color: Color(0xFF65676B))),
)),
Padding(
padding: const EdgeInsets.only(bottom: 18),
child: TextFormField(
controller: controller.email,
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Por favor ingresa un email';
}
final RegExp emailRegExp = RegExp(
r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegExp.hasMatch(value)) {
return 'Por favor ingresa un email válido';
}
return null;
},
decoration: const InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color.fromARGB(
255, 184, 0, 0)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
hintText: 'Hello@gmail.com',
fillColor:
Color.fromARGB(255, 239, 239, 239),
filled: true,
prefixIcon: Icon(Icons.email_outlined),
hintStyle: TextStyle(
color: Colors.grey,
),
),
),
),
const Padding(
padding: EdgeInsets.only(bottom: 5),
child: Align(
alignment: Alignment.topLeft,
child: Text('Password',
style: TextStyle(
fontSize: 18.0,
color: Color(0xFF65676B))),
)),
Padding(
padding: const EdgeInsets.only(bottom: 25),
child: TextFormField(
controller: controller.password,
obscureText: _obscureText,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Por favor ingresa una contraseña';
}
if (value.length <= 6) {
return 'Contraseña muy corta';
}
return null;
},
decoration: InputDecoration(
errorBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Color.fromARGB(
255, 184, 0, 0)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
border: const OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
hintText: 'Contraseña',
hintStyle: const TextStyle(
color: Colors.grey,
),
fillColor: const Color.fromARGB(
255, 239, 239, 239),
filled: true,
prefixIcon:
const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscureText
? Icons.visibility
: Icons.visibility_off,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment
.center, // Centra horizontalmente
children: <Widget>[
Checkbox(
value: _isChecked,
onChanged: (value) {
setState(() {
_isChecked = value!;
});
},
),
GestureDetector(
onTap: () {
if (kIsWeb) {
_launchURL(
settings?.terminosCondiciones ?? '');
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return WebViewScreen(
label: 'Políticas de privacidad',
link: settings
?.terminosCondiciones ??
'',
);
},
),
);
}
},
child: const Text(
'Acepto los términos y condiciones.',
style: TextStyle(
color: Color(0xFF65676B),
decoration: TextDecoration.underline,
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 25),
child: Center(
child: PrimaryButtom(
onPressed: () {
if (_formKey.currentState!.validate()) {
RegisterController.instance
.registerUser(
controller.email.text.trim(),
controller.password.text.trim(),
)
.then((value) =>
Provider.of<UserProvider>(context,
listen: false)
.initUserProvider());
}
},
isEnabled: _isChecked,
label: 'Registrarme',
),
),
),
RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 16.0,
color: Color(0xFF65676B),
fontFamily: 'Poppins',
),
children: [
const TextSpan(text: 'Ya estas registrado? '),
WidgetSpan(
child: GestureDetector(
onTap: () {
Navigator.pushReplacementNamed(
context, '/login');
},
child: const Text(
'Iniciar Sesión',
style: TextStyle(
fontSize: 16.0,
color: Color(0xFF2BA4EC),
fontWeight: FontWeight.w600,
),
),
),
),
],
),
)
],
),
),
],
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/photo_view.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/scores_model.dart';
class ReputationProScreen extends StatefulWidget {
const ReputationProScreen({
super.key,
});
@override
State<ReputationProScreen> createState() => _ReputationProScreenState();
}
class _ReputationProScreenState extends State<ReputationProScreen> {
ScoresModel? scoresModel;
@override
void initState() {
super.initState();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
if (scoresModel == null) {
ScoresModel.scoreTo(uid.toString(), true, true).then(
(ScoresModel s) => setState(() => scoresModel = s),
);
}
}
Widget _scoreList(List<ScoreDetailModel> list) {
if (list.isEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 40),
child: Center(
child: Text('Sin calificaciones'),
),
);
} else {
return Column(
children: list.map((e) => _scoreItem(e)).toList(),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Reputación'),
body: SingleChildScrollView(
child: _scoreList(scoresModel?.details ?? []),
),
);
}
Widget _scoreItem(ScoreDetailModel scoreDetails) {
return ListTile(
onTap: () {},
leading: ReferencePhoto(
ref: scoreDetails.avatar,
size: 50,
sizeCircle: 50,
sizeIcon: 35,
),
title: Row(
children: [
RatingBar.builder(
initialRating: scoreDetails.score,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 22,
maxRating: 5,
itemPadding: const EdgeInsets.symmetric(horizontal: 0),
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC),
),
onRatingUpdate: (rating) {},
ignoreGestures: true,
),
Text(
' (${scoreDetails.score})',
style: const TextStyle(color: Colors.black54, fontSize: 13),
)
],
),
subtitle: Row(
children: [
Expanded(
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: '${scoreDetails.name}, ',
style: const TextStyle(fontSize: 15, color: Colors.black),
),
TextSpan(
text: '"${scoreDetails.comment}"',
style: const TextStyle(fontSize: 15, color: Colors.grey),
),
],
),
),
),
],
));
}
}
@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/photo_view.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/scores_model.dart';
class ReputationScreen extends StatefulWidget {
const ReputationScreen({
super.key,
});
@override
State<ReputationScreen> createState() => _ReputationScreenState();
}
class _ReputationScreenState extends State<ReputationScreen> {
ScoresModel? scoresModel;
@override
void initState() {
super.initState();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
if (scoresModel == null) {
ScoresModel.scoreTo(uid.toString(), false, true).then(
(ScoresModel s) => setState(() => scoresModel = s),
);
}
}
Widget _scoreList(List<ScoreDetailModel> list) {
if (list.isEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 40),
child: Center(
child: Text('Sin calificaciones'),
),
);
} else {
return Column(
children: list.map((e) => _scoreItem(e)).toList(),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Reputación'),
body: SingleChildScrollView(
child: _scoreList(scoresModel?.details ?? []),
),
);
}
Widget _scoreItem(ScoreDetailModel scoreDetails) {
return ListTile(
onTap: () {},
leading: ReferencePhoto(
ref: scoreDetails.avatar,
size: 50,
sizeCircle: 50,
sizeIcon: 35,
),
title: Row(
children: [
RatingBar.builder(
initialRating: scoreDetails.score,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 22,
maxRating: 5,
itemPadding: const EdgeInsets.symmetric(horizontal: 0),
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC),
),
onRatingUpdate: (rating) {},
ignoreGestures: true,
),
Text(
' (${scoreDetails.score})',
style: const TextStyle(color: Colors.black54, fontSize: 13),
)
],
),
subtitle: Row(
children: [
Expanded(
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: '${scoreDetails.name}, ',
style: const TextStyle(fontSize: 15, color: Colors.black),
),
TextSpan(
text: '"${scoreDetails.comment}"',
style: const TextStyle(fontSize: 15, color: Colors.grey),
),
],
),
),
),
],
));
}
}
@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/components/primary_btn.dart';
class RequestSentScreen extends StatelessWidget {
const RequestSentScreen({super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pushReplacementNamed(context, '/servicio');
},
label: 'Solicitud enviada',
),
body: Center(
child: Column(
children: [
const Padding(
padding: EdgeInsets.only(top: 90, bottom: 40),
child: Icon(
Icons.check_circle_outline,
size: 50,
color: Color(0xFF35A8ED),
),
),
const Padding(
padding: EdgeInsets.only(bottom: 0),
child: Text('Información enviada con éxito.',
style: TextStyle(color: Color(0xFF2BA4EC), fontSize: 20)),
),
Container(
margin: const EdgeInsets.only(
left: 40, right: 40, top: 50, bottom: 180),
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
decoration: BoxDecoration(
color: const Color(0xFFD6F4FF),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(1, 3),
),
],
),
child: const SizedBox(
width: 350,
child: Row(
children: [
Expanded(
child: Text(
"¡Gracias por suministrar tu información! Revisaremos los datos proporcionados y, una vez confirmados, podrás convertirte en un profesional registrado en ProsApp. ¡Esperamos contar contigo pronto!",
style: TextStyle(color: Colors.black, fontSize: 14),
),
)
],
),
),
),
PrimaryButtom(
onPressed: () {
Navigator.pushReplacementNamed(context, '/servicio');
},
label: 'Inicio',
),
],
),
),
),
);
}
}
@@ -0,0 +1,126 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/components/primary_btn.dart';
import 'package:prosappco/src/controllers/login_email_controller.dart';
class ResetPasswordScreen extends StatelessWidget {
const ResetPasswordScreen({super.key});
@override
Widget build(BuildContext context) {
final controller = Get.put(LoginEmailController());
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Restablecer Contraseña'),
body: SafeArea(
child: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Center(
child: Container(
padding: const EdgeInsets.all(15),
color: Colors.transparent,
width: MediaQuery.of(context).size.width * 0.9,
child: Column(
children: [
const SizedBox(height: 20),
const Text(
'Restablecer contraseña',
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 25),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
const Text(
'Ingresa ingresa tu correo electrónico y te enviaremos un enlace para restablecer tu contraseña',
style: TextStyle(color: Colors.grey, fontSize: 12),
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
const Padding(
padding: EdgeInsets.only(bottom: 5),
child: Align(
alignment: Alignment.topLeft,
child: Text('Email',
style: TextStyle(
fontSize: 18.0, color: Color(0xFF65676B))),
),
),
TextFormField(
controller: controller.email,
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Por favor, ingresa un Email';
}
final RegExp emailRegExp =
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegExp.hasMatch(value)) {
return 'Por favor, ingresa un Email válido';
}
return null;
},
decoration: const InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFFECECEC)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
errorBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Color.fromARGB(255, 184, 0, 0)),
borderRadius: BorderRadius.all(
Radius.circular(50),
)),
hintText: 'Hello@gmail.com',
fillColor: Color.fromARGB(255, 239, 239, 239),
filled: true,
prefixIcon: Icon(Icons.email_outlined),
hintStyle: TextStyle(
color: Colors.grey,
),
),
),
const SizedBox(height: 80),
PrimaryButtom(
onPressed: () async {
try {
await FirebaseAuth.instance.sendPasswordResetEmail(
email: controller.email.text.trim());
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Se ha enviado un enlace de restablecimiento de contraseña a tu correo electrónico.'),
),
);
Navigator.pop(context);
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(
content: Text(
'Hubo un error al enviar el enlace de restablecimiento de contraseña.'),
));
}
},
label: 'Enviar'),
],
),
),
),
)),
);
}
}
+182
View File
@@ -0,0 +1,182 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:intl/intl.dart';
import 'package:prosappco/src/components/photo_view.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/models/user_model.dart';
class ScoreScreen extends StatefulWidget {
final Event evento;
final bool pro;
const ScoreScreen({super.key, required this.evento, required this.pro});
@override
State<ScoreScreen> createState() => ScoreScreenState();
}
class ScoreScreenState extends State<ScoreScreen> {
TextEditingController commentController = TextEditingController();
Reference? ref_photo;
String nombre = '';
double _rating = 1.0;
@override
Widget build(BuildContext context) {
if (nombre == '') {
if (uid != widget.evento.userId) {
UserModel.getUser(widget.evento.userId).then((value) {
UserModel.getUser(uid.toString()).then((me) {
setState(() {
nombre = value.name;
ref_photo = value.photo;
});
});
});
} else {
UserModel.getUser(widget.evento.professionalId).then((value) {
setState(() {
nombre = value.name;
ref_photo = value.photo;
});
});
}
}
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Puntuación'),
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: ListTile(
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: ReferencePhoto(
ref: ref_photo,
size: 50,
sizeCircle: 50,
sizeIcon: 35,
),
),
title: Text(
nombre,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 16),
),
subtitle: Text(
'${DateFormat('dd MMM', 'es').format(DateTime.parse(widget.evento.day))} ${TimeOfDay.fromDateTime(DateTime.parse(widget.evento.range1Hour1)).format(context)}'),
),
),
RatingBar.builder(
initialRating: _rating,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 40,
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,
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 40),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Comentario',
style: TextStyle(fontSize: 18),
),
TextFormField(
maxLines: null,
maxLength: 250,
keyboardType: TextInputType.multiline,
controller: commentController,
),
],
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Column(
children: [
ElevatedButton(
onPressed: () {
if (widget.pro) {
FirebaseFirestore.instance
.collection("services")
.doc(widget.evento.id)
.update({'professional_scored': true}).then((value) {
FirebaseFirestore.instance.collection("scores").add({
"comment": commentController.text,
"from_user": widget.evento.professionalId,
"is_from_professional": false,
"score": _rating,
"to_user": widget.evento.userId,
}).then((value) {
Navigator.pop(context);
});
});
} else {
FirebaseFirestore.instance
.collection("services")
.doc(widget.evento.id)
.update({'user_scored': true}).then((value) {
FirebaseFirestore.instance.collection("scores").add({
"comment": commentController.text,
"from_user": widget.evento.userId,
"is_from_professional": true,
"score": _rating,
"to_user": widget.evento.professionalId,
}).then((value) {
Navigator.pop(context);
});
});
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(230, 60),
),
child: const Text(
'Enviar',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
],
),
),
],
),
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,219 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:intl/intl.dart';
import 'package:prosappco/src/components/photo_view.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/models/scores_model.dart';
import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/models/user_model.dart';
import 'package:prosappco/src/presentation/screens/service.dart';
class ServiceAfterScreen extends StatefulWidget {
var eventoId;
ServiceAfterScreen({super.key, this.eventoId});
@override
State<ServiceAfterScreen> createState() => _ServiceAfterScreenState();
}
class _ServiceAfterScreenState extends State<ServiceAfterScreen> {
Event? evento;
UserModel? user;
ScoresModel? scoresModel;
SettingModel? settings;
@override
void initState() {
super.initState();
if (settings == null) {
SettingModel.getSettings().then(
(SettingModel value) => setState(() {
settings = value;
}),
);
}
Event.getEventById(widget.eventoId).then((value) {
setState(() {
evento = value;
});
if (scoresModel == null) {
ScoresModel.scoreTo(value.professionalId, true, false).then(
(ScoresModel s) => setState(() {
scoresModel = s;
}),
);
}
UserModel.getUser(value.professionalId).then((s) {
setState(
() => user = s,
);
});
});
}
String formatCurrency(int number) {
final formatter =
NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: '');
return '\$${formatter.format(number)}';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const ServiceScreen();
},
),
);
},
label: 'Servicio'),
body: Column(
children: [
ListTile(
leading: ReferencePhoto(
ref: user?.photo,
size: 55,
sizeCircle: 60,
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${user?.name}',
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'${DateFormat('dd MMMM', 'es').format(DateTime.parse(evento?.day ?? '2023-01-01 00:00:00.000Z'))} ${evento?.range1Hour1 != null ? DateFormat('h:mm a').format(DateTime.parse(evento!.range1Hour1)) : ''}',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
),
],
),
subtitle: Column(
children: [
Row(
children: [
RatingBar.builder(
initialRating: scoresModel?.average ?? 0,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 25,
maxRating: 5,
itemPadding: const EdgeInsets.symmetric(horizontal: 0),
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC),
),
onRatingUpdate: (rating) {},
ignoreGestures: true,
),
const SizedBox(width: 5),
Text(
'(${scoresModel?.total.toString()}) ${scoresModel?.average.toStringAsFixed(1)}'),
],
),
],
),
),
Container(
margin:
const EdgeInsets.only(left: 40, right: 40, top: 20, bottom: 20),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
decoration: BoxDecoration(
color: const Color(0xFFD6F4FF),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(1, 3),
),
],
),
child: Row(
children: [
const Icon(
Icons.error_outline,
size: 27,
color: Colors.black54,
),
const SizedBox(width: 15),
evento?.ubicacion != 'sitio'
? const Text(
'Servicio a su domicilio.',
style: TextStyle(color: Colors.black, fontSize: 14),
)
: const Text(
'Servicio en sitio / consultorio',
style: TextStyle(color: Colors.black, fontSize: 14),
),
],
),
),
settings?.tarifas == true && evento?.tarifa != 0
? Column(children: [
Text(
formatCurrency(evento?.tarifa ?? 0),
style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 25),
),
const Text('Tarifa consulta'),
const SizedBox(height: 10)
])
: const SizedBox(),
ListTile(
leading: const Icon(Icons.near_me),
title: Text(
'${evento?.address}',
style: TextStyle(fontSize: 15, color: Colors.grey[600]),
),
),
Text(
'"${evento?.description}"',
style:
TextStyle(color: Colors.grey[600], fontStyle: FontStyle.italic),
),
const Center(
child: Column(
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Icon(
Icons.check_circle_outline_rounded,
color: Color(0xFF35A8ED),
size: 70,
),
),
Text(
'Servicio solicitado exitosamente',
style: TextStyle(
color: Color(0xFF35A8ED),
fontSize: 17,
fontWeight: FontWeight.w600),
),
],
),
),
],
),
);
}
}
@@ -0,0 +1,98 @@
import 'package:diacritic/diacritic.dart';
import 'package:flutter/material.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/presentation/screens/profession.dart';
class ServiceTypeScreen extends StatefulWidget {
const ServiceTypeScreen({super.key});
@override
State<ServiceTypeScreen> createState() => _ServiceTypeScreenState();
}
class _ServiceTypeScreenState extends State<ServiceTypeScreen> {
List<String>? filteredProfessions;
TextEditingController searchController = TextEditingController();
List<String>? _professions;
@override
void initState() {
super.initState();
searchController.addListener(() {
setState(() {
if (_professions != null) {
if (searchController.text.isEmpty) {
filteredProfessions = _professions!;
} else {
filteredProfessions = _professions!
.where((profession) => removeDiacritics(profession)
.toLowerCase()
.contains(
removeDiacritics(searchController.text.toLowerCase())))
.toList();
}
}
});
});
if (_professions == null) {
getProfessions().then((List<String> element) => setState(() {
_professions = element;
filteredProfessions = element;
}));
}
}
@override
Widget build(BuildContext context) {
if (filteredProfessions == null) {
return const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF2BA4EC)),
),
);
}
var professions = filteredProfessions!;
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Tipo de servicio'),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: TextField(
controller: searchController,
decoration: const InputDecoration(
hintText: 'Escribe el tipo de servicio',
prefixIcon: Icon(Icons.assignment_ind_rounded),
),
),
),
Expanded(
child: ListView.builder(
itemCount: professions.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
professions[index],
style: const TextStyle(
fontSize: 18.0,
color: Colors.black,
),
),
onTap: () {
Navigator.pop(context, professions[index]);
},
);
},
),
),
],
),
);
}
}
@@ -0,0 +1,194 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:intl/intl.dart';
import 'package:prosappco/src/components/drawer_professional.dart';
import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/models/scores_model.dart';
import 'package:prosappco/src/presentation/screens/cita.dart';
class SolicitudScreen extends StatelessWidget {
SolicitudScreen({super.key});
DateTime today = DateTime.now();
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
iconTheme: const IconThemeData(
color: Colors.black,
),
title: const Text(
'Solicitudes',
style: TextStyle(
color: Colors.black,
),
),
),
drawer: DrawerProfessional(),
body: SingleChildScrollView(
child: Column(
children: [
_eventList(),
],
),
),
),
);
}
Widget _eventList() {
return StreamBuilder<List<Event>>(
stream: FirebaseFirestore.instance
.collection('services')
.where('professional_id', isEqualTo: uid)
.where('status', whereIn: ['pendiente', ''])
.snapshots()
.asyncMap((snapshot) async {
try {
List<Event> eventos = [];
for (var element in snapshot.docs) {
final event = Event.fromJson(element.data());
event.scoresModel =
await ScoresModel.scoreTo(event.userId, false, false);
event.id = element.id;
eventos.add(event);
}
return eventos;
} catch (e) {
print('Error getByProId $e');
return [];
}
}),
builder: (BuildContext context, AsyncSnapshot<List<Event>> snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
List<Event> eventos = [];
try {
eventos.addAll(snapshot.data!);
eventos.sort((a, b) => a.timeStamp!.compareTo(b.timeStamp!));
} catch (e) {
print("Error" + e.toString());
}
if (eventos.isEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 50),
child: Center(child: Text('No tienes citas')),
);
}
return Column(
children: [
...eventos.map(
(event) => ListTile(
onTap: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return CitaScreen(evento: event);
},
),
);
},
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(DateFormat('h:mm a')
.format(DateTime.parse(event.range1Hour1))),
],
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.title,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'${DateFormat('dd MMMM', 'es').format(DateTime.parse(event.day))} - ${DateFormat('h:mm a').format(DateTime.parse(event.range1Hour1))}',
style: const TextStyle(
color: Colors.grey,
fontSize: 16,
),
)
],
),
// RichText(
// text: TextSpan(
// children: [
// TextSpan(
// text: '${event.title}, ',
// style: const TextStyle(
// color: Colors.black,
// fontWeight: FontWeight.bold,
// fontSize: 16,
// ),
// ),
// TextSpan(
// text: DateFormat('dd MMM', 'es')
// .format(DateTime.parse(event.day)),
// style: const TextStyle(
// color: Colors.grey,
// fontSize: 16,
// ),
// ),
// ],
// ),
// ),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
RatingBar.builder(
initialRating: event.scoresModel?.average ?? 0,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 25,
maxRating: 5,
itemPadding:
const EdgeInsets.symmetric(horizontal: 0),
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC),
),
onRatingUpdate: (rating) {},
ignoreGestures: true,
),
const SizedBox(width: 5),
Text(
'(${event.scoresModel?.total.toString()}) ${event.scoresModel?.average.toStringAsFixed(1)}'),
],
),
Text(
'"${event.description}"',
style: const TextStyle(fontStyle: FontStyle.italic),
),
],
),
trailing: const Icon(Icons.keyboard_arrow_right),
),
),
],
);
},
);
}
}
+157
View File
@@ -0,0 +1,157 @@
import 'package:community_material_icon/community_material_icon.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/setting_model.dart';
import 'package:url_launcher/url_launcher.dart';
class SupportScreen extends StatefulWidget {
const SupportScreen({super.key});
@override
State<SupportScreen> createState() => SsupportStateScreen();
}
class SsupportStateScreen extends State<SupportScreen> {
SettingModel? settings;
final String subject = 'Soporte Prosapp';
final String body = '';
Future<void> _sendWhatsapp(String? number) async {
final _whatsappUrl = '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';
if (!await launch(_whatsappUrl)) {
throw Exception('Could not launch $_whatsappUrl');
}
}
Future<void> _sendEmail(String recipients) async {
final Email email = Email(
body: body,
subject: subject,
recipients: [recipients],
isHTML: false,
);
await FlutterEmailSender.send(email);
}
void _sendEmailWeb(String recipients) async {
final email = 'mailto:$recipients?subject=${Uri.encodeComponent(recipients)}&body=${Uri.encodeComponent(body)}';
if (await canLaunch(email)) {
await launch(email);
} else {}
}
@override
void initState() {
super.initState();
if (settings == null) {
SettingModel.getSettings().then(
(SettingModel value) => setState(() {
settings = value;
}),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Soporte'),
body: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 30, left: 35, right: 35),
child: Text(
'${settings?.titulo}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 35),
child: Text('${settings?.parrafo}'),
),
Row(
children: [
const Expanded(child: SizedBox()),
ElevatedButton(
onPressed: () {
_sendWhatsapp(settings?.numero);
},
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
side: const BorderSide(
color: Color(0xFF2BA4EC),
width: 2,
),
),
),
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 18, horizontal: 0),
child: Icon(
CommunityMaterialIcons.whatsapp,
size: 30,
color: Colors.white,
),
),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: () {
if (kIsWeb) {
_sendEmailWeb(settings?.email ?? '');
} else {
_sendEmail(settings?.email ?? '');
}
},
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
side: const BorderSide(
color: Color(0xFF2BA4EC),
width: 2,
),
),
),
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 18, horizontal: 0),
child: Icon(
Icons.email_outlined,
size: 30,
color: Colors.white,
),
),
),
const Expanded(child: SizedBox()),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 50),
child: Column(
children: [
const Text(
'Horario de atención:',
style: TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 20),
Text('${settings?.dias}'),
const SizedBox(height: 5),
Text('${settings?.horas}'),
],
),
),
],
),
);
}
}
@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:prosappco/src/components/network_utility.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import '../../authentication/authentication_repository.dart';
class UbicacionScreen extends StatefulWidget {
const UbicacionScreen({super.key});
@override
State<UbicacionScreen> createState() => _UbicacionScreenState();
}
class _UbicacionScreenState extends State<UbicacionScreen> {
List<dynamic> _placesList = [];
String selectedPlace = '';
String _coordsOfCity = '0.0,0.0';
@override
void initState() {
super.initState();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
if (_coordsOfCity == '0.0,0.0') {
AuthenticationRepository.instance
.getCoordsOfCity(uid.toString())
.then((String s) => setState(() {
_coordsOfCity = s;
}));
}
}
void placeAutoComplete(String query) async {
Uri uri = Uri.https("admin.prosapp.co", "/autocomplete", {
"input": query,
"location": _coordsOfCity,
});
String? response = await NetworkUtility.fetchUrl(uri);
if (response != null) {
setState(() {
_placesList = jsonDecode(response.toString())['results'];
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Tu ubicación'),
body: Center(
child: SizedBox(
width: 300,
child: Column(
children: [
const SizedBox(height: 20.0),
TextFormField(
decoration: const InputDecoration(
hintText: 'Escribe tu ubicación',
prefixIcon: Icon(Icons.location_on),
),
onChanged: (value) {
String modifiedValue = value.replaceAll(' ', '_');
placeAutoComplete(modifiedValue);
},
),
const SizedBox(height: 20.0),
Expanded(
child: ListView.builder(
itemCount: _placesList.length,
itemBuilder: (context, index) {
return ListTile(
onTap: () {
Navigator.pop(context, [
_placesList[index]['formatted_address'],
_placesList[index]['geometry']['location']['lat'],
_placesList[index]['geometry']['location']['lng']
]);
},
title: Text(_placesList[index]['formatted_address']),
);
},
),
),
],
),
),
),
);
}
}
@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:webview_flutter/webview_flutter.dart';
class WebViewScreen extends StatefulWidget {
String label;
String link;
WebViewScreen({super.key, required this.label, required this.link});
@override
State<WebViewScreen> createState() => _WebViewScreenState();
}
class _WebViewScreenState extends State<WebViewScreen> {
bool isLoading = true;
late WebViewController controller;
@override
void initState() {
super.initState();
controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {},
onPageStarted: (String url) {
setState(() {
isLoading = true;
});
},
onPageFinished: (String url) {
setState(() {
isLoading = false;
});
},
onWebResourceError: (WebResourceError error) {},
onNavigationRequest: (NavigationRequest request) {
if (request.url.startsWith('https://www.youtube.com/')) {
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadRequest(Uri.parse(widget.link));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: widget.label,
),
body: Stack(
children: [
if (isLoading)
const Center(child: CircularProgressIndicator())
else
const SizedBox(),
WebViewWidget(controller: controller),
],
),
);
}
}
@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
class BirthDatePicker extends StatefulWidget {
final Function(DateTime) onDateSelected;
final TextEditingController controller;
const BirthDatePicker(
{Key? key, required this.onDateSelected, required this.controller})
: super(key: key);
@override
_BirthDatePickerState createState() => _BirthDatePickerState();
}
class _BirthDatePickerState extends State<BirthDatePicker> {
DateTime selectedDate =
DateTime.now().subtract(const Duration(days: 365 * 20));
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime.now().subtract(const Duration(days: 365 * 120)),
lastDate: DateTime.now(),
);
if (picked != null && picked != selectedDate) {
setState(() {
selectedDate = picked;
widget.onDateSelected(selectedDate);
});
}
}
@override
Widget build(BuildContext context) {
return TextFormField(
onTap: () {
_selectDate(context);
},
readOnly: true,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.calendar_month),
suffixIcon: Icon(Icons.arrow_drop_down),
hintText: 'Fecha de nacimiento',
),
controller: widget.controller,
);
}
}
@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
class GenderDropdown extends StatefulWidget {
final Function(String) onChanged;
const GenderDropdown({Key? key, required this.onChanged}) : super(key: key);
@override
_GenderDropdownState createState() => _GenderDropdownState();
}
class _GenderDropdownState extends State<GenderDropdown> {
String? selectedGender;
@override
Widget build(BuildContext context) {
return InputDecorator(
decoration: const InputDecoration(
border: UnderlineInputBorder(),
prefixIcon: Icon(Icons.people_outline),
contentPadding: EdgeInsets.symmetric(horizontal: 8.0),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
value: selectedGender,
hint: const Text('Selecciona tu género'),
onChanged: (String? newValue) {
setState(() {
selectedGender = newValue;
widget.onChanged(selectedGender!);
});
},
items: ['Masculino', 'Femenino', 'Otro']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
),
);
}
}
@@ -0,0 +1,452 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/photo_view.dart';
import 'package:prosappco/src/models/scores_model.dart';
import 'package:prosappco/src/models/user_model.dart';
import 'package:prosappco/src/providers/user_provider.dart';
import 'package:prosappco/src/presentation/screens/configuracion.dart';
import 'package:prosappco/src/presentation/screens/messages_user.dart';
import 'package:prosappco/src/presentation/screens/professional_profile_web.dart';
import 'package:prosappco/src/presentation/screens/profile/profile.dart';
import 'package:prosappco/src/presentation/screens/profile/profile_web.dart';
import 'package:prosappco/src/presentation/screens/reputation.dart';
import 'package:prosappco/src/presentation/screens/support.dart';
import 'package:prosappco/src/presentation/screens/web_view.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
class DrawerMenu extends StatelessWidget {
final uid = AuthenticationRepository.instance.getCurrentUserUid();
Future<void> _irSugerencias() async {
const url = 'https://admin.prosapp.co/sugerencias';
final Uri _url = Uri.parse(url);
if (await canLaunchUrl(_url)) {
await launchUrl(_url);
} else {
throw 'No se pudo abrir la URL $url';
}
}
@override
Widget build(BuildContext context) {
final userProvider = Provider.of<UserProvider>(context);
UserModel? user = userProvider.user;
ScoresModel? score = userProvider.score;
return Drawer(
child: Column(
children: [
Container(
color: Colors.white,
child: Column(
children: [
infoUser(context, user),
], //
),
),
Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 0), // changes position of shadow
),
],
),
child: Divider(
height: 0,
color: Colors.grey[300],
),
),
Expanded(
child: Column(
children: [
ListTile(
onTap: () {
if (ModalRoute.of(context)?.settings.name !=
'/misservicios') {
Navigator.pushNamed(context, '/misservicios');
} else {
Scaffold.of(context).openEndDrawer();
}
},
leading: const Icon(
Icons.history,
color: Colors.black,
),
title: const Text(
'Mis servicios',
style: TextStyle(fontSize: 15),
),
),
ListTile(
onTap: () {
if (kIsWeb) {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const ProfileWebScreen();
},
),
);
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const ProfileScreen();
},
),
);
}
},
leading: const Icon(
Icons.person_outline,
color: Colors.black,
),
title: const Text(
'Mi perfil',
style: TextStyle(fontSize: 15),
),
),
ListTile(
onTap: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const ConfiguracionScreen();
},
),
);
},
leading: const Icon(
Icons.construction_outlined,
color: Colors.black,
),
title: const Text(
'Configuración',
style: TextStyle(fontSize: 15),
),
),
ListTile(
onTap: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const SupportScreen();
},
),
);
},
leading: const Icon(
Icons.question_mark_rounded,
color: Colors.black,
),
title: const Text(
'Soporte',
style: TextStyle(fontSize: 15),
),
),
ListTile(
onTap: () {
if (kIsWeb) {
_irSugerencias();
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return WebViewScreen(
label: 'Sugerencias',
link: 'https://admin.prosapp.co/sugerencias');
},
),
);
}
},
leading: const Icon(
Icons.campaign_outlined,
color: Colors.black,
),
title: const Text(
'Sugerencias',
style: TextStyle(fontSize: 15),
),
),
// ListTile(
// onTap: () {
// Navigator.push(
// context,
// CupertinoPageRoute(
// builder: (BuildContext context) {
// return const MessagesUserScreen();
// },
// ),
// );
// },
// leading: const Icon(
// Icons.messenger_outline,
// color: Colors.black,
// ),
// title: const Text(
// 'Mensajes',
// style: TextStyle(fontSize: 15),
// ),
// ),
Builder(builder: (BuildContext context) {
return ListTile(
onTap: () {
if (ModalRoute.of(context)?.settings.name !=
'/servicio') {
Navigator.pushNamed(context, '/servicio');
} else {
Scaffold.of(context).openEndDrawer();
}
},
trailing: const Icon(
Icons.keyboard_arrow_right,
color: Colors.white,
),
title: const Text(
'Solicitar servicio',
style: TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.bold),
),
tileColor: const Color(0xFF2BA4EC),
contentPadding:
const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
);
}),
Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 3,
offset:
const Offset(0, 2), // changes position of shadow
),
],
),
child: Container(
color: const Color(0xFFD6F4FF),
child: ListTile(
tileColor: const Color(0xFFD6F4FF),
onTap: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const ReputationScreen();
},
),
);
},
trailing: const Icon(Icons.keyboard_arrow_right,
color: Colors.black),
title: const Text(
'Reputación',
style: TextStyle(color: Colors.black),
),
subtitle: Row(
children: [
RatingBar.builder(
initialRating: score?.average ?? 0,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 25,
maxRating: 5,
itemPadding:
const EdgeInsets.symmetric(horizontal: 0),
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC),
),
onRatingUpdate: (rating) {},
ignoreGestures: true,
),
const SizedBox(width: 5),
Text(
'(${score?.total.toString()}) ${score?.average.toStringAsFixed(1)}'),
],
),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Prosapp',
style: TextStyle(fontSize: 10, color: Colors.grey[700]),
),
Padding(
padding: const EdgeInsets.only(top: 7, left: 3, right: 3),
child: Text(
'®',
style: TextStyle(fontSize: 25, color: Colors.grey[700]),
),
),
Text(
'todos los derechos reservados',
style: TextStyle(fontSize: 10, color: Colors.grey[700]),
),
],
),
],
),
),
Column(
children: [
ElevatedButton(
onPressed: () {
UserModel? user = userProvider.user;
if (user?.name == '' ||
user?.city == '' ||
user?.phoneNumber == '' ||
user?.phoneNumber == null) {
Get.snackbar(
'Completa tu perfil',
'Para ser un profesional registrado, asegúrate de llenar todos los campos necesarios y no olvides guardar tus cambios para que surtan efecto.',
snackPosition: SnackPosition.BOTTOM,
);
} else {
if (user?.phoneNumber != user?.phoneNumber) {
Get.snackbar(
'Tu número de teléfono sigue sin cambios.',
'Para guardar esta información, dirígete a tu perfil y selecciona la opción Guardar',
snackPosition: SnackPosition.BOTTOM,
);
} else {
if (user?.state == 'pendiente' || user?.state == null) {
Navigator.pop(context);
if (kIsWeb) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const ProfessionalProfileWebScreen(),
),
);
} else {
Navigator.pushNamed(context, '/profesionalProfile');
}
} else if (user?.state == 'revision') {
Navigator.pushNamed(context, '/profesionalRevision');
} else if (user?.state == 'activo') {
Navigator.pushNamed(context, '/solicitud');
}
}
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(230, 45),
),
child: const Text(
'Modo profesional',
style: TextStyle(
color: Colors.white,
fontSize: 15,
),
),
),
const SizedBox(height: 5),
ElevatedButton(
onPressed: () async {
await AuthenticationRepository.instance.logout(uid!);
userProvider.setNullUser();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
minimumSize: const Size(230, 40),
),
child: const Text(
'Cerrar Sesión',
style: TextStyle(
color: Colors.white,
fontSize: 15,
),
),
),
],
),
const SizedBox(height: 10),
],
),
);
}
ListTile infoUser(BuildContext context, UserModel? user) {
return ListTile(
onTap: () {
if (kIsWeb) {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const ProfileWebScreen();
},
),
);
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const ProfileScreen();
},
),
);
}
},
title: Text(user?.name ?? '',
style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user?.phoneNumber ?? '',
style: const TextStyle(fontSize: 12),
),
Text(
user?.city ?? '',
style: const TextStyle(fontSize: 12),
),
],
),
leading: ReferencePhoto(
ref: user?.photo,
size: 55,
sizeCircle: 60,
),
trailing: const Icon(Icons.keyboard_arrow_right, color: Colors.black),
contentPadding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
);
}
}
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
class PrimaryButton extends StatelessWidget {
final VoidCallback onPressed;
final String text;
final double? minWidth;
final double? minHeight;
const PrimaryButton(
{super.key,
required this.onPressed,
required this.text,
this.minWidth = 200,
this.minHeight = 50});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: Size(minWidth!, minHeight!),
),
child: Text(
text,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 17,
),
));
}
}