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
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 9m3s
This commit is contained in:
parent
ec38e44086
commit
0639685c79
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||||
|
|||||||
@ -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))
|
||||||
|
{
|
||||||
|
IsSearching = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("phMap.searchAddress", MapDivId, SearchAddress);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsSearching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private void OpenGoogleMaps()
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,10 +298,13 @@
|
|||||||
{
|
{
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
SelectedRowIndexes.Add(rowId);
|
var rowId = idObj.ToString();
|
||||||
|
if (rowId is not null && !SelectedRowIndexes.Contains(rowId))
|
||||||
|
{
|
||||||
|
SelectedRowIndexes.Add(rowId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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]) {
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
console.warn(`Map ${mapId} already initialized`);
|
||||||
attribution: '© OpenStreetMap contributors'
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
const marker = L.marker([lat, lng]).addTo(map)
|
|
||||||
.bindPopup('Ubicación inicial')
|
|
||||||
.openPopup();
|
|
||||||
|
|
||||||
// Guardamos el mapa y el marcador
|
|
||||||
window._phMaps = window._phMaps || {};
|
|
||||||
window._phMaps[mapId] = { map, marker };
|
|
||||||
|
|
||||||
map.on('click', function (e) {
|
|
||||||
const newLat = e.latlng.lat;
|
|
||||||
const newLng = e.latlng.lng;
|
|
||||||
|
|
||||||
// Mover el marcador existente
|
|
||||||
window._phMaps[mapId].marker.setLatLng([newLat, newLng]);
|
|
||||||
window._phMaps[mapId].marker.getPopup().setContent('Nueva ubicación').openOn(map);
|
|
||||||
|
|
||||||
// Llamar al método en Blazor
|
|
||||||
DotNet.invokeMethodAsync('phronCare.UIBlazor', 'NotifyLocationChanged', newLat, newLng);
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
searchAddress: async function (mapId, address) {
|
|
||||||
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
alert("Dirección no encontrada.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lat = parseFloat(data[0].lat);
|
try {
|
||||||
const lon = parseFloat(data[0].lon);
|
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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||||
|
maxZoom: 19,
|
||||||
|
crossOrigin: true
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
this._maps[mapId] = {
|
||||||
|
map,
|
||||||
|
marker,
|
||||||
|
clickHandler: this._createClickHandler.bind(this, mapId)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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];
|
||||||
|
|
||||||
const mapData = window._phMaps[mapId];
|
|
||||||
if (!mapData) return;
|
if (!mapData) return;
|
||||||
|
|
||||||
mapData.map.setView([lat, lon], 15);
|
mapData.marker.setLatLng([newLat, newLng]);
|
||||||
mapData.marker.setLatLng([lat, lon]);
|
mapData.marker.setPopupContent(`<strong>Lat:</strong> ${newLat.toFixed(5)}<br><strong>Lng:</strong> ${newLng.toFixed(5)}`);
|
||||||
mapData.marker.getPopup().setContent(address.toUpperCase()).openOn(mapData.map);
|
mapData.marker.getPopup().setLatLng([newLat, newLng]);
|
||||||
|
|
||||||
DotNet.invokeMethodAsync('phronCare.UIBlazor', 'NotifyLocationChanged', lat, lon);
|
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 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 || data.length === 0) {
|
||||||
|
console.warn(`Address not found: ${address}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lat = parseFloat(data[0].lat);
|
||||||
|
const lon = parseFloat(data[0].lon);
|
||||||
|
|
||||||
|
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.setPopupContent(`<strong>${address}</strong><br><strong>Lat:</strong> ${lat.toFixed(5)}<br><strong>Lng:</strong> ${lon.toFixed(5)}`);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user