/* ============================================================ 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(`
${escHtml(c.client_name || "Клиент")}
`); 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 }; })();