Alinear BD local con esquema de produccion, login offline y publicacion portable

- Modelos renombrados para coincidir con columnas de MariaDB (ApPaterno/ApMaterno, HoraSalida, FechaFin, IdAsistenciaProfesor)
- Agregado campo IdAlumno a DetalleContrato
- Seed data con IDs reales de produccion (16451, 16493, profesor 15668563-1)
- Login offline: solo valida RUT contra BD local, sin contraseña
- ConnectivityService: checkea internet al inicio, bloquea si hay conexion
- ViewLocator corregido: busca tipos en todos los ensamblados
- Todos los ViewModels heredan de ViewModelBase
- Publicacion self-contained para win-x64, linux-x64, osx-x64, osx-arm64
- Agregado .gitignore, removidos bin/obj/publish del tracking
This commit is contained in:
Nurfog committed 2026-08-19 16:43:52 -04:00
1 parent 9068122b6b
commit fb275508ee
19 files changed
+196 -77

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
## Build results
[Dd]ebug/
[Rr]elease/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
## Publish output
publish/
## NuGet
*.nupkg
*.snupkg
.nuget/
**/[Pp]ackages/*
!**/[Pp]ackages/build/
## Visual Studio
.vs/
*.suo
*.user
*.userosscache
*.sln.docstates
*.userprefs
## Rider
.idea/
*.sln.iml
## VS Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
## macOS
.DS_Store
*.dmg
## Windows
Thumbs.db
ehthumbs.db
Desktop.ini
## Linux
*~
## SQLite databases
*.db
*.db-shm
*.db-wal
## Resharper
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
## NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
## BenchmarkDotNet
BenchmarkDotNet.Artifacts/
+1
View File
@@ -29,6 +29,7 @@ public partial class App : Application
services.AddSingleton<AsistenciaRepository>();
services.AddSingleton<AuthService>();
services.AddSingleton<AsistenciaService>();
services.AddSingleton<ConnectivityService>();
services.AddSingleton<MainWindowViewModel>();
Services = services.BuildServiceProvider();
+3
View File
@@ -5,6 +5,9 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationManifest>app.manifest</ApplicationManifest>
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
<SelfContained>true</SelfContained>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
</PropertyGroup>
<ItemGroup>
+24 -27
View File
@@ -58,48 +58,48 @@ public class AppDbContext : DbContext
modelBuilder.Entity<Profesor>().HasData(
new Profesor
{
RutPasaporte = "12345678-9",
RutPasaporte = "15668563-1",
Nombre = "Juan",
Paterno = "Perez",
Materno = "Gonzalez",
ApPaterno = "Perez",
ApMaterno = "Gonzalez",
Email = "juan.perez@norteamericano.cl",
Fono = "+56912345678",
Direccion = "Av. Providencia 1234, Santiago",
Clave = "",
CrearClave = true
CrearClave = true,
Activo = true
}
);
modelBuilder.Entity<Curso>().HasData(
new Curso { IdCursos = 1, NombreCurso = "English Basic A1" },
new Curso { IdCursos = 2, NombreCurso = "English Intermediate B1" },
new Curso { IdCursos = 3, NombreCurso = "English Advanced C1" }
new Curso { IdCursos = 16451, NombreCurso = "English Basic A1" },
new Curso { IdCursos = 16493, NombreCurso = "English Intermediate B1" }
);
modelBuilder.Entity<CursoAbierto>().HasData(
new CursoAbierto
{
IdCursoAbierto = 1,
IdCursos = 1,
IdCursoAbierto = 16451,
IdCursos = 16451,
Sala = "Sala 101",
Sede = "Providencia",
HoraInicio = "09:00",
HoraFin = "10:30",
FechaInicio = new DateTime(2026, 3, 1),
FechaTermino = new DateTime(2026, 7, 31),
ProfesorRun = "12345678-9"
FechaFin = new DateTime(2026, 7, 31),
ProfesorRun = "15668563-1"
},
new CursoAbierto
{
IdCursoAbierto = 2,
IdCursos = 2,
IdCursoAbierto = 16493,
IdCursos = 16493,
Sala = "Sala 202",
Sede = "Las Condes",
HoraInicio = "14:00",
HoraFin = "15:30",
FechaInicio = new DateTime(2026, 3, 1),
FechaTermino = new DateTime(2026, 7, 31),
ProfesorRun = "12345678-9"
FechaFin = new DateTime(2026, 7, 31),
ProfesorRun = "15668563-1"
}
);
@@ -107,32 +107,29 @@ public class AppDbContext : DbContext
new DetalleContrato
{
IdDetalleContrato = 1,
IdCursoAbierto = 1,
IdCursoAbierto = 16451,
IdAlumno = "11111111-1",
AP_Paterno = "Garcia",
AP_Materno = "Lopez",
Nombres = "Maria",
Email = "maria.garcia@email.com",
Telefono = "+56998765432"
Nombres = "Maria"
},
new DetalleContrato
{
IdDetalleContrato = 2,
IdCursoAbierto = 1,
IdCursoAbierto = 16451,
IdAlumno = "22222222-2",
AP_Paterno = "Rodriguez",
AP_Materno = "Soto",
Nombres = "Carlos",
Email = "carlos.rodriguez@email.com",
Telefono = "+56987654321"
Nombres = "Carlos"
},
new DetalleContrato
{
IdDetalleContrato = 3,
IdCursoAbierto = 2,
IdCursoAbierto = 16493,
IdAlumno = "33333333-3",
AP_Paterno = "Martinez",
AP_Materno = "Fernandez",
Nombres = "Ana",
Email = "ana.martinez@email.com",
Telefono = "+56976543210"
Nombres = "Ana"
}
);
}
@@ -41,7 +41,7 @@ public class AsistenciaRepository
var sesion = await _context.AsistenciasProfesor.FindAsync(idSesion);
if (sesion == null) return false;
sesion.HoraFin = horaFin;
sesion.HoraSalida = horaFin;
sesion.Estado = "Finalizada";
await _context.SaveChangesAsync();
return true;
+3 -3
View File
@@ -5,7 +5,7 @@ namespace Asistencia.Models;
public class AsistenciaProfesor
{
[Key]
public int Id { get; set; }
public int IdAsistenciaProfesor { get; set; }
public int IdCursoAbierto { get; set; }
@@ -18,12 +18,12 @@ public class AsistenciaProfesor
public string HoraInicio { get; set; } = string.Empty;
[MaxLength(10)]
public string HoraFin { get; set; } = string.Empty;
public string HoraSalida { get; set; } = string.Empty;
[MaxLength(20)]
public string Estado { get; set; } = "Activa";
public bool Temporal { get; set; }
public int Temporal { get; set; }
public CursoAbierto? CursoAbierto { get; set; }
}
+1 -1
View File
@@ -23,7 +23,7 @@ public class CursoAbierto
public DateTime FechaInicio { get; set; }
public DateTime FechaTermino { get; set; }
public DateTime FechaFin { get; set; }
[MaxLength(20)]
public string ProfesorRun { get; set; } = string.Empty;
+3 -6
View File
@@ -9,6 +9,9 @@ public class DetalleContrato
public int IdCursoAbierto { get; set; }
[MaxLength(30)]
public string IdAlumno { get; set; } = string.Empty;
[MaxLength(100)]
public string AP_Paterno { get; set; } = string.Empty;
@@ -18,12 +21,6 @@ public class DetalleContrato
[MaxLength(100)]
public string Nombres { get; set; } = string.Empty;
[MaxLength(200)]
public string Email { get; set; } = string.Empty;
[MaxLength(20)]
public string Telefono { get; set; } = string.Empty;
public CursoAbierto? CursoAbierto { get; set; }
public string NombreCompleto => $"{Nombres} {AP_Paterno} {AP_Materno}".Trim();
+5 -3
View File
@@ -12,10 +12,10 @@ public class Profesor
public string Nombre { get; set; } = string.Empty;
[MaxLength(100)]
public string Paterno { get; set; } = string.Empty;
public string ApPaterno { get; set; } = string.Empty;
[MaxLength(100)]
public string Materno { get; set; } = string.Empty;
public string ApMaterno { get; set; } = string.Empty;
[MaxLength(200)]
public string Email { get; set; } = string.Empty;
@@ -31,5 +31,7 @@ public class Profesor
public bool CrearClave { get; set; }
public string NombreCompleto => $"{Nombre} {Paterno} {Materno}".Trim();
public bool Activo { get; set; } = true;
public string NombreCompleto => $"{Nombre} {ApPaterno} {ApMaterno}".Trim();
}
+3 -10
View File
@@ -17,21 +17,14 @@ public class AuthService
public async Task<(bool Success, string Message)> LoginAsync(string rut, string password)
{
if (string.IsNullOrWhiteSpace(rut) || string.IsNullOrWhiteSpace(password))
return (false, "Ingrese usuario y contraseña");
if (string.IsNullOrWhiteSpace(rut))
return (false, "Ingrese su RUT");
var profesor = await _profesorRepo.BuscarPorRutAsync(rut);
if (profesor == null)
return (false, "Usuario no encontrado");
if (profesor.CrearClave)
return (false, "Debe crear su contraseña primero");
var authenticated = await _profesorRepo.AutenticarAsync(rut, password);
if (authenticated == null)
return (false, "Contraseña incorrecta");
_currentUser = authenticated;
_currentUser = profesor;
return (true, "Inicio de sesión exitoso");
}
@@ -0,0 +1,20 @@
using System.Net.NetworkInformation;
namespace Asistencia.Services;
public class ConnectivityService
{
public async Task<bool> HayConexionAsync()
{
try
{
using var ping = new Ping();
var reply = await ping.SendPingAsync("8.8.8.8", 3000);
return reply.Status == IPStatus.Success;
}
catch
{
return false;
}
}
}
+6 -6
View File
@@ -1,14 +1,12 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Asistencia.ViewModels;
namespace Asistencia;
/// <summary>
/// Given a view model, returns the corresponding view if possible.
/// </summary>
[RequiresUnreferencedCode(
"Default implementation of ViewLocator involves reflection which may be trimmed away.",
Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
@@ -18,15 +16,17 @@ public class ViewLocator : IDataTemplate
{
if (param is null)
return null;
var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name);
var type = AppDomain.CurrentDomain.GetAssemblies()
.Select(a => a.GetType(name))
.FirstOrDefault(t => t != null);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = "Not Found: " + name };
}
+3 -3
View File
@@ -6,7 +6,7 @@ using System.Collections.ObjectModel;
namespace Asistencia.ViewModels;
public partial class AsistenciaViewModel : ObservableObject
public partial class AsistenciaViewModel : ViewModelBase
{
private readonly AsistenciaService _asistenciaService;
private readonly AuthService _authService;
@@ -91,7 +91,7 @@ public partial class AsistenciaViewModel : ObservableObject
var sesion = await _asistenciaService.BuscarSesionActivaAsync(_curso.IdCursoAbierto);
if (sesion != null)
{
await _asistenciaService.FinalizarSesionAsync(sesion.Id);
await _asistenciaService.FinalizarSesionAsync(sesion.IdAsistenciaProfesor);
}
EnSesion = false;
MensajeEstado = "Sesion finalizada";
@@ -121,7 +121,7 @@ public partial class AsistenciaViewModel : ObservableObject
}
}
public partial class AlumnoAsistenciaViewModel : ObservableObject
public partial class AlumnoAsistenciaViewModel : ViewModelBase
{
public int IdDetalleContrato { get; }
public string NombreCompleto { get; }
+1 -1
View File
@@ -6,7 +6,7 @@ using System.Collections.ObjectModel;
namespace Asistencia.ViewModels;
public partial class DashboardViewModel : ObservableObject
public partial class DashboardViewModel : ViewModelBase
{
private readonly AuthService _authService;
private readonly AsistenciaService _asistenciaService;
+1 -1
View File
@@ -4,7 +4,7 @@ using Asistencia.Services;
namespace Asistencia.ViewModels;
public partial class LoginViewModel : ObservableObject
public partial class LoginViewModel : ViewModelBase
{
private readonly AuthService _authService;
private readonly MainWindowViewModel _mainVm;
+28 -3
View File
@@ -8,26 +8,51 @@ using System.Collections.ObjectModel;
namespace Asistencia.ViewModels;
public partial class MainWindowViewModel : ObservableObject
public partial class MainWindowViewModel : ViewModelBase
{
private readonly AuthService _authService;
private readonly AsistenciaService _asistenciaService;
private readonly ConnectivityService _connectivityService;
private object? _currentView;
[ObservableProperty]
private string _currentViewTitle = "Login";
[ObservableProperty]
private bool _conexionDisponible;
[ObservableProperty]
private string _mensajeConexion = string.Empty;
public object? CurrentView
{
get => _currentView;
set => SetProperty(ref _currentView, value);
}
public MainWindowViewModel(AuthService authService, AsistenciaService asistenciaService)
public MainWindowViewModel(AuthService authService, AsistenciaService asistenciaService, ConnectivityService connectivityService)
{
_authService = authService;
_asistenciaService = asistenciaService;
CurrentView = new LoginViewModel(authService, this);
_connectivityService = connectivityService;
_ = VerificarConexionAsync();
}
private async Task VerificarConexionAsync()
{
var hayInternet = await _connectivityService.HayConexionAsync();
ConexionDisponible = hayInternet;
if (hayInternet)
{
MensajeConexion = "Aplicación no disponible con conexión a internet. Desconéctese para usar el sistema.";
CurrentView = null;
}
else
{
MensajeConexion = "Modo offline — Using local database";
CurrentView = new LoginViewModel(_authService, this);
}
}
public void NavigateToLogin()
+1 -1
View File
@@ -4,7 +4,7 @@ using Asistencia.Services;
namespace Asistencia.ViewModels;
public partial class PerfilViewModel : ObservableObject
public partial class PerfilViewModel : ViewModelBase
{
private readonly AuthService _authService;
private readonly MainWindowViewModel _mainVm;
+3 -10
View File
@@ -20,24 +20,17 @@
Foreground="{DynamicResource PrimaryNavy}"
HorizontalAlignment="Center" />
<TextBlock Text="Inicie sesion con su cuenta"
<TextBlock Text="Modo offline — Ingrese su RUT"
FontSize="14" Foreground="{DynamicResource TextSecondary}"
HorizontalAlignment="Center" Margin="0,0,0,24" />
<TextBlock Text="RUT" FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBox Watermark="Ej: 12345678-9"
<TextBox PlaceholderText="Ej: 12345678-9"
Text="{Binding Rut}"
FontSize="14" Margin="0,0,0,8" />
<TextBlock Text="Contrasena" FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBox Watermark="Ingrese su contrasena"
Text="{Binding Password}"
PasswordChar="*"
FontSize="14" Margin="0,0,0,16" />
<Button Content="Iniciar Sesion"
<Button Content="Ingresar"
Command="{Binding LoginCommand}"
Classes="primary"
HorizontalAlignment="Stretch"
+18 -1
View File
@@ -17,7 +17,24 @@
<vm:MainWindowViewModel />
</Design.DataContext>
<Panel>
<Panel Background="{DynamicResource BackgroundSecondary}">
<ContentControl Content="{Binding CurrentView}" />
<Border IsVisible="{Binding ConexionDisponible}"
HorizontalAlignment="Center" VerticalAlignment="Center"
CornerRadius="16" Padding="48" Width="420"
Background="#FFF3CD" BorderBrush="#FFC107" BorderThickness="1">
<StackPanel Spacing="16" HorizontalAlignment="Center">
<TextBlock Text="Sin acceso al sistema"
FontSize="24" FontWeight="Bold"
Foreground="#856404"
HorizontalAlignment="Center" />
<TextBlock Text="{Binding MensajeConexion}"
FontSize="14" TextWrapping="Wrap"
Foreground="#856404"
HorizontalAlignment="Center"
TextAlignment="Center" />
</StackPanel>
</Border>
</Panel>
</Window>