Agrego frontend app
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
class MetodoPago {
|
||||
final int id;
|
||||
final String descripcion;
|
||||
final bool activo;
|
||||
final String? icono;
|
||||
|
||||
const MetodoPago({
|
||||
required this.id,
|
||||
required this.descripcion,
|
||||
this.activo = true,
|
||||
this.icono,
|
||||
});
|
||||
|
||||
factory MetodoPago.fromMap(Map<String, dynamic> map) {
|
||||
return MetodoPago(
|
||||
id: map['id'] as int,
|
||||
descripcion: map['descripcion'] as String? ?? '',
|
||||
activo: map['activo'] as bool? ?? true,
|
||||
icono: map['icono'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import 'package:gimnasio_soma/features/pagos/domain/entities/pago_tipo.dart';
|
||||
|
||||
class Pago {
|
||||
final String id;
|
||||
final PagoTipo tipo;
|
||||
// Cuando tipo == ajuste, anioMesPagado puede venir vacío (NULL en DB).
|
||||
final String anioMesPagado; // date string "YYYY-MM-DD" (siempre día 1) o vacío
|
||||
final DateTime? fechaPago;
|
||||
final double montoTotal;
|
||||
// Cuando tipo == ajuste, metodo puede venir vacío (NULL en DB).
|
||||
final String metodo;
|
||||
final Map<String, dynamic>? detalle;
|
||||
// Sólo presente en fc_obtener_pagos (admin), no en fc_obtener_mis_pagos.
|
||||
final PagoCliente? cliente;
|
||||
|
||||
// Auditoría de creación. createdBy es el UUID del autor — habilita
|
||||
// gating local "este pago lo creé yo".
|
||||
final DateTime? createdAt;
|
||||
final String? createdBy;
|
||||
final String? createdByNombre;
|
||||
|
||||
// Auditoría de última edición (null si nunca se editó).
|
||||
final DateTime? updatedAt;
|
||||
final String? updatedByNombre;
|
||||
|
||||
// Auditoría de anulación (soft-delete).
|
||||
final DateTime? anuladoAt;
|
||||
final String? anuladoPorNombre;
|
||||
final String? motivoAnulacion;
|
||||
|
||||
const Pago({
|
||||
required this.id,
|
||||
required this.tipo,
|
||||
required this.anioMesPagado,
|
||||
this.fechaPago,
|
||||
required this.montoTotal,
|
||||
required this.metodo,
|
||||
this.detalle,
|
||||
this.cliente,
|
||||
this.createdAt,
|
||||
this.createdBy,
|
||||
this.createdByNombre,
|
||||
this.updatedAt,
|
||||
this.updatedByNombre,
|
||||
this.anuladoAt,
|
||||
this.anuladoPorNombre,
|
||||
this.motivoAnulacion,
|
||||
});
|
||||
|
||||
factory Pago.fromMap(Map<String, dynamic> map) {
|
||||
return Pago(
|
||||
id: map['id'] as String,
|
||||
tipo: PagoTipo.fromString(map['tipo'] as String?),
|
||||
anioMesPagado: map['anio_mes_pagado'] as String? ?? '',
|
||||
fechaPago: _parseDate(map['fecha_pago']),
|
||||
montoTotal: (map['monto_total'] as num?)?.toDouble() ?? 0,
|
||||
metodo: map['metodo'] as String? ?? '',
|
||||
detalle: map['detalle'] as Map<String, dynamic>?,
|
||||
cliente: map['cliente'] != null
|
||||
? PagoCliente.fromMap(map['cliente'] as Map<String, dynamic>)
|
||||
: null,
|
||||
createdAt: _parseDate(map['created_at']),
|
||||
createdBy: map['created_by'] as String?,
|
||||
createdByNombre: map['created_by_nombre'] as String?,
|
||||
updatedAt: _parseDate(map['updated_at']),
|
||||
updatedByNombre: map['updated_by_nombre'] as String?,
|
||||
anuladoAt: _parseDate(map['anulado_at']),
|
||||
anuladoPorNombre: map['anulado_por_nombre'] as String?,
|
||||
motivoAnulacion: map['motivo_anulacion'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime? _parseDate(dynamic raw) {
|
||||
if (raw == null) return null;
|
||||
return DateTime.tryParse(raw.toString());
|
||||
}
|
||||
|
||||
/// Mes y año formateado: "Marzo 2026"
|
||||
String get mesPagadoDisplay {
|
||||
final date = DateTime.tryParse(anioMesPagado);
|
||||
if (date == null) return anioMesPagado;
|
||||
const meses = [
|
||||
'', 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
|
||||
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre',
|
||||
];
|
||||
return '${meses[date.month]} ${date.year}';
|
||||
}
|
||||
|
||||
/// Fecha de pago formateada: "15/03/2026"
|
||||
String get fechaPagoDisplay {
|
||||
if (fechaPago == null) return '-';
|
||||
final d = fechaPago!;
|
||||
return '${d.day.toString().padLeft(2, '0')}/${d.month.toString().padLeft(2, '0')}/${d.year}';
|
||||
}
|
||||
|
||||
bool get isAnulado => anuladoAt != null;
|
||||
bool get isEditado => updatedAt != null;
|
||||
|
||||
/// True si el pago todavía está dentro de la ventana de edición desde su
|
||||
/// creación. La ventana se pasa como parámetro para no acoplar la entidad
|
||||
/// a un provider; el caller obtiene el valor de
|
||||
/// AppConstants.pagosVentanaEdicionMinutosDefault.
|
||||
///
|
||||
/// El backend sigue siendo la fuente de verdad: este getter sirve sólo
|
||||
/// para gating local de UI.
|
||||
bool isEditableWindow(int ventanaMinutos) {
|
||||
if (createdAt == null) return false;
|
||||
final diff = DateTime.now().difference(createdAt!).inMinutes;
|
||||
return diff < ventanaMinutos;
|
||||
}
|
||||
|
||||
/// True si el actor puede editar este pago. La ventana aplica a todos
|
||||
/// (incluso superadmin); ownership sólo si NO es superadmin.
|
||||
/// El backend es la fuente de verdad; este getter es para gating local.
|
||||
bool puedeEditar(String? actorUserId, bool isSuperadmin, int ventanaMinutos) {
|
||||
if (isAnulado) return false;
|
||||
if (!isEditableWindow(ventanaMinutos)) return false;
|
||||
if (isSuperadmin) return true;
|
||||
if (actorUserId == null || createdBy == null) return false;
|
||||
return actorUserId == createdBy;
|
||||
}
|
||||
|
||||
/// True si el actor puede anular este pago. Superadmin bypassea ownership
|
||||
/// y ventana; el resto necesita ser owner y estar dentro de ventana.
|
||||
bool puedeAnular(String? actorUserId, bool isSuperadmin, int ventanaMinutos) {
|
||||
if (isAnulado) return false;
|
||||
if (isSuperadmin) return true;
|
||||
if (actorUserId == null || createdBy == null) return false;
|
||||
if (actorUserId != createdBy) return false;
|
||||
return isEditableWindow(ventanaMinutos);
|
||||
}
|
||||
}
|
||||
|
||||
class PagoCliente {
|
||||
final String nombre;
|
||||
final String apellido;
|
||||
final String dni;
|
||||
|
||||
const PagoCliente({
|
||||
required this.nombre,
|
||||
required this.apellido,
|
||||
required this.dni,
|
||||
});
|
||||
|
||||
factory PagoCliente.fromMap(Map<String, dynamic> map) {
|
||||
return PagoCliente(
|
||||
nombre: map['nombre'] as String? ?? '',
|
||||
apellido: map['apellido'] as String? ?? '',
|
||||
dni: map['dni'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
String get displayName {
|
||||
if (nombre.isNotEmpty && apellido.isNotEmpty) return '$nombre $apellido';
|
||||
if (nombre.isNotEmpty) return nombre;
|
||||
return dni;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/// Discriminador de la tabla pagos. El backend acepta los cuatro valores
|
||||
/// del schema; el frontend de este sprint sólo CREA cuotaMensual (los
|
||||
/// correctivos llegan desde SQL directo hasta que tengan sus propias
|
||||
/// funciones fc_*).
|
||||
enum PagoTipo {
|
||||
cuotaMensual,
|
||||
devolucion,
|
||||
descuentoRetroactivo,
|
||||
ajuste;
|
||||
|
||||
/// Parsea el valor del backend. Default defensivo: cuotaMensual.
|
||||
/// El backfill de la migración garantiza que pagos.tipo nunca sea NULL,
|
||||
/// pero ante un valor desconocido no rompemos el parseo.
|
||||
factory PagoTipo.fromString(String? raw) {
|
||||
switch (raw) {
|
||||
case 'cuota_mensual':
|
||||
return PagoTipo.cuotaMensual;
|
||||
case 'devolucion':
|
||||
return PagoTipo.devolucion;
|
||||
case 'descuento_retroactivo':
|
||||
return PagoTipo.descuentoRetroactivo;
|
||||
case 'ajuste':
|
||||
return PagoTipo.ajuste;
|
||||
default:
|
||||
return PagoTipo.cuotaMensual;
|
||||
}
|
||||
}
|
||||
|
||||
/// Valor snake_case que espera el backend.
|
||||
String get backendValue {
|
||||
switch (this) {
|
||||
case PagoTipo.cuotaMensual:
|
||||
return 'cuota_mensual';
|
||||
case PagoTipo.devolucion:
|
||||
return 'devolucion';
|
||||
case PagoTipo.descuentoRetroactivo:
|
||||
return 'descuento_retroactivo';
|
||||
case PagoTipo.ajuste:
|
||||
return 'ajuste';
|
||||
}
|
||||
}
|
||||
|
||||
/// Etiqueta legible en español. Vive con la entidad por simplicidad
|
||||
/// (SOMA es app monolingüe).
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case PagoTipo.cuotaMensual:
|
||||
return 'Cuota mensual';
|
||||
case PagoTipo.devolucion:
|
||||
return 'Devolución';
|
||||
case PagoTipo.descuentoRetroactivo:
|
||||
return 'Descuento retroactivo';
|
||||
case PagoTipo.ajuste:
|
||||
return 'Ajuste';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user