Merge pull request 'feat(ui): Optimizacion de componentes con agente' (#70) from feature/sales/66-optimization-components into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m58s
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m58s
Reviewed-on: #70
This commit is contained in:
commit
e5deef9ca7
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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);
|
||||
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)}";
|
||||
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-->
|
||||
<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,10 +298,13 @@
|
||||
{
|
||||
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)
|
||||
{
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
||||
@ -1,51 +1,153 @@
|
||||
window.phMap = {
|
||||
_maps: {},
|
||||
_searchAbortController: null,
|
||||
|
||||
initMap: function (mapId, lat, lng, zoom) {
|
||||
const map = L.map(mapId).setView([lat, lng], zoom);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
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.");
|
||||
// Evitar reinicializar si ya existe
|
||||
if (this._maps[mapId]) {
|
||||
console.warn(`Map ${mapId} already initialized`);
|
||||
return;
|
||||
}
|
||||
|
||||
const lat = parseFloat(data[0].lat);
|
||||
const lon = parseFloat(data[0].lon);
|
||||
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: '© <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;
|
||||
|
||||
mapData.map.setView([lat, lon], 15);
|
||||
mapData.marker.setLatLng([lat, lon]);
|
||||
mapData.marker.getPopup().setContent(address.toUpperCase()).openOn(mapData.map);
|
||||
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]);
|
||||
|
||||
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