- Remove `get` package (incompatible with Flutter 3.44/Dart 3.12 due to removed ThemeData.backgroundColor and final IconData class) - Replace GetMaterialApp → MaterialApp with GlobalKey navigatorKey - Convert AuthenticationRepository and all 7 controllers from GetxController to plain singletons - Replace Get.snackbar/offAll/to/back/defaultDialog with app_navigator helpers and native Flutter APIs - Add ApiService.baseUrl static + parseJson method - Add UserModel.getUser static method - Add UserProvider.score via ScoresModel API - Fix user?.photo → user?.picture in drawer_menu, service_after - Fix logout(uid!) → logout() in drawer_menu - Fix photo_view_web.dart missing dart:typed_data import - Remove getCoordsOfCity call from ubicacion.dart - Fix UserModel.getUser?.name null-safety in map/service.dart - Upgrade: google_maps_flutter ^2.9.0, url_launcher ^6.3.0, font_awesome_flutter ^10.8.0, image_picker ^1.1.2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
385 lines
15 KiB
Dart
385 lines
15 KiB
Dart
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'dart:io';
|
|
import 'package:prosappco/src/utils/app_navigator.dart';
|
|
import 'package:http/http.dart' as http;
|
|
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/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/presentation/widgets/shared/primary_checkbox.dart';
|
|
import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart';
|
|
import 'package:prosappco/src/providers/user_provider.dart';
|
|
import 'package:prosappco/src/services/api_service.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 'package:provider/provider.dart';
|
|
import 'package:universal_html/html.dart' as html;
|
|
|
|
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;
|
|
Uint8List? webImageBytes;
|
|
final DateFormat formatter = DateFormat('dd/MM/yyyy');
|
|
final uid = AuthenticationRepository.instance.getCurrentUserUid();
|
|
bool _obscureText = true;
|
|
final _formKey = GlobalKey<FormState>();
|
|
final controller = NameEmailCityController.instance;
|
|
final _phoneNumberController = TextEditingController();
|
|
final _nameController = TextEditingController();
|
|
final _emailController = TextEditingController();
|
|
final _passwordController = TextEditingController();
|
|
|
|
var _ciudad = '...';
|
|
String? _photoUrl;
|
|
String? _email;
|
|
String gender = '';
|
|
String genderDb = '';
|
|
DateTime? birthDate;
|
|
String birthDateDb = '';
|
|
bool enableLoginWithEmail = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadCurrentUser();
|
|
}
|
|
|
|
Future<void> _loadCurrentUser() async {
|
|
final currentUser = AuthenticationRepository.instance.currentUser.value;
|
|
if (currentUser != null) {
|
|
_nameController.text = currentUser.name;
|
|
_emailController.text = currentUser.email ?? '';
|
|
_phoneNumberController.text = currentUser.phoneNumber ?? '';
|
|
_photoUrl = currentUser.picture;
|
|
_ciudad = currentUser.city.isNotEmpty ? currentUser.city : '...';
|
|
_email = currentUser.email;
|
|
genderDb = currentUser.gender ?? '';
|
|
birthDateDb = currentUser.birthday ?? '';
|
|
} else {
|
|
// Fallback: fetch from API
|
|
try {
|
|
final Map<String, dynamic> data =
|
|
await ApiService.instance.get('/auth/me');
|
|
if (mounted) {
|
|
setState(() {
|
|
_nameController.text = data['name'] ?? '';
|
|
_emailController.text = data['email'] ?? '';
|
|
_phoneNumberController.text = data['phone'] ?? '';
|
|
_photoUrl = data['picture'];
|
|
_ciudad = data['city'] ?? '...';
|
|
_email = data['email'];
|
|
genderDb = data['gender'] ?? '';
|
|
birthDateDb = data['birthday'] ?? '';
|
|
});
|
|
}
|
|
} catch (e) {
|
|
print('Error loading user: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<String?> _uploadImageFile(File image) async {
|
|
try {
|
|
final uri =
|
|
Uri.parse('${ApiService.baseUrl}/storage/upload');
|
|
final request = http.MultipartRequest('POST', uri);
|
|
final token = await ApiService.instance.getToken();
|
|
if (token != null) {
|
|
request.headers['Authorization'] = 'Bearer $token';
|
|
}
|
|
request.files.add(await http.MultipartFile.fromPath('file', image.path));
|
|
final streamed = await request.send();
|
|
final resp = await http.Response.fromStream(streamed);
|
|
if (resp.statusCode >= 200 && resp.statusCode < 300) {
|
|
final json = ApiService.instance.parseJson(resp.body);
|
|
return json['url'] as String?;
|
|
}
|
|
} catch (e) {
|
|
print('Error uploading image: $e');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<void> updateInfo() async {
|
|
final Map<String, dynamic> body = {};
|
|
|
|
final newName = _nameController.text.trim();
|
|
if (newName.isNotEmpty) body['name'] = newName;
|
|
if (_ciudad != '...' && _ciudad.isNotEmpty) body['city'] = _ciudad;
|
|
if (gender.isNotEmpty) body['gender'] = gender;
|
|
if (birthDate != null) body['birthday'] = birthDate.toString();
|
|
|
|
try {
|
|
if (imagen_to_upload != null) {
|
|
final url = await _uploadImageFile(imagen_to_upload!);
|
|
if (url != null) body['picture'] = url;
|
|
}
|
|
|
|
await ApiService.instance.patch('/users/me', body);
|
|
|
|
WarningSnackbar.show(
|
|
title: 'Informacion actualizada',
|
|
message: 'Tu informacion ha sido actualizada con exito.',
|
|
icon: const Icon(Icons.check, color: Colors.white),
|
|
backgroundColor: Colors.green,
|
|
);
|
|
} catch (e) {
|
|
print('Error updating info: $e');
|
|
showAppSnackBar('Error', 'No se pudo actualizar la información.');
|
|
}
|
|
}
|
|
|
|
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();
|
|
final userProvider = Provider.of<UserProvider>(context);
|
|
|
|
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: _photoUrl),
|
|
),
|
|
),
|
|
Container(
|
|
width: 300,
|
|
padding: const EdgeInsets.only(top: 0),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
children: [
|
|
TextFormField(
|
|
controller: _nameController,
|
|
maxLength: 50,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.deny(RegExp(r'\s{2,}')),
|
|
],
|
|
validator: (value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Porfavor ingrese un nombre.';
|
|
}
|
|
if (value.trim().length < 5) {
|
|
return 'Debe tener al menos 5 caracteres.';
|
|
}
|
|
return null;
|
|
},
|
|
decoration: const InputDecoration(
|
|
prefixIcon: Icon(Icons.person_outline),
|
|
hintText: 'Nombre (Obligatorio)',
|
|
),
|
|
),
|
|
const SizedBox(),
|
|
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 == '' || 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',
|
|
),
|
|
),
|
|
genderDb == ''
|
|
? const SizedBox(height: 20.0)
|
|
: const SizedBox(),
|
|
genderDb == ''
|
|
? GenderDropdown(
|
|
onChanged: (selectedGender) {
|
|
setState(() {
|
|
gender = selectedGender;
|
|
});
|
|
},
|
|
)
|
|
: const SizedBox(),
|
|
genderDb == ''
|
|
? const SizedBox(height: 20.0)
|
|
: const SizedBox(),
|
|
birthDateDb == ''
|
|
? BirthDatePicker(
|
|
onDateSelected: (birthDay) {
|
|
setState(() {
|
|
birthDate = birthDay;
|
|
});
|
|
},
|
|
controller: TextEditingController(
|
|
text: birthDate == null
|
|
? ''
|
|
: formatter.format(birthDate!),
|
|
),
|
|
)
|
|
: const SizedBox(),
|
|
_email != null && _email != ''
|
|
? const SizedBox(height: 20.0)
|
|
: const SizedBox(),
|
|
_email != null && _email != ''
|
|
? TextFormField(
|
|
readOnly: true,
|
|
controller: _emailController,
|
|
decoration: const InputDecoration(
|
|
prefixIcon: Icon(Icons.email_outlined),
|
|
hintText: 'Email',
|
|
),
|
|
)
|
|
: const SizedBox(),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
alignment: Alignment.bottomCenter,
|
|
margin: const EdgeInsets.only(top: 35),
|
|
padding: const EdgeInsets.only(bottom: 30),
|
|
child: Column(
|
|
children: [
|
|
_email != null && _email != ''
|
|
? 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: 40),
|
|
PrimaryButton(
|
|
onPressed: () async {
|
|
if (_formKey.currentState!.validate()) {
|
|
await updateInfo();
|
|
if (kIsWeb) {
|
|
html.window.location.reload();
|
|
}
|
|
}
|
|
await userProvider.updateUserDataAndScores();
|
|
},
|
|
text: 'Guardar',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|