style(analytics): мобильная раскладка списков воронки — без горизонтальной прокрутки

На 390px таблицы «Не поднимают трубку» и «Открыли ссылку, но не подтвердили»
уезжали вправо (статус и телефон не видны без прокрутки). На мобильном они
раскладываются списком: телефон и число попыток в строку, город/тип/статус и
дата — ниже мелким. В таблице попыток процент ответов уходит во вторую строку,
чтобы колонка «Не взяли» влезла. Тест на мобильную раскладку (8 проверок). SW v125.
This commit is contained in:
Hermes 2026-09-29 10:28:35 +02:00
parent 188db318e3
commit 41ac30c090
3 changed files with 73 additions and 2 deletions

View File

@ -1,7 +1,7 @@
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1"; const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
if (!isLocalhost) { if (!isLocalhost) {
const STATIC_CACHE = "construction-delivery-static-v124"; const STATIC_CACHE = "construction-delivery-static-v125";
const RUNTIME_CACHE = "construction-delivery-runtime-v119"; const RUNTIME_CACHE = "construction-delivery-runtime-v119";
const APP_SHELL_URLS = ["/", "/client", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"]; const APP_SHELL_URLS = ["/", "/client", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];

View File

@ -207,7 +207,10 @@ export const CommsFunnelBlock = ({ funnel, isLoading, mobile = false, periodLabe
<Td mobile={mobile} fontSize={fontSize}>{a.attempt}-я</Td> <Td mobile={mobile} fontSize={fontSize}>{a.attempt}-я</Td>
<Td mobile={mobile} fontSize={fontSize} align="right">{a.total}</Td> <Td mobile={mobile} fontSize={fontSize} align="right">{a.total}</Td>
<Td mobile={mobile} fontSize={fontSize} align="right"> <Td mobile={mobile} fontSize={fontSize} align="right">
{a.answered} <span style={{ color: 'var(--color-text-muted)' }}>({a.answered_pct}%)</span> {a.answered}
{mobile
? <div style={{ fontSize: fontSize.xs, color: 'var(--color-text-muted)' }}>{a.answered_pct}%</div>
: <span style={{ color: 'var(--color-text-muted)' }}> ({a.answered_pct}%)</span>}
</Td> </Td>
<Td mobile={mobile} fontSize={fontSize} align="right">{a.confirmed}</Td> <Td mobile={mobile} fontSize={fontSize} align="right">{a.confirmed}</Td>
<Td mobile={mobile} fontSize={fontSize} align="right" muted>{a.no_answer}</Td> <Td mobile={mobile} fontSize={fontSize} align="right" muted>{a.no_answer}</Td>
@ -237,6 +240,34 @@ export const CommsFunnelBlock = ({ funnel, isLoading, mobile = false, periodLabe
<div style={{ fontSize: fontSize.m, fontWeight: 600, color: 'var(--color-text)', marginBottom: '0.35rem' }}> <div style={{ fontSize: fontSize.m, fontWeight: 600, color: 'var(--color-text)', marginBottom: '0.35rem' }}>
Не поднимают трубку Не поднимают трубку
</div> </div>
{mobile ? (
noAnswer.length === 0 ? (
<div style={{ fontSize: fontSize.s, color: 'var(--color-text-muted)', padding: '0.6rem', textAlign: 'center' }}>
Нет таких номеров за период
</div>
) : (
<div>
{noAnswer.map((n) => (
<div key={n.phone} style={{
padding: '0.45rem 0', borderBottom: '1px solid var(--color-border, rgba(51,65,85,0.35))',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.5rem', alignItems: 'baseline' }}>
<span style={{ fontSize: fontSize.m, color: 'var(--color-text)' }}>{n.phone}</span>
<span style={{ fontSize: fontSize.xs, color: 'var(--color-text-muted)' }}>
попыток: {n.attempts}
</span>
</div>
<div style={{ fontSize: fontSize.xs, color: 'var(--color-text-muted)', marginTop: '0.15rem' }}>
{n.town} · {KIND_LABELS[n.kind] || n.kind} · {STATUS_LABELS[n.status] || n.status || '—'}
</div>
<div style={{ fontSize: fontSize.xs, color: 'var(--color-text-muted)' }}>
последний звонок {fmtDateTime(n.last_call)}
</div>
</div>
))}
</div>
)
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}> <table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead> <thead>
<tr> <tr>
@ -263,6 +294,7 @@ export const CommsFunnelBlock = ({ funnel, isLoading, mobile = false, periodLabe
))} ))}
</tbody> </tbody>
</table> </table>
)}
</div> </div>
{/* ── Открыли ссылку, но не подтвердили ───────────────────────────── */} {/* ── Открыли ссылку, но не подтвердили ───────────────────────────── */}
@ -270,6 +302,32 @@ export const CommsFunnelBlock = ({ funnel, isLoading, mobile = false, periodLabe
<div style={{ fontSize: fontSize.m, fontWeight: 600, color: 'var(--color-text)', marginBottom: '0.35rem' }}> <div style={{ fontSize: fontSize.m, fontWeight: 600, color: 'var(--color-text)', marginBottom: '0.35rem' }}>
Открыли ссылку, но не подтвердили Открыли ссылку, но не подтвердили
</div> </div>
{mobile ? (
openedNoConfirm.length === 0 ? (
<div style={{ fontSize: fontSize.s, color: 'var(--color-text-muted)', padding: '0.6rem', textAlign: 'center' }}>
Все, кто открыл ссылку, подтвердили
</div>
) : (
<div>
{openedNoConfirm.map((o) => (
<div key={o.num + o.phone} style={{
padding: '0.45rem 0', borderBottom: '1px solid var(--color-border, rgba(51,65,85,0.35))',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.5rem', alignItems: 'baseline' }}>
<span style={{ fontSize: fontSize.m, color: 'var(--color-text)' }}>{o.num}</span>
<span style={{ fontSize: fontSize.xs, color: 'var(--color-text-muted)' }}>открытий: {o.opens}</span>
</div>
<div style={{ fontSize: fontSize.xs, color: 'var(--color-text-muted)', marginTop: '0.15rem' }}>
{o.name} · {o.phone}
</div>
<div style={{ fontSize: fontSize.xs, color: 'var(--color-text-muted)' }}>
последнее открытие {fmtDateTime(o.opened)}
</div>
</div>
))}
</div>
)
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}> <table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead> <thead>
<tr> <tr>
@ -294,6 +352,7 @@ export const CommsFunnelBlock = ({ funnel, isLoading, mobile = false, periodLabe
))} ))}
</tbody> </tbody>
</table> </table>
)}
</div> </div>
</Panel> </Panel>
); );

View File

@ -97,6 +97,18 @@ describe("CommsFunnelBlock", () => {
expect(empty).toContain("Подтверждений за период нет"); expect(empty).toContain("Подтверждений за период нет");
}); });
it("на мобильном списки «не берут трубку» и «открыли без подтверждения» раскладываются без прокрутки", () => {
const m = renderToStaticMarkup(<CommsFunnelBlock funnel={funnel} mobile periodLabel="30 дней" />);
expect(m).toContain("попыток: 2");
expect(m).toContain("Севастополь · самовывоз · В ручной работе");
expect(m).toContain("последний звонок");
expect(m).toContain("открытий: 14");
expect(m).toContain("ЗАКАЗЧИК · 9788324113");
// на мобильном процент по попыткам уходит во вторую строку, а не в скобках
expect(m).not.toContain("(65.6%)");
expect(m).toContain("65.6%");
});
it("показывает заглушку загрузки, пока данных нет", () => { it("показывает заглушку загрузки, пока данных нет", () => {
const loading = renderToStaticMarkup(<CommsFunnelBlock funnel={null} isLoading mobile={false} periodLabel="7 дней" />); const loading = renderToStaticMarkup(<CommsFunnelBlock funnel={null} isLoading mobile={false} periodLabel="7 дней" />);
expect(loading).toContain("Воронка коммуникаций"); expect(loading).toContain("Воронка коммуникаций");