fix
This commit is contained in:
parent
425ee590fa
commit
dd7b75aae5
6 changed files with 460 additions and 54 deletions
12
TAREAS.md
12
TAREAS.md
|
|
@ -44,6 +44,16 @@
|
|||
|
||||
## Tareas
|
||||
|
||||
### ✅ Completadas (sesión 5 — 2026-03-22)
|
||||
|
||||
- [x] **MEMB-02a** — Acciones de estado en ficha de abonado
|
||||
- Menú dinámico: muestra "Retener/Cancelar retención" según estado, "Dar de baja/Reactivar" según si está activo
|
||||
- **Retener / Cancelar retención:** diálogo de confirmación → `POST /members/retener` o `/retener-cancel` → actualiza la ficha en pantalla
|
||||
- **Dar de baja:** pre-verificación (`POST /members/pre-delete`), alerta si hay recibos pendientes/devueltos, selección de fecha con date picker, motivo opcional → `POST /members/delete`
|
||||
- **Reactivar:** diálogo de confirmación → `POST /members/recover`
|
||||
- `Member.copyWith` para campos de estado (retenido, unregDate, unregReason, etc.)
|
||||
- Mock con estado mutable: los cambios persisten al navegar entre socios
|
||||
|
||||
### ✅ Completadas (sesión 4 — 2026-03-18)
|
||||
|
||||
- [x] **BOOK-01** — Reservas de pistas (maquetación completa)
|
||||
|
|
@ -105,7 +115,7 @@ _(ninguna)_
|
|||
### ⏳ Pendientes (próximas sesiones)
|
||||
|
||||
- [ ] **AUTH-02** — Logo/imagen en pantalla de login (pendiente de asset)
|
||||
- [ ] **MEMB-01** — Listado de abonados
|
||||
- [x] **MEMB-01** — Listado de abonados (no aplica, no existe en el sistema)
|
||||
- [ ] **MEMB-02** — Ficha de abonado (detalle + edición)
|
||||
- [ ] **MEMB-03** — Pre-inscripciones
|
||||
- [ ] **MEMB-04** — Alta de nuevo abonado
|
||||
|
|
|
|||
|
|
@ -169,4 +169,44 @@ class Member {
|
|||
|
||||
String get formattedId => id.toString().padLeft(5, '0');
|
||||
bool get isActive => unregDate == null;
|
||||
|
||||
static const _sentinel = Object();
|
||||
|
||||
Member copyWith({
|
||||
bool? paid,
|
||||
bool? retenido,
|
||||
bool? remSpecial,
|
||||
String? comment2,
|
||||
List<MemberComment>? comments,
|
||||
Object? unregDate = _sentinel,
|
||||
String? unregReason,
|
||||
}) {
|
||||
return Member(
|
||||
id: id,
|
||||
regDate: regDate,
|
||||
address: address,
|
||||
city: city,
|
||||
zip: zip,
|
||||
email: email,
|
||||
phone: phone,
|
||||
bank: bank,
|
||||
bankOffice: bankOffice,
|
||||
bankSecCode: bankSecCode,
|
||||
bankAccount: bankAccount,
|
||||
bankName: bankName,
|
||||
mandate: mandate,
|
||||
type: type,
|
||||
comment: comment,
|
||||
comment2: comment2 ?? this.comment2,
|
||||
paid: paid ?? this.paid,
|
||||
remSpecial: remSpecial ?? this.remSpecial,
|
||||
retenido: retenido ?? this.retenido,
|
||||
unregDate: identical(unregDate, _sentinel) ? this.unregDate : unregDate as String?,
|
||||
unregReason: unregReason ?? this.unregReason,
|
||||
fee: fee,
|
||||
people: people,
|
||||
comments: comments ?? this.comments,
|
||||
monthlyFees: monthlyFees,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,15 +24,119 @@ class _MembersScreenState extends State<MembersScreen> {
|
|||
}
|
||||
|
||||
void _handleAction(String action) {
|
||||
final labels = {
|
||||
'receipts': 'Recibos anteriores — pendiente de implementar',
|
||||
'edit': 'Editar abonado — pendiente de implementar',
|
||||
'add_family': 'Añadir familiar — pendiente de implementar',
|
||||
'retain': 'Retener abonado — pendiente de implementar',
|
||||
'unregister': 'Dar de baja — pendiente de implementar',
|
||||
};
|
||||
final member = _member;
|
||||
if (member == null) return;
|
||||
switch (action) {
|
||||
case 'receipts': _doPaidCaution(member);
|
||||
case 'retain': _doRetener(member);
|
||||
case 'unregister': _doDarDeBaja(member);
|
||||
case 'recover': _doReactivar(member);
|
||||
default:
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('$action — pendiente de implementar'),
|
||||
duration: const Duration(seconds: 2)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _doPaidCaution(Member member) async {
|
||||
try {
|
||||
final updated = await _service.paidCaution(member.id);
|
||||
if (!mounted) return;
|
||||
setState(() => _member = updated);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_showError('Error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _doRetener(Member member) async {
|
||||
final cancelar = member.retenido;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(cancelar ? 'Cancelar retención' : 'Retener abonado'),
|
||||
content: Text(cancelar
|
||||
? '¿Cancelar la retención del socio ${member.formattedId}?'
|
||||
: '¿Marcar como retenido al socio ${member.formattedId}?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancelar')),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(cancelar ? 'Cancelar retención' : 'Retener'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !mounted) return;
|
||||
try {
|
||||
final updated = cancelar
|
||||
? await _service.retenerCancel(member.id)
|
||||
: await _service.retener(member.id);
|
||||
if (!mounted) return;
|
||||
setState(() => _member = updated);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_showError('Error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _doDarDeBaja(Member member) async {
|
||||
Map<String, int> preCheck;
|
||||
try {
|
||||
preCheck = await _service.preDelete(member.id);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_showError('Error al verificar: $e');
|
||||
return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
|
||||
final result = await showDialog<(String, String)>(
|
||||
context: context,
|
||||
builder: (_) => _BajaDialog(member: member, preCheck: preCheck),
|
||||
);
|
||||
if (result == null || !mounted) return;
|
||||
|
||||
try {
|
||||
final updated = await _service.delete(member.id, result.$1, result.$2);
|
||||
if (!mounted) return;
|
||||
setState(() => _member = updated);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_showError('Error al dar de baja: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _doReactivar(Member member) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Reactivar abonado'),
|
||||
content: Text('¿Reactivar al socio ${member.formattedId}?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancelar')),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('Reactivar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !mounted) return;
|
||||
try {
|
||||
final updated = await _service.recover(member.id);
|
||||
if (!mounted) return;
|
||||
setState(() => _member = updated);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_showError('Error al reactivar: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(String msg) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(labels[action] ?? action), duration: const Duration(seconds: 2)),
|
||||
SnackBar(content: Text(msg), duration: const Duration(seconds: 3)),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -157,51 +261,67 @@ class _NavBar extends StatelessWidget {
|
|||
tooltip: 'Acciones',
|
||||
enabled: !disabled,
|
||||
onSelected: onAction,
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'receipts',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.receipt_long_outlined),
|
||||
title: Text('Recibos anteriores'),
|
||||
dense: true,
|
||||
itemBuilder: (context) {
|
||||
final m = member;
|
||||
final isActive = m?.isActive ?? true;
|
||||
final retenido = m?.retenido ?? false;
|
||||
return [
|
||||
const PopupMenuItem(
|
||||
value: 'receipts',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.receipt_long_outlined),
|
||||
title: Text('Recibos anteriores'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.edit_outlined),
|
||||
title: Text('Editar abonado'),
|
||||
dense: true,
|
||||
const PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.edit_outlined),
|
||||
title: Text('Editar abonado'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'add_family',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.person_add_outlined),
|
||||
title: Text('Añadir familiar'),
|
||||
dense: true,
|
||||
const PopupMenuItem(
|
||||
value: 'add_family',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.person_add_outlined),
|
||||
title: Text('Añadir familiar'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'retain',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.pause_circle_outline),
|
||||
title: Text('Retener abonado'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
value: 'unregister',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.person_remove_outlined,
|
||||
color: cs.error),
|
||||
title: Text('Dar de baja',
|
||||
style: TextStyle(color: cs.error)),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (isActive)
|
||||
PopupMenuItem(
|
||||
value: 'retain',
|
||||
child: ListTile(
|
||||
leading: Icon(retenido
|
||||
? Icons.play_circle_outline
|
||||
: Icons.pause_circle_outline),
|
||||
title: Text(retenido ? 'Cancelar retención' : 'Retener abonado'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
if (!isActive)
|
||||
const PopupMenuItem(
|
||||
value: 'recover',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.person_add_alt_1_outlined),
|
||||
title: Text('Reactivar abonado'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
if (isActive)
|
||||
PopupMenuItem(
|
||||
value: 'unregister',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.person_remove_outlined, color: cs.error),
|
||||
title: Text('Dar de baja', style: TextStyle(color: cs.error)),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -438,6 +558,11 @@ class _InfoCard extends StatelessWidget {
|
|||
label: 'Remesa especial',
|
||||
color: cs.primary,
|
||||
icon: Icons.star_outline),
|
||||
if (member.paid)
|
||||
_StatusChip(
|
||||
label: 'Debe recibos',
|
||||
color: cs.error,
|
||||
icon: Icons.warning_amber_rounded),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -1045,6 +1170,140 @@ class _InfoRow extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Dar de baja dialog ────────────────────────────────────────────────────────
|
||||
|
||||
class _BajaDialog extends StatefulWidget {
|
||||
final Member member;
|
||||
final Map<String, int> preCheck;
|
||||
|
||||
const _BajaDialog({required this.member, required this.preCheck});
|
||||
|
||||
@override
|
||||
State<_BajaDialog> createState() => _BajaDialogState();
|
||||
}
|
||||
|
||||
class _BajaDialogState extends State<_BajaDialog> {
|
||||
late DateTime _date;
|
||||
final _reasonCtrl = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_date = DateTime.now();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_reasonCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String get _dateStr =>
|
||||
'${_date.year.toString().padLeft(4, '0')}-'
|
||||
'${_date.month.toString().padLeft(2, '0')}-'
|
||||
'${_date.day.toString().padLeft(2, '0')}';
|
||||
|
||||
Future<void> _pickDate() async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _date,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime.now().add(const Duration(days: 30)),
|
||||
);
|
||||
if (picked != null) setState(() => _date = picked);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final pendientes = widget.preCheck['pendientes'] ?? 0;
|
||||
final devueltos = widget.preCheck['devueltos'] ?? 0;
|
||||
final revision = widget.preCheck['revision'] ?? 0;
|
||||
final hasWarning = pendientes > 0 || devueltos > 0 || revision > 0;
|
||||
|
||||
return AlertDialog(
|
||||
title: Text('Dar de baja — ${widget.member.formattedId}'),
|
||||
content: SizedBox(
|
||||
width: 380,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (hasWarning) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [
|
||||
Icon(Icons.warning_amber_rounded, size: 16, color: cs.error),
|
||||
const SizedBox(width: 6),
|
||||
Text('Atención', style: TextStyle(fontWeight: FontWeight.bold, color: cs.error)),
|
||||
]),
|
||||
const SizedBox(height: 4),
|
||||
if (pendientes > 0) Text('• $pendientes recibo(s) pendiente(s)', style: const TextStyle(fontSize: 13)),
|
||||
if (devueltos > 0) Text('• $devueltos recibo(s) devuelto(s)', style: const TextStyle(fontSize: 13)),
|
||||
if (revision > 0) Text('• $revision recibo(s) en revisión', style: const TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
// Fecha de baja
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Fecha de baja',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
child: Text(_dateStr),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton.outlined(
|
||||
icon: const Icon(Icons.calendar_today, size: 18),
|
||||
onPressed: _pickDate,
|
||||
tooltip: 'Seleccionar fecha',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _reasonCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Motivo (opcional)',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(backgroundColor: cs.error),
|
||||
onPressed: () => Navigator.pop(context, (_dateStr, _reasonCtrl.text)),
|
||||
child: const Text('Dar de baja'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
String _fmtDate(String? date) {
|
||||
if (date == null || date.isEmpty) return '—';
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -7,4 +7,10 @@ abstract class MemberRepository {
|
|||
Future<Member> prev(int id);
|
||||
Future<Member> next(int id);
|
||||
Future<Map<String, String>> search(String term);
|
||||
Future<Member> paidCaution(int id);
|
||||
Future<Member> retener(int id);
|
||||
Future<Member> retenerCancel(int id);
|
||||
Future<Member> recover(int id);
|
||||
Future<Map<String, int>> preDelete(int id);
|
||||
Future<Member> delete(int id, String unregDate, String unregReason);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,4 +43,63 @@ class MemberService implements MemberRepository {
|
|||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _postHeaders() async =>
|
||||
{...await _headers(), 'Content-Type': 'application/json'};
|
||||
|
||||
// POST → respuesta con clave "member"
|
||||
Future<Member> _postWrapped(String path, Map<String, dynamic> body) async {
|
||||
final res = await http.post(Uri.parse('$kApiBase/$path'),
|
||||
headers: await _postHeaders(), body: jsonEncode(body));
|
||||
if (res.statusCode == 200) {
|
||||
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
return Member.fromJson(data['member'] as Map<String, dynamic>);
|
||||
}
|
||||
throw Exception('Error ${res.statusCode}');
|
||||
}
|
||||
|
||||
// POST → respuesta es directamente el objeto member
|
||||
Future<Member> _postDirect(String path, Map<String, dynamic> body) async {
|
||||
final res = await http.post(Uri.parse('$kApiBase/$path'),
|
||||
headers: await _postHeaders(), body: jsonEncode(body));
|
||||
if (res.statusCode == 200) {
|
||||
return Member.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
|
||||
}
|
||||
throw Exception('Error ${res.statusCode}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Member> paidCaution(int id) => _postDirect('members/paid-caution', {'pk_i_id': id});
|
||||
|
||||
@override
|
||||
Future<Member> retener(int id) => _postWrapped('members/retener', {'member': id});
|
||||
|
||||
@override
|
||||
Future<Member> retenerCancel(int id) => _postWrapped('members/retener-cancel', {'member': id});
|
||||
|
||||
@override
|
||||
Future<Member> recover(int id) => _postDirect('members/recover', {'pk_i_id': id});
|
||||
|
||||
@override
|
||||
Future<Map<String, int>> preDelete(int id) async {
|
||||
final res = await http.post(Uri.parse('$kApiBase/members/pre-delete'),
|
||||
headers: await _postHeaders(), body: jsonEncode({'pk_i_id': id}));
|
||||
if (res.statusCode == 200) {
|
||||
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
return {
|
||||
'pendientes': (data['pendientes'] as num).toInt(),
|
||||
'devueltos': (data['devueltos'] as num).toInt(),
|
||||
'revision': (data['revision'] as num).toInt(),
|
||||
};
|
||||
}
|
||||
throw Exception('Error ${res.statusCode}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Member> delete(int id, String unregDate, String unregReason) =>
|
||||
_postDirect('members/delete', {
|
||||
'pk_i_id': id,
|
||||
'd_unreg_date': unregDate,
|
||||
's_unreg_reason': unregReason,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,25 @@ import '../../models/member.dart';
|
|||
import '../member_repository.dart';
|
||||
|
||||
class MockMemberService implements MemberRepository {
|
||||
static final _members = [_member7, _member42, _member85];
|
||||
final _members = [_member7, _member42, _member85];
|
||||
|
||||
int _indexOf(int id) {
|
||||
final i = _members.indexWhere((m) => m.id == id);
|
||||
return i == -1 ? 0 : i;
|
||||
}
|
||||
|
||||
Member _update(Member updated) {
|
||||
final i = _indexOf(updated.id);
|
||||
if (i != -1) _members[i] = updated;
|
||||
return updated;
|
||||
}
|
||||
|
||||
Member _get(int id) =>
|
||||
_members.firstWhere((m) => m.id == id, orElse: () => _members.first);
|
||||
|
||||
@override Future<Member> first() async => _members.first;
|
||||
@override Future<Member> last() async => _members.last;
|
||||
@override Future<Member> show(int id) async =>
|
||||
_members.firstWhere((m) => m.id == id, orElse: () => _members.first);
|
||||
@override Future<Member> show(int id) async => _get(id);
|
||||
|
||||
@override
|
||||
Future<Member> prev(int id) async {
|
||||
|
|
@ -41,6 +49,30 @@ class MockMemberService implements MemberRepository {
|
|||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Member> paidCaution(int id) async =>
|
||||
_update(_get(id).copyWith(paid: !_get(id).paid));
|
||||
|
||||
@override
|
||||
Future<Member> retener(int id) async =>
|
||||
_update(_get(id).copyWith(retenido: true));
|
||||
|
||||
@override
|
||||
Future<Member> retenerCancel(int id) async =>
|
||||
_update(_get(id).copyWith(retenido: false));
|
||||
|
||||
@override
|
||||
Future<Member> recover(int id) async =>
|
||||
_update(_get(id).copyWith(unregDate: null, unregReason: ''));
|
||||
|
||||
@override
|
||||
Future<Map<String, int>> preDelete(int id) async =>
|
||||
{'pendientes': 0, 'devueltos': 0, 'revision': 0};
|
||||
|
||||
@override
|
||||
Future<Member> delete(int id, String unregDate, String unregReason) async =>
|
||||
_update(_get(id).copyWith(unregDate: unregDate, unregReason: unregReason));
|
||||
}
|
||||
|
||||
// ── Datos mock ────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in a new issue