feat(ui): Optimizacion de componentes con agente #70

Merged
leandro merged 1 commits from feature/sales/66-optimization-components into master 2026-06-09 04:03:57 +00:00
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 /Models/obj/Models.csproj.nuget.g.props
/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json /phronCare.API/obj/Debug/net8.0/ApiEndpoints.json
/phronCare.API/.local-chromium/Win64-884014/chrome-win /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 phronCare.UIBlazor.Shared.Services
@using System.Timers
@implements IAsyncDisposable
@inject IJSRuntime JS @inject IJSRuntime JS
@ -10,11 +12,12 @@
{ {
<input @bind="SearchAddress" <input @bind="SearchAddress"
@bind:event="oninput" @bind:event="oninput"
@onchange="OnAddressChanged"
class="form-control form-control-sm" class="form-control form-control-sm"
placeholder="Buscar dirección..." placeholder="Buscar dirección..."
style="width: 250px;" /> style="width: 250px;" />
<button class="btn btn-sm btn-outline-primary" @onclick="CenterByAddress" title="Buscar"> <button class="btn btn-sm btn-outline-primary" @onclick="CenterByAddress" title="Buscar" disabled="@IsSearching">
<i class="fas fa-search-location"></i> <i class="fas @(IsSearching ? "fa-spinner fa-spin" : "fa-search-location")"></i>
</button> </button>
} }
<button class="btn btn-sm btn-outline-secondary" @onclick="OpenGoogleMaps" title="Abrir en Google Maps"> <button class="btn btn-sm btn-outline-secondary" @onclick="OpenGoogleMaps" title="Abrir en Google Maps">
@ -28,11 +31,14 @@
</div> </div>
@code { @code {
private string MapDivId = $"map_{Guid.NewGuid()}"; private string MapDivId = string.Empty;
public static PhMap? CurrentInstance { get; private set; } public static PhMap? CurrentInstance { get; private set; }
private double _latitude = -34.6037; private double _latitude = -34.6037;
private double _longitude = -58.3816; private double _longitude = -58.3816;
private bool _isMapInitialized = false;
private Timer? _searchDebounceTimer;
private bool IsSearching { get; set; } = false;
[Parameter] [Parameter]
public double Latitude public double Latitude
@ -52,22 +58,91 @@
private string SearchAddress { get; set; } = string.Empty; private string SearchAddress { get; set; } = string.Empty;
protected override void OnInitialized()
{
MapDivId = $"map_{Guid.NewGuid()}";
CurrentInstance = this;
}
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender && !_isMapInitialized)
{ {
MapInterop.RegisterInstance(this); // ✅ esto conecta con MapInterop.cs MapInterop.RegisterInstance(this);
await Task.Delay(100); await JS.InvokeVoidAsync("phMap.initMap", MapDivId, _latitude, _longitude, Zoom);
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() private async Task CenterByAddress()
{ {
if (!string.IsNullOrWhiteSpace(SearchAddress)) await JS.InvokeVoidAsync("phMap.searchAddress", MapDivId, SearchAddress); if (!string.IsNullOrWhiteSpace(SearchAddress))
}
private void OpenGoogleMaps()
{ {
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)}"; 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--> <!-- RENDERIZACION DE DATOS-->
<tbody> <tbody>
<!-- RENDERIZAR DATOS POR PAGINA--> <!-- 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> <tr>
<!-- RENDERIZAR COLUMNA DE SELECCION--> <!-- RENDERIZAR COLUMNA DE SELECCION-->
@if (RenderSelect) @if (RenderSelect)
{ {
<td> <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> </td>
} }
<!-- RENDERIZAR COLUMNAS DE DATOS--> <!-- RENDERIZAR COLUMNAS DE DATOS-->
@ -232,7 +238,7 @@
public bool SelectAll { get; set; } = false; public bool SelectAll { get; set; } = false;
public Dictionary<string, bool> SortDirections { get; set; } = new Dictionary<string, bool>(); 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<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(); private List<Dictionary<string, object>> SelectRows => GetSelectedRows();
public event Action<List<Dictionary<string, object>>>? OnGetSelectedRows; public event Action<List<Dictionary<string, object>>>? OnGetSelectedRows;
#endregion #endregion
@ -292,13 +298,16 @@
{ {
foreach (var item in Data) foreach (var item in Data)
{ {
string? rowId = item[SelectionField]?.ToString(); if (item.TryGetValue(SelectionField, out var idObj) && idObj is not null)
if (rowId != null && !SelectedRowIndexes.Contains(rowId)) {
var rowId = idObj.ToString();
if (rowId is not null && !SelectedRowIndexes.Contains(rowId))
{ {
SelectedRowIndexes.Add(rowId); SelectedRowIndexes.Add(rowId);
} }
} }
} }
}
else else
{ {
SelectedRowIndexes.Clear(); SelectedRowIndexes.Clear();
@ -338,8 +347,22 @@
} }
private void PaginateData() 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; int startIndex = (CurrentPage - 1) * RowsPerPage;
PaginatedData = Data.Skip(startIndex).Take(RowsPerPage).ToList(); PaginatedData = source.Skip(startIndex).Take(RowsPerPage).ToList();
} }
private void ChangePage(int pageNumber) private void ChangePage(int pageNumber)
{ {

View File

@ -1,51 +1,153 @@
window.phMap = { window.phMap = {
_maps: {},
_searchAbortController: null,
initMap: function (mapId, lat, lng, zoom) { 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', { 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); }).addTo(map);
const marker = L.marker([lat, lng]).addTo(map) const marker = L.marker([lat, lng], {
.bindPopup('Ubicación inicial') draggable: false,
keyboard: false
}).addTo(map)
.bindPopup(`<strong>Lat:</strong> ${lat.toFixed(5)}<br><strong>Lng:</strong> ${lng.toFixed(5)}`)
.openPopup(); .openPopup();
// Guardamos el mapa y el marcador this._maps[mapId] = {
window._phMaps = window._phMaps || {}; map,
window._phMaps[mapId] = { map, marker }; 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 newLat = e.latlng.lat;
const newLng = e.latlng.lng; const newLng = e.latlng.lng;
const mapData = this._maps[mapId];
// Mover el marcador existente if (!mapData) return;
window._phMaps[mapId].marker.setLatLng([newLat, newLng]);
window._phMaps[mapId].marker.getPopup().setContent('Nueva ubicación').openOn(map);
// Llamar al método en Blazor mapData.marker.setLatLng([newLat, newLng]);
DotNet.invokeMethodAsync('phronCare.UIBlazor', 'NotifyLocationChanged', 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) { 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(); const data = await response.json();
if (data.length === 0) { if (!data || data.length === 0) {
alert("Dirección no encontrada."); console.warn(`Address not found: ${address}`);
return; return;
} }
const lat = parseFloat(data[0].lat); const lat = parseFloat(data[0].lat);
const lon = parseFloat(data[0].lon); const lon = parseFloat(data[0].lon);
const mapData = window._phMaps[mapId]; if (isNaN(lat) || isNaN(lon)) {
if (!mapData) return; console.error('Invalid coordinates from API');
return;
}
mapData.map.setView([lat, lon], 15); mapData.map.setView([lat, lon], 15);
mapData.marker.setLatLng([lat, lon]); 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);
}
} }
}; };