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}
+
+
+
+ `).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 () {
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+