feat(portal): align workspace with Krona library
This commit is contained in:
parent
3e2054480c
commit
1812870dd3
4 changed files with 102 additions and 14 deletions
|
|
@ -141,6 +141,7 @@ export async function buildApp({
|
|||
app.get('/staff/manifest.webmanifest', async (_request, reply) => reply.type('application/manifest+json').send(portalManifest));
|
||||
app.get('/staff/icon-192.svg', async (_request, reply) => reply.type('image/svg+xml').send(portalIcon192));
|
||||
app.get('/staff/icon-512.svg', async (_request, reply) => reply.type('image/svg+xml').send(portalIcon512));
|
||||
app.get('/staff/*', async (_request, reply) => reply.type('text/html; charset=utf-8').send(portalHtml));
|
||||
|
||||
app.addHook('onClose', async () => database.close());
|
||||
return app;
|
||||
|
|
|
|||
21
src/portal-design.ts
Normal file
21
src/portal-design.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
export const portalDesignCss = `
|
||||
:root{--omni-space-1:4px;--omni-space-2:8px;--omni-space-3:12px;--omni-space-4:16px;--omni-space-5:20px;--omni-space-6:24px;--omni-space-8:32px;--omni-radius-xs:6px;--omni-radius-sm:8px;--omni-radius-md:10px;--omni-radius-lg:12px;--omni-radius-xl:16px;--omni-radius-pill:999px;--omni-touch-target:44px;--omni-z-sticky:1100;--omni-z-modal:1300;--omni-font-family-sans:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;--omni-font-family-serif:Georgia,Cambria,"Times New Roman",serif;--omni-text-label:.75rem;--omni-text-body-small:.875rem;--omni-motion-hover:140ms cubic-bezier(.4,0,.2,1);--omni-motion-tabs:180ms cubic-bezier(.4,0,.2,1);--omni-control-height:36px;--omni-control-height-sm:30px;--omni-control-padding-x:12px;--omni-layout-gap:12px;--omni-row-height:40px}
|
||||
[data-omni-theme="parchment-light"]{--omni-bg-app:#EFE3CC;--omni-bg-workspace:#F3EAD7;--omni-bg-section:#F1E6D2;--omni-surface:#F8F2E6;--omni-surface-raised:#FCF8EF;--omni-surface-muted:#E9DDC5;--omni-surface-hover:#EDE0C7;--omni-surface-selected:#E2D0AD;--omni-text-primary:#302A23;--omni-text-secondary:#756A5B;--omni-text-muted:#9A8E7D;--omni-text-inverse:#FFFFFF;--omni-border:#D9C9AC;--omni-border-subtle:#E5D8C0;--omni-border-strong:#CBB99B;--omni-border-focus:#9A743D;--omni-primary:#9A743D;--omni-primary-hover:#7D5B2D;--omni-primary-active:#684923;--omni-primary-soft:#EADBC1;--omni-gold:#B78335;--omni-gold-soft:#EFE0C2;--omni-success:#657255;--omni-success-soft:#E1E6D8;--omni-warning:#A8752F;--omni-warning-soft:#F2E3C8;--omni-danger:#A44E3F;--omni-danger-soft:#F1DDD5;--omni-info:#5F7480;--omni-info-soft:#DFE6E5;--omni-shadow-soft:0 1px 3px rgba(72,55,34,.06);--omni-shadow-medium:0 6px 18px rgba(72,55,34,.1);--omni-shadow-strong:0 14px 36px rgba(52,39,24,.16)}
|
||||
[data-omni-theme="noble-green"]{--omni-bg-app:#EDE5D3;--omni-bg-workspace:#F2EAD9;--omni-bg-section:#ECE2CE;--omni-surface:#F8F2E6;--omni-surface-raised:#FCF8EF;--omni-surface-muted:#E6DBC5;--omni-surface-hover:#E3E9DE;--omni-surface-selected:#DCE5D7;--omni-text-primary:#2E302A;--omni-text-secondary:#6B6C60;--omni-text-muted:#8B8C80;--omni-text-inverse:#FFFFFF;--omni-border:#D2CCB9;--omni-border-subtle:#E2DAC8;--omni-border-strong:#9DAA98;--omni-border-focus:#315F46;--omni-primary:#315F46;--omni-primary-hover:#294F3B;--omni-primary-active:#213F30;--omni-primary-soft:#DDE7DB;--omni-gold:#B78335;--omni-gold-soft:#EFE0C2;--omni-success:#3F7557;--omni-success-soft:#DDE9E0;--omni-warning:#A8752F;--omni-warning-soft:#F2E3C8;--omni-danger:#A44E3F;--omni-danger-soft:#F1DDD5;--omni-info:#527286;--omni-info-soft:#DFE7EA;--omni-shadow-soft:0 1px 3px rgba(72,55,34,.06);--omni-shadow-medium:0 6px 18px rgba(72,55,34,.1);--omni-shadow-strong:0 14px 36px rgba(52,39,24,.16)}
|
||||
[data-omni-theme="graphite-forest-dark"]{--omni-bg-app:#171A18;--omni-bg-workspace:#1D211E;--omni-bg-section:#202620;--omni-surface:#252A26;--omni-surface-raised:#2B312C;--omni-surface-muted:#212621;--omni-surface-hover:#2B342D;--omni-surface-selected:#29382E;--omni-text-primary:#EEE7DA;--omni-text-secondary:#B7B3A8;--omni-text-muted:#888E87;--omni-text-inverse:#171A18;--omni-border:#39423A;--omni-border-subtle:#303831;--omni-border-strong:#465249;--omni-border-focus:#6F9A79;--omni-primary:#527D60;--omni-primary-hover:#648E70;--omni-primary-active:#426A50;--omni-primary-soft:#29382E;--omni-gold:#C99A4D;--omni-gold-soft:#403522;--omni-success:#78916F;--omni-success-soft:#29372D;--omni-warning:#C08A45;--omni-warning-soft:#403422;--omni-danger:#C06B5F;--omni-danger-soft:#432B28;--omni-info:#7693A0;--omni-info-soft:#29363B;--omni-shadow-soft:0 1px 3px rgba(0,0,0,.18);--omni-shadow-medium:0 8px 22px rgba(0,0,0,.24);--omni-shadow-strong:0 16px 40px rgba(0,0,0,.34)}
|
||||
[data-omni-density="workspace"]{--omni-bg-app:rgb(239 232 216);--omni-bg-workspace:rgb(239 232 218);--omni-bg-section:rgb(233 225 207);--omni-surface:rgb(247 241 227);--omni-surface-raised:rgb(250 245 234);--omni-surface-muted:rgb(238 231 214);--omni-surface-hover:rgb(231 230 211);--omni-surface-selected:rgb(221 231 211);--omni-text-primary:rgb(28 39 34);--omni-text-secondary:rgb(93 109 101);--omni-text-muted:rgb(108 123 116);--omni-border:rgb(211 204 184);--omni-border-subtle:rgb(225 217 198);--omni-border-focus:rgb(35 123 91);--omni-primary:rgb(31 117 85);--omni-primary-hover:rgb(24 96 69);--omni-primary-soft:rgb(230 243 236)}
|
||||
[data-omni-theme="parchment-light"][data-omni-density="workspace"]{--omni-bg-app:rgb(244 237 220);--omni-bg-workspace:rgb(244 237 222);--omni-surface:rgb(250 245 232);--omni-surface-raised:rgb(252 247 235);--omni-surface-muted:rgb(242 234 216);--omni-surface-selected:rgb(236 221 190);--omni-primary:rgb(133 99 50);--omni-primary-hover:rgb(107 76 33);--omni-primary-soft:rgb(246 239 225)}
|
||||
[data-omni-theme="graphite-forest-dark"][data-omni-density="workspace"]{--omni-bg-app:rgb(19 24 22);--omni-bg-workspace:rgb(20 26 23);--omni-bg-section:rgb(27 34 30);--omni-surface:rgb(26 33 29);--omni-surface-raised:rgb(35 44 39);--omni-surface-muted:rgb(29 38 33);--omni-surface-hover:rgb(38 49 42);--omni-surface-selected:rgb(39 64 49);--omni-text-primary:rgb(237 243 239);--omni-text-secondary:rgb(171 189 179);--omni-text-muted:rgb(143 164 152);--omni-border:rgb(60 76 66);--omni-border-subtle:rgb(42 54 47);--omni-border-focus:rgb(111 185 144);--omni-primary:rgb(77 147 105);--omni-primary-hover:rgb(93 163 122);--omni-primary-soft:rgb(37 59 45)}
|
||||
*{box-sizing:border-box}html{background:var(--omni-bg-workspace);color-scheme:light}[data-omni-theme="graphite-forest-dark"]{color-scheme:dark}body{margin:0;min-width:320px;min-height:100vh;background:var(--omni-bg-workspace);color:var(--omni-text-primary);font:400 var(--omni-text-body-small)/1.45 var(--omni-font-family-sans)}button,input,select{font:inherit}button,input,select,.nav-item{outline:none}button:focus-visible,input:focus-visible,select:focus-visible,.nav-item:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,var(--omni-border-focus) 30%,transparent);border-color:var(--omni-border-focus)}button{min-height:var(--omni-control-height);padding:0 var(--omni-control-padding-x);border:1px solid var(--omni-primary);border-radius:var(--omni-radius-sm);background:var(--omni-primary);color:var(--omni-text-inverse);font-weight:650;cursor:pointer;transition:background var(--omni-motion-hover),border-color var(--omni-motion-hover)}button:hover{background:var(--omni-primary-hover);border-color:var(--omni-primary-hover)}button:disabled{opacity:.55;cursor:not-allowed}button.quiet,.quiet{background:var(--omni-surface-raised);border-color:var(--omni-border);color:var(--omni-text-primary)}button.quiet:hover{background:var(--omni-surface-hover);border-color:var(--omni-border-strong)}.compact{min-height:var(--omni-control-height-sm);padding-inline:var(--omni-space-2);font-size:var(--omni-text-label)}
|
||||
.topbar{position:sticky;top:0;z-index:var(--omni-z-sticky);min-height:64px;display:flex;align-items:center;justify-content:space-between;gap:var(--omni-space-4);padding:var(--omni-space-2) max(var(--omni-space-5),calc((100vw - 1440px)/2));background:color-mix(in srgb,var(--omni-surface-raised) 94%,transparent);border-bottom:1px solid var(--omni-border-subtle);backdrop-filter:blur(14px)}.brand{display:flex;align-items:center;gap:var(--omni-space-3);color:var(--omni-primary);text-decoration:none;white-space:nowrap;font-size:1.08rem}.mark{width:38px;height:38px;display:grid;place-items:center;border-radius:var(--omni-radius-lg);background:var(--omni-primary);color:var(--omni-text-inverse);font:700 1.25rem/1 var(--omni-font-family-serif)}.actions{display:flex;align-items:center;justify-content:flex-end;gap:var(--omni-space-2)}.status,.live,.tag{display:inline-flex;align-items:center;gap:var(--omni-space-1);min-height:26px;padding:0 var(--omni-space-2);border-radius:var(--omni-radius-pill);background:var(--omni-surface-muted);color:var(--omni-text-secondary);font-size:var(--omni-text-label);white-space:nowrap}.status.ok,.live{background:var(--omni-success-soft);color:var(--omni-success)}.status.offline,.anomaly{color:var(--omni-warning)}.theme-control{display:flex;align-items:center;gap:var(--omni-space-2);color:var(--omni-text-secondary);font-size:var(--omni-text-label)}.theme-control select,.period-control input,input,select{min-height:var(--omni-control-height);border:1px solid var(--omni-border);border-radius:var(--omni-radius-sm);background:var(--omni-surface-raised);color:var(--omni-text-primary);padding:0 var(--omni-space-3)}
|
||||
.section-nav{position:sticky;top:64px;z-index:calc(var(--omni-z-sticky) - 1);display:flex;align-items:center;gap:var(--omni-space-1);padding:var(--omni-space-2) max(var(--omni-space-5),calc((100vw - 1440px)/2));overflow-x:auto;background:color-mix(in srgb,var(--omni-surface) 96%,transparent);border-bottom:1px solid var(--omni-border-subtle);scrollbar-width:thin}.nav-item{flex:0 0 auto;min-height:var(--omni-control-height);border-color:transparent;background:transparent;color:var(--omni-text-secondary)}.nav-item:hover{background:var(--omni-surface-hover);border-color:transparent;color:var(--omni-text-primary)}.nav-item.selected{background:var(--omni-primary-soft);border-color:var(--omni-border);color:var(--omni-primary)}.period-control{display:flex;align-items:center;gap:var(--omni-space-2);margin-left:auto;color:var(--omni-text-secondary);font-size:var(--omni-text-label);white-space:nowrap}
|
||||
main{width:min(100%,1480px);margin:0 auto;padding:var(--omni-space-5)}.app-view{animation:view-enter var(--omni-motion-tabs)}.hero,.view-head{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--omni-space-6);margin-bottom:var(--omni-space-4)}.hero{padding:var(--omni-space-5) var(--omni-space-6);border:1px solid var(--omni-border-subtle);border-radius:var(--omni-radius-lg);background:var(--omni-bg-section)}.hero h1,.view-head h1{margin:var(--omni-space-1) 0;font:700 clamp(1.65rem,3vw,2.3rem)/1.1 var(--omni-font-family-sans);letter-spacing:-.025em}.hero p,.view-head p{margin:0;color:var(--omni-text-secondary)}.eyebrow{margin:0;color:var(--omni-primary);font-size:var(--omni-text-label);font-weight:750;letter-spacing:.04em;text-transform:uppercase}.today{min-width:190px;padding-left:var(--omni-space-5);border-left:1px solid var(--omni-border)}.today span,.today small{display:block;color:var(--omni-text-secondary)}.today strong{display:block;margin:var(--omni-space-1) 0;font-size:1.1rem}.message,.login-error{margin-bottom:var(--omni-space-3);padding:var(--omni-space-3) var(--omni-space-4);border:1px solid var(--omni-warning);border-radius:var(--omni-radius-md);background:var(--omni-warning-soft);color:var(--omni-text-primary)}
|
||||
.stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--omni-layout-gap);margin-bottom:var(--omni-layout-gap)}.stats article,.panel{border:1px solid var(--omni-border-subtle);border-radius:var(--omni-radius-lg);background:var(--omni-surface-raised);box-shadow:var(--omni-shadow-soft)}.stats article{position:relative;min-height:96px;padding:var(--omni-space-4);overflow:hidden}.stats article:before{content:"";position:absolute;inset:0 auto 0 0;width:3px;background:var(--omni-primary)}.stats span{display:block;color:var(--omni-text-secondary)}.stats strong{display:block;margin-top:var(--omni-space-2);font-size:clamp(1.55rem,3vw,2.1rem);font-variant-numeric:tabular-nums}.panel{padding:var(--omni-space-4)}.layout{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(300px,.75fr);gap:var(--omni-layout-gap)}.panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--omni-space-3);margin-bottom:var(--omni-space-3)}.panel-head h2{margin:var(--omni-space-1) 0 0;font-size:1.05rem}.feed,.people{display:grid;gap:var(--omni-space-2)}.event,.person{display:grid;align-items:center;gap:var(--omni-space-3);min-height:56px;padding:var(--omni-space-2) var(--omni-space-3);border:1px solid var(--omni-border-subtle);border-radius:var(--omni-radius-md);background:var(--omni-surface)}.event{grid-template-columns:38px minmax(0,1fr) auto}.person{grid-template-columns:38px minmax(0,1fr) auto auto}.event:hover,.person:hover{background:var(--omni-surface-hover)}.avatar,.event-icon{width:36px;height:36px;display:grid;place-items:center;border-radius:var(--omni-radius-pill);background:var(--omni-primary-soft);color:var(--omni-primary);font-weight:750}.event-icon{border-radius:var(--omni-radius-sm)}.event strong,.person strong,.event small,.person small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.event small,.person small,.event time{color:var(--omni-text-secondary);font-size:var(--omni-text-label)}.empty{padding:var(--omni-space-6);text-align:center;color:var(--omni-text-secondary);background:var(--omni-surface-muted);border-radius:var(--omni-radius-md)}.directory-toolbar{display:flex;align-items:center;gap:var(--omni-space-2);margin-bottom:var(--omni-space-3)}.directory-toolbar input{flex:1;min-width:180px}.directory-list .person{min-height:64px}
|
||||
.timesheet-panel{margin-top:var(--omni-layout-gap)}.table-wrap{width:100%;overflow:auto;border:1px solid var(--omni-border-subtle);border-radius:var(--omni-radius-md)}table{width:100%;min-width:760px;border-collapse:collapse;background:var(--omni-surface-raised)}th,td{height:var(--omni-row-height);padding:var(--omni-space-2) var(--omni-space-3);border-bottom:1px solid var(--omni-border-subtle);text-align:left;vertical-align:middle}th{position:sticky;top:0;z-index:1;background:var(--omni-surface-muted);color:var(--omni-text-secondary);font-size:var(--omni-text-label);font-weight:700;white-space:nowrap}tbody tr:hover{background:var(--omni-surface-hover)}td{font-variant-numeric:tabular-nums}td input,td select{width:100%}.timesheet-create{display:grid;grid-template-columns:repeat(4,minmax(110px,1fr)) minmax(180px,2fr) auto;align-items:end;gap:var(--omni-space-2);padding:var(--omni-space-3);margin-bottom:var(--omni-space-3);background:var(--omni-surface-muted);border-radius:var(--omni-radius-md)}.timesheet-create label,.stack-form label,.inline-form label,.employee-dialog label{display:grid;gap:var(--omni-space-1);color:var(--omni-text-secondary);font-size:var(--omni-text-label);font-weight:650}
|
||||
.finance-layout{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(280px,.7fr);gap:var(--omni-layout-gap);margin-bottom:var(--omni-layout-gap)}.stack-form{display:grid;gap:var(--omni-space-3)}.inline-form{display:grid;grid-template-columns:repeat(6,minmax(110px,1fr)) auto;align-items:end;gap:var(--omni-space-2)}.finance-chart{min-height:280px}.finance-chart svg{display:block;width:100%;height:280px}.chart-legend{display:flex;gap:var(--omni-space-4);color:var(--omni-text-secondary);font-size:var(--omni-text-label)}.legend-dot{display:inline-block;width:9px;height:9px;margin-right:var(--omni-space-1);border-radius:var(--omni-radius-pill)}.income-dot{background:var(--omni-primary)}.expense-dot{background:var(--omni-gold)}.calendar-panel{overflow:auto}.calendar-weekdays,.calendar-grid{display:grid;grid-template-columns:repeat(7,minmax(140px,1fr));min-width:980px}.calendar-weekdays span{padding:var(--omni-space-2);color:var(--omni-text-secondary);text-align:center;font-size:var(--omni-text-label);font-weight:700}.calendar-day{min-height:132px;padding:var(--omni-space-2);border-top:1px solid var(--omni-border-subtle);border-right:1px solid var(--omni-border-subtle);background:var(--omni-surface-raised)}.calendar-day:hover{background:var(--omni-surface-hover)}.calendar-day.outside{opacity:.55}.calendar-day.today{box-shadow:inset 0 0 0 2px var(--omni-border-focus)}.day-number{display:block;margin-bottom:var(--omni-space-2);font-weight:750}.shift-card{margin:var(--omni-space-1) 0;padding:var(--omni-space-2);border-left:3px solid var(--omni-primary);border-radius:var(--omni-radius-xs);background:var(--omni-primary-soft);font-size:var(--omni-text-label)}.shift-card.actual{border-left-color:var(--omni-gold);background:var(--omni-gold-soft)}.shift-card strong,.shift-card span{display:block}.positive{color:var(--omni-success)}.negative{color:var(--omni-danger)}
|
||||
.employee-dialog{width:min(92vw,620px);max-height:90vh;padding:0;overflow:auto;border:1px solid var(--omni-border);border-radius:var(--omni-radius-xl);background:var(--omni-surface-raised);color:var(--omni-text-primary);box-shadow:var(--omni-shadow-strong)}.employee-dialog::backdrop{background:rgb(10 16 13 / 52%);backdrop-filter:blur(3px)}.employee-dialog form{display:grid;gap:var(--omni-space-3);padding:var(--omni-space-6)}.dialog-head{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--omni-space-3)}.dialog-head h2{margin:var(--omni-space-1) 0;font-size:1.5rem}.dialog-actions{display:flex;justify-content:flex-end;gap:var(--omni-space-2)}.icon-button{min-width:var(--omni-control-height);padding:0;background:var(--omni-surface-raised);border-color:var(--omni-border);color:var(--omni-text-primary)}.role-note,.employee-dialog small{color:var(--omni-text-secondary)}.form-row-2{display:grid;grid-template-columns:1fr 1fr;gap:var(--omni-space-3)}.pay-profile-section{display:grid;gap:var(--omni-space-3);padding:var(--omni-space-3);background:var(--omni-surface-muted);border-radius:var(--omni-radius-md)}.payroll-warnings{background:var(--omni-warning-soft)}.session-actions{display:flex;gap:var(--omni-space-1)}.anomaly{display:block;margin-top:var(--omni-space-1);font-size:var(--omni-text-label)}footer{padding:var(--omni-space-6);color:var(--omni-text-muted);text-align:center;font-size:var(--omni-text-label)}
|
||||
.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)}}
|
||||
@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;transition-duration:.01ms!important}}
|
||||
`;
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { portalDesignCss } from './portal-design.js';
|
||||
|
||||
export const portalHtml = `<!doctype html>
|
||||
<html lang="ru" data-omni-theme="noble-green" data-omni-density="workspace">
|
||||
<head>
|
||||
|
|
@ -12,7 +14,6 @@ export const portalHtml = `<!doctype html>
|
|||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<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: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">
|
||||
|
|
@ -36,10 +37,11 @@ export const portalHtml = `<!doctype html>
|
|||
<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>
|
||||
<button class="nav-item manager-only" type="button" data-target="finance-view">Доходы и расходы</button>
|
||||
<button class="nav-item" type="button" data-target="schedule-view">Календарь</button>
|
||||
<button class="nav-item" type="button" data-target="payroll-view">Зарплата</button>
|
||||
<button class="nav-item selected" type="button" data-target="overview-view" data-route="/staff/dashboard">Дашборд</button>
|
||||
<button class="nav-item manager-only" type="button" data-target="employees-view" data-route="/staff/employees">Сотрудники</button>
|
||||
<button class="nav-item manager-only" type="button" data-target="finance-view" data-route="/staff/finance">Финансы</button>
|
||||
<button class="nav-item" type="button" data-target="schedule-view" data-route="/staff/schedules">Графики</button>
|
||||
<button class="nav-item" type="button" data-target="payroll-view" data-route="/staff/payroll">Зарплата</button>
|
||||
<label class="period-control">Месяц <input id="period-month" type="month" aria-label="Месяц данных"></label>
|
||||
</nav>
|
||||
<main>
|
||||
|
|
@ -61,6 +63,11 @@ export const portalHtml = `<!doctype html>
|
|||
<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>
|
||||
</div>
|
||||
<div id="employees-view" class="app-view manager-only" hidden>
|
||||
<section class="view-head"><div><p class="eyebrow">Команда</p><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></section>
|
||||
<section class="stats" aria-label="Сводка сотрудников"><article><span>Всего сотрудников</span><strong id="directory-total">—</strong></article><article><span>Показано</span><strong id="directory-active">—</strong></article><article><span>Требуют внимания</span><strong id="directory-attention">—</strong></article></section>
|
||||
<section class="panel"><div class="directory-toolbar"><input id="employee-search" type="search" placeholder="Поиск по имени, должности или табельному номеру" aria-label="Поиск сотрудников"><span id="employee-search-result" class="status">Все сотрудники</span></div><div id="employees-directory" class="people directory-list"></div></section>
|
||||
</div>
|
||||
<div id="finance-view" class="app-view" hidden>
|
||||
<section class="view-head"><div><p class="eyebrow">Управленческий учёт</p><h1>Доходы и расходы</h1><p>Выручка по каналам, расходы по категориям и прибыль за выбранный месяц.</p></div><button id="finance-refresh" class="quiet" type="button">Обновить</button></section>
|
||||
<section class="finance-stats stats" aria-label="Финансовая сводка"><article><span>Доход</span><strong id="finance-income">—</strong></article><article><span>Расход</span><strong id="finance-expense">—</strong></article><article><span>Прибыль</span><strong id="finance-profit">—</strong></article></section>
|
||||
|
|
@ -74,7 +81,7 @@ export const portalHtml = `<!doctype html>
|
|||
</div>
|
||||
<div id="payroll-view" class="app-view" hidden>
|
||||
<section class="view-head"><div><p class="eyebrow">Расчётный лист</p><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></section>
|
||||
<div id="payroll-calc-status" class="message" style="background:#eaf2e8;border-color:#b4d3af;color:#285223" hidden></div>
|
||||
<div id="payroll-calc-status" class="message" hidden></div>
|
||||
<div id="payroll-warnings" class="payroll-warnings message" hidden></div>
|
||||
<section id="payroll-preview-card" class="panel manager-only" hidden><div class="panel-head"><div><p class="eyebrow">Предварительный результат</p><h2 id="payroll-preview-title">Расчёт зарплаты</h2></div><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>
|
||||
<section class="payroll-stats stats" aria-label="Сводка зарплаты"><article><span>Начислено</span><strong id="payroll-accrued">—</strong></article><article><span>Выплачено</span><strong id="payroll-paid">—</strong></article><article><span>Остаток</span><strong id="payroll-balance">—</strong></article></section>
|
||||
|
|
@ -129,7 +136,7 @@ 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 =
|
||||
const legacyPortalCssEnhanced =
|
||||
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)}
|
||||
|
|
@ -157,6 +164,9 @@ body,.auth-screen{background:var(--omni-bg-workspace);color:var(--omni-text-prim
|
|||
.status.offline{color:var(--omni-warning,#B78335);font-weight:600}
|
||||
`;
|
||||
|
||||
void legacyPortalCssEnhanced;
|
||||
export const portalCssEnhanced = portalDesignCss;
|
||||
|
||||
export const portalJsEnhanced =
|
||||
portalJs.replace('Неверный ключ администратора.', 'Неверный ключ доступа.') +
|
||||
`
|
||||
|
|
@ -180,6 +190,7 @@ export const portalJsEnhanced =
|
|||
try{
|
||||
const state=await get('/portal');
|
||||
role=state.viewer&&state.viewer.role||'manager';
|
||||
if(role!=='manager'&&active==='employees-view')show('overview-view',false);
|
||||
document.querySelectorAll('.manager-only').forEach(node=>node.hidden=role!=='manager');
|
||||
byId('manage-professions').hidden=role!=='manager';
|
||||
byId('viewer').textContent=role==='employee'?'Личный кабинет сотрудника':'Кабинет руководителя';
|
||||
|
|
@ -189,13 +200,40 @@ export const portalJsEnhanced =
|
|||
await loadActive();
|
||||
}catch{initialized=false}
|
||||
}
|
||||
function show(target){active=target;document.querySelectorAll('.app-view').forEach(view=>view.hidden=view.id!==target);document.querySelectorAll('.nav-item').forEach(button=>button.classList.toggle('selected',button.dataset.target===target));loadActive()}
|
||||
const routeToView={'/staff':'overview-view','/staff/':'overview-view','/staff/dashboard':'overview-view','/staff/employees':'employees-view','/staff/finance':'finance-view','/staff/schedules':'schedule-view','/staff/payroll':'payroll-view'};
|
||||
function show(target,updateHistory=true){
|
||||
if(target==='employees-view'&&role!=='manager')target='overview-view';
|
||||
active=target;
|
||||
document.querySelectorAll('.app-view').forEach(view=>view.hidden=view.id!==target);
|
||||
document.querySelectorAll('.nav-item').forEach(button=>button.classList.toggle('selected',button.dataset.target===target));
|
||||
const button=document.querySelector('.nav-item[data-target="'+target+'"]');
|
||||
if(updateHistory&&button?.dataset.route&&location.pathname!==button.dataset.route)history.pushState({view:target},'',button.dataset.route);
|
||||
loadActive();
|
||||
}
|
||||
async function loadActive(){try{if(active==='finance-view')await loadFinance();if(active==='schedule-view')await loadSchedule();if(active==='payroll-view')await loadPayroll()}catch(error){const box=byId('message');if(box){box.textContent=error.message;box.hidden=false}}}
|
||||
async function loadFinance(){if(role!=='manager')return;const data=await get('/finance'+query()),income=data.daily.reduce((sum,x)=>sum+Number(x.income_minor),0),expense=data.daily.reduce((sum,x)=>sum+Number(x.expense_minor),0);byId('finance-income').textContent=rub(income);byId('finance-expense').textContent=rub(expense);byId('finance-profit').textContent=rub(income-expense);byId('finance-count').textContent=data.entries.length+' записей';byId('finance-category').innerHTML=data.categories.map(x=>option(x.key,(x.kind==='income'?'Доход · ':'Расход · ')+x.label)).join('');byId('finance-entries').innerHTML=data.entries.length?data.entries.map(x=>'<tr><td>'+esc(x.business_date)+'</td><td>'+esc(x.org_unit_name)+'</td><td>'+esc(x.label)+'</td><td><span class="tag">'+(x.metric_key.startsWith('income_')?'Доход':'Расход')+'</span></td><td>'+rub(x.value_minor)+'</td></tr>').join(''):'<tr><td colspan="5" class="empty">За месяц операций нет</td></tr>';drawChart(data.daily)}
|
||||
function drawChart(rows){const sums={};rows.forEach(x=>{const key=String(x.business_date).slice(0,10);sums[key]||={income:0,expense:0};sums[key].income+=Number(x.income_minor);sums[key].expense+=Number(x.expense_minor)});const points=Object.entries(sums).sort((a,b)=>a[0].localeCompare(b[0]));if(!points.length){byId('finance-chart').innerHTML='<div class="empty">Добавьте выручку или расходы — здесь появится график</div>';return}const width=900,height=250,pad=28,max=Math.max(1,...points.flatMap(x=>[x[1].income,x[1].expense])),x=i=>pad+(points.length===1?0:i*(width-pad*2)/(points.length-1)),y=v=>height-pad-v*(height-pad*2)/max,path=kind=>points.map((p,i)=>(i?'L':'M')+x(i).toFixed(1)+' '+y(p[1][kind]).toFixed(1)).join(' ');byId('finance-chart').innerHTML='<svg viewBox="0 0 '+width+' '+height+'" aria-hidden="true"><path d="'+path('income')+'" fill="none" stroke="#657255" stroke-width="4"/><path d="'+path('expense')+'" fill="none" stroke="#B78335" stroke-width="4"/></svg><div class="chart-legend"><span><i class="legend-dot income-dot"></i>Доход</span><span><i class="legend-dot expense-dot"></i>Расход</span></div>'}
|
||||
function drawChart(rows){const sums={};rows.forEach(x=>{const key=String(x.business_date).slice(0,10);sums[key]||={income:0,expense:0};sums[key].income+=Number(x.income_minor);sums[key].expense+=Number(x.expense_minor)});const points=Object.entries(sums).sort((a,b)=>a[0].localeCompare(b[0]));if(!points.length){byId('finance-chart').innerHTML='<div class="empty">Добавьте выручку или расходы — здесь появится график</div>';return}const width=900,height=250,pad=28,max=Math.max(1,...points.flatMap(x=>[x[1].income,x[1].expense])),x=i=>pad+(points.length===1?0:i*(width-pad*2)/(points.length-1)),y=v=>height-pad-v*(height-pad*2)/max,path=kind=>points.map((p,i)=>(i?'L':'M')+x(i).toFixed(1)+' '+y(p[1][kind]).toFixed(1)).join(' ');byId('finance-chart').innerHTML='<svg viewBox="0 0 '+width+' '+height+'" aria-hidden="true"><path d="'+path('income')+'" fill="none" stroke="var(--omni-primary)" stroke-width="4"/><path d="'+path('expense')+'" fill="none" stroke="var(--omni-gold)" stroke-width="4"/></svg><div class="chart-legend"><span><i class="legend-dot income-dot"></i>Доход</span><span><i class="legend-dot expense-dot"></i>Расход</span></div>'}
|
||||
async function loadSchedule(){const data=await get('/schedule'+query()),p=period(),start=new Date(p.from+'T00:00:00Z'),end=new Date(p.to+'T00:00:00Z'),first=new Date(start),last=new Date(end),grouped={};first.setUTCDate(first.getUTCDate()-((first.getUTCDay()+6)%7));last.setUTCDate(last.getUTCDate()+(7-last.getUTCDay())%7);(data.shifts||[]).forEach(x=>(grouped[String(x.business_date).slice(0,10)]||=[]).push(x));let html='';for(const d=new Date(first);d<=last;d.setUTCDate(d.getUTCDate()+1)){const key=d.toISOString().slice(0,10),inside=key>=p.from&&key<=p.to,today=key===new Date().toISOString().slice(0,10);html+='<article class="calendar-day '+(!inside?'outside ':'')+(today?'today':'')+'"><span class="day-number">'+d.getUTCDate()+'</span>'+((grouped[key]||[]).map(x=>'<div class="shift-card"><strong>'+esc(x.display_name)+'</strong><span>'+esc(x.org_unit_name)+' · план '+hours(x.planned_minutes)+' ч</span>'+(x.actual_minutes!==null?'<span class="actual">факт '+hours(x.actual_minutes)+' ч'+(x.actual_started_at?' · '+new Date(x.actual_started_at).toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit'}):'')+(x.actual_ended_at?'–'+new Date(x.actual_ended_at).toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit'}):'')+'</span>':'<span>факт: нет отметки</span>')+'</div>').join(''))+'</article>'}byId('calendar-grid').innerHTML=html;fillSelects()}
|
||||
async function loadPayroll(){const data=await get('/payroll'+query()),accrued=data.totals.reduce((s,x)=>s+Number(x.accrued_minor),0),paid=data.totals.reduce((s,x)=>s+Number(x.paid_minor),0),balance=data.totals.reduce((s,x)=>s+Number(x.balance_minor),0);byId('payroll-accrued').textContent=rub(accrued);byId('payroll-paid').textContent=rub(paid);byId('payroll-balance').textContent=rub(balance);byId('payroll-rows').innerHTML=data.totals.length?data.totals.map(x=>'<tr><td>'+esc(x.display_name)+'</td><td>'+esc(x.position||'—')+'</td><td>'+hours(x.worked_minutes)+'</td><td>'+rub(x.accrued_minor)+'</td><td>'+rub(x.paid_minor)+'</td><td class="'+(Number(x.balance_minor)>=0?'positive':'negative')+'">'+rub(x.balance_minor)+'</td></tr>').join(''):'<tr><td colspan="6" class="empty">Нет данных для расчёта</td></tr>';byId('payroll-entries').innerHTML=data.entries.length?data.entries.map(x=>'<tr><td>'+esc(x.business_date)+'</td><td>'+esc(x.display_name)+'</td><td>'+({accrual:'Начисление',payout:'Выплата',adjustment:'Корректировка'}[x.kind]||esc(x.kind))+'</td><td>'+rub(Math.abs(Number(x.amount_minor)))+'</td><td>'+esc(x.note||'—')+'</td></tr>').join(''):'<tr><td colspan="5" class="empty">Операций пока нет</td></tr>'}
|
||||
document.querySelectorAll('.nav-item').forEach(button=>button.addEventListener('click',()=>show(button.dataset.target)));byId('period-month').value=new Date().toISOString().slice(0,7);byId('period-month').addEventListener('change',loadActive);byId('finance-refresh').addEventListener('click',loadFinance);byId('schedule-refresh').addEventListener('click',loadSchedule);byId('payroll-refresh').addEventListener('click',loadPayroll);byId('finance-date').value=new Date().toISOString().slice(0,10);byId('schedule-date').value=new Date().toISOString().slice(0,10);
|
||||
document.querySelectorAll('.nav-item').forEach(button=>button.addEventListener('click',()=>show(button.dataset.target)));
|
||||
window.addEventListener('popstate',()=>show(routeToView[location.pathname]||'overview-view',false));
|
||||
active=routeToView[location.pathname]||'overview-view';show(active,false);
|
||||
byId('period-month').value=new Date().toISOString().slice(0,7);byId('period-month').addEventListener('change',loadActive);byId('finance-refresh').addEventListener('click',loadFinance);byId('schedule-refresh').addEventListener('click',loadSchedule);byId('payroll-refresh').addEventListener('click',loadPayroll);byId('finance-date').value=new Date().toISOString().slice(0,10);byId('schedule-date').value=new Date().toISOString().slice(0,10);
|
||||
function syncEmployeeDirectory(){
|
||||
const source=byId('employees'),target=byId('employees-directory');if(!source||!target)return;
|
||||
target.innerHTML=source.innerHTML;
|
||||
const people=[...target.querySelectorAll('.person')],query=(byId('employee-search')?.value||'').trim().toLocaleLowerCase('ru-RU');
|
||||
people.forEach(person=>person.hidden=query&&!person.textContent.toLocaleLowerCase('ru-RU').includes(query));
|
||||
const visible=people.filter(person=>!person.hidden).length;
|
||||
byId('employee-search-result').textContent=query?'Найдено: '+visible:'Все сотрудники';
|
||||
byId('directory-total').textContent=people.length;
|
||||
byId('directory-active').textContent=visible;
|
||||
byId('directory-attention').textContent='—';
|
||||
}
|
||||
byId('employee-search').addEventListener('input',syncEmployeeDirectory);
|
||||
byId('add-employee-directory').addEventListener('click',()=>byId('add-employee').click());
|
||||
byId('manage-professions-directory').addEventListener('click',()=>byId('manage-professions').click());
|
||||
byId('employees-directory').addEventListener('click',event=>{const edit=event.target.closest('.edit-employee');if(edit)byId('employees').querySelector('.edit-employee[data-id="'+edit.dataset.id+'"]').click()});
|
||||
byId('finance-form').addEventListener('submit',async event=>{event.preventDefault();const button=event.submitter;button.disabled=true;try{await send('/finance/'+byId('finance-unit').value+'/'+byId('finance-date').value+'/'+byId('finance-category').value,'PUT',{value_minor:Math.round(Number(byId('finance-amount').value)*100),currency_code:'RUB'});byId('finance-amount').value='';await loadFinance()}catch(error){alert(error.message)}finally{button.disabled=false}});
|
||||
byId('schedule-form').addEventListener('submit',async event=>{event.preventDefault();const button=event.submitter;button.disabled=true;try{await send('/schedule/'+byId('schedule-employee').value+'/'+byId('schedule-date').value,'PUT',{org_unit_id:byId('schedule-unit').value,planned_minutes:Math.round(Number(byId('schedule-hours').value)*60),start_time:byId('schedule-start').value,end_time:byId('schedule-end').value});await loadSchedule()}catch(error){alert(error.message)}finally{button.disabled=false}});
|
||||
document.addEventListener('click',async event=>{const button=event.target.closest('.person-access');if(!button)return;button.disabled=true;try{const result=await send('/employees/'+button.dataset.id+'/access-token','POST',{});byId('access-token').value=result.access_token;byId('access-dialog').showModal()}catch(error){alert(error.message)}finally{button.disabled=false}});byId('access-copy').addEventListener('click',async()=>{await navigator.clipboard.writeText(byId('access-token').value);byId('access-copy').textContent='Скопировано'});
|
||||
|
|
@ -408,7 +446,7 @@ export const portalJsEnhanced =
|
|||
const conn=byId('connection');if(conn){conn.textContent='Нет связи. Показаны сохранённые данные';conn.className='status offline';}
|
||||
});
|
||||
|
||||
const observer=new MutationObserver(()=>{if(role!=='manager')return;document.querySelectorAll('.edit-employee').forEach(edit=>{if(edit.parentElement.querySelector('.person-access'))return;const button=document.createElement('button');button.type='button';button.className='quiet compact person-access';button.dataset.id=edit.dataset.id;button.textContent='Личный вход';edit.insertAdjacentElement('afterend',button)});fillSelects()});observer.observe(byId('employees'),{childList:true,subtree:true});const shellObserver=new MutationObserver(()=>{if(!byId('app-shell').hidden){initialized=false;initialize()}else initialized=false});shellObserver.observe(byId('app-shell'),{attributes:true,attributeFilter:['hidden']});setTimeout(initialize,0);
|
||||
const observer=new MutationObserver(()=>{if(role==='manager')document.querySelectorAll('#employees .edit-employee').forEach(edit=>{if(edit.parentElement.querySelector('.person-access'))return;const button=document.createElement('button');button.type='button';button.className='quiet compact person-access';button.dataset.id=edit.dataset.id;button.textContent='Личный вход';edit.insertAdjacentElement('afterend',button)});fillSelects();syncEmployeeDirectory()});observer.observe(byId('employees'),{childList:true,subtree:true});const shellObserver=new MutationObserver(()=>{if(!byId('app-shell').hidden){initialized=false;initialize()}else initialized=false});shellObserver.observe(byId('app-shell'),{attributes:true,attributeFilter:['hidden']});setTimeout(initialize,0);
|
||||
})();
|
||||
`;
|
||||
|
||||
|
|
@ -419,7 +457,7 @@ export const portalIcon512 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0
|
|||
export const portalManifest = JSON.stringify({
|
||||
name: 'Krona Staff',
|
||||
short_name: 'Staff',
|
||||
start_url: '/staff',
|
||||
start_url: '/staff/dashboard',
|
||||
scope: '/staff',
|
||||
display: 'standalone',
|
||||
background_color: '#F3EAD7',
|
||||
|
|
@ -441,8 +479,8 @@ export const portalManifest = JSON.stringify({
|
|||
],
|
||||
});
|
||||
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 = `const CACHE='krona-staff-v5';
|
||||
const ASSETS=['/staff','/staff/styles.css','/staff/app.js','/staff/manifest.webmanifest','/staff/icon-192.svg','/staff/icon-512.svg'];
|
||||
export const portalServiceWorkerEnhanced = `const CACHE='krona-staff-v6';
|
||||
const ASSETS=['/staff/dashboard','/staff/styles.css','/staff/app.js','/staff/manifest.webmanifest','/staff/icon-192.svg','/staff/icon-512.svg'];
|
||||
self.addEventListener('install',e=>e.waitUntil(caches.open(CACHE).then(c=>c.addAll(ASSETS)).then(()=>self.skipWaiting())));
|
||||
self.addEventListener('activate',e=>e.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(k=>k!==CACHE).map(k=>caches.delete(k)))).then(()=>self.clients.claim())));
|
||||
self.addEventListener('fetch',e=>{
|
||||
|
|
|
|||
28
tests/unit/portal-ui.test.ts
Normal file
28
tests/unit/portal-ui.test.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
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">Зарплата');
|
||||
expect(portalHtml).not.toContain('<aside class="sidebar"');
|
||||
});
|
||||
|
||||
it('uses the Krona Library semantic theme vocabulary', () => {
|
||||
expect(portalCssEnhanced).toContain('--omni-bg-workspace');
|
||||
expect(portalCssEnhanced).toContain('[data-omni-theme="noble-green"]');
|
||||
expect(portalCssEnhanced).toContain('[data-omni-theme="parchment-light"]');
|
||||
expect(portalCssEnhanced).toContain('[data-omni-theme="graphite-forest-dark"]');
|
||||
expect(portalCssEnhanced).toContain('var(--omni-primary)');
|
||||
});
|
||||
|
||||
it('keeps navigation addressable and starts the PWA on the dashboard', () => {
|
||||
expect(portalJsEnhanced).toContain("'/staff/employees':'employees-view'");
|
||||
expect(portalJsEnhanced).toContain("window.addEventListener('popstate'");
|
||||
expect(JSON.parse(portalManifest)).toMatchObject({ start_url: '/staff/dashboard', scope: '/staff' });
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue