v1.6.0 (web, UI + E2E) : chip Prolonger, split LAB/LABX, avertissement, WEB_VERSION 1.6.0
- chart.js : chip « Prolonger » (off par défaut, désactivé tant que Tracé labs est off), split série "LAB" (ancrée) / "LABX" (prolongée, rose atténué rgba(194,24,91,0.55)), légendes dédiées (clés LAB ET LABX skippées de la boucle — miroir leçon #63), AVERTISSEMENT « simple simulation, sans garantie de correspondre au réel, labs potentiellement erronés » affiché sous la légende tant que la prolongation est visible (condition factorisée : jamais de courbe prolongée sans avertissement). - i18n : show_lab_track_extend, legend_lab_track_extend, lab_track_extend_warning (FR/EN synchronisées). - E2E : scénario v1.6.0 (chip désactivé → activation → légende + avertissement → disparition à la désactivation) ; assertions version 1.6.0.
This commit is contained in:
parent
bd338decbf
commit
551d8fa29e
@ -76,6 +76,13 @@ export function renderChart(container, ctx) {
|
|||||||
showForecast: false,
|
showForecast: false,
|
||||||
showExtrema: false,
|
showExtrema: false,
|
||||||
showLabTrack: false, // v1.5.0 — courbe ancrée sur les labs, OFF par défaut (choix v1.5.0)
|
showLabTrack: false, // v1.5.0 — courbe ancrée sur les labs, OFF par défaut (choix v1.5.0)
|
||||||
|
// v1.6.0 : PROLONGATION du tracé labs au-delà du dernier lab (ρ du
|
||||||
|
// dernier lab constant × modèle, bornée par l'extinction du modèle —
|
||||||
|
// cf LabTrajectoryModel.extensionHorizonEndMs). OFF par défaut : la
|
||||||
|
// partie prolongée est une ESTIMATION non ancrée, elle doit être
|
||||||
|
// demandée explicitement ; le chip n'est actif que si Lab track est ON
|
||||||
|
// (miroir ChartScreen.kt).
|
||||||
|
showLabTrackExtend: false,
|
||||||
panResidualHours: 0.0, // résiduel fractionnaire du pan (fix #62)
|
panResidualHours: 0.0, // résiduel fractionnaire du pan (fix #62)
|
||||||
togglesInitialized: false,
|
togglesInitialized: false,
|
||||||
};
|
};
|
||||||
@ -208,18 +215,39 @@ export function renderChart(container, ctx) {
|
|||||||
modelRow.appendChild(filterChip(t('model_whs'), state.showWhs, () => { state.showWhs = !state.showWhs; redraw(); }));
|
modelRow.appendChild(filterChip(t('model_whs'), state.showWhs, () => { state.showWhs = !state.showWhs; redraw(); }));
|
||||||
// v1.5.0 : courbe hybride ancrée sur les labs (off par défaut)
|
// v1.5.0 : courbe hybride ancrée sur les labs (off par défaut)
|
||||||
modelRow.appendChild(filterChip(t('show_lab_track'), state.showLabTrack, () => { state.showLabTrack = !state.showLabTrack; redraw(); }));
|
modelRow.appendChild(filterChip(t('show_lab_track'), state.showLabTrack, () => { state.showLabTrack = !state.showLabTrack; redraw(); }));
|
||||||
|
// v1.6.0 : prolongation du tracé labs au-delà du dernier lab (off par
|
||||||
|
// défaut, SANS EFFET — donc désactivé — tant que Lab track est off :
|
||||||
|
// la prolongation n'existe que comme extension de CETTE courbe)
|
||||||
|
modelRow.appendChild(filterChip(t('show_lab_track_extend'), state.showLabTrackExtend, () => { state.showLabTrackExtend = !state.showLabTrackExtend; redraw(); }, !state.showLabTrack));
|
||||||
|
|
||||||
// ── Courbes par modèle sélectionné ─────────────────────────────────────
|
// ── Courbes par modèle sélectionné ─────────────────────────────────────
|
||||||
const curves = [];
|
const curves = [];
|
||||||
// ── v1.5.0 : courbe « Tracé labs » ───────────────────────────────────────
|
// ── v1.5.0 : courbe « Tracé labs » ───────────────────────────────────────
|
||||||
// Indépendante des toggles modèles (modèle STOCKÉ — sémantique Home) et
|
// Indépendante des toggles modèles (modèle STOCKÉ — sémantique Home) et
|
||||||
// de la calibration (elle EST sa propre calibration continue — épinglé).
|
// de la calibration (elle EST sa propre calibration continue — épinglé).
|
||||||
|
//
|
||||||
|
// ── v1.6.0 : split ancré / prolongé ──────────────────────────────────────
|
||||||
|
// Avec « Prolonger », la série est SPLITTÉE au dernier lab significatif
|
||||||
|
// (lastAnchorMs) : "LAB" = partie ancrée (passe par les labs), "LABX" =
|
||||||
|
// partie prolongée (M(t) × ρ_last — une estimation). Deux traits
|
||||||
|
// distincts (LABX en atténué) préservent l'honnêteté de l'affichage.
|
||||||
|
// ⚠️ Les DEUX clés doivent être SKIPPÉES par la boucle de légende
|
||||||
|
// (leçon #63 Android : sinon le catchall imprime la légende TFS en
|
||||||
|
// doublon) et avoir leurs entrées dédiées plus bas.
|
||||||
if (state.showLabTrack) {
|
if (state.showLabTrack) {
|
||||||
const step = stepForRange(state.rangeHours);
|
const step = stepForRange(state.rangeHours);
|
||||||
const anchored = computeLabAnchoredCurve(
|
const labCurve = computeLabAnchoredCurve(
|
||||||
treatments, doseLogs, labResults, startMs, endMs, step, null,
|
treatments, doseLogs, labResults, startMs, endMs, step, null,
|
||||||
|
state.showLabTrackExtend,
|
||||||
);
|
);
|
||||||
curves.push(['LAB', anchored.points]);
|
if (state.showLabTrackExtend && labCurve.lastAnchorMs !== null) {
|
||||||
|
const anchored = labCurve.points.filter((p) => p.timestamp <= labCurve.lastAnchorMs);
|
||||||
|
const extended = labCurve.points.filter((p) => p.timestamp > labCurve.lastAnchorMs);
|
||||||
|
curves.push(['LAB', anchored]);
|
||||||
|
if (extended.length > 0) curves.push(['LABX', extended]);
|
||||||
|
} else {
|
||||||
|
curves.push(['LAB', labCurve.points]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (state.showEse || state.showTfs || state.showWhs) {
|
if (state.showEse || state.showTfs || state.showWhs) {
|
||||||
// Home utilise le modèle stocké ; le graphique calibre chaque modèle
|
// Home utilise le modèle stocké ; le graphique calibre chaque modèle
|
||||||
@ -288,6 +316,18 @@ export function renderChart(container, ctx) {
|
|||||||
// (série E2-only) et hors pics/creux (showExtrema=false)
|
// (série E2-only) et hors pics/creux (showExtrema=false)
|
||||||
return { points: pts, e2Style: { color: COLORS.chartLabTrajectory, dashed: true }, tStyle: null, showExtrema: false };
|
return { points: pts, e2Style: { color: COLORS.chartLabTrajectory, dashed: true }, tStyle: null, showExtrema: false };
|
||||||
}
|
}
|
||||||
|
if (model === 'LABX') {
|
||||||
|
// v1.6.0 : partie PROLONGÉE — même couleur ATTÉNUÉE (alpha 0,55,
|
||||||
|
// = #c2185b en rgba) pour distinguer l'estimation non ancrée de
|
||||||
|
// la partie qui passe par les labs. Mêmes exclusions (pas de T,
|
||||||
|
// pas d'extrema).
|
||||||
|
return {
|
||||||
|
points: pts,
|
||||||
|
e2Style: { color: 'rgba(194,24,91,0.55)', dashed: true },
|
||||||
|
tStyle: null,
|
||||||
|
showExtrema: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
const st = seriesStyles(model);
|
const st = seriesStyles(model);
|
||||||
return { points: pts, e2Style: st.e2, tStyle: st.t };
|
return { points: pts, e2Style: st.e2, tStyle: st.t };
|
||||||
}),
|
}),
|
||||||
@ -310,9 +350,10 @@ export function renderChart(container, ctx) {
|
|||||||
|
|
||||||
// Légende dynamique (miroir du bloc Compose)
|
// Légende dynamique (miroir du bloc Compose)
|
||||||
for (const [model] of curves) {
|
for (const [model] of curves) {
|
||||||
// ⚠️ La clé 'LAB' est SKIPPÉE (légende dédiée plus bas, fix #63
|
// ⚠️ Les clés 'LAB' (v1.5.0) et 'LABX' (v1.6.0) sont SKIPPÉES
|
||||||
// Android-miroir : sinon le `else` imprime la légende TFS en doublon)
|
// (légendes dédiées plus bas, fix #63 Android-miroir : sinon le
|
||||||
if (model === 'LAB') continue;
|
// catchall imprime la légende TFS en doublon)
|
||||||
|
if (model === 'LAB' || model === 'LABX') continue;
|
||||||
const st = seriesStyles(model);
|
const st = seriesStyles(model);
|
||||||
if (model === 'ESE') {
|
if (model === 'ESE') {
|
||||||
legend.appendChild(legendLine(t('legend_ese_e2'), st.e2.color));
|
legend.appendChild(legendLine(t('legend_ese_e2'), st.e2.color));
|
||||||
@ -335,6 +376,16 @@ export function renderChart(container, ctx) {
|
|||||||
if (state.showLabTrack && curves.some(([m, pts]) => m === 'LAB' && pts.length >= 2)) {
|
if (state.showLabTrack && curves.some(([m, pts]) => m === 'LAB' && pts.length >= 2)) {
|
||||||
legend.appendChild(legendLine(t('legend_lab_track'), COLORS.chartLabTrajectory));
|
legend.appendChild(legendLine(t('legend_lab_track'), COLORS.chartLabTrajectory));
|
||||||
}
|
}
|
||||||
|
// v1.6.0 : légende de la partie PROLONGÉE + AVERTISSEMENT — seulement
|
||||||
|
// si la série "LABX" existe (chip Prolonger actif ET horizon atteint
|
||||||
|
// dans la fenêtre visible). Même condition pour les deux (factorisée) :
|
||||||
|
// il ne peut jamais y avoir de courbe prolongée sans son avertissement.
|
||||||
|
const labExtendVisible = state.showLabTrack && state.showLabTrackExtend
|
||||||
|
&& curves.some(([m, pts]) => m === 'LABX' && pts.length >= 2);
|
||||||
|
if (labExtendVisible) {
|
||||||
|
legend.appendChild(legendLine(t('legend_lab_track_extend'), 'rgba(194,24,91,0.55)'));
|
||||||
|
legend.appendChild(el('div', { class: 'legend-muted' }, t('lab_track_extend_warning')));
|
||||||
|
}
|
||||||
legend.appendChild(el('div', { class: 'legend-muted' }, t('drag_hint')));
|
legend.appendChild(el('div', { class: 'legend-muted' }, t('drag_hint')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import { el, showDialog, field, switchRow, filterChip, toast } from './component
|
|||||||
* (APK + web au MÊME numéro, tags appariés `vX.Y.Z` + `vX.Y.Z-web`).
|
* (APK + web au MÊME numéro, tags appariés `vX.Y.Z` + `vX.Y.Z-web`).
|
||||||
* v1.4.10 = portage initial de l'Android v1.4.10.
|
* v1.4.10 = portage initial de l'Android v1.4.10.
|
||||||
*/
|
*/
|
||||||
export const WEB_VERSION = '1.5.0';
|
export const WEB_VERSION = '1.6.0';
|
||||||
|
|
||||||
/** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */
|
/** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */
|
||||||
function parseNumOrNull(text) {
|
function parseNumOrNull(text) {
|
||||||
|
|||||||
@ -182,6 +182,10 @@ const EN = {
|
|||||||
show_forecast: 'Forecast',
|
show_forecast: 'Forecast',
|
||||||
show_extrema: 'Peaks & troughs',
|
show_extrema: 'Peaks & troughs',
|
||||||
show_lab_track: 'Lab track',
|
show_lab_track: 'Lab track',
|
||||||
|
// v1.6.0 : prolongation du tracé labs au-delà du dernier lab (opt-in,
|
||||||
|
// actif seulement quand Lab track est ON — miroir strings.xml Android)
|
||||||
|
show_lab_track_extend: 'Extend',
|
||||||
|
lab_track_extend_warning: '⚠ Simulation only, with no guarantee of matching reality: the extension extrapolates your model from your lab results — which can themselves be inaccurate. Trust your next blood test, not this curve.',
|
||||||
back_to_present: 'Back to present',
|
back_to_present: 'Back to present',
|
||||||
legend_ese_e2: 'E2 — Estrannaise (blue)',
|
legend_ese_e2: 'E2 — Estrannaise (blue)',
|
||||||
legend_ese_t: 'T — Estrannaise (pink)',
|
legend_ese_t: 'T — Estrannaise (pink)',
|
||||||
@ -193,6 +197,8 @@ const EN = {
|
|||||||
legend_forecast_dose: 'Dashed line + ▼ = projected dose',
|
legend_forecast_dose: 'Dashed line + ▼ = projected dose',
|
||||||
legend_extrema: '▲▼ = local peaks & troughs (with estimated values)',
|
legend_extrema: '▲▼ = local peaks & troughs (with estimated values)',
|
||||||
legend_lab_track: 'Lab-anchored curve — model shape forced through your labs (E2 only, between first and last lab)',
|
legend_lab_track: 'Lab-anchored curve — model shape forced through your labs (E2 only, between first and last lab)',
|
||||||
|
// v1.6.0 : légende de la partie PROLONGÉE du tracé labs (série "LABX")
|
||||||
|
legend_lab_track_extend: '-- Lab track extended past your last lab (model × last lab ratio — estimate, not anchored)',
|
||||||
drag_hint: 'Drag horizontally to pan (right = past, left = future with forecast). Pinch or − / + to zoom (6 h → 300 d).',
|
drag_hint: 'Drag horizontally to pan (right = past, left = future with forecast). Pinch or − / + to zoom (6 h → 300 d).',
|
||||||
// Rappels (bannière + notification)
|
// Rappels (bannière + notification)
|
||||||
reminder_notif_title: 'HormoneTrack — dose reminder',
|
reminder_notif_title: 'HormoneTrack — dose reminder',
|
||||||
@ -360,6 +366,10 @@ const FR = {
|
|||||||
show_forecast: 'Prévision',
|
show_forecast: 'Prévision',
|
||||||
show_extrema: 'Pics & creux',
|
show_extrema: 'Pics & creux',
|
||||||
show_lab_track: 'Tracé labs',
|
show_lab_track: 'Tracé labs',
|
||||||
|
// v1.6.0 : prolongation du tracé labs au-delà du dernier lab (opt-in,
|
||||||
|
// actif seulement quand Tracé labs est ON — miroir strings.xml Android)
|
||||||
|
show_lab_track_extend: 'Prolonger',
|
||||||
|
lab_track_extend_warning: '⚠ Simple simulation, sans garantie de correspondre au réel : la prolongation extrapole ton modèle à partir de tes résultats de laboratoire — qui peuvent eux-mêmes être erronés. Fie-toi à ta prochaine prise de sang, pas à cette courbe.',
|
||||||
back_to_present: 'Revenir à maintenant',
|
back_to_present: 'Revenir à maintenant',
|
||||||
legend_ese_e2: 'E2 — Estrannaise (bleu)',
|
legend_ese_e2: 'E2 — Estrannaise (bleu)',
|
||||||
legend_ese_t: 'T — Estrannaise (rose)',
|
legend_ese_t: 'T — Estrannaise (rose)',
|
||||||
@ -371,6 +381,8 @@ const FR = {
|
|||||||
legend_forecast_dose: 'Ligne pointillée + ▼ = dose projetée',
|
legend_forecast_dose: 'Ligne pointillée + ▼ = dose projetée',
|
||||||
legend_extrema: '▲▼ = pics/creux locaux (avec valeurs estimées)',
|
legend_extrema: '▲▼ = pics/creux locaux (avec valeurs estimées)',
|
||||||
legend_lab_track: 'E2 ancrée sur tes labs — forme du modèle forcée de passer par chaque prise de sang (entre le 1er et le dernier lab)',
|
legend_lab_track: 'E2 ancrée sur tes labs — forme du modèle forcée de passer par chaque prise de sang (entre le 1er et le dernier lab)',
|
||||||
|
// v1.6.0 : légende de la partie PROLONGÉE du tracé labs (série "LABX")
|
||||||
|
legend_lab_track_extend: '-- Tracé labs prolongé après ton dernier lab (modèle × ratio du dernier lab — estimation, plus ancrée)',
|
||||||
drag_hint: 'Glisser horizontalement pour panoramique (droite = passé, gauche = futur avec la prévision). Pincer ou − / + pour zoomer (6 h → 300 j).',
|
drag_hint: 'Glisser horizontalement pour panoramique (droite = passé, gauche = futur avec la prévision). Pincer ou − / + pour zoomer (6 h → 300 j).',
|
||||||
reminder_notif_title: 'HormoneTrack — rappel de dose',
|
reminder_notif_title: 'HormoneTrack — rappel de dose',
|
||||||
reminder_notif_body: '{0} · {1}{2} — prévu à {3}',
|
reminder_notif_body: '{0} · {1}{2} — prévu à {3}',
|
||||||
|
|||||||
@ -131,7 +131,7 @@ async function main() {
|
|||||||
ok(dialog !== null, 'dialog « Nouveautés » au premier lancement (markdown rendu)');
|
ok(dialog !== null, 'dialog « Nouveautés » au premier lancement (markdown rendu)');
|
||||||
if (dialog) {
|
if (dialog) {
|
||||||
const dlgText = await dialog.textContent();
|
const dlgText = await dialog.textContent();
|
||||||
ok(dlgText.includes('Version 1.5.0'), 'dialog : section de la version courante');
|
ok(dlgText.includes('Version 1.6.0'), 'dialog : section de la version courante');
|
||||||
const hasRenderedMd = await dialog.$('.md-body h3, .md-body strong');
|
const hasRenderedMd = await dialog.$('.md-body h3, .md-body strong');
|
||||||
ok(hasRenderedMd !== null, 'dialog : markdown RENDU (titres/gras), pas de brut');
|
ok(hasRenderedMd !== null, 'dialog : markdown RENDU (titres/gras), pas de brut');
|
||||||
await page.click('.dialog .actions button');
|
await page.click('.dialog .actions button');
|
||||||
@ -167,6 +167,57 @@ async function main() {
|
|||||||
ok(legendText.includes('Glisser horizontalement'), 'hint panoramique');
|
ok(legendText.includes('Glisser horizontalement'), 'hint panoramique');
|
||||||
await page.screenshot({ path: join(SHOTS, 'e2e-chart.png') });
|
await page.screenshot({ path: join(SHOTS, 'e2e-chart.png') });
|
||||||
|
|
||||||
|
// ── 2.bis Tracé labs PROLONGÉ (v1.6.0) : chips, légende, avertissement ──
|
||||||
|
// La démo place le dernier lab à now − 7 j : on dézoome (24 h → 32 j)
|
||||||
|
// pour que la partie ANCRÉE soit dans la fenêtre, puis on active
|
||||||
|
// « Tracé labs » puis « Prolonger ». ⚠️ Chaque interaction re-rend le
|
||||||
|
// DOM (redraw) → re-chercher le chip avant CHAQUE clic. ⚠️ Les chips
|
||||||
|
// filterChip sont des <span class="chip"> (onclick inline), PAS des
|
||||||
|
// <button> — seuls − / + sont de vrais boutons ; « disabled » est
|
||||||
|
// simulé par style opacity:0.4 (pas d'attribut disabled).
|
||||||
|
console.log('── Tracé labs prolongé (#chart, v1.6.0) ──');
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
await page.evaluate(() => {
|
||||||
|
[...document.querySelectorAll('button.chip')].find((x) => x.textContent.trim() === '−')?.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
// 1) Le chip « Prolonger » est DÉSACTIVÉ tant que « Tracé labs » est off
|
||||||
|
const extendDisabledBefore = await page.evaluate((lbl) => {
|
||||||
|
const b = [...document.querySelectorAll('.chip')].find((x) => x.textContent.trim() === lbl);
|
||||||
|
return b ? /opacity:\s*0\.4/.test(b.getAttribute('style') || '') : null;
|
||||||
|
}, 'Prolonger');
|
||||||
|
ok(extendDisabledBefore === true, 'chip « Prolonger » désactivé tant que Tracé labs est off');
|
||||||
|
// 2) Active Tracé labs → la légende ancrée apparaît (sans avertissement)
|
||||||
|
await page.evaluate((lbl) => {
|
||||||
|
[...document.querySelectorAll('.chip')].find((x) => x.textContent.trim() === lbl)?.click();
|
||||||
|
}, 'Tracé labs');
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
const labTrackText = await page.textContent('main.content');
|
||||||
|
ok(labTrackText.includes('E2 ancrée sur tes labs'), 'légende « Tracé labs » (ancrée) affichée');
|
||||||
|
ok(!labTrackText.includes('Simple simulation, sans garantie'),
|
||||||
|
'pas d\'avertissement tant que la prolongation est off');
|
||||||
|
// 3) Active « Prolonger » → légende prolongée + AVERTISSEMENT VISIBLE
|
||||||
|
await page.evaluate((lbl) => {
|
||||||
|
[...document.querySelectorAll('.chip')].find((x) => x.textContent.trim() === lbl)?.click();
|
||||||
|
}, 'Prolonger');
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
const extendText = await page.textContent('main.content');
|
||||||
|
ok(extendText.includes('Tracé labs prolongé'), 'légende de la partie prolongée affichée');
|
||||||
|
ok(extendText.includes('Simple simulation, sans garantie'),
|
||||||
|
'AVERTISSEMENT « simulation sans garantie » visible');
|
||||||
|
ok(extendText.includes('qui peuvent eux-mêmes être erronés'),
|
||||||
|
'avertissement : les résultats de lab peuvent être erronés');
|
||||||
|
// 4) Désactive « Prolonger » → avertissement ET légende prolongée disparaissent
|
||||||
|
await page.evaluate((lbl) => {
|
||||||
|
[...document.querySelectorAll('.chip')].find((x) => x.textContent.trim() === lbl)?.click();
|
||||||
|
}, 'Prolonger');
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
const offText = await page.textContent('main.content');
|
||||||
|
ok(!offText.includes('Tracé labs prolongé') && !offText.includes('Simple simulation, sans garantie'),
|
||||||
|
'désactivation → légende prolongée + avertissement disparaissent');
|
||||||
|
await page.screenshot({ path: join(SHOTS, 'e2e-chart-extend.png') });
|
||||||
|
|
||||||
// ── 3. Navigation : les 5 onglets + settings ────────────────────────────
|
// ── 3. Navigation : les 5 onglets + settings ────────────────────────────
|
||||||
console.log('── Navigation ──');
|
console.log('── Navigation ──');
|
||||||
await page.goto(`${base}/#doses`, { waitUntil: 'load' });
|
await page.goto(`${base}/#doses`, { waitUntil: 'load' });
|
||||||
@ -192,7 +243,7 @@ async function main() {
|
|||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
text = await page.textContent('main.content');
|
text = await page.textContent('main.content');
|
||||||
ok(text.includes('Langue'), 'Paramètres : carte langue');
|
ok(text.includes('Langue'), 'Paramètres : carte langue');
|
||||||
ok(text.includes('Version 1.5.0 (web)'), 'Paramètres : version affichée');
|
ok(text.includes('Version 1.6.0 (web)'), 'Paramètres : version affichée');
|
||||||
ok(text.includes('100 % local'), 'Paramètres : note vie privée');
|
ok(text.includes('100 % local'), 'Paramètres : note vie privée');
|
||||||
await page.screenshot({ path: join(SHOTS, 'e2e-settings.png') });
|
await page.screenshot({ path: join(SHOTS, 'e2e-settings.png') });
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user