mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 15:44:47 +00:00
client card D1: хронология + файлы + быстрые действия
Карточка клиента (#/clients/client/<key>) переработана: 1. Шапка с большой круглой кнопкой 📞 справа — звонок одним тапом через tel: ссылку. 2. Быстрые действия (3 в ряд): - 🤖 Подбор техники — переход в #/podbor с pre-fill ФИО/телефона - 📐 Заказать замер — переход в #/request с pre-fill (через sessionStorage) - 📋 Копировать ФИО+тел — clipboard для пересылки коллеге 3. Примечание менеджера (с голосовым вводом) — уже было. 4. 🕒 Хронология — единая лента событий: - Лиды (подборы) с датой создания и статусом - Заявки на замер - Назначенные замеры (на дату scheduled_at) - Завершённые замеры События отсортированы по дате desc, рядом крупная точка timeline, тап → переход к детали. 5. 📂 Файлы — группы по замерам с миниатюрами фото (до 6 в ряд + плашка «+N» если больше). Тап на миниатюру открывает в новом окне (фото открывается в полный размер). 6. Свёрнутые секции «Подборы» и «Замеры» внизу — для тех кому нужны плоские списки. Cache bust v=20260513za.
This commit is contained in:
parent
366625be66
commit
8201fee9f2
@ -127,73 +127,252 @@ const Clients = (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
// Шапка
|
||||
const phoneNorm = (client.client_phone || "").replace(/[^\d+]/g, "");
|
||||
const callHref = phoneNorm ? `tel:${phoneNorm}` : "";
|
||||
root.appendChild(el(`
|
||||
<div class="client-detail-head">
|
||||
<div class="client-avatar lg">${initial(client.client_name)}</div>
|
||||
<div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<h2 class="client-detail-name">${escHtml(client.client_name)}</h2>
|
||||
${client.client_phone ? `<div class="client-detail-phone">${escHtml(client.client_phone)}</div>` : ""}
|
||||
</div>
|
||||
${callHref ? `<a class="client-call-btn" href="${callHref}" aria-label="Позвонить">📞</a>` : ""}
|
||||
</div>
|
||||
`));
|
||||
|
||||
// Примечание менеджера — текст или голосовой ввод
|
||||
// Быстрые действия для менеджера
|
||||
const actionsRow = el(`
|
||||
<div class="client-quick-actions">
|
||||
<button class="qa-btn" data-act="podbor">🤖<span>Подбор техники</span></button>
|
||||
<button class="qa-btn" data-act="measure">📐<span>Заказать замер</span></button>
|
||||
<button class="qa-btn" data-act="copy">📋<span>Копировать ФИО+тел</span></button>
|
||||
</div>
|
||||
`);
|
||||
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(`<div class="section-head"><span class="label">Подборы · ${client.leads_count}</span></div>`));
|
||||
// Хронология + Файлы — собираются после загрузки замеров
|
||||
const timelinePlaceholder = el(`<div id="clTimelinePlaceholder"></div>`);
|
||||
const filesPlaceholder = el(`<div id="clFilesPlaceholder"></div>`);
|
||||
const detailsPlaceholder = el(`<div id="clDetailsPlaceholder"></div>`);
|
||||
root.appendChild(timelinePlaceholder);
|
||||
root.appendChild(filesPlaceholder);
|
||||
root.appendChild(detailsPlaceholder);
|
||||
|
||||
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);
|
||||
|
||||
// Замеры этого клиента (если есть)
|
||||
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(`<div class="section-head" style="margin-top:24px;"><span class="label">Замеры · ${myMeasurements.length}</span></div>`));
|
||||
const mList = el(`<div class="leads-list"></div>`);
|
||||
for (const m of myMeasurements) {
|
||||
const photoCount = m.photo_count || (m.photos || []).length;
|
||||
const photoBadge = photoCount ? ` · 📷 ${photoCount}` : "";
|
||||
const item = el(`
|
||||
<button class="lead-item">
|
||||
<div class="lead-date">${formatDate(m.created_at)}</div>
|
||||
<div class="lead-id">${escHtml(layoutLabel(m.layout))}</div>
|
||||
<div class="lead-status">${m.area_m2 ? m.area_m2 + " м²" : "—"}${photoBadge}</div>
|
||||
<div class="lead-arrow">${ICONS.chevron || "›"}</div>
|
||||
</button>
|
||||
`);
|
||||
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(`
|
||||
<section class="block client-timeline-block">
|
||||
<div class="block-head">🕒 Хронология · ${events.length}</div>
|
||||
${events.length === 0
|
||||
? `<div class="empty" style="padding:14px;text-align:center;color:var(--muted);font-size:13px;">Пока нет событий</div>`
|
||||
: `<div class="timeline">${events.map(ev => `
|
||||
<a class="tl-item" ${ev.href ? `href="${ev.href}"` : ""}>
|
||||
<div class="tl-dot"></div>
|
||||
<div class="tl-content">
|
||||
<div class="tl-date">${formatDate(ev.ts)}</div>
|
||||
<div class="tl-title"><span class="tl-icon">${ev.icon}</span>${ev.title}</div>
|
||||
${ev.sub ? `<div class="tl-sub">${ev.sub}</div>` : ""}
|
||||
</div>
|
||||
</a>
|
||||
`).join("")}</div>`}
|
||||
</section>
|
||||
`);
|
||||
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(`
|
||||
<section class="block client-files-block">
|
||||
<div class="block-head">📂 Файлы · ${totalPhotos}</div>
|
||||
${groups.length === 0
|
||||
? `<div class="empty" style="padding:14px;text-align:center;color:var(--muted);font-size:13px;">Файлов нет. Появятся после замера и подбора.</div>`
|
||||
: groups.map(g => `
|
||||
<div class="file-group">
|
||||
<div class="file-group-head">
|
||||
<span>${g.title}</span>
|
||||
<span class="muted" style="font-size:11px;">${g.sub}</span>
|
||||
</div>
|
||||
<div class="file-thumbs" data-mid="${g.measurement_id}">
|
||||
${g.photos.slice(0, 6).map((fn, i) => `
|
||||
<a class="file-thumb" href="${BACKEND_URL}/api/photo/${g.measurement_id}/${fn}" target="_blank" rel="noopener">
|
||||
<img src="${BACKEND_URL}/api/photo/${g.measurement_id}/${fn}" alt="">
|
||||
</a>
|
||||
`).join("")}
|
||||
${g.photos.length > 6 ? `<a class="file-thumb more" href="#/clients/measurement/${g.measurement_id}">+${g.photos.length - 6}</a>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</section>
|
||||
`);
|
||||
return section;
|
||||
}
|
||||
|
||||
/* ===================== Детальные списки (свёрнутые) ===================== */
|
||||
|
||||
function renderClientDetails(client, measurements) {
|
||||
const wrap = el(`<div class="client-details"></div>`);
|
||||
|
||||
// Подборы
|
||||
if ((client.leads || []).length) {
|
||||
const detailsLeads = el(`
|
||||
<details class="client-details-collapse">
|
||||
<summary>Подборы · ${client.leads_count}</summary>
|
||||
<div class="leads-list"></div>
|
||||
</details>
|
||||
`);
|
||||
const list = detailsLeads.querySelector(".leads-list");
|
||||
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}`;
|
||||
});
|
||||
list.appendChild(item);
|
||||
}
|
||||
wrap.appendChild(detailsLeads);
|
||||
}
|
||||
|
||||
// Замеры
|
||||
if (measurements.length) {
|
||||
const detailsMs = el(`
|
||||
<details class="client-details-collapse">
|
||||
<summary>Замеры · ${measurements.length}</summary>
|
||||
<div class="leads-list"></div>
|
||||
</details>
|
||||
`);
|
||||
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(`
|
||||
<button class="lead-item">
|
||||
<div class="lead-date">${formatDate(m.created_at)}</div>
|
||||
<div class="lead-id">${escHtml(layoutLabel(m.layout) || (m.status || ""))}</div>
|
||||
<div class="lead-status">${m.area_m2 ? m.area_m2 + " м²" : "—"}${photoBadge}</div>
|
||||
<div class="lead-arrow">${ICONS.chevron || "›"}</div>
|
||||
</button>
|
||||
`);
|
||||
item.addEventListener("click", () => {
|
||||
haptic && haptic("impact");
|
||||
location.hash = `#/clients/measurement/${m.id}`;
|
||||
});
|
||||
list.appendChild(item);
|
||||
}
|
||||
wrap.appendChild(detailsMs);
|
||||
}
|
||||
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function layoutLabel(key) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 () {
|
||||
<div class="form-row">
|
||||
<label class="field">
|
||||
<span class="field-label">ФИО клиента *</span>
|
||||
<input type="text" data-bind="client_name" placeholder="Иванов Иван Иванович" autocomplete="name">
|
||||
<input type="text" data-bind="client_name" value="${escAttr(state.client_name)}" placeholder="Иванов Иван Иванович" autocomplete="name">
|
||||
<span class="field-error" id="errName"></span>
|
||||
</label>
|
||||
</div>
|
||||
@ -49,7 +59,7 @@ const MeasurementRequest = (function () {
|
||||
<div class="form-row">
|
||||
<label class="field">
|
||||
<span class="field-label">Телефон *</span>
|
||||
<input type="tel" data-bind="client_phone" placeholder="+7 921 555-12-34" autocomplete="tel">
|
||||
<input type="tel" data-bind="client_phone" value="${escAttr(state.client_phone)}" placeholder="+7 921 555-12-34" autocomplete="tel">
|
||||
<span class="field-hint">Минимум 10 цифр</span>
|
||||
<span class="field-error" id="errPhone"></span>
|
||||
</label>
|
||||
@ -58,7 +68,7 @@ const MeasurementRequest = (function () {
|
||||
<div class="form-row">
|
||||
<label class="field">
|
||||
<span class="field-label">Адрес замера</span>
|
||||
<input type="text" data-bind="address" placeholder="СПб, Просвещения 87, кв. 12">
|
||||
<input type="text" data-bind="address" value="${escAttr(state.address)}" placeholder="СПб, Просвещения 87, кв. 12">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@ -228,6 +238,7 @@ const MeasurementRequest = (function () {
|
||||
.replace(/&/g, "&").replace(/</g, "<")
|
||||
.replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
function escAttr(s) { return escHtml(s); }
|
||||
|
||||
return { mount };
|
||||
})();
|
||||
|
||||
@ -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&family=Cormorant+Garamond:ital,wght@1,400;1,500;1,600&display=swap">
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
<link rel="stylesheet" href="assets/styles.css?v=20260513z">
|
||||
<link rel="stylesheet" href="assets/podbor.css?v=20260513z">
|
||||
<link rel="stylesheet" href="assets/styles.css?v=20260513za">
|
||||
<link rel="stylesheet" href="assets/podbor.css?v=20260513za">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Splash — за пределами #app, render-функции его не смывают -->
|
||||
@ -31,14 +31,14 @@
|
||||
<div class="loader-tagline">Сделано с душой!</div>
|
||||
</div>
|
||||
<main id="app"></main>
|
||||
<script src="assets/icons.js?v=20260513z"></script>
|
||||
<script src="assets/podbor.config.js?v=20260513z"></script>
|
||||
<script src="assets/podbor.picts.js?v=20260513z"></script>
|
||||
<script src="assets/podbor.js?v=20260513z"></script>
|
||||
<script src="assets/clients.js?v=20260513z"></script>
|
||||
<script src="assets/zamer-picts.js?v=20260513z"></script>
|
||||
<script src="assets/measurements.js?v=20260513z"></script>
|
||||
<script src="assets/request.js?v=20260513z"></script>
|
||||
<script src="assets/app.js?v=20260513z"></script>
|
||||
<script src="assets/icons.js?v=20260513za"></script>
|
||||
<script src="assets/podbor.config.js?v=20260513za"></script>
|
||||
<script src="assets/podbor.picts.js?v=20260513za"></script>
|
||||
<script src="assets/podbor.js?v=20260513za"></script>
|
||||
<script src="assets/clients.js?v=20260513za"></script>
|
||||
<script src="assets/zamer-picts.js?v=20260513za"></script>
|
||||
<script src="assets/measurements.js?v=20260513za"></script>
|
||||
<script src="assets/request.js?v=20260513za"></script>
|
||||
<script src="assets/app.js?v=20260513za"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user