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>
134 lines
5.0 KiB
Dart
134 lines
5.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:injector/injector.dart';
|
|
import 'package:prosappco/blocs/suggestion_bloc/suggestion_bloc.dart';
|
|
|
|
const _kPrimary = Color(0xFF1565C0);
|
|
|
|
class SuggestionScreen extends StatefulWidget {
|
|
const SuggestionScreen({super.key});
|
|
|
|
@override
|
|
State<SuggestionScreen> createState() => _SuggestionScreenState();
|
|
}
|
|
|
|
class _SuggestionScreenState extends State<SuggestionScreen> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _messageController = TextEditingController();
|
|
bool _isLoading = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_messageController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocProvider<SuggestionBloc>(
|
|
create: (_) => Injector.appInstance.get<SuggestionBloc>(),
|
|
child: BlocListener<SuggestionBloc, SuggestionState>(
|
|
listener: (context, state) {
|
|
if (state is SuggestionLoading) {
|
|
setState(() => _isLoading = true);
|
|
} else if (state is SuggestionSuccess) {
|
|
setState(() => _isLoading = false);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('¡Gracias por tu sugerencia!')),
|
|
);
|
|
Navigator.of(context).pop();
|
|
} else if (state is SuggestionFailure) {
|
|
setState(() => _isLoading = false);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('No se pudo enviar la sugerencia'),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
child: Scaffold(
|
|
appBar: AppBar(title: const Text('Sugerencias')),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'¿Tienes una idea o algo que podamos mejorar? '
|
|
'Cuéntanos.',
|
|
style: TextStyle(fontSize: 14, color: Colors.grey),
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextFormField(
|
|
controller: _messageController,
|
|
minLines: 5,
|
|
maxLines: 8,
|
|
maxLength: 1000,
|
|
decoration: InputDecoration(
|
|
hintText: 'Escribe tu sugerencia aquí...',
|
|
alignLabelWithHint: true,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide:
|
|
const BorderSide(color: _kPrimary, width: 2),
|
|
),
|
|
),
|
|
validator: (v) => (v == null || v.trim().isEmpty)
|
|
? 'Escribe un mensaje'
|
|
: null,
|
|
),
|
|
const SizedBox(height: 24),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _kPrimary,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
onPressed: _isLoading
|
|
? null
|
|
: () {
|
|
if (_formKey.currentState?.validate() != true) {
|
|
return;
|
|
}
|
|
context.read<SuggestionBloc>().add(
|
|
SubmitSuggestion(
|
|
message: _messageController.text.trim(),
|
|
),
|
|
);
|
|
},
|
|
child: _isLoading
|
|
? const SizedBox(
|
|
height: 20,
|
|
width: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Colors.white,
|
|
),
|
|
)
|
|
: const Text('Enviar'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|