replace: swap prosappweb content for prosapp_web_app (more complete version)
prosapp_web_app has chat, dashboard, calendar, support, 13 providers and Fluro URL routing. Keep Dockerfile + nginx.conf from previous prosappweb. Upgrade google_fonts 6.2.1 → 8.1.0 (Dart 3.12 compat fix). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
74a4f41902
commit
15175c1b91
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
''';
|
||||
@@ -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
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}''';
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
}''';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}''';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}''';
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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
|
||||
}''';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}''';
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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
|
||||
}''';
|
||||
}
|
||||
}
|
||||
@@ -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}';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}''';
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}''';
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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
|
||||
}''';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}''';
|
||||
}
|
||||
}
|
||||
@@ -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)';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}''';
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}';
|
||||
}
|
||||
}
|
||||
@@ -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 }';
|
||||
}
|
||||
}
|
||||
@@ -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)';
|
||||
}
|
||||
@@ -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 ?? ''),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user