service worker

This commit is contained in:
2026-02-22 09:06:44 -06:00
parent 46bf68cb21
commit bec656b105
40 changed files with 115887 additions and 229 deletions

View File

@@ -34,4 +34,21 @@ public interface IMiembroService
/// Gets all active work groups for dropdown
/// </summary>
Task<IEnumerable<(long Id, string Nombre)>> GetGruposTrabajoAsync();
/// <summary>
/// Imports members from a CSV stream
/// </summary>
/// <param name="csvStream">The stream of the CSV file</param>
/// <param name="createdBy">The user creating the members</param>
/// <returns>A tuple with success count and a list of error messages</returns>
Task<(int SuccessCount, List<string> Errors)> ImportarMiembrosAsync(Stream csvStream, string createdBy);
/// <summary>
/// Gets paginated members with optional search
/// </summary>
/// <param name="page">Current page number (1-based)</param>
/// <param name="pageSize">Number of items per page</param>
/// <param name="searchQuery">Optional search query to filter by name</param>
/// <returns>Paginated result with members</returns>
Task<PaginatedViewModel<MiembroViewModel>> GetPaginatedAsync(int page, int pageSize, string? searchQuery = null);
}

View File

@@ -221,4 +221,265 @@ public class MiembroService : IMiembroService
.Select(g => new ValueTuple<long, string>(g.Id, g.Nombre))
.ToListAsync();
}
public async Task<(int SuccessCount, List<string> Errors)> ImportarMiembrosAsync(Stream csvStream, string createdBy)
{
int successCount = 0;
var errors = new List<string>();
int rowNumber = 1; // 1-based, starting at header
using var reader = new StreamReader(csvStream);
// Read valid groups for validation
var validGroupIds = await _context.GruposTrabajo
.Where(g => g.Activo)
.Select(g => g.Id)
.ToListAsync();
var validGroupIdsSet = new HashSet<long>(validGroupIds);
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (string.IsNullOrWhiteSpace(line)) continue;
rowNumber++;
// Skip header if it looks like one (simple check or just assume first row is header)
// The prompt implies a specific format, we'll assume the first row IS the header based on standard CSV practices,
// but if the user provides a file without header it might be an issue.
// However, usually "loading a csv" implies a header.
// I'll skip the first row (header) in the loop logic by adding a check.
if (rowNumber == 2) continue; // Skip header row (rowNumber started at 1, so first ReadLine is row 1 (header), loop increments to 2)
// Wait, if I increment rowNumber AFTER reading, then:
// Start: rowNumber=1.
// ReadLine (Header). rowNumber becomes 2.
// So if rowNumber == 2, it means we just read the header. Correct.
// Parse CSV line
var values = ParseCsvLine(line);
// Expected columns:
// 0: Nombres
// 1: Apellidos
// 2: Fecha Nacimiento
// 3: Fecha Ingreso Congregacion
// 4: Telefono
// 5: Telefono Emergencia
// 6: Direccion
// 7: Grupo de trabajo (ID)
// 8: Bautizado en El Espiritu Santo (Si/No or True/False)
// 9: Activo (Si/No or True/False)
if (values.Count < 10)
{
errors.Add($"Fila {rowNumber}: Número de columnas insuficiente. Se esperaban 10, se encontraron {values.Count}.");
continue;
}
try
{
// Validation and Parsing
var nombres = values[0].Trim();
var apellidos = values[1].Trim();
if (string.IsNullOrEmpty(nombres) || string.IsNullOrEmpty(apellidos))
{
errors.Add($"Fila {rowNumber}: Nombres y Apellidos son obligatorios.");
continue;
}
DateOnly? fechaNacimiento = ParseDate(values[2]);
DateOnly? fechaIngreso = ParseDate(values[3]);
var telefono = values[4].Trim();
var telefonoEmergencia = values[5].Trim();
var direccion = values[6].Trim();
if (!long.TryParse(values[7], out long grupoId))
{
errors.Add($"Fila {rowNumber}: ID de Grupo de trabajo inválido '{values[7]}'.");
continue;
}
if (!validGroupIdsSet.Contains(grupoId))
{
errors.Add($"Fila {rowNumber}: Grupo de trabajo con ID {grupoId} no existe o no está activo.");
continue;
}
bool bautizado = ParseBool(values[8]);
bool activo = ParseBool(values[9]);
// Create Logic
var strategy = _context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var persona = new Persona
{
Nombres = nombres,
Apellidos = apellidos,
FechaNacimiento = fechaNacimiento,
Direccion = string.IsNullOrEmpty(direccion) ? null : direccion,
Telefono = string.IsNullOrEmpty(telefono) ? null : telefono,
Activo = activo,
CreadoEn = DateTime.UtcNow,
ActualizadoEn = DateTime.UtcNow
};
_context.Personas.Add(persona);
await _context.SaveChangesAsync();
var miembro = new Miembro
{
PersonaId = persona.Id,
BautizadoEspirituSanto = bautizado,
FechaIngresoCongregacion = fechaIngreso,
TelefonoEmergencia = string.IsNullOrEmpty(telefonoEmergencia) ? null : telefonoEmergencia,
GrupoTrabajoId = grupoId,
Activo = activo,
CreadoPor = createdBy,
CreadoEn = DateTime.UtcNow,
ActualizadoEn = DateTime.UtcNow
};
_context.Miembros.Add(miembro);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
successCount++;
}
catch (Exception ex)
{
errors.Add($"Fila {rowNumber}: Error al guardar en base de datos: {ex.Message}");
// Transaction rolls back automatically on dispose if not committed
}
});
}
catch (Exception ex)
{
errors.Add($"Fila {rowNumber}: Error inesperado: {ex.Message}");
}
}
return (successCount, errors);
}
private List<string> ParseCsvLine(string line)
{
var values = new List<string>();
bool inQuotes = false;
string currentValue = "";
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (c == '"')
{
inQuotes = !inQuotes;
}
else if (c == ',' && !inQuotes)
{
values.Add(currentValue);
currentValue = "";
}
else
{
currentValue += c;
}
}
values.Add(currentValue);
// Remove surrounding quotes if present
for (int i = 0; i < values.Count; i++)
{
var val = values[i].Trim();
if (val.StartsWith("\"") && val.EndsWith("\"") && val.Length >= 2)
{
values[i] = val.Substring(1, val.Length - 2).Replace("\"\"", "\"");
}
else
{
values[i] = val;
}
}
return values;
}
private DateOnly? ParseDate(string value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
if (DateOnly.TryParse(value, out var date)) return date;
if (DateTime.TryParse(value, out var dt)) return DateOnly.FromDateTime(dt);
return null; // Or throw depending on strictness, currently lenient
}
private bool ParseBool(string value)
{
if (string.IsNullOrWhiteSpace(value)) return false;
var val = value.Trim().ToLower();
return val == "1" || val == "true" || val == "si" || val == "yes" || val == "s" || val == "verdadero";
}
public async Task<PaginatedViewModel<MiembroViewModel>> GetPaginatedAsync(int page, int pageSize, string? searchQuery = null)
{
// Ensure valid page and pageSize
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100; // Max limit
// Start with base query
var query = _context.Miembros
.Include(m => m.Persona)
.Include(m => m.GrupoTrabajo)
.Where(m => !m.Eliminado && m.Activo);
// Apply search filter if provided
if (!string.IsNullOrWhiteSpace(searchQuery))
{
var search = searchQuery.Trim().ToLower();
query = query.Where(m =>
m.Persona.Nombres.ToLower().Contains(search) ||
m.Persona.Apellidos.ToLower().Contains(search) ||
(m.Persona.Nombres + " " + m.Persona.Apellidos).ToLower().Contains(search)
);
}
// Get total count for pagination
var totalItems = await query.CountAsync();
// Get paginated items
var items = await query
.OrderBy(m => m.Persona.Apellidos)
.ThenBy(m => m.Persona.Nombres)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(m => new MiembroViewModel
{
Id = m.Id,
Nombres = m.Persona.Nombres,
Apellidos = m.Persona.Apellidos,
FechaNacimiento = m.Persona.FechaNacimiento,
BautizadoEspirituSanto = m.BautizadoEspirituSanto,
Direccion = m.Persona.Direccion,
FechaIngresoCongregacion = m.FechaIngresoCongregacion,
Telefono = m.Persona.Telefono,
TelefonoEmergencia = m.TelefonoEmergencia,
GrupoTrabajoId = m.GrupoTrabajoId,
GrupoTrabajoNombre = m.GrupoTrabajo != null ? m.GrupoTrabajo.Nombre : null,
Activo = m.Activo,
FotoUrl = m.Persona.FotoUrl
})
.ToListAsync();
return new PaginatedViewModel<MiembroViewModel>
{
Items = items,
CurrentPage = page,
PageSize = pageSize,
TotalItems = totalItems,
SearchQuery = searchQuery
};
}
}