Fix Mapa Error (Sección Sin Datos)
This commit is contained in:
@@ -252,12 +252,26 @@ export const getPanelElectoral = async (eleccionId: number, ambitoId: string | n
|
|||||||
let url = ambitoId
|
let url = ambitoId
|
||||||
? `/elecciones/${eleccionId}/panel/${ambitoId}`
|
? `/elecciones/${eleccionId}/panel/${ambitoId}`
|
||||||
: `/elecciones/${eleccionId}/panel`;
|
: `/elecciones/${eleccionId}/panel`;
|
||||||
|
|
||||||
// Añadimos categoriaId como un query parameter
|
|
||||||
url += `?categoriaId=${categoriaId}`;
|
url += `?categoriaId=${categoriaId}`;
|
||||||
|
|
||||||
const { data } = await apiClient.get(url);
|
try {
|
||||||
return data;
|
const { data } = await apiClient.get(url);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
console.warn(`API devolvió 404 para ${url}. Devolviendo un estado vacío.`);
|
||||||
|
|
||||||
|
// Devolvemos el objeto vacío PERO con la nueva bandera activada
|
||||||
|
return {
|
||||||
|
ambitoNombre: 'Sin Datos',
|
||||||
|
mapaData: [],
|
||||||
|
resultadosPanel: [],
|
||||||
|
estadoRecuento: { participacionPorcentaje: 0, mesasTotalizadasPorcentaje: 0 },
|
||||||
|
sinDatos: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getComposicionNacional = async (eleccionId: number): Promise<ComposicionNacionalData> => {
|
export const getComposicionNacional = async (eleccionId: number): Promise<ComposicionNacionalData> => {
|
||||||
@@ -295,12 +309,12 @@ export const getMunicipiosPorDistrito = async (distritoId: string): Promise<Cata
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getHomeResumen = async (eleccionId: number, distritoId: string, categoriaId: number): Promise<CategoriaResumenHome> => {
|
export const getHomeResumen = async (eleccionId: number, distritoId: string, categoriaId: number): Promise<CategoriaResumenHome> => {
|
||||||
const queryParams = new URLSearchParams({
|
const queryParams = new URLSearchParams({
|
||||||
eleccionId: eleccionId.toString(),
|
eleccionId: eleccionId.toString(),
|
||||||
distritoId: distritoId,
|
distritoId: distritoId,
|
||||||
categoriaId: categoriaId.toString(),
|
categoriaId: categoriaId.toString(),
|
||||||
});
|
});
|
||||||
const url = `/elecciones/home-resumen?${queryParams.toString()}`;
|
const url = `/elecciones/home-resumen?${queryParams.toString()}`;
|
||||||
const { data } = await apiClient.get(url);
|
const { data } = await apiClient.get(url);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -13,9 +13,10 @@ import Select from 'react-select';
|
|||||||
import type { PanelElectoralDto, ResultadoTicker } from '../../../types/types';
|
import type { PanelElectoralDto, ResultadoTicker } from '../../../types/types';
|
||||||
import { FiMap, FiList, FiChevronDown, FiChevronUp } from 'react-icons/fi';
|
import { FiMap, FiList, FiChevronDown, FiChevronUp } from 'react-icons/fi';
|
||||||
import { useMediaQuery } from './hooks/useMediaQuery';
|
import { useMediaQuery } from './hooks/useMediaQuery';
|
||||||
import { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
|
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
|
||||||
import { assetBaseUrl } from '../../../apiService';
|
import { assetBaseUrl } from '../../../apiService';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
// --- COMPONENTE INTERNO PARA LA TARJETA DE RESULTADOS EN MÓVIL ---
|
// --- COMPONENTE INTERNO PARA LA TARJETA DE RESULTADOS EN MÓVIL ---
|
||||||
interface MobileResultsCardProps {
|
interface MobileResultsCardProps {
|
||||||
@@ -31,25 +32,25 @@ const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ','
|
|||||||
// --- SUB-COMPONENTE PARA UNA FILA DE RESULTADO ---
|
// --- SUB-COMPONENTE PARA UNA FILA DE RESULTADO ---
|
||||||
// 2. Todas las props 'className' ahora usan el objeto 'styles'
|
// 2. Todas las props 'className' ahora usan el objeto 'styles'
|
||||||
const ResultRow = ({ partido }: { partido: ResultadoTicker }) => (
|
const ResultRow = ({ partido }: { partido: ResultadoTicker }) => (
|
||||||
<div className={styles.mobileResultRow} style={{ borderLeftColor: partido.color || '#ccc' }}>
|
<div className={styles.mobileResultRow} style={{ borderLeftColor: partido.color || '#ccc' }}>
|
||||||
<div className={styles.mobileResultLogo} style={{ backgroundColor: partido.color || '#e9ecef' }}>
|
<div className={styles.mobileResultLogo} style={{ backgroundColor: partido.color || '#e9ecef' }}>
|
||||||
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={partido.nombre} />
|
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={partido.nombre} />
|
||||||
</div>
|
|
||||||
<div className={styles.mobileResultInfo}>
|
|
||||||
{partido.nombreCandidato ? (
|
|
||||||
<>
|
|
||||||
<span className={styles.mobileResultPartyName}>{partido.nombreCandidato}</span>
|
|
||||||
<span className={styles.mobileResultCandidateName}>{partido.nombreCorto || partido.nombre}</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className={styles.mobileResultPartyName}>{partido.nombreCorto || partido.nombre}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={styles.mobileResultStats}>
|
|
||||||
<strong>{formatPercent(partido.porcentaje)}</strong>
|
|
||||||
<span>{partido.votos.toLocaleString('es-AR')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.mobileResultInfo}>
|
||||||
|
{partido.nombreCandidato ? (
|
||||||
|
<>
|
||||||
|
<span className={styles.mobileResultPartyName}>{partido.nombreCandidato}</span>
|
||||||
|
<span className={styles.mobileResultCandidateName}>{partido.nombreCorto || partido.nombre}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className={styles.mobileResultPartyName}>{partido.nombreCorto || partido.nombre}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.mobileResultStats}>
|
||||||
|
<strong>{formatPercent(partido.porcentaje)}</strong>
|
||||||
|
<span>{partido.votos.toLocaleString('es-AR')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- COMPONENTE REFACTORIZADO PARA LA TARJETA MÓVIL ---
|
// --- COMPONENTE REFACTORIZADO PARA LA TARJETA MÓVIL ---
|
||||||
@@ -64,74 +65,74 @@ interface MobileResultsCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MobileResultsCard = ({
|
const MobileResultsCard = ({
|
||||||
eleccionId, ambitoId, categoriaId, ambitoNombre, ambitoNivel, mobileView, setMobileView
|
eleccionId, ambitoId, categoriaId, ambitoNombre, ambitoNivel, mobileView, setMobileView
|
||||||
}: MobileResultsCardProps) => {
|
}: MobileResultsCardProps) => {
|
||||||
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
const { data } = useSuspenseQuery<PanelElectoralDto>({
|
const { data } = useSuspenseQuery<PanelElectoralDto>({
|
||||||
queryKey: ['panelElectoral', eleccionId, ambitoId, categoriaId],
|
queryKey: ['panelElectoral', eleccionId, ambitoId, categoriaId],
|
||||||
queryFn: () => getPanelElectoral(eleccionId, ambitoId, categoriaId),
|
queryFn: () => getPanelElectoral(eleccionId, ambitoId, categoriaId),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsExpanded(ambitoNivel === 'municipio');
|
setIsExpanded(ambitoNivel === 'municipio');
|
||||||
}, [ambitoNivel]);
|
}, [ambitoNivel]);
|
||||||
|
|
||||||
const topResults = data.resultadosPanel.slice(0, 3);
|
const topResults = data.resultadosPanel.slice(0, 3);
|
||||||
|
|
||||||
if (topResults.length === 0 && ambitoNivel === 'pais') {
|
if (topResults.length === 0 && ambitoNivel === 'pais') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Clases condicionales también se construyen con el objeto 'styles'
|
// 3. Clases condicionales también se construyen con el objeto 'styles'
|
||||||
const cardClasses = [
|
const cardClasses = [
|
||||||
styles.mobileResultsCardContainer,
|
styles.mobileResultsCardContainer,
|
||||||
isExpanded ? styles.expanded : '',
|
isExpanded ? styles.expanded : '',
|
||||||
styles[`view-${mobileView}`]
|
styles[`view-${mobileView}`]
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cardClasses}>
|
<div className={cardClasses}>
|
||||||
{/* Sección Colapsable con Resultados */}
|
{/* Sección Colapsable con Resultados */}
|
||||||
<div className={styles.collapsibleSection}>
|
<div className={styles.collapsibleSection}>
|
||||||
<div className={styles.mobileResultsHeader} onClick={() => setIsExpanded(!isExpanded)}>
|
<div className={styles.mobileResultsHeader} onClick={() => setIsExpanded(!isExpanded)}>
|
||||||
<div className={styles.headerInfo}>
|
<div className={styles.headerInfo}>
|
||||||
<h4>{ambitoNombre}</h4>
|
<h4>{ambitoNombre}</h4>
|
||||||
<span className={styles.headerActionText}>{isExpanded ? 'Ocultar resultados' : 'Ver top 3'}</span>
|
<span className={styles.headerActionText}>{isExpanded ? 'Ocultar resultados' : 'Ver top 3'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.headerToggleIcon}>
|
<div className={styles.headerToggleIcon}>
|
||||||
{isExpanded ? <FiChevronDown /> : <FiChevronUp />}
|
{isExpanded ? <FiChevronDown /> : <FiChevronUp />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className={styles.mobileResultsContent}>
|
|
||||||
{topResults.length > 0 ? (
|
|
||||||
topResults.map(partido => <ResultRow key={partido.id} partido={partido} />)
|
|
||||||
) : (
|
|
||||||
<p className={styles.noResultsText}>No hay resultados para esta selección.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer Fijo con Botones de Navegación */}
|
|
||||||
<div className={styles.mobileCardViewToggle}>
|
|
||||||
<button
|
|
||||||
className={`${styles.toggleBtn} ${mobileView === 'mapa' ? styles.active : ''}`}
|
|
||||||
onClick={() => setMobileView('mapa')}
|
|
||||||
>
|
|
||||||
<FiMap />
|
|
||||||
<span>Mapa</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`${styles.toggleBtn} ${mobileView === 'resultados' ? styles.active : ''}`}
|
|
||||||
onClick={() => setMobileView('resultados')}
|
|
||||||
>
|
|
||||||
<FiList />
|
|
||||||
<span>Detalles</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className={styles.mobileResultsContent}>
|
||||||
|
{topResults.length > 0 ? (
|
||||||
|
topResults.map(partido => <ResultRow key={partido.id} partido={partido} />)
|
||||||
|
) : (
|
||||||
|
<p className={styles.noResultsText}>No hay resultados para esta selección.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Fijo con Botones de Navegación */}
|
||||||
|
<div className={styles.mobileCardViewToggle}>
|
||||||
|
<button
|
||||||
|
className={`${styles.toggleBtn} ${mobileView === 'mapa' ? styles.active : ''}`}
|
||||||
|
onClick={() => setMobileView('mapa')}
|
||||||
|
>
|
||||||
|
<FiMap />
|
||||||
|
<span>Mapa</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.toggleBtn} ${mobileView === 'resultados' ? styles.active : ''}`}
|
||||||
|
onClick={() => setMobileView('resultados')}
|
||||||
|
>
|
||||||
|
<FiList />
|
||||||
|
<span>Detalles</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- WIDGET PRINCIPAL ---
|
// --- WIDGET PRINCIPAL ---
|
||||||
@@ -157,10 +158,22 @@ const PanelContenido = ({ eleccionId, ambitoActual, categoriaId }: { eleccionId:
|
|||||||
queryKey: ['panelElectoral', eleccionId, ambitoActual.id, categoriaId],
|
queryKey: ['panelElectoral', eleccionId, ambitoActual.id, categoriaId],
|
||||||
queryFn: () => getPanelElectoral(eleccionId, ambitoActual.id, categoriaId),
|
queryFn: () => getPanelElectoral(eleccionId, ambitoActual.id, categoriaId),
|
||||||
});
|
});
|
||||||
|
// Si la API devolvió la bandera 'sinDatos', mostramos un mensaje.
|
||||||
|
if (data.sinDatos) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem', textAlign: 'center', color: '#666' }}>
|
||||||
|
<h4>Sin Resultados Detallados</h4>
|
||||||
|
<p>Aún no hay datos disponibles para esta selección.</p>
|
||||||
|
<p>Por favor, intente de nuevo más tarde.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Si no, renderizamos los resultados.
|
||||||
return <PanelResultados resultados={data.resultadosPanel} estadoRecuento={data.estadoRecuento} />;
|
return <PanelResultados resultados={data.resultadosPanel} estadoRecuento={data.estadoRecuento} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => {
|
export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const [ambitoActual, setAmbitoActual] = useState<AmbitoState>({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null });
|
const [ambitoActual, setAmbitoActual] = useState<AmbitoState>({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null });
|
||||||
const [categoriaId, setCategoriaId] = useState<number>(2);
|
const [categoriaId, setCategoriaId] = useState<number>(2);
|
||||||
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
||||||
@@ -168,6 +181,16 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
|
|||||||
const isMobile = useMediaQuery('(max-width: 800px)');
|
const isMobile = useMediaQuery('(max-width: 800px)');
|
||||||
|
|
||||||
const handleAmbitoSelect = (nuevoAmbitoId: string, nuevoNivel: 'provincia' | 'municipio', nuevoNombre: string) => {
|
const handleAmbitoSelect = (nuevoAmbitoId: string, nuevoNivel: 'provincia' | 'municipio', nuevoNombre: string) => {
|
||||||
|
if (nuevoNivel === 'municipio') {
|
||||||
|
toast.promise(
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['panelElectoral', eleccionId, nuevoAmbitoId, categoriaId] }),
|
||||||
|
{
|
||||||
|
loading: `Cargando datos de ${nuevoNombre}...`,
|
||||||
|
success: <b>Datos cargados</b>,
|
||||||
|
error: <b>No se pudieron cargar los datos.</b>,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
setAmbitoActual(prev => ({
|
setAmbitoActual(prev => ({
|
||||||
id: nuevoAmbitoId,
|
id: nuevoAmbitoId,
|
||||||
nivel: nuevoNivel,
|
nivel: nuevoNivel,
|
||||||
@@ -208,35 +231,37 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.panelNacionalContainer}>
|
<div className={styles.panelNacionalContainer}>
|
||||||
<Toaster containerClassName={styles.widgetToasterContainer} />
|
<Toaster
|
||||||
|
position="bottom-center"
|
||||||
|
containerClassName={styles.widgetToasterContainer}
|
||||||
|
/>
|
||||||
<header className={styles.panelHeader}>
|
<header className={styles.panelHeader}>
|
||||||
<div className={styles.headerTopRow}>
|
<div className={styles.headerTopRow}>
|
||||||
<Select
|
<Select
|
||||||
options={CATEGORIAS_NACIONALES}
|
options={CATEGORIAS_NACIONALES}
|
||||||
value={selectedCategoria}
|
value={selectedCategoria}
|
||||||
onChange={(option) => option && setCategoriaId(option.value)}
|
onChange={(option) => option && setCategoriaId(option.value)}
|
||||||
// 4. Usamos un prefijo de clase simple que se asociará con las clases del módulo CSS
|
|
||||||
classNamePrefix="categoriaSelector"
|
classNamePrefix="categoriaSelector"
|
||||||
className={styles.categoriaSelectorContainer}
|
className={styles.categoriaSelectorContainer}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.headerBottomRow}>
|
<div className={styles.headerBottomRow}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
nivel={ambitoActual.nivel}
|
nivel={ambitoActual.nivel}
|
||||||
nombreAmbito={ambitoActual.nombre}
|
nombreAmbito={ambitoActual.nombre}
|
||||||
nombreProvincia={ambitoActual.provinciaNombre}
|
nombreProvincia={ambitoActual.provinciaNombre}
|
||||||
onReset={handleResetToPais}
|
onReset={handleResetToPais}
|
||||||
onVolverProvincia={handleVolverAProvincia}
|
onVolverProvincia={handleVolverAProvincia}
|
||||||
|
/>
|
||||||
|
{ambitoActual.nivel === 'provincia' && ambitoActual.provinciaDistritoId && (
|
||||||
|
<MunicipioSearch
|
||||||
|
distritoId={ambitoActual.provinciaDistritoId}
|
||||||
|
onMunicipioSelect={(municipioId, municipioNombre) =>
|
||||||
|
handleAmbitoSelect(municipioId, 'municipio', municipioNombre)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{ambitoActual.nivel === 'provincia' && ambitoActual.provinciaDistritoId && (
|
)}
|
||||||
<MunicipioSearch
|
|
||||||
distritoId={ambitoActual.provinciaDistritoId}
|
|
||||||
onMunicipioSelect={(municipioId, municipioNombre) =>
|
|
||||||
handleAmbitoSelect(municipioId, 'municipio', municipioNombre)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className={mainContentClasses}>
|
<main className={mainContentClasses}>
|
||||||
@@ -254,17 +279,17 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<MobileResultsCard
|
<MobileResultsCard
|
||||||
eleccionId={eleccionId}
|
eleccionId={eleccionId}
|
||||||
ambitoId={ambitoActual.id}
|
ambitoId={ambitoActual.id}
|
||||||
categoriaId={categoriaId}
|
categoriaId={categoriaId}
|
||||||
ambitoNombre={ambitoActual.nombre}
|
ambitoNombre={ambitoActual.nombre}
|
||||||
ambitoNivel={ambitoActual.nivel}
|
ambitoNivel={ambitoActual.nivel}
|
||||||
mobileView={mobileView}
|
mobileView={mobileView}
|
||||||
setMobileView={setMobileView}
|
setMobileView={setMobileView}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom
|
|||||||
if (newZoom > initialProvincePositionRef.current.zoom) {
|
if (newZoom > initialProvincePositionRef.current.zoom) {
|
||||||
toast.success('Desplazamiento Habilitado', {
|
toast.success('Desplazamiento Habilitado', {
|
||||||
icon: '🖐️',
|
icon: '🖐️',
|
||||||
style: { background: '#32e5f1ff', color: 'white' },
|
style: { background: '#32e5f1ff', color: 'white', zIndex: 9999},
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ export interface PanelElectoralDto {
|
|||||||
mapaData: ResultadoMapaDto[];
|
mapaData: ResultadoMapaDto[];
|
||||||
resultadosPanel: ResultadoTicker[];
|
resultadosPanel: ResultadoTicker[];
|
||||||
estadoRecuento: EstadoRecuentoTicker;
|
estadoRecuento: EstadoRecuentoTicker;
|
||||||
|
sinDatos?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- TIPOS PARA EL WIDGET DE TARJETAS NACIONALES ---
|
// --- TIPOS PARA EL WIDGET DE TARJETAS NACIONALES ---
|
||||||
|
|||||||
Reference in New Issue
Block a user