feat(ui): align Staff portal with Krona library

This commit is contained in:
Ochenstarik 2026-09-24 21:14:20 +07:00
parent 5b159f785c
commit 9ea9a004ed
9 changed files with 144 additions and 31 deletions

View file

@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: Krona Staff API
version: 0.3.0
version: 0.3.1
description: API посещаемости Krona Staff для кабинета сотрудника, терминала и табеля руководителя.
license:
name: Proprietary
@ -107,6 +107,39 @@ paths:
schema: { $ref: '#/components/schemas/Employee' }
'403': { description: Недостаточно прав }
'409': { description: Табельный номер уже используется }
/professions:
get:
summary: Получить кадровый справочник профессий
operationId: listStaffProfessions
tags: [Employees]
security: [{ bearerAuth: [] }]
responses:
'200': { description: Tenant profession catalogue }
'401': { description: Требуется авторизация }
post:
summary: Добавить профессию в кадровый справочник
operationId: createStaffProfession
tags: [Employees]
security: [{ bearerAuth: [] }]
responses:
'201': { description: Profession created }
'403': { description: Недостаточно прав }
'409': { description: Профессия уже существует }
/professions/{professionId}:
parameters:
- in: path
name: professionId
required: true
schema: { type: string, pattern: '^[0-9A-HJKMNP-TV-Z]{26}$' }
patch:
summary: Изменить профессию или её статус
operationId: updateStaffProfession
tags: [Employees]
security: [{ bearerAuth: [] }]
responses:
'200': { description: Profession updated }
'403': { description: Недостаточно прав }
'404': { description: Профессия не найдена }
/employees/{employeeId}:
patch:
summary: Изменить карточку или статус сотрудника
@ -177,7 +210,8 @@ paths:
date_to: { type: string, format: date }
business_day_start_hour: { type: integer, minimum: 0, maximum: 23, default: 4 }
automatic_close_hour: { type: integer, minimum: 0, maximum: 23, default: 2 }
open_shift_threshold_minutes: { type: integer, minimum: 60, maximum: 2880, default: 840 }
open_shift_threshold_minutes:
{ type: integer, minimum: 60, maximum: 2880, default: 840 }
responses:
'200':
description: Итоги сверки, включая события без назначенного подразделения
@ -185,7 +219,14 @@ paths:
application/json:
schema:
type: object
required: [groups, sessions_created_or_updated, confirmed_sessions_skipped, anomalous_sessions, unresolved_events]
required:
[
groups,
sessions_created_or_updated,
confirmed_sessions_skipped,
anomalous_sessions,
unresolved_events,
]
properties:
groups: { type: integer, minimum: 0 }
sessions_created_or_updated: { type: integer, minimum: 0 }
@ -359,7 +400,12 @@ paths:
tags: [Finance]
security: [{ bearerAuth: [] }]
parameters:
- { in: path, name: orgUnitId, required: true, schema: { $ref: '#/components/schemas/Ulid' } }
- {
in: path,
name: orgUnitId,
required: true,
schema: { $ref: '#/components/schemas/Ulid' },
}
- { in: path, name: businessDate, required: true, schema: { type: string, format: date } }
- { in: path, name: metricKey, required: true, schema: { type: string } }
requestBody:
@ -370,7 +416,8 @@ paths:
type: object
required: [value_minor]
properties:
value_minor: { type: integer, minimum: -9000000000000000, maximum: 9000000000000000 }
value_minor:
{ type: integer, minimum: -9000000000000000, maximum: 9000000000000000 }
currency_code: { type: string, pattern: '^[A-Z]{3}$' }
responses:
'200': { description: Сумма сохранена }
@ -394,7 +441,12 @@ paths:
tags: [Schedule]
security: [{ bearerAuth: [] }]
parameters:
- { in: path, name: employeeId, required: true, schema: { $ref: '#/components/schemas/Ulid' } }
- {
in: path,
name: employeeId,
required: true,
schema: { $ref: '#/components/schemas/Ulid' },
}
- { in: path, name: businessDate, required: true, schema: { type: string, format: date } }
requestBody:
required: true
@ -448,7 +500,8 @@ paths:
application/json:
schema:
type: object
required: [run_id, status, period_start, period_end, total_accrued_minor, totals, warnings]
required:
[run_id, status, period_start, period_end, total_accrued_minor, totals, warnings]
properties:
run_id: { $ref: '#/components/schemas/Ulid' }
status: { const: calculated }
@ -478,7 +531,8 @@ paths:
application/json:
schema:
type: object
required: [run_id, status, approved_by, approved_at, total_accrued_minor, entries_posted]
required:
[run_id, status, approved_by, approved_at, total_accrued_minor, entries_posted]
properties:
run_id: { $ref: '#/components/schemas/Ulid' }
status: { const: approved }
@ -613,7 +667,19 @@ components:
PayAssignment:
type: object
additionalProperties: false
required: [id, employee_id, pay_rule_id, pay_rule_code, pay_rule_name, rate_minor, percent_bp, guarantee_minor, pool_weight_bp, effective_from]
required:
[
id,
employee_id,
pay_rule_id,
pay_rule_code,
pay_rule_name,
rate_minor,
percent_bp,
guarantee_minor,
pool_weight_bp,
effective_from,
]
properties:
id: { $ref: '#/components/schemas/Ulid' }
employee_id: { $ref: '#/components/schemas/Ulid' }

View file

@ -8,9 +8,9 @@
<PlatformTarget>x64</PlatformTarget>
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-x64</RuntimeIdentifier>
<RootNamespace>KronaStaffService</RootNamespace>
<Version>0.3.0</Version>
<AssemblyVersion>0.3.0.0</AssemblyVersion>
<FileVersion>0.3.0.0</FileVersion>
<Version>0.3.1</Version>
<AssemblyVersion>0.3.1.0</AssemblyVersion>
<FileVersion>0.3.1.0</FileVersion>
</PropertyGroup>
<ItemGroup>

View file

@ -17,10 +17,10 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
<Version>0.3.0</Version>
<AssemblyVersion>0.3.0.0</AssemblyVersion>
<FileVersion>0.3.0.0</FileVersion>
<InformationalVersion>0.3.0</InformationalVersion>
<Version>0.3.1</Version>
<AssemblyVersion>0.3.1.0</AssemblyVersion>
<FileVersion>0.3.1.0</FileVersion>
<InformationalVersion>0.3.1</InformationalVersion>
</PropertyGroup>
<ItemGroup>

View file

@ -31,7 +31,7 @@ POST /api/v1/staff/attendance/events
| `Authorization` | String | Yes | Terminal Bearer API Key | `Bearer trm_sec_991823ab...` |
| `Idempotency-Key` | String | Yes | Unique idempotency key per event batch or event | `6ba7b810-9dad-11d1-80b4-00c04fd430c8` |
| `X-Request-Id` | String | Yes | Unique trace ID for telemetry and crash debugging | `req_01HV8G7P89J21...` |
| `User-Agent` | String | Yes | Terminal client identification | `KronaStaffTerminal/0.3.0 (Windows NT 10.0; Win64; x64)` |
| `User-Agent` | String | Yes | Terminal client identification | `KronaStaffTerminal/0.3.1 (Windows NT 10.0; Win64; x64)` |
---

View file

@ -2,7 +2,7 @@
"$schema": "./schemas/module.schema.json",
"id": "ru.krona.staff",
"name": "Krona Staff",
"version": "0.3.0",
"version": "0.3.1",
"min_platform_version": "3.2.0",
"dependencies": ["ru.krona.core"],
"optional_dependencies": [],

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "@krona/staff",
"version": "0.3.0",
"version": "0.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@krona/staff",
"version": "0.3.0",
"version": "0.3.1",
"dependencies": {
"@fastify/helmet": "^13.0.2",
"@fastify/sensible": "^6.0.3",

View file

@ -1,6 +1,6 @@
{
"name": "@krona/staff",
"version": "0.3.0",
"version": "0.3.1",
"private": true,
"type": "module",
"engines": {

View file

@ -1,5 +1,5 @@
export const portalHtml = `<!doctype html>
<html lang="ru">
<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">
@ -7,7 +7,7 @@ export const portalHtml = `<!doctype html>
<title>Krona Staff</title>
<link rel="manifest" href="/staff/manifest.webmanifest">
<link rel="stylesheet" href="/staff/styles.css">
<style>[hidden]{display:none!important}.auth-screen{min-height:100vh;display:grid;place-items:center;padding:28px;background:radial-gradient(circle at 25% 10%,rgba(183,131,53,.18),transparent 24rem),linear-gradient(135deg,#f7f0e3,#e8dcc5)}.auth-card{width:min(100%,460px);padding:42px;background:rgba(252,248,239,.96);border:1px solid #d9c9ac;border-radius:20px;box-shadow:0 24px 70px rgba(48,42,35,.16)}.auth-brand{display:flex;align-items:center;gap:12px;margin-bottom:42px;font-size:20px}.auth-card h1{margin:6px 0 12px;font:500 38px/1.08 Georgia,serif}.auth-copy{margin:0 0 26px;color:#756a5b;line-height:1.55}.auth-card form{display:grid;gap:10px}.auth-card label{font-size:13px;font-weight:700}.auth-card input{width:100%;min-height:50px;padding:0 14px;border:1px solid #cbbb9e;border-radius:10px;background:#fffdf8;color:#302a23}.auth-card button{margin-top:6px;min-height:50px}.auth-card small{display:block;margin-top:20px;color:#756a5b;line-height:1.45}.login-error{margin-top:16px;padding:12px 14px;border:1px solid #dfb2aa;border-radius:10px;background:#fff1ee;color:#78382d}@media(max-width:520px){.auth-screen{padding:16px}.auth-card{padding:28px 22px}.auth-card h1{font-size:32px}.auth-brand{margin-bottom:32px}}</style>
<style>[hidden]{display:none!important}.auth-screen{min-height:100vh;display:grid;place-items:center;padding:28px;background:var(--omni-bg-workspace)}.auth-card{width:min(100%,460px);padding:42px;background:var(--omni-surface-raised);border:1px solid var(--omni-border);border-radius:var(--omni-radius-xl);box-shadow:var(--omni-shadow-strong)}.auth-brand{display:flex;align-items:center;gap:12px;margin-bottom:42px;font-size:20px}.auth-card h1{margin:6px 0 12px;font:500 38px/1.08 var(--omni-font-serif)}.auth-copy{margin:0 0 26px;color:var(--omni-text-secondary);line-height:1.55}.auth-card form{display:grid;gap:10px}.auth-card label{font-size:13px;font-weight:700}.auth-card input{width:100%;min-height:50px;padding:0 14px;border:1px solid var(--omni-border-strong);border-radius:var(--omni-radius-md);background:var(--omni-surface-raised);color:var(--omni-text-primary)}.auth-card button{margin-top:6px;min-height:50px}.auth-card small{display:block;margin-top:20px;color:var(--omni-text-secondary);line-height:1.45}.login-error{margin-top:16px;padding:12px 14px;border:1px solid var(--omni-danger);border-radius:var(--omni-radius-md);background:var(--omni-danger-soft);color:var(--omni-danger)}@media(max-width:520px){.auth-screen{padding:16px}.auth-card{padding:28px 22px}.auth-card h1{font-size:32px}.auth-brand{margin-bottom:32px}}</style>
</head>
<body>
<section id="auth-screen" class="auth-screen" aria-labelledby="login-title">
@ -28,7 +28,7 @@ export const portalHtml = `<!doctype html>
<div id="app-shell" hidden>
<header class="topbar">
<a class="brand" href="/staff" aria-label="Krona Staff"><span class="mark">К</span><span>Krona <b>Staff</b></span></a>
<div class="actions"><span id="connection" class="status">Подключение…</span><button id="notify" class="quiet" type="button">Уведомления</button><button id="logout" class="quiet" type="button">Выйти</button><button id="refresh" type="button">Обновить</button></div>
<div class="actions"><span id="connection" class="status">Подключение…</span><label class="theme-control">Тема<select id="theme-select" aria-label="Тема интерфейса"><option value="noble-green">Зелёная</option><option value="parchment-light">Пергамент</option><option value="graphite-forest-dark">Тёмная</option></select></label><button id="notify" class="quiet" type="button">Уведомления</button><button id="logout" class="quiet" type="button">Выйти</button><button id="refresh" type="button">Обновить</button></div>
</header>
<nav class="section-nav" aria-label="Разделы кабинета">
<button class="nav-item selected" type="button" data-target="overview-view">Обзор</button>
@ -51,7 +51,7 @@ export const portalHtml = `<!doctype html>
</section>
<section class="layout">
<article class="panel attendance-panel"><div class="panel-head"><div><p class="eyebrow">Последние события</p><h2>Отметки терминала</h2></div><span class="live">● в реальном времени</span></div><div id="attendance" class="feed"></div></article>
<aside class="panel people-panel"><div class="panel-head"><div><p class="eyebrow">Команда</p><h2>Сотрудники</h2></div><button id="add-employee" class="quiet compact" type="button">Добавить</button></div><div id="employees" class="people"></div></aside>
<aside class="panel people-panel"><div class="panel-head"><div><p class="eyebrow">Команда</p><h2>Сотрудники</h2></div><div class="actions"><button id="manage-professions" class="quiet compact" type="button">Профессии</button><button id="add-employee" class="quiet compact" type="button">Добавить</button></div></div><div id="employees" class="people"></div></aside>
</section>
<section class="panel sessions-panel timesheet-panel"><div class="panel-head"><div><p class="eyebrow">Сверка терминала</p><h2>Рабочие смены</h2></div><button id="reconcile-sessions" class="quiet" type="button">Сверить 7 дней</button></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 class="panel timesheet-panel"><div class="panel-head"><div><p class="eyebrow">Учёт времени</p><h2>Табель за 40 дней</h2></div><span id="timesheet-help">Изменения сохраняет руководитель</span></div><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>
@ -77,7 +77,9 @@ export const portalHtml = `<!doctype html>
<section class="panel timesheet-panel"><div class="panel-head"><div><p class="eyebrow">История</p><h2>Начисления и выплаты</h2></div></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></section>
</div>
</main>
<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>Должность<input id="employee-position" maxlength="160"></label><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></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>
<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>
@ -122,16 +124,26 @@ export const portalJs = `(() => {
if('serviceWorker' in navigator) navigator.serviceWorker.register('/staff/sw.js'); load(false); setInterval(()=>{if(!el('app-shell').hidden)load(true);},15000);
})();`;
export const portalCssEnhanced = portalCss + `
export const portalCssEnhanced =
portalCss +
`
[data-omni-theme="parchment-light"]{--omni-bg-app:#EFE3CC;--omni-bg-workspace:#F3EAD7;--omni-surface:#F8F2E6;--omni-surface-raised:#FCF8EF;--omni-surface-muted:#E9DDC5;--omni-surface-hover:#EDE0C7;--omni-text-primary:#302A23;--omni-text-secondary:#756A5B;--omni-text-inverse:#FFF;--omni-border:#D9C9AC;--omni-border-strong:#CBB99B;--omni-border-focus:#9A743D;--omni-primary:#9A743D;--omni-primary-hover:#7D5B2D;--omni-primary-soft:#EADBC1;--omni-gold:#B78335;--omni-gold-soft:#EFE0C2;--omni-danger:#A44E3F;--omni-danger-soft:#F1DDD5;--omni-info:#5F7480;--omni-shadow-medium:0 6px 18px rgb(72 55 34/.10);--omni-shadow-strong:0 14px 36px rgb(52 39 24/.16)}
[data-omni-theme="noble-green"]{--omni-bg-app:#EDE5D3;--omni-bg-workspace:#F2EAD9;--omni-surface:#F8F2E6;--omni-surface-raised:#FCF8EF;--omni-surface-muted:#E6DBC5;--omni-surface-hover:#E3E9DE;--omni-text-primary:#2E302A;--omni-text-secondary:#6B6C60;--omni-text-inverse:#FFF;--omni-border:#D2CCB9;--omni-border-strong:#9DAA98;--omni-border-focus:#315F46;--omni-primary:#315F46;--omni-primary-hover:#294F3B;--omni-primary-soft:#DDE7DB;--omni-gold:#B78335;--omni-gold-soft:#EFE0C2;--omni-danger:#A44E3F;--omni-danger-soft:#F1DDD5;--omni-info:#527286;--omni-shadow-medium:0 6px 18px rgb(72 55 34/.10);--omni-shadow-strong:0 14px 36px rgb(52 39 24/.16)}
[data-omni-theme="graphite-forest-dark"]{--omni-bg-app:#171A18;--omni-bg-workspace:#1D211E;--omni-surface:#252A26;--omni-surface-raised:#2B312C;--omni-surface-muted:#212621;--omni-surface-hover:#2B342D;--omni-text-primary:#EEE7DA;--omni-text-secondary:#B7B3A8;--omni-text-inverse:#171A18;--omni-border:#39423A;--omni-border-strong:#465249;--omni-border-focus:#6F9A79;--omni-primary:#527D60;--omni-primary-hover:#648E70;--omni-primary-soft:#29382E;--omni-gold:#C99A4D;--omni-gold-soft:#403522;--omni-danger:#C06B5F;--omni-danger-soft:#432B28;--omni-info:#7693A0;--omni-shadow-medium:0 8px 22px rgb(0 0 0/.24);--omni-shadow-strong:0 16px 40px rgb(0 0 0/.34)}
:root{--omni-radius-sm:6px;--omni-radius-md:8px;--omni-radius-lg:12px;--omni-radius-xl:16px;--omni-font-sans:Inter,"Segoe UI",system-ui,sans-serif;--omni-font-serif:Georgia,serif;--omni-warning:#7A5B16;--omni-warning-soft:#F7EAC9;--bg:var(--omni-bg-app);--workspace:var(--omni-bg-workspace);--surface:var(--omni-surface);--raised:var(--omni-surface-raised);--text:var(--omni-text-primary);--muted:var(--omni-text-secondary);--border:var(--omni-border);--primary:var(--omni-primary);--gold:var(--omni-gold);--danger:var(--omni-danger);--info:var(--omni-info);--shadow:var(--omni-shadow-medium)}
body,.auth-screen{background:var(--omni-bg-workspace);color:var(--omni-text-primary)}.topbar,.section-nav{background:color-mix(in srgb,var(--omni-surface) 94%,transparent)}.hero{background:linear-gradient(120deg,var(--omni-primary-active,var(--omni-primary)),var(--omni-primary))}.stats article,.panel,.employee-dialog{background:var(--omni-surface);border-color:var(--omni-border);box-shadow:var(--omni-shadow-medium)}button{background:var(--omni-primary);color:var(--omni-text-inverse)}button:hover{background:var(--omni-primary-hover);filter:none}.quiet{background:transparent;color:var(--omni-primary);border-color:var(--omni-border)}input,select,.timesheet-create input,.timesheet-create select,.employee-dialog input,.employee-dialog select,.period-control input,.stack-form input,.stack-form select,.inline-form input,.inline-form select{background:var(--omni-surface-raised);color:var(--omni-text-primary);border-color:var(--omni-border)}button:focus-visible,input:focus-visible,select:focus-visible{outline-color:var(--omni-border-focus)}.theme-control{display:flex;align-items:center;gap:6px;color:var(--omni-text-secondary);font-size:12px}.theme-control select{min-height:36px;border:1px solid var(--omni-border);border-radius:var(--omni-radius-md);padding:0 8px}.role-note{margin:0;padding:10px 12px;border-radius:var(--omni-radius-md);background:var(--omni-primary-soft);color:var(--omni-text-secondary);font-size:12px;line-height:1.45}.payroll-warnings{background:var(--omni-warning-soft)!important;color:var(--omni-warning)!important}.message,.login-error{background:var(--omni-danger-soft);color:var(--omni-danger);border-color:var(--omni-danger)}
.section-nav{position:sticky;top:72px;z-index:4;display:flex;align-items:center;gap:8px;padding:10px max(20px,calc((100vw - 1240px)/2));border-bottom:1px solid var(--border);background:rgba(243,234,215,.96);backdrop-filter:blur(12px);overflow-x:auto}.nav-item{flex:0 0 auto;background:transparent;color:var(--muted);border:1px solid transparent}.nav-item.selected{background:var(--surface);color:var(--primary);border-color:var(--border)}.period-control{margin-left:auto;display:flex;align-items:center;gap:8px;color:var(--muted);font-size:13px;font-weight:700;white-space:nowrap}.period-control input,.stack-form input,.stack-form select,.inline-form input,.inline-form select{min-height:42px;border:1px solid var(--border);border-radius:8px;background:#fffdf8;color:var(--text);padding:7px 9px}.view-head{display:flex;justify-content:space-between;align-items:end;gap:24px;margin-bottom:22px}.view-head h1{margin:4px 0 8px;font:500 clamp(30px,4vw,46px)/1.05 Georgia,serif}.view-head p{margin:0;color:var(--muted)}.finance-layout{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(260px,.7fr);gap:18px;margin-bottom:18px}.stack-form{display:grid;gap:13px}.stack-form label,.inline-form label{display:grid;gap:6px;color:var(--muted);font-size:12px;font-weight:700}.inline-form{display:grid;grid-template-columns:repeat(6,minmax(120px,1fr)) auto;align-items:end;gap:10px}.finance-chart{min-height:280px}.finance-chart svg{display:block;width:100%;height:280px}.chart-legend{display:flex;gap:16px;margin-top:10px;color:var(--muted);font-size:12px}.legend-dot{display:inline-block;width:9px;height:9px;margin-right:5px;border-radius:50%}.income-dot{background:var(--primary)}.expense-dot{background:var(--gold)}.calendar-panel{overflow:auto}.calendar-weekdays,.calendar-grid{display:grid;grid-template-columns:repeat(7,minmax(145px,1fr));min-width:1015px}.calendar-weekdays span{padding:8px 10px;color:var(--muted);font-size:12px;font-weight:750;text-align:center}.calendar-day{min-height:142px;padding:10px;border-top:1px solid var(--border);border-right:1px solid var(--border);background:var(--raised)}.calendar-day.outside{opacity:.45}.calendar-day.today{outline:2px solid var(--gold);outline-offset:-2px}.day-number{display:block;margin-bottom:8px;font-weight:750}.shift-card{margin:5px 0;padding:7px;border-left:3px solid var(--primary);border-radius:6px;background:#e8eadf;font-size:11px;line-height:1.35}.shift-card.actual{border-left-color:var(--gold);background:#f3e5cd}.shift-card strong,.shift-card span{display:block}.positive{color:var(--primary)}.negative{color:var(--danger)}.person-access{min-height:36px;padding:0 10px;margin-left:5px}.app-view[hidden]{display:none!important}@media(max-width:900px){.section-nav{top:68px;padding-inline:14px}.period-control{margin-left:8px}.finance-layout{grid-template-columns:1fr}.inline-form{grid-template-columns:repeat(2,minmax(0,1fr))}.inline-form button{grid-column:1/-1}.view-head{align-items:flex-start}.calendar-panel{padding:12px}}@media(max-width:520px){.view-head{display:block}.view-head>button{margin-top:14px}.stats{grid-template-columns:1fr}.stats article:last-child{grid-column:auto}.inline-form{grid-template-columns:1fr}.period-control{order:-1;margin-left:0}.section-nav{top:92px}.finance-chart{min-height:220px}.finance-chart svg{height:220px}}
.form-row-2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.pay-profile-section{display:grid;gap:10px;padding:14px;background:var(--raised);border:1px solid var(--border);border-radius:10px;margin-top:6px}
.payroll-warnings{background:#fff8e6!important;border:1px solid #e2cb91!important;color:#6b5314!important;margin-bottom:18px}
.payroll-warnings ul{margin:6px 0 0 18px;padding:0}
.payroll-warnings li{margin-bottom:4px}
.section-nav{background:color-mix(in srgb,var(--omni-surface) 94%,transparent)}.period-control input,.stack-form input,.stack-form select,.inline-form input,.inline-form select{background:var(--omni-surface-raised)}.shift-card{background:var(--omni-primary-soft)}.shift-card.actual{background:var(--omni-gold-soft)}.payroll-warnings{background:var(--omni-warning-soft)!important;border-color:color-mix(in srgb,var(--omni-warning) 35%,transparent)!important;color:var(--omni-warning)!important}[data-omni-theme="graphite-forest-dark"]{--omni-warning:#E0B96C;--omni-warning-soft:#403522}
`;
export const portalJsEnhanced = portalJs.replace('Неверный ключ администратора.', 'Неверный ключ доступа.') + `
export const portalJsEnhanced =
portalJs.replace('Неверный ключ администратора.', 'Неверный ключ доступа.') +
`
(() => {
const api='/api/v1/staff',byId=id=>document.getElementById(id),esc=value=>String(value??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])),rub=value=>(Number(value||0)/100).toLocaleString('ru-RU',{style:'currency',currency:'RUB',maximumFractionDigits:2}),hours=value=>(Number(value||0)/60).toLocaleString('ru-RU',{maximumFractionDigits:1}),jsonHeaders={'content-type':'application/json'};
let role='manager',units=[],initialized=false,active='overview-view',payRulesCache=[],currentRunId=null;
@ -153,6 +165,7 @@ export const portalJsEnhanced = portalJs.replace('Неверный ключ ад
const state=await get('/portal');
role=state.viewer&&state.viewer.role||'manager';
document.querySelectorAll('.manager-only').forEach(node=>node.hidden=role!=='manager');
byId('manage-professions').hidden=role!=='manager';
byId('viewer').textContent=role==='employee'?'Личный кабинет сотрудника':'Кабинет руководителя';
units=(await get('/org-units')).org_units||[];
try{payRulesCache=(await get('/pay-rules')).pay_rules||[]}catch{}
@ -273,6 +286,29 @@ export const portalJsEnhanced = portalJs.replace('Неверный ключ ад
});
}
const themeSelect=byId('theme-select');
const allowedThemes=['noble-green','parchment-light','graphite-forest-dark'];
const savedTheme=localStorage.getItem('krona-staff-theme');
if(savedTheme&&allowedThemes.includes(savedTheme))document.documentElement.dataset.omniTheme=savedTheme;
themeSelect.value=document.documentElement.dataset.omniTheme||'noble-green';
themeSelect.addEventListener('change',()=>{document.documentElement.dataset.omniTheme=themeSelect.value;localStorage.setItem('krona-staff-theme',themeSelect.value)});
let professionsCache=[];
async function loadProfessions(){
const result=await get('/professions');
professionsCache=result.professions||[];
const select=byId('employee-profession');
const current=select.value;
select.innerHTML='<option value="">Не указана</option>'+professionsCache.filter(x=>x.status==='active').map(x=>option(x.id,x.name)).join('');
if([...select.options].some(x=>x.value===current))select.value=current;
byId('profession-list').innerHTML=professionsCache.length?professionsCache.map(x=>'<div class="person"><span class="avatar">'+esc(x.name).slice(0,1).toUpperCase()+'</span><div><strong>'+esc(x.name)+'</strong><small>'+(x.status==='active'?'Активна':'В архиве')+'</small></div></div>').join(''):'<div class="empty">Профессии ещё не добавлены</div>';
}
byId('employee-profession').addEventListener('change',()=>{const selected=byId('employee-profession').selectedOptions[0];byId('employee-position').value=selected&&selected.value?selected.textContent:''});
document.addEventListener('click',event=>{const target=event.target.closest?.('.edit-employee,#add-employee');if(!target)return;setTimeout(async()=>{await loadProfessions();const position=byId('employee-position').value;const match=professionsCache.find(x=>x.name===position);byId('employee-profession').value=match?.id||''},0)},true);
byId('manage-professions').addEventListener('click',async()=>{byId('profession-error').hidden=true;await loadProfessions();byId('profession-dialog').showModal()});
byId('profession-close').addEventListener('click',()=>byId('profession-dialog').close());
byId('profession-form').addEventListener('submit',async event=>{event.preventDefault();const name=byId('profession-name').value.trim();if(!name)return;try{const created=await send('/professions','POST',{name});byId('profession-name').value='';await loadProfessions();byId('employee-profession').value=created.id;byId('employee-position').value=created.name}catch(error){byId('profession-error').textContent=error.message||'Не удалось сохранить профессию.';byId('profession-error').hidden=false}});
if(byId('payroll-approve-btn')){
byId('payroll-approve-btn').addEventListener('click',async()=>{
if(!currentRunId)return;
@ -292,6 +328,17 @@ export const portalJsEnhanced = portalJs.replace('Неверный ключ ад
})();
`;
export const portalManifest = JSON.stringify({ name: 'Krona Staff', short_name: 'Staff', start_url: '/staff', display: 'standalone', background_color: '#F3EAD7', theme_color: '#657255', lang: 'ru' });
export const portalManifest = JSON.stringify({
name: 'Krona Staff',
short_name: 'Staff',
start_url: '/staff',
display: 'standalone',
background_color: '#F3EAD7',
theme_color: '#657255',
lang: 'ru',
});
export const portalServiceWorker = `const CACHE='krona-staff-v3';const ASSETS=['/staff','/staff/styles.css','/staff/app.js','/staff/manifest.webmanifest'];self.addEventListener('install',e=>e.waitUntil(caches.open(CACHE).then(c=>c.addAll(ASSETS))));self.addEventListener('activate',e=>e.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(k=>k!==CACHE).map(k=>caches.delete(k))))));self.addEventListener('fetch',e=>{if(e.request.method==='GET'&&new URL(e.request.url).pathname.startsWith('/staff'))e.respondWith(fetch(e.request).then(r=>{const copy=r.clone();caches.open(CACHE).then(c=>c.put(e.request,copy));return r;}).catch(()=>caches.match(e.request)));});`;
export const portalServiceWorkerEnhanced = portalServiceWorker.replace('krona-staff-v3', 'krona-staff-v4');
export const portalServiceWorkerEnhanced = portalServiceWorker.replace(
'krona-staff-v3',
'krona-staff-v4',
);

View file

@ -1,2 +1,2 @@
export const SERVICE_NAME = 'krona-staff';
export const SERVICE_VERSION = '0.3.0';
export const SERVICE_VERSION = '0.3.1';