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
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user