feat(ui): Optimizacion de componentes con agente
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 9m3s

This commit is contained in:
Leandro Hernan Rojas 2026-06-09 00:59:58 -03:00
parent ec38e44086
commit 0639685c79
4 changed files with 259 additions and 58 deletions

1
.gitignore vendored
View File

@ -428,3 +428,4 @@ FodyWeavers.xsd
/Models/obj/Models.csproj.nuget.g.props
/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json
/phronCare.API/.local-chromium/Win64-884014/chrome-win
/.agents/skills/analyzing-dotnet-performance

View File

@ -1,4 +1,6 @@
@using phronCare.UIBlazor.Shared.Services
@using System.Timers
@implements IAsyncDisposable
@inject IJSRuntime JS
@ -10,11 +12,12 @@
{
<input @bind="SearchAddress"
@bind:event="oninput"
@onchange="OnAddressChanged"
class="form-control form-control-sm"
placeholder="Buscar dirección..."
style="width: 250px;" />
<button class="btn btn-sm btn-outline-primary" @onclick="CenterByAddress" title="Buscar">
<i class="fas fa-search-location"></i>
<button class="btn btn-sm btn-outline-primary" @onclick="CenterByAddress" title="Buscar" disabled="@IsSearching">
<i class="fas @(IsSearching ? "fa-spinner fa-spin" : "fa-search-location")"></i>
</button>
}
<button class="btn btn-sm btn-outline-secondary" @onclick="OpenGoogleMaps" title="Abrir en Google Maps">
@ -28,11 +31,14 @@
</div>
@code {
private string MapDivId = $"map_{Guid.NewGuid()}";
private string MapDivId = string.Empty;
public static PhMap? CurrentInstance { get; private set; }
private double _latitude = -34.6037;
private double _longitude = -58.3816;
private bool _isMapInitialized = false;
private Timer? _searchDebounceTimer;
private bool IsSearching { get; set; } = false;
[Parameter]
public double Latitude
@ -52,22 +58,91 @@
private string SearchAddress { get; set; } = string.Empty;
protected override void OnInitialized()
{
MapDivId = $"map_{Guid.NewGuid()}";
CurrentInstance = this;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
if (firstRender && !_isMapInitialized)
{
MapInterop.RegisterInstance(this); // ✅ esto conecta con MapInterop.cs
await Task.Delay(100);
await JS.InvokeVoidAsync("phMap.initMap", MapDivId, Latitude, Longitude, Zoom);
MapInterop.RegisterInstance(this);
await JS.InvokeVoidAsync("phMap.initMap", MapDivId, _latitude, _longitude, Zoom);
_isMapInitialized = true;
}
}
protected override async Task OnParametersSetAsync()
{
if (_isMapInitialized && (_latitude != Latitude || _longitude != Longitude))
{
_latitude = Latitude;
_longitude = Longitude;
await JS.InvokeVoidAsync("phMap.updateLocation", MapDivId, _latitude, _longitude);
}
}
private void OnAddressChanged(ChangeEventArgs e)
{
_searchDebounceTimer?.Stop();
_searchDebounceTimer?.Dispose();
_searchDebounceTimer = new Timer(500);
_searchDebounceTimer.Elapsed += async (s, e) =>
{
await CenterByAddress();
_searchDebounceTimer?.Stop();
};
_searchDebounceTimer.AutoReset = false;
_searchDebounceTimer.Start();
}
private async Task CenterByAddress()
{
if (!string.IsNullOrWhiteSpace(SearchAddress)) await JS.InvokeVoidAsync("phMap.searchAddress", MapDivId, SearchAddress);
}
private void OpenGoogleMaps()
if (!string.IsNullOrWhiteSpace(SearchAddress))
{
IsSearching = true;
try
{
await JS.InvokeVoidAsync("phMap.searchAddress", MapDivId, SearchAddress);
}
finally
{
IsSearching = false;
}
}
}
private async Task OpenGoogleMaps()
{
if (!IsValidLocation(_latitude, _longitude))
return;
var url = $"https://www.google.com/maps?q={_latitude.ToString(System.Globalization.CultureInfo.InvariantCulture)},{_longitude.ToString(System.Globalization.CultureInfo.InvariantCulture)}";
JS.InvokeVoidAsync("window.open", url, "_blank");
await JS.InvokeVoidAsync("window.open", url, "_blank");
}
private bool IsValidLocation(double lat, double lng)
{
return lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
_searchDebounceTimer?.Stop();
_searchDebounceTimer?.Dispose();
if (_isMapInitialized)
{
try
{
await JS.InvokeVoidAsync("phMap.destroyMap", MapDivId);
}
catch { }
}
CurrentInstance = null;
}
}

View File

@ -109,15 +109,21 @@
<!-- RENDERIZACION DE DATOS-->
<tbody>
<!-- RENDERIZAR DATOS POR PAGINA-->
@foreach (var item in PaginatedData.Where(row => string.IsNullOrWhiteSpace(SearchTerm) || Columns.Any(col => row[col]?.ToString()?.IndexOf(SearchTerm, StringComparison.OrdinalIgnoreCase) >= 0)))
@foreach (var item in PaginatedData)
{
int index = PaginatedData.IndexOf(item);
<tr>
<!-- RENDERIZAR COLUMNA DE SELECCION-->
@if (RenderSelect)
{
<td>
<input type="checkbox" checked="@SelectedRowIndexes.Contains(item[SelectionField].ToString()??string.Empty)" @onclick="() => ToggleRowSelection(item[SelectionField].ToString()??string.Empty)" />
@{
string rowId = string.Empty;
if (item.TryGetValue(SelectionField, out var idObj) && idObj is not null)
{
rowId = idObj.ToString() ?? string.Empty;
}
}
<input type="checkbox" checked="@SelectedRowIndexes.Contains(rowId)" @onclick="() => ToggleRowSelection(rowId)" />
</td>
}
<!-- RENDERIZAR COLUMNAS DE DATOS-->
@ -232,7 +238,7 @@
public bool SelectAll { get; set; } = false;
public Dictionary<string, bool> SortDirections { get; set; } = new Dictionary<string, bool>();
private List<Dictionary<string, object>> PaginatedData { get; set; } = new List<Dictionary<string, object>>();
private List<string> SelectedRowIndexes = new List<string>();
private HashSet<string> SelectedRowIndexes = new HashSet<string>();
private List<Dictionary<string, object>> SelectRows => GetSelectedRows();
public event Action<List<Dictionary<string, object>>>? OnGetSelectedRows;
#endregion
@ -292,13 +298,16 @@
{
foreach (var item in Data)
{
string? rowId = item[SelectionField]?.ToString();
if (rowId != null && !SelectedRowIndexes.Contains(rowId))
if (item.TryGetValue(SelectionField, out var idObj) && idObj is not null)
{
var rowId = idObj.ToString();
if (rowId is not null && !SelectedRowIndexes.Contains(rowId))
{
SelectedRowIndexes.Add(rowId);
}
}
}
}
else
{
SelectedRowIndexes.Clear();
@ -338,8 +347,22 @@
}
private void PaginateData()
{
IEnumerable<Dictionary<string, object>> source = Data ?? Enumerable.Empty<Dictionary<string, object>>();
if (!string.IsNullOrWhiteSpace(SearchTerm) && Columns?.Count > 0)
{
var term = SearchTerm;
source = source.Where(row => Columns.Any(col => {
if (!row.TryGetValue(col, out var val) || val is null) return false;
return val.ToString().IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0;
}));
}
if (!string.IsNullOrEmpty(SortedColumn))
{
bool asc = SortDirections.ContainsKey(SortedColumn) ? SortDirections[SortedColumn] : true;
source = asc ? source.OrderBy(_ => _[SortedColumn]) : source.OrderByDescending(_ => _[SortedColumn]);
}
int startIndex = (CurrentPage - 1) * RowsPerPage;
PaginatedData = Data.Skip(startIndex).Take(RowsPerPage).ToList();
PaginatedData = source.Skip(startIndex).Take(RowsPerPage).ToList();
}
private void ChangePage(int pageNumber)
{

View File

@ -1,51 +1,153 @@
window.phMap = {
_maps: {},
_searchAbortController: null,
initMap: function (mapId, lat, lng, zoom) {
const map = L.map(mapId).setView([lat, lng], zoom);
// Evitar reinicializar si ya existe
if (this._maps[mapId]) {
console.warn(`Map ${mapId} already initialized`);
return;
}
try {
const map = L.map(mapId, {
zoomControl: true,
attributionControl: true
}).setView([lat, lng], zoom);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors'
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19,
crossOrigin: true
}).addTo(map);
const marker = L.marker([lat, lng]).addTo(map)
.bindPopup('Ubicación inicial')
const marker = L.marker([lat, lng], {
draggable: false,
keyboard: false
}).addTo(map)
.bindPopup(`<strong>Lat:</strong> ${lat.toFixed(5)}<br><strong>Lng:</strong> ${lng.toFixed(5)}`)
.openPopup();
// Guardamos el mapa y el marcador
window._phMaps = window._phMaps || {};
window._phMaps[mapId] = { map, marker };
this._maps[mapId] = {
map,
marker,
clickHandler: this._createClickHandler.bind(this, mapId)
};
map.on('click', function (e) {
// Agregar event listener para clicks en el mapa
map.on('click', this._maps[mapId].clickHandler);
// Resizable map
map.invalidateSize();
} catch (error) {
console.error(`Error initializing map ${mapId}:`, error);
}
},
_createClickHandler: function (mapId, e) {
const newLat = e.latlng.lat;
const newLng = e.latlng.lng;
const mapData = this._maps[mapId];
// Mover el marcador existente
window._phMaps[mapId].marker.setLatLng([newLat, newLng]);
window._phMaps[mapId].marker.getPopup().setContent('Nueva ubicación').openOn(map);
if (!mapData) return;
// Llamar al método en Blazor
DotNet.invokeMethodAsync('phronCare.UIBlazor', 'NotifyLocationChanged', newLat, newLng);
});
mapData.marker.setLatLng([newLat, newLng]);
mapData.marker.setPopupContent(`<strong>Lat:</strong> ${newLat.toFixed(5)}<br><strong>Lng:</strong> ${newLng.toFixed(5)}`);
mapData.marker.getPopup().setLatLng([newLat, newLng]);
if (typeof DotNet !== 'undefined' && DotNet.invokeMethodAsync) {
DotNet.invokeMethodAsync('phronCare.UIBlazor', 'NotifyLocationChanged', newLat, newLng)
.catch(error => console.error('Error invoking Blazor method NotifyLocationChanged:', error));
} else {
console.warn('DotNet interop not available');
}
},
updateLocation: function (mapId, lat, lng) {
const mapData = this._maps[mapId];
if (!mapData) {
console.warn(`Map ${mapId} not found`);
return;
}
mapData.map.setView([lat, lng], mapData.map.getZoom());
mapData.marker.setLatLng([lat, lng]);
mapData.marker.setPopupContent(`<strong>Lat:</strong> ${lat.toFixed(5)}<br><strong>Lng:</strong> ${lng.toFixed(5)}`);
},
searchAddress: async function (mapId, address) {
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`);
const mapData = this._maps[mapId];
if (!mapData) {
console.warn(`Map ${mapId} not found`);
return;
}
// Cancelar búsquedas anteriores
if (this._searchAbortController) {
this._searchAbortController.abort();
}
this._searchAbortController = new AbortController();
try {
const response = await fetch(
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`,
{
signal: this._searchAbortController.signal,
headers: {
'Accept': 'application/json'
}
}
);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
if (data.length === 0) {
alert("Dirección no encontrada.");
if (!data || data.length === 0) {
console.warn(`Address not found: ${address}`);
return;
}
const lat = parseFloat(data[0].lat);
const lon = parseFloat(data[0].lon);
const mapData = window._phMaps[mapId];
if (!mapData) return;
if (isNaN(lat) || isNaN(lon)) {
console.error('Invalid coordinates from API');
return;
}
mapData.map.setView([lat, lon], 15);
mapData.marker.setLatLng([lat, lon]);
mapData.marker.getPopup().setContent(address.toUpperCase()).openOn(mapData.map);
mapData.marker.setPopupContent(`<strong>${address}</strong><br><strong>Lat:</strong> ${lat.toFixed(5)}<br><strong>Lng:</strong> ${lon.toFixed(5)}`);
DotNet.invokeMethodAsync('phronCare.UIBlazor', 'NotifyLocationChanged', lat, lon);
if (typeof DotNet !== 'undefined' && DotNet.invokeMethodAsync) {
DotNet.invokeMethodAsync('phronCare.UIBlazor', 'NotifyLocationChanged', lat, lon)
.catch(error => console.error('Error invoking Blazor method NotifyLocationChanged:', error));
} else {
console.warn('DotNet interop not available');
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('Search cancelled');
} else {
console.error('Search error:', error);
}
}
},
destroyMap: function (mapId) {
const mapData = this._maps[mapId];
if (!mapData) return;
try {
mapData.map.off('click', mapData.clickHandler);
mapData.map.remove();
delete this._maps[mapId];
} catch (error) {
console.error(`Error destroying map ${mapId}:`, error);
}
}
};