fixes
This commit is contained in:
parent
34e7cbc382
commit
425ee590fa
5 changed files with 127 additions and 15 deletions
|
|
@ -1,4 +1,4 @@
|
|||
const String kApiBase = 'https://reservas.madriguera.me/2.0';
|
||||
|
||||
/// Cambiar a false para usar la API real (necesita token).
|
||||
const bool kUseMock = true;
|
||||
const bool kUseMock = false;
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ class Member {
|
|||
final double? fee;
|
||||
final List<Person> people;
|
||||
final List<MemberComment> comments;
|
||||
final Map<int, List<int>> monthlyFees;
|
||||
|
||||
const Member({
|
||||
required this.id,
|
||||
|
|
@ -126,6 +127,7 @@ class Member {
|
|||
this.fee,
|
||||
required this.people,
|
||||
required this.comments,
|
||||
required this.monthlyFees,
|
||||
});
|
||||
|
||||
factory Member.fromJson(Map<String, dynamic> json) => Member(
|
||||
|
|
@ -157,6 +159,12 @@ class Member {
|
|||
comments: (json['comments'] as List<dynamic>? ?? [])
|
||||
.map((c) => MemberComment.fromJson(c as Map<String, dynamic>))
|
||||
.toList(),
|
||||
monthlyFees: (json['monthly_fees'] as Map<String, dynamic>? ?? {}).map(
|
||||
(year, months) => MapEntry(
|
||||
int.parse(year),
|
||||
(months as List<dynamic>).map((v) => (v as num).toInt()).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
String get formattedId => id.toString().padLeft(5, '0');
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class MembersScreen extends StatefulWidget {
|
|||
|
||||
class _MembersScreenState extends State<MembersScreen> {
|
||||
final _service = memberRepository();
|
||||
final _searchKey = GlobalKey<_MemberSearchState>();
|
||||
Member? _member;
|
||||
bool _loading = true;
|
||||
String? _error;
|
||||
|
|
@ -60,17 +61,20 @@ class _MembersScreenState extends State<MembersScreen> {
|
|||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_NavBar(
|
||||
searchKey: _searchKey,
|
||||
member: _member,
|
||||
loading: _loading,
|
||||
service: _service,
|
||||
onFirst: () => _load(_service.first()),
|
||||
onFirst: () { _searchKey.currentState?.clear(); _load(_service.first()); },
|
||||
onPrev: () {
|
||||
_searchKey.currentState?.clear();
|
||||
if (_member != null) _load(_service.prev(_member!.id));
|
||||
},
|
||||
onNext: () {
|
||||
_searchKey.currentState?.clear();
|
||||
if (_member != null) _load(_service.next(_member!.id));
|
||||
},
|
||||
onLast: () => _load(_service.last()),
|
||||
onLast: () { _searchKey.currentState?.clear(); _load(_service.last()); },
|
||||
onSearch: (id) => _load(_service.show(id)),
|
||||
onAction: _handleAction,
|
||||
),
|
||||
|
|
@ -92,6 +96,7 @@ class _MembersScreenState extends State<MembersScreen> {
|
|||
// ── Nav bar ──────────────────────────────────────────────────────────────────
|
||||
|
||||
class _NavBar extends StatelessWidget {
|
||||
final GlobalKey<_MemberSearchState> searchKey;
|
||||
final Member? member;
|
||||
final bool loading;
|
||||
final MemberRepository service;
|
||||
|
|
@ -100,6 +105,7 @@ class _NavBar extends StatelessWidget {
|
|||
final void Function(String action) onAction;
|
||||
|
||||
const _NavBar({
|
||||
required this.searchKey,
|
||||
required this.member,
|
||||
required this.loading,
|
||||
required this.service,
|
||||
|
|
@ -122,7 +128,7 @@ class _NavBar extends StatelessWidget {
|
|||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _MemberSearch(service: service, onSelected: onSearch),
|
||||
child: _MemberSearch(key: searchKey, service: service, onSelected: onSearch),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
|
|
@ -207,13 +213,17 @@ class _MemberSearch extends StatefulWidget {
|
|||
final MemberRepository service;
|
||||
final void Function(int id) onSelected;
|
||||
|
||||
const _MemberSearch({required this.service, required this.onSelected});
|
||||
const _MemberSearch({super.key, required this.service, required this.onSelected});
|
||||
|
||||
@override
|
||||
State<_MemberSearch> createState() => _MemberSearchState();
|
||||
}
|
||||
|
||||
class _MemberSearchState extends State<_MemberSearch> {
|
||||
TextEditingController? _controller;
|
||||
|
||||
void clear() => _controller?.clear();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Autocomplete<MapEntry<String, String>>(
|
||||
|
|
@ -227,11 +237,23 @@ class _MemberSearchState extends State<_MemberSearch> {
|
|||
// key format: 5-digit member_id + 2-digit family index
|
||||
final memberId = int.parse(entry.key.substring(0, entry.key.length - 2));
|
||||
widget.onSelected(memberId);
|
||||
_controller?.clear();
|
||||
},
|
||||
fieldViewBuilder: (context, controller, focusNode, onSubmit) {
|
||||
_controller = controller;
|
||||
return TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
onTap: () => controller.clear(),
|
||||
onSubmitted: (text) {
|
||||
final n = int.tryParse(text.trim());
|
||||
if (n != null) {
|
||||
widget.onSelected(n);
|
||||
controller.clear();
|
||||
} else {
|
||||
onSubmit();
|
||||
}
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Buscar abonado...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
|
|
@ -447,7 +469,7 @@ class _InfoCard extends StatelessWidget {
|
|||
const SizedBox(height: 14),
|
||||
|
||||
// Franjas de años
|
||||
_YearStrips(),
|
||||
_YearStrips(data: member.monthlyFees),
|
||||
|
||||
// Info de baja si está dado de baja
|
||||
if (!member.isActive && member.unregDate != null) ...[
|
||||
|
|
@ -468,8 +490,8 @@ class _InfoCard extends StatelessWidget {
|
|||
enum _MonthStatus { unknown, paid, unpaid }
|
||||
|
||||
class _YearStrips extends StatelessWidget {
|
||||
/// year → month(1–12) → status. Si está vacío todos los meses son amarillo.
|
||||
final Map<int, Map<int, _MonthStatus>> data;
|
||||
/// year → lista de 12 valores: -1 amarillo, 1 verde, 0 o 4 rojo.
|
||||
final Map<int, List<int>> data;
|
||||
|
||||
const _YearStrips({this.data = const {}});
|
||||
|
||||
|
|
@ -478,6 +500,12 @@ class _YearStrips extends StatelessWidget {
|
|||
'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic',
|
||||
];
|
||||
|
||||
static _MonthStatus _toStatus(int v) => switch (v) {
|
||||
1 => _MonthStatus.paid,
|
||||
0 || 4 => _MonthStatus.unpaid,
|
||||
_ => _MonthStatus.unknown,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final now = DateTime.now();
|
||||
|
|
@ -485,7 +513,7 @@ class _YearStrips extends StatelessWidget {
|
|||
|
||||
return Column(
|
||||
children: years.map((year) {
|
||||
final yearData = data[year] ?? {};
|
||||
final months = data[year] ?? [];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
child: Row(
|
||||
|
|
@ -498,7 +526,9 @@ class _YearStrips extends StatelessWidget {
|
|||
color: Theme.of(context).colorScheme.outline)),
|
||||
),
|
||||
...List.generate(12, (i) {
|
||||
final status = yearData[i + 1] ?? _MonthStatus.unknown;
|
||||
final status = i < months.length
|
||||
? _toStatus(months[i])
|
||||
: _MonthStatus.unknown;
|
||||
return Expanded(child: _MonthCell(month: _months[i], status: status));
|
||||
}),
|
||||
],
|
||||
|
|
@ -589,6 +619,44 @@ class _CommentsCard extends StatelessWidget {
|
|||
final Member member;
|
||||
const _CommentsCard({required this.member});
|
||||
|
||||
void _showAddCommentDialog(BuildContext context) {
|
||||
final controller = TextEditingController();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Añadir comentario'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
maxLines: 4,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Escribe el comentario...',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
// TODO: llamar al endpoint de creación de comentario
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Guardar comentario — pendiente de implementar'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Guardar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool get _hasContent =>
|
||||
member.comment.isNotEmpty ||
|
||||
member.comment2.isNotEmpty ||
|
||||
|
|
@ -604,8 +672,22 @@ class _CommentsCard extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Comentarios',
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text('Comentarios',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => _showAddCommentDialog(context),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Añadir'),
|
||||
style: TextButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 20),
|
||||
if (!_hasContent)
|
||||
Text('Sin comentarios',
|
||||
|
|
@ -688,8 +770,10 @@ class _FamiliarsCard extends StatelessWidget {
|
|||
fontSize: 13))
|
||||
else
|
||||
LayoutBuilder(builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: constraints.maxWidth,
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
||||
child: DataTable(
|
||||
headingRowHeight: 36,
|
||||
dataRowMinHeight: 52,
|
||||
|
|
@ -712,6 +796,7 @@ class _FamiliarsCard extends StatelessWidget {
|
|||
],
|
||||
rows: people.map((p) => _personRow(context, p)).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class AuthService {
|
|||
}
|
||||
|
||||
Future<String?> getToken() async {
|
||||
return '11e2d9ac6355c3ed64e7651451f8a8f6fbbe9a6fa1a720a6f390dd598533faf5';
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_tokenKey);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,12 @@ final _member7 = Member(
|
|||
retenido: false,
|
||||
unregDate: null,
|
||||
unregReason: '',
|
||||
monthlyFees: {
|
||||
2025: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
2026: [1, 1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1],
|
||||
2027: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
|
||||
2028: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
|
||||
},
|
||||
people: [
|
||||
Person(
|
||||
memberId: 7, family: 1,
|
||||
|
|
@ -138,6 +144,12 @@ final _member42 = Member(
|
|||
retenido: false,
|
||||
unregDate: null,
|
||||
unregReason: '',
|
||||
monthlyFees: {
|
||||
2025: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
|
||||
2026: [1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
|
||||
2027: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
|
||||
2028: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
|
||||
},
|
||||
people: [
|
||||
Person(
|
||||
memberId: 42, family: 1,
|
||||
|
|
@ -191,6 +203,12 @@ final _member85 = Member(
|
|||
retenido: true,
|
||||
unregDate: null,
|
||||
unregReason: '',
|
||||
monthlyFees: {
|
||||
2025: [1, 1, 1, 1, 1, 0, 4, 0, 0, 0, 0, 0],
|
||||
2026: [0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
|
||||
2027: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
|
||||
2028: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
|
||||
},
|
||||
people: [
|
||||
Person(
|
||||
memberId: 85, family: 1,
|
||||
|
|
|
|||
Loading…
Reference in a new issue