replace: swap prosappweb content for prosapp_web_app (more complete version)

prosapp_web_app has chat, dashboard, calendar, support, 13 providers and
Fluro URL routing. Keep Dockerfile + nginx.conf from previous prosappweb.
Upgrade google_fonts 6.2.1 → 8.1.0 (Dart 3.12 compat fix).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-18 15:26:32 -05:00
co-authored by Claude Sonnet 4.6
parent 74a4f41902
commit 15175c1b91
334 changed files with 12914 additions and 25727 deletions
-76
View File
@@ -1,76 +0,0 @@
const String GOOGLE_MAPS_API_KEY = 'AIzaSyCW_og6qQ8W8G-5_BxIS4sBnl8cLkjL95s';
const String MAP_STYLE = '''
[
{
"elementType": "labels.icon",
"stylers": [
{
"visibility": "on",
"color": "#6F6F6F"
}
]
},
{
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#616161"
}
]
},
{
"elementType": "labels.text.stroke",
"stylers": [
{
"color": "#f5f5f5"
}
]
},
{
"featureType": "administrative.land_parcel",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#bdbdbd"
}
]
},
{
"featureType": "poi",
"elementType": "geometry",
"stylers": [
{
"color": "#eeeeee"
}
]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#757575"
}
]
},
{
"featureType": "poi.park",
"elementType": "geometry",
"stylers": [
{
"color": "#e5e5e5"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9e9e9e"
}
]
}
]
''';
-2
View File
@@ -1,2 +0,0 @@
// ponytail: Firebase removed; this file is kept as an empty stub so any lingering references compile.
// No imports, no exports needed.
+92 -104
View File
@@ -1,46 +1,81 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/presentation/screens/service_web.dart';
import 'package:prosappco/src/providers/user_provider.dart';
import 'package:prosappco/src/presentation/screens/calendar.dart';
import 'package:prosappco/src/presentation/screens/city.dart';
import 'package:prosappco/src/presentation/screens/login/login.dart';
import 'package:prosappco/src/presentation/screens/login/login_email.dart';
import 'package:prosappco/src/presentation/screens/my_services.dart';
import 'package:prosappco/src/presentation/screens/my_services_pro.dart';
import 'package:prosappco/src/presentation/screens/new_number.dart';
import 'package:prosappco/src/presentation/screens/new_password.dart';
import 'package:prosappco/src/presentation/screens/profession.dart';
import 'package:prosappco/src/presentation/screens/professional_profile.dart';
import 'package:prosappco/src/presentation/screens/professional_revision.dart';
import 'package:prosappco/src/presentation/screens/profile/profile.dart';
import 'package:prosappco/src/presentation/screens/profile/profile_pro.dart';
import 'package:prosappco/src/presentation/screens/register/register.dart';
import 'package:prosappco/src/presentation/screens/request_sent.dart';
import 'package:prosappco/src/presentation/screens/reset_password/reset_password.dart';
import 'package:prosappco/src/presentation/screens/map/service.dart';
import 'package:prosappco/src/presentation/screens/solicitudes.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/calendar_services_provider.dart';
import 'package:prosapp_web_app/providers/chat_provider.dart';
import 'package:prosapp_web_app/providers/cities_provider.dart';
import 'package:prosapp_web_app/providers/professional_detail_provider.dart';
import 'package:prosapp_web_app/providers/professional_form_provider.dart';
import 'package:prosapp_web_app/providers/professional_provider.dart';
import 'package:prosapp_web_app/providers/professionals_provider.dart';
import 'package:prosapp_web_app/providers/professions_provider.dart';
import 'package:prosapp_web_app/providers/profile_form_provider.dart';
import 'package:prosapp_web_app/providers/score_provider.dart';
import 'package:prosapp_web_app/providers/services_provider.dart';
import 'package:prosapp_web_app/providers/settings_provider.dart';
import 'package:prosapp_web_app/providers/sidemenu_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/services/local_storage.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:prosapp_web_app/services/notifications_service.dart';
import 'package:prosapp_web_app/ui/layouts/auth/auth_layout.dart';
import 'package:prosapp_web_app/ui/layouts/dashboard/dashboard_layout.dart';
import 'package:prosapp_web_app/ui/layouts/splash/splash_layout.dart';
import 'package:provider/provider.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:prosappco/src/utils/app_navigator.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Trigger singleton init which schedules _checkSession after first frame
AuthenticationRepository.instance;
await dotenv.load(fileName: ".env");
await initializeDateFormatting('es_MX', null);
await Firebase.initializeApp(
options: FirebaseOptions(
apiKey: dotenv.env['API_KEY']!,
authDomain: dotenv.env['AUTH_DOMAIN'],
projectId: dotenv.env['PROJECT_ID']!,
storageBucket: dotenv.env['STORAGE_BUCKET'],
messagingSenderId: dotenv.env['MESSAGING_SENDER_ID']!,
appId: dotenv.env['APP_ID']!,
measurementId: dotenv.env['MEASUREMENT_ID'],
),
);
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
await LocalStorage.configurePrefs();
runApp(const MyApp());
await initializeDateFormatting('es_ES', null);
Flurorouter.configureRoutes();
runApp(const AppState());
}
class AppState extends StatelessWidget {
const AppState({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(lazy: false, create: (_) => AuthProvider()),
ChangeNotifierProvider(lazy: false, create: (_) => SideMenuProvider()),
ChangeNotifierProvider(lazy: false, create: (_) => ProfessionalProvider()),
ChangeNotifierProvider(create: (_) => SettingsProvider()),
ChangeNotifierProvider(create: (_) => ProfileFormProvider()),
ChangeNotifierProvider(create: (_) => ProfessionalFormProvider()),
ChangeNotifierProvider(create: (_) => ProfessionalsProvider()),
ChangeNotifierProvider(create: (_) => ServicesProvider()),
ChangeNotifierProvider(create: (_) => CalendarServicesProvider()),
ChangeNotifierProvider(create: (_) => CitiesProvider()),
ChangeNotifierProvider(create: (_) => ProfessionsProvider()),
ChangeNotifierProvider(create: (_) => ProfessionalDetailProvider()),
ChangeNotifierProvider(create: (_) => ScoreProvider()),
ChangeNotifierProvider(create: (_) => ChatProvider()),
],
child: const MyApp(),
);
}
}
class MyApp extends StatelessWidget {
@@ -48,79 +83,32 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => UserProvider()),
],
child: MaterialApp(
navigatorKey: appNavigatorKey,
theme: ThemeData(fontFamily: 'Poppins'),
debugShowCheckedModeBanner: false,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('es', 'US')],
title: 'ProsApp',
initialRoute: '/',
routes: {
'/': (context) => const LoginScreen(),
'/splash': (context) => const SplashScreen(),
'/login': (context) => const LoginEmailScreen(),
'/profile': (context) => const ProfileScreen(),
'/register': (context) => const RegisterScreen(),
'/city': (context) => const CityScreen(),
'/profession': (context) => const ProfessionScreen(),
'/newNumber': (context) => const NewNumberScreen(),
'/profesionalRevision': (context) => const ProfessionalRevisionScreen(),
'/profesionalProfile': (context) => const ProfessionalProfileScreen(),
'/solicitudEnviada': (context) => const RequestSentScreen(),
'/nuevaPassword': (context) => NewPasswordScreen(),
'/servicio': (context) => const ServiceScreen(),
'/servicioWeb': (context) => const ServiceWebScreen(),
'/profilePro': (context) => const ProfileProScreen(),
'/calendar': (context) => const CalendarScreen(),
'/solicitud': (context) => SolicitudScreen(),
'/misserviciospro': (context) => MyServicesProScreen(),
'/misservicios': (context) => MyServicesScreen(),
'/resetpassword': (context) => const ResetPasswordScreen(),
},
onGenerateRoute: (settings) {
throw Exception('Ruta desconocida: ${settings.name}');
},
),
);
}
}
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'ProsApp',
initialRoute: '/',
onGenerateRoute: Flurorouter.router.generator,
navigatorKey: NavigationService.navigatorKey,
scaffoldMessengerKey: NotificationsService.messengerKey,
builder: (_, child) {
final authProvider = Provider.of<AuthProvider>(context);
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
if (authProvider.authStatus == AuthStatus.checking) {
return const SplashLayout();
}
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
if (kIsWeb) {
Future.delayed(const Duration(seconds: 3), () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginScreen()),
);
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.blue,
body: Center(
child: kIsWeb ? Image.asset('/images/splash.png') : const LoginScreen(),
if (authProvider.authStatus == AuthStatus.authenticated) {
return DashboardLayout(child: child!);
} else {
return AuthLayout(child: child!);
}
},
theme: ThemeData.light().copyWith(
scrollbarTheme: const ScrollbarThemeData().copyWith(
thumbColor: WidgetStateProperty.all(
Colors.grey.withOpacity(0.5),
),
),
),
);
}
+45
View File
@@ -0,0 +1,45 @@
import 'package:prosapp_web_app/models/message_entity.dart';
class ChatEntity {
final String? id;
final String userId;
final String professionalId;
final List<MessageEntity> messages;
const ChatEntity({
required this.id,
required this.userId,
required this.professionalId,
required this.messages,
});
static ChatEntity fromDocument(Map<String, dynamic> doc) {
return ChatEntity(
id: doc['id'] as String,
userId: doc['user_id'] as String,
professionalId: doc['professional_id'] as String,
messages: (doc['messages'] as List)
.map((e) => MessageEntity.fromDocument(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> toDocument() {
return {
'id': id,
'user_id': userId,
'professional_id': professionalId,
'messages': messages.map((e) => e.toDocument()).toList(),
};
}
@override
String toString() {
return '''ChatEntity{
id: $id,
userId: $userId,
professionalId: $professionalId,
messages: $messages
}''';
}
}
+13
View File
@@ -0,0 +1,13 @@
class City {
final String cityName;
final String coordsOfCity;
final String stateOfCity;
final String countryOfCity;
const City({
required this.cityName,
required this.coordsOfCity,
required this.stateOfCity,
required this.countryOfCity,
});
}
+31
View File
@@ -0,0 +1,31 @@
class CityEntity {
final String name;
final String coords;
const CityEntity({
required this.name,
required this.coords,
});
Map<String, Object?> toDocument() {
return {
'name': name,
'coords': coords,
};
}
static CityEntity fromDocument(Map<String, dynamic> doc) {
return CityEntity(
name: doc['name'] as String,
coords: doc['coords'] as String,
);
}
@override
String toString() {
return '''CityEntity {
name: $name
coords: $coords
}''';
}
}
+58
View File
@@ -0,0 +1,58 @@
import 'package:cloud_firestore/cloud_firestore.dart';
class CommentEntity {
final String authorId;
final String destinationId;
final String serviceId;
final String content;
final double score;
final bool isFromUser;
final Timestamp createdAt;
const CommentEntity({
required this.authorId,
required this.destinationId,
required this.serviceId,
required this.content,
required this.score,
required this.isFromUser,
required this.createdAt,
});
static CommentEntity fromDocument(Map<String, dynamic> doc) {
return CommentEntity(
authorId: doc['author_id'] as String,
destinationId: doc['destination_id'] as String,
serviceId: doc['service_id'] as String,
content: doc['content'] as String,
score: doc['score'] as double,
isFromUser: doc['is_from_user'] as bool,
createdAt: doc['created_at'] as Timestamp,
);
}
Map<String, dynamic> toDocument() {
return {
'author_id': authorId,
'destination_id': destinationId,
'service_id': serviceId,
'content': content,
'score': score,
'is_from_user': isFromUser,
'created_at': createdAt,
};
}
@override
String toString() {
return '''CommentEntity{
authorId: $authorId,
destinationId: $destinationId,
serviceId: $serviceId,
content: $content,
score: $score,
isFromUser: $isFromUser,
createdAt: $createdAt
}''';
}
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:prosapp_web_app/models/region_entity.dart';
class CountryEntity {
final String name;
final List<RegionEntity> regions;
const CountryEntity({
required this.name,
required this.regions,
});
Map<String, Object?> toDocument() {
return {
'name': name,
'states': regions,
};
}
static CountryEntity fromDocument(Map<String, dynamic> doc) {
return CountryEntity(
name: doc['name'] as String,
regions: (doc['states'] as List)
.map((region) => RegionEntity.fromDocument(region))
.toList(),
);
}
@override
String toString() {
return '''CountryEntity {
name: $name
regions: $regions
}''';
}
}
+11
View File
@@ -0,0 +1,11 @@
export 'location_preferences.dart';
enum LocationPreferences { office, delivery, both }
int enumToInt(LocationPreferences state) {
return state.index;
}
LocationPreferences intToEnum(int value) {
return LocationPreferences.values[value];
}
+36
View File
@@ -0,0 +1,36 @@
class MessageEntity {
final String ownerId;
final String content;
final DateTime createdAt;
const MessageEntity({
required this.ownerId,
required this.content,
required this.createdAt,
});
static MessageEntity fromDocument(Map<String, dynamic> doc) {
return MessageEntity(
ownerId: doc['owner_id'] as String,
content: doc['content'] as String,
createdAt: DateTime.parse(doc['created_at'] as String),
);
}
Map<String, dynamic> toDocument() {
return {
'owner_id': ownerId,
'content': content,
'created_at': createdAt.toIso8601String(),
};
}
@override
String toString() {
return '''MessageEntity{
ownerId: $ownerId,
content: $content,
createdAt: $createdAt
}''';
}
}
+54
View File
@@ -0,0 +1,54 @@
class PaymentMethodEntity {
final bool nequi;
final bool datafono;
final bool transferencia;
static const empty = PaymentMethodEntity(
nequi: false,
datafono: false,
transferencia: false,
);
const PaymentMethodEntity({
required this.nequi,
required this.datafono,
required this.transferencia,
});
PaymentMethodEntity copyWith({
bool? datafono,
bool? nequi,
bool? transferencia,
}) {
return PaymentMethodEntity(
nequi: nequi ?? this.nequi,
datafono: datafono ?? this.datafono,
transferencia: transferencia ?? this.transferencia,
);
}
Map<String, Object?> toDocument() {
return {
'nequi': nequi,
'datafono': datafono,
'transferencia': transferencia,
};
}
static PaymentMethodEntity fromDocument(Map<String, dynamic> doc) {
return PaymentMethodEntity(
nequi: doc['nequi'] as bool,
datafono: doc['datafono'] as bool,
transferencia: doc['transferencia'] as bool,
);
}
@override
String toString() {
return '''PaymentMethodEntity {
nequi: $nequi
datafono: $datafono
transferencia: $transferencia
}''';
}
}
+9
View File
@@ -0,0 +1,9 @@
enum ProState { inactive, pending, active, denied }
int enumToInt(ProState state) {
return state.index;
}
ProState intToEnum(int value) {
return ProState.values[value];
}
+153
View File
@@ -0,0 +1,153 @@
import 'package:prosapp_web_app/models/location_preferences.dart';
import 'package:prosapp_web_app/models/payment_method_entity.dart';
import 'package:prosapp_web_app/models/schedules.dart';
class Profesional {
final String id;
final String identification;
final String address;
final String aditionalAddress;
final String profession;
final bool ratePreferences;
final String rate;
final LocationPreferences locationPreferences;
final String bannerPicture;
final String identificationPicture;
final String certificatePicture;
final double latitude;
final double longitude;
final List<String> specializations;
final List<String> specializationsPictures;
final Schedules schedules;
final PaymentMethodEntity paymentMethods;
const Profesional({
required this.id,
required this.identification,
required this.address,
required this.aditionalAddress,
required this.profession,
required this.ratePreferences,
required this.rate,
required this.locationPreferences,
required this.bannerPicture,
required this.identificationPicture,
required this.certificatePicture,
required this.latitude,
required this.longitude,
required this.specializations,
required this.specializationsPictures,
required this.schedules,
required this.paymentMethods,
});
static Profesional fromDocument(Map<String, dynamic> doc) {
return Profesional(
id: doc['id'] as String,
identification: doc['identification'] as String,
address: doc['address'] as String,
aditionalAddress: doc['aditional_address'] as String,
profession: doc['profession'] as String,
ratePreferences: doc['rate_preferences'] as bool,
rate: doc['rate'] as String,
locationPreferences: intToEnum(doc['location_preferences'] as int),
bannerPicture: doc['banner_picture'] as String,
identificationPicture: doc['identification_picture'] as String,
certificatePicture: doc['certificate_picture'] as String,
latitude: double.parse(doc['latitude'].toString()),
longitude: double.parse(doc['longitude'].toString()),
specializations: List<String>.from(doc['specializations']),
specializationsPictures:
List<String>.from(doc['specializations_pictures']),
schedules: Schedules.fromDocument(doc['schedules']),
paymentMethods: PaymentMethodEntity.fromDocument(doc['payment_methods']),
);
}
Profesional copyWith({
String? id,
String? identification,
String? address,
String? aditionalAddress,
String? profession,
bool ratePreferences = false,
String? rate,
LocationPreferences? locationPreferences,
String? bannerPicture,
String? identificationPicture,
String? certificatePicture,
double? latitude,
double? longitude,
List<String>? specializations,
List<String>? specializationsPictures,
Schedules? schedules,
PaymentMethodEntity? paymentMethods,
}) {
return Profesional(
id: id ?? this.id,
identification: identification ?? this.identification,
address: address ?? this.address,
aditionalAddress: aditionalAddress ?? this.aditionalAddress,
profession: profession ?? this.profession,
ratePreferences: ratePreferences,
rate: rate ?? this.rate,
locationPreferences: locationPreferences ?? this.locationPreferences,
bannerPicture: bannerPicture ?? this.bannerPicture,
identificationPicture:
identificationPicture ?? this.identificationPicture,
certificatePicture: certificatePicture ?? this.certificatePicture,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
specializations: specializations ?? this.specializations,
specializationsPictures:
specializationsPictures ?? this.specializationsPictures,
schedules: schedules ?? this.schedules,
paymentMethods: paymentMethods ?? this.paymentMethods,
);
}
Map<String, dynamic> toDocument() {
return {
'id': id,
'identification': identification,
'address': address,
'aditional_address': aditionalAddress,
'profession': profession,
'rate_preferences': ratePreferences,
'rate': rate,
'location_preferences': enumToInt(locationPreferences),
'banner_picture': bannerPicture,
'identification_picture': identificationPicture,
'certificate_picture': certificatePicture,
'latitude': latitude,
'longitude': longitude,
'specializations': specializations,
'specializations_pictures': specializationsPictures,
'schedules': schedules.toJson(),
'payment_methods': paymentMethods.toDocument(),
};
}
@override
String toString() {
return '''Profesional{ {
id: $id,
identification: $identification,
address: $address,
aditional_address: $aditionalAddress,
profession: $profession,
rate_preferences: $ratePreferences,
rate: $rate,
location_preferences: $locationPreferences,
bannerPicture: $bannerPicture,
identificationPicture: $identificationPicture,
certificatePicture: $certificatePicture,
latitude: $latitude,
longitude: $longitude,
specializations: $specializations,
specializationsPictures: $specializationsPictures,
schedule: $schedules,
paymentMethods: $paymentMethods
}''';
}
}
+22
View File
@@ -0,0 +1,22 @@
class Profession {
final String name;
Profession({required this.name});
Map<String, Object?> toDocument() {
return {
'name': name,
};
}
static Profession fromDocument(Map<String, dynamic> doc) {
return Profession(
name: doc['name'] as String,
);
}
@override
String toString() {
return 'Profession{name: $name}';
}
}
+36
View File
@@ -0,0 +1,36 @@
import 'package:prosapp_web_app/models/city_entity.dart';
class RegionEntity {
final String name;
final List<CityEntity> cities;
const RegionEntity({
required this.name,
required this.cities,
});
Map<String, Object?> toDocument() {
return {
'name': name,
'cities': cities,
};
}
static RegionEntity fromDocument(Map<String, dynamic> doc) {
return RegionEntity(
name: doc['name'] as String,
cities: (doc['cities'] as List)
.map((city) => CityEntity.fromDocument(city))
.toList(),
);
}
@override
String toString() {
return '''RegionEntity {
name: $name
cities: $cities
}''';
}
}
+76
View File
@@ -0,0 +1,76 @@
import 'package:prosapp_web_app/models/schedules_entity.dart';
class Schedules {
final ScheduleEntity monday;
final ScheduleEntity tuesday;
final ScheduleEntity wednesday;
final ScheduleEntity thursday;
final ScheduleEntity friday;
final ScheduleEntity saturday;
final ScheduleEntity sunday;
const Schedules({
required this.monday,
required this.tuesday,
required this.wednesday,
required this.thursday,
required this.friday,
required this.saturday,
required this.sunday,
});
static const empty = Schedules(
monday: ScheduleEntity.empty,
tuesday: ScheduleEntity.empty,
wednesday: ScheduleEntity.empty,
thursday: ScheduleEntity.empty,
friday: ScheduleEntity.empty,
saturday: ScheduleEntity.empty,
sunday: ScheduleEntity.empty,
);
// Copy function
Schedules copyWith({
ScheduleEntity? monday,
ScheduleEntity? tuesday,
ScheduleEntity? wednesday,
ScheduleEntity? thursday,
ScheduleEntity? friday,
ScheduleEntity? saturday,
ScheduleEntity? sunday,
}) {
return Schedules(
monday: monday ?? this.monday,
tuesday: tuesday ?? this.tuesday,
wednesday: wednesday ?? this.wednesday,
thursday: thursday ?? this.thursday,
friday: friday ?? this.friday,
saturday: saturday ?? this.saturday,
sunday: sunday ?? this.sunday,
);
}
factory Schedules.fromDocument(Map<String, dynamic> doc) {
return Schedules(
monday: ScheduleEntity.fromDocument(doc['monday']),
tuesday: ScheduleEntity.fromDocument(doc['tuesday']),
wednesday: ScheduleEntity.fromDocument(doc['wednesday']),
thursday: ScheduleEntity.fromDocument(doc['thursday']),
friday: ScheduleEntity.fromDocument(doc['friday']),
saturday: ScheduleEntity.fromDocument(doc['saturday']),
sunday: ScheduleEntity.fromDocument(doc['sunday']),
);
}
Map<String, dynamic> toJson() {
return {
'monday': monday.toJson(),
'tuesday': tuesday.toJson(),
'wednesday': wednesday.toJson(),
'thursday': thursday.toJson(),
'friday': friday.toJson(),
'saturday': saturday.toJson(),
'sunday': sunday.toJson(),
};
}
}
+114
View File
@@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class ScheduleEntity {
final bool enabled;
final bool continuousDay;
final TimeOfDay? range1Hour1;
final TimeOfDay? range1Hour2;
final TimeOfDay? range2Hour1;
final TimeOfDay? range2Hour2;
const ScheduleEntity({
required this.enabled,
required this.continuousDay,
required this.range1Hour1,
required this.range1Hour2,
required this.range2Hour1,
required this.range2Hour2,
});
static const empty = ScheduleEntity(
enabled: false,
continuousDay: false,
range1Hour1: null,
range1Hour2: null,
range2Hour1: null,
range2Hour2: null,
);
ScheduleEntity copyWith({
bool? enabled,
bool? continuousDay,
TimeOfDay? range1Hour1,
TimeOfDay? range1Hour2,
TimeOfDay? range2Hour1,
TimeOfDay? range2Hour2,
}) {
return ScheduleEntity(
enabled: enabled ?? this.enabled,
continuousDay: continuousDay ?? this.continuousDay,
range1Hour1: range1Hour1 ?? this.range1Hour1,
range1Hour2: range1Hour2 ?? this.range1Hour2,
range2Hour1: range2Hour1 ?? this.range2Hour1,
range2Hour2: range2Hour2 ?? this.range2Hour2,
);
}
static ScheduleEntity fromDocument(Map<String, dynamic> doc) {
return ScheduleEntity(
enabled: doc['habilitado'] as bool,
continuousDay: doc['continuous_day'] as bool,
range1Hour1: _parseTime(doc['range1Hour1']),
range1Hour2: _parseTime(doc['range1Hour2']),
range2Hour1: _parseTime(doc['range2Hour1']),
range2Hour2: _parseTime(doc['range2Hour2']),
);
}
static TimeOfDay? _parseTime(String? time) {
try {
if (time == null) return null;
final components = time.split(':');
if (components.length != 2) {
return null;
}
final hour = int.parse(components[0]);
final minutes = int.parse(components[1]);
return TimeOfDay(hour: hour, minute: minutes);
} catch (e) {
return null;
}
}
Map<String, dynamic> toJson() {
return {
'habilitado': enabled,
'continuous_day': continuousDay,
'range1Hour1': formatTimeOfDay(range1Hour1),
'range1Hour2': formatTimeOfDay(range1Hour2),
'range2Hour1': formatTimeOfDay(range2Hour1),
'range2Hour2': formatTimeOfDay(range2Hour2),
};
}
String? formatTimeOfDay(TimeOfDay? time) {
if (time != null) {
return "${time.hour.toString()}:${time.minute.toString()}";
}
return null;
}
static String? getFormatTime(TimeOfDay? time) {
if (time == null) {
return null;
}
final now = DateTime.now();
final dateTime =
DateTime(now.year, now.month, now.day, time.hour, time.minute);
final format = DateFormat.jm();
return format.format(dateTime);
}
@override
String toString() {
return '''ScheduleEntity {
enabled: $enabled,
continuousDay: $continuousDay,
range1Hour1: $range1Hour1,
range1Hour2: $range1Hour2,
range2Hour1: $range2Hour1,
range2Hour2: $range2Hour2
}''';
}
}
+36
View File
@@ -0,0 +1,36 @@
class ReputationEntity {
final int total;
final double average;
final int totalPro;
final double averagePro;
const ReputationEntity({
required this.total,
required this.average,
required this.totalPro,
required this.averagePro,
});
static ReputationEntity fromDocument(Map<String, dynamic> doc) {
final total = doc['total'] ?? 0;
final totalPro = doc['total_pro'] ?? 0;
final average = doc['average'] ?? 0.0;
final averagePro = doc['average_pro'] ?? 0.0;
return ReputationEntity(
total: int.parse(total.toString()),
average: double.parse(average.toString()),
totalPro: int.parse(totalPro.toString()),
averagePro: double.parse(averagePro.toString()),
);
}
Map<String, dynamic> toDocument() {
return {
'total': total,
'average': average,
'total_pro': totalPro,
'average_pro': averagePro,
};
}
}
+116
View File
@@ -0,0 +1,116 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/models/service_location_preferences.dart';
import 'package:prosapp_web_app/models/service_status.dart';
class Service {
final String? id;
final String professionalId;
final bool professionalScored;
final String userId;
final bool userScored;
final String address;
final String aditionalAddress;
final double latitude;
final double longitude;
final String day;
final Timestamp createdAt;
final String description;
final TimeOfDay range1Hour1;
final TimeOfDay range1Hour2;
final String rate;
final ServiceStatus status;
final ServiceLocationPreferences location;
Service({
required this.id,
required this.professionalId,
required this.professionalScored,
required this.userId,
required this.userScored,
required this.address,
required this.aditionalAddress,
required this.latitude,
required this.longitude,
required this.day,
required this.createdAt,
required this.description,
required this.range1Hour1,
required this.range1Hour2,
required this.rate,
required this.status,
required this.location,
});
static Service fromJson(Map<String, dynamic> doc, String id) {
return Service(
id: id,
professionalId: doc['professional_id'] as String,
professionalScored: doc['professional_scored'] as bool,
userId: doc['user_id'] as String,
userScored: doc['user_scored'] as bool,
address: doc['address'] as String,
aditionalAddress: doc['aditional_address'] as String,
latitude: doc['latitude'] as double,
longitude: doc['longitude'] as double,
day: doc['day'] as String,
createdAt: doc['created_at'] as Timestamp,
description: doc['description'] as String,
range1Hour1: parseTimeOfDay(doc['range1_hour1'] as String),
range1Hour2: parseTimeOfDay(doc['range1_hour2'] as String),
status: intToEnumService(doc['status'] as int),
rate: doc['rate'] as String,
location: intToEnum(doc['location'] as int),
);
}
static Service fromDocument(DocumentSnapshot<Map<String, dynamic>> doc) {
return fromJson(doc.data()!, doc.id);
}
Map<String, dynamic> toDocument() {
return {
'id': id,
'professional_id': professionalId,
'professional_scored': professionalScored,
'user_id': userId,
'user_scored': userScored,
'address': address,
'aditional_address': aditionalAddress,
'latitude': latitude,
'longitude': longitude,
'day': day,
'created_at': createdAt,
'description': description,
'range1_hour1': formatTimeOfDay(range1Hour1),
'range1_hour2': formatTimeOfDay(range1Hour2),
'rate': rate,
'status': enumToIntService(status),
'location': enumToInt(location),
};
}
static TimeOfDay parseTimeOfDay(String timeString) {
final parts = timeString.split(':');
return TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1]));
}
String? formatTimeOfDay(TimeOfDay? time) {
if (time != null) {
return "${time.hour.toString()}:${time.minute.toString()}";
}
return null;
}
@override
String toString() {
return '''Service {
professionalId: $professionalId,
userId: $userId,
day: $day,
createdAt: $createdAt,
status: $status,
location: $location
}''';
}
}
@@ -0,0 +1,11 @@
export 'service_location_preferences.dart';
enum ServiceLocationPreferences { office, delivery }
int enumToInt(ServiceLocationPreferences state) {
return state.index;
}
ServiceLocationPreferences intToEnum(int value) {
return ServiceLocationPreferences.values[value];
}
+19
View File
@@ -0,0 +1,19 @@
export 'service_status.dart';
enum ServiceStatus {
pending, // 0
acepted, // 1
denied, // 2
active, // 3
cancelled, // 4
completed, // 5
selfBooked // 6
}
int enumToIntService(ServiceStatus state) {
return state.index;
}
ServiceStatus intToEnumService(int value) {
return ServiceStatus.values[value];
}
+20
View File
@@ -0,0 +1,20 @@
import 'package:prosapp_web_app/models/service.dart';
import 'package:prosapp_web_app/models/usuario.dart';
class ServicioProfesional {
final Service service;
final Usuario user;
ServicioProfesional({
required this.user,
required this.service,
});
@override
String toString() {
return '''UserProfessional{
myUser: $user,
service: $service
}''';
}
}
+88
View File
@@ -0,0 +1,88 @@
class Setting {
final bool tarifas;
final bool domicilios;
final bool google;
final String versionIos;
final String versionAndroid;
final String horaNotificacion;
final String tituloSoporte;
final String parrafoSoporte;
final String numeroSoporte;
final String emailSoporte;
final String diasSoporte;
final String horasSoporte;
final String politicasPrivacidad;
final String politicasPrivacidadTitle;
final String politicasPrivacidadBody;
final String terminosCondiciones;
final String terminosCondicionesTitle;
final String terminosCondicionesBody;
const Setting({
required this.tarifas,
required this.domicilios,
required this.google,
required this.versionIos,
required this.versionAndroid,
required this.horaNotificacion,
required this.tituloSoporte,
required this.parrafoSoporte,
required this.numeroSoporte,
required this.emailSoporte,
required this.diasSoporte,
required this.horasSoporte,
required this.politicasPrivacidad,
required this.politicasPrivacidadTitle,
required this.politicasPrivacidadBody,
required this.terminosCondiciones,
required this.terminosCondicionesTitle,
required this.terminosCondicionesBody,
});
static Setting fromDocument(Map<String, dynamic> doc) {
return Setting(
tarifas: doc['tarifas'] as bool,
domicilios: doc['domicilios'] as bool,
google: doc['google'] as bool,
versionIos: doc['version_ios'] as String,
versionAndroid: doc['version_android'] as String,
horaNotificacion: doc['hora_notificacion'] as String,
tituloSoporte: doc['titulo_soporte'] as String,
parrafoSoporte: doc['parrafo_soporte'] as String,
numeroSoporte: doc['numero_soporte'] as String,
emailSoporte: doc['email_soporte'] as String,
diasSoporte: doc['dias_soporte'] as String,
horasSoporte: doc['horas_soporte'] as String,
politicasPrivacidad: doc['politicas_privacidad'] as String,
politicasPrivacidadTitle: doc['politicas_privacidad_title'] as String,
politicasPrivacidadBody: doc['politicas_privacidad_body'] as String,
terminosCondiciones: doc['terminos_condiciones'] as String,
terminosCondicionesTitle: doc['terminos_condiciones_title'] as String,
terminosCondicionesBody: doc['terminos_condiciones_body'] as String,
);
}
@override
String toString() {
return '''Setting {
tarifas: $tarifas
domicilios: $domicilios
google: $google
versionIos: $versionIos
versionAndroid: $versionAndroid
horaNotificacion: $horaNotificacion
tituloSoporte: $tituloSoporte
parrafoSoporte: $parrafoSoporte
numeroSoporte: $numeroSoporte
emailSoporte: $emailSoporte
diasSoporte: $diasSoporte
horasSoporte: $horasSoporte
politicasPrivacidad: $politicasPrivacidad
politicasPrivacidadTitle: $politicasPrivacidadTitle
politicasPrivacidadBody: $politicasPrivacidadBody
terminosCondiciones: $terminosCondiciones
terminosCondicionesTitle: $terminosCondicionesTitle
terminosCondicionesBody: $terminosCondicionesBody
}''';
}
}
+66
View File
@@ -0,0 +1,66 @@
import 'package:prosapp_web_app/models/pro_state.dart';
class Usuario {
final String id;
final String? email;
final String? phone;
final String name;
final String? nickname;
final String? city;
final String? picture;
final String? birthday;
final String? gender;
final ProState proState;
final String? token;
Usuario({
required this.id,
required this.email,
required this.phone,
required this.name,
required this.nickname,
required this.city,
required this.picture,
required this.birthday,
required this.gender,
required this.proState,
required this.token,
});
Map<String, Object?> toDocument() {
return {
'id': id,
'email': email,
'phone': phone,
'name': name,
'nickname': name.toLowerCase().trim().replaceAll(' ', '_'),
'city': city,
'picture': picture,
'birthday': birthday,
'gender': gender,
'professional_state': enumToInt(proState),
'token': token,
};
}
static Usuario fromDocument(Map<String, dynamic> doc) {
return Usuario(
id: doc['id'],
email: doc['email'],
phone: doc['phone'],
name: doc['name'] ?? '',
nickname: doc['nickname'],
city: doc['city'],
picture: doc['picture'],
birthday: doc['birthday'],
gender: doc['gender'],
proState: intToEnum(doc['professional_state'] as int),
token: doc['token'],
);
}
@override
String toString() {
return 'User(id: $id, email: $email, phone: $phone, name: $name, nickname: $nickname, city: $city, picture: $picture, birthday: $birthday, gender: $gender, proState: $proState, token: $token)';
}
}
+41
View File
@@ -0,0 +1,41 @@
import 'package:prosapp_web_app/models/profesional.dart';
import 'package:prosapp_web_app/models/usuario.dart';
class UsuarioProfesional {
final Usuario user;
final Profesional professionalInfo;
final double averageScore;
UsuarioProfesional({
required this.user,
required this.professionalInfo,
required this.averageScore,
});
// Método para serializar a JSON
Map<String, dynamic> toJson() {
return {
'user': user.toDocument(),
'professionalInfo': professionalInfo.toDocument(),
'averageScore': averageScore,
};
}
// Método para deserializar desde JSON
factory UsuarioProfesional.fromJson(Map<String, dynamic> json) {
return UsuarioProfesional(
user: Usuario.fromDocument(json['user']),
professionalInfo: Profesional.fromDocument(json['professionalInfo']),
averageScore: json['averageScore'].toDouble(),
);
}
@override
String toString() {
return '''UsuarioProfesional{
user: $user,
professionalInfo: $professionalInfo,
averageScore: $averageScore
}''';
}
}
+334
View File
@@ -0,0 +1,334 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:prosapp_web_app/models/comment_entity.dart';
import 'package:prosapp_web_app/models/profesional.dart';
import 'package:prosapp_web_app/models/usuario.dart';
import 'package:prosapp_web_app/providers/professional_provider.dart';
import 'package:prosapp_web_app/providers/services_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/services/navigation_service.dart';
import 'package:prosapp_web_app/services/notifications_service.dart';
import 'package:provider/provider.dart';
enum AuthStatus { checking, authenticated, notAuthenticated }
class AuthProvider extends ChangeNotifier {
Usuario? user;
Profesional? professional;
double userAverageScore = 0.0;
AuthStatus authStatus = AuthStatus.checking;
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
String? _verificationId;
AuthProvider() {
isAuthenticated();
}
Future<void> login(String email, String password) async {
try {
await _firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
final userData = await _firestore
.collection('users')
.doc(_firebaseAuth.currentUser!.uid)
.get();
user = Usuario.fromDocument(userData.data()!);
authStatus = AuthStatus.authenticated;
notifyListeners();
NavigationService.replaceTo(Flurorouter.dashboardRoute);
} catch (e) {
authStatus = AuthStatus.notAuthenticated;
notifyListeners();
NotificationsService.showSnackBarError(
'Usuario o contraseña incorrectos');
}
}
Future<void> verifyPhoneNumberForLink(String phoneNumber) async {
try {
await _firebaseAuth.verifyPhoneNumber(
phoneNumber: phoneNumber,
timeout: const Duration(seconds: 60),
verificationCompleted: (PhoneAuthCredential credential) async {
if (_firebaseAuth.currentUser != null) {
// Vincula el número de teléfono a la cuenta actual
await _firebaseAuth.currentUser!.linkWithCredential(credential);
NotificationsService.showSnackbar('Número vinculado exitosamente');
authStatus = AuthStatus.authenticated;
notifyListeners();
}
},
verificationFailed: (FirebaseAuthException e) {
NotificationsService.showSnackBarError(
'Error en la verificación del teléfono: ${e.message}');
},
codeSent: (String verificationId, int? resendToken) {
_verificationId = verificationId;
notifyListeners();
},
codeAutoRetrievalTimeout: (String verificationId) {
_verificationId = verificationId;
},
);
} catch (e) {
NotificationsService.showSnackBarError('Error al enviar OTP');
}
}
Future<void> linkPhoneWithOTP(String smsCode) async {
try {
final credential = PhoneAuthProvider.credential(
verificationId: _verificationId!,
smsCode: smsCode,
);
if (_firebaseAuth.currentUser != null) {
await _firebaseAuth.currentUser!.linkWithCredential(credential);
await FirebaseFirestore.instance
.collection('users')
.doc(_firebaseAuth.currentUser!.uid)
.update({'phone': _firebaseAuth.currentUser!.phoneNumber});
NotificationsService.showSnackbar('Número vinculado exitosamente');
authStatus = AuthStatus.authenticated;
notifyListeners();
NavigationService.replaceTo(Flurorouter.profileRoute);
}
} catch (e) {
authStatus = AuthStatus.notAuthenticated;
notifyListeners();
NotificationsService.showSnackBarError('Error en la verificación OTP');
}
}
Future<void> register(String email, String password, String name) async {
try {
UserCredential userCredential =
await _firebaseAuth.createUserWithEmailAndPassword(
email: email,
password: password,
);
User? user = userCredential.user;
if (user != null) {
await _firestore.collection('users').doc(user.uid).set({
'birthday': "",
'city': "",
'email': user.email,
'gender': "",
'id': user.uid,
'name': name.trim(),
'nickname': name.trim().toLowerCase(),
'phone': "",
'picture': "",
'professional_state': 0,
'token': "",
});
authStatus = AuthStatus.authenticated;
notifyListeners();
NavigationService.replaceTo(Flurorouter.dashboardRoute);
}
} catch (e) {
authStatus = AuthStatus.notAuthenticated;
notifyListeners();
NotificationsService.showSnackBarError('Email ya registrado');
}
}
Future<bool> isAuthenticated() async {
final User? firebaseUser = _firebaseAuth.currentUser;
if (firebaseUser == null) {
authStatus = AuthStatus.notAuthenticated;
notifyListeners();
return false;
}
final userData =
await _firestore.collection('users').doc(firebaseUser.uid).get();
if (userData.exists) {
user = Usuario.fromDocument(userData.data()!);
userAverageScore = await _getUserAverageScore(user!.id);
authStatus = AuthStatus.authenticated;
notifyListeners();
return true;
} else {
authStatus = AuthStatus.notAuthenticated;
notifyListeners();
return false;
}
}
Future<void> logout() async {
await _firebaseAuth.signOut();
authStatus = AuthStatus.notAuthenticated;
notifyListeners();
Provider.of<ServicesProvider>(
NavigationService.navigatorKey.currentContext!,
listen: false)
.logout();
Provider.of<ProfessionalProvider>(
NavigationService.navigatorKey.currentContext!,
listen: false)
.logout();
NavigationService.replaceTo(Flurorouter.phoneLoginRoute);
}
void refreshUser() {
isAuthenticated();
}
Future<void> verifyPhoneNumber(String phoneNumber) async {
try {
await _firebaseAuth.verifyPhoneNumber(
phoneNumber: phoneNumber,
timeout: const Duration(seconds: 60),
verificationCompleted: (PhoneAuthCredential credential) async {
await _firebaseAuth.signInWithCredential(credential);
authStatus = AuthStatus.authenticated;
notifyListeners();
NavigationService.replaceTo(Flurorouter.dashboardRoute);
},
verificationFailed: (FirebaseAuthException e) {
NotificationsService.showSnackBarError(
'La verificación del teléfono falló: ${e.message}');
},
codeSent: (String verificationId, int? resendToken) {
_verificationId = verificationId;
notifyListeners();
},
codeAutoRetrievalTimeout: (String verificationId) {
_verificationId = verificationId;
},
);
} catch (e) {
NotificationsService.showSnackBarError('Error al enviar OTP');
}
}
Future<void> signInWithOTP(String smsCode) async {
try {
final credential = PhoneAuthProvider.credential(
verificationId: _verificationId!,
smsCode: smsCode,
);
await _firebaseAuth.signInWithCredential(credential);
User? currentUser = _firebaseAuth.currentUser;
if (currentUser != null) {
final userData =
await _firestore.collection('users').doc(currentUser.uid).get();
if (userData.exists) {
user = Usuario.fromDocument(userData.data()!);
} else {
await _firestore.collection('users').doc(currentUser.uid).set({
'birthday': "",
'city': "",
'email': "",
'gender': "",
'id': currentUser.uid,
'name': "",
'nickname': "",
'phone': currentUser.phoneNumber,
'picture': "",
'professional_state': 0,
'token': "",
});
final newUserData =
await _firestore.collection('users').doc(currentUser.uid).get();
user = Usuario.fromDocument(newUserData.data()!);
}
authStatus = AuthStatus.authenticated;
notifyListeners();
NavigationService.replaceTo(Flurorouter.dashboardRoute);
}
} catch (e) {
authStatus = AuthStatus.notAuthenticated;
notifyListeners();
NotificationsService.showSnackBarError('Error en la verificación OTP');
}
}
Future<void> addEmailAndPassword(String email, String password) async {
try {
final User? currentUser = _firebaseAuth.currentUser;
if (currentUser != null) {
await currentUser.updateEmail(email);
await currentUser.updatePassword(password);
await _firestore.collection('users').doc(currentUser.uid).update({
'email': email,
});
notifyListeners();
NotificationsService.showSnackbar(
'Email y contraseña añadidos exitosamente');
NavigationService.replaceTo(Flurorouter.profileRoute);
}
} catch (e) {
if (e is FirebaseAuthException && e.code == 'email-already-in-use') {
NotificationsService.showSnackBarError('El correo ya existe');
}
if (e is FirebaseAuthException && e.code == 'invalid-email') {
NotificationsService.showSnackBarError('El correo no es vßlido');
}
if (e is FirebaseAuthException && e.code == 'weak-password') {
NotificationsService.showSnackBarError(
'La contraseña debe tener al menos 6 caracteres');
}
if (e is FirebaseAuthException && e.code == 'requires-recent-login') {
NotificationsService.showSnackBarError(
'Debes iniciar sesión recientemente antes de agregar email y contraseña');
}
}
}
Future<double> _getUserAverageScore(String userId) async {
final querySnapshot = await FirebaseFirestore.instance
.collection('comments')
.where('destination_id', isEqualTo: userId)
.where('is_from_user', isEqualTo: false)
.get();
if (querySnapshot.docs.isEmpty) {
return 0.0;
}
final scores = querySnapshot.docs
.map((e) => CommentEntity.fromDocument(e.data()).score)
.toList();
final averageScore = scores.reduce((a, b) => a + b) / scores.length;
return averageScore;
}
}
@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:prosapp_web_app/models/service.dart';
import 'package:prosapp_web_app/models/servicio_profesional.dart';
import 'package:prosapp_web_app/models/usuario.dart';
class CalendarServicesProvider extends ChangeNotifier {
List<ServicioProfesional> services = [];
ServicioProfesional? service;
bool isLoading = true;
final _servicesCollection = FirebaseFirestore.instance.collection('services');
final _usersCollection = FirebaseFirestore.instance.collection('users');
void logout() {
services = [];
service = null;
isLoading = false;
notifyListeners();
}
getServicesForProfessional(String userId) async {
try {
isLoading = true;
final queryServices = await _servicesCollection
.where('professional_id', isEqualTo: userId)
.where('status', whereIn: [1, 3]).get();
final servicios = queryServices.docs.map(Service.fromDocument).toList();
final userIds = servicios.map((service) => service.userId).toList();
final queryUsers = await _usersCollection
.where(FieldPath.documentId, whereIn: userIds)
.get();
final users =
queryUsers.docs.map((e) => Usuario.fromDocument(e.data())).toList();
final usersMap = {for (var doc in users) doc.id: doc};
final servicesMap = (servicios.map((service) {
final user = usersMap[service.userId];
return ServicioProfesional(
user: user!,
service: service,
);
}).toList());
services = servicesMap
..sort((a, b) {
final dateA = DateTime.parse(a.service.day);
final dateB = DateTime.parse(b.service.day);
final range1A =
a.service.range1Hour1.hour * 60 + a.service.range1Hour1.minute;
final range1B =
b.service.range1Hour1.hour * 60 + b.service.range1Hour1.minute;
if (dateA.compareTo(dateB) != 0) {
return dateA.compareTo(dateB);
} else {
return range1A.compareTo(range1B);
}
});
} catch (e) {
print('Error obteniendo servicios: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
}
+34
View File
@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/models/chat_entity.dart';
import 'package:prosapp_web_app/models/message_entity.dart';
import 'package:prosapp_web_app/repositories/firebase_chat_repository.dart';
class ChatProvider with ChangeNotifier {
final FirebaseChatRepository _firebaseChatRepository =
FirebaseChatRepository();
ChatEntity? _currentChat;
ChatEntity? get currentChat => _currentChat;
Stream<ChatEntity?> getChat(String chatId) {
return _firebaseChatRepository.getChatById(chatId).map((chat) {
_currentChat = chat;
notifyListeners();
return chat;
});
}
Future<ChatEntity> createChat(
String chatId, String userId, String professionalId) async {
final chat = await _firebaseChatRepository.createNewChat(
chatId, userId, professionalId);
_currentChat = chat;
notifyListeners();
return chat;
}
Future<void> sendMessage(String chatId, MessageEntity message) async {
await _firebaseChatRepository.sendMessage(chatId, message);
notifyListeners();
}
}
+55
View File
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:prosapp_web_app/models/city.dart';
import 'package:prosapp_web_app/models/country_entity.dart';
class CitiesProvider extends ChangeNotifier {
List<City> cities = [];
bool isLoading = true;
final _citiesCollection =
FirebaseFirestore.instance.collection('countries v2');
CitiesProvider() {
getCities();
}
getCoordsOfCity(String cityName) {
if (cities.isEmpty) return null;
for (var city in cities) {
if (city.cityName == cityName) {
return city.coordsOfCity;
}
}
return null;
}
getCities() async {
try {
final querySnapshot = await _citiesCollection.doc('Colombia').get();
final data = querySnapshot.data();
final country = CountryEntity.fromDocument(data as Map<String, dynamic>);
for (var region in country.regions) {
for (var city in region.cities) {
cities.add(City(
cityName: city.name,
coordsOfCity: city.coords,
stateOfCity: region.name,
countryOfCity: country.name,
));
}
}
cities.sort((a, b) => a.cityName.compareTo(b.cityName));
} catch (e) {
print('Error obteniendo ciudades: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
}
+18
View File
@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
class EmailFormProvider extends ChangeNotifier {
GlobalKey<FormState> formKey = GlobalKey<FormState>();
String email = '';
String password = '';
bool validateForm() {
if (formKey.currentState!.validate()) {
return true;
} else {
print('Formulario no válido');
return false;
}
}
}
+20
View File
@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
class LoginFormProvider extends ChangeNotifier {
GlobalKey<FormState> formKey = GlobalKey<FormState>();
String email = "";
String password = "";
bool validateForm() {
if (formKey.currentState!.validate()) {
// print('Formulario válido');
// print('Email: $email, password: $password');
return true;
} else {
print('Formulario no válido');
return false;
}
}
}
+18
View File
@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
class PhoneFormProvider extends ChangeNotifier {
GlobalKey<FormState> formKey = GlobalKey<FormState>();
String phone = "";
String code = "";
bool validateForm() {
if (formKey.currentState!.validate()) {
return true;
} else {
print('Formulario no válido');
return false;
}
}
}
@@ -0,0 +1,72 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/models/comment_entity.dart';
import 'package:prosapp_web_app/models/profesional.dart';
import 'package:prosapp_web_app/models/usuario.dart';
import 'package:prosapp_web_app/models/usuario_profesional.dart';
class ProfessionalDetailProvider extends ChangeNotifier {
UsuarioProfesional? professional;
bool isLoading = true;
Future<void> getProfessionalById(String uid) async {
try {
isLoading = true;
notifyListeners();
final userDoc =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
if (!userDoc.exists) {
print('Profesional no encontrado.');
return;
}
final user = Usuario.fromDocument(userDoc.data()!);
final professionalDoc = await FirebaseFirestore.instance
.collection('professional_info')
.doc(uid)
.get();
if (!professionalDoc.exists) {
print('Información profesional no encontrada.');
return;
}
final professionalInfo =
Profesional.fromDocument(professionalDoc.data()!);
final averageScore = await _getAverageScore(uid);
professional = UsuarioProfesional(
user: user,
professionalInfo: professionalInfo,
averageScore: averageScore,
);
} catch (e) {
print('Error obteniendo profesional: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<double> _getAverageScore(String userId) async {
final querySnapshot = await FirebaseFirestore.instance
.collection('comments')
.where('destination_id', isEqualTo: userId)
.where('is_from_user', isEqualTo: true)
.get();
if (querySnapshot.docs.isEmpty) {
return 0.0;
}
final scores = querySnapshot.docs
.map((e) => CommentEntity.fromDocument(e.data()).score)
.toList();
final averageScore = scores.reduce((a, b) => a + b) / scores.length;
return averageScore;
}
}
@@ -0,0 +1,200 @@
import 'dart:typed_data';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/models/location_preferences.dart';
import 'package:prosapp_web_app/models/payment_method_entity.dart';
import 'package:prosapp_web_app/models/profesional.dart';
import 'package:prosapp_web_app/models/schedules.dart';
import 'package:prosapp_web_app/services/notifications_service.dart';
class ProfessionalFormProvider with ChangeNotifier {
Profesional? profesional;
GlobalKey<FormState> formKey = GlobalKey<FormState>();
GlobalKey<FormState> profileFormKey = GlobalKey<FormState>();
copyProfesionalWith({
String? id,
String? identification,
String? address,
String? aditionalAddress,
String? profession,
bool ratePreferences = false,
String? rate,
LocationPreferences? locationPreferences,
String? bannerPicture,
String? identificationPicture,
String? certificatePicture,
double? latitude,
double? longitude,
List<String>? specializations,
List<String>? specializationsPictures,
Schedules? schedules,
PaymentMethodEntity? paymentMethods,
}) {
profesional = Profesional(
id: id ?? profesional!.id,
identification: identification ?? profesional!.identification,
address: address ?? profesional!.address,
aditionalAddress: aditionalAddress ?? profesional!.aditionalAddress,
profession: profession ?? profesional!.profession,
ratePreferences: ratePreferences,
rate: rate ?? profesional!.rate,
locationPreferences:
locationPreferences ?? profesional!.locationPreferences,
bannerPicture: bannerPicture ?? profesional!.bannerPicture,
identificationPicture:
identificationPicture ?? profesional!.identificationPicture,
certificatePicture: certificatePicture ?? profesional!.certificatePicture,
latitude: latitude ?? profesional!.latitude,
longitude: longitude ?? profesional!.longitude,
specializations: specializations ?? profesional!.specializations,
specializationsPictures:
specializationsPictures ?? profesional!.specializationsPictures,
schedules: schedules ?? profesional!.schedules,
paymentMethods: paymentMethods ?? profesional!.paymentMethods,
);
notifyListeners();
}
bool _validForm() {
return formKey.currentState!.validate();
}
bool _validProfileForm() {
return profileFormKey.currentState!.validate();
}
setProfesional(Profesional profesional) {
this.profesional = profesional;
notifyListeners();
}
Future<bool> updateProfesionalInfo(String userId) async {
if (!_validForm()) return false;
final docProfessional = FirebaseFirestore.instance
.collection('professional_info')
.doc(userId)
.withConverter(
fromFirestore: (snapshot, _) =>
Profesional.fromDocument(snapshot.data()!),
toFirestore: (user, _) => user.toDocument(),
);
await docProfessional.set(profesional!);
NotificationsService.showSnackbar('Información actualizada');
return true;
}
Future<bool> updateProfesionalProfileInfo(String userId) async {
if (!_validProfileForm()) return false;
final docProfessional = FirebaseFirestore.instance
.collection('professional_info')
.doc(userId)
.withConverter(
fromFirestore: (snapshot, _) =>
Profesional.fromDocument(snapshot.data()!),
toFirestore: (user, _) => user.toDocument(),
);
await docProfessional.set(profesional!);
NotificationsService.showSnackbar('Información actualizada');
return true;
}
Future<bool> updateProfesionalProfileScheduleInfo(String userId) async {
final docProfessional = FirebaseFirestore.instance
.collection('professional_info')
.doc(userId)
.withConverter(
fromFirestore: (snapshot, _) =>
Profesional.fromDocument(snapshot.data()!),
toFirestore: (user, _) => user.toDocument(),
);
await docProfessional.set(profesional!);
NotificationsService.showSnackbar('Información actualizada');
return true;
}
Future<Profesional> uploadPdfIdentification(
Uint8List fileBytes, String userId) async {
try {
final storageRef = FirebaseStorage.instance
.ref()
.child('$userId/PDF/${userId}_cedula.pdf');
await storageRef.putData(fileBytes);
final url = await storageRef.getDownloadURL();
copyProfesionalWith(identificationPicture: url);
notifyListeners();
return profesional!;
} catch (e) {
print("Error al subir el PDF de identificación: $e");
NotificationsService.showSnackbar('Error al subir el PDF');
rethrow;
}
}
Future<Profesional> uploadPdfCertificate(
Uint8List fileBytes, String userId) async {
try {
final storageRef = FirebaseStorage.instance
.ref()
.child('$userId/PDF/${userId}_certificado.pdf');
await storageRef.putData(fileBytes);
final url = await storageRef.getDownloadURL();
copyProfesionalWith(certificatePicture: url);
notifyListeners();
return profesional!;
} catch (e) {
print("Error al subir el PDF de identificación: $e");
NotificationsService.showSnackbar('Error al subir el PDF');
rethrow;
}
}
Future<Profesional> uploadPdfSpecializations(
List<Uint8List> filesBytes, String userId) async {
try {
List<String> urls = [];
for (int i = 0; i < filesBytes.length; i++) {
final storageRef = FirebaseStorage.instance.ref().child(
'$userId/PDF/${userId}_${DateTime.now().millisecondsSinceEpoch}_especializacion_$i.pdf');
await storageRef.putData(filesBytes[i]);
final url = await storageRef.getDownloadURL();
urls.add(url);
}
copyProfesionalWith(specializationsPictures: urls);
notifyListeners();
return profesional!;
} catch (e) {
print("Error al subir los PDFs de especialización: $e");
NotificationsService.showSnackbar('Error al subir los PDFs');
rethrow;
}
}
}
+69
View File
@@ -0,0 +1,69 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/widgets.dart';
import 'package:prosapp_web_app/models/location_preferences.dart';
import 'package:prosapp_web_app/models/payment_method_entity.dart';
import 'package:prosapp_web_app/models/profesional.dart';
import 'package:prosapp_web_app/models/schedules.dart';
class ProfessionalProvider extends ChangeNotifier {
Profesional? profesional;
bool _isProModeActive = false;
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
bool get isProModeActive {
return _isProModeActive;
}
Future<Profesional> getProfessional(String uid) async {
try {
final professional = await FirebaseFirestore.instance
.collection('professional_info')
.doc(uid)
.get();
profesional = Profesional.fromDocument(professional.data()!);
} catch (e) {
profesional = Profesional(
id: _firebaseAuth.currentUser!.uid,
identification: '',
address: '',
aditionalAddress: '',
profession: '',
ratePreferences: false,
rate: '',
locationPreferences: LocationPreferences.office,
bannerPicture: '',
identificationPicture: '',
certificatePicture: '',
latitude: 0,
longitude: 0,
specializations: [],
specializationsPictures: [],
schedules: Schedules.empty,
paymentMethods: PaymentMethodEntity.empty,
);
}
notifyListeners();
return profesional!;
}
logout() {
_isProModeActive = false;
notifyListeners();
}
void toggleProMode() {
_isProModeActive = !_isProModeActive;
notifyListeners();
}
void setIsProModeActive(bool value) {
_isProModeActive = value;
notifyListeners();
}
sendProfessionalToReview() {}
}
+80
View File
@@ -0,0 +1,80 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/models/comment_entity.dart';
import 'package:prosapp_web_app/models/pro_state.dart';
import 'package:prosapp_web_app/models/profesional.dart';
import 'package:prosapp_web_app/models/usuario.dart';
import 'package:prosapp_web_app/models/usuario_profesional.dart';
class ProfessionalsProvider extends ChangeNotifier {
List<UsuarioProfesional> professionals = [];
bool isLoading = true;
ProfessionalsProvider() {
getProfessionals();
}
getProfessionals() async {
try {
final querySnapshot = await FirebaseFirestore.instance
.collection('users')
.where('professional_state', isEqualTo: ProState.active.index)
.get();
final users = querySnapshot.docs
.map((e) => Usuario.fromDocument(e.data()))
.toList();
final ids = users.map((e) => e.id).toList();
final querySnapshot2 = await FirebaseFirestore.instance
.collection('professional_info')
.where(FieldPath.documentId, whereIn: ids)
.get();
final professionalsInfo = querySnapshot2.docs
.map((e) => Profesional.fromDocument(e.data()))
.toList();
final professionalInfoMap = {
for (var doc in professionalsInfo) doc.id: doc
};
final usersMap = await Future.wait(users.map((user) async {
final professionalInfo = professionalInfoMap[user.id];
final averageScore = await _getAverageScore(user.id);
return UsuarioProfesional(
user: user,
professionalInfo: professionalInfo!,
averageScore: averageScore,
);
}).toList());
professionals = usersMap;
} catch (e) {
print('Error obteniendo profesionales: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<double> _getAverageScore(String userId) async {
final querySnapshot = await FirebaseFirestore.instance
.collection('comments')
.where('destination_id', isEqualTo: userId)
.where('is_from_user', isEqualTo: true)
.get();
if (querySnapshot.docs.isEmpty) {
return 0.0;
}
final scores = querySnapshot.docs
.map((e) => CommentEntity.fromDocument(e.data()).score)
.toList();
final averageScore = scores.reduce((a, b) => a + b) / scores.length;
return averageScore;
}
}
+33
View File
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:prosapp_web_app/models/profession.dart';
class ProfessionsProvider extends ChangeNotifier {
List<Profession> professions = [];
bool isLoading = true;
final _professionsCollection =
FirebaseFirestore.instance.collection('professions');
ProfessionsProvider() {
getProfessions();
}
getProfessions() async {
try {
final documentSnapshot =
await _professionsCollection.doc('professions').get();
final data = documentSnapshot.data() as Map<String, dynamic>;
professions = (data['professions'] as List<dynamic>)
.map((item) => Profession(name: item as String))
.toList();
} catch (e) {
print('Error obteniendo profesiones: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
}
+159
View File
@@ -0,0 +1,159 @@
import 'dart:typed_data';
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:prosapp_web_app/models/pro_state.dart';
import 'package:prosapp_web_app/models/usuario.dart';
import 'package:prosapp_web_app/services/notifications_service.dart';
class ProfileFormProvider extends ChangeNotifier {
Usuario? user;
GlobalKey<FormState> formKey = GlobalKey<FormState>();
String? _verificationId;
void copyUserWith({
String? id,
String? email,
String? phone,
String? name,
String? nickname,
String? city,
String? picture,
String? birthday,
String? gender,
ProState? proState,
String? token,
}) {
user = Usuario(
id: id ?? user!.id,
email: email ?? user!.email,
phone: phone ?? user!.phone,
name: name ?? user!.name,
nickname: nickname ?? user!.nickname,
city: city ?? user!.city,
picture: picture ?? user!.picture,
birthday: birthday ?? user!.birthday,
gender: gender ?? user!.gender,
proState: proState ?? user!.proState,
token: token ?? user!.token,
);
notifyListeners();
}
bool _validForm() {
return formKey.currentState!.validate();
}
Future<void> updateUserInfo() async {
if (!_validForm()) return;
final docUser = FirebaseFirestore.instance
.collection('users')
.doc(user!.id)
.withConverter(
fromFirestore: (snapshot, _) =>
Usuario.fromDocument(snapshot.data()!),
toFirestore: (user, _) => user.toDocument(),
);
await docUser.set(user!);
NotificationsService.showSnackbar('Información actualizada');
}
Future<void> updateUserInfoNoValid() async {
final docUser = FirebaseFirestore.instance
.collection('users')
.doc(user!.id)
.withConverter(
fromFirestore: (snapshot, _) =>
Usuario.fromDocument(snapshot.data()!),
toFirestore: (user, _) => user.toDocument(),
);
await docUser.set(user!);
NotificationsService.showSnackbar('Información actualizada');
}
Future<Usuario> uploadPicture(Uint8List bytes) async {
try {
final storageRef = FirebaseStorage.instance
.ref()
.child('${user!.id}/PP/${user!.id}_lead');
await storageRef.putData(bytes);
final url = await storageRef.getDownloadURL();
copyUserWith(picture: url);
notifyListeners();
return user!;
} catch (e) {
print("Error al subir la imagen: $e");
rethrow;
}
}
// Agregar numero
Future<void> signUpWithPhoneNumber(String phoneNumber) async {
try {
await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: phoneNumber,
verificationCompleted: (PhoneAuthCredential credential) async {
await FirebaseAuth.instance.signInWithCredential(credential);
NotificationsService.showSnackbar('Autenticación exitosa');
},
verificationFailed: (FirebaseAuthException e) {
NotificationsService.showSnackbar(
'Error en la verificación: ${e.message}');
},
codeSent: (String verificationId, int? resendToken) {
_verificationId = verificationId;
NotificationsService.showSnackbar(
'Código enviado al número $phoneNumber');
},
codeAutoRetrievalTimeout: (String verificationId) {
_verificationId = verificationId;
},
);
NotificationsService.showSnackbar('Código enviado');
} catch (e) {
NotificationsService.showSnackbar('Error al registrar con teléfono: $e');
}
}
Future<bool> linkPhoneNumberToExistingAccount(
String phoneNumber, String code) async {
try {
var phoneAuthCredential = PhoneAuthProvider.credential(
verificationId: _verificationId!,
smsCode: code,
);
User? user = FirebaseAuth.instance.currentUser;
if (user != null) {
await user.linkWithCredential(phoneAuthCredential);
copyUserWith(phone: phoneNumber);
notifyListeners();
NotificationsService.showSnackbar('Número vinculado exitosamente');
return true;
}
return false;
} catch (e) {
if (e is FirebaseAuthException && e.code == 'invalid-verification-code') {
NotificationsService.showSnackbar('Código de verificación inválido');
return false;
} else {
NotificationsService.showSnackbar('Error al vincular número: $e');
rethrow;
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
class RegisterFormProvider extends ChangeNotifier {
GlobalKey<FormState> formKey = GlobalKey<FormState>();
String name = "";
String email = "";
String password = "";
validateForm() {
if (formKey.currentState!.validate()) {
print('Formulario válido');
print('Name: $name, Email: $email, password: $password');
return true;
} else {
print('Formulario no válido');
return false;
}
}
}
+53
View File
@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/models/comment_entity.dart';
import 'package:prosapp_web_app/models/score_entity.dart';
import 'package:prosapp_web_app/repositories/firebase_score_repository.dart';
class ScoreProvider with ChangeNotifier {
final FirebaseScoreRepository _firebaseScoreRepository =
FirebaseScoreRepository();
ReputationEntity _reputation = const ReputationEntity(
total: 0,
average: 0,
totalPro: 0,
averagePro: 0,
);
ReputationEntity get reputation => _reputation;
List<CommentEntity> _comments = [];
List<CommentEntity> get comments => _comments;
// Método para cargar reputación dinámica según el ID
Future<void> loadReputation(String userId) async {
try {
final reputation =
await _firebaseScoreRepository.getReputationByUserId(userId);
_reputation = reputation;
notifyListeners();
} catch (e) {
debugPrint('Error loading reputation: $e');
}
}
// Método para cargar comentarios dinámicamente
void loadComments(String userId, {required bool isProfessional}) {
final commentStream = isProfessional
? _firebaseScoreRepository.getScoresForProfessional(userId)
: _firebaseScoreRepository.getScoresForUser(userId);
commentStream.listen((comments) {
_comments = comments;
notifyListeners();
});
}
Future<void> addComment(CommentEntity comment) async {
try {
await _firebaseScoreRepository.addComment(comment);
notifyListeners();
} catch (e) {
debugPrint('Error adding comment: $e');
}
}
}
+390
View File
@@ -0,0 +1,390 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:prosapp_web_app/models/pro_state.dart';
import 'package:prosapp_web_app/models/service.dart';
import 'package:prosapp_web_app/models/service_status.dart';
import 'package:prosapp_web_app/models/servicio_profesional.dart';
import 'package:prosapp_web_app/models/usuario.dart';
class ServicesProvider extends ChangeNotifier {
List<ServicioProfesional> services = [];
ServicioProfesional? service;
bool isLoading = true;
final _servicesCollection = FirebaseFirestore.instance.collection('services');
final _usersCollection = FirebaseFirestore.instance.collection('users');
void logout() {
services = [];
service = null;
isLoading = false;
notifyListeners();
}
Future<void> changeServiceStatus(
String serviceId, ServiceStatus newStatus) async {
try {
final statusValue = enumToIntService(newStatus);
await FirebaseFirestore.instance
.collection('services')
.doc(serviceId)
.update({'status': statusValue});
notifyListeners();
} catch (e) {
print('Error al actualizar el estado: $e');
}
}
// cambiar el user_scored a true
Future<void> changeUserScored(String serviceId) async {
try {
await FirebaseFirestore.instance
.collection('services')
.doc(serviceId)
.update({'user_scored': true});
notifyListeners();
} catch (e) {
print('Error al actualizar el estado: $e');
}
}
// cambiar el professional_scored a true
Future<void> changeProfessionalScored(String serviceId) async {
try {
await FirebaseFirestore.instance
.collection('services')
.doc(serviceId)
.update({'professional_scored': true});
notifyListeners();
} catch (e) {
print('Error al actualizar el estado: $e');
}
}
void clearServices() {
services.clear();
notifyListeners();
}
getServiceForUser(String serviceId) async {
try {
isLoading = true;
final queryService = await _servicesCollection.doc(serviceId).get();
final servicio = Service.fromDocument(queryService);
final queryUsers =
await _usersCollection.doc(servicio.professionalId).get();
final user = Usuario.fromDocument(queryUsers.data()!);
service = ServicioProfesional(
user: user,
service: servicio,
);
} catch (e) {
service = null;
} finally {
isLoading = false;
notifyListeners();
}
}
getServiceForProfessional(String serviceId) async {
try {
isLoading = true;
final queryService = await _servicesCollection.doc(serviceId).get();
final servicio = Service.fromDocument(queryService);
final queryUsers = await _usersCollection.doc(servicio.userId).get();
final user = Usuario.fromDocument(queryUsers.data()!);
service = ServicioProfesional(
user: user,
service: servicio,
);
} catch (e) {
service = null;
} finally {
isLoading = false;
notifyListeners();
}
}
getServicesForUser(String userId) async {
try {
isLoading = true;
final queryServices = await _servicesCollection
.where('user_id', isEqualTo: userId)
.where('status', whereIn: [0, 1, 3]).get();
final servicios = queryServices.docs.map(Service.fromDocument).toList();
final professionalIds =
servicios.map((service) => service.professionalId).toList();
final queryUsers = await _usersCollection
.where('professional_state', isEqualTo: ProState.active.index)
.where(FieldPath.documentId, whereIn: professionalIds)
.get();
final professionalUsers =
queryUsers.docs.map((e) => Usuario.fromDocument(e.data())).toList();
final professionalUsersMap = {
for (var user in professionalUsers) user.id: user
};
final servicesMap = (servicios.map((service) {
final user = professionalUsersMap[service.professionalId];
return ServicioProfesional(
user: user!,
service: service,
);
}).toList());
services = servicesMap
..sort((a, b) {
final dateA = DateTime.parse(a.service.day);
final dateB = DateTime.parse(b.service.day);
final range1A =
a.service.range1Hour1.hour * 60 + a.service.range1Hour1.minute;
final range1B =
b.service.range1Hour1.hour * 60 + b.service.range1Hour1.minute;
if (dateA.compareTo(dateB) != 0) {
return dateA.compareTo(dateB);
} else {
return range1A.compareTo(range1B);
}
});
} catch (e) {
print('Error obteniendo servicios: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
getServicesRequestsForProfessional(String userId) async {
try {
isLoading = true;
final queryServices = await _servicesCollection
.where('professional_id', isEqualTo: userId)
.where('status', whereIn: [0]).get();
final servicios = queryServices.docs.map(Service.fromDocument).toList();
final userIds = servicios.map((service) => service.userId).toList();
final queryUsers = await _usersCollection
.where(FieldPath.documentId, whereIn: userIds)
.get();
final users =
queryUsers.docs.map((e) => Usuario.fromDocument(e.data())).toList();
final usersMap = {for (var doc in users) doc.id: doc};
final servicesMap = (servicios.map((service) {
final user = usersMap[service.userId];
return ServicioProfesional(
user: user!,
service: service,
);
}).toList());
services = servicesMap
..sort((a, b) {
final dateA = DateTime.parse(a.service.day);
final dateB = DateTime.parse(b.service.day);
final range1A =
a.service.range1Hour1.hour * 60 + a.service.range1Hour1.minute;
final range1B =
b.service.range1Hour1.hour * 60 + b.service.range1Hour1.minute;
if (dateA.compareTo(dateB) != 0) {
return dateA.compareTo(dateB);
} else {
return range1A.compareTo(range1B);
}
});
} catch (e) {
print('Error obteniendo servicios: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
getServicesForProfessional(String userId) async {
try {
isLoading = true;
final queryServices = await _servicesCollection
.where('professional_id', isEqualTo: userId)
.where('status', whereIn: [1, 3]).get();
final servicios = queryServices.docs.map(Service.fromDocument).toList();
final userIds = servicios.map((service) => service.userId).toList();
final queryUsers = await _usersCollection
.where(FieldPath.documentId, whereIn: userIds)
.get();
final users =
queryUsers.docs.map((e) => Usuario.fromDocument(e.data())).toList();
final usersMap = {for (var doc in users) doc.id: doc};
final servicesMap = (servicios.map((service) {
final user = usersMap[service.userId];
return ServicioProfesional(
user: user!,
service: service,
);
}).toList());
services = servicesMap
..sort((a, b) {
final dateA = DateTime.parse(a.service.day);
final dateB = DateTime.parse(b.service.day);
final range1A =
a.service.range1Hour1.hour * 60 + a.service.range1Hour1.minute;
final range1B =
b.service.range1Hour1.hour * 60 + b.service.range1Hour1.minute;
if (dateA.compareTo(dateB) != 0) {
return dateA.compareTo(dateB);
} else {
return range1A.compareTo(range1B);
}
});
} catch (e) {
print('Error obteniendo servicios: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
getServicesHistoryForUser(String userId) async {
try {
isLoading = true;
final queryServices = await _servicesCollection
.where('user_id', isEqualTo: userId)
.where('status', whereIn: [2, 4, 5]).get();
final servicios = queryServices.docs.map(Service.fromDocument).toList();
final professionalIds =
servicios.map((service) => service.professionalId).toList();
final queryUsers = await _usersCollection
.where('professional_state', isEqualTo: ProState.active.index)
.where(FieldPath.documentId, whereIn: professionalIds)
.get();
final professionalUsers =
queryUsers.docs.map((e) => Usuario.fromDocument(e.data())).toList();
final professionalUsersMap = {
for (var user in professionalUsers) user.id: user
};
final servicesMap = (servicios.map((service) {
final user = professionalUsersMap[service.professionalId];
return ServicioProfesional(
user: user!,
service: service,
);
}).toList());
services = servicesMap
..sort((a, b) {
final dateA = DateTime.parse(a.service.day);
final dateB = DateTime.parse(b.service.day);
final range1A =
a.service.range1Hour1.hour * 60 + a.service.range1Hour1.minute;
final range1B =
b.service.range1Hour1.hour * 60 + b.service.range1Hour1.minute;
if (dateA.compareTo(dateB) != 0) {
return dateA.compareTo(dateB);
} else {
return range1A.compareTo(range1B);
}
});
} catch (e) {
print('Error obteniendo servicios: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
getServicesHistoryForProfessional(String userId) async {
try {
isLoading = true;
final queryServices = await _servicesCollection
.where('professional_id', isEqualTo: userId)
.where('status', whereIn: [2, 4, 5]).get();
final servicios = queryServices.docs.map(Service.fromDocument).toList();
final userIds = servicios.map((service) => service.userId).toList();
final queryUsers = await _usersCollection
.where(FieldPath.documentId, whereIn: userIds)
.get();
final users =
queryUsers.docs.map((e) => Usuario.fromDocument(e.data())).toList();
final usersMap = {for (var doc in users) doc.id: doc};
final servicesMap = (servicios.map((service) {
final user = usersMap[service.userId];
return ServicioProfesional(
user: user!,
service: service,
);
}).toList());
services = servicesMap
..sort((a, b) {
final dateA = DateTime.parse(a.service.day);
final dateB = DateTime.parse(b.service.day);
final range1A =
a.service.range1Hour1.hour * 60 + a.service.range1Hour1.minute;
final range1B =
b.service.range1Hour1.hour * 60 + b.service.range1Hour1.minute;
if (dateA.compareTo(dateB) != 0) {
return dateA.compareTo(dateB);
} else {
return range1A.compareTo(range1B);
}
});
} catch (e) {
print('Error obteniendo servicios: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
}
+26
View File
@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:prosapp_web_app/models/setting.dart';
class SettingsProvider extends ChangeNotifier {
Setting? settings;
bool isLoading = true;
final _settingsCollection = FirebaseFirestore.instance.collection('settings');
SettingsProvider() {
getSettings();
}
getSettings() async {
try {
final querySnapshot = await _settingsCollection.doc('global').get();
settings = Setting.fromDocument(querySnapshot.data()!);
} catch (e) {
print('Error obteniendo los settings: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
}
+44
View File
@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
class SideMenuProvider extends ChangeNotifier {
static late AnimationController menuController;
static bool isOpen = false;
String _currentPage = '';
String get currentPage {
return _currentPage;
}
void setCurrentPageUrl(String routeName) {
_currentPage = routeName;
Future.delayed(const Duration(milliseconds: 100), () {
notifyListeners();
});
}
static Animation<double> movement =
Tween<double>(begin: -200, end: 0).animate(
CurvedAnimation(parent: menuController, curve: Curves.easeInOut),
);
static Animation<double> opacity = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: menuController, curve: Curves.easeInOut),
);
static void openMenu() {
isOpen = true;
menuController.forward();
}
static void closeMenu() {
isOpen = false;
menuController.reverse();
}
static void toggleMenu() {
(isOpen) ? menuController.reverse() : menuController.forward();
isOpen = !isOpen;
}
}
@@ -0,0 +1,43 @@
import 'dart:developer';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:prosapp_web_app/models/chat_entity.dart';
import 'package:prosapp_web_app/models/message_entity.dart';
class FirebaseChatRepository {
final chatCollection = FirebaseFirestore.instance.collection('chats');
Stream<ChatEntity?> getChatById(String chatId) {
return chatCollection.doc(chatId).snapshots().map((snapshot) {
try {
if (snapshot.exists) {
return ChatEntity.fromDocument(snapshot.data()!);
} else {
return null;
}
} catch (e) {
log(e.toString());
return null;
}
});
}
Future<ChatEntity> createNewChat(
String chatId, String userId, String professionalId) async {
ChatEntity chat = ChatEntity(
id: chatId,
userId: userId,
professionalId: professionalId,
messages: const [],
);
await chatCollection.doc(chatId).set(chat.toDocument());
return chat;
}
sendMessage(String chatId, MessageEntity message) {
chatCollection.doc(chatId).update({
'messages': FieldValue.arrayUnion([message.toDocument()])
});
}
}
@@ -0,0 +1,91 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:prosapp_web_app/models/comment_entity.dart';
import 'package:prosapp_web_app/models/score_entity.dart';
class FirebaseScoreRepository {
final reputationsCollection =
FirebaseFirestore.instance.collection('reputations');
final commentsCollection = FirebaseFirestore.instance.collection('comments');
Future<ReputationEntity> getReputationByUserId(String userId) async {
try {
final snap = await reputationsCollection.doc(userId).get();
return ReputationEntity.fromDocument(snap.data()!);
} catch (e) {
return const ReputationEntity(
total: 0,
average: 0,
totalPro: 0,
averagePro: 0,
);
}
}
Stream<ReputationEntity> streamReputation() {
return FirebaseAuth.instance.userChanges().asyncMap((user) async {
if (user != null) {
return await getReputationByUserId(user.uid);
} else {
return const ReputationEntity(
total: 0,
average: 0,
totalPro: 0,
averagePro: 0,
);
}
});
}
Stream<List<CommentEntity>> getScoresForUser(String userId) {
return commentsCollection
.where('destination_id', isEqualTo: userId)
.where('is_from_user', isEqualTo: false)
.snapshots()
.map((querySnapshot) => querySnapshot.docs
.map((doc) => CommentEntity.fromDocument(doc.data()))
.toList());
}
Stream<List<CommentEntity>> getScoresForProfessional(String userId) {
return commentsCollection
.where('destination_id', isEqualTo: userId)
.where('is_from_user', isEqualTo: true)
.snapshots()
.map((querySnapshot) => querySnapshot.docs
.map((doc) => CommentEntity.fromDocument(doc.data()))
.toList());
}
Future<void> addComment(CommentEntity comment) async {
await commentsCollection.add(comment.toDocument());
final query = await commentsCollection
.where('destination_id', isEqualTo: comment.destinationId)
.where('is_from_user', isEqualTo: comment.isFromUser)
.get();
var total = 0.0;
var count = 0;
for (var doc in query.docs) {
final comment = CommentEntity.fromDocument(doc.data());
total += comment.score;
count++;
}
if (count > 0) {
final average = total / count;
if (comment.isFromUser) {
await reputationsCollection.doc(comment.destinationId).set(
{'total_pro': count, 'average_pro': average},
SetOptions(merge: true));
} else {
await reputationsCollection.doc(comment.destinationId).set({
'total': count,
'average': average,
}, SetOptions(merge: true));
}
}
}
}
+39
View File
@@ -0,0 +1,39 @@
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/ui/views/dashboard_view.dart';
import 'package:prosapp_web_app/ui/views/login_view.dart';
import 'package:prosapp_web_app/ui/views/phone_login_view.dart';
import 'package:prosapp_web_app/ui/views/register_view.dart';
import 'package:fluro/fluro.dart';
import 'package:provider/provider.dart';
class AdminHandlers {
static Handler phone = Handler(handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
if (authProvider.authStatus == AuthStatus.notAuthenticated) {
return const PhoneLoginView();
} else {
return const DashboardView();
}
});
static Handler login = Handler(handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
if (authProvider.authStatus == AuthStatus.notAuthenticated) {
return const LoginView();
} else {
return const DashboardView();
}
});
static Handler register = Handler(handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
if (authProvider.authStatus == AuthStatus.notAuthenticated) {
return const RegisterView();
} else {
return const DashboardView();
}
});
}
+404
View File
@@ -0,0 +1,404 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/providers/auth_provider.dart';
import 'package:prosapp_web_app/providers/sidemenu_provider.dart';
import 'package:prosapp_web_app/router/router.dart';
import 'package:prosapp_web_app/ui/views/calendar_view.dart';
import 'package:prosapp_web_app/ui/views/chat_view.dart';
import 'package:prosapp_web_app/ui/views/email_view.dart';
import 'package:prosapp_web_app/ui/views/phone_view.dart';
import 'package:prosapp_web_app/ui/views/professional_calendar_view.dart';
import 'package:prosapp_web_app/ui/views/dashboard_view.dart';
import 'package:prosapp_web_app/ui/views/login_view.dart';
import 'package:prosapp_web_app/ui/views/no_page_found_view.dart';
import 'package:prosapp_web_app/ui/views/professional_profile_view.dart';
import 'package:prosapp_web_app/ui/views/professionals_view.dart';
import 'package:prosapp_web_app/ui/views/profile_view.dart';
import 'package:prosapp_web_app/ui/views/rating_view.dart';
import 'package:prosapp_web_app/ui/views/request_professional_view.dart';
import 'package:prosapp_web_app/ui/views/schedule_view.dart';
import 'package:prosapp_web_app/ui/views/service_view.dart';
import 'package:prosapp_web_app/ui/views/services_history_view.dart';
import 'package:prosapp_web_app/ui/views/services_requests_view.dart';
import 'package:prosapp_web_app/ui/views/services_view.dart';
import 'package:prosapp_web_app/ui/views/support_view.dart';
import 'package:provider/provider.dart';
class DashboardHandlers {
static Handler dashboard = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.dashboardRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const DashboardView();
} else {
return const LoginView();
}
},
);
static Handler support = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.supportRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const SupportView();
} else {
return const LoginView();
}
},
);
// users
static Handler profile = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.profileRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const ProfileView();
} else {
return const LoginView();
}
},
);
static Handler phone = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.phoneRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const PhoneView();
} else {
return const LoginView();
}
},
);
static Handler email = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.emailRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const EmailView();
} else {
return const LoginView();
}
},
);
static Handler professionals = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.professionalsRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const ProfessionalsView();
} else {
return const LoginView();
}
},
);
static Handler userServices = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.userServicesRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const ServicesView(type: 'user');
} else {
return const LoginView();
}
},
);
static Handler professionalServices = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.professionalServicesRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const ServicesView(type: 'professional');
} else {
return const LoginView();
}
},
);
static Handler userService = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.userServiceRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
if (params['uid']?.first != null) {
return ServiceView(type: 'user', serviceId: params['uid']!.first);
} else {
// return const NoPageFoundView();
return Center(child: Text('No se encontró el servicio.'));
}
} else {
return const LoginView();
}
},
);
static Handler professionalService = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.professionalServiceRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
if (params['uid']?.first != null) {
return ServiceView(
type: 'professional', serviceId: params['uid']!.first);
} else {
return Center(child: Text('No se encontró el servicio.'));
// return const NoPageFoundView();
}
} else {
return const LoginView();
}
},
);
static Handler userRating = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.userRatingRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
if (params['serviceId']?.first != null &&
params['professionalId']?.first != null) {
final serviceId = params['serviceId']!.first;
final professionalId = params['professionalId']!.first;
return RatingView(
type: 'user',
serviceId: serviceId,
professionalId: professionalId);
} else {
return const NoPageFoundView();
}
} else {
return const LoginView();
}
},
);
static Handler professionalRating = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.professionalRatingRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
if (params['serviceId']?.first != null &&
params['userId']?.first != null) {
final serviceId = params['serviceId']!.first;
final userId = params['userId']!.first;
return RatingView(
type: 'professional',
serviceId: serviceId,
professionalId: userId);
} else {
return const NoPageFoundView();
}
} else {
return const LoginView();
}
},
);
static Handler userChat = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.userChatRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
if (params['serviceId']?.first != null &&
params['professionalId']?.first != null) {
final serviceId = params['serviceId']!.first;
final professionalId = params['professionalId']!.first;
return ChatView(
type: 'user',
serviceId: serviceId,
professionalId: professionalId);
} else {
return const NoPageFoundView();
}
} else {
return const LoginView();
}
},
);
static Handler professionalChat = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.professionalChatRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
if (params['serviceId']?.first != null &&
params['userId']?.first != null) {
final serviceId = params['serviceId']!.first;
final userId = params['userId']!.first;
return ChatView(
type: 'professional',
serviceId: serviceId,
professionalId: userId);
} else {
return const NoPageFoundView();
}
} else {
return const LoginView();
}
},
);
static Handler userServicesHistory = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.userServicesHistoryRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const ServicesHistoryView(type: 'user');
} else {
return const LoginView();
}
},
);
static Handler professionalServicesRequests = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.professionalServicesRequestsRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const ServicesRequestsView();
} else {
return const LoginView();
}
},
);
static Handler professionalServicesHistory = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.professionalServicesHistoryRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const ServicesHistoryView(type: 'professional');
} else {
return const LoginView();
}
},
);
static Handler requestProfessional = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.requestProfessionalRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const RequestProfessionalView();
} else {
return const LoginView();
}
},
);
// static Handler servicesHistory = Handler(
// handlerFunc: (context, params) {
// final authProvider = Provider.of<AuthProvider>(context!);
// Provider.of<SideMenuProvider>(context, listen: false)
// .setCurrentPageUrl(Flurorouter.servicesRoute);
// if (authProvider.authStatus == AuthStatus.authenticated) {
// if (params['type']?.first != null &&
// (params['type']?.first == 'user' ||
// params['type']?.first == 'professional')) {
// return ServicesHistoryView(type: params['type']!.first);
// } else {
// return const NoPageFoundView();
// }
// } else {
// return const LoginView();
// }
// },
// );
// professionals
static Handler professionalProfile = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.professionalProfileRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const ProfessionalProfileView();
} else {
return const LoginView();
}
},
);
static Handler professionalSchedule = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.professionalProfileRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const ScheduleView();
} else {
return const LoginView();
}
},
);
static Handler calendar = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.calendarRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
if (params['uid']?.first != null) {
return CalendarView(professionalId: params['uid']!.first);
} else {
return const NoPageFoundView();
}
} else {
return const LoginView();
}
},
);
static Handler professionalCalendar = Handler(
handlerFunc: (context, params) {
final authProvider = Provider.of<AuthProvider>(context!);
Provider.of<SideMenuProvider>(context, listen: false)
.setCurrentPageUrl(Flurorouter.professionalCalendarRoute);
if (authProvider.authStatus == AuthStatus.authenticated) {
return const ProfessionalCalendarView();
} else {
return const LoginView();
}
},
);
}
+15
View File
@@ -0,0 +1,15 @@
import 'package:prosapp_web_app/providers/sidemenu_provider.dart';
import 'package:prosapp_web_app/ui/views/no_page_found_view.dart';
import 'package:fluro/fluro.dart';
import 'package:provider/provider.dart';
class NoPageFoundHandlers {
static Handler noPageFound = Handler(
handlerFunc: (context, params) {
Provider.of<SideMenuProvider>(context!, listen: false)
.setCurrentPageUrl('/404');
return const NoPageFoundView();
},
);
}
+196
View File
@@ -0,0 +1,196 @@
import 'package:prosapp_web_app/router/admin_handlers.dart';
import 'package:prosapp_web_app/router/dashboard_handlers.dart';
import 'package:prosapp_web_app/router/no_page_found_handlers.dart';
import 'package:fluro/fluro.dart';
class Flurorouter {
static final FluroRouter router = FluroRouter();
static String rootRoute = "/";
// Auth Router
static String phoneLoginRoute = "/auth/phone_login";
static String loginRoute = "/auth/login";
static String registerRoute = "/auth/register";
// Dashboard
static String dashboardRoute = "/dashboard";
static String supportRoute = "/dashboard/support";
// Users
static String profileRoute = "/dashboard/profile";
static String phoneRoute = "/dashboard/phone";
static String emailRoute = "/dashboard/email";
static String userServicesRoute = "/dashboard/user/services";
static String userServicesHistoryRoute = "/dashboard/user/services/history";
static String userServiceRoute = "/dashboard/user/service/:uid";
static String calendarRoute = "/dashboard/calendar/:uid";
static String requestProfessionalRoute = "/dashboard/request/professional";
static String userRatingRoute = "/dashboard/user/service/:serviceId/rating/:professionalId";
static String professionalRatingRoute = "/dashboard/professional/service/:serviceId/rating/:userId";
static String userChatRoute = "/dashboard/user/service/:serviceId/chat/:professionalId";
static String professionalChatRoute = "/dashboard/professional/service/:serviceId/chat/:userId";
// Professionals
static String professionalProfileRoute = "/dashboard/professional/profile";
static String professionalScheduleRoute = "/dashboard/professional/schedule";
static String professionalServicesRoute = "/dashboard/professional/services";
static String professionalServicesRequestsRoute =
"/dashboard/professional/requests";
static String professionalServicesHistoryRoute =
"/dashboard/professional/services/history";
static String professionalServiceRoute =
"/dashboard/professional/service/:uid";
static String professionalsRoute = "/dashboard/professionals";
static String professionalCalendarRoute = "/dashboard/professional/calendar";
static void configureRoutes() {
// Auth Routes
router.define(
rootRoute,
handler: AdminHandlers.phone,
transitionType: TransitionType.none,
);
router.define(
phoneLoginRoute,
handler: AdminHandlers.phone,
transitionType: TransitionType.none,
);
router.define(
loginRoute,
handler: AdminHandlers.login,
transitionType: TransitionType.none,
);
router.define(
registerRoute,
handler: AdminHandlers.register,
transitionType: TransitionType.none,
);
// Dashboard Routes
router.define(
dashboardRoute,
handler: DashboardHandlers.dashboard,
transitionType: TransitionType.none,
);
router.define(
supportRoute,
handler: DashboardHandlers.support,
transitionType: TransitionType.none,
);
// users
router.define(
profileRoute,
handler: DashboardHandlers.profile,
transitionType: TransitionType.none,
);
router.define(
phoneRoute,
handler: DashboardHandlers.phone,
transitionType: TransitionType.none,
);
router.define(
emailRoute,
handler: DashboardHandlers.email,
transitionType: TransitionType.none,
);
router.define(
professionalsRoute,
handler: DashboardHandlers.professionals,
transitionType: TransitionType.none,
);
router.define(
userServicesRoute,
handler: DashboardHandlers.userServices,
transitionType: TransitionType.none,
);
router.define(
userServicesHistoryRoute,
handler: DashboardHandlers.userServicesHistory,
transitionType: TransitionType.none,
);
router.define(
userServiceRoute,
handler: DashboardHandlers.userService,
transitionType: TransitionType.none,
);
router.define(
professionalServiceRoute,
handler: DashboardHandlers.professionalService,
transitionType: TransitionType.none,
);
router.define(
userRatingRoute,
handler: DashboardHandlers.userRating,
transitionType: TransitionType.none,
);
router.define(
professionalRatingRoute,
handler: DashboardHandlers.professionalRating,
transitionType: TransitionType.none,
);
router.define(
userChatRoute,
handler: DashboardHandlers.userChat,
transitionType: TransitionType.none,
);
router.define(
professionalChatRoute,
handler: DashboardHandlers.professionalChat,
transitionType: TransitionType.none,
);
router.define(
calendarRoute,
handler: DashboardHandlers.calendar,
transitionType: TransitionType.none,
);
router.define(
requestProfessionalRoute,
handler: DashboardHandlers.requestProfessional,
transitionType: TransitionType.none,
);
// profesionals
router.define(
professionalProfileRoute,
handler: DashboardHandlers.professionalProfile,
transitionType: TransitionType.none,
);
router.define(
professionalScheduleRoute,
handler: DashboardHandlers.professionalSchedule,
transitionType: TransitionType.none,
);
router.define(
professionalServicesRoute,
handler: DashboardHandlers.professionalServices,
transitionType: TransitionType.none,
);
router.define(
professionalServicesRequestsRoute,
handler: DashboardHandlers.professionalServicesRequests,
transitionType: TransitionType.none,
);
router.define(
professionalServicesHistoryRoute,
handler: DashboardHandlers.professionalServicesHistory,
transitionType: TransitionType.none,
);
router.define(
professionalCalendarRoute,
handler: DashboardHandlers.professionalCalendar,
transitionType: TransitionType.none,
);
// 404
router.notFoundHandler = NoPageFoundHandlers.noPageFound;
}
}
+9
View File
@@ -0,0 +1,9 @@
import 'package:shared_preferences/shared_preferences.dart';
class LocalStorage {
static late SharedPreferences prefs;
static Future<void> configurePrefs() async {
prefs = await SharedPreferences.getInstance();
}
}
+17
View File
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class NavigationService {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
static navigateTo(String routeName) {
navigatorKey.currentState!.pushNamed(routeName);
}
static Future<dynamic> navigateToFuture(String routeName) async {
return await navigatorKey.currentState!.pushNamed(routeName);
}
static replaceTo(String routeName) {
navigatorKey.currentState!.pushReplacementNamed(routeName);
}
}
+53
View File
@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
class NotificationsService {
static GlobalKey<ScaffoldMessengerState> messengerKey = GlobalKey<ScaffoldMessengerState>();
static showSnackBarError(String message) {
final snackBar = SnackBar(
backgroundColor: Colors.red.withOpacity(0.9),
content: Text(
message,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
),
),
);
messengerKey.currentState!
..removeCurrentSnackBar()
..showSnackBar(snackBar);
}
static showSnackbar(String message) {
final snackBar = SnackBar(
backgroundColor: Colors.blue.withOpacity(0.9),
content: Text(
message,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
),
),
);
messengerKey.currentState!
..removeCurrentSnackBar()
..showSnackBar(snackBar);
}
static showBusyIndicator(BuildContext context) {
const AlertDialog dialog = AlertDialog(
content: SizedBox(
width: 100,
height: 100,
child: Center(
child: CircularProgressIndicator(),
),
),
);
showDialog(context: context, builder: (_) => dialog);
}
}
@@ -1,85 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:prosappco/src/models/user_model.dart';
import 'package:prosappco/src/presentation/screens/login/login.dart';
import 'package:prosappco/src/presentation/screens/map/service.dart';
import 'package:prosappco/src/presentation/screens/service_web.dart';
import 'package:prosappco/src/services/api_service.dart';
import 'package:prosappco/src/utils/app_navigator.dart';
class AuthenticationRepository {
static final AuthenticationRepository instance = AuthenticationRepository._();
AuthenticationRepository._() {
WidgetsBinding.instance.addPostFrameCallback((_) => _checkSession());
}
final _api = ApiService.instance;
UserModel? currentUser;
bool isLoggedIn = false;
Future<void> _checkSession() async {
final token = await _api.getToken();
if (token == null) {
pushOffAll(const LoginScreen());
return;
}
try {
final data = await _api.get('/auth/me');
currentUser = UserModel.fromApi(data as Map<String, dynamic>);
isLoggedIn = true;
_navigateHome();
} catch (_) {
await _api.clearToken();
pushOffAll(const LoginScreen());
}
}
void _navigateHome() {
kIsWeb
? pushOffAll(const ServiceWebScreen())
: pushOffAll(const ServiceScreen());
}
Future<void> loginWithEmailAndPassword(String email, String password) async {
final data = await _api.post('/auth/login', {'email': email, 'password': password});
await _api.saveToken(data['access_token']);
currentUser = UserModel.fromApi(data['user'] as Map<String, dynamic>);
isLoggedIn = true;
_navigateHome();
}
Future<void> createUserWithEmailAndPassword(String email, String password, String name) async {
final data = await _api.post('/auth/register', {
'email': email,
'password': password,
'name': name,
});
await _api.saveToken(data['access_token']);
currentUser = UserModel.fromApi(data['user'] as Map<String, dynamic>);
isLoggedIn = true;
_navigateHome();
}
Future<void> phoneAuthentication(String phone, {String? name}) async {
final data = await _api.post('/auth/phone', {'phone': phone, 'name': name ?? phone});
await _api.saveToken(data['access_token']);
currentUser = UserModel.fromApi(data['user'] as Map<String, dynamic>);
isLoggedIn = true;
_navigateHome();
}
Future<void> logout() async {
await _api.clearToken();
currentUser = null;
isLoggedIn = false;
pushOffAll(const LoginScreen());
}
Future<void> signInWithGoogle() async {
throw UnimplementedError('Google Sign-In no disponible en esta versión');
}
String? getCurrentUserUid() => currentUser?.id;
String? getCurrentUserPhone() => currentUser?.phoneNumber;
}
@@ -1,19 +0,0 @@
class SignUpWithEmailAndPasswordFailure {
final String message;
const SignUpWithEmailAndPasswordFailure(
[this.message = "An Unknown error ocurred."]);
factory SignUpWithEmailAndPasswordFailure.code(String code) {
switch (code) {
case 'weak-password':
return const SignUpWithEmailAndPasswordFailure(
'Please enter a stronger password.');
case 'email-alredy-in-use':
return const SignUpWithEmailAndPasswordFailure(
'An account alredy exists for that email.');
default:
return const SignUpWithEmailAndPasswordFailure();
}
}
}
-136
View File
@@ -1,136 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:prosappco/src/components/column_padding.dart';
const double photoSize = 150;
class ReferenceBannerPhoto extends StatelessWidget {
/// Accepts either a String URL or null.
final Object? ref;
final double size;
final double sizeCircle;
const ReferenceBannerPhoto({
super.key,
required this.ref,
this.size = photoSize,
this.sizeCircle = photoSize,
});
@override
Widget build(BuildContext context) {
final url = ref is String ? ref as String : null;
if (url == null || url.isEmpty || url.startsWith('...')) {
return DefaultPhoto(sizeDefault: sizeCircle);
}
return Image.network(
url,
width: double.infinity,
height: size,
fit: BoxFit.fill,
errorBuilder: (_, __, ___) => DefaultPhoto(sizeDefault: sizeCircle),
);
}
}
class DefaultPhoto extends StatelessWidget {
final double sizeDefault;
const DefaultPhoto({
super.key,
this.sizeDefault = photoSize,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 5,
offset: const Offset(0, 1),
),
],
),
height: 150,
child: ColumnPadding(
alineacion: MainAxisAlignment.spaceAround,
padding: const EdgeInsets.symmetric(horizontal: 70),
children: [
Container(
decoration: BoxDecoration(
color: const Color(0xFFD6F4FF),
borderRadius: BorderRadius.circular(50),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.3),
spreadRadius: 2,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
),
constraints: const BoxConstraints(minWidth: 250, minHeight: 50),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Center(
child: Text(
'Foto portada',
style: TextStyle(
color: Color(0xFF2BA4EC),
fontSize: 17,
),
),
),
),
Padding(
padding: EdgeInsets.only(right: 15),
child: Icon(
Icons.file_upload_outlined,
color: Color(0xFF2BA4EC),
size: 30,
),
),
],
),
),
],
),
);
}
}
class LocalPhoto extends StatelessWidget {
final File file;
const LocalPhoto({super.key, required this.file});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Image.file(
file,
width: double.infinity,
height: photoSize,
fit: BoxFit.fill,
),
);
}
}
-69
View File
@@ -1,69 +0,0 @@
import 'package:flutter/material.dart';
import 'package:animate_do/animate_do.dart';
class BottomSheetExpanded extends StatelessWidget {
final List<Widget> children;
final double horizontalPadding;
const BottomSheetExpanded({
Key? key,
required this.children,
this.horizontalPadding = 35,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
colors: [
Color.fromARGB(255, 139, 224, 255),
Color.fromARGB(255, 152, 228, 255),
Color(0xFFD6F4FF),
],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(height: 50),
Center(
child: FadeInLeft(
duration: const Duration(milliseconds: 1000),
child: const Image(
image: AssetImage('images/logo_prosapp.png'),
),
),
),
const SizedBox(height: 30),
Expanded(
child: FadeInUpBig(
duration: const Duration(milliseconds: 1000),
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(60),
topRight: Radius.circular(60),
),
),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: horizontalPadding,
vertical: 15,
),
child: SingleChildScrollView(
child: Column(children: children)),
),
),
),
),
],
),
),
);
}
}
-21
View File
@@ -1,21 +0,0 @@
import 'package:flutter/material.dart';
class ColumnPadding extends StatelessWidget {
final List<Widget> children;
final EdgeInsetsGeometry padding;
final MainAxisAlignment alineacion;
const ColumnPadding({
super.key,
required this.children,
required this.padding,
required this.alineacion,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: padding,
child: Column(mainAxisAlignment: alineacion, children: children));
}
}
-407
View File
@@ -1,407 +0,0 @@
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/user_model.dart';
import 'package:prosappco/src/presentation/screens/calendar.dart';
import 'package:prosappco/src/presentation/screens/my_services_pro.dart';
import 'package:prosappco/src/presentation/screens/profile/profile.dart';
import 'package:prosappco/src/presentation/screens/profile/profile_pro.dart';
import 'package:prosappco/src/presentation/screens/profile/profile_pro_web.dart';
import 'package:prosappco/src/presentation/screens/reputacion_pro.dart';
import 'package:prosappco/src/presentation/screens/web_view.dart';
import '../models/scores_model.dart';
import '../presentation/screens/configuracion.dart';
import '../presentation/screens/support.dart';
import 'package:url_launcher/url_launcher.dart';
class DrawerProfessional extends StatefulWidget {
@override
State<DrawerProfessional> createState() => _DrawerProfessionalState();
}
class _DrawerProfessionalState extends State<DrawerProfessional> {
final uid = AuthenticationRepository.instance.getCurrentUserUid();
ScoresModel? scoresModel;
Future<void> _irSugerencias() async {
const url = 'https://admin.prosapp.co/sugerencias';
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'No se pudo abrir la URL $url';
}
}
@override
void initState() {
super.initState();
if (scoresModel == null) {
ScoresModel.scoreTo(uid.toString(), true, false).then(
(ScoresModel s) => setState(() => scoresModel = s),
);
}
}
@override
Widget build(BuildContext context) {
// Use in-memory currentUser — no Firebase needed
final UserModel? currentUser =
AuthenticationRepository.instance.currentUser;
return Drawer(
child: Container(
color: const Color(0xFFE9F9FF),
child: Column(
children: [
Container(
color: Colors.white,
child: Column(
children: [
ListTile(
onTap: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const ProfileScreen();
},
),
);
},
title: Text(currentUser?.name ?? '',
style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
currentUser?.phoneNumber ?? '',
style: const TextStyle(fontSize: 12),
),
Text(
currentUser?.city ?? '',
style: const TextStyle(fontSize: 12),
),
],
),
leading: ReferencePhoto(
ref: currentUser?.picture,
size: 55,
sizeCircle: 60,
),
trailing: const Icon(Icons.keyboard_arrow_right,
color: Colors.black),
contentPadding: const EdgeInsets.symmetric(
vertical: 20, horizontal: 16),
),
],
),
),
Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 0),
),
],
),
child: Divider(
height: 0,
color: Colors.grey[300],
),
),
Expanded(
child: Column(
children: [
ListTile(
onTap: () {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (BuildContext context) {
return MyServicesProScreen();
},
),
);
},
leading: const Icon(
Icons.history,
color: Colors.black,
),
title: const Text(
'Mis servicios',
style: TextStyle(fontSize: 15),
),
),
ListTile(
onTap: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
if (kIsWeb) {
return const ProfileProWebScreen();
} else {
return const ProfileProScreen();
}
},
),
);
},
leading: const Icon(
Icons.person_outline,
color: Colors.black,
),
title: const Text(
'Perfil profesional',
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 CalendarScreen();
},
),
);
},
leading: const Icon(
Icons.calendar_month,
color: Colors.black,
),
title: const Text(
'Calendario',
style: TextStyle(fontSize: 15),
),
),
Builder(builder: (BuildContext context) {
return Container(
color: const Color(0xFF2BA4EC),
child: ListTile(
onTap: () {
if (ModalRoute.of(context)?.settings.name !=
'/solicitud') {
Navigator.pushNamed(context, '/solicitud');
} else {
Scaffold.of(context).openEndDrawer();
}
},
trailing: const Icon(
Icons.keyboard_arrow_right,
color: Colors.white,
),
title: const Text(
'Solicitudes',
style: TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.bold),
),
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),
),
],
),
child: Container(
color: Colors.white,
child: ListTile(
onTap: () async {
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return const ReputationProScreen();
},
),
);
},
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: 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)}'),
],
),
),
),
),
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]),
),
],
),
],
),
),
ElevatedButton(
onPressed: () {
Navigator.pushReplacementNamed(context, '/servicio');
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 0,
minimumSize: const Size(230, 45),
),
child: const Text(
'Modo usuario',
style: TextStyle(
color: Colors.white,
fontSize: 15,
),
),
),
const SizedBox(height: 5),
ElevatedButton(
onPressed: () {
AuthenticationRepository.instance.logout();
},
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: 5),
],
),
),
);
}
}
-100
View File
@@ -1,100 +0,0 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
const double photoSize = 100;
const double iconSize = 35;
class ReferencePhoto extends StatelessWidget {
/// Accepts either a String URL or null.
final Object? ref;
final double size;
final double sizeIcon;
final double sizeCircle;
const ReferencePhoto({
super.key,
required this.ref,
this.sizeIcon = iconSize,
this.size = photoSize,
this.sizeCircle = photoSize,
});
@override
Widget build(BuildContext context) {
final url = ref is String ? ref as String : null;
if (url == null || url.isEmpty || url.startsWith('...')) {
return DefaultPhoto(sizeDefault: sizeCircle, iconDefault: sizeIcon);
}
return ClipOval(
child: Image.network(
url,
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
DefaultPhoto(sizeDefault: sizeCircle, iconDefault: sizeIcon),
),
);
}
}
class DefaultPhoto extends StatelessWidget {
final double sizeDefault;
final double iconDefault;
const DefaultPhoto({
super.key,
this.sizeDefault = photoSize,
this.iconDefault = iconSize,
});
@override
Widget build(BuildContext context) {
return Container(
width: sizeDefault,
height: sizeDefault,
decoration: BoxDecoration(
color: const Color(0xFF2BA4EC),
borderRadius: BorderRadius.circular(50),
),
child: Icon(
Icons.person,
color: const Color.fromARGB(255, 255, 255, 255),
size: iconDefault,
),
);
}
}
class LocalPhoto extends StatelessWidget {
final File file;
const LocalPhoto({super.key, required this.file});
@override
Widget build(BuildContext context) {
if (kIsWeb) {
return ClipOval(
child: Image.network(
file.path,
width: photoSize,
height: photoSize,
fit: BoxFit.cover,
),
);
}
return ClipOval(
child: Image.file(
file,
width: photoSize,
height: photoSize,
fit: BoxFit.cover,
),
);
}
}
-91
View File
@@ -1,91 +0,0 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
const double photoSize = 100;
const double iconSize = 55;
class ReferencePhotoWeb extends StatelessWidget {
/// Accepts either a String URL or null.
final Object? ref;
final double size;
final double sizeIcon;
final double sizeCircle;
const ReferencePhotoWeb({
super.key,
required this.ref,
this.sizeIcon = iconSize,
this.size = photoSize,
this.sizeCircle = photoSize,
});
@override
Widget build(BuildContext context) {
final url = ref is String ? ref as String : null;
if (url == null || url.isEmpty || url.startsWith('...')) {
return DefaultPhotoWeb(sizeDefault: sizeCircle, iconDefault: sizeIcon);
}
return ClipOval(
child: Image.network(
url,
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
DefaultPhotoWeb(sizeDefault: sizeCircle, iconDefault: sizeIcon),
),
);
}
}
class DefaultPhotoWeb extends StatelessWidget {
final double sizeDefault;
final double iconDefault;
const DefaultPhotoWeb({
super.key,
this.sizeDefault = photoSize,
this.iconDefault = iconSize,
});
@override
Widget build(BuildContext context) {
return Container(
width: sizeDefault,
height: sizeDefault,
decoration: BoxDecoration(
color: const Color(0xFF2BA4EC),
borderRadius: BorderRadius.circular(50),
),
child: Icon(
Icons.person,
color: const Color.fromARGB(255, 255, 255, 255),
size: iconDefault,
),
);
}
}
class LocalPhotoWeb extends StatelessWidget {
final Uint8List? file;
const LocalPhotoWeb({super.key, required this.file});
@override
Widget build(BuildContext context) {
if (file == null) {
return DefaultPhotoWeb();
} else {
return ClipOval(
child: Image.memory(
file!,
width: photoSize,
height: photoSize,
fit: BoxFit.cover,
),
);
}
}
}
-35
View File
@@ -1,35 +0,0 @@
import 'package:flutter/material.dart';
class PopAppbar extends StatelessWidget implements PreferredSizeWidget {
final VoidCallback onPressed;
final String label;
const PopAppbar({
super.key,
required this.onPressed,
required this.label,
});
@override
Size get preferredSize => Size.fromHeight(kToolbarHeight);
@override
Widget build(BuildContext context) {
return AppBar(
backgroundColor: Colors.white,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: onPressed,
),
iconTheme: const IconThemeData(
color: Colors.black,
),
title: Text(
label,
style: const TextStyle(
color: Colors.black,
),
),
);
}
}
-46
View File
@@ -1,46 +0,0 @@
import 'package:flutter/material.dart';
class PrimaryButtom extends StatelessWidget {
final VoidCallback onPressed;
final String label;
final bool
isEnabled; // Nuevo parámetro para indicar si el botón está habilitado
const PrimaryButtom({
Key? key,
required this.onPressed,
required this.label,
this.isEnabled = true, // Valor predeterminado: habilitado
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: isEnabled
? onPressed
: null, // Habilita/deshabilita el botón según isEnabled
style: ElevatedButton.styleFrom(
backgroundColor: isEnabled
? const Color(0xFF2BA4EC)
: Colors.grey, // Cambia el color de fondo
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: isEnabled
? 0
: 0, // Cambia la elevación para dar una sensación de clickeabilidad
minimumSize: const Size(230, 60),
),
child: Text(
label,
style: TextStyle(
color: isEnabled
? Colors.white
: Colors.black, // Cambia el color del texto
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
);
}
}
-210
View File
@@ -1,210 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:prosappco/src/services/api_service.dart';
typedef TimeCallback = void Function(TimeOfDay? pickedTime);
class SchedulePicker extends StatefulWidget {
final String name;
Schedule schedule;
SchedulePicker({super.key, required this.name, required this.schedule});
@override
State<SchedulePicker> createState() => _SchedulePickerState();
}
class _SchedulePickerState extends State<SchedulePicker> {
@override
Widget build(BuildContext context) {
return Column(
children: [
customSwitch(widget.name, widget.schedule.habilitado, (value) {
widget.schedule.habilitado = value;
}),
...datePickers(widget.schedule.habilitado),
const Divider(
height: 15,
color: Colors.grey,
),
],
);
}
List<Widget> datePickers(bool value) {
if (!value) return [];
return [
customSwitch(
'Jornada continua',
widget.schedule.jornadaContinua,
(value) {
widget.schedule.jornadaContinua = value;
},
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Row(children: [
datePicker(widget.schedule.range1Hour1,
(pickedTime) => widget.schedule.range1Hour1 = pickedTime),
const Text('-'),
...(!widget.schedule.jornadaContinua
? [
datePicker(widget.schedule.range1Hour2,
(pickedTime) => widget.schedule.range1Hour2 = pickedTime),
const Text(' '),
]
: []),
...(!widget.schedule.jornadaContinua
? [
datePicker(widget.schedule.range2Hour1,
(pickedTime) => widget.schedule.range2Hour1 = pickedTime),
const Text('-'),
]
: []),
datePicker(widget.schedule.range2Hour2,
(pickedTime) => widget.schedule.range2Hour2 = pickedTime),
]),
),
];
}
datePicker(TimeOfDay? time, TimeCallback callback) {
return Expanded(
child: TextFormField(
textAlign: TextAlign.center,
onTap: () async {
final TimeOfDay? pickedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (pickedTime != null) {
setState(() {
callback(pickedTime);
});
}
},
readOnly: true,
decoration: const InputDecoration(
hintText: 'Hora',
),
controller: TextEditingController(
text: time == null ? '' : time.format(context),
),
style: const TextStyle(fontSize: 15),
),
);
}
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: (bool newValue) {
setState(() {
onChanged(newValue);
});
},
),
),
],
),
),
);
}
}
class Schedule {
bool habilitado;
bool jornadaContinua;
TimeOfDay? range1Hour1;
TimeOfDay? range1Hour2;
TimeOfDay? range2Hour1;
TimeOfDay? range2Hour2;
Schedule(
this.habilitado,
this.jornadaContinua,
this.range1Hour1,
this.range1Hour2,
this.range2Hour1,
this.range2Hour2,
);
static Schedule fromJson(Map<String, dynamic> json) {
return Schedule(
json['habilitado'],
json['jornadaContinua'],
_parseTime(json['range1Hour1']),
_parseTime(json['range1Hour2']),
_parseTime(json['range2Hour1']),
_parseTime(json['range2Hour2']),
);
}
static TimeOfDay? _parseTime(String? time) {
if (time == null) return null;
final components = time.split(' ');
final hourMinutes = components[0].split(':');
final hour = int.parse(hourMinutes[0]);
final minutes = int.parse(hourMinutes[1]);
if (components[1] == 'PM' && hour < 12) {
return TimeOfDay(hour: hour + 12, minute: minutes);
} else if (components[1] == 'AM' && hour == 12) {
return TimeOfDay(hour: 0, minute: minutes);
}
return TimeOfDay(hour: hour, minute: minutes);
}
static TimeOfDay stringToTimeOfDay(String? tod) {
if (tod == null) {
return TimeOfDay.now();
}
final format = DateFormat.jm();
return TimeOfDay.fromDateTime(format.parse(tod));
}
@override
String toString() {
return 'Schedule(habilitado: $habilitado, jornadaContinua: $jornadaContinua, '
'range1Hour1: $range1Hour1, range1Hour2: $range1Hour2, '
'range2Hour1: $range2Hour1, range2Hour2: $range2Hour2)';
}
static Map<String, Schedule> _emptySchedules() => {
for (var i = 1; i <= 7; i++)
'$i': Schedule(false, false, null, null, null, null),
};
static Future<Map<String, Schedule>> getHorarios(String uid) async {
try {
final Map<String, dynamic> data =
await ApiService.instance.get('/users/$uid');
final Map<String, dynamic>? horarioData =
data['horario'] as Map<String, dynamic>?;
if (horarioData == null) return _emptySchedules();
final horarios = <String, Schedule>{};
horarioData.forEach((key, value) {
horarios[key] = Schedule.fromJson(value as Map<String, dynamic>);
});
return horarios;
} catch (e) {
print('Error getting horarios: $e');
return _emptySchedules();
}
}
}
@@ -1,9 +0,0 @@
import 'package:flutter/material.dart';
class NameEmailCityController {
static final instance = NameEmailCityController();
final name = TextEditingController();
final email = TextEditingController();
final city = TextEditingController();
}
@@ -1,9 +0,0 @@
import 'package:flutter/material.dart';
class InforProfessionalController {
static final instance = InforProfessionalController();
final cedula = TextEditingController();
final profesion = TextEditingController();
final especializacion = TextEditingController();
}
@@ -1,13 +0,0 @@
import 'package:flutter/material.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
class LoginEmailController {
static final instance = LoginEmailController();
final email = TextEditingController();
final password = TextEditingController();
Future<void> loginUser(String email, String password) async {
await AuthenticationRepository.instance.loginWithEmailAndPassword(email, password);
}
}
@@ -1,20 +0,0 @@
import 'package:flutter/material.dart';
import 'package:prosappco/src/services/api_service.dart';
import 'package:prosappco/src/utils/app_navigator.dart';
class NewPhoneController {
static final instance = NewPhoneController();
final newPhoneNo = TextEditingController(text: '');
final otpCode = TextEditingController(text: '');
Future<void> updatePhoneNumber(String newPhone) async {
try {
await ApiService.instance.patch('/users/me', {'phone': newPhone});
showAppSnackBar('Número actualizado', 'El número de teléfono se actualizó correctamente.',
color: const Color(0xFF2BA4EC));
} catch (e) {
showAppSnackBar('Error al actualizar', 'No se pudo actualizar el número: $e');
}
}
}
-13
View File
@@ -1,13 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:prosappco/src/presentation/screens/map/service.dart';
import 'package:prosappco/src/presentation/screens/service_web.dart';
import 'package:prosappco/src/utils/app_navigator.dart';
class OTPController {
static final instance = OTPController();
Future<void> verifyOTP(String otp) async {
// OTP verification handled server-side; navigate home on completion
kIsWeb ? pushOffAll(const ServiceWebScreen()) : pushOffAll(const ServiceScreen());
}
}
@@ -1,12 +0,0 @@
import 'package:flutter/material.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
class PhoneAuthController {
static final instance = PhoneAuthController();
final phoneNo = TextEditingController();
Future<void> phoneAuthentication(String phoneNo) async {
await AuthenticationRepository.instance.phoneAuthentication(phoneNo);
}
}
@@ -1,14 +0,0 @@
import 'package:flutter/material.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
class RegisterController {
static final instance = RegisterController();
final email = TextEditingController();
final password = TextEditingController();
final name = TextEditingController();
Future<void> registerUser(String email, String password, String name) async {
await AuthenticationRepository.instance.createUserWithEmailAndPassword(email, password, name);
}
}
-69
View File
@@ -1,69 +0,0 @@
import 'package:prosappco/src/models/user_model.dart';
import 'package:prosappco/src/services/api_service.dart';
class ChatModel {
List<MessageModel> messages;
String professional_id;
String user_id;
UserModel? user;
UserModel? professional;
String id;
ChatModel({
required this.messages,
required this.professional_id,
required this.user_id,
this.user,
this.professional,
required this.id,
});
static ChatModel fromJson(Map<String, dynamic> data) {
final List<dynamic> msgs = data['messages'] ?? data['message'] ?? [];
final messages = msgs.map((m) {
return MessageModel(
user: m['user'] ?? m['sender_id'] ?? '',
content: m['content'] ?? m['text'] ?? '',
timestamp: m['timestamp'] != null
? DateTime.tryParse(m['timestamp'].toString()) ?? DateTime.now()
: DateTime.now(),
);
}).toList();
return ChatModel(
messages: messages,
professional_id: data['professional_id'] ?? '',
user_id: data['user_id'] ?? '',
id: data['id']?.toString() ?? '',
);
}
// Both roles use GET /chat/my — JWT identifies the current user
static Future<List<ChatModel>> getChatsByProId(String userId) =>
_getMyChats();
static Future<List<ChatModel>> getChatsByUserId(String userId) =>
_getMyChats();
static Future<List<ChatModel>> _getMyChats() async {
try {
final data = await ApiService.instance.get('/chat/my');
final List raw = data is List ? data : (data['data'] ?? []);
return raw
.map((e) => ChatModel.fromJson(e as Map<String, dynamic>))
.toList();
} catch (e) {
print('error getMyChats $e');
return [];
}
}
}
class MessageModel {
String user;
String content;
DateTime timestamp;
MessageModel(
{required this.user, required this.content, required this.timestamp});
}
-266
View File
@@ -1,266 +0,0 @@
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/models/scores_model.dart';
import 'package:prosappco/src/services/api_service.dart';
final uid = AuthenticationRepository.instance.getCurrentUserUid();
// Backend status strings
const _statusMap = {
'pendiente': 'pending',
'aprobado': 'accepted',
'negado': 'denied',
'activo': 'active',
'cancelado': 'cancelled',
'completado': 'completed',
};
class EventoService {
Future<String?> createEvent(
String title,
String description,
String day,
String range1Hour1,
String range1Hour2,
String professionalId,
String ubicacion,
String address,
double latitude,
double longitude,
String status,
int? tarifa,
bool professionalScored,
bool userScored,
) async {
try {
String fmt(String t) {
// Normalize "8:30 AM" / "08:30" → "HH:MM"
try {
final parts = t.replaceAll(RegExp(r'[APM ]'), '').split(':');
final h = int.parse(parts[0]).toString().padLeft(2, '0');
final m = (parts.length > 1 ? int.parse(parts[1]) : 0)
.toString()
.padLeft(2, '0');
return '$h:$m';
} catch (_) {
return '00:00';
}
}
final Map<String, dynamic> result =
await ApiService.instance.post('/services', {
'professional_id': professionalId,
'day': day,
'description': description,
'rate': tarifa ?? 0,
'range1_hour1': fmt(range1Hour1),
'range1_hour2': fmt(range1Hour2),
'address': address,
'latitude': latitude,
'longitude': longitude,
'location_preference': ubicacion == 'domicilio' ? 'delivery' : 'office',
});
return result['id']?.toString();
} catch (e) {
print('Evento $e');
return null;
}
}
}
// Calendar for logged-in professional — uses JWT, ignores day filter on backend
Future<List<Event>> getByProId(String day) async {
try {
final data = await ApiService.instance.get('/services/professional/calendar');
final List raw = data is List ? data : (data['data'] ?? []);
return raw
.map((e) => Event.fromJson(e as Map<String, dynamic>))
.where((e) => e.day.startsWith(day.substring(0, 10)))
.toList();
} catch (e) {
print('Error getByProId $e');
return [];
}
}
// Active/accepted services for logged-in professional
Future<List<Event>> getByProIdAll(String state1, String state2) async {
try {
final data = await ApiService.instance.get('/services/professional');
final List raw = data is Map ? (data['data'] ?? []) : (data as List);
final allowed = {
_statusMap[state1] ?? state1,
_statusMap[state2] ?? state2,
};
return raw
.map((e) => Event.fromJson(e as Map<String, dynamic>))
.where((e) => allowed.contains(e.status))
.toList();
} catch (e) {
print('Error getByProIdAll $e');
return [];
}
}
// Active/accepted services for logged-in user
Future<List<Event>> getByUserIdAll(String state1, String state2) async {
try {
final data = await ApiService.instance.get('/services/me');
final List raw = data is Map ? (data['data'] ?? []) : (data as List);
final allowed = {
_statusMap[state1] ?? state1,
_statusMap[state2] ?? state2,
};
return raw
.map((e) => Event.fromJson(e as Map<String, dynamic>))
.where((e) => allowed.contains(e.status))
.toList();
} catch (e) {
print('Error getByUserIdAll $e');
return [];
}
}
class Event {
String? id;
String title;
String? description;
String day;
String range1Hour1;
String? range1Hour2;
String userId;
String professionalId;
String? ubicacion;
String? address;
double? longitud;
double? latitud;
String status;
ScoresModel? scoresModel;
int? tarifa;
bool professionalScored;
bool userScored;
DateTime? timeStamp;
Event({
this.id,
required this.title,
this.description,
required this.day,
required this.range1Hour1,
this.range1Hour2,
required this.userId,
required this.professionalId,
this.ubicacion,
this.address,
this.longitud,
this.latitud,
this.status = 'pending',
this.timeStamp,
this.tarifa,
this.professionalScored = false,
this.userScored = false,
});
factory Event.fromJson(Map<String, dynamic> json) {
DateTime? ts;
final raw = json['created_at'] ?? json['Timestamp'];
if (raw is String) ts = DateTime.tryParse(raw);
// Backend sends ISO date for range times; extract HH:MM
String parseTime(dynamic v) {
if (v == null) return '';
final s = v.toString();
if (s.contains('T')) {
final dt = DateTime.tryParse(s);
if (dt != null) {
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
}
}
return s;
}
// Nested professional user_id takes priority as professionalId
final nestedPro = json['professionals'];
final professionalId = (nestedPro is Map)
? (nestedPro['user_id']?.toString() ?? json['professional_id']?.toString() ?? '')
: json['professional_id']?.toString() ?? '';
return Event(
id: json['id']?.toString() ?? '',
title: json['description'] ?? json['title'] ?? '',
description: json['description'],
day: json['day'] ?? '',
range1Hour1: parseTime(json['range1_hour1'] ?? json['range1Hour1']),
range1Hour2: parseTime(json['range1_hour2'] ?? json['range1Hour2']),
userId: json['user_id'] ?? '',
professionalId: professionalId,
ubicacion: json['location_preference'] ?? json['ubicacion'] ?? '',
address: json['address'] ?? '',
longitud: (json['longitude'] ?? 0).toDouble(),
latitud: (json['latitude'] ?? 0).toDouble(),
status: json['status'] ?? 'pending',
timeStamp: ts,
tarifa: (json['rate'] as num?)?.toInt() ?? json['tarifa'] ?? 0,
professionalScored: json['professional_scored'] ?? false,
userScored: json['user_scored'] ?? false,
);
}
static Future<Event> getEventById(String eventId) async {
try {
final Map<String, dynamic> data =
await ApiService.instance.get('/services/$eventId');
return Event.fromJson(data);
} catch (e) {
print('Error getting event: $e');
return Event(
title: '', day: '', range1Hour1: '', userId: '', professionalId: '');
}
}
// All services for a given professional (by user_id) — uses public calendar
static Future<List<Event>> getEventsAllById(String proId) async {
try {
final data =
await ApiService.instance.get('/services/public-calendar/$proId');
final List raw = (data is Map ? data['services'] : data) ?? [];
return raw
.map((e) => Event.fromJson(e as Map<String, dynamic>))
.toList();
} catch (e) {
print('Error getEventsAllById $e');
return [];
}
}
static Future<List<Event>> getEventsAllByIdAndStatus(String proId) async {
try {
final data =
await ApiService.instance.get('/services/public-calendar/$proId');
final List raw = (data is Map ? data['services'] : data) ?? [];
return raw
.map((e) => Event.fromJson(e as Map<String, dynamic>))
.where((e) => e.status == 'accepted' || e.status == 'pending')
.toList();
} catch (e) {
print('Error getEventsAllByIdAndStatus $e');
return [];
}
}
static Future<List<Event>> getEventsAllByIdStatus(
String proId, String state) async {
try {
final backendStatus = _statusMap[state] ?? state;
final data =
await ApiService.instance.get('/services/professional/calendar');
final List raw = data is List ? data : [];
return raw
.map((e) => Event.fromJson(e as Map<String, dynamic>))
.where((e) => e.status == backendStatus)
.toList();
} catch (e) {
print('Error getEventsAllByIdStatus $e');
return [];
}
}
}
-76
View File
@@ -1,76 +0,0 @@
import 'package:prosappco/src/models/scores_model.dart';
import 'package:prosappco/src/services/api_service.dart';
class Professional {
final String id;
final String? photoUrl;
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,
this.photoUrl,
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 { photoUrl: $photoUrl, name: $name, professionName: $professionName, cityName: $cityName }';
}
static Future<Professional?> getProfessional(String uid) async {
try {
final Map<String, dynamic> data =
await ApiService.instance.get('/professionals/$uid');
final List<String> especializaciones =
((data['especialidades'] ?? data['specialties'] ?? []) as List<dynamic>)
.map((e) => e.toString())
.toList();
return Professional(
id: uid,
photoUrl: data['picture'] ?? data['photo_url'],
name: data['name'] ?? '',
professionName: data['profession'] ?? data['profesion'] ?? '',
cityName: data['city'] ?? '',
professionalEspecializado: especializaciones,
ubicacion: data['ubicacion'] ?? '',
realAddress: data['address'] ?? '',
latitude: (data['latitude'] ?? 0).toDouble(),
longitude: (data['longitude'] ?? 0).toDouble(),
scores: await ScoresModel.scoreTo(uid, true, false),
tarifa: data['rate'] != null
? double.tryParse(data['rate'].toString())?.toInt()
: data['tarifas'],
token: data['token'],
);
} catch (e) {
print('Error al obtener profesional: $e');
return null;
}
}
}
-84
View File
@@ -1,84 +0,0 @@
import 'package:prosappco/src/services/api_service.dart';
class ScoresModel {
late int total;
late double average;
final List<ScoreDetailModel> details;
ScoresModel(this.details) {
total = details.length;
average = averageScore(details);
}
@override
String toString() {
return 'ScoresModel{total: $total, average: $average, details: $details}';
}
double averageScore(List<ScoreDetailModel> details) {
if (details.isEmpty) return 0.0;
final sum = details.map((d) => d.score).reduce((a, b) => a + b);
return sum / details.length;
}
static Future<ScoresModel> scoreTo(
String? userId, bool isFromClient, bool userInfo) async {
if (userId == null || userId.isEmpty) return ScoresModel([]);
try {
final List<dynamic> data =
await ApiService.instance.get('/comments/reputation/$userId');
final details =
data.map((e) => ScoreDetailModel.fromJson(e as Map<String, dynamic>)).toList();
return ScoresModel(details);
} catch (e) {
print('Error scoreTo: $e');
return ScoresModel([]);
}
}
// ponytail: scoreFrom and scoreTo now both hit the same reputation endpoint
static Future<ScoresModel> scoreFrom(
String userId, bool isFromClient, bool userInfo) async {
return scoreTo(userId, isFromClient, userInfo);
}
}
class ScoreDetailModel {
final String id;
final double score;
final String fromUser;
final String toUser;
final String comment;
final bool isFromClient;
final String name;
final String? avatar;
ScoreDetailModel({
required this.id,
required this.score,
required this.fromUser,
required this.toUser,
required this.comment,
required this.isFromClient,
required this.name,
this.avatar,
});
factory ScoreDetailModel.fromJson(Map<String, dynamic> data) {
return ScoreDetailModel(
id: data['id']?.toString() ?? '',
score: double.tryParse(data['score']?.toString() ?? '0') ?? 0,
fromUser: data['from_user'] ?? '',
toUser: data['to_user'] ?? '',
comment: data['comment'] ?? '',
isFromClient: data['is_from_professional'] ?? false,
name: data['from_user_name'] ?? data['name'] ?? '...',
avatar: data['avatar'],
);
}
@override
String toString() {
return 'ScoreDetailModel{id: $id, score: $score, fromUser: $fromUser, name: $name}';
}
}
-77
View File
@@ -1,77 +0,0 @@
import 'package:prosappco/src/services/api_service.dart';
class SettingModel {
final bool domicilios;
final bool google;
final bool tarifas;
final String titulo;
final String parrafo;
final String numero;
final String email;
final String dias;
final String horas;
final String version;
final String proliticasPrivacidad;
final String terminosCondiciones;
SettingModel(
this.domicilios,
this.google,
this.tarifas,
this.titulo,
this.parrafo,
this.numero,
this.email,
this.dias,
this.horas,
this.version,
this.proliticasPrivacidad,
this.terminosCondiciones,
);
static SettingModel _empty() =>
SettingModel(false, false, false, '', '', '', '', '', '', '', '', '');
static Future<SettingModel> fromJson(Map<String, dynamic>? json) async {
if (json == null) return _empty();
try {
return SettingModel(
json['domicilios'] ?? false,
json['google'] ?? false,
json['tarifas'] ?? false,
json['titulo_soporte'] ?? '',
json['parrafo_soporte'] ?? '',
json['numero_soporte'] ?? '',
json['email_soporte'] ?? '',
json['dias_soporte'] ?? '',
json['horas_soporte'] ?? '',
json['version'] ?? '',
json['politicas_privacidad'] ?? '',
json['terminos_condiciones'] ?? '',
);
} catch (e) {
print('Error settings: $e');
return _empty();
}
}
static Future<SettingModel> getSettings() async {
try {
final Map<String, dynamic> data =
await ApiService.instance.get('/settings');
return fromJson(data);
} catch (e) {
print('Error getting settings: $e');
return _empty();
}
}
@override
String toString() {
return 'SettingModel { domicilios: $domicilios, google: $google, tarifas: $tarifas, '
'titulo: $titulo, parrafo: $parrafo, numero: $numero, '
'email: $email, dias: $dias, horas: $horas, '
'version: $version, politicasPrivacidad: $proliticasPrivacidad, '
'terminosCondiciones: $terminosCondiciones }';
}
}
-64
View File
@@ -1,64 +0,0 @@
import 'package:prosappco/src/services/api_service.dart';
class UserModel {
final String id;
final String name;
final String city;
final String? profession;
final int proState;
final String? picture;
final String? banner;
final int? tarifa;
final String? phoneNumber;
final String? email;
final String? gender;
final String? birthday;
UserModel({
required this.id,
required this.name,
required this.city,
this.profession,
this.proState = 0,
this.picture,
this.banner,
this.tarifa,
this.phoneNumber,
this.email,
this.gender,
this.birthday,
});
factory UserModel.fromApi(Map<String, dynamic> json) {
final professional = json['professionals'] as Map<String, dynamic>?;
return UserModel(
id: json['id'] ?? '',
name: json['name'] ?? 'Sin nombre',
city: json['city'] ?? '',
profession: professional?['profession'],
proState: json['pro_state'] ?? 0,
picture: json['picture'],
banner: professional?['banner_picture'],
tarifa: professional?['rate'] != null
? double.tryParse(professional!['rate'].toString())?.toInt()
: null,
phoneNumber: json['phone'],
email: json['email'],
gender: json['gender'],
birthday: json['birthday'],
);
}
static Future<UserModel?> getUser(String id) async {
try {
final data = await ApiService.instance.get('/users/$id');
return UserModel.fromApi(data as Map<String, dynamic>);
} catch (_) {
return null;
}
}
@override
String toString() =>
'UserModel(id: $id, name: $name, city: $city, profession: $profession, proState: $proState)';
}
-101
View File
@@ -1,101 +0,0 @@
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 ?? ''),
),
],
),
);
}
}
-496
View File
@@ -1,496 +0,0 @@
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:prosappco/src/presentation/widgets/shared/loading_item_list.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();
today = DateTime.utc(today.year, today.month, today.day);
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 Scaffold(
body: 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),
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w600,
)),
),
),
const Divider(
height: 0,
),
Expanded(child: SingleChildScrollView(child: _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) {
List<Event> eventos = [];
if (snapshot.connectionState == ConnectionState.waiting) {
return const Column(
children: [
LoadingItemList(useCircleAvatar: false),
LoadingItemList(useCircleAvatar: false),
LoadingItemList(useCircleAvatar: false),
LoadingItemList(useCircleAvatar: false),
LoadingItemList(useCircleAvatar: false),
],
);
}
try {
snapshot.data!.sort((a, b) {
String? range1Hour1A = a.range1Hour1;
String? range1Hour1B = b.range1Hour1;
DateTime dateTimeA = DateTime.parse(range1Hour1A);
DateTime dateTimeB = DateTime.parse(range1Hour1B);
return dateTimeB.compareTo(dateTimeA);
});
eventos.addAll(snapshot.data!);
} catch (e) {
print("Error al cargar eventos: inflar $e");
}
if (eventos.isEmpty) {
return const Padding(
padding: EdgeInsets.only(top: 30),
child: Center(
child: Text(
'No tienes citas',
style: TextStyle(
color: Colors.black,
fontSize:
18, // Tamaño de fuente ajustado según tus preferencias
fontWeight:
FontWeight.w500, // Puedes ajustar el peso de la fuente
fontStyle: FontStyle.italic, // Puedes agregar estilo italic
// Otros estilos según tus preferencias
),
),
),
);
}
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),
),
),
],
);
},
);
}
}
@@ -1,315 +0,0 @@
import 'package:flutter/foundation.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/schedule_picker.dart';
import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/models/professional_model.dart';
import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart';
import 'package:prosappco/src/utils/time_of_day_utils.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:intl/intl.dart';
class CalendarProScreen extends StatefulWidget {
final Professional professional;
const CalendarProScreen({super.key, required this.professional});
@override
State<CalendarProScreen> createState() => _CalendarProScreenState();
}
class _CalendarProScreenState extends State<CalendarProScreen> {
final uid = AuthenticationRepository.instance.getCurrentUserUid();
EventoService eventoService = EventoService();
CalendarFormat _calendarFormat = CalendarFormat.month;
DateTime today = DateTime.now();
DateTime now = DateTime.now();
late int numDay;
List<Event>? _events;
Map<String, Schedule>? _horarios;
@override
void initState() {
super.initState();
if (_horarios == null) {
Schedule.getHorarios(widget.professional.id.toString()).then(
(Map<String, Schedule> data) {
setState(() {
_horarios = data;
});
},
);
}
if (_events == null) {
Event.getEventsAllByIdAndStatus(widget.professional.id.toString())
.then((value) {
setState(() {
_events = value;
});
});
}
today = DateTime.utc(today.year, today.month, today.day);
numDay = today.weekday;
}
void _onDaySelected(DateTime day, DateTime focusedDay) {
setState(() {
today = day;
numDay = today.weekday;
});
}
void _onFormatChange(CalendarFormat format) {
setState(() {
_calendarFormat = format;
});
}
@override
Widget build(BuildContext context) {
if (_events == null) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
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.now(),
lastDay: lastDay,
focusedDay: today,
availableGestures: AvailableGestures.all,
onDaySelected: _onDaySelected,
selectedDayPredicate: (day) => isSameDay(day, today),
calendarFormat: _calendarFormat,
onFormatChanged: _onFormatChange,
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),
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w600,
)),
),
),
const Divider(
height: 0,
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 15),
child: Column(
children: [...rangesItems(_horarios?[numDay.toString()])],
),
))
],
),
);
}
List<Widget> rangesItems(Schedule? schedule) {
if (schedule == null) {
return const [
Padding(
padding: EdgeInsets.only(top: 20, left: 30, right: 30),
child: Text(
'El profesional no acepta turnos este día',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
textAlign: TextAlign.center,
),
),
];
}
if (!schedule.habilitado) {
return const [
Padding(
padding: EdgeInsets.only(top: 20, left: 30, right: 30),
child: Text(
'El profesional no acepta turnos este día',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
textAlign: TextAlign.center,
),
)
];
}
if (schedule.jornadaContinua) {
List<TimeOfDay> ranges = TimeOfDayUtils.genRanges(
schedule.range1Hour1!,
schedule.range2Hour2!,
);
return rangesItemList(ranges, _events);
} else {
List<TimeOfDay> ranges1 = TimeOfDayUtils.genRanges(
schedule.range1Hour1!,
schedule.range1Hour2!,
);
List<TimeOfDay> ranges2 = TimeOfDayUtils.genRanges(
schedule.range2Hour1!,
schedule.range2Hour2!,
);
return [
...rangesItemList(ranges1, _events),
...rangesItemList(ranges2, _events),
];
}
}
List<Widget> rangesItemList(List<TimeOfDay> ranges, List<Event>? events) {
return ranges.map((time) {
if (_isHora1Ocupada(time, events)) {
return Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: ListTile(
onTap: () {
WarningSnackbar.show(
title: 'Ocupado',
message: 'Este horário ya se encuentra ocupado',
);
},
contentPadding: const EdgeInsets.all(16),
leading: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.yellow, Colors.red, Colors.red],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.access_time,
color: Colors.white,
),
),
),
title: Text(
time.format(context),
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
subtitle: const Text(
'Ocupado',
style: TextStyle(
color: Colors.red, fontSize: 13, fontWeight: FontWeight.bold),
),
trailing: const Icon(
Icons.arrow_forward_ios,
color: Colors.grey,
),
),
);
} else {
return Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: ListTile(
onTap: () {
Navigator.pop(context, [today, time, widget.professional]);
},
contentPadding: const EdgeInsets.all(16),
leading: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.green],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.access_time,
color: Colors.white,
),
),
),
title: Text(
time.format(context),
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
subtitle: const Text(
'Disponible',
style: TextStyle(
color: Colors.green,
fontSize: 13,
fontWeight: FontWeight.bold),
),
trailing: const Icon(
Icons.arrow_forward_ios,
color: Colors.grey,
),
),
);
}
}).toList();
}
bool _isHora1Ocupada(TimeOfDay hora1, List<Event>? events) {
if (events != null) {
for (Event event in events) {
DateTime time1 = DateTime(
today.year,
today.month,
today.day,
hora1.hour,
hora1.minute,
);
if (event.range1Hour1 == time1.toString()) {
return true;
}
}
}
return false;
}
}
-319
View File
@@ -1,319 +0,0 @@
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/professional_model.dart';
import 'package:prosappco/src/presentation/screens/professional_info.dart';
import 'package:prosappco/src/services/api_service.dart';
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();
// Other user info
String otherName = '';
String? otherPhoto;
String? otherProfession;
Professional? professional;
bool pro = false;
// Chat state
String? chatId;
List<MessageModel> messages = [];
bool loading = true;
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
try {
final event = await Event.getEventById(widget.eventoId!);
final isClient = uid == event.userId;
final otherUserId =
isClient ? event.professionalId : event.userId;
pro = isClient;
// Load other user info
final Map<String, dynamic> userData =
await ApiService.instance.get('/users/$otherUserId');
if (mounted) {
setState(() {
otherName = userData['name'] ?? '';
otherPhoto = userData['picture'];
otherProfession = userData['profession'];
});
}
if (isClient) {
professional = await Professional.getProfessional(event.professionalId);
}
// Start/get chat
final Map<String, dynamic> chatData = await ApiService.instance
.post('/chat/start/${event.professionalId}', {});
chatId = chatData['id']?.toString() ?? chatData['chatId']?.toString();
await _loadMessages();
} catch (e) {
print('Error initializing chat: $e');
if (mounted) setState(() => loading = false);
}
}
Future<void> _loadMessages() async {
if (chatId == null) return;
try {
final List<dynamic> data =
await ApiService.instance.get('/chat/$chatId/messages');
if (mounted) {
setState(() {
messages = data.map((m) {
return MessageModel(
user: m['sender_id'] ?? m['user'] ?? '',
content: m['content'] ?? m['text'] ?? '',
timestamp: m['created_at'] != null
? DateTime.tryParse(m['created_at'].toString()) ??
DateTime.now()
: DateTime.now(),
);
}).toList();
loading = false;
});
}
} catch (e) {
print('Error loading messages: $e');
if (mounted) setState(() => loading = false);
}
}
Future<void> _sendMessage() async {
final text = _textController.text.trim();
if (text.isEmpty || chatId == null) return;
_textController.clear();
try {
await ApiService.instance
.post('/chat/$chatId/message', {'content': text});
await _loadMessages();
} catch (e) {
print('Error sending message: $e');
}
}
@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 && professional != null) {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (BuildContext context) {
return ProfessionalInfoScreen(
professional: professional!,
);
},
),
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: ReferencePhoto(
ref: otherPhoto,
size: 55,
sizeCircle: 60,
),
),
),
title: Text(
otherName,
style: const TextStyle(
color: Colors.black, fontWeight: FontWeight.w600),
),
subtitle: Text(otherProfession ?? ''),
trailing: const Icon(Icons.keyboard_arrow_right),
),
),
),
Expanded(
child: loading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.only(top: 10),
reverse: true,
child: _messageList(),
),
),
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) => _sendMessage(),
),
),
const SizedBox(width: 12),
GestureDetector(
onTap: _sendMessage,
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),
),
),
)
],
),
),
),
],
),
);
}
Widget _messageList() {
return Column(
children: [
...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,
),
),
),
],
),
),
)
],
);
}
}
-675
View File
@@ -1,675 +0,0 @@
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/utils/app_navigator.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/presentation/screens/chat.dart';
import 'package:prosappco/src/presentation/screens/score.dart';
import 'package:prosappco/src/services/api_service.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:community_material_icon/community_material_icon.dart';
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();
DateTime today = DateTime.now();
String nombre = '';
String numberPhone = '';
String? photoUrl;
ScoresModel? scoresModel;
bool? ver = true;
bool? pro;
String formatCurrency(int number) {
final formatter =
NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: '');
return '\$${formatter.format(number)}';
}
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();
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;
}),
);
}
}
_loadOtherUser();
}
Future<void> _loadOtherUser() async {
final targetId = (uid != widget.evento.userId)
? widget.evento.userId
: widget.evento.professionalId;
try {
final Map<String, dynamic> data =
await ApiService.instance.get('/users/$targetId');
if (mounted) {
setState(() {
nombre = data['name'] ?? '';
photoUrl = data['picture'];
numberPhone = data['phone'] ?? '';
});
}
} catch (e) {
print('Error loading user: $e');
}
}
static const _toBackendStatus = {
'pendiente': 'pending',
'aprobado': 'accepted',
'denegado': 'cancelled',
'iniciado': 'active',
'terminado': 'completed',
};
Future<void> _updateStatus(String status) async {
try {
final backendStatus = _toBackendStatus[status] ?? status;
await ApiService.instance
.patch('/services/${widget.evento.id}/status', {'status': backendStatus});
} catch (e) {
print('Error updating service status: $e');
}
}
@override
Widget build(BuildContext context) {
final eventDate = DateFormat('yyyy-MM-dd').parse(widget.evento.day);
return Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Servicio'),
body: Column(
children: [
Expanded(
child: Column(
children: [
ListTile(
leading: ReferencePhoto(
ref: photoUrl,
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(
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 == 'accepted' ||
widget.evento.status == 'active'
? 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 == 'completed'
? 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 == 'accepted' ||
widget.evento.status == 'active'
? 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 != 'pending') {
// Start or retrieve chat via API
try {
await ApiService.instance.post(
'/chat/start/${widget.evento.professionalId}',
{});
} catch (_) {}
Navigator.push(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return ChatScreen(
eventoId: widget.evento.id);
},
),
);
} else {
showAppSnackBar(
'El profesional aún no ha aceptado',
'Debes esperar a que el profesional acepte tu solicitud.',
color: Colors.grey.shade700,
);
}
},
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()),
],
)
: const 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 == 'accepted'
? 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: () async {
await _updateStatus('iniciado');
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: () async {
await _updateStatus('denegado');
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 == 'pending'
? Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: widget.evento.professionalId == uid
? ElevatedButton(
onPressed: () async {
await _updateStatus('aprobado');
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: () async {
await _updateStatus('denegado');
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 == 'active' || ver == false
? Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Column(
children: [
ElevatedButton(
onPressed: () async {
await _updateStatus('terminado');
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(),
],
),
);
}
}
-178
View File
@@ -1,178 +0,0 @@
import 'package:diacritic/diacritic.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/services/api_service.dart';
class CityScreen extends StatefulWidget {
const CityScreen({super.key});
@override
State<CityScreen> createState() => _CityScreenState();
}
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 {
final List<dynamic> countries =
await ApiService.instance.get('/locations/countries');
for (final country in countries) {
final String countryName = country['name'] ?? '';
final List<dynamic> regions = country['regions'] ?? [];
for (final region in regions) {
final String regionName = region['name'] ?? '';
final List<dynamic> cities = region['cities'] ?? [];
for (final city in cities) {
citys.add(City(
cityName: city['name'] ?? '',
coordsOfCity:
'${city['latitude'] ?? 0},${city['longitude'] ?? 0}',
stateOfCity: regionName,
countryOfCity: countryName,
));
}
}
}
} catch (e) {
print('$e');
}
return citys;
}
class _CityScreenState extends State<CityScreen> {
List<City>? filteredCities;
TextEditingController searchController = TextEditingController();
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 ApiService.instance.patch('/users/me', {'city': cityName});
} catch (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 ?? "");
},
);
},
),
),
],
),
),
);
}
}
@@ -1,273 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_otp_text_field/flutter_otp_text_field.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 = OTPController.instance;
@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: 30.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),
],
),
],
),
),
),
],
),
),
);
}
}
@@ -1,88 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/utils/app_navigator.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> {
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: () {
Navigator.of(context).pop();
showAppSnackBar('Eliminar cuenta', 'Para eliminar tu cuenta contacta a soporte.', color: Colors.grey.shade700);
},
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),
),
),
],
),
);
}
}
-160
View File
@@ -1,160 +0,0 @@
import 'package:flutter/material.dart';
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/components/primary_btn.dart';
import 'package:prosappco/src/services/api_service.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();
Future<void> updateHorario(BuildContext context) async {
try {
Map<String, dynamic> horariosMap = {};
horarios.forEach((key, value) {
if (value.habilitado && !value.jornadaContinua) {
if (value.range1Hour1 == null ||
value.range1Hour2 == null ||
value.range2Hour1 == null ||
value.range2Hour2 == null) {
value.habilitado = false;
value.jornadaContinua = false;
}
}
if (value.habilitado && value.jornadaContinua) {
if (value.range1Hour1 == null || value.range2Hour2 == null) {
value.habilitado = false;
value.jornadaContinua = false;
}
}
horariosMap[key] = {
'habilitado': value.habilitado,
'jornadaContinua': value.jornadaContinua,
'range1Hour1': formatTimeOfDay(value.range1Hour1),
'range1Hour2': formatTimeOfDay(value.range1Hour2),
'range2Hour1': formatTimeOfDay(value.range2Hour1),
'range2Hour2': formatTimeOfDay(value.range2Hour2),
};
});
// Convert map keyed by day number to array format expected by backend
final List<Map<String, dynamic>> schedulesList = [];
horariosMap.forEach((key, value) {
schedulesList.add({
'day_of_week': int.tryParse(key) ?? 1,
'enabled': value['habilitado'] ?? false,
'continuous_day': value['jornadaContinua'] ?? false,
'range1_hour1': _fmtTime(value['range1Hour1']),
'range1_hour2': _fmtTime(value['range1Hour2']),
'range2_hour1': _fmtTime(value['range2Hour1']),
'range2_hour2': _fmtTime(value['range2Hour2']),
});
});
await ApiService.instance
.patch('/professionals/me/schedules', {'schedules': schedulesList});
} catch (e) {
print('Error al actualizar el horario: $e');
}
}
// Convert "8:30 AM" / "HH:MM" strings to "HH:MM" for the backend
String? _fmtTime(String? t) {
if (t == null) return null;
try {
final clean = t.replaceAll(RegExp(r'\s?[APM]+', caseSensitive: false), '').trim();
final parts = clean.split(':');
return '${int.parse(parts[0]).toString().padLeft(2, '0')}:${(parts.length > 1 ? int.parse(parts[1]) : 0).toString().padLeft(2, '0')}';
} catch (_) {
return null;
}
}
String? formatTimeOfDay(TimeOfDay? time) {
if (time != null) {
final now = DateTime.now();
final dateTime =
DateTime(now.year, now.month, now.day, time.hour, time.minute);
final format = DateFormat.jm();
return format.format(dateTime);
}
return null;
}
int _dayOfWeekToInt(String dayOfWeek) {
switch (dayOfWeek) {
case '1': return 1;
case '2': return 2;
case '3': return 3;
case '4': return 4;
case '5': return 5;
case '6': return 6;
case '7': return 7;
default: throw ArgumentError('Invalid day of week: $dayOfWeek');
}
}
String _stringToDayOfWeek(String dayOfWeek) {
switch (dayOfWeek) {
case '1': return 'Lunes';
case '2': return 'Martes';
case '3': return 'Miércoles';
case '4': return 'Jueves';
case '5': return 'Viernes';
case '6': return 'Sábado';
case '7': return 'Domingo';
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: _stringToDayOfWeek(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',
),
)
],
),
),
),
);
}
}
@@ -1,456 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl_phone_field/intl_phone_field.dart';
import 'package:prosappco/src/components/bottom_sheet.dart';
import 'package:prosappco/src/presentation/widgets/shared/primary_button.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 = PhoneAuthController.instance;
final _formKey = GlobalKey<FormState>();
String completePhoneNumber = '';
bool _isChecked = false;
SettingModel? settings;
void _clearPhoneNumber() {
if (mounted) {
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) {
if (mounted) {
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: 30.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,
),
),
),
],
),
),
PrimaryButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
PhoneAuthController.instance.phoneAuthentication(
completePhoneNumber.trim(),
);
_clearPhoneNumber();
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CodeValidationScreen(
phoneNumber: completePhoneNumber.trim(),
),
),
);
Provider.of<UserProvider>(context, listen: false)
.initUserProvider();
}
},
text: 'Enviar código',
isEnabled: _isChecked,
),
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: 30.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
),
),
),
],
),
),
PrimaryButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
PhoneAuthController.instance.phoneAuthentication(
completePhoneNumber.trim(),
);
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CodeValidationScreen(
phoneNumber: completePhoneNumber.trim(),
),
),
);
_clearPhoneNumber();
Provider.of<UserProvider>(context, listen: false)
.initUserProvider();
}
},
text: 'Enviar código',
isEnabled: _isChecked,
),
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,
),
),
),
),
],
),
);
}
}
@@ -1,550 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/bottom_sheet.dart';
import 'package:prosappco/src/presentation/widgets/shared/primary_button.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 = LoginEmailController.instance;
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 BottomSheetExpanded(
horizontalPadding: 10,
children: [
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: 30.0,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.right,
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Form(
key: _formKey,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 20),
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),
const Text('G', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18)),
],
)),
)
: 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: 10),
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: 30.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: 25),
child: Center(
child: PrimaryButton(
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();
});
}
},
text: '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,
),
),
),
),
],
),
);
}
}
File diff suppressed because it is too large Load Diff
-104
View File
@@ -1,104 +0,0 @@
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();
}
},
),
),
],
),
);
}
}
@@ -1,113 +0,0 @@
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();
}
},
),
),
],
),
);
}
}
@@ -1,234 +0,0 @@
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';
import 'package:prosappco/src/presentation/screens/score.dart';
import 'package:prosappco/src/services/api_service.dart';
class MyServicesScreen extends StatefulWidget {
MyServicesScreen({super.key});
@override
State<MyServicesScreen> createState() => _MyServicesScreenState();
}
class _MyServicesScreenState extends State<MyServicesScreen> {
DateTime today = DateTime.now();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
List<Event> eventos = [];
bool loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final raw = await ApiService.instance.get('/services/me');
final List<dynamic> data = raw is Map ? (raw['data'] ?? []) : (raw as List);
final List<Event> loaded = [];
for (final e in data) {
final event = Event.fromJson(e as Map<String, dynamic>);
event.scoresModel =
await ScoresModel.scoreTo(event.userId, false, false);
loaded.add(event);
}
loaded.sort((a, b) {
if (a.timeStamp == null || b.timeStamp == null) return 0;
return a.timeStamp!.compareTo(b.timeStamp!);
});
if (mounted) setState(() {
eventos = loaded;
loading = false;
});
} catch (e) {
print('Error loading my services: $e');
if (mounted) setState(() => loading = false);
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Mis servicios'),
drawer: DrawerProfessional(),
body: loading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
child: Column(children: [_eventList(context)]),
),
),
);
}
Widget _eventList(BuildContext context) {
final filtered =
eventos.where((e) => e.professionalId != e.userId).toList();
if (filtered.isEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 50),
child: Center(child: Text('No tienes citas')),
);
}
return FutureBuilder<Map<String, String>>(
future: _loadProfessionalNames(filtered),
builder: (context, snapshot) {
final names = snapshot.data ?? {};
return Column(
children: [
...filtered.map((event) => ListTile(
tileColor: event.status == 'cancelled' || event.status == 'denied'
? Colors.red[100]
: Colors.blue[100],
onTap: () {
if (event.status == 'completed') {
if (event.userId == uid) {
if (event.userScored) {
Navigator.push(
context,
CupertinoPageRoute(
builder: (_) => CitaScreen(evento: event),
),
);
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (_) =>
ScoreScreen(evento: event, pro: false),
),
);
}
}
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (_) => CitaScreen(evento: event),
),
);
}
},
leading: _statusIcon(event.status),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
names[event.professionalId] ?? 'N/D',
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)],
),
)),
],
);
},
);
}
Future<Map<String, String>> _loadProfessionalNames(
List<Event> events) async {
final ids = events.map((e) => e.professionalId).toSet();
final Map<String, String> names = {};
for (final id in ids) {
try {
final Map<String, dynamic> data =
await ApiService.instance.get('/users/$id');
names[id] = data['name'] ?? 'N/D';
} catch (_) {
names[id] = 'N/D';
}
}
return names;
}
Widget _statusIcon(String status) {
switch (status) {
case 'accepted':
return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.check, color: Colors.blue, size: 30),
Text('Aceptado', style: TextStyle(fontSize: 12)),
]);
case 'active':
return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.access_time, color: Colors.blue, size: 30),
Text('Iniciado', style: TextStyle(fontSize: 12)),
]);
case 'pending':
return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.access_time_outlined, color: Colors.blue, size: 30),
Text('Pendiente', style: TextStyle(fontSize: 12)),
]);
case 'completed':
return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.rocket_launch, color: Colors.blue, size: 30),
Text('Finalizado', style: TextStyle(fontSize: 12)),
]);
default:
return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.close, color: Colors.red, size: 30),
Text('Cancelado', style: TextStyle(fontSize: 12)),
]);
}
}
}
@@ -1,204 +0,0 @@
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';
import 'package:prosappco/src/presentation/screens/score.dart';
import 'package:prosappco/src/services/api_service.dart';
class MyServicesProScreen extends StatefulWidget {
MyServicesProScreen({super.key});
@override
State<MyServicesProScreen> createState() => _MyServicesProScreenState();
}
class _MyServicesProScreenState extends State<MyServicesProScreen> {
DateTime today = DateTime.now();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
List<Event> eventos = [];
bool loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final raw = await ApiService.instance.get('/services/professional');
final List<dynamic> data = raw is Map ? (raw['data'] ?? []) : (raw as List);
final List<Event> loaded = [];
for (final e in data) {
final event = Event.fromJson(e as Map<String, dynamic>);
event.scoresModel =
await ScoresModel.scoreTo(event.userId, false, false);
loaded.add(event);
}
loaded.sort((a, b) {
if (a.timeStamp == null || b.timeStamp == null) return 0;
return a.timeStamp!.compareTo(b.timeStamp!);
});
if (mounted) setState(() {
eventos = loaded;
loading = false;
});
} catch (e) {
print('Error loading pro services: $e');
if (mounted) setState(() => loading = false);
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () {
Navigator.pop(context);
},
label: 'Mis servicios',
),
drawer: DrawerProfessional(),
body: loading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
child: Column(children: [_eventList(context)]),
),
),
);
}
Widget _eventList(BuildContext context) {
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 == 'cancelled' || event.status == 'denied'
? Colors.red[100]
: Colors.blue[100],
onTap: () {
if (event.status == 'completed') {
if (event.professionalId == uid) {
if (event.professionalScored) {
Navigator.push(
context,
CupertinoPageRoute(
builder: (_) => CitaScreen(evento: event),
),
);
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (_) => ScoreScreen(evento: event, pro: true),
),
);
}
}
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (_) => CitaScreen(evento: event),
),
);
}
},
leading: _statusIcon(event.status),
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)],
),
),
),
],
);
}
Widget _statusIcon(String status) {
switch (status) {
case 'accepted':
return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.check, color: Colors.blue, size: 30),
Text('Aceptado', style: TextStyle(fontSize: 12)),
]);
case 'active':
return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.access_time, color: Colors.blue, size: 30),
Text('Iniciado', style: TextStyle(fontSize: 12)),
]);
case 'completed':
return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.rocket_launch, color: Colors.blue, size: 30),
Text('Finalizado', style: TextStyle(fontSize: 12)),
]);
default:
return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.close, color: Colors.red, size: 30),
Text('Cancelado', style: TextStyle(fontSize: 12)),
]);
}
}
}
@@ -1,261 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.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();
}
class _NewNumberScreenState extends State<NewNumberScreen> {
final controller = NewPhoneController.instance;
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: 'Actualizar número'),
),
),
],
),
)
: 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: 'Actualizar número'),
),
),
],
),
),
),
),
);
}
}
@@ -1,153 +0,0 @@
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: 30.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,
),
),
)),
),
]),
),
],
),
));
}
}
@@ -1,109 +0,0 @@
import 'package:flutter/material.dart';
import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/utils/app_navigator.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;
void updatePassword(String currentPassword, String newPassword) {
// ponytail: password change requires backend endpoint; show support message
showAppSnackBar('Cambio de contraseña', 'Para cambiar tu contraseña contacta a soporte.', color: Colors.grey.shade700);
}
@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),
),
],
),
),
),
),
),
);
}
}
@@ -1,198 +0,0 @@
import 'package:diacritic/diacritic.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/services/api_service.dart';
Future<List<String>> getProfessions() async {
try {
final List<dynamic> data = await ApiService.instance.get('/professions');
return data.map((e) => (e['name'] ?? e.toString()) as String).toList();
} 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();
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 ApiService.instance.patch('/users/me', {'profession': profession});
} catch (e) {
print('Error al actualizar la profesion: $e');
}
}
Future<void> saveProfession(String newProfession) async {
try {
// Add profession via API (POST /professions or use existing endpoint)
// For now just add it locally to the list
setState(() {
_professions ??= [];
_professions!.add(newProfession);
filteredProfessions = List.from(_professions!);
isNewProfessionAdded = true;
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
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]);
},
);
},
),
),
],
),
),
);
}
}
@@ -1,349 +0,0 @@
import 'package:diacritic/diacritic.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/professional_model.dart';
import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/presentation/screens/calendar_pro.dart';
import 'package:prosappco/src/presentation/screens/professional_info.dart';
import 'package:prosappco/src/presentation/widgets/shared/loading_item_list.dart';
import 'package:prosappco/src/services/api_service.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();
}
class _ProfessionalScreenState extends State<ProfessionalScreen> {
SettingModel? settings;
List<Professional>? filteredProfessionals;
List<Professional>? _professionals;
TextEditingController searchController = TextEditingController();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
String formatCurrency(int number) {
final formatter =
NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: '');
return '\$${formatter.format(number)}';
}
@override
void initState() {
super.initState();
SettingModel.getSettings().then((s) {
if (mounted) setState(() => settings = s);
});
searchController.addListener(() {
setState(() {
if (_professionals != null) {
if (searchController.text.isEmpty) {
filteredProfessionals =
_professionals!.where((p) => p.id != uid).toList();
} else {
filteredProfessionals = _professionals!
.where((p) =>
removeDiacritics(p.name)
.toLowerCase()
.contains(removeDiacritics(
searchController.text.toLowerCase())) &&
p.id != uid)
.toList();
}
}
});
});
_loadProfessionals();
}
Future<void> _loadProfessionals() async {
try {
final String query = widget.profession.isNotEmpty
? '/professionals?profession=${Uri.encodeComponent(widget.profession)}'
: '/professionals';
final raw = await ApiService.instance.get(query);
final List<dynamic> data = raw is Map ? (raw['data'] ?? []) : (raw as List);
final List<Professional> loaded = [];
for (final e in data) {
final json = e as Map<String, dynamic>;
if (json['id'] == uid) continue;
final List<String> especializaciones =
((json['especialidades'] ?? json['specialties'] ?? []) as List)
.map((x) => x.toString())
.toList();
final scores =
await ScoresModel.scoreTo(json['id']?.toString(), true, false);
loaded.add(Professional(
id: json['id']?.toString() ?? '',
photoUrl: json['picture'] ?? json['photo_url'],
name: json['name'] ?? '',
professionName: json['profession'] ?? json['profesion'] ?? '',
cityName: json['city'] ?? '',
professionalEspecializado: especializaciones,
ubicacion: json['ubicacion'] ?? '',
realAddress: json['address'] ?? '',
latitude: (json['latitude'] ?? 0).toDouble(),
longitude: (json['longitude'] ?? 0).toDouble(),
scores: scores,
tarifa: json['rate'] != null
? double.tryParse(json['rate'].toString())?.toInt()
: json['tarifas'],
token: json['token'],
));
}
if (mounted) {
setState(() {
_professionals = loaded;
filteredProfessionals = loaded.where((p) => p.id != uid).toList();
});
}
} catch (e) {
print('Error loading professionals: $e');
if (mounted) setState(() => filteredProfessionals = []);
}
}
Future<String?> _showChoiceDialog(BuildContext context) async {
return showDialog<String>(
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"),
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
if (filteredProfessionals == null) {
return SafeArea(
child: Scaffold(
appBar: PopAppbar(
onPressed: () => Navigator.pop(context),
label: 'Seleccione un profesional',
),
body: Column(
children: [
const Padding(
padding: EdgeInsets.all(10),
child: TextField(
readOnly: true,
decoration: InputDecoration(
hintText: 'Escriba un nombre',
prefixIcon: Icon(Icons.assignment_ind_rounded),
),
),
),
Expanded(
child: ListView.builder(
itemCount: 8,
itemBuilder: (BuildContext context, int index) {
return const LoadingItemList(useCircleAvatar: true);
},
),
),
],
),
),
);
}
final 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),
),
),
),
professionals.isEmpty
? Expanded(
child: Padding(
padding:
const EdgeInsets.only(left: 20, right: 20, top: 50),
child:
Text('Aún no tenemos ningun(a) ${widget.profession}'),
))
: Expanded(
child: ListView.builder(
itemCount: professionals.length,
itemBuilder: (BuildContext context, int index) {
final pro = professionals[index];
return ListTile(
leading: GestureDetector(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(
builder: (_) =>
ProfessionalInfoScreen(professional: pro),
));
},
child: ReferencePhoto(
ref: pro.photoUrl,
sizeCircle: 50,
size: 50,
sizeIcon: 35,
),
),
trailing: GestureDetector(
child: const Icon(Icons.keyboard_arrow_right),
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(
builder: (_) =>
ProfessionalInfoScreen(professional: pro),
));
},
),
title: RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 15.0, color: Colors.black),
children: <TextSpan>[
TextSpan(
text: '${pro.name}, ',
style: const TextStyle(
fontWeight: FontWeight.bold),
),
TextSpan(
text:
"${pro.professionName}, ${pro.cityName}",
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
settings?.tarifas == true
? pro.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(pro.tarifa ?? 0),
style: TextStyle(
color: Colors.grey[850],
fontWeight: FontWeight.w600,
),
),
)
: const SizedBox(),
pro.ubicacion == 'ambos' &&
settings?.domicilios == true
? const Text(
'Disponibilidad a domicilio y en sitio',
style: TextStyle(color: Colors.blue),
)
: pro.ubicacion == 'sitio' ||
settings?.domicilios == false
? const Text(
'Disponibilidad en sitio',
style: TextStyle(color: Colors.blue),
)
: const Text(
'Disponibilidad a domicilio',
style: TextStyle(color: Colors.blue),
),
],
),
onTap: () async {
if (pro.ubicacion == 'ambos' &&
settings?.domicilios == true) {
_showChoiceDialog(context)
.then((String? value) async {
if (value != null) {
var datos = await Navigator.of(context).push(
CupertinoPageRoute(
builder: (_) =>
CalendarProScreen(professional: pro),
),
);
if (datos != null) {
datos.add(value);
Navigator.pop(context, datos);
}
}
});
} else if (pro.ubicacion == 'sitio' ||
settings?.domicilios == false) {
var datos = await Navigator.of(context).push(
CupertinoPageRoute(
builder: (_) =>
CalendarProScreen(professional: pro),
),
);
if (datos != null) {
datos.add('sitio');
Navigator.pop(context, datos);
}
} else {
Navigator.pop(context, [pro, 'domicilio']);
}
},
);
},
),
),
],
),
),
);
}
}
@@ -1,241 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:prosappco/src/services/api_service.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:prosappco/src/utils/app_navigator.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 ApiService.instance.patch('/users/me', {
'address': addressName,
'latitude': latitude,
'longitude': longitude,
});
} catch (e) {
print('Error al actualizar la dirección: $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) {
showAppSnackBar('Ubicación desactivada', 'Por favor activa la ubicación de tu teléfono.');
}
// 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'),
),
),
)
],
),
),
);
}
}
@@ -1,359 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:prosappco/src/services/api_service.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/professional_model.dart';
import 'package:prosappco/src/models/setting_model.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;
}),
);
}
loadPaymentMethods();
}
Map<String, bool> paymentMethods = {};
String _formatPaymentMethods(Map<String, bool> paymentMethods) {
List<String> enabledMethods = paymentMethods.entries
.where((entry) => entry.value)
.map((entry) => entry.key)
.toList();
return enabledMethods.join(', ');
}
void loadPaymentMethods() {
ApiService.instance.get('/professionals/${widget.professional.id}').then((data) {
if (data is Map) {
setState(() {
paymentMethods = Map<String, bool>.from(data['paymentMethods'] ?? {});
});
}
}).catchError((e) {
print('Error loading paymentMethods: $e');
});
}
@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: 5),
paymentMethods.containsValue(true)
? Container(
width: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 12,
color: Colors.blue,
),
children: [
const TextSpan(
text: 'Métodos de pago recibidos: ',
style:
TextStyle(fontWeight: FontWeight.normal),
),
TextSpan(
text: _formatPaymentMethods(paymentMethods),
style: const TextStyle(
fontWeight: FontWeight.bold),
),
],
),
))
: const SizedBox(),
const SizedBox(height: 5),
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.photoUrl,
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),
),
],
),
),
),
],
));
}
}
@@ -1,586 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
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/presentation/widgets/shared/warning_snackbar.dart';
import 'package:prosappco/src/services/api_service.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;
final uid = AuthenticationRepository.instance.getCurrentUserUid();
final _formKey = GlobalKey<FormState>();
final controller = InforProfessionalController.instance;
final _cedulaController = TextEditingController();
final _especializacionController = TextEditingController();
List<File> images_especializacion = [];
var _profession = '...';
String? _photoUrl;
@override
void initState() {
super.initState();
final currentUser = AuthenticationRepository.instance.currentUser;
_photoUrl = currentUser?.picture;
_profession = currentUser?.profession ?? '...';
if (_photoUrl == null || _profession == '...') {
_loadUserData();
}
}
Future<void> _loadUserData() async {
try {
final Map<String, dynamic> data =
await ApiService.instance.get('/auth/me');
if (mounted) {
setState(() {
_photoUrl ??= data['picture'];
if (_profession == '...') {
_profession = data['profession'] ?? data['profesion'] ?? '...';
}
});
}
} catch (e) {
print('Error loading user data: $e');
}
}
Future<File?> getPdf() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf'],
);
if (result != null) {
return File(result.files.single.path!);
}
return null;
}
Future<List<File>?> getPdfs() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf'],
allowMultiple: true,
);
if (result != null) {
return result.files.map((f) => File(f.path!)).toList();
}
return null;
}
Future<String?> _uploadFile(File file) 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', file.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 file: $e');
}
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();
if (imagen != null) {
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();
if (imagen != null) {
setState(() => image_certificado = File(imagen.path));
}
Navigator.of(context).pop();
},
),
],
),
),
);
},
);
}
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<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) {
WarningSnackbar.show(
title: 'Te faltan campos!!',
message: 'Por favor, ingresa tu cedula',
);
return;
}
if (image_cedula == null) {
WarningSnackbar.show(
title: 'Te faltan archivos!!',
message: 'Por favor, adjunta el documento PDF de tu cedula',
);
return;
}
if (_profession.isEmpty || _profession == '...') {
WarningSnackbar.show(
title: 'Te falta elegir una profesión!!',
message:
'Por favor, elige tu profesión antes de enviar la información.',
);
return;
}
if (image_certificado == null) {
WarningSnackbar.show(
title: 'Te faltan archivos!!',
message: 'Por favor, adjunta el documento PDF de tu certificado',
);
return;
}
if (imagen_to_upload == null && (_photoUrl == null || _photoUrl!.isEmpty)) {
WarningSnackbar.show(
title: 'Sube una foto de perfil',
message: 'Para continuar debes subir una imagen de perfil',
);
return;
}
final Map<String, dynamic> body = {
'cedula': cedula,
'estado': 'revision',
'especializaciones': especializaciones,
};
// Upload profile photo if changed
if (imagen_to_upload != null) {
final url = await _uploadFile(imagen_to_upload!);
if (url != null) body['picture'] = url;
}
// Upload cedula PDF
final cedulaUrl = await _uploadFile(image_cedula!);
if (cedulaUrl != null) body['imgCedula'] = cedulaUrl;
// Upload certificado PDF
final certUrl = await _uploadFile(image_certificado!);
if (certUrl != null) body['imgCertificado'] = certUrl;
// Upload especializaciones PDFs
if (images_especializacion.isNotEmpty) {
final List<String> espUrls = [];
for (final f in images_especializacion) {
final url = await _uploadFile(f);
if (url != null) espUrls.add(url);
}
if (espUrls.isNotEmpty) body['imgEspecializaciones'] = espUrls;
}
try {
await ApiService.instance.patch('/users/me', body);
Navigator.pushReplacementNamed(context, '/solicitudEnviada');
} catch (e) {
print('Error sending professional info: $e');
}
}
@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: _photoUrl,
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: () =>
_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,
),
],
),
),
),
],
),
),
),
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More