feat(portal): rebuild staff operational workspace
Some checks are pending
ci / verify (push) Waiting to run

This commit is contained in:
Ochenstarik 2026-09-26 14:13:21 +07:00
parent 778ba2d15b
commit 331e4909c9
4 changed files with 205 additions and 24 deletions

View file

@ -17,5 +17,15 @@ main{width:min(100%,1480px);margin:0 auto;padding:var(--omni-space-5)}.app-view{
.auth-screen{min-height:100vh;display:grid;place-items:center;padding:var(--omni-space-6);background:var(--omni-bg-workspace)}.auth-card{width:min(100%,460px);padding:var(--omni-space-8);border:1px solid var(--omni-border);border-radius:var(--omni-radius-xl);background:var(--omni-surface-raised);box-shadow:var(--omni-shadow-strong)}.auth-brand{display:flex;align-items:center;gap:var(--omni-space-3);margin-bottom:var(--omni-space-8);font-size:1.2rem}.auth-card h1{margin:var(--omni-space-1) 0 var(--omni-space-3);font-size:1.875rem}.auth-copy{margin:0 0 var(--omni-space-5);color:var(--omni-text-secondary)}.auth-card form{display:grid;gap:var(--omni-space-2)}.auth-card label{font-size:var(--omni-text-label);font-weight:700}.auth-card input,.auth-card button{width:100%;min-height:48px}.auth-card small{display:block;margin-top:var(--omni-space-4);color:var(--omni-text-secondary)}@keyframes view-enter{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
@media(max-width:900px){:root{--omni-control-height:44px;--omni-control-height-sm:40px;--omni-row-height:48px}.topbar{align-items:flex-start;flex-wrap:wrap;padding:var(--omni-space-3) var(--omni-space-4)}.actions{width:100%;justify-content:flex-start;overflow-x:auto}.section-nav{top:108px;padding-inline:var(--omni-space-4)}.period-control{margin-left:var(--omni-space-2)}main{padding:var(--omni-space-4)}.layout,.finance-layout{grid-template-columns:1fr}.stats{grid-template-columns:repeat(2,minmax(0,1fr))}.inline-form{grid-template-columns:repeat(2,minmax(0,1fr))}.inline-form button{grid-column:1/-1}.timesheet-create{grid-template-columns:repeat(2,minmax(0,1fr))}.timesheet-create .grow,.timesheet-create button{grid-column:1/-1}}
@media(max-width:600px){.status,.theme-control{display:none}.section-nav{top:112px}.nav-item{min-height:var(--omni-touch-target)}main{padding:var(--omni-space-3)}.hero,.view-head{display:block}.hero{padding:var(--omni-space-4)}.today{margin-top:var(--omni-space-4);padding:var(--omni-space-3) 0 0;border-left:0;border-top:1px solid var(--omni-border)}.view-head button,.view-head .actions{margin-top:var(--omni-space-3)}.stats{grid-template-columns:1fr}.panel{padding:var(--omni-space-3)}.event{grid-template-columns:36px minmax(0,1fr)}.event time{grid-column:2}.person{grid-template-columns:36px minmax(0,1fr)}.person button{grid-column:2;width:100%}.directory-toolbar{align-items:stretch;flex-direction:column}.timesheet-create,.inline-form,.form-row-2{grid-template-columns:1fr}.calendar-weekdays,.calendar-grid{min-width:840px;grid-template-columns:repeat(7,120px)}.employee-dialog form{padding:var(--omni-space-4)}}
/* Workspace composition follows the supplied Krona Staff layouts while every visual value stays on Krona Library tokens. */
.platform-bar{min-height:44px;display:grid;grid-template-columns:auto minmax(280px,560px) auto;align-items:center;gap:var(--omni-space-4);padding:var(--omni-space-1) var(--omni-space-5);background:var(--omni-surface-raised);border-bottom:1px solid var(--omni-border-subtle)}.platform-context,.platform-actions{display:flex;align-items:center;gap:var(--omni-space-2);white-space:nowrap}.platform-context{color:var(--omni-text-secondary)}.platform-context strong{color:var(--omni-primary)}.app-grid{min-width:34px;min-height:34px;padding:0;background:transparent;border-color:transparent;color:var(--omni-primary);font-size:1.25rem}.global-search{min-height:34px;display:flex;align-items:center;gap:var(--omni-space-2);padding:0 var(--omni-space-3);border:1px solid var(--omni-border-subtle);border-radius:var(--omni-radius-sm);background:var(--omni-surface-muted);color:var(--omni-text-muted)}.global-search input{width:100%;min-height:30px;padding:0;border:0;background:transparent}.platform-actions{justify-content:flex-end}.module-bar{position:sticky;top:0;z-index:var(--omni-z-sticky);min-height:62px;display:flex;align-items:center;gap:var(--omni-space-5);padding:0 var(--omni-space-5);background:color-mix(in srgb,var(--omni-surface-raised) 96%,transparent);border-bottom:1px solid var(--omni-border-subtle);backdrop-filter:blur(14px)}.module-bar .brand{flex:0 0 auto}.module-bar .brand>span:last-child{display:grid}.module-bar .brand small{color:var(--omni-text-secondary);font-size:.67rem;font-weight:500}.module-bar .section-nav{position:static;flex:1;padding:0;background:transparent;border:0}.module-bar .nav-item{min-height:38px;padding-inline:var(--omni-space-2);font-size:var(--omni-text-label)}.module-bar .nav-item>span{font-size:1rem}
main{width:min(100%,1720px);padding:var(--omni-space-4) var(--omni-space-5) var(--omni-space-8)}.page-title-row{min-height:62px;display:flex;align-items:center;justify-content:space-between;gap:var(--omni-space-4);margin-bottom:var(--omni-space-3)}.page-title-row h1{margin:0;font-size:clamp(1.55rem,2vw,2rem);letter-spacing:-.025em}.page-title-row p{margin:var(--omni-space-1) 0 0;color:var(--omni-text-secondary)}.page-context{display:grid;justify-items:end;color:var(--omni-text-secondary)}.page-context small{color:var(--omni-text-muted)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.text-action{min-height:32px;padding:0;border:0;background:transparent;color:var(--omni-primary)}.text-action:hover{background:transparent;color:var(--omni-primary-hover)}
.metric-strip{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:var(--omni-space-3);margin-bottom:var(--omni-space-3)}.metric-strip article{min-height:106px;display:flex;align-items:center;gap:var(--omni-space-3);padding:var(--omni-space-4);border:1px solid var(--omni-border-subtle);border-radius:var(--omni-radius-lg);background:var(--omni-surface-raised);box-shadow:var(--omni-shadow-soft)}.metric-strip small,.metric-strip span{display:block;color:var(--omni-text-secondary)}.metric-strip strong{display:block;margin:var(--omni-space-1) 0;font-size:1.55rem;font-variant-numeric:tabular-nums}.metric-icon{width:46px;height:46px;flex:0 0 auto;display:grid!important;place-items:center;border-radius:var(--omni-radius-pill);background:var(--omni-primary-soft);color:var(--omni-primary)!important;font-size:1.25rem;font-weight:750}.metric-icon.danger{background:var(--omni-danger-soft);color:var(--omni-danger)!important}.compact-metrics{grid-template-columns:repeat(3,minmax(0,1fr))}.compact-metrics article{min-height:84px;padding-block:var(--omni-space-3)}
.dashboard-grid{display:grid;grid-template-columns:minmax(420px,1.45fr) minmax(380px,1.15fr) minmax(290px,.8fr);grid-template-areas:"people attendance terminal" "payroll analytics alerts";gap:var(--omni-space-3)}.dashboard-grid>.panel{min-width:0}.dashboard-people{grid-area:people}.dashboard-attendance{grid-area:attendance}.dashboard-terminal{grid-area:terminal}.dashboard-payroll{grid-area:payroll}.dashboard-analytics{grid-area:analytics}.dashboard-alerts{grid-area:alerts}.dense-list,.alert-list{display:grid;gap:var(--omni-space-1)}.dense-row,.alert-row{min-height:46px;display:grid;grid-template-columns:34px minmax(0,1fr) auto;align-items:center;gap:var(--omni-space-2);padding:var(--omni-space-1) var(--omni-space-2);border-bottom:1px solid var(--omni-border-subtle)}.dense-row:last-child,.alert-row:last-child{border-bottom:0}.dense-row strong,.dense-row small,.alert-row strong,.alert-row small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dense-row small,.alert-row small,.dense-row time{color:var(--omni-text-secondary);font-size:var(--omni-text-label)}.alert-mark{width:28px;height:28px;display:grid;place-items:center;border-radius:var(--omni-radius-pill);background:var(--omni-warning-soft);color:var(--omni-warning);font-weight:800}.terminal-device{display:flex;align-items:center;gap:var(--omni-space-4);padding:var(--omni-space-4);margin-bottom:var(--omni-space-3);background:var(--omni-surface-muted);border-radius:var(--omni-radius-md)}.terminal-device strong,.terminal-device small{display:block}.terminal-device small{color:var(--omni-text-secondary)}.terminal-glyph{width:58px;height:72px;display:grid;place-items:center;border-radius:var(--omni-radius-sm);background:var(--omni-text-primary);color:var(--omni-success);font-size:1.8rem}.terminal-glyph.large{width:88px;height:112px;font-size:2.5rem}.dashboard-terminal dl,.terminal-card dl,.system-list,.inspector-list,.summary-dl{display:grid;gap:var(--omni-space-2);margin:0 0 var(--omni-space-3)}.dashboard-terminal dl div,.terminal-card dl div,.system-list div,.inspector-list div,.summary-dl div{display:flex;justify-content:space-between;gap:var(--omni-space-3);padding-bottom:var(--omni-space-2);border-bottom:1px solid var(--omni-border-subtle)}dl dt{color:var(--omni-text-secondary)}dl dd{margin:0;text-align:right;font-weight:650}.dashboard-totals{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--omni-space-2)}.dashboard-totals>div{padding:var(--omni-space-3);background:var(--omni-surface-muted);border-radius:var(--omni-radius-md)}.dashboard-totals small,.dashboard-totals strong{display:block}.rule-chips{display:flex;flex-wrap:wrap;gap:var(--omni-space-1);margin-top:var(--omni-space-3)}.mini-chart svg{width:100%;height:150px}.mini-chart .chart-legend{margin-top:var(--omni-space-2)}.quick-bar{position:sticky;bottom:var(--omni-space-2);z-index:5;display:flex;align-items:center;gap:var(--omni-space-2);margin-top:var(--omni-space-3);padding:var(--omni-space-2) var(--omni-space-3);border:1px solid var(--omni-border);border-radius:var(--omni-radius-lg);background:color-mix(in srgb,var(--omni-surface-raised) 96%,transparent);box-shadow:var(--omni-shadow-medium);backdrop-filter:blur(12px)}
.employee-workspace{display:grid;grid-template-columns:minmax(620px,1fr) minmax(300px,360px);gap:var(--omni-space-3)}.employee-list-panel{min-width:0}.directory-list{display:grid;gap:0}.directory-list .person{min-height:58px;grid-template-columns:38px minmax(180px,1fr) auto auto;border-width:0 0 1px;border-radius:0;background:transparent;cursor:pointer}.directory-list .person.selected{background:var(--omni-surface-selected);box-shadow:inset 3px 0 var(--omni-primary)}.inspector{position:sticky;top:78px;align-self:start}.inspector-head{display:flex;align-items:center;gap:var(--omni-space-3);padding-bottom:var(--omni-space-4);border-bottom:1px solid var(--omni-border-subtle)}.inspector-head h2{margin:0 0 var(--omni-space-2)}.avatar.large{width:64px;height:64px;font-size:1.35rem}.inspector-list{margin-top:var(--omni-space-4)}.inspector-actions{display:grid;grid-template-columns:1fr 1fr;gap:var(--omni-space-2)}
.segmented{display:inline-flex;align-items:center;padding:2px;border-radius:var(--omni-radius-sm);background:var(--omni-surface-muted)}.segmented button{min-height:34px;background:transparent;border-color:transparent;color:var(--omni-text-secondary)}.segmented button.selected{background:var(--omni-primary);border-color:var(--omni-primary);color:var(--omni-text-inverse)}.attendance-workspace{display:grid;grid-template-columns:minmax(600px,1.5fr) minmax(280px,.55fr);gap:var(--omni-space-3)}.attendance-workspace .event{border-width:0 0 1px;border-radius:0;background:transparent}.summary-dl dd{font-size:1.15rem}.finance-workspace{display:grid;grid-template-columns:minmax(680px,1.5fr) minmax(340px,.65fr);gap:var(--omni-space-3)}.finance-side{display:grid;align-content:start;gap:var(--omni-space-3)}.payroll-workspace{display:grid;grid-template-columns:minmax(720px,1.4fr) minmax(420px,.8fr);gap:var(--omni-space-3)}.payroll-workspace>.panel{min-width:0}.payroll-detail table{min-width:620px}.payroll-row{cursor:pointer}.rules-workspace,.terminal-workspace,.settings-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--omni-space-3)}.rule-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--omni-space-2)}.rule-card{display:grid;grid-template-columns:46px minmax(0,1fr) auto;align-items:center;gap:var(--omni-space-3);padding:var(--omni-space-3);border:1px solid var(--omni-border-subtle);border-radius:var(--omni-radius-md);background:var(--omni-surface)}.rule-card strong,.rule-card small{display:block}.rule-card small{color:var(--omni-text-secondary)}.terminal-card{display:flex;align-items:flex-start;gap:var(--omni-space-5)}.terminal-card h2{margin-top:0}.settings-field{display:grid;gap:var(--omni-space-2);color:var(--omni-text-secondary)}.calendar-legend{display:flex;justify-content:flex-end;gap:var(--omni-space-4);margin-bottom:var(--omni-space-2);color:var(--omni-text-secondary);font-size:var(--omni-text-label)}.calendar-legend i{width:9px;height:9px;display:inline-block;margin-right:var(--omni-space-1);border-radius:var(--omni-radius-pill);background:var(--omni-primary)}.calendar-legend .actual{background:var(--omni-info)}.calendar-legend .warning{background:var(--omni-warning)}.schedule-tabs{margin-bottom:var(--omni-space-3)}
@media(max-width:1380px){.module-bar{align-items:flex-start;flex-direction:column;gap:0;padding-top:var(--omni-space-2)}.module-bar .section-nav{width:100%}.dashboard-grid{grid-template-columns:1fr 1fr;grid-template-areas:"people attendance" "payroll analytics" "terminal alerts"}.metric-strip{grid-template-columns:repeat(3,minmax(0,1fr))}.employee-workspace{grid-template-columns:minmax(560px,1fr) 320px}.finance-workspace,.payroll-workspace{grid-template-columns:1fr}.payroll-detail{max-height:440px}}
@media(max-width:900px){.platform-bar{grid-template-columns:1fr auto}.global-search{grid-column:1/-1;grid-row:2}.module-bar{position:static}.module-bar .section-nav{top:auto}.dashboard-grid{grid-template-columns:1fr;grid-template-areas:"people" "attendance" "terminal" "payroll" "analytics" "alerts"}.metric-strip,.compact-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.employee-workspace,.attendance-workspace,.rules-workspace,.terminal-workspace,.settings-grid{grid-template-columns:1fr}.inspector{position:static}.quick-bar{position:static;flex-wrap:wrap}.finance-workspace{grid-template-columns:1fr}.rule-grid{grid-template-columns:1fr}}
@media(max-width:600px){.platform-context span,.platform-context b,.platform-actions .status{display:none}.platform-bar{padding-inline:var(--omni-space-3)}.module-bar{padding-inline:var(--omni-space-3)}.module-bar .brand{display:none}.page-title-row{align-items:flex-start;flex-direction:column}.page-title-row .actions{width:100%;overflow-x:auto}.metric-strip,.compact-metrics{grid-template-columns:1fr}.metric-strip article{min-height:82px}.dashboard-totals{grid-template-columns:1fr}.employee-workspace{display:block}.inspector{margin-top:var(--omni-space-3)}.directory-toolbar select{width:100%}.directory-list .person{grid-template-columns:36px minmax(0,1fr)}.directory-list .person .tag,.directory-list .person button{grid-column:2}.inspector-actions,.rule-grid{grid-template-columns:1fr}.terminal-card{display:grid}.quick-bar>*{width:100%}}
@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;transition-duration:.01ms!important}}
`;

File diff suppressed because one or more lines are too long

129
src/portal-workspace.ts Normal file
View file

@ -0,0 +1,129 @@
export const portalWorkspaceHtml = `<!doctype html>
<html lang="ru" data-omni-theme="noble-green" data-omni-density="workspace">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#315F46">
<title>Krona Staff</title>
<link rel="manifest" href="/staff/manifest.webmanifest">
<link rel="icon" type="image/svg+xml" href="/staff/icon-192.svg">
<link rel="apple-touch-icon" href="/staff/icon-192.svg">
<link rel="stylesheet" href="/staff/styles.css">
</head>
<body>
<section id="auth-screen" class="auth-screen" aria-labelledby="login-title">
<div class="auth-card">
<div class="auth-brand"><span class="mark">К</span><span>Krona <b>Staff</b></span></div>
<p class="eyebrow">Защищённый кабинет</p>
<h1 id="login-title">Вход в Krona Staff</h1>
<p class="auth-copy">Введите ключ руководителя или личный ключ сотрудника.</p>
<form id="login-form"><label for="login-token">Ключ доступа</label><input id="login-token" type="password" autocomplete="current-password" required minlength="32" autofocus><button id="login-submit" type="submit">Войти</button></form>
<div id="login-error" class="login-error" role="alert" hidden></div>
<small>Сессия действует 8 часов и хранится только в защищённой cookie.</small>
</div>
</section>
<div id="app-shell" hidden>
<header class="platform-bar">
<div class="platform-context"><button class="app-grid" type="button" aria-label="Модули платформы">⠿</button><span>Бизнес-платформа</span><b>/</b><strong>KRONA Staff</strong></div>
<label class="global-search"><span aria-hidden="true">⌕</span><input id="global-search" type="search" placeholder="Поиск по сотрудникам, операциям, документам…" aria-label="Глобальный поиск"></label>
<div class="platform-actions"><span id="connection" class="status">Подключение…</span><button id="refresh" class="quiet compact" type="button">Обновить</button><button id="logout" class="quiet compact" type="button">Выйти</button></div>
</header>
<header class="module-bar">
<a class="brand" href="/staff/dashboard" aria-label="Krona Staff"><span class="mark">К</span><span><b>KRONA Staff</b><small>Люди. Время. Развитие.</small></span></a>
<nav class="section-nav" aria-label="Разделы Krona Staff">
<button class="nav-item selected" type="button" data-target="overview-view" data-route="/staff/dashboard"><span>▦</span>Дашборд</button>
<button class="nav-item manager-only" type="button" data-target="employees-view" data-route="/staff/employees"><span>♟</span>Сотрудники</button>
<button class="nav-item" type="button" data-target="attendance-view" data-route="/staff/attendance"><span>◷</span>Посещаемость</button>
<button class="nav-item" type="button" data-target="shifts-view" data-route="/staff/shifts"><span>◫</span>Смены</button>
<button class="nav-item" type="button" data-target="schedule-view" data-route="/staff/schedules"><span>▦</span>Графики</button>
<button class="nav-item manager-only" type="button" data-target="finance-view" data-route="/staff/finance"><span>◎</span>Финансы</button>
<button class="nav-item" type="button" data-target="payroll-view" data-route="/staff/payroll"><span>▤</span>Зарплата</button>
<button class="nav-item manager-only" type="button" data-target="rules-view" data-route="/staff/rules"><span>⌘</span>Правила</button>
<button class="nav-item manager-only" type="button" data-target="terminal-view" data-route="/staff/terminal"><span>▣</span>Терминал</button>
<button class="nav-item" type="button" data-target="notifications-view" data-route="/staff/notifications"><span>♢</span>Уведомления</button>
<button class="nav-item" type="button" data-target="reports-view" data-route="/staff/reports"><span>▥</span>Отчёты</button>
<button class="nav-item" type="button" data-target="settings-view" data-route="/staff/settings"><span>⚙</span>Настройки</button>
</nav>
</header>
<main>
<div id="message" class="message" hidden></div>
<section id="overview-view" class="app-view">
<div class="page-title-row"><div><h1>Дашборд</h1><p id="viewer">Кабинет руководителя</p></div><div class="page-context"><span id="today">—</span><small id="last-refresh">Данные загружаются</small></div></div>
<h2 id="greeting" class="sr-only">Команда и рабочее время</h2>
<section class="metric-strip" aria-label="Оперативная сводка">
<article><span class="metric-icon">♟</span><div><small>Всего сотрудников</small><strong id="employees-count">—</strong><span>В штате компании</span></div></article>
<article><span class="metric-icon">◷</span><div><small>Отметок сегодня</small><strong id="today-count">—</strong><span>Приходы и уходы</span></div></article>
<article><span class="metric-icon">◫</span><div><small>Часов в табеле</small><strong id="hours-count">—</strong><span>За доступный период</span></div></article>
<article class="manager-only"><span class="metric-icon">◎</span><div><small>Финансовый результат</small><strong id="dashboard-profit">—</strong><span>За выбранный месяц</span></div></article>
<article><span class="metric-icon">!</span><div><small>Требуют внимания</small><strong id="dashboard-attention">—</strong><span>Смены и проверки</span></div></article>
</section>
<div class="dashboard-grid">
<section class="panel dashboard-people"><div class="panel-head"><h2>Сотрудники</h2><button class="text-action" type="button" data-open-view="employees-view">Все сотрудники →</button></div><div id="dashboard-employees" class="dense-list"><div class="empty">Загрузка…</div></div></section>
<section class="panel dashboard-attendance"><div class="panel-head"><h2>Посещаемость сегодня</h2><button class="text-action" type="button" data-open-view="attendance-view">Подробнее →</button></div><div id="dashboard-attendance" class="dense-list"><div class="empty">Загрузка…</div></div></section>
<aside class="panel dashboard-terminal"><div class="panel-head"><h2>Терминал учёта</h2><span class="status ok">Онлайн</span></div><div class="terminal-device"><span class="terminal-glyph">▣</span><div><strong>SLK20R</strong><small>Локальный терминал отпечатков</small></div></div><dl><div><dt>Последнее событие</dt><dd id="terminal-last-event">—</dd></div><div><dt>Синхронизация</dt><dd id="terminal-last-sync">Ожидание данных</dd></div></dl><button class="quiet" type="button" data-open-view="terminal-view">Открыть терминал</button></aside>
<section class="panel dashboard-payroll manager-only"><div class="panel-head"><h2>Расчёт зарплаты</h2><button class="text-action" type="button" data-open-view="payroll-view">Подробнее →</button></div><div class="dashboard-totals"><div><small>Начислено</small><strong id="dashboard-accrued">—</strong></div><div><small>Выплачено</small><strong id="dashboard-paid">—</strong></div><div><small>Остаток</small><strong id="dashboard-balance">—</strong></div></div><div class="rule-chips" id="dashboard-rules"></div></section>
<section class="panel dashboard-analytics manager-only"><div class="panel-head"><h2>Финансы месяца</h2><button class="text-action" type="button" data-open-view="finance-view">Подробнее →</button></div><div id="dashboard-finance-chart" class="mini-chart"><div class="empty">Загрузка…</div></div></section>
<section class="panel dashboard-alerts"><div class="panel-head"><h2>Уведомления</h2><button class="text-action" type="button" data-open-view="notifications-view">Все →</button></div><div id="dashboard-alert-list" class="alert-list"><div class="empty">Нет новых данных</div></div></section>
</div>
<div class="quick-bar manager-only"><strong>Быстрые действия</strong><button type="button" id="dashboard-add-employee">+ Добавить сотрудника</button><button type="button" class="quiet" data-open-view="shifts-view">Открыть смены</button><button type="button" class="quiet" data-open-view="payroll-view">Запустить расчёт</button><button type="button" class="quiet" data-open-view="rules-view">Настроить правила</button></div>
</section>
<section id="employees-view" class="app-view manager-only" hidden>
<div class="page-title-row"><div><h1>Сотрудники</h1><p>Персонал, профессии, статусы и зарплатные профили</p></div><div class="actions"><button id="manage-professions-directory" class="quiet" type="button">Профессии</button><button id="add-employee-directory" type="button">+ Добавить сотрудника</button></div></div>
<section class="metric-strip compact-metrics"><article><span class="metric-icon">♟</span><div><small>Всего сотрудников</small><strong id="directory-total">—</strong></div></article><article><span class="metric-icon">✓</span><div><small>Показано</small><strong id="directory-active">—</strong></div></article><article><span class="metric-icon">!</span><div><small>Требуют внимания</small><strong id="directory-attention">—</strong></div></article></section>
<button id="add-employee" type="button" hidden>Добавить сотрудника</button><div class="employee-workspace"><section class="panel employee-list-panel"><div class="directory-toolbar"><input id="employee-search" type="search" placeholder="Поиск по имени, должности или табельному номеру" aria-label="Поиск сотрудников"><select id="employee-status-filter" aria-label="Статус"><option value="">Все статусы</option><option value="active">Работает</option><option value="blocked">Заблокирован</option><option value="dismissed">Уволен</option></select><span id="employee-search-result" class="status">Все сотрудники</span></div><div id="employees-directory" class="people directory-list"></div><div id="employees" hidden></div></section><aside id="employee-inspector" class="panel inspector"><div class="empty">Выберите сотрудника в списке</div></aside></div>
</section>
<section id="attendance-view" class="app-view" hidden>
<div class="page-title-row"><div><h1>Посещаемость</h1><p>Фактические приходы и уходы со сканера</p></div><span class="live">● в реальном времени</span></div>
<div class="attendance-workspace"><section class="panel"><div class="panel-head"><h2>Журнал отметок</h2><div class="segmented"><button class="selected" type="button" data-event-filter="">Все</button><button type="button" data-event-filter="check_in">Приходы</button><button type="button" data-event-filter="check_out">Уходы</button></div></div><div id="attendance" class="feed"></div></section><aside class="panel"><div class="panel-head"><h2>Сводка дня</h2></div><div id="attendance-summary" class="summary-stack"><div class="empty">Загрузка…</div></div></aside></div>
</section>
<section id="shifts-view" class="app-view" hidden>
<div class="page-title-row"><div><h1>Смены</h1><p>Сверка отметок, аномалии и подтверждение рабочего времени</p></div><button id="reconcile-sessions" type="button">Сверить 7 дней</button></div>
<section class="panel"><div class="panel-head"><h2>Рабочие смены</h2><span>План и фактический интервал</span></div><div class="table-wrap"><table><thead><tr><th>Дата</th><th>Сотрудник</th><th>Подразделение</th><th>Интервал</th><th>Часы</th><th>Состояние</th><th>Причина правки</th><th></th></tr></thead><tbody id="work-sessions"></tbody></table></div></section>
</section>
<section id="schedule-view" class="app-view" hidden>
<div class="page-title-row"><div><h1>Графики</h1><p>Планирование смен и контроль фактических выходов</p></div><div class="actions"><label class="period-control">Месяц <input id="period-month" type="month" aria-label="Месяц данных"></label><button id="schedule-refresh" class="quiet" type="button">Обновить</button></div></div>
<div class="schedule-tabs segmented"><button type="button">Год</button><button type="button">Квартал</button><button class="selected" type="button">Месяц</button><button type="button">Неделя</button></div>
<section class="panel manager-only schedule-editor"><form id="schedule-form" class="inline-form"><label>Сотрудник<select id="schedule-employee" required></select></label><label>Точка<select id="schedule-unit" required></select></label><label>Дата<input id="schedule-date" type="date" required></label><label>Часы<input id="schedule-hours" type="number" inputmode="decimal" min="0" max="24" step="0.5" value="12" required></label><label>Начало<input id="schedule-start" type="time" value="10:00"></label><label>Конец<input id="schedule-end" type="time" value="22:00"></label><button type="submit">+ Добавить</button></form></section>
<section class="panel calendar-panel"><div class="calendar-legend"><span><i class="planned"></i>Запланировано</span><span><i class="actual"></i>Есть факты</span><span><i class="warning"></i>Требует внимания</span></div><div class="calendar-weekdays" aria-hidden="true"><span>Пн</span><span>Вт</span><span>Ср</span><span>Чт</span><span>Пт</span><span>Сб</span><span>Вс</span></div><div id="calendar-grid" class="calendar-grid"></div></section>
</section>
<section id="finance-view" class="app-view manager-only" hidden>
<div class="page-title-row"><div><h1>Финансы</h1><p>Выручка, расходы и финансовый результат</p></div><div class="actions"><button id="finance-refresh" class="quiet" type="button">Обновить</button></div></div>
<section class="metric-strip compact-metrics"><article><span class="metric-icon">↗</span><div><small>Доход</small><strong id="finance-income">—</strong></div></article><article><span class="metric-icon danger">↘</span><div><small>Расход</small><strong id="finance-expense">—</strong></div></article><article><span class="metric-icon">◕</span><div><small>Прибыль</small><strong id="finance-profit">—</strong></div></article></section>
<div class="finance-workspace"><section class="panel finance-journal"><div class="panel-head"><h2>Журнал операций</h2><span id="finance-count">0 записей</span></div><div class="table-wrap"><table><thead><tr><th>Дата</th><th>Точка</th><th>Категория</th><th>Тип</th><th>Сумма</th></tr></thead><tbody id="finance-entries"></tbody></table></div></section><aside class="finance-side"><section class="panel"><div class="panel-head"><h2>Быстрое внесение</h2></div><form id="finance-form" class="stack-form"><label>Дата<input id="finance-date" type="date" required></label><label>Торговая точка<select id="finance-unit" required></select></label><label>Категория<select id="finance-category" required></select></label><label>Сумма, ₽<input id="finance-amount" type="number" inputmode="decimal" min="0" step="0.01" required></label><button type="submit">Сохранить операцию</button></form></section><section class="panel"><div class="panel-head"><h2>Доходы и расходы</h2><span>₽</span></div><div id="finance-chart" class="finance-chart" role="img" aria-label="График доходов и расходов"></div></section></aside></div>
</section>
<section id="payroll-view" class="app-view" hidden>
<div class="page-title-row"><div><h1>Зарплата</h1><p>Расчёт начислений, выплат и остатка по сотрудникам</p></div><div class="actions"><button id="payroll-calc-btn" class="manager-only" type="button">▶ Запустить расчёт</button><button id="payroll-refresh" class="quiet" type="button">Пересчитать</button></div></div>
<div id="payroll-calc-status" class="message" hidden></div><div id="payroll-warnings" class="payroll-warnings message" hidden></div>
<section class="metric-strip compact-metrics"><article><span class="metric-icon">◎</span><div><small>Начислено</small><strong id="payroll-accrued">—</strong></div></article><article><span class="metric-icon">▣</span><div><small>Выплачено</small><strong id="payroll-paid">—</strong></div></article><article><span class="metric-icon">◷</span><div><small>Остаток</small><strong id="payroll-balance">—</strong></div></article></section>
<section id="payroll-preview-card" class="panel manager-only" hidden><div class="panel-head"><h2 id="payroll-preview-title">Предварительный результат</h2><button id="payroll-approve-btn" type="button">Утвердить расчёт</button></div><div class="table-wrap"><table><thead><tr><th>Сотрудник</th><th>Должность</th><th>Дней</th><th>Факт, часов</th><th>Начислено</th></tr></thead><tbody id="payroll-preview-rows"></tbody></table></div></section>
<div class="payroll-workspace"><section class="panel"><div class="panel-head"><h2>Расчёт по сотрудникам</h2><span>По подтверждённым сменам</span></div><div class="table-wrap"><table><thead><tr><th>Сотрудник</th><th>Должность</th><th>Факт, часов</th><th>Начислено</th><th>Выплачено</th><th>Остаток</th></tr></thead><tbody id="payroll-rows"></tbody></table></div></section><aside class="panel payroll-detail"><div class="panel-head"><h2>История операций</h2></div><div class="table-wrap"><table><thead><tr><th>Дата</th><th>Сотрудник</th><th>Операция</th><th>Сумма</th><th>Примечание</th></tr></thead><tbody id="payroll-entries"></tbody></table></div></aside></div>
</section>
<section id="rules-view" class="app-view manager-only" hidden><div class="page-title-row"><div><h1>Правила начислений</h1><p>Схемы оплаты и профессии сотрудников</p></div><button id="manage-professions" class="quiet" type="button">+ Профессия</button></div><div class="rules-workspace"><section class="panel"><div class="panel-head"><h2>Активные схемы оплаты</h2></div><div id="rules-list" class="rule-grid"><div class="empty">Загрузка…</div></div></section><section class="panel"><div class="panel-head"><h2>Профессии</h2></div><div id="rules-professions" class="dense-list"><div class="empty">Загрузка…</div></div></section></div></section>
<section id="terminal-view" class="app-view manager-only" hidden><div class="page-title-row"><div><h1>Терминал</h1><p>Состояние локального сканера и последние события</p></div><span class="status ok">Сервер онлайн</span></div><div class="terminal-workspace"><section class="panel terminal-card"><span class="terminal-glyph large">▣</span><div><h2>BioLink SLK20R</h2><p>Локальный агент фиксирует отпечатки и безопасно передаёт события на сервер.</p><dl><div><dt>Последняя синхронизация</dt><dd id="terminal-sync-detail">—</dd></div><div><dt>Последняя отметка</dt><dd id="terminal-event-detail">—</dd></div></dl></div></section><section class="panel"><div class="panel-head"><h2>Последние события терминала</h2></div><div id="terminal-events" class="dense-list"><div class="empty">Загрузка…</div></div></section></div></section>
<section id="notifications-view" class="app-view" hidden><div class="page-title-row"><div><h1>Уведомления</h1><p>События, требующие внимания руководителя</p></div><button id="notify" class="quiet" type="button">Включить push</button></div><section class="panel"><div id="notifications-list" class="alert-list"><div class="empty">Нет новых уведомлений</div></div></section></section>
<section id="reports-view" class="app-view" hidden><div class="page-title-row"><div><h1>Отчёты</h1><p>Табель рабочего времени и подтверждённые данные</p></div><span id="timesheet-help">Изменения сохраняет руководитель</span></div><section class="panel"><form id="timesheet-create" class="timesheet-create"><label>Сотрудник<select id="new-time-employee" required></select></label><label>Дата<input id="new-time-date" type="date" required></label><label>Часы<input id="new-time-hours" type="number" min="0" max="24" step="0.25" value="8" required></label><label>Статус<select id="new-time-status"><option value="draft">Черновик</option><option value="submitted">На проверке</option><option value="approved">Утверждён</option><option value="rejected">Возвращён</option></select></label><label class="grow">Примечание<input id="new-time-note" maxlength="1000" placeholder="Необязательно"></label><button type="submit">Добавить строку</button></form><div class="table-wrap"><table><thead><tr><th>Дата</th><th>Сотрудник</th><th>Часы</th><th>Статус</th><th>Примечание</th><th></th></tr></thead><tbody id="timesheet"></tbody></table></div></section></section>
<section id="settings-view" class="app-view" hidden><div class="page-title-row"><div><h1>Настройки</h1><p>Внешний вид, уведомления и состояние подключения</p></div></div><div class="settings-grid"><section class="panel"><div class="panel-head"><h2>Интерфейс</h2></div><label class="settings-field">Тема<select id="theme-select" aria-label="Тема интерфейса"><option value="noble-green">Благородный зелёный</option><option value="parchment-light">Пергамент</option><option value="graphite-forest-dark">Тёмная</option></select></label></section><section class="panel"><div class="panel-head"><h2>О системе</h2></div><dl class="system-list"><div><dt>Модуль</dt><dd>Krona Staff</dd></div><div><dt>Режим</dt><dd>Автономный с интеграцией платформы</dd></div><div><dt>Данные</dt><dd>PostgreSQL с tenant isolation</dd></div></dl></section></div></section>
</main>
<input id="employee-position" type="hidden">
<dialog id="employee-dialog" class="employee-dialog"><form id="employee-form" method="dialog"><div class="dialog-head"><div><p class="eyebrow">Карточка сотрудника</p><h2 id="employee-dialog-title">Новый сотрудник</h2></div><button id="employee-close" class="icon-button" type="button" aria-label="Закрыть">×</button></div><input id="employee-id" type="hidden"><label>Табельный номер<input id="employee-number" maxlength="80" required></label><label>Фамилия, имя и отчество<input id="employee-name" maxlength="240" required></label><label>Профессия<select id="employee-profession"><option value="">Не указана</option></select></label><p class="role-note">Роли и права платформы управляются в Krona Core отдельно от кадровой профессии.</p><label>Пол<select id="employee-gender"><option value="">Не указан</option><option value="female">Женский</option><option value="male">Мужской</option></select></label><label id="employee-status-wrap" hidden>Статус<select id="employee-status"><option value="active">Работает</option><option value="blocked">Заблокирован</option><option value="dismissed">Уволен</option></select></label><div id="employee-pay-section" class="pay-profile-section manager-only"><p class="eyebrow">Зарплатный профиль</p><label>Торговая точка<select id="employee-pay-unit"></select></label><label>Схема расчёта<select id="employee-pay-rule"></select></label><div class="form-row-2"><label>Ставка в час, ₽<input id="employee-pay-rate" type="number" min="0" step="1" value="100"></label><label>Процент от выручки, %<input id="employee-pay-percent" type="number" min="0" max="100" step="0.1" value="0"></label></div><div class="form-row-2"><label>Гарантия за смену, ₽<input id="employee-pay-guarantee" type="number" min="0" step="100" value="0"></label><label>Доля в фонде, %<input id="employee-pay-pool-weight" type="number" min="0" max="200" step="10" value="100"></label></div><label>Действует с даты<input id="employee-pay-effective" type="date"></label><small>Новая версия действует с выбранной даты.</small></div><div id="employee-error" class="login-error" role="alert" hidden></div><div class="dialog-actions"><button id="employee-cancel" class="quiet" type="button">Отмена</button><button id="employee-save" type="submit">Сохранить</button></div></form></dialog>
<dialog id="profession-dialog" class="employee-dialog"><form id="profession-form" method="dialog"><div class="dialog-head"><div><p class="eyebrow">Кадровый справочник</p><h2>Профессии</h2></div><button id="profession-close" class="icon-button" type="button" aria-label="Закрыть">×</button></div><label>Новая профессия<input id="profession-name" maxlength="160" required placeholder="Например, Повар"></label><button type="submit">Добавить профессию</button><div id="profession-list" class="people"></div><div id="profession-error" class="login-error" role="alert" hidden></div></form></dialog>
<dialog id="access-dialog" class="employee-dialog"><form method="dialog"><div class="dialog-head"><div><p class="eyebrow">Личный кабинет</p><h2>Ключ сотрудника</h2></div><button id="access-close" class="icon-button" type="submit" aria-label="Закрыть">×</button></div><p>Скопируйте ключ сейчас. Повторно показать его нельзя.</p><label>Ключ доступа<input id="access-token" readonly></label><div class="dialog-actions"><button id="access-copy" type="button">Скопировать</button><button class="quiet" type="submit">Закрыть</button></div></form></dialog>
<footer>Krona Business Platform · Staff</footer>
</div>
<script src="/staff/app.js" defer></script>
</body>
</html>`;

View file

@ -3,12 +3,19 @@ import { describe, expect, it } from 'vitest';
import { portalCssEnhanced, portalHtml, portalJsEnhanced, portalManifest } from '../../src/portal-ui.js';
describe('Krona Staff workspace shell', () => {
it('exposes the five approved top-level sections with deep routes', () => {
expect(portalHtml).toContain('data-route="/staff/dashboard">Дашборд');
expect(portalHtml).toContain('data-route="/staff/employees">Сотрудники');
expect(portalHtml).toContain('data-route="/staff/finance">Финансы');
expect(portalHtml).toContain('data-route="/staff/schedules">Графики');
expect(portalHtml).toContain('data-route="/staff/payroll">Зарплата');
it('exposes the operational workspace sections with deep routes', () => {
expect(portalHtml).toContain('data-route="/staff/dashboard"');
expect(portalHtml).toContain('data-route="/staff/employees"');
expect(portalHtml).toContain('data-route="/staff/finance"');
expect(portalHtml).toContain('data-route="/staff/schedules"');
expect(portalHtml).toContain('data-route="/staff/payroll"');
expect(portalHtml).toContain('data-route="/staff/attendance">');
expect(portalHtml).toContain('data-route="/staff/shifts">');
expect(portalHtml).toContain('data-route="/staff/rules">');
expect(portalHtml).toContain('data-route="/staff/terminal">');
expect(portalHtml).toContain('id="employee-inspector"');
expect(portalHtml).toContain('id="finance-entries"');
expect(portalHtml).toContain('id="calendar-grid"');
expect(portalHtml).not.toContain('<aside class="sidebar"');
});
@ -23,6 +30,8 @@ describe('Krona Staff workspace shell', () => {
it('keeps navigation addressable and starts the PWA on the dashboard', () => {
expect(portalJsEnhanced).toContain("'/staff/employees':'employees-view'");
expect(portalJsEnhanced).toContain("'/staff/attendance':'attendance-view'");
expect(portalJsEnhanced).toContain("'/staff/reports':'reports-view'");
expect(portalJsEnhanced).toContain("window.addEventListener('popstate'");
expect(JSON.parse(portalManifest)).toMatchObject({ start_url: '/staff/dashboard', scope: '/staff' });
});