/* ============================================================
StaffClients — список клиентов для сборщика / замерщика
#/master/clients
============================================================ */
const StaffClients = (function () {
"use strict";
function escHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
}
function el(html) {
const t = document.createElement("template");
t.innerHTML = html.trim();
return t.content.firstChild;
}
function fmtDate(iso) {
if (!iso) return null;
try {
return new Date(iso).toLocaleDateString("ru-RU", {
day: "numeric", month: "short", year: "numeric",
});
} catch { return iso.slice(0, 10); }
}
async function _api(path, body = {}) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 20000);
try {
const res = await fetch(`${BACKEND_URL}/api/${path}`, {
method: "POST", signal: ctrl.signal,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
initData: (typeof Platform !== "undefined" ? Platform.initData : (window.tg?.initData || "")),
initDataUnsafe: (typeof Platform !== "undefined" ? Platform.initDataUnsafe : null),
...body,
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (e) {
if (e.name === "AbortError") throw new Error("Сервер не отвечает");
throw e;
} finally { clearTimeout(t); }
}
const ASM_STATUS = {
created: { icon: "🆕", text: "Создана", color: "#8e8e8e" },
scheduled: { icon: "📅", text: "Запланирована", color: "#2980B9" },
in_progress: { icon: "🔨", text: "В процессе", color: "#F39C12" },
done: { icon: "✅", text: "Завершена", color: "#27AE60" },
cancelled: { icon: "❌", text: "Отменена", color: "#C0392B" },
};
const MEAS_STATUS = {
new: { icon: "🆕", text: "Новый", color: "#8e8e8e" },
scheduled: { icon: "📅", text: "Назначен", color: "#2980B9" },
done: { icon: "✅", text: "Выполнен", color: "#27AE60" },
cancelled: { icon: "❌", text: "Отменён", color: "#C0392B" },
};
function _statusBadge(status, map) {
const s = map[status] || { icon: "•", text: status, color: "#aaa" };
return `${s.icon} ${escHtml(s.text)}`;
}
/* ── Главный экран ─────────────────────────────────────────── */
async function mount(container) {
container.innerHTML = "";
document.body.classList.remove("has-bottom-nav");
document.getElementById("bottom-nav")?.remove();
// Header
const h = el(`
`);
h.querySelector(".podbor-back").addEventListener("click", () => {
haptic && haptic("impact");
history.back();
});
// Фильтр
const filterEl = el(`
`);
const screen = el(``);
container.appendChild(h);
container.appendChild(filterEl);
container.appendChild(screen);
let currentFilter = "active";
const load = async (filter) => {
currentFilter = filter;
screen.innerHTML = ``;
try {
const data = await _api("staff_clients", { filter });
if (data.error) {
screen.innerHTML = `${escHtml(data.error)}
`;
return;
}
_render(screen, data, container);
} catch (e) {
screen.innerHTML = `Ошибка: ${escHtml(e.message)}
`;
}
};
filterEl.querySelectorAll(".sc-filter").forEach(btn => {
btn.addEventListener("click", () => {
filterEl.querySelectorAll(".sc-filter").forEach(b => {
b.style.background = "var(--surface)";
b.style.color = "var(--muted)";
b.style.borderColor = "var(--border)";
});
btn.style.background = "var(--accent)";
btn.style.color = "#fff";
btn.style.borderColor = "var(--accent)";
haptic && haptic("selection");
load(btn.dataset.f);
});
});
h.querySelector("#reloadBtn").addEventListener("click", () => {
haptic && haptic("impact");
load(currentFilter);
});
load("active");
}
/* ── Рендер ─────────────────────────────────────────────────── */
function _render(screen, data, container) {
screen.innerHTML = "";
const clients = data.clients || [];
if (!clients.length) {
screen.appendChild(el(`
📋
Клиентов нет
По выбранному фильтру ничего не найдено
`));
return;
}
// Роль-бейдж в шапке
const roles = [];
if (data.is_assembler) roles.push("сборщик");
if (data.is_measurer) roles.push("замерщик");
if (roles.length) {
screen.appendChild(el(`
${escHtml(roles.join(" · "))} · ${clients.length} клиентов
`));
}
clients.forEach(c => {
const asmCount = c.assemblies.length;
const measCount = c.measurements.length;
// Ближайшая дата
const dates = [
...c.assemblies.map(a => a.scheduled_at),
...c.measurements.map(m => m.scheduled_at),
].filter(Boolean).sort();
const nearestDate = dates[0] || null;
// Статусы для превью
const asmStatuses = c.assemblies.map(a => a.status);
const measStatuses = c.measurements.map(m => m.status);
const card = el(`
${escHtml(c.client_name || "Без имени")}
${c.client_phone ? `
${escHtml(c.client_phone)}
` : ""}
${nearestDate ? `
${escHtml(fmtDate(nearestDate))}
` : ""}
${asmCount ? c.assemblies.map(a => `
${_statusBadge(a.status, ASM_STATUS)}
${a.address ? `${escHtml(a.address.split(",")[0])}` : ""}
`).join("") : ""}
${measCount ? c.measurements.map(m => `
📐 ${_statusBadge(m.status, MEAS_STATUS).replace(/<[^>]+>/g,'').trim()}
`).join("") : ""}
`);
card.addEventListener("click", () => {
haptic && haptic("impact");
_openClientDetail(container, c, data);
});
screen.appendChild(card);
});
screen.appendChild(el(``));
}
/* ── Детальная карточка клиента ────────────────────────────── */
function _openClientDetail(container, c, listData) {
container.innerHTML = "";
const h = el(`
`);
h.querySelector(".podbor-back").addEventListener("click", () => {
haptic && haptic("impact");
mount(container);
});
const screen = el(``);
container.appendChild(h);
container.appendChild(screen);
// Контакты
const phone = c.client_phone || "";
screen.appendChild(el(`
${escHtml(c.client_name || "Без имени")}
${phone ? `
📞
${escHtml(phone)}
` : `
Телефон не указан
`}
`));
// Сборки
if (c.assemblies.length) {
screen.appendChild(el(`🔨 Сборки · ${c.assemblies.length}
`));
c.assemblies.forEach(a => {
const s = ASM_STATUS[a.status] || { icon: "•", text: a.status, color: "#aaa" };
const asmCard = el(`
${s.icon} ${escHtml(s.text)}
${a.address ? `
${escHtml(a.address)}
` : ""}
${a.scope_of_work ? `
${escHtml(a.scope_of_work.slice(0,60))}
` : ""}
${a.scheduled_at ? `
${escHtml(fmtDate(a.scheduled_at))}
` : ""}
${a.signed_by_name ? `
✅ Подписан
` : ""}
`);
asmCard.addEventListener("click", () => {
haptic && haptic("impact");
if (typeof AssemblyDetailScreen !== "undefined") {
location.hash = `#/assembly/${a.id}`;
}
});
screen.appendChild(asmCard);
});
}
// Замеры
if (c.measurements.length) {
screen.appendChild(el(`📐 Замеры · ${c.measurements.length}
`));
c.measurements.forEach(m => {
const s = MEAS_STATUS[m.status] || { icon: "•", text: m.status, color: "#aaa" };
const mCard = el(`
${s.icon} ${escHtml(s.text)}
${m.address ? `
${escHtml(m.address)}
` : ""}
${m.zamer_no ? `
Замер №${escHtml(m.zamer_no)}
` : ""}
${m.scheduled_at ? `
${escHtml(fmtDate(m.scheduled_at))}
` : ""}
`);
screen.appendChild(mCard);
});
}
screen.appendChild(el(``));
}
return { mount };
})();