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