diff --git a/miniapp/assets/clients.js b/miniapp/assets/clients.js index 862415e..cfda357 100644 --- a/miniapp/assets/clients.js +++ b/miniapp/assets/clients.js @@ -127,73 +127,252 @@ const Clients = (function () { return; } + // Шапка + const phoneNorm = (client.client_phone || "").replace(/[^\d+]/g, ""); + const callHref = phoneNorm ? `tel:${phoneNorm}` : ""; root.appendChild(el(`
${initial(client.client_name)}
-
+

${escHtml(client.client_name)}

${client.client_phone ? `
${escHtml(client.client_phone)}
` : ""}
+ ${callHref ? `📞` : ""}
`)); - // Примечание менеджера — текст или голосовой ввод + // Быстрые действия для менеджера + const actionsRow = el(` +
+ + + +
+ `); + actionsRow.querySelectorAll(".qa-btn").forEach(btn => { + btn.addEventListener("click", () => { + haptic && haptic("impact"); + const act = btn.dataset.act; + if (act === "podbor") { + location.hash = `#/podbor?client_name=${encodeURIComponent(client.client_name || "")}&client_phone=${encodeURIComponent(client.client_phone || "")}`; + } else if (act === "measure") { + // Pre-fill request with client info + sessionStorage.setItem("prefillClient", JSON.stringify({ + name: client.client_name, phone: client.client_phone, + })); + location.hash = "#/request"; + } else if (act === "copy") { + const txt = `${client.client_name || ""} ${client.client_phone || ""}`.trim(); + (navigator.clipboard?.writeText(txt) || Promise.resolve()) + .then(() => tg?.showAlert?.("Скопировано")); + } + }); + }); + root.appendChild(actionsRow); + + // Примечание менеджера с голосовым вводом root.appendChild(renderClientNoteBlock(client)); - root.appendChild(el(`
Подборы · ${client.leads_count}
`)); + // Хронология + Файлы — собираются после загрузки замеров + const timelinePlaceholder = el(`
`); + const filesPlaceholder = el(`
`); + const detailsPlaceholder = el(`
`); + root.appendChild(timelinePlaceholder); + root.appendChild(filesPlaceholder); + root.appendChild(detailsPlaceholder); - const leadsList = el(`
`); - for (const lead of client.leads) { - const item = el(` - - `); - item.addEventListener("click", () => { - haptic && haptic("impact"); - location.hash = `#/clients/lead/${lead.id}`; - }); - leadsList.appendChild(item); - } - root.appendChild(leadsList); - - // Замеры этого клиента (если есть) + let myMeasurements = []; try { const ms = await fetchMeasurements({ client_tg_id: client.client_tg_id || "" }); - const myMeasurements = (ms.measurements || []).filter(m => { - // Если client_tg_id зарегистрирован — фильтруем по нему + myMeasurements = (ms.measurements || []).filter(m => { if (client.client_tg_id) return String(m.client_tg_id) === String(client.client_tg_id); - // Иначе — ищем имя клиента в notes (упрощённая логика для новых клиентов) return (m.notes || "").toLowerCase().includes((client.client_name || "").toLowerCase()); }); - if (myMeasurements.length) { - root.appendChild(el(`
Замеры · ${myMeasurements.length}
`)); - const mList = el(`
`); - for (const m of myMeasurements) { - const photoCount = m.photo_count || (m.photos || []).length; - const photoBadge = photoCount ? ` · 📷 ${photoCount}` : ""; - const item = el(` - - `); - item.addEventListener("click", () => { - haptic && haptic("impact"); - location.hash = `#/clients/measurement/${m.id}`; - }); - mList.appendChild(item); - } - root.appendChild(mList); - } - } catch (e) { - // Игнорируем — секция замеров просто не покажется + } catch (e) { /* пусто */ } + + // Хронология + timelinePlaceholder.replaceWith(renderClientTimeline(client, myMeasurements)); + // Файлы + filesPlaceholder.replaceWith(renderClientFiles(client, myMeasurements)); + // Детальные списки внизу (свёрнуты) + detailsPlaceholder.replaceWith(renderClientDetails(client, myMeasurements)); + } + + /* ===================== Хронология ===================== */ + + function renderClientTimeline(client, measurements) { + // Собираем события из лидов и замеров + const events = []; + + for (const lead of client.leads || []) { + events.push({ + ts: lead.created_at, + icon: "🤖", + title: "Подбор техники", + sub: `#${(lead.id || "").slice(0, 8)} · ${statusLabel(lead.status)}`, + href: `#/clients/lead/${lead.id}`, + }); } + + for (const m of measurements) { + const photoCount = m.photo_count || (m.photos || []).length; + // Создание заявки / замера + events.push({ + ts: m.created_at, + icon: m.status === "requested" ? "📋" : "📐", + title: m.status === "requested" ? "Заявка на замер" : "Замер создан", + sub: m.address ? escHtml(m.address) : (m.status === "requested" ? "ожидает согласования" : ""), + href: `#/clients/measurement/${m.id}`, + }); + // Если назначен — отдельное событие на момент scheduled_at + if (m.scheduled_at) { + events.push({ + ts: m.scheduled_at, + icon: "📅", + title: "Замер назначен", + sub: formatDate(m.scheduled_at) + (m.address ? " · " + escHtml(m.address) : ""), + href: `#/clients/measurement/${m.id}`, + }); + } + // Если завершён — отдельное событие + if (m.status === "completed") { + events.push({ + ts: m.created_at, // нет updated_at, используем created + icon: "✅", + title: "Замер выполнен", + sub: `${photoCount} фото` + (m.area_m2 ? ` · ${m.area_m2} м²` : ""), + href: `#/clients/measurement/${m.id}`, + }); + } + } + + events.sort((a, b) => (b.ts || "").localeCompare(a.ts || "")); + + const section = el(` +
+
🕒 Хронология · ${events.length}
+ ${events.length === 0 + ? `
Пока нет событий
` + : ``} +
+ `); + return section; + } + + /* ===================== Файлы клиента ===================== */ + + function renderClientFiles(client, measurements) { + const groups = []; + for (const m of measurements) { + const photos = m.photos || []; + if (photos.length) { + groups.push({ + title: `📐 Замер от ${formatDate(m.created_at)}`, + sub: `${photos.length} фото` + (m.area_m2 ? ` · ${m.area_m2} м²` : ""), + photos, + measurement_id: m.id, + }); + } + } + const totalPhotos = groups.reduce((s, g) => s + g.photos.length, 0); + + const section = el(` +
+
📂 Файлы · ${totalPhotos}
+ ${groups.length === 0 + ? `
Файлов нет. Появятся после замера и подбора.
` + : groups.map(g => ` +
+
+ ${g.title} + ${g.sub} +
+
+ ${g.photos.slice(0, 6).map((fn, i) => ` + + + + `).join("")} + ${g.photos.length > 6 ? `+${g.photos.length - 6}` : ""} +
+
+ `).join("")} +
+ `); + return section; + } + + /* ===================== Детальные списки (свёрнутые) ===================== */ + + function renderClientDetails(client, measurements) { + const wrap = el(`
`); + + // Подборы + if ((client.leads || []).length) { + const detailsLeads = el(` +
+ Подборы · ${client.leads_count} +
+
+ `); + const list = detailsLeads.querySelector(".leads-list"); + for (const lead of client.leads) { + const item = el(` + + `); + item.addEventListener("click", () => { + haptic && haptic("impact"); + location.hash = `#/clients/lead/${lead.id}`; + }); + list.appendChild(item); + } + wrap.appendChild(detailsLeads); + } + + // Замеры + if (measurements.length) { + const detailsMs = el(` +
+ Замеры · ${measurements.length} +
+
+ `); + const list = detailsMs.querySelector(".leads-list"); + for (const m of measurements) { + const photoCount = m.photo_count || (m.photos || []).length; + const photoBadge = photoCount ? ` · 📷 ${photoCount}` : ""; + const item = el(` + + `); + item.addEventListener("click", () => { + haptic && haptic("impact"); + location.hash = `#/clients/measurement/${m.id}`; + }); + list.appendChild(item); + } + wrap.appendChild(detailsMs); + } + + return wrap; } function layoutLabel(key) { diff --git a/miniapp/assets/podbor.css b/miniapp/assets/podbor.css index 2e9c941..0e6722d 100644 --- a/miniapp/assets/podbor.css +++ b/miniapp/assets/podbor.css @@ -2066,6 +2066,154 @@ flex-shrink: 0; } +/* ===== Карточка клиента: шапка + действия ===== */ +.client-detail-head { position: relative; display: flex; align-items: center; gap: 14px; } +.client-call-btn { + display: grid; + place-items: center; + width: 44px; + height: 44px; + border-radius: 50%; + background: var(--walnut, #6B4A2B); + color: var(--paper, #FBF7F0); + text-decoration: none; + font-size: 20px; + flex-shrink: 0; + box-shadow: 0 2px 6px rgba(107, 74, 43, 0.25); +} +.client-call-btn:active { transform: scale(0.95); } + +.client-quick-actions { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 8px; + margin: 14px 0 18px; +} +.client-quick-actions .qa-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 12px 6px; + background: var(--card, #fff); + border: 1px solid rgba(107, 74, 43, 0.18); + border-radius: 12px; + cursor: pointer; + font-family: inherit; + font-size: 19px; + line-height: 1; +} +.client-quick-actions .qa-btn:active { background: var(--paper-2, #F5EDDC); } +.client-quick-actions .qa-btn span { + font-size: 11px; + font-weight: 500; + color: var(--ink, #1F1A14); + margin-top: 4px; + line-height: 1.2; + text-align: center; +} + +/* ===== Хронология клиента ===== */ +.client-timeline-block .timeline { padding: 10px 4px 4px; position: relative; } +.client-timeline-block .timeline::before { + content: ""; + position: absolute; + left: 13px; + top: 18px; + bottom: 18px; + width: 1px; + background: rgba(107, 74, 43, 0.18); +} +.tl-item { + display: flex; + gap: 14px; + padding: 8px 0; + text-decoration: none; + color: inherit; + position: relative; + cursor: pointer; +} +.tl-item:active { background: rgba(107, 74, 43, 0.04); border-radius: 6px; } +.tl-dot { + width: 11px; + height: 11px; + border-radius: 50%; + background: var(--walnut, #6B4A2B); + flex-shrink: 0; + margin-top: 8px; + z-index: 1; + box-shadow: 0 0 0 3px var(--paper, #FBF7F0); +} +.tl-content { flex: 1; min-width: 0; } +.tl-date { + font-family: var(--font-mono, "JetBrains Mono", monospace); + font-size: 10px; + color: var(--muted, #998877); + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 2px; +} +.tl-title { font-size: 14px; font-weight: 500; color: var(--ink, #1F1A14); } +.tl-icon { margin-right: 6px; } +.tl-sub { font-size: 12px; color: var(--muted, #998877); margin-top: 2px; } + +/* ===== Файлы клиента ===== */ +.client-files-block .file-group { padding: 8px 4px 12px; } +.client-files-block .file-group-head { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 8px; + font-size: 13px; + font-weight: 500; + color: var(--ink, #1F1A14); +} +.file-thumbs { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); + gap: 6px; +} +.file-thumb { + display: block; + aspect-ratio: 1 / 1; + border-radius: 8px; + overflow: hidden; + background: var(--warm, rgba(107, 74, 43, 0.08)); + border: 1px solid rgba(107, 74, 43, 0.15); +} +.file-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; } +.file-thumb.more { + display: grid; + place-items: center; + background: rgba(107, 74, 43, 0.10); + color: var(--walnut, #6B4A2B); + font-weight: 600; + font-size: 14px; + text-decoration: none; +} + +/* ===== Свёрнутые детали (подборы / замеры) ===== */ +.client-details { margin-top: 14px; } +.client-details-collapse { + background: var(--card, #fff); + border: 1px solid rgba(107, 74, 43, 0.12); + border-radius: 10px; + margin-bottom: 10px; + overflow: hidden; +} +.client-details-collapse summary { + padding: 12px 14px; + font-size: 13px; + font-weight: 600; + color: var(--ink, #1F1A14); + cursor: pointer; + user-select: none; + font-family: inherit; + letter-spacing: 0.02em; +} +.client-details-collapse[open] summary { border-bottom: 1px solid rgba(107, 74, 43, 0.10); } +.client-details-collapse .leads-list { padding: 4px 8px 8px; } + /* ===== Примечание по клиенту ===== */ .client-note-block .block-head { display: flex; diff --git a/miniapp/assets/request.js b/miniapp/assets/request.js index dfb06a6..335c808 100644 --- a/miniapp/assets/request.js +++ b/miniapp/assets/request.js @@ -24,6 +24,16 @@ const MeasurementRequest = (function () { client_name: "", client_phone: "", address: "", assigned_to_tg_id: "", preferred_note: "", }; + // Prefill из карточки клиента (sessionStorage перед navigate) + try { + const raw = sessionStorage.getItem("prefillClient"); + if (raw) { + const pre = JSON.parse(raw); + if (pre.name) state.client_name = pre.name; + if (pre.phone) state.client_phone = pre.phone; + sessionStorage.removeItem("prefillClient"); + } + } catch (e) {} render(); loadMeasurers(); } @@ -41,7 +51,7 @@ const MeasurementRequest = (function () {
@@ -49,7 +59,7 @@ const MeasurementRequest = (function () {
@@ -58,7 +68,7 @@ const MeasurementRequest = (function () {
@@ -228,6 +238,7 @@ const MeasurementRequest = (function () { .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } + function escAttr(s) { return escHtml(s); } return { mount }; })(); diff --git a/miniapp/index.html b/miniapp/index.html index edde1cf..43e87b7 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -12,8 +12,8 @@ - - + + @@ -31,14 +31,14 @@
Сделано с душой!
- - - - - - - - - + + + + + + + + +