Compare commits
No commits in common. "a310b0a8b945f3d4ae6a7162089a9c6aab42dde3" and "da8ce9147fc2a68870a86b5143ac66185d710472" have entirely different histories.
a310b0a8b9
...
da8ce9147f
@ -61,15 +61,6 @@ Workflow config is seeded outside this repo, from `sm2/custom-apps/zurich-kotak/
|
||||
- `src/components/ActivityForm.jsx` — renders the **live** activity schema and submits it
|
||||
back. `src/components/FileField.jsx` handles `file`/`ocr` fields;
|
||||
`src/components/Timeline.jsx` renders the audit trail as a story.
|
||||
- `src/api/thread.js` — the WhatsApp conversation, assembled once from the audit rows
|
||||
and shared by the timeline entry and the dialog. `turns` is every message deduped
|
||||
(one submission is recorded up to three times); `episodes` are its contiguous bursts,
|
||||
each carrying the workflow steps that ran before it; `byRow` says which steps sent a
|
||||
message from inside their own trigger. The trail renders ONE entry per burst and never
|
||||
a turn — a burst rather than the whole thread, because the payment exchange is the same
|
||||
conversation resumed twenty minutes and four steps later, and one entry for all of it
|
||||
would date those messages to the moment of the first. `THREAD_FIELDS` is why no step
|
||||
quotes `customer_reply` / `customer_answer` itself: the thread owns them.
|
||||
|
||||
Routing: `/stage/:stageUid`, `/lead/:instanceId`, `/add`; unauthenticated renders
|
||||
`Login` for every path.
|
||||
|
||||
@ -19,40 +19,24 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mast__burger {
|
||||
display: none; /* a phone-only control; see the media block */
|
||||
width: 34px; height: 34px; flex: none;
|
||||
place-items: center; margin-left: -6px;
|
||||
border: 0; border-radius: 8px; background: transparent; color: #fff; cursor: pointer;
|
||||
}
|
||||
.mast__burger svg { width: 20px; height: 20px; }
|
||||
.mast__burger:hover { background: rgba(255, 255, 255, .12); }
|
||||
.mast__burger:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(255, 255, 255, .28); }
|
||||
|
||||
.mast__logo { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||
.mast__logo { display: flex; align-items: center; gap: 10px; }
|
||||
.mast__z {
|
||||
width: 30px; height: 30px; border-radius: 50%;
|
||||
background: #fff; color: var(--zk-navy);
|
||||
font-weight: 700; font-size: 17px;
|
||||
display: grid; place-items: center;
|
||||
}
|
||||
.mast__word b { display: block; white-space: nowrap; font-weight: 700; font-size: 17px; letter-spacing: .06em; line-height: 1.15; }
|
||||
.mast__word b { display: block; font-weight: 700; font-size: 17px; letter-spacing: .06em; line-height: 1.15; }
|
||||
.mast__word b span { font-weight: 500; letter-spacing: 0; }
|
||||
.mast__word small { display: block; white-space: nowrap; font-size: 11.5px; opacity: .75; margin-top: -1px; letter-spacing: .02em; }
|
||||
.mast__word small { display: block; font-size: 11.5px; opacity: .75; margin-top: -1px; letter-spacing: .02em; }
|
||||
|
||||
.mast__sep { width: 1px; height: 26px; background: rgba(255, 255, 255, .22); }
|
||||
|
||||
.mast__app { font-size: 17px; font-weight: 600; white-space: nowrap; }
|
||||
.mast__app { font-size: 17px; font-weight: 600; }
|
||||
.mast__app span { font-weight: 400; opacity: .7; margin-left: 8px; font-size: 15px; }
|
||||
|
||||
.mast__user { margin-left: auto; }
|
||||
|
||||
.shell__scrim {
|
||||
display: none;
|
||||
position: fixed; inset: 56px 0 0 0; z-index: 35;
|
||||
border: 0; padding: 0; background: rgba(11, 42, 91, .35); cursor: default;
|
||||
}
|
||||
|
||||
/* ── Frame ────────────────────────────────────────────────────────────────── */
|
||||
.shell__body { display: flex; flex: 1; min-height: 0; align-items: stretch; }
|
||||
|
||||
@ -149,45 +133,13 @@
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
/* ── PHONE ──────────────────────────────────────────────────────────────
|
||||
The masthead keeps one line and one line only: the mark, the app name,
|
||||
the person. Everything secondary goes, because a 56px bar that wraps is
|
||||
a bar that clips, which is what it did.
|
||||
|
||||
The rail becomes a drawer over the page rather than a block above it —
|
||||
nine queues stacked at the top meant scrolling past the whole menu on
|
||||
every screen before reaching a single lead. */
|
||||
.shell__top { gap: 12px; padding: 0 14px; }
|
||||
.mast__burger { display: grid; }
|
||||
.mast__sep,
|
||||
.mast__word small,
|
||||
.mast__app span { display: none; }
|
||||
.mast__word b { font-size: 15.5px; }
|
||||
.mast__app { font-size: 15.5px; }
|
||||
|
||||
.shell__body { flex-direction: row; }
|
||||
.shell__body { flex-direction: column; }
|
||||
.shell__nav {
|
||||
position: fixed; top: 56px; left: 0; bottom: 0; z-index: 40;
|
||||
width: 268px; max-width: 82vw; min-height: 0; max-height: none;
|
||||
border-right: 1px solid var(--zk-line);
|
||||
transform: translateX(-100%);
|
||||
transition: transform .22s var(--ease);
|
||||
overflow-y: auto; overscroll-behavior: contain;
|
||||
position: static; width: 100%; min-height: 0; max-height: none;
|
||||
border-right: 0; border-bottom: 1px solid var(--zk-line);
|
||||
flex-direction: row; flex-wrap: wrap; padding: 10px 12px;
|
||||
}
|
||||
.shell__nav.is-open { transform: none; box-shadow: 0 12px 40px rgba(11, 42, 91, .22); }
|
||||
.shell__scrim { display: block; }
|
||||
.shell__main { padding: 20px 16px 48px; min-width: 0; flex: 1; }
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
/* Below this the app name and the wordmark cannot both fit beside the user
|
||||
menu without one of them truncating. The mark wins — it is the brand. */
|
||||
.mast__app { display: none; }
|
||||
.um__id { display: none; }
|
||||
.um__caret { display: none; }
|
||||
.um__trigger { padding: 4px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.shell__nav { transition: none; }
|
||||
.nav__h { display: none; }
|
||||
.nav__group { flex-direction: row; flex-wrap: wrap; }
|
||||
.shell__main { padding: 20px 16px 48px; }
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { useState } from 'react'
|
||||
import { NavLink, Outlet } from 'react-router-dom'
|
||||
import { NAV_GROUPS, STAGES } from '../api/config.js'
|
||||
import { rolesOf, visibleStages } from '../api/permissions.js'
|
||||
@ -31,14 +30,6 @@ export default function Shell() {
|
||||
const roles = rolesOf(user)
|
||||
const stages = visibleStages(roles)
|
||||
const { counts, ready } = useStageCounts()
|
||||
// On a phone the rail becomes a drawer. It is closed on every navigation,
|
||||
// because a menu that stays open over the page you just chose is a menu you
|
||||
// have to dismiss twice. Desktop never reads this — the rail is always there.
|
||||
const [navOpen, setNavOpen] = useState(false)
|
||||
// Closing on the link rather than on the route: a menu that stays open over
|
||||
// the page you just chose is a menu you have to dismiss twice. Keyed off the
|
||||
// anchor so opening the Closed fold — which is not navigation — leaves it up.
|
||||
const closeIfLink = (e) => { if (e.target.closest('a')) setNavOpen(false) }
|
||||
|
||||
const count = (s) => counts[s.uid] || { total: 0, waiting: 0, working: 0, urgent: 0 }
|
||||
|
||||
@ -67,17 +58,6 @@ export default function Shell() {
|
||||
return (
|
||||
<div className="shell">
|
||||
<header className="shell__top">
|
||||
<button
|
||||
type="button" className="mast__burger"
|
||||
aria-label={navOpen ? 'Close the menu' : 'Open the menu'} aria-expanded={navOpen}
|
||||
onClick={() => setNavOpen((o) => !o)}
|
||||
>
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true">
|
||||
{navOpen
|
||||
? <path d="M5 5l10 10M15 5L5 15" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||
: <path d="M3 6h14M3 10h14M3 14h14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />}
|
||||
</svg>
|
||||
</button>
|
||||
<div className="mast__logo">
|
||||
<span className="mast__z" aria-hidden="true">Z</span>
|
||||
<div className="mast__word">
|
||||
@ -91,10 +71,7 @@ export default function Shell() {
|
||||
</header>
|
||||
|
||||
<div className="shell__body">
|
||||
{/* The scrim only exists while the drawer is open, and only on a phone:
|
||||
tapping the page you can see should put the menu away. */}
|
||||
{navOpen ? <button type="button" className="shell__scrim" aria-label="Close the menu" onClick={() => setNavOpen(false)} /> : null}
|
||||
<nav className={'shell__nav' + (navOpen ? ' is-open' : '')} aria-label="Pipeline" onClick={closeIfLink}>
|
||||
<nav className="shell__nav" aria-label="Pipeline">
|
||||
<NavLink to="/" end className={({ isActive }) => 'nav__link' + (isActive ? ' is-on' : '')}>
|
||||
<span className="nav__name">Overview</span>
|
||||
</NavLink>
|
||||
|
||||
@ -46,9 +46,8 @@ export default function UserMenu() {
|
||||
// Role slugs read as titles: partner_agent -> Partner agent.
|
||||
const roles = (user?.roles || [])
|
||||
// `sme_underwriter` → "SME underwriter": the acronyms the roles here use stay upper.
|
||||
// `sme_underwriter` → "SME underwriter". Ops is a word, not an acronym.
|
||||
.map((r) => String(r).replace(/_/g, ' ').replace(/^./, (c) => c.toUpperCase())
|
||||
.replace(/\b(sme|uw|rm|posp|csr|kyc)\b/gi, (m) => m.toUpperCase()))
|
||||
.replace(/\b(sme|uw|rm|posp|csr|ops|kyc)\b/gi, (m) => m.toUpperCase()))
|
||||
.join(', ')
|
||||
|
||||
return (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user