fix: port 7 web features and repair the endless-loading screens
Root cause behind most "stuck loading" reports: the backend changed shape (schedules became an array, location_preferences a string) while the mobile parser still hard-cast to Map/int. The TypeError was swallowed by a silent catch that returned null, and screens only handled the success state, so a parse failure rendered as a permanent spinner. Same class of bug appeared across service lists via non-null map lookups and a total absence of request timeouts. Ported from prosappweb: - in-app suggestions (POST /suggestions) - policies/terms from GET /settings/policies - configurable appointment length (slot_duration_minutes) - block/unblock calendar slots (POST /services/block) - GPS city detection on the profile (Nominatim) - server-side professional search with haversine distance - retry cooldown after a rejected professional application Reliability: - parse schedules array (day_of_week 0=Mon) and string location_preferences - read times as wall clock, so 08:00 stays 08:00 across timezones - carry minutes into hours in TimeOfDay.add; a minute-based step used to loop forever and freeze the calendar (covered by test/time_slots_test.dart) - semver update check instead of string equality, which blocked every build that did not exactly match the configured version - request timeouts across all repositories - surface HTTP >= 400 instead of reporting failed writes as success - error states with retry instead of an indefinite shimmer Includes pre-existing uncommitted work from the UI redesign. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
06a89df690
commit
8631e6f729
+336
-308
@@ -12,7 +12,6 @@ import 'package:prosappco/components/general_drawer_header.dart';
|
||||
import 'package:prosappco/components/general_drawer_item.dart';
|
||||
import 'package:prosappco/screens/configuration/configuration_screen.dart';
|
||||
import 'package:prosappco/screens/configuration/configuration_support_screen.dart';
|
||||
import 'package:prosappco/screens/lists/dropwdon.dart';
|
||||
import 'package:prosappco/screens/lists/professional_score_list_screen.dart';
|
||||
import 'package:prosappco/screens/lists/professional_service_history_list_screen.dart';
|
||||
import 'package:prosappco/screens/lists/professional_service_list_screen.dart';
|
||||
@@ -25,23 +24,22 @@ import 'package:prosappco/screens/professional/professional_form_screen.dart';
|
||||
import 'package:prosappco/screens/professional/professional_pending_screen.dart';
|
||||
import 'package:prosappco/screens/professional/professional_profile_screen.dart';
|
||||
import 'package:prosappco/screens/profile/profile_screen.dart';
|
||||
import 'package:prosappco/screens/web/web_view_screen.dart';
|
||||
import 'package:prosappco/screens/suggestions/suggestion_screen.dart';
|
||||
import 'package:service_repository/service_repository.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:user_repository/user_repository.dart';
|
||||
|
||||
const _kPro = Color(0xFF0D9488);
|
||||
const _kUser = Color(0xFF1565C0);
|
||||
|
||||
class GeneralDrawer extends StatelessWidget {
|
||||
const GeneralDrawer({super.key});
|
||||
|
||||
Future<void> _irSugerencias() async {
|
||||
const url = 'https://admin.prosapp.co/sugerencias';
|
||||
|
||||
final Uri _url = Uri.parse(url);
|
||||
|
||||
if (await canLaunchUrl(_url)) {
|
||||
await launchUrl(_url);
|
||||
} else {
|
||||
throw 'No se pudo abrir la URL $url';
|
||||
final Uri uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +51,9 @@ class GeneralDrawer extends StatelessWidget {
|
||||
builder: (context, userState) {
|
||||
return BlocBuilder<ProfessionalBloc, ProfessionalState>(
|
||||
builder: (context, professionalState) {
|
||||
final isProMode = _isProModeActive(professionalState);
|
||||
return Drawer(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
@@ -71,111 +70,100 @@ class GeneralDrawer extends StatelessWidget {
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
shouldProModeActive(context, professionalState)
|
||||
? GeneralDrawerItem(
|
||||
leading: Icons.person_outline_rounded,
|
||||
label: 'Perfil profesional',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
const ProfessionalProfileScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: const SizedBox(),
|
||||
if (isProMode)
|
||||
GeneralDrawerItem(
|
||||
leading: Icons.person_outline_rounded,
|
||||
label: 'Perfil profesional',
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) =>
|
||||
const ProfessionalProfileScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
GeneralDrawerItem(
|
||||
leading: Icons.checklist_outlined,
|
||||
label: 'Mis servicios',
|
||||
onTap: () {
|
||||
shouldProModeActive(context, professionalState)
|
||||
? Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
const ProfessionalServiceListScreen(),
|
||||
// const UserServicesScreen(),
|
||||
),
|
||||
)
|
||||
: Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
const UserServiceListScreen(),
|
||||
// const UserServicesScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => isProMode
|
||||
? Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) =>
|
||||
const ProfessionalServiceListScreen()),
|
||||
)
|
||||
: Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) =>
|
||||
const UserServiceListScreen()),
|
||||
),
|
||||
),
|
||||
GeneralDrawerItem(
|
||||
leading: Icons.access_time_outlined,
|
||||
label: 'Historial',
|
||||
onTap: () {
|
||||
shouldProModeActive(context, professionalState)
|
||||
? Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
const ProfessionalServiceHistoryListScreen(),
|
||||
),
|
||||
)
|
||||
: Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
const UserServiceHistoryListScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => isProMode
|
||||
? Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) =>
|
||||
const ProfessionalServiceHistoryListScreen()),
|
||||
)
|
||||
: Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) =>
|
||||
const UserServiceHistoryListScreen()),
|
||||
),
|
||||
),
|
||||
shouldProModeActive(context, professionalState)
|
||||
? GeneralDrawerItem(
|
||||
onTap: () {
|
||||
if (professionalState
|
||||
is LoadedModeProState) {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
ProfessionalCalendarScreen(
|
||||
userProfessional:
|
||||
professionalState.proInfo!,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
label: 'Calendario',
|
||||
leading: Icons.calendar_month_outlined,
|
||||
)
|
||||
: const SizedBox(),
|
||||
if (isProMode)
|
||||
GeneralDrawerItem(
|
||||
leading: Icons.calendar_month_outlined,
|
||||
label: 'Calendario',
|
||||
onTap: () {
|
||||
if (professionalState
|
||||
is LoadedModeProState &&
|
||||
professionalState.proInfo != null) {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) =>
|
||||
ProfessionalCalendarScreen(
|
||||
userProfessional:
|
||||
professionalState.proInfo!,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context)
|
||||
.clearSnackBars();
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
'Cargando tu información de profesional, intenta de nuevo en un momento'),
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
GeneralDrawerItem(
|
||||
leading: Icons.settings_outlined,
|
||||
label: 'Configuración',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
const ConfigurationScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) =>
|
||||
const ConfigurationScreen()),
|
||||
),
|
||||
),
|
||||
GeneralDrawerItem(
|
||||
leading: Icons.help_outline,
|
||||
label: 'Soporte',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
const ConfigurationSupportScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) =>
|
||||
const ConfigurationSupportScreen()),
|
||||
),
|
||||
),
|
||||
GeneralDrawerItem(
|
||||
leading: Icons.campaign_outlined,
|
||||
@@ -187,137 +175,45 @@ class GeneralDrawer extends StatelessWidget {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WebViewScreen(
|
||||
label: 'Sugerencias',
|
||||
link:
|
||||
'https://admin.prosapp.co/sugerencias');
|
||||
},
|
||||
builder: (_) => const SuggestionScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
Container(
|
||||
color: const Color(0xFF2BA4EC),
|
||||
child: ListTile(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
trailing: shouldProModeActive(context, professionalState)
|
||||
? FutureBuilder(builder: (context, AsyncSnapshot<int> snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data! < 1) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final int notificationCount = snapshot.data!;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
notificationCount > 9
|
||||
? '+9'
|
||||
: notificationCount.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
future: Injector.appInstance
|
||||
.get<ApiServiceRepository>()
|
||||
.countPendingServicesForProfessional(
|
||||
ApiUserRepository.currentUserId ?? ''),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
color: Colors.white,
|
||||
size: 25,
|
||||
),
|
||||
title: Text(
|
||||
shouldProModeActive(context, professionalState)
|
||||
? 'Solicitudes'
|
||||
: 'Solicitar servicio',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
// CTA item — go to main action
|
||||
_CtaItem(
|
||||
isProMode: isProMode,
|
||||
professionalState: professionalState,
|
||||
),
|
||||
// Rating row
|
||||
DrawerReputation(builder: (reputation) {
|
||||
final isProModeActive =
|
||||
(professionalState is LoadedModeProState) &&
|
||||
professionalState.isProModeActive;
|
||||
|
||||
final total = isProModeActive
|
||||
final total = isProMode
|
||||
? reputation.totalPro
|
||||
: reputation.total;
|
||||
|
||||
final average = isProModeActive
|
||||
final average = isProMode
|
||||
? reputation.averagePro
|
||||
: reputation.average;
|
||||
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
final isProModeActive = (professionalState is LoadedModeProState) && professionalState.isProModeActive;
|
||||
isProModeActive ? Navigator.push(context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) {
|
||||
return const ProfessionalScoreListScreen();
|
||||
}))
|
||||
: Navigator.push(context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) {
|
||||
return const UserScoreListScreen();
|
||||
}));
|
||||
},
|
||||
trailing: const Icon(Icons.keyboard_arrow_right,
|
||||
color: Colors.black),
|
||||
title: Row(
|
||||
children: [
|
||||
RatingBar.builder(
|
||||
initialRating: calculoRating(average),
|
||||
minRating: 1,
|
||||
direction: Axis.horizontal,
|
||||
allowHalfRating: true,
|
||||
itemCount: 5,
|
||||
itemSize: 25,
|
||||
maxRating: 5,
|
||||
itemBuilder: (context, _) => const Icon(
|
||||
Icons.star,
|
||||
color: Color(0xFF2BA4EC),
|
||||
),
|
||||
onRatingUpdate: (rating) {},
|
||||
ignoreGestures: true,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
'${average.toStringAsFixed(1)} (${total.toString()})',
|
||||
),
|
||||
],
|
||||
),
|
||||
return _RatingRow(
|
||||
average: average,
|
||||
total: total,
|
||||
isProMode: isProMode,
|
||||
professionalState: professionalState,
|
||||
);
|
||||
}),
|
||||
const Divider(
|
||||
height: 1,
|
||||
thickness: 0.5,
|
||||
),
|
||||
const Divider(height: 1, thickness: 0.5),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 4),
|
||||
child: Text(
|
||||
'Prosapp ® todos los derechos reservados',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[700],
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -336,7 +232,11 @@ class GeneralDrawer extends StatelessWidget {
|
||||
const SizedBox(height: 15),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: buttonOfState(context, professionalState),
|
||||
child: _ModeButton(
|
||||
professionalState: professionalState,
|
||||
userState: userState,
|
||||
isProMode: isProMode,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
],
|
||||
@@ -349,145 +249,273 @@ class GeneralDrawer extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
double calculoRating(double average) {
|
||||
String numeroString = average.toString();
|
||||
List<String> partes = numeroString.split('.');
|
||||
int parteEntera = int.parse(partes[0]);
|
||||
int parteFraccionaria = partes.length > 1 ? int.parse(partes[1]) : 0;
|
||||
bool _isProModeActive(ProfessionalState state) =>
|
||||
(state is LoadedModeProState) ? state.isProModeActive : false;
|
||||
}
|
||||
|
||||
if (parteFraccionaria >= 3) {
|
||||
parteFraccionaria = 5;
|
||||
} else {
|
||||
parteFraccionaria = 0;
|
||||
}
|
||||
// CTA tile navigating to solicitudes / solicitar servicio
|
||||
class _CtaItem extends StatelessWidget {
|
||||
final bool isProMode;
|
||||
final ProfessionalState professionalState;
|
||||
_CtaItem(
|
||||
{required this.isProMode, required this.professionalState});
|
||||
|
||||
// Unir la parte entera y fraccionaria y convertirlo nuevamente a double
|
||||
double resultado = double.parse('$parteEntera.$parteFraccionaria');
|
||||
return resultado;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isProMode
|
||||
? _kPro.withOpacity(0.1)
|
||||
: _kUser.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isProMode
|
||||
? _kPro.withOpacity(0.25)
|
||||
: _kUser.withOpacity(0.25)),
|
||||
),
|
||||
child: ListTile(
|
||||
onTap: () => Navigator.pop(context),
|
||||
leading: Icon(
|
||||
isProMode ? Icons.inbox_outlined : Icons.search_rounded,
|
||||
color: isProMode ? _kPro : _kUser,
|
||||
),
|
||||
title: Text(
|
||||
isProMode ? 'Solicitudes' : 'Solicitar servicio',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isProMode ? _kPro : _kUser,
|
||||
),
|
||||
),
|
||||
trailing: isProMode
|
||||
? FutureBuilder<int>(
|
||||
future: Injector.appInstance
|
||||
.get<ApiServiceRepository>()
|
||||
.countPendingServicesForProfessional(
|
||||
ApiUserRepository.currentUserId ?? ''),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data! < 1) {
|
||||
return Icon(Icons.keyboard_arrow_right,
|
||||
color: _kPro.withOpacity(0.5));
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
snapshot.data! > 9
|
||||
? '+9'
|
||||
: snapshot.data!.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Icon(Icons.keyboard_arrow_right,
|
||||
color: _kUser.withOpacity(0.5)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RatingRow extends StatelessWidget {
|
||||
final double average;
|
||||
final int total;
|
||||
final bool isProMode;
|
||||
final ProfessionalState professionalState;
|
||||
_RatingRow(
|
||||
{required this.average,
|
||||
required this.total,
|
||||
required this.isProMode,
|
||||
required this.professionalState});
|
||||
|
||||
double _calcRating(double avg) {
|
||||
final parts = avg.toString().split('.');
|
||||
final frac = parts.length > 1 ? int.parse(parts[1]) : 0;
|
||||
return double.parse(
|
||||
'${parts[0]}.${frac >= 3 ? 5 : 0}');
|
||||
}
|
||||
|
||||
buttonOfState(BuildContext context, ProfessionalState state) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
isProMode
|
||||
? Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => const ProfessionalScoreListScreen()))
|
||||
: Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => const UserScoreListScreen()));
|
||||
},
|
||||
leading: Icon(Icons.star_outline_rounded,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6)),
|
||||
trailing: Icon(Icons.keyboard_arrow_right,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.4)),
|
||||
title: Row(
|
||||
children: [
|
||||
RatingBar.builder(
|
||||
initialRating: _calcRating(average),
|
||||
minRating: 1,
|
||||
direction: Axis.horizontal,
|
||||
allowHalfRating: true,
|
||||
itemCount: 5,
|
||||
itemSize: 20,
|
||||
maxRating: 5,
|
||||
itemBuilder: (_, __) => const Icon(Icons.star,
|
||||
color: Color(0xFF1565C0)),
|
||||
onRatingUpdate: (_) {},
|
||||
ignoreGestures: true,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'${average.toStringAsFixed(1)} ($total)',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.7)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ModeButton extends StatelessWidget {
|
||||
final ProfessionalState professionalState;
|
||||
final MyUserState userState;
|
||||
final bool isProMode;
|
||||
|
||||
_ModeButton({
|
||||
required this.professionalState,
|
||||
required this.userState,
|
||||
required this.isProMode,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
child: ElevatedButton(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
final myUserState = context.read<MyUserBloc>().state;
|
||||
if (myUserState.status == MyUserStatus.success) {
|
||||
final user = myUserState.user!;
|
||||
|
||||
if (user.name != null && user.email != null && user.city != null && user.phone != null) {
|
||||
if (userState.status == MyUserStatus.success) {
|
||||
final user = userState.user!;
|
||||
if (user.name != null &&
|
||||
user.city != null &&
|
||||
user.phone != null) {
|
||||
switch (user.proState) {
|
||||
case ProState.active: Navigator.pop(context);
|
||||
|
||||
context.read<ProfessionalBloc>().add(const SwitchProModeEvent());
|
||||
|
||||
case ProState.active:
|
||||
Navigator.pop(context);
|
||||
context
|
||||
.read<ProfessionalBloc>()
|
||||
.add(const SwitchProModeEvent());
|
||||
break;
|
||||
case ProState.inactive:
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => const ProfessionalFormScreen(),
|
||||
),
|
||||
builder: (_) => const ProfessionalFormScreen()),
|
||||
);
|
||||
break;
|
||||
case ProState.pending:
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => const ProfessionalPendingScreen(),
|
||||
),
|
||||
builder: (_) =>
|
||||
const ProfessionalPendingScreen()),
|
||||
);
|
||||
break;
|
||||
case ProState.denied:
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => const ProfessionalDeniedScreen(),
|
||||
),
|
||||
builder: (_) =>
|
||||
const ProfessionalDeniedScreen()),
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
final missing = <String>[
|
||||
if (user.name == null) 'nombre',
|
||||
if (user.city == null) 'ciudad',
|
||||
if (user.phone == null) 'teléfono',
|
||||
];
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('Por favor, completa tu perfil'),
|
||||
));
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Completa tu perfil: falta ${missing.join(', ')}')),
|
||||
);
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => const ProfileScreen(),
|
||||
),
|
||||
builder: (_) => const ProfileScreen()),
|
||||
);
|
||||
}
|
||||
} else {}
|
||||
}
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
icon: const Icon(Icons.swap_horiz, size: 20),
|
||||
label: Text(
|
||||
isProMode ? 'Cambiar a modo cliente' : 'Cambiar a modo profesional',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
isProMode ? _kPro : _kUser,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: (state is LoadedModeProState)
|
||||
? Text(
|
||||
state.isProModeActive ? 'Modo cliente' : 'Modo profesional',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 18),
|
||||
)
|
||||
: const Text(
|
||||
'Modo profesional',
|
||||
style: TextStyle(color: Colors.white, fontSize: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
(state is LoadedModeProState)
|
||||
? Visibility(
|
||||
visible: !state.isProModeActive,
|
||||
child: Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: FutureBuilder(
|
||||
builder: (context, AsyncSnapshot<int> snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data! < 1) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final int notificationCount = snapshot.data!;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
notificationCount > 9
|
||||
? '+9'
|
||||
: notificationCount.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
future: Injector.appInstance
|
||||
.get<ApiServiceRepository>()
|
||||
.countPendingServicesForProfessional(
|
||||
ApiUserRepository.currentUserId ?? ''),
|
||||
if (!isProMode && professionalState is LoadedModeProState)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: FutureBuilder<int>(
|
||||
future: Injector.appInstance
|
||||
.get<ApiServiceRepository>()
|
||||
.countPendingServicesForProfessional(
|
||||
ApiUserRepository.currentUserId ?? ''),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data! < 1) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
child: Text(
|
||||
snapshot.data! > 9 ? '+9' : snapshot.data!.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
shouldProModeActive(BuildContext context, ProfessionalState state) {
|
||||
return (state is LoadedModeProState) ? state.isProModeActive : false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,98 +12,139 @@ class GeneralDrawerHeader extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<MyUserBloc, MyUserState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == MyUserStatus.success) {
|
||||
final user = state.user!;
|
||||
builder: (context, userState) {
|
||||
if (userState.status == MyUserStatus.success) {
|
||||
final user = userState.user!;
|
||||
return BlocBuilder<ProfessionalBloc, ProfessionalState>(
|
||||
builder: (context, state) {
|
||||
return ListTile(
|
||||
builder: (context, proState) {
|
||||
final isProMode = (proState is LoadedModeProState) &&
|
||||
proState.isProModeActive;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
shouldProModeActive(context, state)
|
||||
isProMode
|
||||
? Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const ProfessionalProfileScreen();
|
||||
},
|
||||
builder: (_) => const ProfessionalProfileScreen(),
|
||||
),
|
||||
)
|
||||
: Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const ProfileScreen();
|
||||
},
|
||||
builder: (_) => const ProfileScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
title: Text(user.name ?? '',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(user.drawerLabel,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
leading: pictureWidget(user.picture, context),
|
||||
trailing:
|
||||
const Icon(Icons.keyboard_arrow_right, color: Colors.black),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
20,
|
||||
MediaQuery.of(context).padding.top + 20,
|
||||
20,
|
||||
16),
|
||||
child: Row(
|
||||
children: [
|
||||
_avatar(user.picture),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
user.name ?? '',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
_modePill(isProMode),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (state.status == MyUserStatus.failure) {
|
||||
return const Text('Error obteniendo datos del usuario');
|
||||
} else if (userState.status == MyUserStatus.failure) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text('Error obteniendo datos del usuario'),
|
||||
);
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
shouldProModeActive(BuildContext context, ProfessionalState state) {
|
||||
return (state is LoadedModeProState) ? state.isProModeActive : false;
|
||||
Widget _modePill(bool isProMode) {
|
||||
final color =
|
||||
isProMode ? const Color(0xFF0D9488) : const Color(0xFF1565C0);
|
||||
final icon = isProMode ? Icons.badge_outlined : Icons.person_outline;
|
||||
final label = isProMode ? 'Profesional' : 'Cliente';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: Colors.white, size: 12),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget pictureWidget(String? pictureUrl, BuildContext context) {
|
||||
Widget _avatar(String? pictureUrl) {
|
||||
ImageProvider<Object>? imageProvider;
|
||||
|
||||
if (pictureUrl != null && pictureUrl.isNotEmpty) {
|
||||
imageProvider = NetworkImage(pictureUrl);
|
||||
}
|
||||
|
||||
return Hero(
|
||||
tag: 'picture-profile',
|
||||
child: pictureContainerWidget(
|
||||
imageProvider,
|
||||
child: Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: const Color(0xFF1565C0).withOpacity(0.3), width: 2),
|
||||
image: imageProvider == null
|
||||
? null
|
||||
: DecorationImage(image: imageProvider, fit: BoxFit.cover),
|
||||
),
|
||||
child: imageProvider == null
|
||||
? Icon(CupertinoIcons.person,
|
||||
color: Colors.grey.shade500, size: 28)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget pictureContainerWidget(ImageProvider<Object>? imageProvider) {
|
||||
final image = imageProvider == null
|
||||
? null
|
||||
: DecorationImage(
|
||||
image: imageProvider,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
|
||||
final widget = image == null
|
||||
? Icon(
|
||||
CupertinoIcons.person,
|
||||
color: Colors.grey.shade400,
|
||||
size: 40,
|
||||
)
|
||||
: null;
|
||||
|
||||
return Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
shape: BoxShape.circle,
|
||||
image: image,
|
||||
),
|
||||
child: widget,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,28 +20,23 @@ class GeneralDrawerItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final onSurface = Theme.of(context).colorScheme.onSurface;
|
||||
return ListTile(
|
||||
onTap: onTap == null ? null : () => onTap!(),
|
||||
leading: leading == null
|
||||
? null
|
||||
: Icon(
|
||||
leading,
|
||||
color: Colors.black,
|
||||
),
|
||||
: Icon(leading, color: color ?? onSurface.withOpacity(0.75)),
|
||||
trailing: trailing
|
||||
? const Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
color: Colors.black,
|
||||
)
|
||||
? Icon(Icons.keyboard_arrow_right, color: onSurface.withOpacity(0.4))
|
||||
: null,
|
||||
title: Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 15, color: color),
|
||||
style: TextStyle(fontSize: 15, color: color ?? onSurface),
|
||||
),
|
||||
subtitle: subtitle == null
|
||||
? null
|
||||
: Text(subtitle ?? "", style: const TextStyle(color: Colors.black54)),
|
||||
// dense: true,
|
||||
: Text(subtitle!,
|
||||
style: TextStyle(color: onSurface.withOpacity(0.55))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user