mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 16:24:50 +00:00
miniapp: client profile tab — list + history + lead detail
NEW FILE assets/clients.js: - Clients.mount(container) — hash-routed view - #/clients — list of all clients (cards: avatar, name, phone, leads count, last date) - #/clients/client/<key> — single client history (all leads as items) - #/clients/lead/<id> — full lead detail with re-rendered report UI: - Card style: avatar with initial, name + phone, footer with N подборов + дата - Pluralization for Russian (1 подбор / 2 подбора / 5 подборов) - Date format: 'сегодня · 14:30' or 'DD.MM.YYYY' - Status pills: new / sent / viewed / ordered PODBOR.JS: - Exposed renderSavedReport(ai, leadId) for Clients module reuse - Same renderer as live podbor — same matrix, pros/cons, links APP.JS: - Quick action 'Клиенты' added (icon: user) - Hash router: #/clients → Clients.mount() INDEX.HTML: - clients.js script added - Cache bumped to v=20260512a CSS: - .client-list, .client-card with avatar+meta+footer - .client-detail-head (big card with avatar 56px) - .leads-list with .lead-item (grid: date | id | status | arrow) - .loader-inline for async fetch - .ai-text-fallback for legacy text-only responses
This commit is contained in:
parent
9a2dcbc3fe
commit
c4f3016b56
@ -177,10 +177,10 @@ function renderManagerHome(me) {
|
||||
|
||||
// Quick actions
|
||||
const quickActions = [
|
||||
{ icon: "user", title: "Клиенты", subtitle: "История подборов", href: "#/clients" },
|
||||
{ icon: "package", title: "Подбор техники", subtitle: "Встройка + AI", href: "#/podbor" },
|
||||
{ icon: "camera", title: "Новый замер", subtitle: "С фото", href: null },
|
||||
{ icon: "cube", title: "3D просмотр", subtitle: "Проекты", href: null },
|
||||
{ icon: "bolt", title: "Коммуникации", subtitle: "Чек-лист", href: null },
|
||||
{ icon: "package", title: "Подбор техники", subtitle: "Встройка + AI", href: "#/podbor" },
|
||||
];
|
||||
app.appendChild(el(`<div class="section-head"><span class="label">Быстрые действия</span></div>`));
|
||||
const grid = el(`<div class="quick-grid"></div>`);
|
||||
@ -366,6 +366,10 @@ async function init() {
|
||||
Podbor.mount(app);
|
||||
return;
|
||||
}
|
||||
if (location.hash.startsWith("#/clients")) {
|
||||
Clients.mount(app);
|
||||
return;
|
||||
}
|
||||
if (me.role === "manager") renderManager(me);
|
||||
else renderClient(me);
|
||||
} catch (e) {
|
||||
@ -377,6 +381,8 @@ async function init() {
|
||||
function routeByHash() {
|
||||
if (location.hash.startsWith("#/podbor")) {
|
||||
Podbor.mount(app);
|
||||
} else if (location.hash.startsWith("#/clients")) {
|
||||
Clients.mount(app);
|
||||
} else {
|
||||
// Главный экран по роли
|
||||
const me = window.__zovMe;
|
||||
|
||||
301
miniapp/assets/clients.js
Normal file
301
miniapp/assets/clients.js
Normal file
@ -0,0 +1,301 @@
|
||||
/* ============================================================
|
||||
Клиенты — список + история подборов
|
||||
============================================================ */
|
||||
|
||||
const Clients = (function () {
|
||||
let root = null;
|
||||
let clientsCache = null;
|
||||
|
||||
/* ===================== Mount ===================== */
|
||||
|
||||
function mount(container) {
|
||||
root = container;
|
||||
document.body.classList.remove("has-bottom-nav");
|
||||
const oldNav = document.getElementById("bottom-nav");
|
||||
if (oldNav) oldNav.remove();
|
||||
|
||||
const sub = location.hash.replace(/^#\/clients\/?/, "");
|
||||
if (sub.startsWith("lead/")) {
|
||||
const leadId = sub.slice(5);
|
||||
renderLead(leadId);
|
||||
} else if (sub.startsWith("client/")) {
|
||||
const clientKey = decodeURIComponent(sub.slice(7));
|
||||
renderClientHistory(clientKey);
|
||||
} else {
|
||||
renderList();
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== Список клиентов ===================== */
|
||||
|
||||
async function renderList() {
|
||||
root.innerHTML = "";
|
||||
root.appendChild(headerEl("Клиенты", null));
|
||||
const loading = el(`<div class="loader-inline"><div class="spinner"></div></div>`);
|
||||
root.appendChild(loading);
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await fetchClients();
|
||||
clientsCache = data;
|
||||
} catch (e) {
|
||||
loading.remove();
|
||||
root.appendChild(el(`<div class="error">Не удалось загрузить: ${e.message}</div>`));
|
||||
return;
|
||||
}
|
||||
loading.remove();
|
||||
|
||||
if (!data.clients || !data.clients.length) {
|
||||
root.appendChild(el(`
|
||||
<div class="empty">
|
||||
<p class="lede" style="text-align:center;padding:40px 20px;color:var(--muted)">
|
||||
У тебя пока нет подборов с клиентами.<br>
|
||||
Сделай первый — в кабинете «Подбор техники».
|
||||
</p>
|
||||
</div>
|
||||
`));
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = el(`
|
||||
<div class="kicker" style="margin-bottom:8px;">
|
||||
${data.count} ${pluralize(data.count, "клиент", "клиента", "клиентов")} · ${countLeads(data.clients)} ${pluralize(countLeads(data.clients), "подбор", "подбора", "подборов")}
|
||||
</div>
|
||||
`);
|
||||
root.appendChild(meta);
|
||||
|
||||
const list = el(`<div class="client-list"></div>`);
|
||||
for (const c of data.clients) {
|
||||
list.appendChild(renderClientCard(c));
|
||||
}
|
||||
root.appendChild(list);
|
||||
}
|
||||
|
||||
function renderClientCard(c) {
|
||||
const lastAt = formatDate(c.last_lead_at);
|
||||
const card = el(`
|
||||
<article class="client-card">
|
||||
<div class="client-card-head">
|
||||
<div class="client-avatar">${initial(c.client_name)}</div>
|
||||
<div class="client-meta">
|
||||
<div class="client-name">${escHtml(c.client_name || "Без имени")}</div>
|
||||
${c.client_phone ? `<div class="client-phone">${escHtml(c.client_phone)}</div>` : ""}
|
||||
</div>
|
||||
<div class="client-arrow">${ICONS.chevron || "›"}</div>
|
||||
</div>
|
||||
<div class="client-footer">
|
||||
<span class="leads-count">${c.leads_count} ${pluralize(c.leads_count, "подбор", "подбора", "подборов")}</span>
|
||||
<span class="muted">${lastAt}</span>
|
||||
</div>
|
||||
</article>
|
||||
`);
|
||||
card.addEventListener("click", () => {
|
||||
haptic && haptic("impact");
|
||||
const key = c.client_tg_id || c.client_name.toLowerCase();
|
||||
location.hash = `#/clients/client/${encodeURIComponent(key)}`;
|
||||
});
|
||||
return card;
|
||||
}
|
||||
|
||||
/* ===================== История клиента ===================== */
|
||||
|
||||
async function renderClientHistory(clientKey) {
|
||||
root.innerHTML = "";
|
||||
root.appendChild(headerEl("История подборов", "#/clients"));
|
||||
|
||||
// Берём из кеша если есть
|
||||
let clients = clientsCache?.clients;
|
||||
if (!clients) {
|
||||
try {
|
||||
const data = await fetchClients();
|
||||
clients = data.clients;
|
||||
clientsCache = data;
|
||||
} catch (e) {
|
||||
root.appendChild(el(`<div class="error">${e.message}</div>`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const client = clients.find(c =>
|
||||
(c.client_tg_id && c.client_tg_id === clientKey) ||
|
||||
(c.client_name && c.client_name.toLowerCase() === clientKey)
|
||||
);
|
||||
if (!client) {
|
||||
root.appendChild(el(`<div class="empty">Клиент не найден</div>`));
|
||||
return;
|
||||
}
|
||||
|
||||
root.appendChild(el(`
|
||||
<div class="client-detail-head">
|
||||
<div class="client-avatar lg">${initial(client.client_name)}</div>
|
||||
<div>
|
||||
<h2 class="client-detail-name">${escHtml(client.client_name)}</h2>
|
||||
${client.client_phone ? `<div class="client-detail-phone">${escHtml(client.client_phone)}</div>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`));
|
||||
|
||||
root.appendChild(el(`<div class="section-head"><span class="label">Подборы · ${client.leads_count}</span></div>`));
|
||||
|
||||
const leadsList = el(`<div class="leads-list"></div>`);
|
||||
for (const lead of client.leads) {
|
||||
const item = el(`
|
||||
<button class="lead-item">
|
||||
<div class="lead-date">${formatDate(lead.created_at)}</div>
|
||||
<div class="lead-id">#${(lead.id || "").slice(0, 8)}</div>
|
||||
<div class="lead-status status-${lead.status || "new"}">${statusLabel(lead.status)}</div>
|
||||
<div class="lead-arrow">${ICONS.chevron || "›"}</div>
|
||||
</button>
|
||||
`);
|
||||
item.addEventListener("click", () => {
|
||||
haptic && haptic("impact");
|
||||
location.hash = `#/clients/lead/${lead.id}`;
|
||||
});
|
||||
leadsList.appendChild(item);
|
||||
}
|
||||
root.appendChild(leadsList);
|
||||
}
|
||||
|
||||
/* ===================== Детали лида (re-render отчёта) ===================== */
|
||||
|
||||
async function renderLead(leadId) {
|
||||
root.innerHTML = "";
|
||||
root.appendChild(headerEl("Подбор", "back"));
|
||||
const loading = el(`<div class="loader-inline"><div class="spinner"></div></div>`);
|
||||
root.appendChild(loading);
|
||||
|
||||
let lead;
|
||||
try {
|
||||
lead = await fetchLead(leadId);
|
||||
} catch (e) {
|
||||
loading.remove();
|
||||
root.appendChild(el(`<div class="error">${e.message}</div>`));
|
||||
return;
|
||||
}
|
||||
loading.remove();
|
||||
|
||||
if (lead.error) {
|
||||
root.appendChild(el(`<div class="error">${lead.error}</div>`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Шапка
|
||||
root.appendChild(el(`
|
||||
<div class="lead-detail-head">
|
||||
<div class="kicker">Подбор #${(lead.id || "").slice(0, 8)}</div>
|
||||
<h2 class="display-title">${escHtml(lead.client_name || "Клиент")}</h2>
|
||||
<p class="lede">Сохранён ${formatDate(lead.created_at)}</p>
|
||||
</div>
|
||||
`));
|
||||
|
||||
// Рендерим отчёт через Podbor.renderReport если ai-json есть
|
||||
if (lead.ai && typeof window.Podbor?.renderSavedReport === "function") {
|
||||
const reportNode = window.Podbor.renderSavedReport(lead.ai, lead.id);
|
||||
root.appendChild(reportNode);
|
||||
} else if (lead.ai_text) {
|
||||
// Fallback — AI вернул plain text
|
||||
root.appendChild(el(`
|
||||
<div class="block">
|
||||
<div class="block-head">AI ответ</div>
|
||||
<pre class="ai-text-fallback">${escHtml(lead.ai_text)}</pre>
|
||||
</div>
|
||||
`));
|
||||
} else {
|
||||
root.appendChild(el(`<div class="empty">Для этого лида нет AI-ответа.</div>`));
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== Helpers ===================== */
|
||||
|
||||
function headerEl(title, backHref) {
|
||||
const h = el(`
|
||||
<header class="podbor-header">
|
||||
<button class="podbor-back" aria-label="Назад">${ICONS.arrow_left || "‹"}</button>
|
||||
<div class="podbor-title">${escHtml(title)}</div>
|
||||
<div style="width:28px"></div>
|
||||
</header>
|
||||
`);
|
||||
h.querySelector(".podbor-back").addEventListener("click", () => {
|
||||
if (backHref === "back") {
|
||||
history.back();
|
||||
} else if (backHref) {
|
||||
location.hash = backHref;
|
||||
} else {
|
||||
location.hash = "";
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
return h;
|
||||
}
|
||||
|
||||
async function fetchClients() {
|
||||
if (!BACKEND_URL) throw new Error("BACKEND_URL не задан");
|
||||
const res = await fetch(`${BACKEND_URL}/api/clients`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ initData: tg?.initData || "" }),
|
||||
});
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
async function fetchLead(leadId) {
|
||||
if (!BACKEND_URL) throw new Error("BACKEND_URL не задан");
|
||||
const res = await fetch(`${BACKEND_URL}/api/lead`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ initData: tg?.initData || "", lead_id: leadId }),
|
||||
});
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
function initial(name) {
|
||||
return ((name || "?").trim()[0] || "?").toUpperCase();
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s == null ? "" : s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
if (!iso) return "—";
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
const now = new Date();
|
||||
const sameDay = d.toDateString() === now.toDateString();
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const yy = d.getFullYear();
|
||||
const hh = String(d.getHours()).padStart(2, "0");
|
||||
const mi = String(d.getMinutes()).padStart(2, "0");
|
||||
if (sameDay) return `сегодня · ${hh}:${mi}`;
|
||||
return `${dd}.${mm}.${yy}`;
|
||||
} catch (e) {
|
||||
return iso.slice(0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
function pluralize(n, one, few, many) {
|
||||
const last = n % 10, lastTwo = n % 100;
|
||||
if (lastTwo >= 11 && lastTwo <= 14) return many;
|
||||
if (last === 1) return one;
|
||||
if (last >= 2 && last <= 4) return few;
|
||||
return many;
|
||||
}
|
||||
|
||||
function countLeads(clients) {
|
||||
return clients.reduce((s, c) => s + (c.leads_count || 0), 0);
|
||||
}
|
||||
|
||||
function statusLabel(s) {
|
||||
const map = {
|
||||
"new": "Новый",
|
||||
"sent": "Отправлен",
|
||||
"viewed": "Просмотрен",
|
||||
"ordered": "Оформлен",
|
||||
};
|
||||
return map[s] || s || "—";
|
||||
}
|
||||
|
||||
return { mount };
|
||||
})();
|
||||
@ -1789,3 +1789,202 @@
|
||||
margin-top: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Клиенты — список + история
|
||||
============================================================ */
|
||||
|
||||
.client-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
}
|
||||
.client-card:active { transform: scale(0.99); background: var(--warm); }
|
||||
|
||||
.client-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.client-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--r-pill);
|
||||
background: var(--warm);
|
||||
color: var(--accent-2);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.client-avatar.lg {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.client-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.client-name {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.client-phone {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.client-arrow {
|
||||
color: var(--muted);
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.client-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--line);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.client-footer .leads-count {
|
||||
color: var(--accent-2);
|
||||
font-weight: 500;
|
||||
}
|
||||
.client-footer .muted { color: var(--muted); }
|
||||
|
||||
/* Детальный экран клиента */
|
||||
.client-detail-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 24px;
|
||||
padding: 14px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
}
|
||||
.client-detail-name {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
color: var(--ink);
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.client-detail-phone {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.leads-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.lead-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto 20px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.lead-item:active { background: var(--warm); }
|
||||
|
||||
.lead-date {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 13.5px;
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
}
|
||||
.lead-id {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.lead-status {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--r-pill);
|
||||
background: var(--warm);
|
||||
color: var(--accent-2);
|
||||
}
|
||||
.lead-status.status-new { background: var(--warm); color: var(--accent-2); }
|
||||
.lead-status.status-sent { background: rgba(42,107,63,0.12); color: #2A6B3F; }
|
||||
.lead-arrow {
|
||||
color: var(--muted);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Заглушка-loader */
|
||||
.loader-inline {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
.loader-inline .spinner {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border: 2.5px solid var(--line);
|
||||
border-top-color: var(--accent-2);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.lead-detail-head {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.ai-text-fallback {
|
||||
white-space: pre-wrap;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11.5px;
|
||||
line-height: 1.5;
|
||||
color: var(--ink-2);
|
||||
background: var(--warm);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@ -1780,5 +1780,12 @@ ${reportEl.outerHTML}
|
||||
});
|
||||
}
|
||||
|
||||
return { mount, go, getState: () => state, reset: () => { state = defaultState(); saveState(); render(); } };
|
||||
return {
|
||||
mount,
|
||||
go,
|
||||
getState: () => state,
|
||||
reset: () => { state = defaultState(); saveState(); render(); },
|
||||
// Внешний API для рендеринга сохранённого отчёта (используется в Clients)
|
||||
renderSavedReport: (ai, leadId) => renderReport(ai, leadId || ""),
|
||||
};
|
||||
})();
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap">
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
<link rel="stylesheet" href="assets/styles.css?v=20260511p">
|
||||
<link rel="stylesheet" href="assets/podbor.css?v=20260511p">
|
||||
<link rel="stylesheet" href="assets/styles.css?v=20260512a">
|
||||
<link rel="stylesheet" href="assets/podbor.css?v=20260512a">
|
||||
</head>
|
||||
<body>
|
||||
<main id="app">
|
||||
@ -21,10 +21,11 @@
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="assets/icons.js?v=20260511p"></script>
|
||||
<script src="assets/podbor.config.js?v=20260511p"></script>
|
||||
<script src="assets/podbor.picts.js?v=20260511p"></script>
|
||||
<script src="assets/podbor.js?v=20260511p"></script>
|
||||
<script src="assets/app.js?v=20260511p"></script>
|
||||
<script src="assets/icons.js?v=20260512a"></script>
|
||||
<script src="assets/podbor.config.js?v=20260512a"></script>
|
||||
<script src="assets/podbor.picts.js?v=20260512a"></script>
|
||||
<script src="assets/podbor.js?v=20260512a"></script>
|
||||
<script src="assets/clients.js?v=20260512a"></script>
|
||||
<script src="assets/app.js?v=20260512a"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user