Rejection retry flow with configurable wait days

- Usuario model: proRejectedAt parsed from professionals.updated_at
- _StatusCard: shows countdown or retry button based on rejection_wait_days setting
- Retry clears the rejected record via DELETE /professionals/me

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-28 18:03:20 -05:00
co-authored by Claude Sonnet 4.6
parent 9634aa13ff
commit 391c32f765
2 changed files with 110 additions and 52 deletions
+15
View File
@@ -11,6 +11,7 @@ class Usuario {
final String? birthday;
final String? gender;
final ProState proState;
final DateTime? proRejectedAt;
final String? token;
final bool isPhoneVerified;
@@ -26,6 +27,7 @@ class Usuario {
required this.gender,
required this.proState,
required this.token,
this.proRejectedAt,
this.isPhoneVerified = false,
});
@@ -57,11 +59,24 @@ class Usuario {
birthday: doc['birthday'],
gender: doc['gender'],
proState: intToEnum((doc['professional_state'] as int?) ?? 0),
proRejectedAt: _parseRejectedAt(doc),
token: doc['token'],
isPhoneVerified: doc['is_phone_verified'] as bool? ?? false,
);
}
static DateTime? _parseRejectedAt(Map<String, dynamic> doc) {
// /auth/me returns professionals nested; if pro_state==3 use professionals.updated_at
final state = (doc['professional_state'] as int?) ?? 0;
if (state != 3) return null;
final pros = doc['professionals'];
if (pros is Map) {
final raw = pros['updated_at'];
if (raw is String) return DateTime.tryParse(raw);
}
return null;
}
@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)';
+95 -52
View File
@@ -611,7 +611,7 @@ class _UploadTile extends StatelessWidget {
}
}
class _StatusCard extends StatelessWidget {
class _StatusCard extends StatefulWidget {
final Color cardBg;
final Color border;
final Color textPrimary;
@@ -626,26 +626,62 @@ class _StatusCard extends StatelessWidget {
required this.isPending,
});
@override
State<_StatusCard> createState() => _StatusCardState();
}
class _StatusCardState extends State<_StatusCard> {
int _waitDays = 7;
bool _loadingSettings = true;
@override
void initState() {
super.initState();
if (!widget.isPending) _loadWaitDays();
}
Future<void> _loadWaitDays() async {
try {
final data = await ApiService.instance.get('/settings') as Map<String, dynamic>;
final v = data['rejection_wait_days'];
if (v != null) setState(() => _waitDays = (v as num).toInt());
} catch (_) {}
if (mounted) setState(() => _loadingSettings = false);
}
Future<void> _retry() async {
try {
await ApiService.instance.delete('/professionals/me');
} catch (_) {}
if (mounted) await context.read<AuthProvider>().isAuthenticated();
}
@override
Widget build(BuildContext context) {
final accent = isPending
final accent = widget.isPending
? const Color(0xFFF59E0B)
: const Color(0xFFEF4444);
final user = context.watch<AuthProvider>().user;
final rejectedAt = user?.proRejectedAt;
final daysSince = rejectedAt != null
? DateTime.now().difference(rejectedAt).inDays
: _waitDays;
final daysLeft = (_waitDays - daysSince).clamp(0, _waitDays);
final canRetry = !widget.isPending && daysLeft == 0;
return Container(
margin: const EdgeInsets.only(top: 24),
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: cardBg,
color: widget.cardBg,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: border),
border: Border.all(color: widget.border),
),
child: Column(
children: [
if (isPending)
const Image(
image: AssetImage('assets/checklist.gif'),
width: 160)
if (widget.isPending)
const Image(image: AssetImage('assets/checklist.gif'), width: 160)
else
Container(
padding: const EdgeInsets.all(20),
@@ -653,63 +689,70 @@ class _StatusCard extends StatelessWidget {
color: accent.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
isPending
? Icons.hourglass_empty_rounded
: Icons.sentiment_dissatisfied_outlined,
size: 52,
color: accent),
child: Icon(Icons.sentiment_dissatisfied_outlined, size: 52, color: accent),
),
const SizedBox(height: 18),
Text(
isPending
? 'Solicitud en revisión'
: 'Solicitud no aprobada',
style: TextStyle(
fontSize: 19,
fontWeight: FontWeight.bold,
color: textPrimary),
widget.isPending ? 'Solicitud en revisión' : 'Solicitud no aprobada',
style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold, color: widget.textPrimary),
),
const SizedBox(height: 10),
Text(
isPending
widget.isPending
? 'Estamos revisando tus datos. Te notificaremos cuando tu cuenta esté aprobada.'
: 'Tu solicitud no fue aprobada. Revisa tus documentos y vuelve a intentarlo.',
: canRetry
? 'Puedes volver a enviar tu solicitud con los documentos corregidos.'
: 'Tu solicitud no fue aprobada. Podrás reintentar en $daysLeft día${daysLeft == 1 ? '' : 's'}.',
textAlign: TextAlign.center,
style:
TextStyle(color: textSec, fontSize: 13, height: 1.5),
style: TextStyle(color: widget.textSec, fontSize: 13, height: 1.5),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: accent.withOpacity(0.12),
borderRadius: BorderRadius.circular(10),
border:
Border.all(color: accent.withOpacity(0.4)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isPending
? Icons.hourglass_empty_rounded
: Icons.cancel_outlined,
color: accent,
size: 16,
if (!widget.isPending && !_loadingSettings) ...[
if (canRetry)
FilledButton.icon(
onPressed: _retry,
icon: const Icon(Icons.refresh, size: 18),
label: const Text('Volver a solicitar'),
style: FilledButton.styleFrom(backgroundColor: const Color(0xFF42A4EF)),
)
else
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: accent.withOpacity(0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: accent.withOpacity(0.4)),
),
const SizedBox(width: 6),
Text(
isPending ? 'Revisión en proceso' : 'No aprobado',
style: TextStyle(
color: accent,
fontWeight: FontWeight.w600,
fontSize: 13),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.timer_outlined, color: accent, size: 16),
const SizedBox(width: 6),
Text(
'Disponible en $daysLeft día${daysLeft == 1 ? '' : 's'}',
style: TextStyle(color: accent, fontWeight: FontWeight.w600, fontSize: 13),
),
],
),
],
),
] else if (widget.isPending)
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: accent.withOpacity(0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: accent.withOpacity(0.4)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.hourglass_empty_rounded, color: accent, size: 16),
const SizedBox(width: 6),
Text('Revisión en proceso',
style: TextStyle(color: accent, fontWeight: FontWeight.w600, fontSize: 13)),
],
),
),
),
],
),
);