Files
crm-embed-test/public/index.html

265 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CRM embed test</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Стили CRM Tvorymo: сначала дизайн-система, затем стили самой CRM (так требует шапка crm.css). -->
<link rel="stylesheet" href="https://test.units.tvorymo.com/tvorymo-crm/css/tvorymo-ui.css">
<link rel="stylesheet" href="https://test.units.tvorymo.com/tvorymo-crm/css/crm.css">
</head>
<body class="side-off">
<main id="main">
<div class="page">
<div class="head">
<div>
<h1>Тест встраивания в&nbsp;CRM</h1>
<div class="sub">Оформлено стилями CRM: <b>tvorymo-ui.css</b> + <b>crm.css</b></div>
</div>
<span class="chip"><span class="syncdot off" id="dot"></span><span id="mode">—</span></span>
</div>
<div class="wsinfo">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16h.01"/></svg>
<div class="grow">
<span id="status" class="chip gold">жду авторизацию от CRM</span>
<div class="small mt-6">Страница получает сессию CRM через <span class="code">postMessage</span> и проверяет токен в Supabase на сервере.</div>
</div>
</div>
<div class="sec"><h2>Пользователь CRM <b id="count">0</b></h2><span class="small mute">токенов получено</span></div>
<div id="info" hidden>
<div class="pcard">
<div class="t1">
<span class="mono s44" id="initials">?</span>
<div class="min-w-0">
<div class="ttl" id="name"></div>
<div class="ds" id="email"></div>
</div>
<div class="n"><b id="mins">—</b><small>мин до exp</small></div>
</div>
<div class="mt">
<span class="chip fill">Supabase ✓</span>
<span class="chip gold">aud: authenticated</span>
<span class="chip" id="changed">токен первый</span>
</div>
<div>
<div class="mrow"><div class="t">ID<small class="code" id="uid"></small></div></div>
<div class="mrow"><div class="t">Origin отправителя<small id="origin"></small></div></div>
<div class="mrow"><div class="t">expires_at<small id="expires"></small></div></div>
</div>
<div class="row wrap">
<button class="btn gold" id="refresh" hidden>
<svg class="ic" viewBox="0 0 24 24"><path d="M20 11a8 8 0 1 0-2.3 5.7M20 4v7h-7"/></svg>
Обновить токен
</button>
</div>
</div>
</div>
<div class="empty" id="noinfo">Пока нет данных. Откройте страницу из боковой панели CRM — она пришлёт <b>crm:auth</b>.</div>
<div class="sec"><h2>Журнал</h2></div>
<div id="log"><div class="empty">событий нет</div></div>
<hr>
<div class="sec"><h2>Демо компонентов CRM</h2><span class="small mute">не функциональны</span></div>
<div class="stat">
<div><b>12</b><small>задач</small></div>
<div><b class="gold">5</b><small>в работе</small></div>
<div><b class="danger">2</b><small>просрочено</small></div>
<div><b class="ok">7</b><small>готово</small></div>
</div>
<div class="f"><label>Кнопки</label></div>
<div class="row wrap mt-6">
<button class="btn gold">Главная</button>
<button class="btn">Обычная</button>
<button class="btn ghost">Ghost</button>
<button class="btn dng">Удалить</button>
<button class="btn sm">Маленькая</button>
<button class="btn" disabled>Disabled</button>
<button class="icon-btn" title="Настройки"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19 12h2M3 12h2M12 3v2M12 19v2M5.6 5.6l1.4 1.4M17 17l1.4 1.4M5.6 18.4 7 17M17 7l1.4-1.4"/></svg></button>
<button class="planbtn on"><svg class="ic" viewBox="0 0 24 24"><path d="M5 12l5 5 9-10"/></svg>В плане</button>
<button class="timebtn est">~1ч 30м</button>
</div>
<div class="f"><label>Бейджи и теги</label></div>
<div class="row wrap mt-6">
<span class="chip">chip</span>
<span class="chip gold">gold</span>
<span class="chip dng">dng</span>
<span class="chip fill">fill</span>
<span class="chip"><span class="dot" style="background:#7DD3FC"></span>с точкой</span>
<span class="tag" style="background:rgba(74,222,128,.16);color:#4ADE80"><span class="d" style="background:#4ADE80"></span>Frontend</span>
<span class="tag" style="background:rgba(255,90,78,.16);color:#FF5A4E"><span class="d" style="background:#FF5A4E"></span>Срочно</span>
</div>
<div class="f"><label>Переключатели</label></div>
<div class="row wrap mt-6 gap-14">
<div class="seg"><button class="on">Мои</button><button>Все</button><button>Архив</button></div>
<button class="switch on" role="switch" aria-checked="true"></button>
<span class="cbx on"><svg class="ic" viewBox="0 0 24 24"><path d="M5 12l5 5 9-10"/></svg></span>
<span class="cbx"><svg class="ic" viewBox="0 0 24 24"><path d="M5 12l5 5 9-10"/></svg></span>
</div>
<div class="f2">
<div class="f"><label>Поле</label><div class="in"><input placeholder="Название задачи" value="Проверить встраивание"></div></div>
<div class="f"><label>Список</label><div class="in"><select><option>Tvorymo shotam</option><option>Другой проект</option></select></div></div>
</div>
<div class="qa mt-14">
<input placeholder="Быстро добавить задачу…">
<button class="go ok"><svg class="ic" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></button>
</div>
<div class="sec"><h2>Канбан</h2></div>
<div class="board">
<div class="col">
<div class="ch"><h2>В работе</h2><span class="n">2</span><span class="sum">3ч</span></div>
<div class="cards">
<div class="tcard plan">
<div class="t">Передавати авторизацію CRM сайтам в iframe через postMessage</div>
<div class="m"><span class="chip gold">Tvorymo shotam</span><span>сегодня</span></div>
<div class="sb"><div class="pbar"><i style="width:66%"></i></div><span class="cnt">2/3</span></div>
<div class="ft">
<span class="by"><i class="av" style="background:#FFCA40">О</i></span>
<span class="to"><i class="av" style="background:#7DD3FC">М</i><i class="av" style="background:#4ADE80">Л</i></span>
</div>
</div>
<div class="tcard over-due">
<div class="t">Подключить стили CRM на тестовой странице</div>
<div class="m"><span class="ov">просрочено</span><span class="chip dng">блокирует</span></div>
</div>
</div>
<button class="add">+ Задача</button>
</div>
<div class="col">
<div class="ch"><h2>Готово</h2><span class="n">1</span></div>
<div class="cards">
<div class="tcard done">
<div class="t">Задеплоить test-crm.tes.gd</div>
<div class="sb"><div class="pbar"><i style="width:100%"></i></div><span class="cnt full">3/3</span></div>
</div>
</div>
</div>
</div>
</div>
</main>
<script>
(() => {
// Сессия только в памяти, токен не выводится и не логируется.
let session = null; // { token, expiresAt }
let crmSource = null, crmOrigin = null, count = 0, logged = false;
const $ = (id) => document.getElementById(id);
const inIframe = window.parent !== window;
// kind: "wait" | "ok" | "err"
function setStatus(text, kind) {
const s = $("status");
s.textContent = text;
s.className = "chip " + (kind === "ok" ? "fill" : kind === "err" ? "dng" : "gold");
$("dot").className = "syncdot" + (kind === "ok" ? "" : " off");
}
function log(text) {
if (!logged) { $("log").textContent = ""; logged = true; }
const row = document.createElement("div");
row.className = "arcitem";
const s = document.createElement("span"); s.className = "s";
s.textContent = new Date().toLocaleTimeString().slice(0, 5);
const t = document.createElement("span"); t.className = "t"; t.textContent = text;
row.append(s, t);
$("log").prepend(row);
}
function toDate(v) {
if (v == null) return null;
const n = Number(v);
const d = new Date(Number.isFinite(n) ? (n < 1e12 ? n * 1000 : n) : v);
return isNaN(d) ? null : d;
}
function fmtTime(v) {
const d = toDate(v);
return d ? d.toLocaleString() + " (" + d.toISOString() + ")" : (v == null ? "—" : String(v));
}
function initials(s) {
return (s || "?").split(/[\s@._-]+/).filter(Boolean).slice(0, 2).map((w) => w[0]).join("").toUpperCase() || "?";
}
async function onAuth(data, ev) {
crmSource = ev.source; crmOrigin = ev.origin;
$("origin").textContent = ev.origin;
const token = data.access_token;
if (typeof token !== "string" || !token) { setStatus("crm:auth без access_token", "err"); return; }
const changed = session && session.token !== token;
const prevExp = session && session.expiresAt;
session = { token, expiresAt: data.expires_at };
count++;
setStatus("проверяю токен в Supabase…", "wait");
try {
const r = await fetch("/api/verify", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ access_token: token }),
});
const res = await r.json();
if (!res.ok) { setStatus("Токен не принят: " + res.error, "err"); log("crm:auth от " + ev.origin + " — отклонён"); return; }
$("name").textContent = res.name || "имя не задано в Supabase";
$("email").textContent = res.email || "—";
$("initials").textContent = initials(res.name || res.email);
$("uid").textContent = res.id;
const exp = data.expires_at != null ? data.expires_at : res.exp;
$("expires").textContent = fmtTime(exp);
const d = toDate(exp);
$("mins").textContent = d ? String(Math.max(0, Math.round((d - Date.now()) / 60000))) : "—";
$("count").textContent = String(count);
$("changed").textContent = changed ? "токен сменился" : count > 1 ? "тот же токен" : "токен первый";
$("changed").className = changed ? "chip fill" : "chip";
$("info").hidden = false; $("noinfo").hidden = true; $("refresh").hidden = false;
setStatus(changed ? "Токен сменился, пользователь подтверждён Supabase" : "Пользователь подтверждён Supabase", "ok");
log("crm:auth от " + ev.origin + (changed
? " — новый токен, expires_at: " + fmtTime(prevExp) + " → " + fmtTime(data.expires_at)
: count > 1 ? " — тот же токен" : " — принят"));
} catch (e) {
setStatus("Ошибка проверки токена", "err");
}
}
window.addEventListener("message", (ev) => {
const data = ev.data;
if (!data || typeof data !== "object") return;
// Для теста принимаем от любого origin (решение владельца).
if (data.type === "crm:auth") onAuth(data, ev);
else if (data.type === "crm:theme") onTheme(data);
});
// Тема и палитра CRM: дальше всё делают селекторы tvorymo-ui.css ([data-theme][data-pal]).
function onTheme(data) {
const theme = data.theme === "dark" ? "dark" : "light";
const pal = typeof data.palette === "string" && data.palette ? data.palette : "classic";
document.documentElement.dataset.theme = theme;
document.documentElement.dataset.pal = pal;
log("crm:theme " + theme + "/" + pal);
}
$("refresh").addEventListener("click", () => {
const target = crmSource || window.parent;
target.postMessage({ type: "crm:refresh" }, crmOrigin || "*");
setStatus("запросил новый токен (crm:refresh)…", "wait");
log("отправлен crm:refresh → " + (crmOrigin || "*"));
});
$("mode").textContent = inIframe ? "в iframe" : "без iframe";
if (inIframe) {
window.parent.postMessage({ type: "crm:ready", version: 1 }, "*");
log("отправлен crm:ready");
} else {
log("страница открыта не в iframe");
}
})();
</script>
</body>
</html>