update
This commit is contained in:
@@ -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 ?? ''),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user