Init
Checks / test (push) Failing after 12m20s

This commit is contained in:
ab committed 2026-09-24 14:35:05 +03:00
commit a47c4a0398
96 files changed
+25721

No files matched your search

+196
View File
@@ -0,0 +1,196 @@
:root{font-family:Inter,Segoe UI,Arial,sans-serif;color:#243a36;background:#f7f9f8;font-size:14px;--green:#176b52;--ink:#233d35;--muted:#7d8a85;--line:#e3e9e5;--soft:#edf3ef}*{box-sizing:border-box}body{margin:0}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer;background:white;border:1px solid #dbe3de;border-radius:7px;padding:9px 13px;transition:background .15s}button:hover{background:#eef4f0}button:disabled{opacity:.5;cursor:wait}button.primary{background:var(--green);color:white;border-color:var(--green);font-weight:600}button.primary:hover{background:#12543f}button.text{border:0;background:none;padding:6px 3px;color:var(--green)}button.danger{color:#a55252}input,select,textarea{border:1px solid #d8e1da;border-radius:7px;background:white;padding:11px 12px;max-width:100%;outline:none}input:focus,select:focus,textarea:focus,button:focus-visible{outline:2px solid #4f9d7e;outline-offset:2px}textarea{min-height:85px;resize:vertical}label{display:flex;flex-direction:column;gap:8px;font-size:13px;font-weight:600;margin:16px 0}label.check{flex-direction:row;align-items:center;font-weight:400;gap:9px}input[type=checkbox]{accent-color:var(--green);width:16px;height:16px}h1,h2,p{margin-top:0}h1{font-size:30px;letter-spacing:-1px;font-weight:600;margin-bottom:9px}h2{font-size:17px;letter-spacing:-.3px;font-weight:600;margin-bottom:12px}p{line-height:1.6}small{font-size:12px}.muted,small{color:var(--muted)}.eyebrow{font-size:10px;letter-spacing:1.8px;font-weight:700;color:#788b81}.brand{display:flex;align-items:center;gap:10px;color:#215b45;text-decoration:none;font-size:38px}.brand span{font-size:28px;font-weight:650;letter-spacing:-1px}.shell{display:flex;min-height:100vh}.sidebar{width:236px;padding:29px 21px 15px;background:#fff;border-right:1px solid var(--line);position:fixed;top:0;bottom:0;display:flex;flex-direction:column}.workspace-label{margin-top:38px;margin-bottom:11px}.workspace-select{width:100%;font-weight:600;background:#f6f8f6;padding:12px 10px}.sidebar nav{margin-top:24px;display:flex;flex-direction:column;gap:6px}.sidebar nav :is(button,a){display:flex;align-items:center;gap:12px;border:0;background:none;text-align:left;padding:13px 12px;color:#728179;font-size:13px}.sidebar nav :is(button,a).active{background:#eaf3ed;color:#236648;font-weight:600}.nav-icon{font-size:20px;width:21px;text-align:center;font-weight:400}.counter{margin-left:auto;background:#d8e8df;padding:1px 6px;border-radius:4px;font-size:11px}.sidebar-bottom{margin-top:auto}.side-note{display:flex;align-items:center;gap:7px;font-size:10px;color:#8a958e;margin:30px 0 22px}.status-dot{display:inline-block;width:7px;height:7px;background:#639b7c;border-radius:50%;flex-shrink:0}.account-button{display:flex;width:100%;gap:10px;text-align:left;padding:14px 0;border:0;border-top:1px solid var(--line);border-radius:0;background:none}.account-button strong,.account-button small{display:block;margin:2px 0}.account-button>span:last-child{margin-left:auto;align-self:center;color:#8c9991}.avatar{width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;font-size:12px;font-weight:600}.avatar.small{width:27px;height:27px;font-size:10px}.teal{background:#e3f2ed;color:#3d846b}.purple{background:#eee9f7;color:#8874aa}.amber{background:#fcf0da;color:#a27d39}.blue{background:#e6effa;color:#5b84b4}.rose{background:#f7e8eb;color:#ac7482}.signout{font-size:11px;margin-left:47px}.main{margin-left:236px;flex:1;min-width:0}.topbar{height:78px;background:#fff;border-bottom:1px solid var(--line);padding:0 40px;display:flex;align-items:center;justify-content:space-between;font-size:12px}.slash{margin:0 14px;color:#b7c1ba}.top-actions{display:flex;gap:20px;align-items:center}.content{max-width:1460px;margin:0 auto;padding:38px 40px 45px}.page-heading{display:flex;justify-content:space-between;align-items:center;gap:15px;margin-bottom:24px}.page-heading .eyebrow{margin-bottom:10px}.page-heading p{margin:0;font-size:13px}.sync-bar{display:flex;align-items:center;gap:9px;border:1px solid #dfe9e2;background:#eef5ef;border-radius:7px;padding:11px 15px;font-size:11px;margin-bottom:25px;flex-wrap:wrap}.sync-bar .muted{margin-left:8px}.sync-bar button{margin-left:auto;font-size:11px}.sync-problem{background:#fff4e8;border-color:#edd6bb}.amber-dot{background:#c2a268}.sync-error{color:#ac733d;max-width:480px;overflow-wrap:anywhere}.summary-grid{display:grid;grid-template-columns:1.35fr 1fr 1fr;gap:18px;margin-bottom:24px}.summary-card{border:1px solid var(--line);border-radius:9px;background:#fff;padding:19px 22px;min-height:120px}.summary-card .eyebrow{display:block;margin-bottom:17px}.summary-card small{display:block;margin-top:5px}.person-line{display:flex;align-items:center;gap:12px}.person-line .avatar{width:42px;height:42px}.person-line strong{font-size:17px}.stat{font-size:23px;font-weight:600;letter-spacing:-.4px}.panel{background:white;border:1px solid var(--line);border-radLine truncated
@media(min-width:1600px){.content{padding-top:48px}.day{min-height:130px}}@media(max-width:1200px){.sidebar{width:210px;padding:25px 16px}.main{margin-left:210px}.content{padding:28px 24px}.topbar{padding:0 24px}.page-footer{margin:0 24px}.summary-card{padding:18px 15px}.person-line strong{font-size:15px}.toolbar-controls{gap:7px}.calendar-toolbar{padding:18px 15px}.day{padding:9px 5px;min-height:102px}.assignment{padding:6px 4px;font-size:10px}}@media(max-width:950px){.sidebar{width:75px;padding:23px 10px}.sidebar .brand span,.workspace-label,.workspace-select,.sidebar nav button>span:not(.nav-icon),.side-note,.account-button>span:not(.avatar),.signout{display:none}.sidebar nav{margin-top:40px}.sidebar nav :is(button,a){padding:12px;justify-content:center}.sidebar .brand{justify-content:center}.main{margin-left:75px}.sidebar .account-button{justify-content:center}.summary-grid{gap:10px}.summary-card{padding:15px 12px}.page-heading h1{font-size:25px}.page-heading>.primary{font-size:12px}.top-actions{gap:10px}.login-story{padding:40px}.login-story h1{font-size:38px}}@media(max-width:700px){.sidebar{position:sticky;top:0;width:100%;height:auto;z-index:10;flex-direction:row;align-items:center;gap:10px;padding:8px 12px;border-right:0;border-bottom:1px solid var(--line)}.sidebar .brand{font-size:29px}.sidebar nav{margin:0;flex-direction:row;gap:0;flex:1;justify-content:center}.sidebar nav :is(button,a){padding:8px}.sidebar .nav-icon{font-size:20px;width:18px}.sidebar-bottom{margin:0}.account-button{padding:0;border:0}.account-button .avatar{width:28px;height:28px;font-size:10px}.shell{display:block}.main{margin:0}.topbar{height:auto;min-height:57px;padding:10px 15px;gap:10px;flex-wrap:wrap}.topbar strong{font-size:11px}.top-actions{margin-left:auto}.top-actions button{font-size:10px}.content{padding:25px 15px}.page-heading{align-items:flex-start;flex-direction:column}.page-heading h1{font-size:27px}.page-heading p{font-size:12px}.summary-grid{grid-template-columns:1fr 1fr}.summary-card:first-child{grid-column:1/-1}.summary-card{min-height:100px}.summary-card .eyebrow{margin-bottom:13px}.calendar-toolbar{flex-wrap:wrap;padding:16px 12px}.toolbar-controls{width:100%;justify-content:space-between}.toolbar-controls select{max-width:165px}.weekdays span{padding:10px 3px;text-align:center;font-size:8px}.day{min-height:90px;padding:6px 3px}.day-date{font-size:10px;width:20px;height:20px;line-height:20px;margin-bottom:4px}.assignment{font-size:9px;padding:6px 3px;gap:3px}.mini-dot{display:none}.calendar-footer{padding:12px;flex-direction:column;font-size:9px}.two-columns,.card-grid{grid-template-columns:1fr}.modal .two-columns{grid-template-columns:1fr 1fr}.sync-bar{font-size:10px;padding:10px}.sync-bar .muted{margin-left:0}.page-footer{margin:0 15px;gap:20px}.page-footer span{font-size:9px;letter-spacing:0}.section-line h2{font-size:15px}.rule-card{padding:20px}.login{grid-template-columns:1fr}.login-story{min-height:0;padding:28px 30px}.login-story>div{display:none}.login-story .quiet{margin-top:14px}.login-form{padding:35px 0}.modal-backdrop{padding:12px}.modal{padding:20px;max-height:calc(100dvh - 24px)}.modal-heading h2{font-size:22px}.modal-footer button{font-size:11px;padding:9px}.audit-row summary{font-size:11px}.audit-row time{margin-left:0}}
.mobile-workspace-select{display:none}@media(max-width:950px){.mobile-workspace-select{display:block;max-width:210px}}@media(max-width:700px){.mobile-workspace-select{width:100%;max-width:100%}}
/* People, country calendars, and readable policy flows. */
.person-select{display:flex;align-items:center;justify-content:space-between;gap:12px;text-align:left;min-height:44px}.wide{width:100%;margin-top:15px}.picker-results{max-height:260px;overflow:auto;border:1px solid var(--line);border-radius:8px}.picker-option{display:flex;align-items:center;gap:12px;width:100%;border:0;border-bottom:1px solid var(--line);border-radius:0;text-align:left;padding:13px}.picker-option>span:nth-child(2){flex:1;min-width:0;overflow-wrap:anywhere}.picker-option strong,.picker-option small{display:block}.picker-option small{margin-top:5px}.picker-option .badge{font-size:9px}.email-contact{margin-top:20px;padding:18px;background:#f3f7f4;border-radius:8px}.person-picker .section-line{margin-top:0}.person-picker h3{margin:0}.person-picker .primary{overflow-wrap:anywhere;max-width:100%}.holiday-group{margin:12px 0 18px}.holiday-group h3{display:flex;gap:8px;align-items:center;font-size:14px;border-bottom:1px solid var(--line);padding-bottom:12px}.holiday-group .list-row{padding:10px 0}.csv-input{min-height:200px;font-family:Consolas,monospace;font-size:12px;line-height:1.6}.import-summary{display:flex;gap:10px;flex-wrap:wrap;padding:15px;background:#eaf4ed;border-radius:8px;font-size:12px}.import-summary strong{color:var(--green)}.import-table{max-height:260px;overflow:auto;border:1px solid var(--line);border-radius:8px}.import-table>div{display:grid;grid-template-columns:1fr 100px 1.5fr;gap:10px;padding:10px;border-bottom:1px solid var(--line);font-size:12px;overflow-wrap:anywhere}.import-table-head{background:#f2f6f2;font-weight:600;position:sticky;top:0}.validation-error,.validation-warning{padding:9px 12px;border-radius:6px;font-size:12px}.validation-error{background:#fff0eb;color:#9c492f}.validation-warning{background:#fff6e5;color:#806136}.danger-text{color:#9c492f}.flow-origin{display:inline-block;border:1px dashed #a9c4b2;color:#678373;border-radius:20px;padding:7px 12px;font-size:11px}.flow-arrow{color:#789184;font-size:11px;padding:9px 0;display:flex;gap:10px;align-items:center}.flow-arrow>span:first-child{font-size:22px;line-height:1}.flow-node{display:flex;align-items:center;gap:11px;background:#f3f7f4;border:1px solid #dce8df;border-radius:8px;padding:13px;font-size:12px}.step-number{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;background:#dcecdf;color:#407553;font-size:11px;flex-shrink:0}.escalation-flow{margin:18px 0}.coverage-flow>div{display:flex;align-items:center;gap:10px;font-size:12px}.coverage-flow b{display:inline-grid;place-items:center;width:23px;height:23px;background:#edf3ef;border-radius:50%;font-size:11px}.coverage-flow small{margin-left:auto}.coverage-flow .flow-arrow{padding-left:7px}.routing-list{display:grid;gap:12px}.routing-card{padding:18px 21px}.route-heading{display:flex;align-items:center;gap:12px}.route-heading button{font-size:11px}.route-flow{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:15px;font-size:12px}.logic-key{font-size:10px;font-weight:700;letter-spacing:1px;color:#468364}.logic-conditions{display:flex;align-items:center;flex-wrap:wrap;gap:7px;flex:1;min-width:140px}.condition-chip{display:inline-block;padding:7px 9px;border:1px solid #dae5df;border-radius:5px;background:#f5f8f6;overflow-wrap:anywhere;max-width:100%;font-size:11px}.condition-chip.hours{background:#f9f3e8;color:#967444;border-color:#ebdec3}.condition-group{display:inline-flex;align-items:center;gap:7px;max-width:100%}.logic-and{font-size:9px;font-weight:700;color:#93a399;letter-spacing:.5px}.route-result{padding:9px 12px;border-radius:6px;background:#eaf3ed;color:#33744e;font-weight:600;max-width:100%;overflow-wrap:anywhere}.horizontal{font-size:23px}.logic-editor{background:#f7f9f7;border:1px solid #e0e8e1;border-radius:9px;padding:15px;margin-top:20px}.logic-header{display:flex;gap:10px;align-items:center;font-size:12px;flex-wrap:wrap}.condition-editor{display:grid;grid-template-columns:32px minmax(75px,1fr) minmax(85px,1fr) minmax(90px,1.1fr) auto 28px;gap:6px;align-items:center;margin:12px 0}.condition-editor select,.condition-editor input:not([type=checkbox]){width:100%;max-width:none;font-size:11px;padding:8px 6px;min-width:0}.condition-editor label{margin:0;font-size:11px;gap:4px}.condition-editor button{padding:5px}.day-clock{font-size:9px;color:#899b8e;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.day{min-height:135px}.rule-card p,.list-row strong{overflow-wrap:anywhere}.step-editor .flow-arrow{border-bottom:1px solid var(--line);margin-top:12px}.modal details summary{cursor:pointer;font-size:12px;color:var(--green);margin:10px 0}.section-line .actions{margin-top:0}.display-zone{font-size:10px;color:#6f8779}.topbar{flex-wrap:wrap;gap:8px}.topbar .top-actions{margin-left:auto}
@media(max-width:700px){.condition-editor{grid-template-columns:1fr 1fr 1fr}.condition-editor>.logic-and{grid-column:1/-1}.condition-editor label{justify-self:start}.condition-editor button{justify-self:end}.condition-editor input:not([type=checkbox]){grid-column:3}.route-flow{gap:8px}.logic-conditions{flex-basis:80%}.routing-card{padding:15px}.route-result{font-size:11px}.import-table>div{grid-template-columns:75px 85px 1fr;font-size:10px}.day-clock{font-size:7px}.day{min-height:100px}.picker-option{padding:10px 7px}.picker-option .badge{display:none}.coverage-flow>div{flex-wrap:wrap}.coverage-flow small{margin-left:0}}
/* Configuration graph operations. */
.drag-handle{padding:5px 7px;font-size:22px;line-height:1;border-color:transparent;background:transparent;color:#8ba292;cursor:grab;touch-action:manipulation}.drag-handle:active{cursor:grabbing}.route-moves{display:flex;gap:3px}.move-route{padding:5px 8px;font-size:13px!important}.move-route:disabled{cursor:default;opacity:.3}.routing-card{position:relative;transition:opacity .15s,box-shadow .15s}.route-dragging{opacity:.4}.routing-card.drop-before:before,.routing-card.drop-after:after{content:'';position:absolute;height:3px;left:0;right:0;background:#2e8b60;border-radius:3px}.routing-card.drop-before:before{top:-8px}.routing-card.drop-after:after{bottom:-8px}.route-heading .route-position{min-width:28px;padding:6px}.route-heading .grow{overflow-wrap:anywhere}.dependency-list{border:1px solid var(--line);border-radius:8px;padding:14px 16px;margin:16px 0}.dependency-list h3{font-size:12px;color:#68816f;margin:0 0 8px}.dependency-list .list-row{font-size:12px}.removal-preview .validation-warning p{margin:7px 0 0;font-weight:600}button.primary.remove-confirm{background:#985b45;border-color:#985b45}.removal-preview p{font-size:13px}.removal-preview .badge{white-space:nowrap}
@media(max-width:700px){.route-heading{flex-wrap:wrap;gap:7px}.route-heading .grow{flex-basis:calc(100% - 70px)}.route-heading .route-moves{margin-left:auto}.dependency-list .list-row{align-items:flex-start;flex-wrap:wrap}.removal-preview .badge{font-size:9px}}
.date-field{display:flex;position:relative;align-items:stretch;min-width:0}.date-field>input[type=text]{width:100%;min-width:0;border-radius:7px 0 0 7px}.date-field>button{border-left:0;border-radius:0 7px 7px 0;padding:8px 11px}.date-field .native-date{position:absolute;width:1px;height:1px;padding:0;border:0;opacity:0;bottom:0;right:0;pointer-events:none}.holiday-group>summary{display:flex;align-items:center;gap:10px;cursor:pointer;padding:17px 0;font-weight:600;list-style:none}.holiday-group>summary::before{content:'▸';color:var(--muted)}.holiday-group[open]>summary::before{content:'▾'}.holiday-group>summary .badge{margin-left:auto}.absence-range{padding:0 14px 12px;border:1px solid var(--line);border-radius:8px;margin:14px 0}.absence-preview{border-top:1px solid var(--line);margin-top:22px;padding-top:8px}.impact-scroll{overflow-x:auto}.impact-table{width:100%;border-collapse:collapse;font-size:12px}.impact-table th{text-align:left;color:var(--muted);font-weight:500}.impact-table td,.impact-table th{padding:12px 8px;border-bottom:1px solid var(--line);vertical-align:top}.impact-table small{display:block;font-size:10px;line-height:1.6;margin-top:5px}.impact-table td:first-child{min-width:160px}.impact-table td{overflow-wrap:anywhere}.preview-count{font-size:12px;color:var(--green)}
/* Typed workspace data and generation preview. */
.wide-modal{width:100%;max-width:1000px}.generation-toolbar{display:flex;align-items:center;gap:12px;margin-bottom:18px;flex-wrap:wrap}.generation-toolbar .info-note{flex-basis:45%;margin:0}.generation-panel{padding:22px;min-width:0}.generation-panel .section-line{margin-top:0}.generation-panel>p{font-size:12px;line-height:1.6}.generation-columns{align-items:start}.variable-card{display:flex;align-items:center;gap:7px;padding:16px 0;border-top:1px solid var(--line);font-size:13px}.variable-card .grow{min-width:0;overflow-wrap:anywhere}.variable-card small{display:block;margin-top:6px;color:var(--muted);font-size:10px}.variable-card button{font-size:10px;padding:7px}.generation-preview{margin-top:18px}.data-tree{border:1px solid var(--line);border-radius:8px;overflow:hidden}.data-row{display:grid;grid-template-columns:minmax(80px,1fr) 130px minmax(180px,2fr) 92px;gap:8px;align-items:center;padding:10px;border-bottom:1px solid var(--line);background:#fafcf9}.data-row input,.data-row select{min-width:0;width:100%;font-size:12px}.data-key{font-size:12px;color:var(--muted)}.data-value{min-width:0}.data-depth-1{padding-left:24px}.data-depth-2{padding-left:40px}.data-depth-3,.data-depth-4,.data-depth-5,.data-depth-6,.data-depth-7,.data-depth-8{padding-left:56px}.data-actions{display:flex;gap:3px}.data-actions button{padding:5px}.data-value .actions{margin:0;flex-wrap:wrap}.data-value .actions button{padding:7px;font-size:11px}.person-value{display:grid;gap:7px}.bulk-editor,.binding-editor{background:#edf5ef;border:1px solid #c9dfce;border-radius:10px;padding:18px;margin:18px 0}.bulk-editor h3,.binding-editor h3{font-size:15px}.bulk-editor textarea{width:100%}.binding-button{text-align:left;width:100%;color:#34744f;background:#f0f7f1;border-color:#d6e6d8;overflow-wrap:anywhere;line-height:1.5;min-width:0}.binding-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 18px}.binding-grid label{min-width:0}.generation-block{border:1px solid var(--line);border-radius:10px;padding:18px;margin:18px 0}.iteration-row,.generation-step{display:flex;gap:8px;align-items:center;margin:13px 0}.iteration-row>*{min-width:0}.iteration-row>input{max-width:150px}.generation-step{flex-wrap:wrap}.generation-step .binding-button{width:auto}.generation-step .grow{flex:1}.generation-step select{font-size:11px;max-width:180px}.generation-step .check{margin:0}.binding-source{padding:0 4px}.binding-key{margin:14px 0 0 16px;padding-left:16px;border-left:2px solid #b5d2be}.priority-person{margin:14px 0}.demo-note dl{margin:8px 0}.demo-note dl>div{display:flex;gap:12px}.demo-note dt{min-width:34px;font-weight:600}.demo-note dd{margin:0}.demo-note{font-size:11px;line-height:1.8}
@media(max-width:900px){.generation-columns{grid-template-columns:1fr}.data-row{grid-template-columns:80px 110px minmax(100px,1fr)}.data-actions{grid-column:3;justify-content:flex-end}.iteration-row{flex-wrap:wrap}.iteration-row .binding-button{flex-basis:100%}}
@media(max-width:700px){.wide-modal .two-columns,.binding-grid{grid-template-columns:1fr}.data-row{grid-template-columns:1fr 1fr;padding:12px}.data-value{grid-column:1/-1}.data-actions{grid-column:2}.data-depth-1{border-left:8px solid #e6eee8}.data-depth-2,.data-depth-3{border-left:16px solid #e6eee8}.generation-toolbar>button{flex:1}.generation-block{padding:12px}.generation-panel{padding:15px}.variable-card{flex-wrap:wrap}.variable-card .grow{flex-basis:50%}.binding-key{margin-left:5px;padding-left:10px}.sidebar nav :is(button,a){padding:6px}.sidebar .nav-icon{font-size:18px;width:17px}}
.demo-banner{position:sticky;top:0;z-index:40;background:#fff4d9;border-bottom:1px solid #dfc68e;padding:10px 22px;color:#725624;font-size:11px;line-height:1.6}.demo-banner>div{display:flex;gap:8px 20px;flex-wrap:wrap}.demo-banner strong{font-weight:700}.demo-protection{margin:5px 0 0;border-left:3px solid #b36040;padding-left:10px;color:#8a4229}.demo-mode .sidebar{top:var(--demo-offset,0px);height:calc(100dvh - var(--demo-offset,0px))}@media(max-width:700px){.demo-banner{padding:8px 12px;font-size:10px}.demo-banner>div{gap:3px 10px}.demo-mode .sidebar{height:auto}}
@media(min-width:701px){.sidebar{overflow-y:auto}.sidebar-bottom{padding-top:20px}}
/* Daily collaboration has one primary action; setup is a separate destination. */
.sr-only{position:absolute;width:1px;height:1px;clip-path:inset(50%);overflow:hidden;white-space:nowrap}.sidebar nav .setup-toggle{margin-top:20px;border-top:1px solid var(--line);border-radius:0;padding-top:20px}.setup-nav{padding-left:20px;border-left:1px solid var(--line);margin-left:20px}.sidebar nav .setup-nav :is(button,a){width:100%;padding:10px;font-size:12px}.daily-overview{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:24px}.today-card{background:white;border:1px solid var(--line);padding:20px 22px;border-radius:12px;display:flex;flex-direction:column;gap:13px}.today-card .person-line small,.today-card>small{display:block;margin-top:5px;line-height:1.6}.attention-panel{background:#fff;border:1px solid #bdd8cb;border-radius:12px;padding:0 20px;margin-bottom:24px}.reply-row{display:flex;align-items:center;gap:14px;padding:18px 0;border-top:1px solid var(--line)}.reply-row small{display:block;margin:7px 0;line-height:1.6}.reply-row p{margin:8px 0 0}.reply-row .actions{flex-shrink:0;margin:0}.waiting-line{display:flex;align-items:center;gap:15px;margin-bottom:18px;font-size:12px;color:var(--muted)}.action-comment{font-size:13px;border-left:2px solid #cadbce;padding-left:10px;overflow-wrap:anywhere}.page-heading .actions{margin:0}.schedule-control{position:relative}.schedule-trigger{min-width:180px;display:flex;justify-content:space-between;gap:12px;font-weight:600}.schedule-picker{position:absolute;right:0;top:45px;width:280px;padding:12px;background:white;box-shadow:0 8px 28px #142a2825;border:1px solid var(--line);border-radius:10px;z-index:15}.schedule-picker input{width:100%}.schedule-results{max-height:240px;overflow:auto;margin-top:8px}.schedule-results button{width:100%;display:block;text-align:left;border:0;padding:12px}.chosen{background:#e6f2eb!important;border-color:#548b6b!important;color:#19513a!important}.day{transition:background .15s,box-shadow .15s}.day:hover{background:#edf5ef;box-shadow:inset 0 0 0 1px #b5cebc}.shift-ticket{background:#f3f7f4;border:1px solid #dce6df;border-radius:12px;padding:18px;margin:16px 0 22px}.shift-ticket .person-line .badge{margin-left:auto}.shift-times{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:20px;padding-top:15px;border-top:1px solid #dde7e0}.shift-times strong{display:block;font-size:12px;margin-top:6px}.shift-times>span{color:#85968a}.cover-self{display:flex;align-items:center;gap:12px;margin:18px 0;flex-wrap:wrap}.cover-options{display:flex;gap:8px;flex-wrap:wrap;max-height:190px;overflow:auto}.cover-options button{display:flex;align-items:center;gap:8px;padding:10px 13px}.selection-tick{color:var(--green);font-weight:bold}.range-options{margin:20px 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:5px 0}.range-options>summary{padding:8px 0;font-weight:500}.request-ready{font-size:12px;background:#f0f7f1;padding:12px;border-radius:8px;line-height:1.6}.request-ready small{display:block;margin-bottom:6px;font-size:10px}.cover-footer{flex-wrap:wrap;gap:8px}.cover-footer .primary{margin-left:auto}.force-action{color:#82633e;font-size:11px}.shift-option{display:flex;flex-direction:column;gap:10px;width:100%;text-align:left;margin:12px 0}.simple-list-row{display:flex;gap:8px;align-items:center;margin:12px 0}.simple-list-row input{flex:1;min-width:0}.simple-list-row button{padding:6px 9px}.matrix-head,.matrix-row{display:grid;grid-template-columns:150px 1fr 24px;gap:14px;align-items:center}.matrix-head{font-size:11px;color:var(--muted);padding:12px 0}.matrix-row{padding:16px 0;border-top:1px solid var(--line)}.matrix-row input{width:100%;min-width:0}.matrix-people{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.matrix-person{display:flex;align-items:center;gap:7px;border-radius:7px;background:#edf4ef;padding:7px 9px;font-size:12px}.matrix-person b{color:#84978a;font-weight:500;font-size:10px}.matrix-person button{padding:0 3px;background:none;border:0;color:#75917d}.inline-people{background:#f3f7f4;border-radius:10px;padding:16px;margin:16px 0}.inline-people input{width:100%;margin:10px 0}.inline-people .section-line{margin:0}.data-mode{margin:14px 0}.modal-heading .eyebrow{font-size:9px}.modal-footer{position:sticky;bottom:-28px;background:white;padding-bottom:18px;z-index:5}.modal{scrollbar-gutter:stable}.request-card .action-comment{margin-top:10px}
@media(max-width:950px) and (min-width:701px){.sidebar .setup-nav{position:fixed;left:74px;top:calc(var(--demo-offset,0px) + 240px);background:white;box-shadow:0 6px 20px #20392c25;padding:12px;margin:0;width:210px;border-radius:8px;z-index:20}.sidebar nav .setup-toggle{justify-content:center}}
@media(max-width:700px){.sidebar nav .setup-toggle{margin:0;border:0;padding:8px}.sidebar .setup-nav{position:absolute;right:10px;top:55px;width:220px;padding:12px;margin:0;background:white;border:1px solid var(--line);border-radius:10px;box-shadow:0 8px 25px #1f372925}.sidebar nav .setup-nav :is(button,a){display:block;text-align:left;padding:12px}.sidebar nav>button span:not(.nav-icon){display:none}.daily-overview{grid-template-columns:1fr;gap:10px}.today-card{padding:16px}.reply-row{flex-wrap:wrap;align-items:flex-start}.reply-row>.grow{width:calc(100% - 60px)}.reply-row>.actions{margin-left:50px}.attention-panel{padding:0 14px}.shift-times{align-items:flex-start;flex-direction:column;gap:10px}.shift-times>span{display:none}.shift-times>div{display:flex;justify-content:space-between;gap:10px;width:100%}.shift-times strong{margin:0}.cover-footer .force-action{order:3;width:100%;border:0}.cover-footer .primary{flex:1}.modal-footer{bottom:-20px}.schedule-picker{left:0;right:auto;width:min(280px,calc(100vw - 55px))}.matrix-head{display:none}.matrix-row{grid-template-columns:1fr 24px;gap:10px}.matrix-people{grid-column:1/-1;grid-row:2}.matrix-row>button{grid-column:2;grid-row:1}.simple-list-row{gap:4px}.simple-list-row button{padding:5px}.simple-list-row .step-number{display:none}.top-actions>.text,.top-actions>button{font-size:10px}.cover-options button{font-size:12px}.cover-sheet .two-columns{grid-template-columns:1fr}.day-clock{display:none}.day{min-height:85px}}
.shift-ticket .person-line small{display:block;margin-top:5px}.request-ready>span{display:block;font-weight:500;color:var(--green)}
.sidebar nav a{text-decoration:none;border-radius:7px}.sidebar nav a:focus-visible{outline:2px solid #4f9d7e;outline-offset:2px}.setup-section{scroll-margin-top:150px;margin-bottom:38px}.generated-group{border-top:1px dashed #baccc2;margin-top:24px;padding-top:18px}.generated-group>h3{font-size:12px;color:var(--muted);font-weight:500}.generated-resource{background:#f3f6f5;box-shadow:none}.manual-resource{border-left:3px solid #59977c}.resource-origin{display:block;font-size:10px;color:#667b70;margin-bottom:10px}
.recipe-section{border-top:1px solid var(--line);margin-top:18px;padding-top:18px}.recipe-section h3{display:flex;align-items:center;gap:10px;font-size:15px}.recipe-number{display:inline-flex;width:25px;height:25px;align-items:center;justify-content:center;background:#e3eee8;color:#236648;border-radius:50%;font-size:12px}.advanced-rule{font-size:12px;color:#687e71;margin:12px 0}.advanced-rule summary{cursor:pointer}.recipe-field{min-width:0}.recipe-field>label{margin:12px 0 6px}.recipe-field>select,.recipe-field .inline-value{width:100%}.recipe-field .inline-value{margin-top:7px}.binding-tweak{font-size:11px!important}.recipe-example{background:#f3f7f4;border-radius:9px;padding:16px}.recipe-example table{width:100%;font-size:12px;border-collapse:collapse;text-align:left}.recipe-example td,.recipe-example th{padding:9px 8px;border-bottom:1px solid var(--line)}
.coverage-problems{border-color:#e5c9a8;background:#fffbf5}.coverage-issue-list{max-height:230px;overflow:auto}
.setup-jumps{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 20px}.setup-jumps a{padding:8px 12px;border:1px solid var(--line);border-radius:6px;color:var(--green);text-decoration:none;font-size:12px;background:white}
.recipe-number{flex-shrink:0}.rule-identity{grid-template-columns:minmax(0,1fr) auto!important;align-items:end}.rule-identity .check{margin-bottom:25px}@media(max-width:640px){.recipe-section>.section-line{flex-direction:column;align-items:stretch;gap:4px}.recipe-section>.section-line>select{width:100%}.recipe-section h3{margin:0 0 12px}.recipe-section{margin-top:12px;padding-top:14px}.rule-identity{gap:12px!important}}
.cover-range{margin-top:18px;padding-top:14px;border-top:1px solid var(--line)}.cover-boundary{display:grid;grid-template-columns:minmax(0,1fr) 130px;gap:14px}.cover-boundary label{margin:10px 0}.cover-boundary input{min-width:0;width:100%}.modal-footer{flex-wrap:wrap}.generation-live-status{flex-basis:100%;width:100%;padding:10px 12px;border-radius:7px;background:#edf5ef;font-size:12px}.generation-live-status.has-errors{background:#fff0ed;color:#913d32;border:1px solid #e9beb5}.validation-status-line{display:flex;align-items:center;justify-content:space-between;gap:10px}.validation-status-line button{font-size:11px;padding:5px 8px}.validation-first{margin:6px 0 0;line-height:1.4;overflow-wrap:anywhere}.generation-live-errors{max-height:150px;overflow:auto}.generation-live-errors p{line-height:1.4;overflow-wrap:anywhere}.generation-live-status .validation-first{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}@media(max-width:500px){.cover-boundary{grid-template-columns:minmax(0,1fr) 110px;gap:10px}.generation-live-errors{max-height:100px}}
.recipe-field>div>select{width:100%}.name-composer{display:flex;align-items:center;min-width:0;border:1px solid #d8e1da;border-radius:7px;background:white;overflow:hidden}.name-composer:focus-within{outline:2px solid #4f9d7e;outline-offset:2px}.recipe-field .name-composer>select{flex:0 1 46%;width:46%;min-width:0;border:0;border-right:1px solid #d8e1da;border-radius:0;background:white;padding:11px 8px;text-overflow:ellipsis}.recipe-field .name-composer>.inline-value{flex:1 1 54%;width:54%;min-width:0;margin:0;border:0;border-radius:0;padding:11px 8px}.name-composer>input:focus,.name-composer>select:focus{outline:none}.name-composer>.name-prefix{flex:0 1 25%;width:25%;min-width:0;border:0;border-radius:0;padding:11px 6px}
.binding-value-preview{margin:16px 0;padding:12px;background:#f3f7f4;border:1px solid var(--line);border-radius:8px;min-width:0}.binding-value-preview .section-line{margin:0 0 8px;gap:8px;flex-wrap:wrap}.binding-value-preview strong{font-size:12px}.binding-value-preview small{font-size:11px;color:#65796d;overflow-wrap:anywhere}.binding-value-preview textarea{display:block;width:100%;box-sizing:border-box;height:116px;min-height:80px;max-height:240px;resize:vertical;overflow:auto;white-space:pre;font:12px/1.5 Consolas,"Courier New",monospace;background:#fff;color:#244332;margin:0;padding:10px}.binding-value-preview .validation-warning{margin-bottom:0}
.template-token{font-family:Consolas,"Courier New",monospace!important;font-size:12px;letter-spacing:0;font-weight:500;color:#285c48;background:#edf5f0;border-radius:5px}.binding-template{display:block;padding:8px 10px;overflow-wrap:anywhere;margin:12px 0}.route-heading .template-token{padding:5px 8px}
/* Schedule setup: sources and review, contiguous resources, occasional calendar maintenance. */
#generation .setup-intro{margin:24px 0 18px}#generation .setup-intro h2{font-size:20px;margin-bottom:6px}#generation .setup-intro p{margin:0}#generation .generation-columns{grid-template-columns:minmax(0,.85fr) minmax(0,1.35fr);gap:18px;align-items:start}#generation .generation-panel{padding:20px}#generation .variable-card{gap:8px;padding:12px 0}#generation .variable-card .grow{min-width:0}#generation .variable-card strong{overflow-wrap:anywhere}#generation .variable-card button{padding:7px 9px;font-size:11px}#generation .section-line{margin-top:0}.generation-review-bar{margin-top:18px;border:1px solid var(--line);border-radius:9px;background:#fff}.generation-review>summary{display:flex;align-items:center;justify-content:space-between;gap:16px;cursor:pointer;padding:15px 18px;font-size:13px;list-style:disclosure-closed}.generation-review .review-status{font-size:12px}.generation-review[open]>summary{border-bottom:1px solid var(--line)}.review-body{padding:16px 18px}.review-body .generation-preview>h3{display:none}.review-actions{border-top:1px solid var(--line);padding:12px 18px;justify-content:flex-end}.setup-resources{margin-top:32px;padding-top:24px;border-top:1px solid var(--line)}.setup-resources>.section-line{margin-top:0}.section-count{font-size:12px;color:var(--muted);font-weight:400;margin-left:10px}.setup-resources .rule-card{padding:18px}.setup-resources .rule-card h2{font-size:16px}.setup-resources .generated-group{margin-top:18px;padding-top:16px}.setup-resources .generated-group>h3{font-size:11px;color:var(--muted);letter-spacing:.5px}.setup-resources .escalation-flow{margin:12px 0}.setup-availability{margin-top:32px;padding-top:24px;border-top:1px solid var(--line)}.setup-utilities{display:grid;grid-template-columns:1fr 1fr;gap:18px;align-items:start}.setup-widget{background:#fff;border:1px solid var(--line);border-radius:9px;padding:16px 18px;min-width:0}.widget-heading{display:flex;gap:10px;align-items:center}.widget-heading h3{font-size:14px;margin:0}.widget-heading button{margin-left:auto}.setup-widget>p{font-size:12px;margin:12px 0 0}.holiday-widget>summary{display:flex;align-items:center;gap:10px;cursor:pointer;font-weight:600;flex-wrap:wrap}.holiday-widget>summary:before,.absence-widget-list>summary:before{content:'›';font-size:20px;color:var(--muted)}.holiday-widget[open]>summary:before,.absence-widget-list[open]>summary:before{transform:rotate(90deg)}.holiday-widget>summary small{width:100%;font-weight:400;padding-left:18px}.widget-body{margin-top:16px}.widget-scroll{max-height:360px;overflow:auto}.absence-widget-list>summary{display:flex;align-items:center;gap:8px;cursor:pointer;margin-top:8px;font-size:12px}.setup-widget .holiday-group{margin:10px 0 0}.setup-widget .list-row{padding:10px 0}.setup-widget .list-row strong{font-size:12px}.setup-widget .badge{font-size:11px}.setup-widget .actions button{font-size:12px}.setup-jumps a{background:white}.setup-jumps a:hover{border-color:#8eb5a2}
@media(max-width:1050px){#generation .generation-columns{grid-template-columns:1fr}}@media(max-width:650px){.setup-utilities{grid-template-columns:1fr}.generation-review>summary{align-items:flex-start;flex-direction:column;gap:6px}.review-actions{flex-wrap:wrap}.setup-resources>.section-line{align-items:flex-start;gap:12px;flex-direction:column}.setup-resources>.section-line .actions{flex-wrap:wrap}#generation .generation-panel{padding:16px}.setup-widget{padding:14px}.variable-card{flex-wrap:wrap}}
.resource-summary{display:flex;align-items:center;justify-content:space-between;gap:14px;cursor:pointer;list-style:none}.resource-summary>div{min-width:0}.resource-summary h2{margin:5px 0 0;overflow-wrap:anywhere}.resource-summary .resource-origin{display:block;margin-bottom:6px}.resource-summary .eyebrow{display:block}.resource-summary>button{font-size:11px;flex-shrink:0}.resource-summary:before{content:'›';color:var(--muted);font-size:22px}.resource-disclosure[open]>.resource-summary:before{transform:rotate(90deg)}.resource-summary>div{flex:1}.resource-body{margin-top:16px}.setup-resources .card-grid{align-items:start}.setup-resources .resource-disclosure:not([open]){padding-top:14px;padding-bottom:14px}@media(max-width:500px){.resource-summary{gap:8px}.resource-summary>button{max-width:100px;white-space:normal}}
/* Compact setup utilities and a shared resource inventory. */
#availability.setup-availability{margin:0 0 20px;padding:0;border:0}.setup-utilities{display:flex;gap:10px;align-items:flex-start;flex-wrap:wrap}.setup-utilities .setup-widget{padding:0;flex:0 1 auto;min-width:180px}.setup-widget>summary{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;font-size:12px;list-style:none}.setup-widget>summary:before{content:'›';font-size:17px;color:var(--muted)}.setup-widget[open]>summary:before{transform:rotate(90deg)}.setup-widget[open]{flex-basis:420px}.setup-widget .widget-body{padding:0 14px 14px;margin-top:8px}.setup-widget .badge{margin-left:auto}.setup-widget .widget-scroll{max-height:280px}
#generation .setup-intro{margin:0 0 14px}#generation .setup-intro h2{font-size:16px}#generation .setup-intro p{display:none}#generation .generation-panel{padding:16px}#generation .generation-columns{gap:12px}.generation-review-bar{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;margin-top:12px}.generation-review>summary{justify-content:flex-start;gap:12px;padding:13px 14px;min-height:48px;font-size:12px}.generation-review>summary:before{content:'›';font-size:18px}.generation-review[open]>summary:before{transform:rotate(90deg)}.generation-review .review-status{font-size:11px}.review-actions{border:0;padding:8px;gap:6px}.review-actions button{font-size:11px;padding:7px 9px}.generation-review-bar:has(.generation-review[open]){grid-template-columns:1fr}.generation-review-bar:has(.generation-review[open]) .review-actions{border-top:1px solid var(--line)}
.resource-inventory{margin-top:24px}.resource-inventory>.section-line{margin:0 0 12px}.resource-create{gap:7px}.resource-create button,.schedule-create>summary{font-size:12px;padding:8px 11px}.schedule-create{position:relative}.schedule-create>summary{cursor:pointer;border:1px solid #dbe3de;border-radius:7px;background:#fff;list-style:none}.schedule-create-options{position:absolute;right:0;top:calc(100% + 5px);min-width:220px;padding:5px;border:1px solid var(--line);border-radius:8px;background:white;box-shadow:0 8px 24px #233d351a;z-index:5;display:grid;gap:3px}.schedule-create-options button{border:0;text-align:left}
.resource-table{border:1px solid var(--line);border-radius:9px;overflow:hidden;background:white}.resource-table .resource-group{margin:0;padding:0;border:0}.resource-table .generated-group{border-top:2px solid #d6dfe8}.resource-group-label{margin:0;padding:10px 15px;font-size:11px;color:var(--muted);font-weight:500;background:#f7f9f8}.resource-table .generated-group .resource-group-label{background:#eef2f8;color:#63768e}.resource-table .resource-row{margin:0;padding:0;border:0;border-top:1px solid var(--line);border-radius:0;box-shadow:none}.resource-table .manual-resource{background:white;border-left:3px solid #59977c}.resource-table .generated-resource{background:#f7f9fc;color:#3e5069;border-left:3px solid #aabbd3}.resource-table .resource-summary{display:grid;grid-template-columns:12px 105px minmax(0,1fr) 20px;gap:12px;padding:12px 15px;min-height:47px}.resource-table .resource-summary:hover{background:#edf2f566}.resource-table .resource-summary:before{font-size:19px}.resource-kind{font-size:11px;color:var(--muted)}.resource-name{font-size:13px;font-weight:500;overflow-wrap:anywhere}.generated-mark{font-size:17px;color:#7b8fab}.resource-table .resource-body{border-top:1px solid var(--line);margin:0;padding:16px 30px 18px 42px;font-size:12px}.resource-table .resource-body>p:last-child{margin-bottom:0}.resource-table .escalation-flow{margin-top:0}.resource-table .routing-list{display:block}.resource-table .route-heading{margin-bottom:12px}.resource-table .route-flow{margin:0}.resource-table .route-position{font-size:10px;padding:2px 5px;margin-right:6px}.resource-table .route-dragging{opacity:.5}.resource-table .drop-before{border-top:2px solid var(--green)}.resource-table .drop-after{border-bottom:2px solid var(--green)}
@media(max-width:700px){.resource-inventory>.section-line{align-items:flex-start;flex-direction:column;gap:10px}.resource-create{flex-wrap:wrap}.resource-create button,.schedule-create>summary{font-size:11px;padding:8px}.generation-review-bar{grid-template-columns:1fr}.generation-review>summary{flex-direction:row;align-items:center;flex-wrap:wrap}.review-actions{justify-content:flex-start;padding-top:0}.resource-table .resource-summary{grid-template-columns:10px 75px minmax(0,1fr) 15px;gap:7px;padding:11px 9px}.resource-kind{font-size:10px}.resource-name{font-size:12px}.resource-table .resource-body{padding:14px}.setup-utilities .setup-widget{min-width:0;flex:1}.setup-utilities .setup-widget[open]{flex-basis:100%}.schedule-create-options{left:0;right:auto}}
.generation-review-bar .review-actions{margin:0}
.resource-table .resource-summary .resource-title{min-width:0}.resource-title .resource-name{display:block}.resource-brief{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:5px;font-size:11px;font-weight:400;color:#77857e}.resource-source{display:flex;justify-content:flex-end;margin:-5px 0 12px}.source-rule-link{font-size:11px;padding:5px 8px;border:1px solid #d8e2ed;background:#f1f5fa;color:#526e90;max-width:100%;text-align:right;overflow-wrap:anywhere}.source-rule-link:disabled{opacity:1;cursor:default}.resource-table .resource-summary:has(.resource-title){align-items:start}.resource-table .resource-summary>.resource-kind{padding-top:2px}@media(max-width:700px){.resource-brief{white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.resource-source{margin-top:0}}
.form-validation{width:100%;flex-basis:100%}.validation-control{width:100%;font-size:12px}.validation-toggle{width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px;background:#f4f6f5;font-size:12px}.validation-toggle.is-checked{background:#edf5ef;color:#27684b}.validation-toggle.has-errors{background:#fff0ed;border-color:#e9beb5;color:#913d32}.validation-toggle.is-checking{opacity:.8}.validation-details{max-height:230px;overflow:auto;padding:8px 12px;border:1px solid var(--line);border-top:0;background:white}.validation-issue{padding:10px 0;border-bottom:1px solid var(--line);overflow-wrap:anywhere}.validation-issue>strong,.validation-issue>small{display:block;margin-bottom:5px}.validation-issue .actions{margin:6px 0 0;flex-wrap:wrap}.validation-issue button{font-size:11px;padding:5px 8px}.page-validation{position:sticky;top:70px;z-index:4;margin-top:12px}.generation-block{scroll-margin-top:16px}@media(max-width:700px){.validation-details{max-height:160px}.page-validation{top:150px}}
.absence-range .absence-zone{display:block;margin:8px 0}.absence-range .two-columns{align-items:start}.absence-range input[type=time]{width:100%}.impact-table .impact-interval{min-width:180px}.impact-interval>strong{display:block;margin-bottom:6px;overflow-wrap:anywhere}.impact-interval>small{display:flex;flex-direction:column;gap:3px;line-height:1.5;white-space:normal}.absence-range select{min-width:0;width:100%}
.coverage-problems.panel{padding:20px;margin:20px 0 26px;border:1px solid #e5c9a8;border-radius:10px}
.coverage-problems>p{margin-bottom:16px}.coverage-issue-list{display:flex;flex-direction:column;gap:12px;max-height:320px}
.coverage-issue{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:14px 16px;border:1px solid #ebc9c2;border-radius:8px;background:#fff8f6}
.coverage-issue.is-pending{background:#fffdf7;border-color:#e5dcc0}.coverage-issue-body{display:flex;flex-direction:column;gap:8px;min-width:0;overflow-wrap:anywhere}
.coverage-state{font-size:12px;font-weight:600;color:#9a4436}.is-pending .coverage-state{color:#806221}.coverage-times{display:flex;flex-wrap:wrap;gap:6px;line-height:1.6}.coverage-issue>button{flex-shrink:0}
@media(max-width:640px){.coverage-problems.panel{padding:16px}.coverage-issue{align-items:stretch;flex-direction:column;padding:12px}.coverage-times{flex-direction:column;gap:3px}}
#generation .generation-columns{grid-template-columns:minmax(0,.9fr) minmax(0,1.15fr) minmax(0,.85fr);gap:16px;align-items:start}
#generation .generation-panel{padding:18px;min-width:0}#generation .generation-panel>.section-line{flex-wrap:wrap;gap:10px}#generation .generation-panel h2{font-size:15px}
#generation .variable-card{flex-wrap:wrap;gap:6px}#generation .variable-card>.grow{flex:1 0 100%;min-width:0}#generation .variable-card button{font-size:11px;padding:6px 8px}
#generation #availability{margin:0;min-width:0}#availability .setup-utilities{display:flex;flex-direction:column;gap:16px}#availability .setup-widget{width:100%;min-width:0;padding:16px;flex:none}
#availability .widget-heading{font-size:14px;font-weight:600}#availability .widget-body{padding:0;margin-top:12px}#availability .widget-scroll{max-height:240px;overflow:auto;overscroll-behavior:contain;scrollbar-gutter:stable}
#availability .actions{flex-wrap:wrap;gap:8px;margin-top:0;margin-bottom:12px}#availability .list-row{gap:8px;align-items:flex-start}#availability .list-row>div{min-width:0;flex:1}#availability .list-row small{display:block;margin-top:5px;line-height:1.5}#availability .list-row button{font-size:11px;padding:5px 0}
#availability .holiday-group>summary{display:flex;gap:8px;align-items:center;padding:8px 0;font-size:12px;cursor:pointer}
@media(max-width:1200px){#generation .generation-columns{grid-template-columns: minmax(0,1fr) minmax(0,1fr)}#generation #availability{grid-column:1/-1}#availability .setup-utilities{display:grid;grid-template-columns:1fr 1fr}}
@media(max-width:700px){#generation .generation-columns{grid-template-columns:minmax(0,1fr)}#availability .setup-utilities{grid-template-columns:minmax(0,1fr)}}
#generation .variable-card .grow>small{display:block;white-space:normal;overflow-wrap:anywhere;line-height:1.5}
.workspace-variable-list,.generation-rule-list{max-height:360px;overflow:auto;overscroll-behavior:contain;scrollbar-gutter:stable;padding-right:4px}.workspace-variable-list:focus-visible,.generation-rule-list:focus-visible{outline:2px solid #4f9d7e;outline-offset:3px;border-radius:4px}
.resource-inventory{padding:20px;background:#fff;border:1px solid var(--line);border-radius:12px}
.resource-icon{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}
.resource-kind{display:flex;align-items:center;gap:7px}.resource-table .resource-summary{grid-template-columns:12px 116px minmax(0,1fr) 18px;align-items:center}
.resource-table .resource-body{padding:16px 20px 20px 150px;background:#fbfcfc;border-top:1px solid #e6ebe8}.resource-table .generated-resource .resource-body{background:#f8fafc}
.resource-source{margin:0 0 14px;min-height:0}.source-rule-link{display:inline-flex;align-items:center;font-size:12px;padding:7px 10px;border-radius:6px}
.resource-detail-list{border:1px solid var(--line);border-radius:8px;background:white;overflow:hidden}
.resource-detail-item{display:flex;position:relative;align-items:flex-start;gap:14px;padding:14px 16px}.resource-detail-item+.resource-detail-item{border-top:1px solid var(--line)}
.resource-detail-icon{width:32px;height:32px;display:flex;align-items:center;justify-content:center;border:1px solid #dce7e1;border-radius:8px;background:#f0f5f2;color:#54806a;flex-shrink:0;margin-top:2px}
.resource-detail-copy{min-width:0;display:flex;flex-direction:column;gap:6px;overflow-wrap:anywhere}.resource-detail-copy strong{font-size:13px;font-weight:600;line-height:1.5}.resource-detail-copy small{line-height:1.5;display:block}.resource-detail-label{font-size:11px;color:#6c8075}
.resource-detail-item.is-flow-step:before{content:'';position:absolute;top:-5px;left:29px;width:6px;height:6px;border-right:1.5px solid #8aaa97;border-bottom:1.5px solid #8aaa97;transform:rotate(45deg);background:white;z-index:1}
.resource-actions,.resource-body .route-heading{margin:14px 0 0;gap:8px}.resource-body .route-heading{padding:0 0 14px}.resource-actions button,.resource-body .route-heading button{font-size:12px}
@media(max-width:700px){.resource-inventory{padding:12px}.resource-table .resource-summary{grid-template-columns:10px 25px minmax(0,1fr) 15px}.resource-kind>span{display:none}.resource-table .resource-body{padding:12px}.resource-detail-item{padding:12px;gap:10px}.resource-detail-item.is-flow-step:before{left:25px}.resource-source{justify-content:flex-start}.source-rule-link{text-align:left}.resource-detail-copy strong{font-size:12px}}
@media(max-width:700px){.resource-table .resource-summary{grid-template-columns:10px 66px minmax(0,1fr) 15px}.resource-kind{flex-direction:column;align-items:flex-start;gap:5px}.resource-kind>span{display:block;font-size:9px}}
.resource-table .resource-body{padding:10px 18px 14px 42px}.resource-source{margin:0 0 8px}.source-rule-link{font-size:11px;padding:4px 7px}
.resource-widget{min-width:0}.widget-meta{display:flex;align-items:center;flex-wrap:wrap;gap:8px 16px;font-size:11px;color:#748379;margin-bottom:10px}.widget-type{display:inline-flex;align-items:center;gap:6px;color:#486153;font-weight:600}
.widget-chain{display:flex;align-items:center;flex-wrap:wrap;gap:12px 24px}.widget-node{position:relative;display:flex;align-items:center;gap:8px;min-width:0;max-width:100%;padding:8px 11px;border:1px solid #dce5e0;border-radius:7px;background:white;font-size:12px;overflow-wrap:anywhere}
.widget-node>div{min-width:0}.widget-node strong{display:block;font-size:12px;font-weight:600;line-height:1.5}.widget-node small{display:block;font-size:10px;line-height:1.5}.widget-node~.widget-node:before{content:'';position:absolute;left:-17px;top:calc(50% - 3px);width:6px;height:6px;border-top:1.5px solid #8ba595;border-right:1.5px solid #8ba595;transform:rotate(45deg)}
.widget-order{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;background:#edf3ef;color:#5e7e6b;font-size:10px;flex-shrink:0}.person-node{min-height:38px}.fallback-node,.route-destination{background:#eef5f1}.trigger-node{border:0;background:none;padding-left:0;color:#738578}.escalation-node{align-items:flex-start}.widget-end{margin:10px 0 0}.widget-end .resource-icon{width:14px;height:14px}.widget-condition-list{display:flex;flex-wrap:wrap;gap:5px;margin-top:3px}.widget-condition{padding:3px 6px;background:#f2f5f3;border-radius:4px;font-size:11px}.routing-conditions{flex:0 1 auto}.routing-widget{align-items:center}
@media(max-width:700px){.resource-table .resource-body{padding:10px}.widget-chain{gap:10px 20px}.widget-node{padding:7px 9px}.widget-node~.widget-node:before{left:-14px}.widget-meta{gap:6px 12px}.source-rule-link{font-size:10px}}
.modal.constructor-modal{width:min(1120px,calc(100vw - 40px));max-width:1120px}
.rule-constructor .rule-identity{grid-template-columns:minmax(0,1fr) auto!important;align-items:center;gap:16px}.rule-constructor .rule-identity label{margin:8px 0 16px}.rule-constructor .rule-identity .check{margin:8px 0 16px}
.constructor-layout{display:grid;grid-template-columns:260px minmax(0,1fr);gap:18px;align-items:start}.constructor-inputs{background:#f5f8f6;border:1px solid var(--line);border-radius:8px;padding:12px;min-width:0}.constructor-section+.constructor-section{border-top:1px solid var(--line);margin-top:14px;padding-top:12px}
.rule-constructor h3{font-size:13px;margin:0}.rule-constructor .section-line{margin:0 0 10px;gap:8px;flex-wrap:wrap}.constructor-loop{border:1px solid #dbe5de;border-left:3px solid #94b5a1;border-radius:6px;padding:10px;margin-top:10px;background:white}.constructor-loop-title{display:flex;align-items:center;justify-content:space-between;gap:8px}.constructor-loop label{margin:8px 0;gap:4px;font-size:11px}.constructor-loop input,.constructor-loop select{width:100%;min-width:0}.constructor-predicate{display:grid;gap:7px}.constructor-filter>.check{font-size:11px;line-height:1.5;margin:10px 0}.constructor-empty{font-size:12px;color:#7d8a85;padding:12px;border:1px dashed #d3dfd7;border-radius:6px;margin:8px 0}
.rule-constructor input:not([type=checkbox]),.rule-constructor select,.rule-constructor button{font-size:12px;padding:7px 9px;min-height:32px}.rule-constructor button.text{padding:3px 5px;min-height:24px}.rule-constructor .binding-button{background:white;color:inherit;font-size:11px;padding:7px 9px}.constructor-binding{font-family:Consolas,monospace}.constructor-outputs{min-width:0}.constructor-outputs>.recipe-section{margin:0;padding:0;border:0}.constructor-outputs .generation-block{padding:12px;margin:10px 0;border-radius:7px;background:#fff}.constructor-outputs .generation-block>.route-heading{padding-bottom:9px;border-bottom:1px solid var(--line);gap:6px;flex-wrap:wrap}.constructor-outputs .route-heading>.grow{font-size:12px;min-width:100px}.constructor-outputs .badge{font-size:10px}
.rule-constructor .binding-grid{gap:8px 14px;align-items:start}.rule-constructor .recipe-field{position:relative;min-width:0;padding-top:6px}.rule-constructor .recipe-field>label{font-size:11px;margin:0 0 5px;padding-right:54px;line-height:1.4}.rule-constructor .recipe-field input,.rule-constructor .recipe-field select{width:100%;min-width:0;margin-top:0}.rule-constructor .recipe-field .binding-tweak{position:absolute;top:2px;right:0;font-size:10px!important;min-height:20px;padding:2px 3px}.rule-constructor .recipe-field .inline-value{margin-top:5px}.rule-constructor .generation-step{gap:6px;padding:8px 0;border-bottom:1px solid var(--line);margin:0}.rule-constructor .generation-step .flow-arrow{padding:0}.rule-constructor .recipe-example{margin-top:14px;padding:12px;background:#f5f8f6;border-radius:7px}.rule-constructor .recipe-example h3{margin-bottom:7px}.rule-constructor .recipe-example td,.rule-constructor .recipe-example th{padding:7px;font-size:11px}
@media(max-width:800px){.constructor-layout{grid-template-columns:1fr}.constructor-inputs{display:grid;grid-template-columns:1fr 1fr;gap:12px}.constructor-section+.constructor-section{border:0;margin:0;padding:0}.modal.constructor-modal{width:calc(100vw - 20px)}.rule-constructor .binding-grid{grid-template-columns:1fr 1fr}}
@media(max-width:520px){.constructor-inputs,.rule-constructor .binding-grid{grid-template-columns:1fr}.rule-constructor .rule-identity{gap:8px}.rule-constructor .name-composer{flex-wrap:wrap}.constructor-layout{gap:12px}}
.compact-preview{margin:12px 0 0!important;border:1px solid var(--line);border-radius:7px;background:#fff;padding:0!important}.compact-preview>summary{display:flex;align-items:center;flex-wrap:wrap;gap:8px 14px;padding:9px 12px!important;font-size:11px!important;cursor:pointer;min-height:36px!important}.preview-totals{display:flex;flex-wrap:wrap;gap:10px;color:#738478}.compact-preview-body{padding:8px 12px}.compact-change-list{max-height:180px;overflow:auto}.compact-change{display:flex;gap:10px;align-items:center;padding:6px 0;border-bottom:1px solid var(--line)}.compact-change>div{min-width:0}.compact-change strong,.compact-change small{display:block;overflow-wrap:anywhere;line-height:1.4}.compact-change strong{font-size:11px}.compact-change small{font-size:10px;margin-top:2px}.compact-change .badge{font-size:10px;min-width:58px;text-align:center}.compact-preview-body>div>small{font-size:10px}.compact-example{border:1px solid var(--line)!important;padding:0!important;margin-top:12px!important}.compact-example>summary{font-size:11px;font-weight:600;padding:9px 12px;cursor:pointer}.compact-example table{font-size:11px}.generation-review-bar>.compact-preview{margin:0!important;border:0;border-radius:0;background:transparent;min-width:0}.generation-review-bar .review-actions{padding:10px 12px;gap:8px;flex-wrap:wrap;align-items:center}
.binding-popover{position:fixed;inset:auto;margin:0!important;padding:12px!important;border:1px solid #bacec1;border-radius:9px;background:#fff;box-shadow:0 12px 36px #1b342833;overflow:auto;overscroll-behavior:contain;z-index:1000;color:var(--ink);font-size:12px}.binding-popover::backdrop{background:transparent}.binding-popover>.section-line{position:sticky;top:-12px;background:white;padding:8px 0;z-index:2;margin:-4px 0 8px;border-bottom:1px solid var(--line)}.binding-popover h3{font-size:13px;margin:0}.binding-popover label{font-size:11px;margin:9px 0;gap:4px}.binding-popover input,.binding-popover select,.binding-popover textarea{font-size:12px;padding:7px 8px;min-width:0;width:100%}.binding-popover button{font-size:11px;padding:5px 8px}.binding-popover .two-columns{gap:10px;grid-template-columns:1fr 1fr}.binding-popover .binding-source{padding:0}.binding-popover .binding-key{margin:10px 0 0 8px;padding-left:10px}.binding-popover .binding-template{font-size:11px;margin-bottom:8px;max-width:100%;overflow-wrap:anywhere}.binding-popover .info-note{font-size:10px;padding:7px;margin:7px 0}.binding-popover .binding-value-preview{margin-top:12px;padding:8px;border-radius:5px}.binding-popover .binding-value-preview textarea{min-height:65px;max-height:130px;font-size:11px}.binding-popover .binding-value-preview .section-line{margin:0 0 5px}.binding-popover .binding-value-preview strong{font-size:11px}.binding-popover .binding-value-preview small{font-size:10px}
.rule-constructor .recipe-field{padding-right:34px}.rule-constructor .recipe-field>label{padding-right:0}.rule-constructor .recipe-field .binding-tweak{top:26px;right:0;font-size:20px!important;border:1px solid var(--line);width:29px;height:32px;line-height:1;background:white}.inline-binding{display:flex;gap:5px;align-items:center;min-width:0}.inline-delay{max-width:210px}.inline-delay input{width:65px!important;min-width:0}.binding-popup-trigger{font-size:20px!important;padding:0 7px!important;line-height:1}.inline-delay .binding-button{max-width:145px}.inline-delay>span{font-size:11px}
.compact-preview .compact-change-list{max-height:320px;overscroll-behavior:contain}.compact-preview .preview-status{display:inline-block;padding:3px 7px;border-radius:4px;font-size:10px;line-height:1.4;font-weight:500}.compact-preview .status-create{background:#e5f3eb;color:#246346}.compact-preview .status-update{background:#e8effb;color:#365e98}.compact-preview .status-remove{background:#fbe9e5;color:#a24435}.compact-preview .status-unchanged{background:#edf0f2;color:#626f79}.preview-list-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding-top:9px}.compact-preview .preview-list-toggle{font-size:11px!important;padding:4px 7px!important;min-height:0!important;line-height:1.4;border-radius:5px;font-weight:500}.compact-preview .preview-list-toggle:hover{background:#edf3ef}.compact-preview .preview-totals{gap:6px}@media(max-width:640px){.compact-preview .compact-change-list{max-height:280px}}
.rule-result{border:1px solid var(--line)!important;border-radius:7px;padding:0!important;margin-top:12px!important}.rule-result>summary{padding:10px 12px;cursor:pointer;font-size:12px}.rule-result>summary small{margin-left:12px;font-weight:400;color:var(--muted)}.rule-result-list{max-height:260px;overflow:auto;overscroll-behavior:contain}.rule-result-row{display:flex;gap:9px;padding:9px 12px;border-top:1px solid var(--line);align-items:flex-start}.rule-result-row>div{min-width:0}.rule-result-row small{display:block;font-size:11px;margin-top:4px;overflow-wrap:anywhere}.rule-result-name{display:flex;align-items:center;gap:7px;flex-wrap:wrap;font-size:12px}.rule-result-row svg{width:17px;height:17px;flex:none}.rule-result>summary .danger{color:#a24435}
/* Scalar controls fit their values; references keep room for descriptive names. */
input[type=number]{max-width:110px;min-width:0}
input[type=time]{max-width:132px;min-width:0}
input[type=date]:not(.native-date),.date-field{max-width:176px;min-width:0}
.date-field{width:176px;justify-self:start}
.date-field>input:not(.native-date){width:100%;min-width:0}
.rule-constructor .recipe-field.compact-scalar-field{width:210px;max-width:100%;justify-self:start}
.rule-constructor .recipe-field.compact-number-field.compact-scalar-field{width:144px}
.rule-constructor .compact-scalar-field .inline-value{margin-top:0}
.rule-constructor .compact-time-field.compact-scalar-field{width:166px}
.rule-constructor .compact-scalar-field .binding-tweak{top:auto;bottom:0}
/* Keep each value picker in the same grid row as its control, below its label. */
.rule-constructor .recipe-field{display:grid;grid-template-columns:minmax(0,1fr) 29px;column-gap:5px;row-gap:5px;padding-right:0;align-items:start}
.rule-constructor .recipe-field>label{grid-column:1 / -1;margin:0;padding:0}
.rule-constructor .recipe-field>input,.rule-constructor .recipe-field>div{grid-column:1;grid-row:2;min-width:0}
.rule-constructor .recipe-field .binding-tweak{position:static;grid-column:2;grid-row:2;align-self:start;margin:0}
.rule-constructor .recipe-field .inline-value{margin-top:0}
.rule-constructor .recipe-field .check{margin:0;min-height:32px;justify-content:flex-start}
.rule-constructor .recipe-field input[type=checkbox]{width:16px;height:16px;min-height:16px;flex:0 0 16px;margin:0;padding:0}
.rule-constructor .recipe-field .date-field{width:100%}
.generation-review-bar{overflow:hidden}
.generation-review-bar>.compact-preview>summary{padding:12px 14px!important;justify-content:flex-start;gap:8px 12px}
.generation-review-bar .preview-totals{flex-wrap:wrap;gap:6px}
.generation-review-bar .preview-status{white-space:nowrap}
.generation-review-bar>.compact-preview>.compact-preview-body{padding:12px 14px}
@media(max-width:700px){.generation-review-bar .review-actions{border-top:1px solid var(--line);justify-content:flex-start;padding:10px 14px}}
/* Compact variable tree: values first, schema controls on demand. */
.compact-data-tree{max-height:52vh;overflow:auto;background:var(--surface,#fff)}
.compact-data-tree .data-row{grid-template-columns:minmax(130px,1fr) minmax(140px,2fr) auto;gap:8px;padding:6px 8px 6px calc(12px + var(--data-depth,0)*20px);border-left:0;background:transparent;min-height:43px}
.compact-data-tree.structure-mode .data-row{grid-template-columns:minmax(100px,1fr) 112px minmax(130px,2fr) auto}
.compact-data-tree .container-row{background:#f7f9f7}
.compact-data-tree .data-key{display:flex;align-items:center;gap:5px;min-width:0;color:var(--ink);font-family:Consolas,monospace}
.compact-data-tree .data-key>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.compact-data-tree .data-disclosure{border:0;background:none;padding:4px;width:23px;flex-shrink:0}
.compact-data-tree input,.compact-data-tree select{padding:6px 8px;min-height:32px;margin:0}
.compact-data-tree .data-value .actions{flex-wrap:nowrap;gap:6px;min-width:0}
.data-container-preview{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:1;font-size:11px;color:var(--muted)}
.compact-data-tree .data-actions{grid-column:auto;gap:2px}
.compact-data-tree .data-actions button{padding:4px;min-width:23px;font-size:11px}
.compact-data-tree .data-value .actions button{padding:4px 6px;white-space:nowrap;font-size:10px}
@media(max-width:700px){.compact-data-tree .data-row,.compact-data-tree.structure-mode .data-row{grid-template-columns:minmax(0,1fr) auto;padding-left:calc(12px + var(--data-depth,0)*12px);gap:5px}.compact-data-tree .data-key{grid-column:1}.compact-data-tree .data-actions{grid-column:2;grid-row:auto}.compact-data-tree .data-value{grid-column:1/-1;grid-row:auto}.compact-data-tree .data-type{grid-column:1/-1}.compact-data-tree .data-container-preview{max-width:180px}}
/* Depth lives in the stylesheet: CSP deliberately rejects inline styles. */
.compact-data-tree .data-depth-0{--data-depth:0}
.compact-data-tree .data-depth-1{--data-depth:1}
.compact-data-tree .data-depth-2{--data-depth:2}
.compact-data-tree .data-depth-3{--data-depth:3}
.compact-data-tree .data-depth-4{--data-depth:4}
.compact-data-tree .data-depth-5{--data-depth:5}
.compact-data-tree .data-depth-6{--data-depth:6}
.compact-data-tree .data-depth-7{--data-depth:7}
.compact-data-tree .data-depth-8{--data-depth:8}
.compact-data-tree .data-depth-9{--data-depth:9}
.compact-data-tree .data-row{position:relative;--tree-step:20px}
.compact-data-tree .data-row::before{content:"";position:absolute;inset:0 auto 0 12px;width:calc(var(--data-depth,0)*var(--tree-step));background:repeating-linear-gradient(to right,var(--line) 0 1px,transparent 1px var(--tree-step));pointer-events:none}
.compact-data-tree .data-row:not(.container-row) .data-key{padding-left:28px}
.compact-data-tree .data-key{min-width:0}
@media(max-width:700px){.compact-data-tree .data-row{--tree-step:12px}.compact-data-tree .data-row:not(.container-row) .data-key{padding-left:0}}
.generation-map{width:calc(100vw - 36px);max-width:1600px;height:calc(100vh - 36px);max-height:100vh;border:1px solid var(--line);border-radius:16px;padding:0;color:var(--ink);background:#fff;overflow:hidden}
.generation-map::backdrop{background:#172e24a6;backdrop-filter:blur(3px)}
.generation-map[open]{display:flex;flex-direction:column}.map-heading{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:20px 24px 12px}.map-heading h2{margin:0;font-size:22px}.map-heading p{font-size:12px;color:var(--muted);margin:8px 0 0}.map-toolbar{display:flex;align-items:center;gap:8px;padding:10px 24px;border-bottom:1px solid var(--line);flex-wrap:wrap}.map-toolbar button,.map-heading button{font-size:12px;padding:7px 12px}.map-status{font-size:11px;color:#33764f}.map-status-error{color:#a54432}.map-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;flex:1;min-height:0}.map-canvas{min-height:0;position:relative;background-color:#f7faf8;background-image:radial-gradient(#c8d7ce 1px,transparent 1px);background-size:20px 20px;overflow:hidden}.map-inspector{padding:18px;border-left:1px solid var(--line);overflow:auto;min-width:0}.map-inspector label{margin:0 0 14px}.map-inspector select{font-size:12px;padding:8px;width:100%}.map-inspector h3{font-size:16px;margin:14px 0 7px;overflow-wrap:anywhere}.map-inspector h4{font-size:12px;margin:18px 0 8px}.map-inspector p{font-size:12px;line-height:1.5}.map-inspector .actions{margin:12px 0;gap:6px;flex-wrap:wrap}.map-inspector button{padding:7px 10px;font-size:11px}.map-inputs,.map-resource{border:1px solid var(--line);border-radius:7px;margin:8px 0;overflow:hidden}.map-inspector summary{cursor:pointer;padding:10px;font-size:12px}.map-resource strong{display:block;margin:5px 0 0}.map-inspector pre{font:11px/1.6 Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere;background:#f5f8f6;margin:0;padding:10px;max-height:280px;overflow:auto}.map-legend{display:flex;gap:16px;align-items:center;flex-wrap:wrap;border-top:1px solid var(--line);padding:10px 24px;font-size:10px;color:var(--muted)}.map-legend span:last-child{margin-left:auto}.map-legend-variable::before,.map-legend-rule::before,.map-legend-iteration::before{content:"";display:inline-block;width:9px;height:9px;border-radius:3px;margin-right:5px;background:#d8c9aa}.map-legend-rule::before{background:#79a68a}.map-legend-iteration::before{background:#94add2}
@media(max-width:700px){.generation-map{width:calc(100vw - 12px);height:calc(100dvh - 12px)}.map-heading{padding:12px}.map-heading h2{font-size:18px}.map-toolbar{padding:8px 12px}.map-layout{grid-template-columns:1fr;grid-template-rows:minmax(180px,1fr) minmax(150px,40%)}.map-inspector{border-left:0;border-top:1px solid var(--line);padding:12px}.map-legend{padding:8px 12px;gap:8px}.map-legend span:last-child{display:none}}
.map-input-content{padding:0 12px 12px;max-height:420px;overflow:auto}.map-input-section+.map-input-section{border-top:1px solid var(--line);margin-top:14px}.map-input-section h4{color:var(--muted);font-size:11px;margin:12px 0}.map-input-field{margin:12px 0}.map-input-label{display:flex;gap:8px;justify-content:space-between;font-size:11px;margin-bottom:5px;color:var(--muted)}.map-input-label small{font-size:9px}.map-input-value+.map-input-value{border-top:1px dashed var(--line);margin-top:6px;padding-top:6px}.map-input-token{font-size:12px;overflow-wrap:anywhere;margin:3px 0}.map-input-token>small{display:block;font:10px Consolas,monospace;color:var(--muted);margin-bottom:3px}.map-input-token strong{font-weight:500}.map-reference{border:1px solid #dce6df;background:#f5f8f6;border-radius:6px;padding:7px 9px;display:flex;gap:5px;flex-wrap:wrap;align-items:center}.map-reference>small{flex-basis:100%}.map-reference-kind{font-size:10px;color:#54836a}.map-reference strong{font-weight:600}
.map-source-card{border:1px solid var(--line);border-radius:7px;padding:10px;margin:8px 0;background:#f8faf9}.map-source-title{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:12px}.map-source-title>small{margin-left:auto;color:var(--muted)}.map-source-addresses{display:flex;flex-direction:column;gap:4px;margin-top:7px}.map-source-addresses code{font:10px/1.5 Consolas,monospace;overflow-wrap:anywhere;color:#567565}.map-source-card>button{margin-top:6px;padding:3px 0}
.map-resource-fields{padding:0 12px 12px;border-top:1px solid var(--line)}.map-field-hint{display:block;font-size:10px;line-height:1.5;color:var(--muted);margin-bottom:6px}.map-resource-fields .map-reference>small{flex-basis:auto;margin:0;min-width:15px}.map-resource-fields .map-input-field:last-child{margin-bottom:0}
+335
View File
@@ -0,0 +1,335 @@
const copy = value => JSON.parse(JSON.stringify(value));
const countryCodes = 'AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW'.split(' ');
const timeValue = value => value?.length === 5 ? value + ':00' : value;
const browserZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
const displayDate = value => /^\d{4}-\d{2}-\d{2}$/.test(value || '') ? value.split('-').reverse().join('-') : '';
const parseDisplayDate = value => {
if(!/^\d{2}-\d{2}-\d{4}$/.test(value))return '';
const iso=value.split('-').reverse().join('-'),date=new Date(iso+'T12:00:00Z');
return !isNaN(date) && date.toISOString().slice(0,10)===iso ? iso : '';
};
document.addEventListener('alpine:init', () => {
Alpine.data('dateField',()=>({
value:'',text:'',init(){this.$watch('value',v=>{if(v)this.text=displayDate(v);});},
edit(input){this.text=input.value;this.value=parseDisplayDate(this.text);input.setCustomValidity(this.value?'':tr('enter_a_valid_date_as_dd_mm_yyyy_f6df03'));},
picked(){this.text=displayDate(this.value);this.$refs.textDate.setCustomValidity('');}
}));
Alpine.data('meguri', () => ({
...generationUi(),
user: null, csrf: '', ready: false, busy: false, error: '', toast: '', allowSignup: false, demo: false, demoLifecycle:null,
loginForm: { username: '', password: '', display_name: '' }, registering: false,
directoryState: { workspaces: [] }, selected: '', detail: null, calendar: { days: [], assignments: [] },
view: 'calendar', scheduleId: '', startDay: '', modal: '', form: {}, menu: false,
navigationRestoring:0, navigationEpoch:0, setupOpen:false, scheduleSearch:'', schedulePicker:false, coverQuery:'', requestResult:null, requestError:'', requestLoading:false, requestSequence:0,
t:tr, l:I18n.diagnostic,
get language(){return I18n.locale();},
label(value){return MEGURI_MESSAGES['enum_'+value]?tr('enum_'+value):value;},
get countries(){const names=new Intl.DisplayNames([this.language],{type:'region'});return countryCodes.map(code=>({code,name:names.of(code)})).sort((a,b)=>a.name.localeCompare(b.name,this.language));},
browserZone, zones: [...new Set(['UTC',browserZone,...Intl.supportedValuesOf('timeZone')])].sort(),
draftPeople: [], picker: null, personQuery: '', pickerCountry: 'CY', pickerName: '', pickerEmail: '', pickerEditing: null,
csvText: '', csvResult: null, csvLoading: false, csvError: '', csvSequence: 0,
removal: null, draggedRoute: '', dragRevision: null, dragWorkspace: '', routeDropTarget: '', routeDropSide: 'before',
absenceResult:null, absenceError:'', absenceLoading:false, absenceSequence:0, absenceRevision:0, showAllAbsence:false,
formatDate:displayDate,
ttlText(seconds){return seconds>=3600?tr('duration_hours',{count:seconds/3600}):tr('duration_minutes',{count:seconds/60});},
get orderedRoutes() { return [...(this.doc?.routes || [])].sort((a,b)=>a.order-b.order); },
get canRemoveSchedule() { const p=this.removal?.plan; return !!p && !p.active_replacements.length && (!!this.form.redirect_to || (!p.coverage_fallbacks.length && !p.policies_emptied.length)); },
get displayZone() { return this.user?.timezone || browserZone; },
get directory() { return this.detail?.directory || []; },
get people() {
const people = new Map(this.directory.map(u=>[u.id,{id:u.id,user_id:u.id,display_name:u.display_name,email:null,country:u.country || 'CY',username:u.username}]));
for(const p of [...(this.doc?.people || []),...this.draftPeople]) people.set(p.id,{...people.get(p.id),...p});
return [...people.values()];
},
get pickerResults() { const q=this.personQuery.trim().toLowerCase(); return this.people.filter(p=>[p.display_name,p.email,p.username].some(s=>s?.toLowerCase().includes(q))); },
get selectedSchedule() { return this.doc?.schedules.find(s=>s.id===this.scheduleId); },
get holidayGroups() { return [...new Set((this.doc?.holidays || []).map(h=>h.country))].sort((a,b)=>this.countryName(a).localeCompare(this.countryName(b))).map(country=>({country,holidays:this.doc.holidays.filter(h=>h.country===country).sort((a,b)=>a.date.localeCompare(b.date))})); },
countryName(code) { return code==='*'?tr('all_countries_legacy_04451d'):new Intl.DisplayNames([this.language],{type:'region'}).of(code || 'CY'); },
holidaysOn(day) { return (this.doc?.holidays || []).filter(h=>h.date===day.date).map(h=>h.country+' · '+h.name).join(' / '); },
workingSummary() { const w=this.doc?.working_interval; return w ? w.weekdays.map(d=>[tr('mon_24b2a0'),tr('tue_529541'),tr('wed_23408b'),tr('thu_3593cc'),tr('fri_bbd6e3'),tr('sat_6b782d'),tr('sun_48c98c')][d-1]).join(', ')+' · '+w.start.slice(0,5)+'–'+w.end.slice(0,5)+' · '+w.timezone : ''; },
conditionText(c) { return (c.negate?tr('not_975146'):'')+this.label(c.field)+' '+this.label(c.operation)+' “'+c.value+'”'; },
scheduleTime(id) { return this.doc?.schedules.find(s=>s.id===id)?.handover?.slice(0,5) || '11:00'; },
get nav(){return [{ id: 'calendar', name: tr('calendar_adab50'), icon: '▦' }, { id: 'requests', name: tr('requests_f7194e'), icon: '⇄' }, { id: 'configuration', name: tr('schedule_setup'), icon: '⊞' }, { id: 'activity', name: tr('activity_81c0d9'), icon: '≡' }, { id: 'settings', name: tr('team_settings_06b774'), icon: '⚙' }];},
get grantOptions(){return [{ id: 'view', name: tr('view_calendar_b702b1') }, { id: 'request_overrides', name: tr('request_replacements_386c5e') }, { id: 'edit_schedules', name: tr('edit_schedules_policies_a02744') }, { id: 'force_overrides', name: tr('apply_without_confirmation_f0350d') }, { id: 'manage_members', name: tr('manage_team_access_db8b53') }, { id: 'manage_integration', name: tr('manage_provider_804901') }, { id: 'view_audit', name: tr('view_audit_history_896487') }];},
get workspace() { return this.detail?.workspace; },
get doc() { return this.workspace?.document; },
get members() { return this.detail?.members || []; },
get pending() { return (this.doc?.requests || []).filter(r => r.status === 'pending'); },
get coverageItems() {
return (this.detail?.coverage?.issues||[]).flatMap(gap=>{
const pending=this.pending.flatMap(request=>request.legs.filter(leg=>leg.schedule_id===gap.schedule_id && leg.from_user===gap.user_id && Date.parse(leg.start)<Date.parse(gap.end) && Date.parse(gap.start)<Date.parse(leg.end)).map(leg=>({request,leg})));
const bounds=[...new Set([Date.parse(gap.start),Date.parse(gap.end),...pending.flatMap(({leg})=>[Math.max(Date.parse(leg.start),Date.parse(gap.start)),Math.min(Date.parse(leg.end),Date.parse(gap.end))])])].sort((a,b)=>a-b);
return bounds.slice(0,-1).map((start,i)=>{
const end=bounds[i+1],requests=pending.filter(({leg})=>Date.parse(leg.start)<=start && end<=Date.parse(leg.end)).map(({request})=>request);
const approvers=[...new Set(requests.flatMap(r=>r.required_approvers.filter(id=>!r.approvals.includes(id))))];
return {...gap,start:new Date(start).toISOString(),end:new Date(end).toISOString(),awaiting:requests.length>0,approvers,substitutes:[...new Set(pending.filter(({leg})=>Date.parse(leg.start)<=start && end<=Date.parse(leg.end)).map(({leg})=>leg.to_user))]};
});
});
},
get needsMe() { return this.pending.filter(r=>r.required_approvers.includes(this.user?.id) && !r.approvals.includes(this.user?.id)); },
get myRequests() { return this.pending.filter(r=>r.requested_by===this.user?.id); },
get setupView() { return ['configuration','settings'].includes(this.view); },
get coverPeople() { const q=this.coverQuery.toLowerCase(); return this.people.filter(p=>p.id!==this.form.from_user && (this.members.some(m=>m.user.id===p.id) || this.form.force_mode) && [p.display_name,p.email,p.username].some(v=>v?.toLowerCase().includes(q))); },
get nextDuty() { const now=Date.now(); return this.calendar.assignments.filter(a=>a.schedule_id===this.scheduleId && a.user_id===this.user?.id && !a.unavailable && Date.parse(a.end)>now).sort((a,b)=>a.start.localeCompare(b.start))[0]; },
get matchingRequest() { return this.needsMe.find(r=>!r.absence_id && r.legs.length===1 && r.legs[0].schedule_id===this.form.schedule_id && r.legs[0].from_user===this.form.from_user && r.legs[0].to_user===this.form.to_user && Date.parse(r.legs[0].start)===Date.parse(this.form.start) && Date.parse(r.legs[0].end)===Date.parse(this.form.end)); },
get inbox() { return [...(this.doc?.requests || [])].reverse(); },
get title() { return this.nav.find(n => n.id === this.view)?.name || tr('calendar_adab50'); },
get monthTitle() { return new Date((this.startDay || this.today()) + 'T12:00:00Z').toLocaleDateString(this.language, { month: 'long', year: 'numeric', timeZone: 'UTC' }); },
get nowAssignment() { const now = new Date(); return this.calendar.assignments.find(a => !a.unavailable && a.schedule_id === this.scheduleId && new Date(a.start) <= now && now < new Date(a.end)); },
can(permission) { return (this.detail?.permissions || []).includes(permission); },
person(id) { return this.people.find(p=>p.id===id)?.display_name || this.members.find(m=>m.user.id===id)?.user.display_name || tr('unknown_employee_f68692'); },
shortName(id) { return this.person(id).split(' ')[0]; },
initials(id) { return this.person(id).split(' ').map(s => s[0]).slice(0, 2).join(''); },
color(id) { return ['teal', 'purple', 'amber', 'blue', 'rose'][Math.max(0, this.people.findIndex(p => p.id === id)) % 5]; },
scheduleName(id) { return this.doc?.schedules.find(s => s.id === id)?.name || tr('archived_schedule_40f4da'); },
today() { return this.dayKey(new Date(), this.doc?.timezone || 'UTC'); },
dayKey(date, zone = 'UTC') { const p = Object.fromEntries(new Intl.DateTimeFormat('en', { timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(date).map(p => [p.type, p.value])); return `${p.year}-${p.month}-${p.day}`; },
addDays(day, count) { const d = new Date(day + 'T12:00:00Z'); d.setUTCDate(d.getUTCDate() + count); return this.dayKey(d); },
monday() { const day = this.today(); const d = new Date(day + 'T12:00:00Z').getUTCDay(); return this.addDays(day, -((d + 6) % 7)); },
dayNumber(day) { return new Intl.DateTimeFormat('en',{timeZone:this.displayZone,day:'numeric'}).format(new Date(day.start)); },
weekday(day) { return new Intl.DateTimeFormat(this.language,{timeZone:this.displayZone,weekday:'short'}).format(new Date(day.start)).toUpperCase(); },
localClock(value) { return new Date(value).toLocaleTimeString('en-GB',{timeZone:this.displayZone,hour:'2-digit',minute:'2-digit',timeZoneName:'shortOffset'}); },
dayInterval(day) { return this.formatTime(day.start)+' → '+this.formatTime(day.end); },
assignmentInterval(a,day) { return this.formatTime(new Date(Math.max(Date.parse(a.start),Date.parse(day.start))))+' → '+this.formatTime(new Date(Math.min(Date.parse(a.end),Date.parse(day.end)))); },
onDay(day) { return this.calendar.assignments.filter(a => a.schedule_id === this.scheduleId && Date.parse(a.start) < Date.parse(day.end) && Date.parse(day.start) < Date.parse(a.end)); },
formatTime(value) { const p=Object.fromEntries(new Intl.DateTimeFormat('en-GB',{timeZone:this.displayZone,year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit',timeZoneName:'shortOffset'}).formatToParts(new Date(value)).map(p=>[p.type,p.value]));return `${p.day}-${p.month}-${p.year} ${p.hour}:${p.minute}:${p.second} ${p.timeZoneName}`; },
endpoint(suffix = '') { return `/api/workspaces/${this.workspace.id}${suffix}`; },
async api(path, method = 'GET', body) {
const response = await fetch(path, { method, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.csrf }, credentials: 'same-origin', ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
const data = await response.json();
if (!response.ok) { if (response.status === 401) this.user = null; throw new Error(data.error || tr('the_request_failed_02d674')); }
return data;
},
async run(task, message = '') { if (this.busy) return; this.busy = true; this.error = ''; try { await task(); if (message) { this.toast = message; setTimeout(() => this.toast = '', 4500); } } catch (e) { this.error = e.message; } finally { this.busy = false; } },
readNavigation(){const query=new URLSearchParams(location.search);const raw=query.get('view');const view=['schedules','policies','generation'].includes(raw)?'configuration':raw;return {view:this.nav.some(n=>n.id===view)?view:'calendar',workspace:query.get('workspace')||''};},
menuHref(view,workspace=this.selected){const url=new URL(location.href);url.searchParams.set('view',view);if(workspace)url.searchParams.set('workspace',workspace);else url.searchParams.delete('workspace');return url.pathname+url.search+url.hash;},
writeNavigation(replace=false){if(!this.ready || this.navigationRestoring)return;const href=this.menuHref(this.view);if(href!==location.pathname+location.search+location.hash)history[replace?'replaceState':'pushState']({},'',href);},
menuClick(event,view){if(event.button!==0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)return;event.preventDefault();this.view=view;},
async changeWorkspace(){this.modal='';this.scheduleId='';this.startDay='';this.writeNavigation();await this.run(()=>this.loadWorkspace());},
async restoreNavigation(){const epoch=++this.navigationEpoch;this.navigationRestoring++;try{const route=this.readNavigation();this.modal='';this.error='';this.view=route.view;if(this.user){const selected=this.directoryState.workspaces.some(t=>t.id===route.workspace)?route.workspace:this.directoryState.workspaces[0]?.id||'';if(selected!==this.selected){this.selected=selected;this.scheduleId='';this.startDay='';await this.loadWorkspace();}}else this.selected=route.workspace;await this.$nextTick();}catch(e){if(epoch===this.navigationEpoch)this.error=e.message;}finally{this.navigationRestoring--;if(!this.navigationRestoring)this.writeNavigation(true);}},
async init() {
this.$watch('user',user=>{Alpine.store('locale').language=user?.language || I18n.browser;});
this.$watch('form',()=>{if(this.modal==='absence')this.absenceChanged();if(this.modal==='request')this.requestChanged();if(['variable','generation-rule'].includes(this.modal))this.generationChanged();});
this.$watch('draftPeople',()=>{if(this.modal==='absence')this.absenceChanged();});
this.$watch('generationDraft',()=>{if(this.generationDraft && this.can('edit_schedules'))this.generationChanged();});
this.$watch('modal',(value,old)=>{if(['variable','generation-rule'].includes(old) && !value){this.bindingEditor=null;this.rulePeopleTarget=null;this.generationChanged();}});
this.$watch('view',v=>{if(v==='configuration')this.loadGenerationDraft();if(this.setupView)this.setupOpen=true;this.writeNavigation();});
const initial=this.readNavigation();this.selected=initial.workspace;this.view=initial.view;window.addEventListener('popstate',()=>this.restoreNavigation());
await this.run(async () => { const session = await this.api('/api/session'); this.user = session.user; this.csrf = session.csrf || ''; this.allowSignup = session.allow_signup; this.demo = session.demo;this.demoLifecycle=session.demo_lifecycle; if (this.user) await this.loadWorkspaces(); });
if(this.setupView)this.setupOpen=true;this.ready = true;this.writeNavigation(true);this.$nextTick(()=>{const update=()=>document.documentElement.style.setProperty('--demo-offset',(this.$refs.demoBanner?.getBoundingClientRect().height||0)+'px');new ResizeObserver(update).observe(this.$refs.demoBanner);update();});if(this.view==='configuration')this.loadGenerationDraft();
setInterval(() => { if (this.user && this.workspace && this.view!=='configuration' && !this.modal && !this.busy && !this.draggedRoute) this.run(() => this.loadWorkspace()); }, 20000);
},
async signIn() { await this.run(async () => { const s = await this.api(this.registering ? '/api/register' : '/api/login', 'POST', this.loginForm); this.user = s.user; this.csrf = s.csrf; this.loginForm.password = ''; await this.loadWorkspaces(); }); },
async signOut() { await this.run(async () => { await this.api('/api/logout', 'POST', {}); this.generationSequence++;clearTimeout(this.generationTimer);this.generationDraft=null;this.generationResult=null;this.generationWorkspace='';this.view='calendar';this.modal='';this.form={};this.user = null; this.detail = null; this.menu = false; this.selected = '';this.directoryState={workspaces:[]};this.startDay=''; }); },
async loadWorkspaces() { this.directoryState = await this.api('/api/workspaces'); if (!this.directoryState.workspaces.some(t => t.id === this.selected)) this.selected = (this.demo && ['alice','bob','carol'].includes(this.user?.username) ? this.directoryState.workspaces.find(t=>t.name===(this.user.username==='carol'?'Customer Support':'Acme Engineering'))?.id : '') || this.directoryState.workspaces[0]?.id || ''; if (this.selected) await this.loadWorkspace(); else this.detail = null; this.writeNavigation(true); },
async loadWorkspace() { const selected = this.directoryState.workspaces.find(t => t.id === this.selected); if (!selected) return; const detail = await this.api(`/api/workspaces/${selected.id}`);if(this.selected!==selected.id)return;this.detail=detail; if (!this.doc.schedules.some(s => s.id === this.scheduleId)) this.scheduleId = this.doc.schedules[0]?.id || ''; if (!this.startDay) this.startDay = this.monday(); await this.loadCalendar(); if(this.view==='configuration')this.loadGenerationDraft(); },
async loadCalendar() { const workspace=this.workspace.id;const calendar=await this.api(this.endpoint(`/calendar?from=${this.startDay}&days=28&schedule=${this.scheduleId}`));if(this.selected===workspace)this.calendar=calendar; },
async navigateDays(n) { await this.run(async () => { this.startDay = n === 0 ? this.monday() : this.addDays(this.startDay, n); await this.loadCalendar(); }); },
trapDialog(event){const list=[...this.$refs.dialog.querySelectorAll('button,input,select,textarea,summary,[tabindex="0"]')].filter(e=>!e.disabled && e.offsetParent!==null && e.tabIndex>=0);const first=list[0],last=list.at(-1);if(event.shiftKey && document.activeElement===first){event.preventDefault();last?.focus();}else if(!event.shiftKey && document.activeElement===last){event.preventDefault();first?.focus();}},
open(type, data = {}) {
this.error = ''; this.toast=''; this.menu = false; this.modal = type; this.form = copy(data); this.draftPeople=[]; this.picker=null; this.csvSequence++;
if(type==='holiday-import') { this.csvText='';this.csvResult=null;this.csvError='';this.csvLoading=false; }
if(type==='working') this.form=copy(this.doc.working_interval);
if(type==='person') this.form=copy(data);
if (type === 'rotation' && !data.id) this.form = { id: crypto.randomUUID(), name: '', type: 'rotation', anchor: this.today(), shift_days: 7, handover:'11:00:00', participants: this.members.map(m => m.user.id) };
if (type === 'priority') this.form = { id: crypto.randomUUID(), name: '', type: 'priority', handover:'11:00:00', participants: this.members.slice(0,2).map(m=>m.user.id), fallback: this.doc.schedules.find(s => s.type === 'rotation')?.id, ...data };
if (type === 'absence') {this.absenceResult=null;this.absenceError='';this.absenceRevision=this.workspace.revision;this.showAllAbsence=false;this.form = { id: crypto.randomUUID(), user_id: this.user.id, start: this.today(), end: this.today(), ranges:[],force:false,reason:'',...data };this.absenceChanged();}
if (type === 'holiday') this.form = { date: this.today(), name: '', country: this.user.country || 'CY', ...data };
if (type === 'member') this.form = { username: '', permissions: ['view', 'request_overrides'], ...data };
if (type === 'workspace') this.form = { name: '' };
if (type === 'account') this.form = { language:this.user.language || '', display_name: this.user.display_name, country: this.user.country || 'CY', timezone:this.user.timezone || '', current_password: '', password: '' };
if (type === 'policy') this.form = { id: crypto.randomUUID(), name: '', steps: [{ delay_minutes: 0, condition: 'if-not-acked', target: `schedule:${this.doc.schedules[0]?.id}` }], repeat: { count: 0, wait_minutes: 0, reset_recipient_states: false, close_alert_after_all: false }, ...data };
if (type === 'policy' && data.steps) this.form.steps = data.steps.map(s => ({ ...s, target: s.recipient.id ? `${s.recipient.type}:${s.recipient.id}` : s.recipient.type }));
if (type === 'route') this.form = { id: crypto.randomUUID(), name: '', conditions: [], escalation_id: '', working: false, ...data, position:data.id?this.orderedRoutes.findIndex(r=>r.id===data.id)+1:this.doc.routes.length+1 };
if (type === 'route' && data.window) Object.assign(this.form, { working: true, ...data.window });
if (type === 'integration') this.form = { provider: 'mock', cloud_id: '', team_id: '', advanced_escalations: false, users: {}, ...copy(this.doc.integration) };
if (type === 'preferences') this.form = { name: this.doc.name, horizon_days: this.doc.horizon_days };
this.$nextTick(() => this.$refs.dialog?.querySelector('input,select,button')?.focus());
},
choosePerson(target, index=null) { this.picker={target,index}; this.personQuery=''; this.pickerEditing=null; this.pickerName=''; this.pickerEmail=''; this.pickerCountry='CY'; this.$nextTick(()=>this.$refs.personSearch?.focus()); },
pickPerson(person) {
const p=copy(person); delete p.username;
if(!this.doc.people?.some(x=>x.id===p.id) && !this.draftPeople.some(x=>x.id===p.id)) this.draftPeople.push(p);
if(this.picker.target==='participants') this.addParticipant(p.id); else if(this.picker.target==='absenceReplacement')this.form.ranges[this.picker.index].replacement=p.id;else this.form[this.picker.target]=p.id;
this.picker=null;
},
addEmailPerson() {
const email=this.personQuery.trim().toLowerCase();
if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {this.error=tr('enter_a_complete_email_address_5f5f50');return;}
const existing=this.people.find(p=>p.email?.toLowerCase()===email);
if(existing) {this.pickPerson(existing);return;}
this.pickPerson({id:crypto.randomUUID(),user_id:null,email,display_name:this.pickerName.trim() || email,country:this.pickerCountry});
},
editPerson(id) {
const p=this.people.find(p=>p.id===id); if(!p)return;
this.picker={target:'edit'};this.pickerEditing=p.id;this.personQuery='';this.pickerName=p.display_name;this.pickerEmail=p.email || '';this.pickerCountry=p.country || 'CY';
},
savePersonDetails() {
const p=copy(this.people.find(p=>p.id===this.pickerEditing));delete p.username;
Object.assign(p,{display_name:this.pickerName,email:this.pickerEmail.trim().toLowerCase() || null,country:this.pickerCountry});
this.draftPeople=this.draftPeople.filter(x=>x.id!==p.id);this.draftPeople.push(p);this.picker=null;
},
async previewHolidays() {
const sequence=++this.csvSequence;const csv=this.csvText; this.csvResult=null;this.csvError='';
if(!csv.trim()){this.csvLoading=false;return;}
this.csvLoading=true;
try { const result=await this.api(this.endpoint('/holiday-preview'),'POST',{csv}); if(sequence===this.csvSequence && this.modal==='holiday-import') this.csvResult={...result,csv}; }
catch(e){if(sequence===this.csvSequence)this.csvError=e.message;}
finally{if(sequence===this.csvSequence)this.csvLoading=false;}
},
csvChanged() { this.csvSequence++;this.csvResult=null;this.csvLoading=true;clearTimeout(this.csvTimer);this.csvTimer=setTimeout(()=>this.previewHolidays(),300); },
get canImport() { return !this.csvLoading && this.csvResult?.csv===this.csvText && !this.csvResult.preview.errors.length && this.csvResult.preview.additions.length>0; },
absencePayload() {
const people=new Map(this.draftPeople.map(p=>[p.id,copy(p)]));
for(const id of [this.form.user_id,...this.form.ranges.map(r=>r.replacement)]) {
if(id && !this.doc.people.some(p=>p.id===id) && !people.has(id)){const p=copy(this.people.find(p=>p.id===id));delete p.username;people.set(id,p);}
}
return {revision:this.absenceRevision,absence:{id:this.form.id,user_id:this.form.user_id,start:this.form.start,end:this.form.end},ranges:this.form.ranges.map(r=>({start:r.start,end:r.end,replacement:r.replacement,replace_projects:r.replace_projects,local:{schedule_id:r.schedule_id||null,start:r.start+'T'+timeValue(r.start_time),end:r.end+'T'+timeValue(r.end_time)}})),people:[...people.values()],force:this.form.force,reason:this.form.reason};
},
absenceChanged() {this.absenceSequence++;this.absenceResult=null;this.absenceError='';this.absenceLoading=true;clearTimeout(this.absenceTimer);this.absenceTimer=setTimeout(()=>this.previewAbsence(),300);},
async previewAbsence() {
const seq=++this.absenceSequence;
if(!this.form.start || !this.form.end || !this.form.user_id || this.form.ranges.some(r=>!r.start || !r.end || !r.start_time || !r.end_time || !r.replacement)){this.absenceLoading=false;this.absenceError=tr('complete_the_dates_and_choose_a_person_for_each_repl_6187c4');return;}
const payload=this.absencePayload();
try{const result=await this.api(this.endpoint('/absence-preview'),'POST',payload);if(seq===this.absenceSequence && this.modal==='absence')this.absenceResult={...result,key:JSON.stringify(payload)};}
catch(e){if(seq===this.absenceSequence)this.absenceError=e.message;}
finally{if(seq===this.absenceSequence)this.absenceLoading=false;}
},
get canSaveAbsence(){return this.modal==='absence' && !this.absenceLoading && this.absenceResult && !this.absenceResult.plan.errors.length && this.absenceResult.key===JSON.stringify(this.absencePayload());},
newAbsenceRange(values={}) { const clock=(this.doc?.working_interval?.start||'11:00:00');return {id:crypto.randomUUID(),start:this.form.start,end:this.addDays(this.form.end,1),start_time:clock,end_time:clock,schedule_id:'',replacement:'',replace_projects:false,...values}; },
coverAbsenceRow(row){
const clock=iso=>new Intl.DateTimeFormat('en-GB',{timeZone:this.doc.timezone,hour:'2-digit',minute:'2-digit',second:'2-digit',hourCycle:'h23'}).format(new Date(iso));
const values={schedule_id:row.schedule_id,start:this.dayKey(new Date(row.start),this.doc.timezone),end:this.dayKey(new Date(row.end),this.doc.timezone),start_time:clock(row.start),end_time:clock(row.end)};
let index=this.form.ranges.findIndex(r=>Object.entries(values).every(([k,v])=>r[k]===v));
if(index<0){this.form.ranges.push(this.newAbsenceRange(values));index=this.form.ranges.length-1;}
this.choosePerson('absenceReplacement',index);
},
addAbsenceRange(){const last=this.form.ranges.at(-1);this.form.ranges.push(this.newAbsenceRange(last?{start:last.end,start_time:last.end_time}:{}));},
clickDay(day) {
const assignments=this.onDay(day);
if(assignments.length===1){this.openDuty(day,assignments[0]);return;}
this.open('day',{day,assignments});
},
openDuty(day,assignment) {
const start=new Date(Math.max(Date.parse(day.start),Date.parse(assignment.start))).toISOString(),end=new Date(Math.min(Date.parse(day.end),Date.parse(assignment.end))).toISOString();
this.open('request',{revision:this.workspace.revision,schedule_id:assignment.schedule_id,from_user:assignment.user_id,to_user:assignment.user_id===this.user.id?'':this.user.id,start,end,start_day:this.dayKey(new Date(start),this.doc.timezone),end_day:this.dayKey(new Date(end),this.doc.timezone),start_time:this.workspaceClock(start),end_time:this.workspaceClock(end),return_start:this.addDays(day.date,7),return_end:this.addDays(day.date,8),return_start_time:this.doc?.working_interval?.start?.slice(0,5)||'11:00',return_end_time:this.doc?.working_interval?.start?.slice(0,5)||'11:00',reason:'',swap:false,custom_range:false,force_mode:false});
this.coverQuery='';this.requestResult=null;this.requestError='';this.requestChanged();
},
nextOwnDuty() {
const day=this.calendar.days.find(d=>Date.parse(d.end)>Date.now() && this.onDay(d).some(a=>a.user_id===this.user.id));
if(day){this.openDuty(day,this.onDay(day).find(a=>a.user_id===this.user.id));const time=this.doc?.working_interval?.start?.slice(0,5)||'11:00';Object.assign(this.form,{start_day:day.date,end_day:this.addDays(day.date,1),start_time:time,end_time:time,custom_range:true});}
else {this.view='calendar';this.toast=tr('choose_the_day_you_want_to_change_6ce301');setTimeout(()=>this.toast='',4500);}
},
workspaceClock(value){return new Intl.DateTimeFormat('en-GB',{timeZone:this.doc.timezone,hour:'2-digit',minute:'2-digit',hourCycle:'h23'}).format(new Date(value));},
coverRangeChanged(){this.form.custom_range=true;},
requestPayload() {
const f=this.form,p={revision:f.revision,reason:f.reason.trim(),force:!!f.force_mode,remaining:true};
if(f.custom_range || f.swap)Object.assign(p,{schedule_id:f.schedule_id,from_user:f.from_user,to_user:f.to_user,start_day:f.start_day,end_day:f.end_day,start_time:f.start_time,end_time:f.end_time,swap:f.swap,return_start:f.return_start,return_end:f.return_end,return_start_time:f.return_start_time,return_end_time:f.return_end_time});
else p.legs=[{schedule_id:f.schedule_id,from_user:f.from_user,to_user:f.to_user,start:f.start,end:f.end}];
return p;
},
requestChanged() {this.requestSequence++;this.requestResult=null;this.requestError='';clearTimeout(this.requestTimer);this.requestLoading=!!this.form.to_user;this.requestTimer=setTimeout(()=>this.previewRequest(),250);},
async previewRequest() {
const seq=++this.requestSequence;
if(this.modal!=='request' || !this.form.to_user || !this.form.start_day || !this.form.end_day){this.requestLoading=false;return;}
const payload=this.requestPayload();
try {const result=await this.api(this.endpoint('/request-preview'),'POST',payload);if(seq===this.requestSequence && this.modal==='request')this.requestResult={...result,key:JSON.stringify(payload)};}
catch(e){if(seq===this.requestSequence)this.requestError=e.message;}
finally{if(seq===this.requestSequence)this.requestLoading=false;}
},
get canSendRequest(){return this.modal==='request' && !!this.form.to_user && !this.requestLoading && this.requestResult?.key===JSON.stringify(this.requestPayload());},
async sendQuickRequest(){
if(this.matchingRequest && !this.form.force_mode && !this.form.custom_range && !this.form.swap){await this.decide(this.matchingRequest,'approve');if(!this.error){this.modal='';this.toast=tr('replacement_accepted_publication_is_queued_afea8f');}return;}
if(!this.canSendRequest)return;
const force=this.form.force_mode;
await this.run(async()=>{await this.api(this.endpoint('/requests'),'POST',this.requestPayload());await this.loadWorkspace();this.modal='';},force?tr('replacement_applied_publication_is_queued_89e283'):tr('request_sent_waiting_for_your_colleague_b70b64'));
},
requestTitle(r){const leg=r.legs[0];return leg.to_user===r.requested_by?this.shortName(r.requested_by)+tr('offers_to_cover_935d75')+this.shortName(leg.from_user):this.shortName(r.requested_by)+tr('needs_cover_9e1ff7');},
async locateCoverage(gap){this.view='calendar';this.scheduleId=gap.schedule_id;const local=this.dayKey(new Date(gap.start),this.doc.timezone);this.startDay=this.addDays(local,-1);await this.run(()=>this.loadCalendar());const day=this.calendar.days.find(d=>Date.parse(d.start)<=Date.parse(gap.start) && Date.parse(gap.start)<Date.parse(d.end));const assignment=this.calendar.assignments.find(a=>a.schedule_id===gap.schedule_id && a.user_id===gap.user_id && Date.parse(a.start)<=Date.parse(gap.start) && Date.parse(gap.start)<Date.parse(a.end));if(day && assignment)this.openDuty(day,assignment);},
async chooseSchedule(id){this.scheduleId=id;this.schedulePicker=false;this.scheduleSearch='';await this.run(()=>this.loadCalendar());},
moveParticipant(index, delta) { const other = index + delta; if (other < 0 || other >= this.form.participants.length) return; [this.form.participants[index], this.form.participants[other]] = [this.form.participants[other], this.form.participants[index]]; },
addParticipant(id) { if (id && !this.form.participants.includes(id)) this.form.participants.push(id); },
async save(force = false) {
if(this.bindingEditor || this.rulePeopleTarget){this.closeRuleEditor();return;}
if(this.modal==='request'){await this.sendQuickRequest();return;}
if(['variable','generation-rule'].includes(this.modal)){this.saveGenerationModal();return;}
await this.run(async () => {
const f = copy(this.form); const kind = this.modal; const revision = this.workspace?.revision;
if (kind === 'workspace') { const created=await this.api('/api/workspaces','POST',f);this.selected=created.id; }
else if (kind === 'account') { const r = await this.api('/api/account', 'PUT', f); this.user = r.user; }
else if(kind==='working') await this.api(this.endpoint('/working-interval'),'PUT',{revision,interval:{...f,weekdays:f.weekdays.map(Number),start:timeValue(f.start),end:timeValue(f.end)}});
else if(kind==='holiday-import') { if(!this.canImport)throw new Error(tr('preview_the_csv_and_fix_any_errors_before_importing_2ccc4c'));await this.api(this.endpoint('/holiday-import'),'POST',{revision:this.csvResult.revision,csv:this.csvText}); }
else if(kind==='remove-schedule') { if(!this.canRemoveSchedule)throw new Error(tr('resolve_the_listed_dependencies_before_removing_this_fa1445'));await this.api(this.endpoint('/schedules/'+f.id+'/remove'),'POST',{revision:this.removal.revision,redirect_to:f.redirect_to || null}); }
else if(kind==='absence') {if(!this.canSaveAbsence)throw new Error(tr('wait_for_a_valid_absence_preview_before_saving_c743bf'));await this.api(this.endpoint('/absences'),'POST',this.absencePayload());}
else if (kind === 'member') await this.api(this.endpoint('/members'), 'PUT', { revision, ...f });
else if (kind === 'integration') await this.api(this.endpoint('/integration'), 'PUT', { revision, integration: f.provider === 'mock' ? { provider: 'mock' } : {...f,users:Object.fromEntries(Object.entries(f.users).filter(([id,value])=>value?.trim()).map(([id,value])=>[id,value.trim()]))} });
else if (kind === 'force') await this.api(this.endpoint(`/requests/${f.id}`), 'POST', { revision, action: 'force', reason: f.reason });
else {
const doc = copy(this.doc);
const people=new Map((doc.people || []).map(p=>[p.id,p]));
for(const p of this.draftPeople)people.set(p.id,p);
if(kind==='person'){ const p=copy(f);delete p.username;people.set(p.id,p); }
// Snapshot selected workspace users so their scheduling country and external identity are explicit.
const selectedIds=['rotation','priority'].includes(kind)?f.participants:kind==='absence'?[f.user_id]:[];
for(const id of selectedIds){if(!people.has(id)){const p=copy(this.people.find(p=>p.id===id));delete p.username;people.set(id,p);}}
doc.people=[...people.values()];
const upsert = (list, entry) => { const i = list.findIndex(x => x.id === entry.id); i === -1 ? list.push(entry) : list.splice(i, 1, entry); };
if (['rotation','priority'].includes(kind)) { if(kind==='rotation')f.shift_days = Number(f.shift_days); f.handover=timeValue(f.handover || '11:00'); upsert(doc.schedules, f); }
if (kind === 'absence') upsert(doc.absences, f);
if (kind === 'holiday') { if(doc.holidays.some(h=>h.date===f.date && h.country===f.country))throw new Error(tr('this_country_already_has_a_holiday_on_that_date_7a457a'));doc.holidays.push(f); }
if (kind === 'preferences') Object.assign(doc, f, { horizon_days: Number(f.horizon_days) });
if (kind === 'policy') { f.steps = f.steps.map(s => { const [type, id] = s.target.split(':'); return { delay_minutes: Number(s.delay_minutes), condition: s.condition, recipient: id ? { type, id } : { type } }; }); f.repeat.count = Number(f.repeat.count); f.repeat.wait_minutes = Number(f.repeat.wait_minutes); upsert(doc.escalations, f); }
if (kind === 'route') {
const position=Number(f.position);delete f.position;f.escalation_id ||= null;f.window = f.working ? copy(this.doc.working_interval) : null;
doc.routes=this.orderedRoutes.filter(r=>r.id!==f.id).map(copy);doc.routes.splice(position-1,0,f);doc.routes.forEach((r,index)=>r.order=index);
}
await this.api(this.endpoint('/config'), 'PUT', { revision, document: doc });
}
await this.loadWorkspaces(); this.modal = ''; this.draftPeople=[];
}, force ? tr('replacement_applied_publication_is_queued_89e283') : tr('changes_saved_5ffbaf'));
},
async decide(request, action) { await this.run(async () => { await this.api(this.endpoint(`/requests/${request.id}`), 'POST', { revision: this.workspace.revision, action }); await this.loadWorkspace(); }, tr('request_updated_c52ad2')); },
async removeItem(collection, id) {
if(collection==='schedules') { await this.run(async()=>{const result=await this.api(this.endpoint('/schedules/'+id+'/removal'));this.open('remove-schedule',{id,redirect_to:''});this.removal=result;});return; }
await this.run(async () => { const doc = copy(this.doc); if (collection === 'holidays') doc.holidays=doc.holidays.filter(h=>h.country+':'+h.date!==id); else doc[collection] = doc[collection].filter(x => x.id !== id); if(collection==='routes')doc.routes.sort((a,b)=>a.order-b.order).forEach((r,index)=>r.order=index); await this.api(this.endpoint('/config'), 'PUT', { revision: this.workspace.revision, document: doc }); await this.loadWorkspace(); }, tr('configuration_updated_a81653'));
},
startRouteDrag(event,id) {
if(!this.can('edit_schedules') || this.busy){event.preventDefault();return;}
this.draggedRoute=id;this.dragRevision=this.workspace.revision;this.dragWorkspace=this.workspace.id;
event.dataTransfer.effectAllowed='move';event.dataTransfer.setData('text/plain',id);
},
overRoute(event,id) {
if(!this.draggedRoute || this.draggedRoute===id || this.dragWorkspace!==this.workspace.id)return;
event.dataTransfer.dropEffect='move';const box=event.currentTarget.getBoundingClientRect();
this.routeDropTarget=id;this.routeDropSide=event.clientY<box.top+box.height/2?'before':'after';
},
clearRouteDrag() { this.draggedRoute='';this.routeDropTarget='';this.dragRevision=null;this.dragWorkspace=''; },
async dropRoute(event,id) {
if(!this.draggedRoute || this.draggedRoute===id || this.dragWorkspace!==this.workspace.id){this.clearRouteDrag();return;}
const moving=this.draggedRoute,revision=this.dragRevision;
const box=event.currentTarget.getBoundingClientRect(),after=event.clientY>=box.top+box.height/2;
const ids=this.orderedRoutes.map(r=>r.id).filter(r=>r!==moving);const block=ids.filter(r=>this.generated(r));const target=this.generated(id)?(after?block.at(-1):block[0]):id;ids.splice(ids.indexOf(target)+(after?1:0),0,moving);
this.clearRouteDrag();await this.saveRouteOrder(ids,revision);
},
async moveRoute(id,delta) {
const ids=this.orderedRoutes.map(r=>r.id),from=ids.indexOf(id);let to=from+delta;
if(to<0 || to>=ids.length)return;if(this.generated(ids[to])){while(to+delta>=0 && to+delta<ids.length && this.generated(ids[to+delta]))to+=delta;}ids.splice(from,1);ids.splice(to,0,id);
await this.saveRouteOrder(ids,this.workspace.revision);
this.$nextTick(()=>this.$refs.routingList?.querySelector('[data-route-id="'+id+'"] .move-route')?.focus());
},
async saveRouteOrder(ids,revision) {
if(ids.join(',')===this.orderedRoutes.map(r=>r.id).join(','))return;
await this.run(async()=>{await this.api(this.endpoint('/routes/order'),'PUT',{revision,ids});await this.loadWorkspace();},tr('routing_order_saved_publication_is_queued_cfb972'));
},
async setDrift(value) { await this.run(async () => { await this.api(this.endpoint(), 'PATCH', {revision:this.workspace.revision, overwrite_drift: value }); await this.loadWorkspaces(); }, tr('drift_policy_updated_be40fd')); },
async retry() { await this.run(async () => { await this.api(this.endpoint('/sync'), 'POST', {}); await this.loadWorkspace(); }, tr('publication_queued_8355e1')); },
async preview() { await this.run(async () => { const plan = await this.api(this.endpoint('/preview')); const url = URL.createObjectURL(new Blob([JSON.stringify(plan, null, 2)], { type: 'application/json' })); const link = document.createElement('a'); link.href = url; link.download = 'meguri-publication.json'; link.click(); URL.revokeObjectURL(url); }, tr('publication_preview_downloaded_afd91a')); },
auditDetails(details){const localize=(value,key='')=>{if(Array.isArray(value))return value.map(item=>localize(item,key));if(value && typeof value==='object')return Object.fromEntries(Object.entries(value).map(([k,v])=>[k,localize(v,k)]));if(typeof value==='string' && ['start','end','created_at','applied_at','effective_at'].includes(key) && /^\d{4}-\d{2}-\d{2}T/.test(value) && Number.isFinite(Date.parse(value)))return this.formatTime(value);return value;};return JSON.stringify(localize(details),null,2);},
describeRecipient(recipient) { return recipient.type === 'schedule' ? this.scheduleName(recipient.id) : recipient.type === 'user' ? this.person(recipient.id) : recipient.type === 'team_all' ? tr('everyone_on_the_team_235b55') : tr('a_random_team_member_8f35f3'); },
secretPrefix() { return 'MEGURI_JSM_' + this.workspace.id.replaceAll('-', '').toUpperCase(); },
}));
});
+31
View File
@@ -0,0 +1,31 @@
<template x-if="bindingEditor"><section class="binding-editor binding-popover" popover="manual" role="region" :aria-label="t('value_binding_1bb7eb')" @click.outside="if(bindingEditor && !$event.target.closest('.binding-popup-trigger,.binding-tweak,.binding-button,select'))closeRuleEditor(false)" @resize.window="positionBindingPopover()" @keydown.escape.stop.prevent="closeRuleEditor()" @keydown.enter.stop="if($event.target.tagName!=='TEXTAREA'){$event.preventDefault();closeRuleEditor()}" x-effect="if(modal!=='generation-rule')closeRuleEditor(false);else if(bindingEditor){JSON.stringify(bindingEditor.binding);$nextTick(()=>positionBindingPopover())}" @scroll.window.capture="positionBindingPopover()">
<div class="section-line"><h3 x-text="bindingEditor.label"></h3><button type="button" @click="closeRuleEditor()"><span x-text="t('done_e9b450')"></span></button></div>
<code class="template-token binding-template" x-show="bindingEditor.binding?.kind!=='literal' && bindingEditor.binding?.kind!=='output'" x-text="bindingSummary(bindingEditor.binding)"></code>
<template x-for="row in bindingRows()" :key="row.depth"><div class="binding-source" :class="{'binding-key':row.depth}">
<template x-if="row.parent"><div class="section-line"><button type="button" :disabled="row.index===0" :aria-label="t('move_block_up_b67b5b')" @click="const items=row.parent.items;[items[row.index-1],items[row.index]]=[items[row.index],items[row.index-1]]" x-text="t('symbol_9555ff')"></button><button type="button" class="text danger" @click="row.parent.items.splice(row.index,1)" x-text="t('remove_e96390')"></button></div></template>
<label><span x-text="row.label"></span><select :aria-label="t('value_source_c0ae15')" :value="row.binding.kind" @change="changeBindingKind(row.binding,$event.target.value)"><option value="list" x-show="row.binding.kind==='list'" x-text="t('responsible_people_in_priority_order_7205a5')"></option><option value="literal" x-text="t('fixed_value_191429')"></option><option value="variable" x-text="t('workspace_variable_7abc57')"></option><option value="item" x-text="t('current_iteration_value_39357a')"></option><option value="key" x-text="t('current_iteration_key_347e79')"></option><option value="lookup" x-text="t('dictionary_lookup_f38672')"></option><option value="output" x-text="t('created_resource_c04e1e')"></option></select></label>
<template x-if="row.binding.kind==='list'"><button type="button" @click="row.binding.items.push(literal(''))" x-text="t('add_person_c126e2')"></button></template>
<template x-if="row.binding.kind==='literal'"><div>
<template x-if="row.type==='list' || row.type==='collection'"><label><span x-text="t('values_one_per_line_b8e245')"></span><textarea :aria-label="t('fixed_list_a199a2')" :value="Array.isArray(row.binding.value)?row.binding.value.join('\n'):''" @input="row.binding.value=$event.target.value.split(/[\n,;]+/).map(s=>s.trim()).filter(Boolean)" :placeholder="t('alice_example_com_bob_example_com_f2697e')"></textarea></label></template>
<template x-if="row.type==='text'"><label><span x-text="t('fixed_text_ab18dc')"></span><input :aria-label="t('fixed_text_ab18dc')" x-model="row.binding.value"></label></template>
<template x-if="row.type==='number'"><label><span x-text="t('fixed_number_017ee6')"></span><input :aria-label="t('fixed_number_017ee6')" type="number" :value="row.binding.value" @input="row.binding.value=$event.target.value===''?null:Number($event.target.value)"></label></template>
<template x-if="row.type==='boolean'"><label><span x-text="t('fixed_boolean_38876f')"></span><select :aria-label="t('fixed_boolean_38876f')" :value="String(row.binding.value)" @change="row.binding.value=$event.target.value==='true'"><option value="true" x-text="t('true_88b33e')"></option><option value="false" x-text="t('false_97cdbd')"></option></select></label></template>
<template x-if="row.type==='time'"><label><span x-text="t('fixed_time_40176a')"></span><input :aria-label="t('fixed_time_40176a')" type="time" x-model="row.binding.value"></label></template>
<template x-if="row.type==='date'"><label><span x-text="t('fixed_date_1c63b4')"></span><span class="date-field" x-data="dateField" x-modelable="value" x-model="row.binding.value"><input type="text" :aria-label="t('fixed_date_1c63b4')" x-ref="textDate" :value="text" @input="edit($event.target)" :placeholder="t('dd_mm_yyyy_d7e5fe')" maxlength="10"><button type="button" :aria-label="t('open_calendar_41547e')" @click="$refs.calendar.showPicker()"><span x-text="t('symbol_21887f')"></span></button><input class="native-date" type="date" x-ref="calendar" x-model="value" @input="picked()" tabindex="-1" aria-hidden="true"></span></label></template>
<template x-if="row.type==='person'"><div><label><span x-text="t('workspace_person_844f8f')"></span><select :aria-label="t('workspace_person_844f8f')" :value="people.some(p=>p.id===row.binding.value)?row.binding.value:''" @change="row.binding.value=$event.target.value"><option value="" x-text="t('use_email_address_c12ddf')"></option><template x-for="p in people" :key="p.id"><option :value="p.id" :selected="row.binding.value===p.id" x-text="p.display_name"></option></template></select></label><label x-show="!people.some(p=>p.id===row.binding.value)"><span x-text="t('email_84add5')"></span><input :aria-label="t('fixed_email_189696')" :type="people.some(p=>p.id===row.binding.value)?'text':'email'" x-model="row.binding.value" :placeholder="t('person_example_com_371ab9')"></label></div></template>
<p class="validation-warning" x-show="['schedule','policy'].includes(row.type) && row.binding.kind==='literal' && generated(row.binding.value)" x-text="t('generated_direct_reference_hint')"></p>
<template x-if="row.type==='schedule' || row.type==='policy'"><label><span x-text="t('existing_resource_1752f5')"></span><select :aria-label="t('existing_resource_1752f5')" x-model="row.binding.value"><option value="" x-text="t('choose_a_resource_3e0493')"></option><template x-for="resource in manualResources(row.type)" :key="resource.id"><option :value="resource.id" :selected="row.binding.value===resource.id" x-text="resource.name"></option></template></select></label></template>
</div></template>
<label x-show="['variable','lookup'].includes(row.binding.kind)"><span x-text="t('variable_19de69')"></span><select :aria-label="t('binding_variable_137329')" x-model="row.binding.variable"><option value="" x-text="t('choose_a_variable_e27462')"></option><template x-for="v in generationDraft.variables" :key="v.id"><option :value="v.id" :selected="row.binding.variable===v.id" x-text="bindingSummary({kind:'variable',variable:v.id})+' · '+label(v.node.kind)"></option></template></select></label>
<label x-show="['item','key'].includes(row.binding.kind)"><span x-text="t('iteration_18905a')"></span><select :aria-label="t('binding_iteration_f1778d')" x-model="row.binding.iteration"><option value="" x-text="t('choose_an_iteration_5d2162')"></option><template x-for="i in form.iterations" :key="i.id"><option :value="i.id" :selected="row.binding.iteration===i.id" x-text="bindingSummary({kind:row.binding.kind==='key'?'key':'item',iteration:i.id})"></option></template></select></label>
<p class="info-note" x-show="row.binding.kind==='output'" x-text="t('generated_reference_hint')"></p>
<label x-show="row.binding.kind==='output'"><span x-text="t('output_block_3853ab')"></span><select :aria-label="t('binding_output_block_e2b0ab')" x-model="row.binding.output"><option value="" x-text="t('choose_an_output_747632')"></option><template x-for="b in matchingOutputs(row.type)" :key="b.id"><option :value="b.id" :selected="row.binding.output===b.id" x-text="generatedResourceLabel(b)+' · '+bindingSummary(b.fields.name)"></option></template></select></label>
<label x-show="!['literal','output','key'].includes(row.binding.kind)"><span x-text="t('nested_field_list_position_optional_bac49a')"></span><input :aria-label="t('nested_field_or_position_eaeb07')" :value="(row.binding.path||[]).join(' / ')" @input="row.binding.path=$event.target.value.split('/').map(s=>s.trim()).filter(Boolean)" :placeholder="t('primary_1_4b34b8')"><small><span x-text="t('leave_empty_to_use_the_whole_value_list_positions_st_8da342')"></span></small></label>
<div class="two-columns" x-show="row.type==='text'"><label><span x-text="t('prefix_90eceb')"></span><input :aria-label="t('text_prefix_62b30b')" x-model="row.binding.prefix" :placeholder="t('optional_0c6c41')"></label><label><span x-text="t('suffix_885db9')"></span><input :aria-label="t('text_suffix_f191fa')" x-model="row.binding.suffix" :placeholder="t('optional_0c6c41')"></label></div>
</div></template>
<div class="binding-value-preview" x-data="{sample:{text:'',context:'',error:''}}" x-effect="sample=bindingValuePreview()">
<div class="section-line"><strong x-text="t('binding_preview_title')"></strong><small x-text="sample.context"></small></div>
<textarea readonly spellcheck="false" wrap="off" rows="3" :aria-label="t('binding_preview_title')" :value="sample.text" :placeholder="sample.error"></textarea>
<p class="validation-warning" x-show="sample.error" x-text="sample.error"></p>
</div>
</section></template>
+16
View File
@@ -0,0 +1,16 @@
<dialog class="generation-map" x-ref="generationMap" @cancel.prevent="closeGenerationMap()">
<header class="map-heading"><div><h2 x-text="t('map_title')"></h2><p x-text="t('map_hint')"></p></div><button type="button" @click="closeGenerationMap()" :aria-label="t('close_dialog_7b2902')" x-text="t('map_close')"></button></header>
<div class="map-toolbar"><span class="map-status" :class="{'map-status-error':mapSnapshot?.report.issues.length}" x-text="mapBusy?t('cover_checking'):mapSnapshot?.report.issues.length?t('diagnostics_count',{count:mapSnapshot.report.issues.length}):t('generation_checked')"></span><button @click="refreshGenerationMap()" :disabled="mapBusy" x-text="t('map_refresh')"></button><div class="grow"></div><button @click="mapZoom(.8)" :aria-label="t('map_zoom_out')" x-text="t('map_minus')"></button><button @click="mapZoom(1.25)" :aria-label="t('map_zoom_in')" x-text="t('map_plus')"></button><button @click="mapFit()" x-text="t('map_fit')"></button></div>
<p class="validation-error" x-show="mapError" x-text="mapError"></p>
<div class="map-layout"><div class="map-canvas" x-ref="generationCanvas" :aria-label="t('map_title')"></div><aside class="map-inspector">
<label><span x-text="t('map_selected')"></span><select :aria-label="t('map_selected')" :value="mapSelected" @change="mapSelect($event.target.value)"><template x-for="n in mapNodes" :key="n.id"><option :value="n.id" x-text="n.title"></option></template></select></label>
<template x-if="mapCurrent()"><div><h3 x-text="mapCurrent()?.title"></h3><p class="muted" x-text="mapCurrent()?.subtitle"></p><div class="actions"><button x-show="mapCurrent()?.iteration" @click="mapToggle()" x-text="mapExpanded[mapCurrent()?.rule]?t('map_collapse'):t('map_expand')"></button><button @click="mapEdit()" x-text="t('map_edit')"></button></div>
<label x-show="mapRows().length"><span x-text="t('map_pass')"></span><select :aria-label="t('map_pass')" x-model="mapRow"><option value="all" x-text="t('map_all')"></option><template x-for="(row,index) in mapRows()" :key="index"><option :value="String(index)" x-text="mapRowLabel(row,index)"></option></template></select></label>
<details class="map-inputs" open><summary x-text="t('map_inputs')"></summary><div class="map-input-content"><template x-if="mapRow==='all'"><div class="map-source-list"><p class="muted" x-show="!mapInputSources().length" x-text="t('map_no_sources')"></p><template x-for="source in mapInputSources()" :key="source.id"><div class="map-source-card"><div class="map-source-title"><span class="badge" x-text="(source.generated?t('generated_symbol')+' ':'')+source.type"></span><strong x-text="source.name"></strong><small x-show="source.count>1" x-text="source.count"></small></div><div class="map-source-addresses"><template x-for="address in source.addresses" :key="address"><code x-text="address"></code></template></div><button class="text" x-show="source.target && source.target!==mapSelected" @click="mapSelect(source.target)" x-text="t('map_view_source')"></button></div></template><p class="muted" x-show="mapRows().length" x-text="t('map_choose_pass_values')"></p></div></template><div x-show="mapRow!=='all'"><p class="muted" x-show="!mapInputSections().length" x-text="t('map_no_inputs')"></p><template x-for="(section,sectionIndex) in mapInputSections()" :key="sectionIndex"><section class="map-input-section"><h4 x-text="section.title"></h4><template x-for="(input,inputIndex) in section.rows" :key="inputIndex"><div class="map-input-field"><div class="map-input-label"><span x-text="input.label"></span><small x-show="input.values.length>1" x-text="t('map_distinct_values',{count:input.values.length})"></small></div><template x-for="(value,valueIndex) in input.values" :key="valueIndex"><div class="map-input-value"><template x-for="(token,tokenIndex) in mapInputTokens(value)" :key="tokenIndex"><div class="map-input-token" :class="{'map-reference':token.kind}"><small x-show="token.path" x-text="token.path"></small><span class="map-reference-kind" x-show="token.kind" x-text="(token.generated?t('generated_symbol')+' ':'')+(token.kind||'')"></span><strong x-text="token.text"></strong></div></template></div></template></div></template></section></template></div></div></details>
<div class="map-issues"><template x-for="(issue,index) in mapIssues()" :key="index"><p class="validation-error" x-text="l(issue.message)"></p></template></div>
<h4 x-text="t('map_resources',{count:mapResources().length})"></h4><p class="muted" x-show="!mapResources().length" x-text="mapSnapshot?.report.issues.length?t('map_unverified'):t('map_empty')"></p>
<template x-for="r in mapResources()" :key="r.id"><details class="map-resource"><summary><span class="badge" x-text="label(r.kind)"></span><strong x-text="r.name"></strong></summary><div class="map-resource-fields"><template x-for="(field,fieldIndex) in mapResourceFields(r)" :key="fieldIndex"><div class="map-input-field"><div class="map-input-label" x-text="field.label"></div><small class="map-field-hint" x-show="field.hint" x-text="field.hint"></small><template x-for="(token,tokenIndex) in mapInputTokens(field.value)" :key="tokenIndex"><div class="map-input-token" :class="{'map-reference':token.kind}"><small x-show="token.path" x-text="token.path"></small><span class="map-reference-kind" x-show="token.kind" x-text="(token.generated?t('generated_symbol')+' ':'')+(token.kind||'')"></span><strong x-text="token.text"></strong></div></template></div></template></div></details></template>
</div></template></aside></div>
<footer class="map-legend"><span class="map-legend-variable" x-text="t('map_variables')"></span><span class="map-legend-rule" x-text="t('map_rules')"></span><span class="map-legend-iteration" x-text="t('map_iterations')"></span><span x-text="t('map_readonly')"></span></footer>
</dialog>
+95
View File
@@ -0,0 +1,95 @@
// Canvas graph is kept outside Alpine's reactive proxy. It never mutates scheduling state.
function generationMapUi(){let graph=null,origin=null,request=0;return {
mapOpen:false,mapBusy:false,mapError:'',mapSnapshot:null,mapNodes:[],mapSelected:'',mapExpanded:{},mapRow:'all',
async openGenerationMap(){origin=document.activeElement;this.mapOpen=true;this.mapBusy=true;this.mapSnapshot=null;this.mapNodes=[];this.mapSelected='';this.mapExpanded={};this.mapRow='all';await this.$nextTick();this.$refs.generationMap.showModal();await this.refreshGenerationMap();},
closeGenerationMap(){request++;graph?.destroy();graph=null;this.$refs.generationMap.close();this.mapOpen=false;origin?.focus();},
async refreshGenerationMap(){const sequence=++request;this.mapBusy=true;this.mapError='';try{const payload=this.generationPayload(),workspace=this.workspace.id;const report=await this.api(this.endpoint('/generation-preview'),'POST',payload);if(sequence!==request || !this.mapOpen || workspace!==this.workspace.id)return;this.mapSnapshot={payload,report};await this.$nextTick();this.drawGenerationMap();}catch(e){if(sequence===request)this.mapError=e.message;}finally{if(sequence===request)this.mapBusy=false;}},
mapSelect(id){this.mapSelected=id;this.mapRow='all';if(graph){graph.elements().removeClass('selected related');const n=graph.getElementById(id);n.addClass('selected');n.closedNeighborhood().addClass('related');}},
mapCurrent(){return this.mapNodes.find(n=>n.id===this.mapSelected);},
mapTrace(rule){return this.mapSnapshot?.report.trace?.find(t=>t.rule===rule);},
mapResources(){const n=this.mapCurrent(),report=this.mapSnapshot?.report;if(this.mapBusy || this.mapError || !n || n.variable || !report?.result)return [];const row=this.mapRow==='all'?null:this.mapTrace(n.rule)?.rows[Number(this.mapRow)];return report.result.managed.filter(m=>(!n.rule || m.rule===n.rule)&&(!n.block || m.block===n.block)&&(!row || row.resources.includes(m.id))).map(m=>{const kind=['rotation','priority'].includes(m.kind)?'schedules':m.kind==='escalation'?'escalations':'routes';const resource=report.result[kind].find(r=>r.id===m.id);return {...m,resource,name:resource?.name||m.id};});},
mapIssues(){const n=this.mapCurrent();return (this.mapSnapshot?.report.issues||[]).filter(i=>!n || (n.variable?i.variable_id===n.variable:!i.rule_id || i.rule_id===n.rule)&&(!n.block || !i.block_id || i.block_id===n.block));},
mapRows(){return this.mapTrace(this.mapCurrent()?.rule)?.rows||[];},
mapRowLabel(row,index){const rule=this.mapSnapshot.payload.rules.find(r=>r.id===this.mapCurrent()?.rule);const names=(rule?.iterations||[]).map((iteration,i)=>{const v=row.inputs?.[iteration.id];return typeof v==='object'&&v!==null?(v.name||v.title||row.labels?.[i]||iteration.name):String(v??iteration.name);});return (index+1)+'. '+(names.join(' / ')||tr('map_once'))+' · '+tr('map_'+row.status,{count:1});},
mapInputSources(){
const n=this.mapCurrent(),snapshot=this.mapSnapshot;if(!n||!snapshot||this.mapBusy)return [];
const {payload,report}=snapshot,rule=payload.rules.find(r=>r.id===n.rule),sources=new Map();
const variable=(id,address)=>{const v=payload.variables.find(v=>v.id===id);if(!v)return;const key='v:'+id;if(!sources.has(key))sources.set(key,{id:key,name:v.name,type:this.label(v.node.kind),count:v.node.entries?.length||0,addresses:[],target:key});const item=sources.get(key);if(address&&!item.addresses.includes(address))item.addresses.push(address);};
const scan=(value,owner=rule,depth=0)=>{if(!value||typeof value!=='object'||depth>12)return;
if(value.kind==='variable'||value.kind==='lookup')variable(value.variable,'{ '+this.bindingAddress(value,0,owner?.iterations||[])+' }');
if(value.kind==='item'||value.kind==='key'){const iteration=owner?.iterations.find(i=>i.id===value.iteration);if(iteration){const before=new Set(sources.keys());scan(iteration.source,owner,depth+1);for(const item of sources.values())if(item.id.startsWith('v:')&&(!before.has(item.id)||iteration.source.variable===item.id.slice(2))){const address='{ '+this.bindingAddress(value,0,owner.iterations)+' }';if(!item.addresses.includes(address))item.addresses.push(address);}}}
if(value.kind==='output'){
const producer=payload.rules.find(r=>r.blocks.some(b=>b.id===value.output)),block=producer?.blocks.find(b=>b.id===value.output);
if(block && (n.block || producer.id!==n.rule)){const managed=report.result?.managed.filter(m=>m.block===block.id)||[],resources=[...(report.result?.schedules||[]),...(report.result?.escalations||[]),...(report.result?.routes||[])],resource=managed.length===1?resources.find(r=>r.id===managed[0].id):null,key='o:'+block.id;const target=this.mapNodes.find(v=>v.block===block.id)||this.mapNodes.find(v=>v.id==='g:'+producer.id)||this.mapNodes.find(v=>v.id==='r:'+producer.id);sources.set(key,{id:key,name:resource?.name||producer.name,type:['rotation','priority'].includes(block.kind)?tr('resource_schedule'):this.label(block.kind),count:managed.length,generated:true,addresses:[],target:target?.id});}
}
if(value.kind==='literal'&&typeof value.value==='string'){const id=value.value,resource=[...this.doc.schedules,...this.doc.escalations,...this.doc.routes].find(r=>r.id===id);if(resource)sources.set('saved:'+id,{id:'saved:'+id,name:resource.name,type:tr('source_saved_resource',{name:''}),addresses:[]});}
Object.values(value).forEach(child=>scan(child,owner,depth+1));
};
if(n.variable)variable(n.variable);else if(n.kind==='iteration')scan(n.source.source);else if(n.kind==='condition')scan(rule?.when);else if(rule){if(!n.block)scan(rule.iterations);scan(rule.blocks.filter(b=>!n.block||b.id===n.block));if(!n.block)scan(rule.when);}
return [...sources.values()];
},
mapInputSections(){
const n=this.mapCurrent();if(!n||!this.mapSnapshot||this.mapBusy)return [];
if(n.variable)return [{title:n.title,rows:[{label:this.label(n.source.kind),values:[this.sampleNode(n.source)]}]}];
const rule=this.mapSnapshot.payload.rules.find(r=>r.id===n.rule);if(!rule)return [];
const all=this.mapRows(),rows=this.mapRow==='all'?all:[all[Number(this.mapRow)]].filter(Boolean),unique=values=>[...new Map(values.filter(v=>v!==undefined).map(v=>[JSON.stringify(v),v])).values()];
if(n.kind==='iteration')return [{title:n.title,rows:[{label:n.source.name,values:unique(rows.map(r=>r.inputs?.[n.source.id]))}]}];
if(n.kind==='condition')return [{title:tr('map_condition'),rows:[{label:tr('map_condition_left'),values:unique(rows.map(r=>r.condition?.left))},{label:tr('map_condition_operation'),values:[this.label(rule.when.operation)]},{label:tr('map_condition_right'),values:unique(rows.map(r=>r.condition?.right))}]}];
return rule.blocks.filter(b=>!n.block||n.block===b.id).map(block=>{
const inputs=rows.flatMap(r=>(r.blocks||[]).filter(b=>b.block===block.id)),fields=this.fieldsFor(block.kind),out=[];
for(const field of fields){const values=unique(inputs.map(b=>b.fields[field.key]));if(values.length)out.push({label:field.label,values});}
const maxSteps=Math.max(0,...inputs.map(b=>b.steps.length));for(let i=0;i<maxSteps;i++){out.push({label:tr('map_step_delay',{step:i+1}),values:unique(inputs.map(b=>b.steps[i]?.delay))},{label:tr('map_step_recipient',{step:i+1}),values:unique(inputs.map(b=>b.steps[i]?.recipient))});}
const conditions=unique(inputs.flatMap(b=>b.conditions));for(const c of conditions)out.push({label:this.label(c.field)+' · '+(c.negate?tr('map_not')+' ':'')+this.label(c.operation),values:[c.value]});
if(block.kind==='route'&&inputs.length&&!conditions.length)out.push({label:tr('map_condition'),values:[tr('map_matches_all')]});
return {title:this.label(block.kind),rows:out};
}).filter(section=>section.rows.length);
},
mapResourceFields(item){
const r=item.resource;if(!r)return [];const fields=[],add=(key,value)=>fields.push({label:tr(key),value});
if(['rotation','priority'].includes(item.kind)){
add(item.kind==='priority'?'responsible_people_in_priority_order_7205a5':'participants_in_rotation_order_4edee0',r.participants);
if(item.kind==='priority')add('fallback_schedule_7b7a21',r.fallback);
else {add('anchor_date_e77bd6',this.formatDate(r.anchor));add('shift_length_days_0b28fa',r.shift_days);}
add('handover_time_c9e315',r.handover.slice(0,5)+' · '+this.mapSnapshot.report.result.timezone);
}else if(item.kind==='escalation'){
r.steps.forEach((step,index)=>{fields.push({label:tr('map_step_recipient',{step:index+1}),value:step.recipient.id||(step.recipient.type==='team_all'?tr('everyone_on_the_team_235b55'):tr('a_random_team_member_8f35f3')),hint:(step.delay_minutes===0?tr('immediately_21b60a'):step.delay_minutes+tr('min_after_alert_8adeb2'))+' · '+tr(step.condition==='if-not-acked'?'if_unacknowledged_407d03':'if_still_open_c34949')});});
add('repeat_count_912e08',r.repeat.count&&r.repeat.wait_minutes?tr('repeat_654579')+r.repeat.count+tr('times_wait_e1b163')+r.repeat.wait_minutes+tr('min_f4e8a8'):tr('end_of_policy_no_repeats_c2ac4f'));
if(r.repeat.count&&r.repeat.wait_minutes){add('reset_recipient_states_on_repeat_8152c6',r.repeat.reset_recipient_states);add('close_alert_after_all_repeats_2bec22',r.repeat.close_alert_after_all);}
}else if(item.kind==='route'){
add('resource_all_conditions',r.conditions.length?r.conditions.map(c=>this.conditionText(c)):tr('any_alert_4496c3'));
if(r.window){const w=r.window,days=['mon_24b2a0','tue_529541','wed_23408b','thu_3593cc','fri_bbd6e3','sat_6b782d','sun_48c98c'];add('workspace_working_interval_c7d250',w.weekdays.map(d=>tr(days[d-1])).join(', ')+' · '+w.start.slice(0,5)+'–'+w.end.slice(0,5)+' · '+w.timezone);}
add('resource_detail_destination',r.escalation_id||tr('do_not_notify_1a59a6'));
}
return fields;
},
mapInputTokens(value){
const result=this.mapSnapshot?.report.result,refs=new Map();
for(const p of [...this.people,...(result?.people||[])]){const item={kind:tr('map_person'),text:p.display_name};refs.set(p.id,item);if(p.email)refs.set(p.email,item);}
for(const [kind,list] of [['schedule',result?.schedules||this.doc.schedules],['escalation',result?.escalations||this.doc.escalations],['route',result?.routes||this.doc.routes]])for(const r of list){refs.set(r.id,{kind:this.label(kind),text:r.name,generated:!!result?.managed.some(m=>m.id===r.id)});}
const tokens=[];const visit=(v,path='',depth=0)=>{if(tokens.length>=80||depth>10)return;if(typeof v==='string'&&refs.has(v)){tokens.push({...refs.get(v),path});return;}if(Array.isArray(v)){if(!v.length)tokens.push({text:tr('map_empty_value'),path});else v.forEach((item,i)=>visit(item,path?path+' / '+(i+1):String(i+1),depth+1));return;}if(v&&typeof v==='object'){Object.entries(v).forEach(([key,item])=>visit(item,path?path+' / '+key:key,depth+1));return;}tokens.push({path,text:v===null?tr('map_missing_value'):typeof v==='boolean'?tr(v?'true_88b33e':'false_97cdbd'):typeof v==='string'&&/^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(v)?tr('map_missing_reference'):String(v)});};visit(value);return tokens;
},
mapToggle(){const n=this.mapCurrent();if(!n?.rule)return;this.mapExpanded[n.rule]=!this.mapExpanded[n.rule];this.drawGenerationMap();},
mapFit(){graph?.fit(undefined,45);},mapZoom(factor){if(graph)graph.zoom({level:Math.max(.2,Math.min(2,graph.zoom()*factor)),renderedPosition:{x:graph.width()/2,y:graph.height()/2}});},
async mapEdit(){const n=this.mapCurrent();if(!n)return;this.closeGenerationMap();await this.openIssue(n.variable?{kind:'variable',id:n.variable}:{kind:'rule',id:n.rule,block:n.block});},
drawGenerationMap(){
if(!this.mapSnapshot || !this.mapOpen)return;graph?.destroy();const {payload,report}=this.mapSnapshot,nodes=[],edges=[],ids=new Set(),blockTargets=new Map();
const add=(id,title,subtitle,kind,x,y,meta={})=>{ids.add(id);nodes.push({id,title,subtitle,kind,...meta,position:{x,y}});};
const edge=(source,target,role='flow')=>{if(source!==target)edges.push({data:{id:role+':'+source+'>'+target,source,target,role}});};
payload.variables.forEach((v,i)=>add('v:'+v.id,v.name,this.nodeSummary(v.node),'variable',i*300,-160,{variable:v.id,source:v.node}));
for(const [ruleIndex,rule] of payload.rules.entries()){const trace=this.mapTrace(rule.id),count=report.result?.managed.filter(m=>m.rule===rule.id).length||0,iterated=rule.iterations.length>0;const x=ruleIndex*350,y=0;
const issue=report.issues.some(i=>i.rule_id===rule.id),status=!rule.enabled?tr('disabled_f4f447'):issue?tr('map_invalid'):!report.result?tr('map_unverified'):tr('map_count',{count});
add('r:'+rule.id,rule.name,status,!rule.enabled?'disabled':issue?'error':'rule',x,y,{rule:rule.id,source:rule,step:ruleIndex+1});
if(ruleIndex)edge('r:'+payload.rules[ruleIndex-1].id,'r:'+rule.id,'order');
let parent='r:'+rule.id;
if(iterated){for(const [k,iteration] of rule.iterations.entries()){const id='i:'+iteration.id;add(id,iteration.name+' · '+tr('map_loop',{count:trace?.stages.find(s=>s.iteration===iteration.id)?.count||0}),'{ '+this.bindingAddress(iteration.source,0,rule.iterations)+' }','iteration',x,120+k*110,{rule:rule.id,source:iteration,iteration:true});edge(parent,id);parent=id;}}
const outputY=120+rule.iterations.length*110;
if(rule.when){const id='c:'+rule.id;add(id,tr('map_condition'),tr('map_filtered',{count:trace?.rows.filter(r=>r.status==='filtered').length||0}),'condition',x,outputY,{rule:rule.id,source:rule.when});edge(parent,id);parent=id;}
if(iterated&&!this.mapExpanded[rule.id]){const id='g:'+rule.id;add(id,tr('map_group'),report.result?tr('map_count',{count}):tr('map_unverified'),'batch',x,outputY+(rule.when?110:0),{rule:rule.id,source:rule.blocks,iteration:true});edge(parent,id);parent=id;}
for(const [j,b] of rule.blocks.entries()){if(iterated&&!this.mapExpanded[rule.id]){blockTargets.set(b.id,parent);continue;}const count=report.result?.managed.filter(m=>m.block===b.id).length||0;const id='b:'+b.id;blockTargets.set(b.id,id);const produced=report.result?.managed.filter(m=>m.block===b.id)||[],resource=produced.length===1?[...report.result.schedules,...report.result.escalations,...report.result.routes].find(r=>r.id===produced[0].id):null;const kind=['rotation','priority'].includes(b.kind)?tr('schedule_type_named',{name:this.label(b.kind)}):this.label(b.kind);add(id,resource?resource.name:kind,report.result?(resource?kind:tr('map_count',{count})):tr('map_unverified'),iterated?'batch':'output',x,outputY+(rule.when?110:0)+j*110,{rule:rule.id,block:b.id,source:b});edge(parent,id);parent=id;}
}
const scan=(value,target)=>{if(!value||typeof value!=='object')return;if(value.kind==='variable'||value.kind==='lookup'){if(ids.has('v:'+value.variable))edge('v:'+value.variable,target,'dependency');}if(value.kind==='output'&&blockTargets.has(value.output))edge(blockTargets.get(value.output),target,'dependency');Object.values(value).forEach(v=>scan(v,target));};
for(const rule of payload.rules){rule.iterations.forEach(i=>scan(i.source,'i:'+i.id));if(rule.when)scan(rule.when,'c:'+rule.id);rule.blocks.forEach(b=>scan(b,blockTargets.get(b.id)));}
const sheets=nodes.filter(n=>n.kind==='batch').flatMap(n=>[2,1].map(layer=>({data:{id:n.id+':sheet:'+layer,owner:n.id,layer,kind:'sheet'},classes:'stack-sheet',position:{x:n.position.x+layer*8,y:n.position.y-layer*8},selectable:false,grabbable:false})));
this.mapNodes=nodes;graph=cytoscape({container:this.$refs.generationCanvas,elements:[...sheets,...nodes.map(n=>({data:{id:n.id,label:(n.step?n.step+'. ':'')+n.title+'\n'+n.subtitle,kind:n.kind},position:n.position})),...Array.from(new Map(edges.filter(e=>ids.has(e.data.source)&&ids.has(e.data.target)).map(e=>[e.data.id,e])).values())],layout:{name:'preset'},minZoom:.2,maxZoom:2,wheelSensitivity:.2,style:[{selector:'node',style:{shape:'round-rectangle',width:220,height:66,'background-color':'#fff','border-color':'#cad9cf','border-width':1.5,label:'data(label)','text-wrap':'wrap','text-max-width':195,'text-valign':'center',color:'#234236','font-family':'system-ui','font-size':11,'line-height':1.6,'z-index-compare':'manual','z-index':5}},{selector:'node[kind="variable"]',style:{'background-color':'#f4f0e7','border-color':'#d8c9aa'}},{selector:'node[kind="iteration"],node[kind="batch"]',style:{'background-color':'#edf2fb','border-color':'#94add2','border-style':'dashed'}},{selector:'.stack-sheet',style:{label:'','background-color':'#f4f7fc','border-color':'#94add2','border-style':'solid','border-width':1.5,'z-index':1,events:'no'}},{selector:'node[kind="batch"]',style:{'border-style':'solid','border-width':2}},{selector:'node[kind="condition"]',style:{'background-color':'#fff5df','border-color':'#d4b773'}},{selector:'node[kind="rule"]',style:{'background-color':'#e8f3ec','border-color':'#79a68a'}},{selector:'node[kind="disabled"]',style:{opacity:.5}},{selector:'node[kind="error"]',style:{'background-color':'#fcece7','border-color':'#ce7863'}},{selector:'edge',style:{width:1.5,'line-color':'#a4b6ab','target-arrow-color':'#839e8e','target-arrow-shape':'triangle','curve-style':'bezier'}},{selector:'edge[role="order"]',style:{width:3,'line-color':'#428465','target-arrow-color':'#428465','curve-style':'straight','z-index':3}},{selector:'edge[role="dependency"]',style:{'line-style':'dashed',opacity:.2,'curve-style':'unbundled-bezier','control-point-distances':-45,'control-point-weights':.5}},{selector:'edge.related',style:{opacity:1}},{selector:'.related',style:{'border-color':'#428465','line-color':'#428465','target-arrow-color':'#428465'}},{selector:'node.selected',style:{'border-width':3,'border-color':'#27694a'}}]});graph.on('tap','node:not(.stack-sheet)',e=>this.mapSelect(e.target.id()));graph.on('position','node[kind="batch"]',e=>{const p=e.target.position();for(const layer of [1,2])graph.getElementById(e.target.id()+':sheet:'+layer).position({x:p.x+layer*8,y:p.y-layer*8});});graph.fit(undefined,45);if(!ids.has(this.mapSelected))this.mapSelected=nodes.find(n=>n.rule)?.id||nodes[0]?.id||'';this.mapSelect(this.mapSelected);
}
};}
+73
View File
@@ -0,0 +1,73 @@
<template x-if="modal==='variable'"><div>
<label><span x-text="t('variable_name_39179c')"></span><input x-model="form.name" required maxlength="80" :placeholder="t('responsible_37926b')"></label>
<div class="section-line data-mode"><span class="badge" x-text="label(form.node.kind)"></span><button type="button" class="text" @click="dataAdvanced=!dataAdvanced" x-text="dataAdvanced?t('data_values_mode'):t('data_structure_mode')"></button></div>
<template x-if="!dataAdvanced && simpleData(form.node)"><div class="simple-data">
<template x-if="form.node.kind==='list'"><div><template x-for="(entry,index) in form.node.entries" :key="entry.id"><div class="simple-list-row"><span class="step-number" x-text="index+1"></span><template x-if="entry.kind==='text'"><input :aria-label="t('list_value_4318c3')" x-model="entry.value" :placeholder="t('value_8dce17')"></template><template x-if="entry.kind==='person'"><button type="button" class="grow person-select" @click="editDataPeople(form.node,entry)" x-text="dataPersonName(entry.value)"></button></template><button type="button" :aria-label="t('move_entry_up_647a57')" @click="moveNode(form.node,index,-1)" :disabled="!index"><span x-text="t('symbol_9555ff')"></span></button><button type="button" :aria-label="t('move_entry_down_bc86af')" @click="moveNode(form.node,index,1)" :disabled="index===form.node.entries.length-1"><span x-text="t('symbol_fc4866')"></span></button><button type="button" class="text danger" :aria-label="t('remove_entry_18ffc8')" @click="form.node.entries.splice(index,1)"><span x-text="t('symbol_67fba2')"></span></button></div></template><div class="actions"><button type="button" @click="form.node.entries[0]?.kind==='person'?editDataPeople(form.node):addNode(form.node)"><span x-text="t('add_item_51e41d')"></span></button><button type="button" @click="openBulk(form.node)"><span x-text="t('paste_rows_24d667')"></span></button></div></div></template>
<template x-if="form.node.kind!=='list'"><div><div class="matrix-head"><span><span x-text="t('project_key_1c60ec')"></span></span><span><span x-text="t('responsible_people_in_order_d69f7b')"></span></span></div><template x-for="(group,index) in form.node.entries" :key="group.id"><div class="matrix-row"><input :aria-label="t('dictionary_key_9777bd')" x-model="group.key" :placeholder="t('project_name_ab9773')"><div class="matrix-people"><template x-for="(entry,i) in group.entries" :key="entry.id"><span class="matrix-person"><b x-text="i+1"></b><span x-text="dataPersonName(entry.value)"></span><button type="button" :aria-label="t('move_person_earlier_52af52')" x-show="i>0" @click="moveNode(group,i,-1)"><span x-text="t('symbol_817964')"></span></button><button type="button" :aria-label="t('remove_person_2ea467')" @click="group.entries.splice(i,1)"><span x-text="t('symbol_67fba2')"></span></button></span></template><button type="button" class="text" @click="editDataPeople(group)" :aria-label="t('add_person_to_4670b9')+group.key"><span x-text="t('person_485c1d')"></span></button></div><button type="button" class="text danger" :aria-label="t('remove_project_333b0c')" @click="form.node.entries.splice(index,1)"><span x-text="t('symbol_67fba2')"></span></button></div></template><div class="actions"><button type="button" @click="addDataGroup()"><span x-text="t('add_project_key_71d0f2')"></span></button><button type="button" @click="openBulk(form.node)"><span x-text="t('paste_spreadsheet_rows_e07917')"></span></button></div></div></template>
<div class="inline-people" x-show="dataPersonTarget"><div class="section-line"><strong x-text="t('add_to_10956d')+(dataPersonTarget?.key || form.name)"></strong><button type="button" @click="dataPersonTarget=null"><span x-text="t('done_e9b450')"></span></button></div><input type="search" :aria-label="t('find_person_or_email_b640f7')" x-model="dataPersonQuery" :placeholder="t('search_name_or_enter_email_863139')"><div class="cover-options"><template x-for="p in people.filter(p=>[p.display_name,p.email,p.username].some(v=>v?.toLowerCase().includes(dataPersonQuery.toLowerCase())))" :key="p.id"><button type="button" @click="addDataPerson(p.id)" :disabled="dataPersonTarget?.entries.some(e=>e.value===p.id)" x-text="p.display_name"></button></template><button type="button" x-show="/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(dataPersonQuery)" @click="addDataPerson(dataPersonQuery.toLowerCase())" x-text="t('add_e2b431')+dataPersonQuery"></button></div></div>
</div></template>
<div class="data-tree compact-data-tree" :class="{'structure-mode':dataAdvanced}" x-show="dataAdvanced || !simpleData(form.node)"><template x-for="row in nodeRows(form.node)" :key="row.node.id"><div class="data-row" :class="['data-depth-'+row.depth,{'container-row':dataContainer(row.node)}]">
<div class="data-key"><button type="button" class="data-disclosure" x-show="dataContainer(row.node)" :aria-expanded="!!dataExpanded[row.node.id]" :aria-label="t('data_toggle',{name:row.node.key || nodeSummary(row.node)})" @click="toggleDataNode(row.node)" x-text="dataExpanded[row.node.id]?t('data_expanded_symbol'):t('data_collapsed_symbol')"></button><template x-if="row.parent && row.parent.kind!=='list'"><input x-show="dataAdvanced" :aria-label="t('dictionary_key_9777bd')" x-model="row.node.key" :placeholder="t('project_key_98f3fa')" maxlength="120"></template><span x-show="row.parent && row.parent.kind!=='list' && !dataAdvanced" x-text="row.node.key"></span><span x-show="!row.parent || row.parent.kind==='list'" x-text="row.parent?(row.node.kind==='dictionary'?nodeSummary(row.node):t('item_284e12')+(row.index+1)):form.name"></span></div>
<select x-show="dataAdvanced" class="data-type" :aria-label="t('value_type_a95648')" :value="row.node.kind" @change="setNodeKind(row.node,$event.target.value)"><template x-for="kind in nodeKinds"><option :value="kind" :selected="row.node.kind===kind" x-text="kind==='duration'?t('duration_minutes_77d597'):label(kind)"></option></template></select>
<div class="data-value">
<template x-if="['list','dictionary'].includes(row.node.kind)"><div class="actions"><span class="data-container-preview" :title="dataCaption(row.node)" x-text="dataCaption(row.node)"></span><span class="badge" x-text="row.node.entries.length"></span><button type="button" @click="addNode(row.node)"><span x-text="t('entry_dd9298')"></span></button><button type="button" x-show="row.node.entries.every(n=>!dataContainer(n))" @click="openBulk(row.node)"><span x-text="t('paste_rows_24d667')"></span></button></div></template>
<template x-if="row.node.kind==='text'"><input :aria-label="t('text_value_9fe378')" x-model="row.node.value" :placeholder="t('value_8dce17')" maxlength="2000"></template>
<template x-if="row.node.kind==='person'"><div class="person-value"><select :aria-label="t('registered_person_734f05')" :value="people.some(p=>p.id===row.node.value)?row.node.value:''" @change="row.node.value=$event.target.value"><option value="" x-text="t('use_email_address_c12ddf')"></option><template x-for="person in people" :key="person.id"><option :value="person.id" :selected="row.node.value===person.id" x-text="person.display_name"></option></template></select><input :aria-label="t('person_email_1ed8a9')" :type="people.some(p=>p.id===row.node.value)?'text':'email'" x-show="!people.some(p=>p.id===row.node.value)" x-model="row.node.value" :placeholder="t('person_example_com_371ab9')"></div></template>
<template x-if="['number','duration'].includes(row.node.kind)"><input :aria-label="t('number_value_f84357')" type="number" step="any" :value="row.node.value" @input="row.node.value=$event.target.value===''?null:Number($event.target.value)"></template>
<template x-if="row.node.kind==='boolean'"><select :aria-label="t('boolean_value_4d92eb')" :value="String(row.node.value)" @change="row.node.value=$event.target.value==='true'"><option value="true" x-text="t('true_88b33e')"></option><option value="false" x-text="t('false_97cdbd')"></option></select></template>
<template x-if="row.node.kind==='time'"><input :aria-label="t('time_value_1072a0')" type="time" x-model="row.node.value"></template>
<template x-if="row.node.kind==='date'"><span class="date-field" x-data="dateField" x-modelable="value" x-model="row.node.value"><input type="text" :aria-label="t('date_value_1f290a')" x-ref="textDate" :value="text" @input="edit($event.target)" :placeholder="t('dd_mm_yyyy_d7e5fe')" inputmode="numeric" maxlength="10"><button type="button" :aria-label="t('open_calendar_41547e')" @click="$refs.calendar.showPicker()"><span x-text="t('symbol_21887f')"></span></button><input class="native-date" type="date" x-ref="calendar" x-model="value" @input="picked()" tabindex="-1" aria-hidden="true"></span></template>
</div>
<div class="data-actions" x-show="row.parent"><button type="button" :aria-label="t('data_duplicate')" @click="duplicateDataNode(row.parent,row.index)" x-text="t('data_duplicate_symbol')"></button><button type="button" :aria-label="t('move_entry_up_647a57')" @click="moveNode(row.parent,row.index,-1)" :disabled="row.index===0"><span x-text="t('symbol_9555ff')"></span></button><button type="button" :aria-label="t('move_entry_down_bc86af')" @click="moveNode(row.parent,row.index,1)" :disabled="!row.parent || row.index===row.parent.entries.length-1"><span x-text="t('symbol_fc4866')"></span></button><button type="button" class="text danger" :aria-label="t('remove_entry_18ffc8')" @click="row.parent.entries.splice(row.index,1)"><span x-text="t('symbol_67fba2')"></span></button></div>
</div></template></div>
<template x-if="bulkNode"><div class="bulk-editor"><h3><span x-text="t('append_rows_03d5d2')"></span></h3><p class="muted" x-text="bulkNode.kind==='list'?t('one_value_per_line_existing_entries_are_kept_244563'):t('paste_two_spreadsheet_columns_project_key_then_comma_9b3966')"></p><textarea :aria-label="t('rows_to_append_2ddee4')" x-model="bulkText" rows="5"></textarea><div class="actions"><button type="button" @click="try{appendBulk()}catch(e){error=e.message}"><span x-text="t('append_rows_03d5d2')"></span></button><button type="button" @click="bulkNode=null"><span x-text="t('cancel_paste_fbbf61')"></span></button></div></div></template>
<div x-show="!rulePeopleTarget"><!-- GENERATION_PREVIEW --></div>
</div></template>
<template x-if="modal==='generation-rule'"><div><div class="rule-constructor" x-show="!rulePeopleTarget">
<div class="two-columns rule-identity"><label><span x-text="t('rule_name_9e84d7')"></span><input x-model="form.name" required maxlength="120" :placeholder="t('builder_rule_name')"></label><label class="check"><input type="checkbox" x-model="form.enabled"><span x-text="t('enabled_df174a')"></span></label></div>
<div class="constructor-layout"><aside class="constructor-inputs">
<section class="constructor-section"><div class="section-line"><h3 x-text="t('builder_iterations')"></h3><button type="button" @click="addIteration()" :disabled="form.iterations.length>=4" x-text="t('iteration_3b56cf')"></button></div>
<p class="constructor-empty" x-show="!form.iterations.length" x-text="t('builder_once')"></p>
<template x-for="(iteration,index) in form.iterations" :key="iteration.id"><div class="constructor-loop">
<div class="constructor-loop-title"><span class="logic-key" x-text="index?t('then_for_each_4ca48c'):t('for_each_fb4174')"></span><button type="button" class="text danger" :aria-label="t('builder_remove_iteration',{name:iteration.name})" @click="form.iterations.splice(index,1)" x-text="t('remove_e96390')"></button></div>
<label><span x-text="t('builder_alias')"></span><input :aria-label="t('iteration_name_fc3d26')" x-model="iteration.name" :placeholder="t('builder_alias_placeholder')"></label>
<label><span x-text="t('iteration_collection_bd6344')"></span><select :aria-label="t('iteration_collection_bd6344')" :value="simpleSource(iteration.source,'collection')" @change="selectSimpleSource(iteration.source,$event.target.value,'collection')"><option value="literal" x-text="t('builder_choose_collection')"></option><template x-for="option in sourceOptions('collection')" :key="option.id"><option :value="option.id" :selected="simpleSource(iteration.source,'collection')===option.id" x-text="option.name"></option></template><option value="advanced" x-text="t('custom_binding')"></option></select></label>
<button type="button" class="binding-button constructor-binding" @click="editBinding(iteration.source,'collection',t('iteration_collection_bd6344'))" x-text="bindingSummary(iteration.source)"></button>
</div></template>
</section>
<section class="constructor-section constructor-filter"><h3 x-text="t('builder_filter')"></h3><label class="check"><input type="checkbox" :checked="!!form.when" @change="toggleGenCondition($event.target.checked)"><span x-text="t('only_generate_when_a_condition_matches_78d185')"></span></label>
<template x-if="form.when"><div class="constructor-predicate"><button type="button" class="binding-button" @click="editBinding(form.when.left,'text',t('condition_left_value_cc4063'))" x-text="bindingSummary(form.when.left)"></button><select :aria-label="t('generation_condition_424e30')" x-model="form.when.operation"><option value="equals" x-text="t('equals_09b6a6')"></option><option value="not_equals" x-text="t('does_not_equal_86e30f')"></option><option value="contains" x-text="t('contains_33e15d')"></option><option value="not_empty" x-text="t('is_not_empty_414a30')"></option></select><button type="button" class="binding-button" x-show="form.when.operation!=='not_empty'" @click="editBinding(form.when.right,'text',t('condition_right_value_5e741f'))" x-text="bindingSummary(form.when.right)"></button></div></template>
</section></aside><main class="constructor-outputs">
<section class="recipe-section"><div class="section-line"><h3><span x-text="t('builder_outputs')"></span></h3><select :aria-label="t('add_output_block_00047b')" @change="if($event.target.value)addBlock($event.target.value);$event.target.value=''"><option value="" x-text="t('add_another_resource')"></option><optgroup :label="t('schedules_26d9d0')"><template x-for="kind in ['priority','rotation']"><option :value="kind" x-text="label(kind)"></option></template></optgroup><optgroup :label="t('escalation_policies_ad128e')"><option value="escalation" x-text="label('escalation')"></option></optgroup><optgroup :label="t('routing_rules_49ce36')"><option value="route" x-text="label('route')"></option></optgroup></select></div>
<p class="constructor-empty" x-show="!form.blocks.length" x-text="t('builder_add_output')"></p><template x-for="(block,index) in form.blocks" :key="block.id"><section class="generation-block" :data-block-id="block.id">
<div class="route-heading"><span class="badge" x-text="(index+1)+' · '+label(block.kind)"></span><strong class="grow" :class="{'template-token':block.fields.name?.kind!=='literal'}" x-text="bindingSummary(block.fields.name)"></strong><button type="button" :aria-label="t('move_block_up_b67b5b')" :disabled="index===0" @click="moveBlock(index,-1)"><span x-text="t('symbol_9555ff')"></span></button><button type="button" :aria-label="t('move_block_down_714fea')" :disabled="index===form.blocks.length-1" @click="moveBlock(index,1)"><span x-text="t('symbol_fc4866')"></span></button><button type="button" class="text danger" @click="form.blocks.splice(index,1)"><span x-text="t('remove_e96390')"></span></button></div>
<div class="binding-grid"><template x-for="field in fieldsFor(block.kind)" :key="field.key"><div class="recipe-field" :class="{'compact-scalar-field':['number','date','time','boolean'].includes(field.type) && block.fields[field.key]?.kind==='literal','compact-number-field':field.type==='number','compact-time-field':field.type==='time'}"><label :for="block.id+'-'+field.key" x-text="field.label"></label>
<template x-if="field.key==='name'"><input class="block-name-input" :id="block.id+'-'+field.key" :aria-label="field.label" :value="bindingSummary(block.fields.name)" :readonly="block.fields.name.kind!=='literal' || !!block.fields.name.prefix || !!block.fields.name.suffix || !!block.fields.name.path?.length" @input="block.fields.name.value=$event.target.value"></template>
<template x-if="field.key!=='name'"><div><select x-show="['list','schedule','policy'].includes(field.type) || block.fields[field.key]?.kind!=='literal'" :id="block.id+'-'+field.key" :aria-label="field.label" :value="simpleSource(block.fields[field.key],field.type)" @change="selectSimpleSource(block.fields[field.key],$event.target.value,field.type)"><option value="literal" x-text="t('fixed_value_191429')"></option><template x-for="option in sourceOptions(field.type)" :key="option.id"><option :value="option.id" :selected="simpleSource(block.fields[field.key],field.type)===option.id" x-text="option.name"></option></template><option value="advanced" x-text="simpleSource(block.fields[field.key],field.type)==='advanced'?bindingSummary(block.fields[field.key]):t('custom_binding')"></option></select>
<template x-if="block.fields[field.key]?.kind==='literal' && !['schedule','policy'].includes(field.type)"><div>
<template x-if="['text','time','number','person'].includes(field.type)"><input class="inline-value" :aria-label="field.label+' · '+t('fixed_value_191429')" :type="field.type==='number'?'number':field.type==='time'?'time':'text'" :value="block.fields[field.key].value" @input="block.fields[field.key].value=field.type==='number'?($event.target.value===''?null:Number($event.target.value)):$event.target.value"></template>
<template x-if="field.type==='date'"><span class="date-field" x-data="dateField" x-modelable="value" x-model="block.fields[field.key].value"><input type="text" :aria-label="field.label+' · '+t('fixed_value_191429')" x-ref="textDate" :value="text" @input="edit($event.target)" :placeholder="t('dd_mm_yyyy_d7e5fe')" maxlength="10"><button type="button" :aria-label="t('open_calendar_41547e')" @click="$refs.calendar.showPicker()"><span x-text="t('symbol_21887f')"></span></button><input class="native-date" type="date" x-ref="calendar" x-model="value" @input="picked()" tabindex="-1" aria-hidden="true"></span></template>
<template x-if="field.type==='boolean'"><label class="check"><input type="checkbox" x-model="block.fields[field.key].value"><span x-text="t('enabled_df174a')"></span></label></template>
<template x-if="field.type==='list'"><div><div class="people-row"><template x-for="(id,personIndex) in block.fields[field.key].value"><button type="button" class="person-chip" @click="block.fields[field.key].value.splice(personIndex,1)" :aria-label="t('remove_e96390')+' '+dataPersonName(id)" x-text="(personIndex+1)+'. '+dataPersonName(id)+' ×'"></button></template></div><button type="button" class="text" @click="openRulePeople(block.fields[field.key])" x-text="t('add_person_c126e2')"></button></div></template>
</div></template>
</div></template><button type="button" class="text binding-tweak" @click="editBinding(block.fields[field.key] ||= literal(''),field.type,field.label)" :aria-label="t('bind_891f4e')+field.label" x-text="t('binding_picker_button')" :title="t('value_source_c0ae15')"></button>
</div></template></div>
<template x-if="block.kind==='escalation'"><div><template x-for="(step,stepIndex) in block.steps" :key="stepIndex"><div class="generation-step"><span class="flow-arrow"><span x-text="t('symbol_fc4866')"></span></span><div class="inline-binding inline-delay"><template x-if="step.delay.kind==='literal'"><input type="number" min="0" :aria-label="t('delay_in_minutes_e18c95')" :value="step.delay.value" @input="step.delay.value=$event.target.value===''?null:Number($event.target.value)"></template><button type="button" class="binding-button" x-show="step.delay.kind!=='literal'" @click="editBinding(step.delay,'number',t('delay_in_minutes_e18c95'))" x-text="bindingSummary(step.delay)"></button><span x-text="t('min_f4e8a8')"></span><button type="button" class="binding-popup-trigger" :aria-label="t('bind_891f4e')+t('delay_in_minutes_e18c95')" @click="editBinding(step.delay,'number',t('delay_in_minutes_e18c95'))" x-text="t('binding_picker_button')"></button></div><select :aria-label="t('escalation_condition_5b4c13')" x-model="step.condition"><option value="if-not-acked" x-text="t('if_not_acknowledged_5fc93a')"></option><option value="if-not-closed" x-text="t('if_not_closed_4369ad')"></option></select><select :aria-label="t('recipient_type_32b1f5')" :value="step.user?'person':'schedule'" @change="step.user=$event.target.value==='person'"><option value="schedule" x-text="t('schedule_0a8ada')"></option><option value="person" x-text="t('person_8c41ae')"></option></select><button type="button" class="binding-button grow" @click="editBinding(step.recipient,step.user?'person':'schedule',t('escalation_recipient_9dfd31'))" x-text="bindingSummary(step.recipient)"></button><button type="button" class="text danger" @click="block.steps.splice(stepIndex,1)"><span x-text="t('symbol_67fba2')"></span></button></div></template><button type="button" class="text" @click="addGenStep(block)"><span x-text="t('escalation_step_72b706')"></span></button></div></template>
<template x-if="block.kind==='route'"><div><p class="muted" x-show="!block.conditions.length"><span x-text="t('matches_any_alert_add_conditions_to_target_this_proj_299abf')"></span></p><template x-for="(condition,conditionIndex) in block.conditions" :key="conditionIndex"><div class="generation-step"><span class="logic-key" x-text="conditionIndex?t('and_c2f934'):t('if_7be63e')"></span><select :aria-label="t('routing_field_a63fdc')" x-model="condition.field"><option value="tags" x-text="t('tags_848eed')"></option><option value="message" x-text="t('message_68f414')"></option><option value="priority" x-text="t('priority_886cbf')"></option><option value="description" x-text="t('description_55f8eb')"></option><option value="source" x-text="t('source_6da13a')"></option></select><select :aria-label="t('routing_operation_01689c')" x-model="condition.operation"><option value="contains" x-text="t('contains_33e15d')"></option><option value="equals" x-text="t('equals_09b6a6')"></option><option value="starts-with" x-text="t('starts_with_2a61f5')"></option><option value="matches" x-text="t('matches_ee2dbd')"></option></select><button type="button" class="binding-button grow" @click="editBinding(condition.value,'text',t('routing_condition_value_f76e2d'))" x-text="bindingSummary(condition.value)"></button><label class="check"><input type="checkbox" x-model="condition.negate"><span x-text="t('not_ca1dd3')"></span></label><button type="button" class="text danger" @click="block.conditions.splice(conditionIndex,1)"><span x-text="t('symbol_67fba2')"></span></button></div></template><button type="button" class="text" @click="addGenCondition(block)"><span x-text="t('and_condition_63ce48')"></span></button></div></template>
</section></template>
</section>
<details class="recipe-section rule-result" open>
<summary><strong x-text="t('rule_result_title')"></strong><small :class="{'danger':generationError || generationResult?.issues.length}" x-text="ruleResultStatus()" aria-live="polite"></small></summary>
<div class="rule-result-list" x-show="!generationLoading && ruleResultDocument()">
<template x-for="entry in ruleResultResources()" :key="entry.id"><div class="rule-result-row">
<svg class="resource-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path :d="resourceIcon(entry.kind)"></path></svg>
<div><div class="rule-result-name"><span class="badge" x-text="label(entry.kind)"></span><strong x-text="entry.resource.name"></strong></div><small x-text="resourceBrief(entry.resource,entry.kind,ruleResultDocument())"></small></div>
</div></template></div>
</details>
</main></div></div><template x-if="rulePeopleTarget"><section class="binding-editor"><div class="section-line"><input type="search" :aria-label="t('search_people_or_email')" :placeholder="t('search_people_or_email')" x-model="rulePeopleQuery"><button type="button" @click="closeRuleEditor()" x-text="t('back_c32ae9')"></button></div><div class="people-row"><template x-for="person in people.filter(p=>[p.display_name,p.email,p.username].some(v=>v?.toLowerCase().includes(rulePeopleQuery.toLowerCase())) && !rulePeopleTarget.value.includes(p.id))"><button type="button" @click="addRulePerson(person.id)" x-text="person.display_name"></button></template><button type="button" x-show="/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(rulePeopleQuery)" @click="addRulePerson(rulePeopleQuery.trim())" x-text="rulePeopleQuery"></button></div></section></template>
<!-- BINDING_POPOVER -->
<div x-show="!rulePeopleTarget"><!-- GENERATION_PREVIEW --></div>
</div></template>
+58
View File
@@ -0,0 +1,58 @@
<section id="generation" class="setup-section" x-show="view==='configuration'">
<template x-if="generationDraft"><div>
<div class="setup-intro"><h2 x-text="t('setup_sources')"></h2><p class="muted" x-text="t('setup_sources_hint')"></p></div>
<div class="two-columns generation-columns">
<div class="panel generation-panel"><div class="section-line"><h2><span x-text="t('workspace_variables_2c8617')"></span></h2><button @click="editVariable()" x-show="can('edit_schedules')"><span x-text="t('variable_596d48')"></span></button></div><p class="muted"><span x-text="t('projects_people_and_shared_values_0cd0a8')"></span></p>
<div class="workspace-variable-list" role="region" :aria-label="t('workspace_variables_2c8617')" tabindex="0"><template x-for="variable in generationDraft.variables" :key="variable.id"><div class="variable-card"><div class="grow"><strong x-text="variable.name"></strong><small><span x-text="label(variable.node.kind)"></span> <span x-text="t('symbol_1fdf0d')"></span> <span x-text="nodeSummary(variable.node)"></span></small></div><button @click="editVariable(variable)" x-show="can('edit_schedules')" :aria-label="t('edit_variable_22ec4a')+variable.name"><span x-text="t('edit_530164')"></span></button><button class="text danger" @click="removeGenerationItem('variables',variable.id)" x-show="can('edit_schedules')" :aria-label="t('remove_variable_bdd11d')+variable.name"><span x-text="t('remove_e96390')"></span></button></div></template>
</div><p class="muted" x-show="!generationDraft.variables.length"><span x-text="t('no_variables_yet_start_with_a_list_of_projects_and_a_84c8f7')"></span></p></div>
<div class="panel generation-panel"><div class="section-line"><h2><span x-text="t('team_generation_rules_f882ba')"></span></h2><button @click="openGenerationMap()" x-show="can('edit_schedules')" x-text="t('map_open')"></button><button @click="editGenerationRule()" x-show="can('edit_schedules')"><span x-text="t('generation_rule_610cb3')"></span></button></div><p class="muted"><span x-text="t('turn_shared_values_into_schedules_automatically_66a530')"></span></p>
<div class="generation-rule-list" role="region" :aria-label="t('team_generation_rules_f882ba')" tabindex="0"><template x-for="(rule,ruleIndex) in generationDraft.rules" :key="rule.id"><div class="variable-card"><div class="grow"><strong x-text="rule.name"></strong><small x-text="ruleOverview(rule)"></small></div><button :aria-label="t('move_rule_up_a8ab03')" @click="moveGenerationRule(ruleIndex,-1)" :disabled="ruleIndex===0" x-show="can('edit_schedules')"><span x-text="t('symbol_9555ff')"></span></button><button :aria-label="t('move_rule_down_2c58ed')" @click="moveGenerationRule(ruleIndex,1)" :disabled="ruleIndex===generationDraft.rules.length-1" x-show="can('edit_schedules')"><span x-text="t('symbol_fc4866')"></span></button><button @click="editGenerationRule(rule)" x-show="can('edit_schedules')" :aria-label="t('edit_rule_d0838a')+rule.name"><span x-text="t('edit_530164')"></span></button><button class="text danger" @click="removeGenerationItem('rules',rule.id)" x-show="can('edit_schedules')" :aria-label="t('remove_rule_2dd5ee')+rule.name"><span x-text="t('remove_e96390')"></span></button></div></template>
</div><p class="muted" x-show="!generationDraft.rules.length"><span x-text="t('one_rule_can_produce_all_your_project_schedules_bb9e45')"></span></p>
<label class="generated-routing-placement" x-show="orderedRoutes.some(r=>!generated(r.id)) && generationDraft.rules.some(r=>r.enabled && r.blocks.some(b=>b.kind==='route'))"><span x-text="t('generated_routing_block_position_e14d64')"></span><select :aria-label="t('generated_routing_block_position_e14d64')" :value="generationDraft.route_position" @change="generationDraft.route_position=Number($event.target.value)"><option value="0" :selected="generationDraft.route_position===0" x-text="t('first_before_manual_routing_rules_b44e75')"></option><template x-for="(route,index) in orderedRoutes.filter(r=>!generated(r.id))" :key="route.id"><option :value="index+1" :selected="generationDraft.route_position===index+1" x-text="t('generated_routing_after_named',{name:route.name})"></option></template></select></label></div>
<section id="availability" class="setup-section setup-availability" x-show="view==='configuration'">
<div class="setup-utilities">
<section class="setup-widget absence-widget"><div class="widget-heading"><span x-text="t('absences_0f9ec2')"></span><span class="badge" x-text="doc.absences.length"></span></div><div class="widget-body"><button class="text" x-show="can('edit_schedules') || can('request_overrides') || can('force_overrides')" @click="open('absence')" x-text="t('absence_0e0616')"></button>
<div class="widget-scroll">
<p class="muted" x-show="!doc.absences.length"><span x-text="t('no_absences_planned_224ab7')"></span></p>
<template x-for="a in doc.absences" :key="a.id">
<div class="list-row">
<div>
<strong x-text="person(a.user_id)">
</strong>
<small x-text="formatDate(a.start)+' — '+formatDate(a.end)+t('inclusive_75b495')">
</small>
</div>
<button class="text danger" x-show="can('edit_schedules')" @click="removeItem('absences',a.id)"><span x-text="t('remove_e96390')"></span></button>
</div>
</template></div></div></section>
<section id="public-holidays" class="setup-widget holiday-widget"><div class="widget-heading"><span x-text="t('public_holidays_badf9f')"></span><span class="badge" x-text="doc.holidays.length"></span></div>
<div class="widget-body"><div class="actions" x-show="can('edit_schedules')"><button @click="open('holiday-import')" x-text="t('import_csv_eeec98')"></button><button class="text" @click="open('holiday')" x-text="t('holiday_85f1a0')"></button></div><div class="widget-scroll"><p class="muted" x-show="!doc.holidays.length"><span x-text="t('no_holidays_yet_import_dates_for_the_countries_your__c8ed79')"></span></p>
<template x-for="group in holidayGroups" :key="group.country">
<details class="holiday-group">
<summary>
<span x-text="countryName(group.country)">
</span>
<span class="badge" x-text="group.holidays.length" :aria-label="group.holidays.length+t('holidays_9db6c2')">
</span>
</summary>
<template x-for="h in group.holidays" :key="h.date">
<div class="list-row">
<div class="grow">
<strong x-text="h.name">
</strong>
<small x-text="formatDate(h.date)+t('country_calendar_date_cc65d7')">
</small>
</div>
<button class="text danger" x-show="can('edit_schedules')" @click="removeItem('holidays',h.country+':'+h.date)"><span x-text="t('remove_e96390')"></span></button>
</div>
</template>
</details>
</template></div></div></section>
</div></section>
</div>
<div class="page-validation" x-data="{validationScope:'generation'}"><!-- VALIDATION_STATUS --></div><div class="generation-review-bar"><!-- GENERATION_PREVIEW --><div class="actions review-actions"><button @click="loadGenerationDraft(true)"><span x-text="t('reload_saved_state_1b1cb9')"></span></button><button class="primary" @click="applyGeneration()" :disabled="busy || !generationCanApply()" x-show="can('edit_schedules')"><span x-text="t('apply_checked_changes_efa2a4')"></span></button></div></div>
</div></template>
</section>
+8
View File
@@ -0,0 +1,8 @@
<details class="generation-preview generation-review compact-preview" aria-live="polite">
<summary><strong x-text="t(modal?'server_preview_09022a':'setup_review_changes')"></strong><span class="preview-totals" x-show="generationResult"><span class="preview-status status-create" x-text="t('preview_created',{count:generationCount('create')})"></span><span class="preview-status status-update" x-text="t('preview_updated',{count:generationCount('update')})"></span><span class="preview-status status-remove" x-text="t('preview_removed',{count:generationCount('remove')})"></span></span><small x-show="generationLoading" x-text="t('cover_checking')"></small></summary>
<div class="compact-preview-body"><template x-if="generationResult"><div>
<p class="validation-warning" x-show="generationResult.cancelled_requests" x-text="generationResult.cancelled_requests+t('pending_requests_will_be_cancelled_because_their_sch_ea9b18')"></p>
<div class="compact-change-list"><template x-for="change in generationVisibleChanges()" :key="change.id"><div class="compact-change"><span class="badge preview-status" :class="'status-'+change.action" x-text="label(change.action)"></span><div><strong x-text="change.name"></strong><small x-text="label(change.kind)+(change.source?' · '+change.source:'')"></small></div></div></template></div>
<div class="preview-list-footer"><small class="preview-status status-unchanged" x-text="t('preview_unchanged',{count:generationCount('unchanged')})"></small><button class="text preview-list-toggle" type="button" x-show="generationChanges().length>generationVisibleChanges().length || generationAll" @click="generationAll=!generationAll" x-text="generationAll?t('show_fewer_resources_3953fb'):t('show_all_382ebd')+generationChanges().length+t('resources_015def')"></button></div>
</div></template></div>
</details>
+249
View File
@@ -0,0 +1,249 @@
// Visual data and binding editors. Only typed data is sent to the server; no user code is evaluated.
function generationUi(){return {
...generationMapUi(),
generationDraft:null,generationWorkspace:'',generationResult:null,generationError:'',generationLoading:false,generationSequence:0,bindingEditor:null,bulkNode:null,bulkText:'',generationAll:false,dataAdvanced:false,dataExpanded:{},dataPersonTarget:null,dataPersonQuery:'',ruleAdvanced:false,rulePeopleTarget:null,rulePeopleQuery:'',generationIssuesOpen:false,editorScroll:0,editorOrigin:null,
nodeKinds:['text','person','number','boolean','date','time','duration','list','dictionary'],
newNode(kind='text',value=null,key=''){return {id:crypto.randomUUID(),kind,key,value:value??(kind==='boolean'?false:['number','duration'].includes(kind)?0:kind==='time'?'11:00':kind==='date'?this.today():''),entries:[]};},
literal(value){return {kind:'literal',value,path:[],prefix:'',suffix:''};},
loadGenerationDraft(reset=false){
if(!this.workspace)return;
const clean=this.generationDraft && JSON.stringify(this.generationDraft.variables)===JSON.stringify(this.doc.generation.variables||[]) && JSON.stringify(this.generationDraft.rules)===JSON.stringify(this.doc.generation?.rules||[]) && this.generationDraft.route_position===(this.doc.generation?.route_position||0);
if(reset || !this.generationDraft || this.generationWorkspace!==this.workspace.id || (clean && this.generationDraft.revision!==this.workspace.revision)){
this.generationWorkspace=this.workspace.id;this.generationDraft={revision:this.workspace.revision,variables:copy(this.doc.generation.variables||[]),rules:copy(this.doc.generation?.rules||[]),route_position:this.doc.generation?.route_position||0};
this.generationResult=null;this.generationAll=false;
}
if(this.can('edit_schedules'))this.generationChanged();
},
generationPayload(){
if(!this.generationDraft)return null;
const payload=copy(this.generationDraft);
if(this.modal==='variable'){const at=payload.variables.findIndex(v=>v.id===this.form.id);at<0?payload.variables.push(copy(this.form)):payload.variables.splice(at,1,copy(this.form));}
if(this.modal==='generation-rule'){const at=payload.rules.findIndex(v=>v.id===this.form.id);at<0?payload.rules.push(copy(this.form)):payload.rules.splice(at,1,copy(this.form));}
const normalize=value=>{if(!value || typeof value!=='object')return;for(const [key,child] of Object.entries(value)){if(['variable','iteration','output'].includes(key)&&child==='')value[key]=null;else normalize(child);}};normalize(payload);return payload;
},
generationChanged(){this.generationSequence++;this.generationResult=null;this.generationError='';this.generationLoading=true;clearTimeout(this.generationTimer);this.generationTimer=setTimeout(()=>this.previewGeneration(),300);},
async previewGeneration(){
const sequence=++this.generationSequence,payload=this.generationPayload();
if(!payload || !this.can('edit_schedules')){this.generationLoading=false;return;}
try{const result=await this.api(this.endpoint('/generation-preview'),'POST',payload);if(sequence===this.generationSequence)this.generationResult={...result,key:JSON.stringify(payload)};}
catch(e){if(sequence===this.generationSequence)this.generationError=e.message;}
finally{if(sequence===this.generationSequence)this.generationLoading=false;}
},
validationState(scope='form'){
let loading=false,checked=false,issues=[];
const messages=items=>(items||[]).map(message=>({message}));
if(scope==='generation' || ['variable','generation-rule'].includes(this.modal)){loading=this.generationLoading;checked=!!this.generationResult;issues=[...(this.generationResult?.issues||[]),...messages(this.generationError?[this.generationError]:[])];}
else if(this.modal==='absence'){loading=this.absenceLoading;checked=!!this.absenceResult;issues=messages([...(this.absenceResult?.plan.errors||[]),...(this.absenceError?[this.absenceError]:[])]);}
else if(this.modal==='holiday-import'){loading=this.csvLoading;checked=!!this.csvResult;issues=messages([...(this.csvResult?.preview.errors||[]),...(this.csvError?[this.csvError]:[])]);}
else if(this.modal==='request'){loading=this.requestLoading;checked=!!this.requestResult;issues=messages(this.requestError?[this.requestError]:[]);}
if(scope==='form' && this.error && !issues.some(i=>i.message===this.error))issues.push({message:this.error});
return {loading,checked,issues,label:loading?tr('cover_checking'):issues.length?tr('diagnostics_count',{count:issues.length}):checked?tr('generation_checked'):tr('validation_not_checked')};
},
issueActions(issue){
const payload=this.generationPayload(),actions=[];if(!payload)return actions;
const rule=payload.rules.find(r=>r.id===issue.rule_id);
if(rule)actions.push({kind:'rule',id:rule.id,block:issue.block_id,name:rule.name});
const variables=new Set(issue.variable_id?[issue.variable_id]:[]);
const scan=value=>{if(!value || typeof value!=='object')return;if(value.variable)variables.add(value.variable);Object.values(value).forEach(scan);};
if(rule){scan(rule.blocks.find(b=>b.id===issue.block_id));scan(rule.iterations);}
for(const id of variables){const variable=payload.variables.find(v=>v.id===id);if(variable)actions.push({kind:'variable',id,name:variable.name});}
return actions;
},
async openIssue(action){
// Switch editors only after unmounting the old form; preserve the local draft first.
const app=Alpine.$data(document.body);
app.generationDraft=app.generationPayload();
const item=(action.kind==='rule'?app.generationDraft.rules:app.generationDraft.variables).find(v=>v.id===action.id);
if(!item)return;
app.modal='';app.bindingEditor=null;app.rulePeopleTarget=null;
await app.$nextTick();
action.kind==='rule'?app.editGenerationRule(item):app.editVariable(item);
await app.$nextTick();
requestAnimationFrame(()=>{const target=action.block?document.querySelector('[role="dialog"] [data-block-id="'+action.block+'"]'):null;if(target)target.scrollIntoView({block:'start'});});
},
generationCanApply(){return !this.generationLoading && this.generationResult && !this.generationResult.issues.length && this.generationResult.key===JSON.stringify(this.generationPayload());},
generationChanges(){return this.generationResult?.changes||[];},
generationVisibleChanges(){const changes=this.generationChanges();return this.generationAll?changes:changes.filter(c=>c.action!=='unchanged').slice(0,8);},
generationCount(action){return this.generationChanges().filter(c=>c.action===action).length;},
async applyGeneration(){if(!this.generationCanApply())return;await this.run(async()=>{await this.api(this.endpoint('/generation'),'POST',{...this.generationPayload(),review_id:this.generationResult.review_id});await this.loadWorkspaces();this.loadGenerationDraft(true);},tr('workspace_data_and_generated_schedules_saved_publica_e4ae3d'));},
generated(id){return (this.doc?.generation?.managed||[]).some(m=>m.id===id);},
moveGenerationRule(index,delta){const list=this.generationDraft.rules,to=index+delta;if(to<0||to>=list.length)return;[list[index],list[to]]=[list[to],list[index]];},
sourceRuleName(id){const owner=this.doc?.generation?.managed?.find(m=>m.id===id);return this.doc?.generation?.rules?.find(r=>r.id===owner?.rule)?.name || this.generationDraft?.rules?.find(r=>r.id===owner?.rule)?.name || tr('generated_resource');},
ruleOverview(rule){
const parts=[tr(rule.enabled?'enabled_df174a':'disabled_f4f447')];
if(rule.iterations.length)parts.push(tr('rule_iterations_short',{count:rule.iterations.length})+' ('+rule.iterations.map(i=>'{ '+this.bindingAddress(i.source,0,rule.iterations)+' }').join(', ')+')');
const kinds=new Map();for(const block of rule.blocks)kinds.set(block.kind,(kinds.get(block.kind)||0)+1);
parts.push(tr('rule_blocks_short',{count:rule.blocks.length})+' ('+[...kinds].map(([kind,count])=>(['rotation','priority'].includes(kind)?tr('schedule_type_named',{name:this.label(kind)}):this.label(kind))+(count>1?' ×'+count:'')).join(', ')+')');
return parts.join(' · ');
},
resourceIcon(kind){
return ({schedule:'M5 5h14v16H5z M8 3v4 M16 3v4 M5 10h14 M8 14h2 M14 14h2 M8 17h2',rotation:'M20 7v5h-5 M4 17v-5h5 M6 7a7 7 0 0 1 12-1l2 3 M4 15l2 3a7 7 0 0 0 12-1',person:'M16 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0 M4 21v-2a8 8 0 0 1 16 0v2',clock:'M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0 M12 7v5l3 2',fallback:'M5 4v9a4 4 0 0 0 4 4h10 M15 13l4 4-4 4',escalation:'M6 20v-5h6v-5h6V4 M14 8l4-4 4 4',route:'M5 4v12a4 4 0 0 0 4 4h10 M5 8h14 M15 4l4 4-4 4 M15 16l4 4-4 4',condition:'M4 6h16 M7 12h10 M10 18h4',end:'M5 12h14 M12 5l7 7-7 7'})[kind] || 'M12 4v16 M4 12h16';
},
resourceBrief(resource,kind,document=this.doc){
const person=id=>document.people?.find(p=>p.id===id)?.display_name || this.person(id);
const scheduleName=id=>document.schedules.find(s=>s.id===id)?.name || this.scheduleName(id);
const recipient=r=>r.type==='schedule'?scheduleName(r.id):r.type==='user'?person(r.id):this.describeRecipient(r);
if(kind==='schedule'){
const people=resource.participants.slice(0,3).map(id=>person(id)).join(', ')+(resource.participants.length>3?'…':'');
return resource.type==='rotation'?tr('rotation_brief',{days:resource.shift_days,time:resource.handover.slice(0,5),zone:document.timezone})+' · '+people:this.label(resource.type)+' · '+people+' · '+tr('fallback_brief',{name:scheduleName(resource.fallback)});
}
if(kind==='escalation')return resource.steps.map(step=>tr('escalation_step_brief',{minutes:step.delay_minutes,recipient:recipient(step.recipient)})).join(' → ');
const conditions=resource.conditions.length?resource.conditions.map(c=>this.conditionText(c)).join(' · '):tr('any_alert_4496c3');
return conditions+(resource.window?' · '+tr('workspace_working_hours_b8ec6f'):'')+' → '+(document.escalations.find(p=>p.id===resource.escalation_id)?.name || tr('do_not_notify_1a59a6'));
},
editGenerated(id){this.view='configuration';this.loadGenerationDraft();const owner=this.doc.generation.managed.find(m=>m.id===id);const rule=this.generationDraft.rules.find(r=>r.id===owner?.rule);if(rule)this.editGenerationRule(rule);},
editVariable(variable=null){this.loadGenerationDraft();this.open('variable',variable || {id:crypto.randomUUID(),name:'',node:this.newNode('list')});this.bindingEditor=null;this.bulkNode=null;this.dataAdvanced=!variable;this.dataExpanded={[this.form.node.id]:true};this.dataPersonTarget=null;this.generationChanged();},
simpleData(node){return node.kind==='list' && node.entries.every(n=>['text','person'].includes(n.kind)) || ['dictionary'].includes(node.kind) && node.entries.every(n=>n.kind==='list' && n.entries.every(p=>p.kind==='person'));},
dataPersonName(value){return this.people.find(p=>p.id===value)?.display_name || value || tr('choose_person_f20051');},
editDataPeople(node,entry=null){this.dataPersonTarget=node;this.dataPersonEntry=entry;this.dataPersonQuery='';},
addDataPerson(value){if(!value.trim())return;if(this.dataPersonEntry){this.dataPersonEntry.value=value.trim();this.dataPersonTarget=null;this.dataPersonEntry=null;}else this.dataPersonTarget.entries.push(this.newNode('person',value.trim()));this.dataPersonQuery='';},
addDataGroup(){this.form.node.entries.push(this.newNode('list',null,''));},
editGenerationRule(rule=null){this.loadGenerationDraft();this.open('generation-rule',rule || {id:crypto.randomUUID(),name:'',enabled:true,iterations:[],when:null,blocks:[]});for(const block of this.form.blocks)if(block.kind==='escalation')for(const key of ['repeat_count','repeat_wait'])if(!Object.hasOwn(block.fields,key))block.fields[key]=this.literal(0);this.bindingEditor=null;this.ruleAdvanced=false;this.rulePeopleTarget=null;this.generationIssuesOpen=false;this.generationChanged();},
manualResources(type){return (type==='schedule'?this.doc.schedules:this.doc.escalations).filter(resource=>!this.generated(resource.id));},
defaultScheduleSource(){const output=this.matchingOutputs('schedule')[0];return output?{kind:'output',output:output.id,path:[]}:this.literal(this.manualResources('schedule')[0]?.id||'');},
sourceOptions(type){const options=[];for(const v of this.generationDraft.variables){if(type==='list' && ['dictionary'].includes(v.node.kind)){for(const i of this.form.iterations)options.push({id:'lookup:'+v.id+':'+i.id,name:tr('owners_by_project',{name:v.name}),kind:'lookup',variable:v.id,iteration:i.id});}if(type==='collection'?['list','dictionary'].includes(v.node.kind):type==='list'?v.node.kind==='list':type==='person'?v.node.kind==='person':type==='number'?['number','duration'].includes(v.node.kind):v.node.kind===type)options.push({id:'variable:'+v.id,name:v.name,kind:'variable',variable:v.id});}if(type==='text')for(const i of this.form.iterations)options.push({id:'item:'+i.id,name:tr('each_project_name',{name:i.name}),kind:'item',iteration:i.id});if(['schedule','policy'].includes(type)){for(const b of this.matchingOutputs(type))options.push({id:'output:'+b.id,name:this.generatedResourceLabel(b)+' · '+this.bindingSummary(b.fields.name),kind:'output',output:b.id});for(const r of this.manualResources(type))options.push({id:'saved:'+r.id,name:tr(type==='schedule'?'source_saved_resource':'source_saved_policy',{name:r.name}),kind:'literal',value:r.id});}return options.map(option=>{if(['variable','item','key','lookup'].includes(option.kind)){const binding={...option};if(option.kind==='lookup'){const loop=this.form.iterations.find(i=>i.id===option.iteration),variable=this.generationDraft.variables.find(v=>v.id===loop?.source.variable);binding.key={kind:variable?.node.kind==='list'?'item':'key',iteration:option.iteration};}option.name=this.bindingSummary(binding);}return option;});},
simpleSource(binding,type){if(!binding)return '';if(binding.path?.length)return 'advanced';const option=this.sourceOptions(type).find(o=>o.kind===binding.kind && (o.kind==='lookup'?binding.variable===o.variable && binding.key?.iteration===o.iteration && !binding.key.path?.length:o.kind==='literal'?o.value===binding.value:o.kind==='variable'?o.variable===binding.variable:o.kind==='output'?o.output===binding.output:o.iteration===binding.iteration));return option?.id || (binding.kind==='literal'?'literal':'advanced');},
selectSimpleSource(binding,id,type){if(id==='advanced'){this.editBinding(binding,type,tr('value_source_c0ae15'));return;}const option=this.sourceOptions(type).find(o=>o.id===id);let next=option?{kind:option.kind,path:[],...(option.variable?{variable:option.variable}:{}),...(option.output?{output:option.output}:{}),...(option.iteration && option.kind!=='lookup'?{iteration:option.iteration}:{}),...(option.kind==='literal'?{value:option.value}:{})}:this.literal(type==='list'?[]:type==='number'?0:type==='boolean'?false:'');if(option?.kind==='lookup'){const loop=this.form.iterations.find(i=>i.id===option.iteration);const variable=this.generationDraft.variables.find(v=>v.id===loop?.source.variable);next.key={kind:variable?.node.kind==='list'?'item':'key',iteration:option.iteration,path:[]};}for(const key of Object.keys(binding))delete binding[key];Object.assign(binding,next);},
addRulePerson(value){if(!value || !this.rulePeopleTarget)return;const list=this.rulePeopleTarget.value;if(!list.includes(value))list.push(value);this.rulePeopleQuery='';},
sampleNode(node){if(node.kind==='list')return node.entries.map(n=>this.sampleNode(n));if(['dictionary'].includes(node.kind))return Object.fromEntries(node.entries.map(n=>[n.key,this.sampleNode(n)]));return node.value;},
sampleBinding(binding,context){if(!binding)return '';const variable=id=>{const v=this.generationDraft.variables.find(v=>v.id===id);return v?this.sampleNode(v.node):undefined;};let value;if(binding.kind==='list')value=binding.items.map(item=>this.sampleBinding(item,context));else if(binding.kind==='literal')value=binding.value;else if(binding.kind==='variable')value=variable(binding.variable);else if(binding.kind==='item')value=context[binding.iteration]?.value;else if(binding.kind==='key')value=context[binding.iteration]?.key;else if(binding.kind==='lookup')value=variable(binding.variable)?.[this.sampleBinding(binding.key,context)];else if(binding.kind==='output'){const block=this.matchingOutputs('all').find(b=>b.id===binding.output);return block?this.sampleBinding(block.fields.name,context):'';}for(const key of binding.path||[])value=value?.[Array.isArray(value)?Number(key)-1:key];if(value===undefined)throw new Error(tr('sample_missing'));return binding.prefix || binding.suffix?(binding.prefix||'')+value+(binding.suffix||''):value;},
// Presentation only: build a scoped reference index once, then walk nested values.
previewValue(value){
const references=new Map();
const formatters={
person:p=>({type:'person',name:p.display_name,...(p.username?{username:p.username}:{}),...(p.email?{email:p.email}:{}),id:p.id}),
schedule:s=>({type:'schedule',name:s.name,kind:s.type,id:s.id}),
escalation:p=>({type:'escalation',name:p.name,id:p.id}),
route:r=>({type:'route',name:r.name,id:r.id}),
};
for(const [kind,items] of [['person',this.people],['schedule',this.doc?.schedules||[]],['escalation',this.doc?.escalations||[]],['route',this.doc?.routes||[]]]){
for(const item of items){const brief=formatters[kind](item);references.set(item.id,brief);if(kind==='person' && item.email)references.set(item.email,brief);}
}
const visit=(item,depth=0)=>{
if(depth>32)return '…';
if(typeof item==='string')return references.get(item)||item;
if(Array.isArray(item))return item.map(child=>visit(child,depth+1));
if(item && typeof item==='object')return Object.fromEntries(Object.entries(item).map(([key,child])=>[key,visit(child,depth+1)]));
return item;
};
return visit(value);
},
bindingValuePreview(){
const binding=this.bindingEditor?.binding;
if(!binding)return {text:'',context:'',error:''};
try{
const context={},names=[];
// Preview one row only; unrelated incomplete loops must not hide literal values.
for(const iteration of this.form.iterations||[]){
try{const source=this.sampleBinding(iteration.source,context);if(!source || typeof source!=='object')break;const entry=Object.entries(source)[0];if(!entry)break;const [key,value]=entry;context[iteration.id]={key:Array.isArray(source)?'':key,value};names.push(iteration.name+': '+(typeof value==='string'?value:key));}catch{break;}
}
let value;
if(binding.kind==='output'){
const block=this.matchingOutputs('all').find(b=>b.id===binding.output);
if(!block)throw new Error(tr('sample_missing'));
value={type:block.kind,name:this.sampleBinding(block.fields.name,context),block_id:block.id};
}else value=this.sampleBinding(binding,context);
if(value===undefined)throw new Error(tr('sample_missing'));
const text=JSON.stringify(this.previewValue(value),null,2);
return {text:text.length>20000?text.slice(0,20000)+'\n…':text,context:binding.kind==='output'?tr('binding_preview_resource'):names.length?tr('binding_preview_row',{row:names.join(' · ')}):tr('binding_preview_hint'),error:''};
}catch(error){return {text:'',context:'',error:error.message||tr('sample_missing')};}
},
ruleResultDocument(){return this.generationResult?.result;},
ruleResultResources(){const doc=this.ruleResultDocument();if(!doc)return [];return doc.managed.filter(m=>m.rule===this.form.id).flatMap(m=>{const kind=['rotation','priority'].includes(m.kind)?'schedule':m.kind;const resource=(kind==='schedule'?doc.schedules:kind==='escalation'?doc.escalations:doc.routes).find(r=>r.id===m.id);return resource?[{...m,kind,resource}]:[];});},
ruleResultStatus(){if(this.generationLoading)return tr('cover_checking');if(this.generationError || this.generationResult?.issues.length)return tr('rule_result_invalid');if(!this.generationResult)return tr('rule_result_waiting');if(!this.form.enabled)return tr('rule_result_disabled');const count=this.ruleResultResources().length;return count?tr('rule_result_count',{count}):tr('rule_result_empty');},
saveGenerationModal(){
if(!this.form.name.trim()){this.error=tr('enter_a_name_86c810');return;}
// An incomplete cross-reference may remain in the local draft, but activation is always blocked.
const collection=this.modal==='variable'?'variables':'rules',list=this.generationDraft[collection],at=list.findIndex(v=>v.id===this.form.id);
at<0?list.push(copy(this.form)):list.splice(at,1,copy(this.form));this.modal='';this.bindingEditor=null;this.generationChanged();
},
removeGenerationItem(collection,id){this.generationDraft[collection]=this.generationDraft[collection].filter(v=>v.id!==id);},
nodeRows(root){const rows=[];const visit=(node,parent,depth,index)=>{rows.push({node,parent,depth,index});if(depth<9 && this.dataExpanded[node.id])node.entries.forEach((child,i)=>visit(child,node,depth+1,i));};visit(root,null,0,0);return rows;},
setNodeKind(node,kind){const previous=node.kind;node.kind=kind;node.entries=[];if(['list','dictionary'].includes(kind))node.value=null;else if(kind==='boolean')node.value=false;else if(['number','duration'].includes(kind))node.value=0;else if(kind==='date')node.value=this.today();else if(kind==='time')node.value='11:00';else if(typeof node.value!=='string' || ['date','time'].includes(previous))node.value='';},
toggleDataNode(node){this.dataExpanded[node.id]=!this.dataExpanded[node.id];},
dataContainer(node){return ['list','dictionary'].includes(node.kind);},
dataCaption(node){if(node.kind==='dictionary'){const lists=node.entries.filter(n=>n.kind==='list');return lists.map(n=>this.nodeSummary(n)).join(' · ');}return this.nodeSummary(node);},
duplicateDataNode(parent,index){const renew=n=>{n.id=crypto.randomUUID();n.entries.forEach(renew);};const n=copy(parent.entries[index]);renew(n);parent.entries.splice(index+1,0,n);this.dataExpanded[parent.id]=true;},
addNode(parent){this.dataExpanded[parent.id]=true;const kind=parent.entries[0]?.kind||'text';parent.entries.push(this.newNode(kind));},
moveNode(parent,index,delta){const to=index+delta;if(to<0 || to>=parent.entries.length)return;[parent.entries[index],parent.entries[to]]=[parent.entries[to],parent.entries[index]];},
nodeSummary(node,depth=0){
const entries=node.entries||[];
if(node.kind==='list')return depth>=2?entries.length+tr('entries_7c3fe9'):entries.slice(0,4).map(n=>this.nodeSummary(n,depth+1)).join(', ')+(entries.length>4?'…':'');
if(node.kind==='dictionary'){
const name=['name','title'].map(key=>entries.find(n=>n.key===key && n.kind==='text' && String(n.value??'').trim())).find(Boolean);
return name?String(name.value):entries.length+tr('entries_7c3fe9')+(entries.length?' · '+entries.slice(0,3).map(n=>n.key).join(', ')+(entries.length>3?'…':''):'');
}
return node.kind==='person'?this.dataPersonName(node.value):node.kind==='date'?this.formatDate(node.value):String(node.value??'');
},
appendBulk(){
const lines=this.bulkText.split(/\r?\n/).map(s=>s.trim()).filter(Boolean),target=this.bulkNode;
if(!target || !lines.length)return;
if(target.kind==='list')target.entries.push(...lines.map(s=>this.newNode(target.entries[0]?.kind||'text',s)));
else {
const parsed=lines.map((line,i)=>{const cells=line.split('\t');if(cells.length!==2 || !cells[0].trim() || !cells[1].trim())throw new Error(tr('line_2b754a')+(i+1)+tr('paste_a_project_key_a_tab_and_comma_separated_emails_2b81fc'));const n=this.newNode('list',null,cells[0].trim());n.entries=cells[1].split(',').map(s=>this.newNode('person',s.trim()));return n;});target.entries.push(...parsed);
}
this.bulkNode=null;this.bulkText='';
},
openBulk(node){this.bulkNode=node;this.bulkText='';},
addIteration(){if(this.form.iterations.length>=4)return;const v=this.generationDraft.variables.find(v=>['list','dictionary'].includes(v.node.kind));this.form.iterations.push({id:crypto.randomUUID(),name:tr('item_284e12')+(this.form.iterations.length+1),source:{kind:'variable',variable:v?.id||null,path:[]}});},
fieldsFor(kind){
const common=[{key:'name',label:tr('name_709a23'),type:'text'}],handover={key:'handover',label:tr('handover_time_c9e315'),type:'time'};
if(kind==='rotation')return [...common,{key:'participants',label:tr('participants_in_rotation_order_4edee0'),type:'list'},{key:'anchor',label:tr('anchor_date_e77bd6'),type:'date'},{key:'shift_days',label:tr('shift_length_days_0b28fa'),type:'number'},handover];
if(kind==='priority')return [...common,{key:'participants',label:tr('responsible_people_in_priority_order_7205a5'),type:'list'},{key:'fallback',label:tr('fallback_schedule_7b7a21'),type:'schedule'},handover];
if(kind==='escalation')return [...common,{key:'repeat_count',label:tr('repeat_count_912e08'),type:'number'},{key:'repeat_wait',label:tr('minutes_between_repeats_f17ab0'),type:'number'}];
return [...common,{key:'escalation',label:tr('escalation_policy_bbb0f7'),type:'policy'},{key:'working',label:tr('only_during_working_hours_fa1eb6'),type:'boolean'}];
},
newBlock(kind){
const fields={};for(const field of this.fieldsFor(kind)){const val=field.key==='name'?tr('new_61bd57')+this.label(kind):field.key==='anchor'?this.today():field.key==='handover'?'11:00':field.key==='shift_days'?7:field.type==='number'?0:field.type==='boolean'?false:field.type==='list'?[this.user.id]:field.type==='person'?this.user.id:'';fields[field.key]=this.literal(val);}
return {id:crypto.randomUUID(),kind,fields,steps:kind==='escalation'?[{delay:this.literal(0),recipient:this.defaultScheduleSource(),user:false,condition:'if-not-acked'}]:[],conditions:[]};
},
addBlock(kind){this.form.blocks.push(this.newBlock(kind));this.$nextTick(()=>this.$refs.dialog.querySelector('.generation-block:last-of-type')?.scrollIntoView({block:'center',behavior:'smooth'}));},
moveBlock(index,delta){const to=index+delta;if(to<0 || to>=this.form.blocks.length)return;[this.form.blocks[index],this.form.blocks[to]]=[this.form.blocks[to],this.form.blocks[index]];},
addGenStep(block){block.steps.push({delay:this.literal(10),recipient:this.defaultScheduleSource(),user:false,condition:'if-not-acked'});},
addGenCondition(block){block.conditions.push({field:'tags',operation:'contains',value:this.literal(''),negate:false});},
templatePath(base,key){return /^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(key)?base+'.'+key:base+'['+JSON.stringify(key)+']';},
bindingAddress(b,depth=0,iterations=this.form.iterations||[]){
if(!b || depth>16)return '?';
const variable=this.generationDraft?.variables.find(v=>v.id===b.variable)?.name;
const iteration=iterations.find(i=>i.id===b.iteration)?.name;
let address='?';
if(b.kind==='literal')address=JSON.stringify(b.value??null);
if(b.kind==='variable' || b.kind==='lookup')address=variable?this.templatePath('var',variable):'var[?]';
if(b.kind==='lookup')address+='['+this.bindingAddress(b.key,depth+1,iterations)+']';
if(['item','key'].includes(b.kind)){const name=iteration?(/^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(iteration)?iteration:'iteration['+JSON.stringify(iteration)+']'):'iteration[?]';address=name+(b.kind==='key'?'.key':'.value');}
if(b.kind==='list')address='['+(b.items||[]).map(item=>this.bindingAddress(item,depth+1,iterations)).join(', ')+']';
for(const key of b.path||[])address=/^[1-9][0-9]*$/.test(key)?address+'['+key+']':this.templatePath(address,key);
return address;
},
bindingSummary(b,type='',depth=0){
if(!b || depth>16)return tr('choose_a_value_e0b399');
if(b.kind==='output'){const resource=this.matchingOutputs('all').find(o=>o.id===b.output);return this.generatedResourceLabel(resource)+' · '+(resource?this.bindingSummary(resource.fields.name,'text',depth+1):tr('choose_an_output_747632'));}
let text;
if(b.kind==='literal' && !b.path?.length){if(type==='date')text=this.formatDate(b.value);else if(Array.isArray(b.value))text=b.value.map(v=>this.people.some(p=>p.id===v)?this.person(v):v).join(', ');else text=this.people.some(p=>p.id===b.value)?this.person(b.value):String(b.value??tr('choose_a_value_e0b399'));}
else text='{ '+this.bindingAddress(b)+' }';
return (b.prefix||'')+text+(b.suffix||'');
},
editBinding(binding,type,label){this.rememberEditorOrigin();this.bindingEditor={binding,type,label};this.$nextTick(()=>{const popup=document.querySelector('.binding-popover');if(!popup || !this.bindingEditor)return;popup.showPopover();this.positionBindingPopover();popup.querySelector('select')?.focus({preventScroll:true});});},
positionBindingPopover(){const popup=document.querySelector('.binding-popover');if(!popup || !this.bindingEditor)return;const rect=this.editorOrigin?.getBoundingClientRect()||{left:16,right:16,top:80,bottom:100},width=Math.min(390,innerWidth-24),height=Math.min(560,innerHeight-32);popup.style.width=width+'px';popup.style.maxHeight=height+'px';const right=rect.right+10,left=rect.left-width-10;popup.style.left=Math.max(12,Math.min(right+width<=innerWidth-12?right:left>=12?left:innerWidth-width-12,innerWidth-width-12))+'px';popup.style.top=Math.max(12,Math.min(rect.top,innerHeight-Math.min(popup.scrollHeight,height)-16))+'px';},
rememberEditorOrigin(){this.editorScroll=this.$refs.dialog.scrollTop;this.editorOrigin=document.activeElement;},
closeRuleEditor(restoreFocus=true){
if(this.bindingEditor){const popup=document.querySelector('.binding-popover');if(popup?.matches(':popover-open'))popup.hidePopover();this.bindingEditor=null;if(restoreFocus)this.editorOrigin?.focus({preventScroll:true});return;}
const dialog=this.$refs.dialog,scroll=this.editorScroll,origin=this.editorOrigin,formId=this.form.id;
this.bindingEditor=null;this.rulePeopleTarget=null;
// Alpine restores x-show sections on the next frame; scrolling before layout clamps to the shorter editor.
this.$nextTick(()=>requestAnimationFrame(()=>requestAnimationFrame(()=>{
if(!dialog?.isConnected || this.modal!=='generation-rule' || this.form.id!==formId || this.bindingEditor || this.rulePeopleTarget)return;
if(origin?.isConnected)origin.focus({preventScroll:true});
dialog.scrollTop=scroll;
})));
},
openRulePeople(binding){this.rememberEditorOrigin();this.rulePeopleTarget=binding;this.rulePeopleQuery='';this.$nextTick(()=>{const dialog=this.$refs.dialog;if(!dialog || !this.rulePeopleTarget)return;dialog.scrollTop=0;dialog.querySelector('.binding-editor input')?.focus({preventScroll:true});});},
bindingRows(){const rows=[];const walk=(binding,type,label,level=0,parent=null,index=0)=>{if(!binding || level>15)return;rows.push({binding,type,label,parent,index,depth:rows.length});if(binding.kind==='lookup')walk(binding.key,'text',tr('dictionary_key_source_b4a40b'),level+1);if(binding.kind==='list')binding.items.forEach((item,index)=>walk(item,'person',String(index+1),level+1,binding,index));};walk(this.bindingEditor?.binding,this.bindingEditor?.type,tr('value_source_c0ae15'));return rows;},
changeBindingKind(binding,kind){binding.kind=kind;binding.path=[];if(kind==='lookup' && !binding.key)binding.key={kind:'item',iteration:this.form.iterations[0]?.id||null,path:[]};if(kind==='literal' && binding.value===undefined)binding.value='';},
generatedResourceLabel(block){return tr(block?.kind==='escalation'?'source_generated_policy':['rotation','priority'].includes(block?.kind)?'source_generated_schedule':'source_generated_resource');},
matchingOutputs(type){return [...(this.form.blocks||[]),...(this.generationDraft?.rules||[]).filter(r=>r.id!==this.form.id && r.enabled && !r.iterations.length).flatMap(r=>r.blocks)].filter(b=>type==='policy'?b.kind==='escalation':type==='schedule'?['rotation','priority'].includes(b.kind):true);},
toggleGenCondition(enabled){this.form.when=enabled?{left:this.literal(''),operation:'equals',right:this.literal('')}:null;},
};}
+35
View File
@@ -0,0 +1,35 @@
// The complete, versioned catalog is embedded before this script by the server.
// Only textContent/Alpine x-text is used: parameters are never interpreted as HTML.
const I18n = (() => {
const supported=['en','ru','fr'];
const detect=(languages)=>{for(const raw of languages || []){const base=String(raw).toLowerCase().split(/[-_]/)[0];if(supported.includes(base))return base;}return 'en';};
const browser=detect(navigator.languages?.length?navigator.languages:[navigator.language]);
const locale=()=>globalThis.Alpine?.store('locale')?.language || browser;
const translate=(key,params={},language=locale())=>{
const entry=MEGURI_MESSAGES[key];
if(!entry)throw new Error('Missing localization key: '+key);
const pattern=entry[language] || entry.en;
return pattern.replace(/\{([\w]+)\}/g,(match,name)=>Object.hasOwn(params,name)?String(params[name]):match);
};
const exact=new Map();
for(const [key,entry]of Object.entries(MEGURI_MESSAGES))for(const text of Object.values(entry))if(text)exact.set(text,key);
const patterns=[];
for(const [key,entry]of Object.entries(MEGURI_MESSAGES)){
if(!entry.en.includes('{'))continue;
const names=[];let from=0,expression='^';
for(const match of entry.en.matchAll(/\{(\w+)\}/g)){expression+=entry.en.slice(from,match.index).replace(/[.*+?^${}()|[\]\\]/g,'\\$&')+'([\\s\\S]*?)';names.push(match[1]);from=match.index+match[0].length;}
if(names.length){expression+=entry.en.slice(from).replace(/[.*+?^${}()|[\]\\]/g,'\\$&')+'$';patterns.push({key,names,re:new RegExp(expression)});}
}
const diagnostic=(text)=>{
if(text==null)return '';
text=String(text);const key=exact.get(text);if(key)return translate(key);
for(const item of patterns){const match=item.re.exec(text);if(match)return translate(item.key,Object.fromEntries(item.names.map((name,i)=>[name,match[i+1]])));}
return text; // User values and third-party diagnostics are opaque, never guessed or rewritten.
};
document.addEventListener('alpine:init',()=>{
Alpine.store('locale',{language:browser});
Alpine.effect(()=>{const language=locale();document.documentElement.lang=language;const title=document.querySelector('[data-title-key]');if(title)document.title=translate(title.dataset.titleKey);});
});
return Object.freeze({supported,browser,detect,locale,translate,diagnostic});
})();
function tr(key,params){return I18n.translate(key,params);}
+930
View File
@@ -0,0 +1,930 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title data-title-key="meguri_on_call_together_f7894c"></title>
<link rel="stylesheet" href="/assets/app.css">
<script defer src="/assets/i18n.js"></script>
<script defer src="/assets/cytoscape.js"></script>
<script defer src="/assets/generation-map.js"></script>
<script defer src="/assets/generation.js"></script>
<script defer src="/assets/app.js">
</script>
<script defer src="/assets/alpine.js">
</script>
</head>
<body x-data="meguri" :class="{'demo-mode':demo}">
<div class="demo-banner" x-ref="demoBanner" x-cloak x-show="demo" role="status" :aria-label="t('demo_data_retention_e272aa')"><div><strong><span x-text="t('demo_temporary_data_8112dd')"></span></strong><span x-text="demoLifecycle?t('teams_workspaces_332acb')+ttlText(demoLifecycle.retention.workspaces)+t('requests_0e1805')+ttlText(demoLifecycle.retention.requests)+t('visitor_accounts_f4ac15')+ttlText(demoLifecycle.retention.accounts):''"></span><span><span x-text="t('restart_resets_demo_data_examples_refresh_automatica_0082d1')"></span></span></div><p class="demo-protection" x-show="demoLifecycle?.isolated"><strong><span x-text="t('protected_database_isolated_demo_active_2e4ba8')"></span></strong> <span x-text="t('the_configured_database_was_not_verified_as_demo_and_c1a54e')"></span></p></div>
<datalist id="timezones">
<template x-for="zone in zones">
<option :value="zone">
</option>
</template>
</datalist>
<div class="loading" x-show="!ready"><span x-text="t('loading_meguri_856b9a')"></span></div>
<div class="toast" role="status" x-cloak x-show="toast" x-text="l(toast)">
</div>
<div class="error" role="alert" x-cloak x-show="error">
<span x-text="l(error)">
</span>
<button @click="error=''" :aria-label="t('dismiss_error_347aaa')"><span x-text="t('symbol_67fba2')"></span></button>
</div>
<main class="login" x-cloak x-show="ready && !user">
<div class="login-story">
<a class="brand" href="/"><span x-text="t('symbol_4d82f1')"></span> <span><span x-text="t('meguri_d160e8')"></span></span>
</a>
<div>
<span class="eyebrow"><span x-text="t('on_call_in_good_hands_f1cf2d')"></span></span>
<h1><span x-text="t('a_clear_rhythm_c58fae')"></span><br><span x-text="t('a_shared_responsibility_2c5e48')"></span></h1>
<p><span x-text="t('one_calendar_for_your_team_thoughtful_rotations_simp_52aff3')"></span></p>
</div>
<span class="quiet"><span x-text="t('plan_here_stay_in_sync_everywhere_70e528')"></span></span>
</div>
<form class="login-form" @submit.prevent="signIn">
<span class="eyebrow"><span x-text="t('your_team_s_home_for_on_call_66a895')"></span></span>
<h2 x-text="registering ? t('create_your_account_4046b5') : t('welcome_back_b80783')"></h2>
<p class="muted"><span x-text="t('make_space_for_a_smoother_handover_44adaf')"></span></p>
<label><span x-text="t('username_84c290')"></span><input autocomplete="username" x-model="loginForm.username" required maxlength="64">
</label>
<label x-show="registering"><span x-text="t('display_name_c7874a')"></span><input x-model="loginForm.display_name" :required="registering">
</label>
<label><span x-text="t('password_8be3c9')"></span><input type="password" :autocomplete="registering ? 'new-password' : 'current-password'" x-model="loginForm.password" required :minlength="registering ? 12 : 1">
</label>
<button class="primary" :disabled="busy" x-text="registering ? t('create_account_aaf374') : t('sign_in_ada2e9')">
</button>
<button type="button" class="text" x-show="allowSignup" @click="registering=!registering" x-text="registering ? t('already_registered_sign_in_0df05c') : t('new_here_create_an_account_2b2c3f')">
</button>
<div class="demo-note" x-show="demo && !registering"><strong><span x-text="t('try_the_demo_38cfaf')"></span></strong><dl><div><dt><span x-text="t('alice_522b27')"></span></dt><dd><span x-text="t('lead_both_workspaces_variables_rules_ed9a19')"></span></dd></div><div><dt><span x-text="t('bob_48181a')"></span></dt><dd><span x-text="t('engineering_weekly_on_call_swaps_de49e8')"></span></dd></div><div><dt><span x-text="t('carol_28b92b')"></span></dt><dd><span x-text="t('support_daily_shifts_approvals_c9a0d8')"></span></dd></div><div><dt x-text="t('demo_extra_users')"></dt><dd x-text="t('demo_extra_users_hint')"></dd></div></dl><span><span x-text="t('password_for_all_2f1a67')"></span> <code><span x-text="t('meguri_demo_only_0c7b5d')"></span></code></span></div>
</form>
</main>
<div class="shell" :inert="!!modal" x-cloak x-show="ready && user">
<aside class="sidebar">
<a class="brand" href="/"><span x-text="t('symbol_4d82f1')"></span> <span><span x-text="t('meguri_d160e8')"></span></span>
</a>
<span class="eyebrow workspace-label"><span x-text="t('workspace_703988')"></span></span>
<select class="workspace-select" :aria-label="t('team_218887')" x-model="selected" :disabled="busy" @change="changeWorkspace()">
<option value="" x-show="!directoryState.workspaces.length" x-text="t('choose_a_team_1853ee')"></option>
<template x-for="t in directoryState.workspaces" :key="t.id">
<option :value="t.id" :selected="selected===(t.id)" x-text="t.name">
</option>
</template>
</select>
<nav :aria-label="t('main_navigation_efd197')">
<template x-for="item in nav.filter(n=>['calendar','requests','activity'].includes(n.id))" :key="item.id">
<a :href="menuHref(item.id)" :aria-current="view===item.id?'page':null" :class="{active:view===item.id}" @click="menuClick($event,item.id)">
<span aria-hidden="true" class="nav-icon" x-text="item.icon">
</span>
<span x-text="item.name">
</span>
<span class="counter" x-show="item.id==='requests' && pending.length" x-text="pending.length">
</span>
</a>
</template>
<button class="setup-toggle" :aria-label="t('setup_cdd7bb')" :class="{active:setupView}" @click="setupOpen=!setupOpen" :aria-expanded="setupOpen"><span class="nav-icon"><span x-text="t('symbol_be22a3')"></span></span><span><span x-text="t('setup_cdd7bb')"></span></span><span class="grow"></span><span x-text="setupOpen?'−':'+'"></span></button>
<div class="setup-nav" x-show="setupOpen"><template x-for="item in nav.filter(n=>['configuration','settings'].includes(n.id))" :key="item.id"><a :href="menuHref(item.id)" :aria-current="view===item.id?'page':null" :class="{active:view===item.id}" @click="menuClick($event,item.id)" x-text="item.name"></a></template></div>
</nav>
<div class="sidebar-bottom">
<button class="account-button" @click="open('account')">
<span class="avatar teal" x-text="user?.display_name?.slice(0,1)">
</span>
<span>
<strong x-text="user?.display_name">
</strong>
<small x-text="'@'+user?.username">
</small>
</span>
<span><span x-text="t('symbol_be22a3')"></span></span>
</button>
<button class="text signout" @click="signOut"><span x-text="t('sign_out_dc1649')"></span></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<select class="mobile-workspace-select" :aria-label="t('choose_team_9c6970')" x-model="selected" :disabled="busy" @change="changeWorkspace()">
<template x-for="t in directoryState.workspaces" :key="t.id">
<option :value="t.id" :selected="selected===(t.id)" x-text="t.name">
</option>
</template>
</select>
<span>
<strong x-text="doc?.name || t('getting_started_76e7a3')">
</strong>
</span>
<div class="top-actions">
<span class="display-zone" x-text="t('your_time_31bdbf')+displayZone"></span>
<button class="text" @click="open('workspace')"><span x-text="t('new_workspace_b21b86')"></span></button>
</div>
</header>
<div class="content">
<template x-if="!workspace">
<section class="empty welcome">
<span class="empty-symbol"><span x-text="t('symbol_4d82f1')"></span></span>
<h1><span x-text="t('your_team_s_next_chapter_starts_here_4e986d')"></span></h1>
<p><span x-text="t('create_a_workspace_add_a_team_and_give_your_on_call__af9412')"></span></p>
<button class="primary" @click="open('workspace')" x-text="t('create_a_workspace_e94c64')">
</button>
</section>
</template>
<template x-if="workspace">
<div>
<div class="page-heading">
<div>
<div class="eyebrow" x-text="setupView?t('team_setup_504738'):t('your_team_303d2a')"></div>
<h1 x-text="title">
</h1>
</div>
<div class="actions" x-show="view==='calendar'"><button @click="open('absence')" x-show="can('request_overrides')"><span x-text="t('plan_time_off_33c0fc')"></span></button><button class="primary" x-show="can('request_overrides') || can('force_overrides')" @click="nextOwnDuty()"><span x-text="t('ask_for_cover_53c1cd')"></span></button></div>
</div>
<div class="sync-bar" x-show="setupView || ['failed','conflict'].includes(detail?.sync?.state)" :class="{'sync-problem':['failed','conflict'].includes(detail?.sync?.state)}">
<span class="status-dot" :class="detail?.sync?.state==='synced'?'green':'amber-dot'">
</span>
<span x-text="detail?.sync?.state==='synced' ? t('schedule_is_in_sync_be394a') : t('publication_fe5f07')+label(detail?.sync?.state || 'pending')">
</span>
<span class="muted" x-text="doc.integration.provider==='mock'?t('local_preview_provider_aca82c'):t('jira_service_management_071f90')">
</span>
<span class="sync-error" x-text="l(detail?.sync?.last_error || '')">
</span>
<button class="text" x-show="can('manage_integration')" @click="retry" :disabled="busy"><span x-text="t('sync_now_2b7d93')"></span></button>
</div>
<section class="coverage-problems panel" x-show="detail?.coverage?.total">
<h2 x-text="t('coverage_attention')"></h2>
<p class="muted" x-text="t('coverage_publication_wait')"></p>
<div class="coverage-issue-list"><template x-for="(gap,index) in coverageItems" :key="index">
<div class="coverage-issue" :class="{'is-pending':gap.awaiting}">
<div class="coverage-issue-body"><span class="coverage-state" x-text="t(gap.awaiting?'coverage_waiting':'coverage_missing')"></span>
<strong x-text="person(gap.user_id)+' · '+scheduleName(gap.schedule_id)"></strong>
<small class="coverage-times"><span x-text="formatTime(gap.start)"></span><span x-text="t('symbol_213edd')"></span><span x-text="formatTime(gap.end)"></span></small>
<small x-show="gap.awaiting" x-text="t('coverage_substitute',{names:gap.substitutes.map(id=>person(id)).join(', ')})"></small>
<small x-show="gap.awaiting" x-text="t('coverage_waiting_for',{names:gap.approvers.map(id=>person(id)).join(', ')})"></small></div>
<button x-show="gap.awaiting" @click="view='requests'" x-text="t('coverage_open_requests')"></button>
<button x-show="!gap.awaiting" @click="locateCoverage(gap)" x-text="t('open_affected_day')"></button>
</div></template></div></section>
<section x-show="view==='calendar'">
<div class="daily-overview">
<div class="today-card"><span class="eyebrow"><span x-text="t('on_call_now_0ec724')"></span> <span x-text="selectedSchedule?.name"></span></span><div class="person-line"><span class="avatar" :class="color(nowAssignment?.user_id)" x-text="nowAssignment?initials(nowAssignment.user_id):'—'"></span><div><strong x-text="nowAssignment?person(nowAssignment.user_id):t('no_coverage_252d1f')"></strong><small x-text="nowAssignment?t('until_24bf03')+formatTime(nowAssignment.end):t('choose_a_schedule_below_dea8d5')"></small></div></div></div>
<div class="today-card"><span class="eyebrow"><span x-text="t('your_next_shift_next_4_weeks_0e4beb')"></span></span><strong x-text="nextDuty?scheduleName(nextDuty.schedule_id):t('no_shifts_in_this_view_e878f4')"></strong><small x-text="nextDuty?formatTime(nextDuty.start)+' → '+formatTime(nextDuty.end):t('enjoy_your_time_off_44297f')"></small></div>
</div>
<section class="attention-panel" x-show="needsMe.length" :aria-label="t('needs_your_reply_9e26e0')"><div class="section-line"><h2><span x-text="t('needs_your_reply_9e26e0')"></span> <span class="badge" x-text="needsMe.length"></span></h2><button class="text" @click="view='requests'"><span x-text="t('all_requests_63246c')"></span></button></div>
<template x-for="r in needsMe" :key="r.id"><article class="reply-row"><span class="avatar" :class="color(r.requested_by)" x-text="initials(r.requested_by)"></span><div class="grow"><strong x-text="requestTitle(r)"></strong><template x-for="leg in r.legs"><small x-text="scheduleName(leg.schedule_id)+' · '+formatTime(leg.start)+' → '+formatTime(leg.end)"></small></template><p class="action-comment" x-show="r.reason" x-text="r.reason"></p></div><div class="actions"><button @click="decide(r,'reject')" :disabled="busy"><span x-text="t('decline_b59cf9')"></span></button><button class="primary" @click="decide(r,'approve')" :disabled="busy"><span x-text="t('accept_bb54db')"></span></button></div></article></template></section>
<div class="waiting-line" x-show="myRequests.length"><span x-text="t('waiting_count',{count:myRequests.length})"></span><button class="text" @click="view='requests'"><span x-text="t('view_cf3dbd')"></span></button></div>
<div class="panel calendar-panel">
<div class="calendar-toolbar">
<div>
<h2 x-text="monthTitle">
</h2>
<span class="muted"><span x-text="t('select_a_day_to_arrange_cover_510f60')"></span></span>
</div>
<div class="toolbar-controls">
<div class="schedule-control"><button class="schedule-trigger" @click="schedulePicker=!schedulePicker" :aria-expanded="schedulePicker"><span x-text="selectedSchedule?.name || t('choose_schedule_33049c')"></span> <span x-text="t('symbol_00d74e')"></span></button><div class="schedule-picker" x-show="schedulePicker" @click.outside="schedulePicker=false"><input :aria-label="t('search_schedules_235648')" type="search" x-model="scheduleSearch" :placeholder="t('search_schedules_85f641')"><div class="schedule-results"><template x-for="s in doc.schedules.filter(s=>s.name.toLowerCase().includes(scheduleSearch.toLowerCase()))" :key="s.id"><button @click="chooseSchedule(s.id)" :class="{chosen:s.id===scheduleId}" x-text="s.name"></button></template></div></div><select class="sr-only" tabindex="-1" :aria-label="t('schedule_0a8ada')" x-model="scheduleId" @change="run(()=>loadCalendar())">
<template x-for="s in doc.schedules" :key="s.id">
<option :value="s.id" :selected="scheduleId===(s.id)" x-text="s.name">
</option>
</template>
</select></div>
<div class="segmented">
<button :aria-label="t('previous_four_weeks_37f96a')" @click="navigateDays(-28)"><span x-text="t('symbol_0dccf9')"></span></button>
<button @click="navigateDays(0)"><span x-text="t('today_24345a')"></span></button>
<button :aria-label="t('next_four_weeks_08a1b8')" @click="navigateDays(28)"><span x-text="t('symbol_60ecbc')"></span></button>
</div>
</div>
</div>
<div class="weekdays">
<template x-for="day in calendar.days.slice(0,7)">
<span x-text="weekday(day)">
</span>
</template>
</div>
<div class="calendar-grid">
<template x-for="day in calendar.days" :key="day.date">
<button class="day" :data-duty-date="day.date" :class="{today:day.date===today(),weekend:[0,6].includes(new Date(day.date+'T12:00Z').getUTCDay())}" @click="clickDay(day)" :title="dayInterval(day)" :aria-label="dayInterval(day)+' '+onDay(day).map(a=>person(a.user_id)).join(', ')">
<span class="day-date" x-text="dayNumber(day)">
</span>
<small class="day-clock" x-text="localClock(day.start)">
</small>
<span class="holiday" x-show="holidaysOn(day)" x-text="holidaysOn(day)" :title="holidaysOn(day)">
</span>
<template x-for="a in onDay(day)" :key="a.schedule_id+a.start">
<span class="assignment" :class="color(a.user_id)" :title="assignmentInterval(a,day)">
<span class="mini-dot">
</span>
<span x-text="a.unavailable?t('uncovered_77617b')+shortName(a.user_id)+t('off_e362e3'):shortName(a.user_id)">
</span>
<span x-show="a.reason==='Approved replacement'"><span x-text="t('symbol_0380cf')"></span></span>
<span x-show="a.unavailable" :title="t('no_available_replacement_publication_is_blocked_144e92')"><span x-text="t('symbol_0ab831')"></span></span>
</span>
</template>
</button>
</template>
</div>
<div class="calendar-footer">
<span><span x-text="t('times_shown_in_1f96a9')"></span> <strong x-text="displayZone">
</strong>
</span>
<span><span x-text="t('approved_change_coverage_needs_attention_fc97bf')"></span></span>
</div>
</div>
<div class="section-line">
<h2><span x-text="t('your_team_80b301')"></span></h2>
<button class="text" @click="view='settings'"><span x-text="t('manage_team_68a698')"></span></button>
</div>
<div class="people-row">
<template x-for="m in members" :key="m.user?.id">
<span class="person-chip">
<span class="avatar small" :class="color(m.user?.id)" x-text="initials(m.user?.id)">
</span>
<span x-text="m.user.display_name">
</span>
<small x-show="m.user?.id===user?.id"><span x-text="t('you_8af56d')"></span></small>
</span>
</template>
</div>
</section>
<section x-show="view==='requests'">
<div class="section-line">
<p class="muted"><span x-text="t('replacements_take_effect_after_everyone_involved_con_a63d3a')"></span></p>
<button class="primary" @click="nextOwnDuty()" x-show="can('request_overrides') || can('force_overrides')"><span x-text="t('new_request_8a3714')"></span></button>
</div>
<div class="empty panel" x-show="!inbox.length">
<h2><span x-text="t('nothing_waiting_on_a_handover_1a3b46')"></span></h2>
<p><span x-text="t('your_team_s_replacements_and_swaps_will_appear_here_ee9501')"></span></p>
</div>
<template x-for="r in inbox" :key="r.id">
<article class="panel request-card">
<div class="section-line">
<strong x-text="requestTitle(r)">
</strong>
<span class="badge" :class="r.status" x-text="label(r.status)">
</span>
</div>
<p class="action-comment" x-show="r.reason" x-text="r.reason">
</p>
<template x-for="leg in r.legs">
<p class="leg">
<strong x-text="scheduleName(leg.schedule_id)">
</strong>
<span x-text="person(leg.from_user)+' → '+person(leg.to_user)">
</span>
<small x-text="formatTime(leg.start)+' — '+formatTime(leg.end)">
</small>
</p>
</template>
<small class="muted" x-text="t('created_05e8fd')+formatTime(r.created_at)">
</small>
<p class="muted" x-show="r.status==='pending'" x-text="t('waiting_for_f91441')+r.required_approvers.filter(id=>!r.approvals.includes(id)).map(id=>person(id)).join(', ')">
</p>
<p class="muted" x-show="r.forced_by" x-text="t('applied_without_confirmation_f694d2')+(r.force_reason || '')">
</p>
<div class="actions" x-show="r.status==='pending'">
<template x-if="r.required_approvers.includes(user?.id) && !r.approvals.includes(user?.id)">
<span>
<button class="primary" @click="decide(r,'approve')" :disabled="busy"><span x-text="t('accept_bb54db')"></span></button> <button @click="decide(r,'reject')" :disabled="busy"><span x-text="t('decline_b59cf9')"></span></button>
</span>
</template>
<button x-show="r.requested_by===user?.id" @click="decide(r,'cancel')" :disabled="busy"><span x-text="t('cancel_request_84837a')"></span></button>
<button x-show="can('force_overrides')" @click="open('force',{id:r.id,reason:''})"><span x-text="t('apply_without_confirmation_f0350d')"></span></button>
</div>
</article>
</template>
</section>
<!-- GENERATION_PAGE -->
<!-- RESOURCE_INVENTORY -->
<section x-show="view==='activity'">
<p class="muted" x-show="!can('view_audit')"><span x-text="t('your_role_does_not_include_access_to_audit_history_bb8e51')"></span></p>
<div class="panel compact" x-show="can('view_audit')">
<template x-for="event in detail.audit || []" :key="event.id">
<details class="audit-row">
<summary>
<span class="audit-dot">
</span>
<strong x-text="label(event.action)">
</strong>
<span x-text="event.actor_name || event.actor">
</span>
<time x-text="formatTime(event.created_at)">
</time>
</summary>
<pre x-text="auditDetails(event.resolved_details || event.details)"></pre>
<details class="audit-raw"><summary x-text="t('audit_raw_details')"></summary><pre x-text="JSON.stringify(event.details,null,2)"></pre></details>
</details>
</template>
</div>
</section>
<section x-show="view==='settings'">
<div class="two-columns">
<article class="panel rule-card">
<span class="eyebrow"><span x-text="t('team_preferences_440fbe')"></span></span>
<h2 x-text="doc.name">
</h2>
<p x-text="t('workspace_schedule_timezone_954c44')+doc.timezone">
</p>
<p x-text="t('planning_horizon_b249cc')+doc.horizon_days+t('days_e72d54')">
</p>
<button x-show="can('edit_schedules')" @click="open('preferences')"><span x-text="t('edit_preferences_4cefcb')"></span></button>
</article>
<article class="panel rule-card">
<span class="eyebrow"><span x-text="t('connected_provider_f22440')"></span></span>
<h2 x-text="doc.integration.provider==='mock'?t('local_preview_8d4a3f'):t('jira_service_management_071f90')">
</h2>
<p class="muted"><span x-text="t('approved_changes_are_published_automatically_a715bf')"></span></p>
<div class="actions" x-show="can('manage_integration')">
<button @click="open('integration')"><span x-text="t('configure_792c81')"></span></button>
<button @click="preview"><span x-text="t('download_preview_30507d')"></span></button>
</div>
</article>
</div>
<div class="section-line">
<h2><span x-text="t('team_access_8fb357')"></span></h2>
<button class="primary" x-show="can('manage_members')" @click="open('member')"><span x-text="t('add_teammate_d6bc5a')"></span></button>
</div>
<div class="panel compact">
<template x-for="m in members" :key="m.user?.id">
<div class="list-row">
<span class="avatar" :class="color(m.user?.id)" x-text="initials(m.user?.id)">
</span>
<div class="grow">
<strong x-text="m.user.display_name">
</strong>
<small x-text="'@'+m.user.username+' · '+m.permissions.length+t('permissions_607d39')">
</small>
</div>
<button x-show="can('manage_members')" @click="open('member',{username:m.user.username,permissions:m.permissions})"><span x-text="t('edit_access_e381a8')"></span></button>
</div>
</template>
</div>
<div class="section-line">
<h2><span x-text="t('scheduling_directory_e8fdd4')"></span></h2>
<span class="muted"><span x-text="t('country_controls_public_holidays_defaults_to_cyprus_e828a2')"></span></span>
</div>
<div class="panel compact">
<template x-for="p in people" :key="p.id">
<div class="list-row">
<span class="avatar" :class="color(p.id)" x-text="initials(p.id)">
</span>
<div class="grow">
<strong x-text="p.display_name">
</strong>
<small x-text="(p.email || (p.username?'@'+p.username:t('no_email_configured_b309ce')))+' · '+countryName(p.country)+' · '+(p.user_id?t('workspace_user_95981a'):t('email_contact_e9e0b3'))">
</small>
</div>
<button x-show="can('edit_schedules')" @click="open('person',p)"><span x-text="t('edit_person_d98516')"></span></button>
</div>
</template>
</div>
<article class="panel rule-card spaced">
<span class="eyebrow"><span x-text="t('workspace_time_a3e57f')"></span></span>
<h2><span x-text="t('working_interval_schedule_timezone_fc825f')"></span></h2>
<p x-text="workingSummary()">
</p>
<p class="muted"><span x-text="t('shared_by_every_team_rotations_use_this_timezone_wit_e06e20')"></span></p>
<button x-show="detail?.is_owner" @click="open('working')"><span x-text="t('edit_working_interval_290690')"></span></button>
</article>
<article class="panel rule-card spaced" x-show="detail?.is_owner">
<span class="eyebrow"><span x-text="t('workspace_policy_4437ae')"></span></span>
<h2><span x-text="t('changes_made_outside_meguri_98ff25')"></span></h2>
<p class="muted"><span x-text="t('applies_to_every_team_in_this_workspace_and_only_to__909c8c')"></span></p>
<label><span x-text="t('when_external_configuration_changes_18f056')"></span><select :value="doc?.overwrite_drift?'overwrite':'block'" @change="setDrift($event.target.value==='overwrite')">
<option value="block" x-text="t('pause_publication_and_report_a_conflict_default_5b9c4a')"></option>
<option value="overwrite" x-text="t('overwrite_external_changes_with_meguri_s_state_36db36')"></option>
</select>
</label>
</article>
</section>
</div>
</template>
</div>
<footer class="page-footer"><span x-text="t('meguri_8bdb60')"></span> <span><span x-text="t('good_rotations_start_with_a_shared_plan_2e3f86')"></span></span>
</footer>
</main>
</div>
<div class="modal-backdrop" x-cloak x-show="modal" @keydown.escape.window="picker?picker=null:(bindingEditor || rulePeopleTarget)?closeRuleEditor():modal=''" @click.self="modal=''">
<section class="modal" :class="{'wide-modal':['variable','generation-rule','absence'].includes(modal),'constructor-modal':modal==='generation-rule'}" role="dialog" aria-modal="true" aria-labelledby="dialog-title" @keydown.tab="trapDialog($event)" x-ref="dialog">
<form @submit.prevent="save()">
<div class="modal-heading">
<div>
<span class="eyebrow" x-show="modal!=='workspace'" x-text="['request','day','force'].includes(modal)?scheduleName(form.schedule_id || scheduleId):doc?.name || 'MEGURI'"></span>
<h2 id="dialog-title" x-text="modal==='request'?(form.force_mode?t('apply_without_approval_d72926'):form.from_user===user?.id?t('who_can_cover_for_you_6e894c'):t('cover_person',{name:shortName(form.from_user)})):modal==='day'?t('choose_a_shift_330348'):({variable:t('workspace_variable_7abc57'),'generation-rule':t('generation_rule_d34ccf'),rotation:t('rotation_906fe5'),priority:t('ordered_project_coverage_cf12c3'),coverage:t('project_coverage_536388'),request:t('request_a_change_3d465f'),force:t('apply_without_confirmation_f0350d'),absence:t('planned_absence_bfcea7'),holiday:t('public_holiday_49ec03'),'holiday-import':t('import_public_holidays_8098e3'),'remove-schedule':t('remove_schedule_ef0a8a'),person:t('scheduling_person_1273c6'),working:t('workspace_working_interval_c7d250'),member:t('team_access_8fb357'),workspace:t('new_workspace_b21b86'),account:t('your_account_4ab291'),policy:t('escalation_policy_bbb0f7'),route:t('routing_rule_b00b22'),integration:t('connected_provider_9702c0'),preferences:t('team_preferences_7fcab5')})[modal]">
</h2>
</div>
<button type="button" class="icon-button" @click="modal=''" :aria-label="t('close_dialog_7b2902')"><span x-text="t('symbol_67fba2')"></span></button>
</div>
<template x-if="picker"><div class="person-picker" role="region" :aria-label="t('choose_a_person_202007')">
<div class="section-line">
<h3 x-text="pickerEditing?t('person_details_0574fe'):t('choose_a_person_202007')">
</h3>
<button type="button" @click="picker=null"><span x-text="t('back_c32ae9')"></span></button>
</div>
<div x-show="!pickerEditing">
<label><span x-text="t('search_workspace_users_or_enter_email_b3922e')"></span><input x-ref="personSearch" x-model="personQuery" type="search" :placeholder="t('name_or_person_example_com_c4e3fd')">
</label>
<div class="picker-results">
<template x-for="p in pickerResults" :key="p.id">
<button type="button" class="picker-option" @click="pickPerson(p)">
<span class="avatar small" :class="color(p.id)" x-text="initials(p.id)">
</span>
<span>
<strong x-text="p.display_name">
</strong>
<small x-text="(p.email || (p.username?'@'+p.username:t('workspace_user_95981a')))+' · '+countryName(p.country)">
</small>
</span>
<span class="badge" x-text="p.user_id?t('user_9f8a23'):t('contact_b37456')">
</span>
</button>
</template>
<p class="muted" x-show="!pickerResults.length"><span x-text="t('no_matching_person_in_this_workspace_dcec7a')"></span></p>
</div>
<div class="email-contact" x-show="personQuery.includes('@') && !people.some(p=>p.email?.toLowerCase()===personQuery.trim().toLowerCase())">
<h3><span x-text="t('add_by_email_9368f3')"></span></h3>
<p class="muted"><span x-text="t('they_do_not_need_a_meguri_account_adding_a_contact_d_15a721')"></span></p>
<label><span x-text="t('name_optional_9c9f03')"></span><input x-model="pickerName" :placeholder="t('displayed_in_the_calendar_923a0e')">
</label>
<label><span x-text="t('country_d523eb')"></span><select :aria-label="t('country_d523eb')" x-model="pickerCountry">
<template x-for="country in countries" :key="country.code">
<option :value="country.code" :selected="pickerCountry===country.code" x-text="country.name">
</option>
</template>
</select>
</label>
<button type="button" class="primary" @click="addEmailPerson()" x-text="t('add_e2b431')+personQuery.trim()">
</button>
</div>
</div>
<div x-show="pickerEditing">
<label><span x-text="t('name_709a23')"></span><input x-model="pickerName">
</label>
<label><span x-text="t('email_for_jsm_lookup_193cfb')"></span><input type="email" x-model="pickerEmail">
</label>
<label><span x-text="t('scheduling_country_393322')"></span><select :aria-label="t('scheduling_country_393322')" x-model="pickerCountry">
<template x-for="country in countries" :key="country.code">
<option :value="country.code" :selected="pickerCountry===country.code" x-text="country.name">
</option>
</template>
</select>
</label>
<button type="button" class="primary" @click="savePersonDetails()"><span x-text="t('use_these_details_775a68')"></span></button>
</div>
</div>
</template><div x-show="!picker">
<template x-if="modal==='workspace'">
<div>
<label><span x-text="t('name_709a23')"></span><input x-model="form.name" required maxlength="100">
</label>
</div>
</template>
<template x-if="modal==='account'">
<div>
<label><span x-text="t('language')"></span><select x-model="form.language" :aria-label="t('language')"><option value="" x-text="t('language_auto')"></option><template x-for="code in I18n.supported"><option :value="code" :selected="form.language===code" x-text="t('language_'+code)"></option></template></select></label>
<label><span x-text="t('display_name_c7874a')"></span><input x-model="form.display_name" required>
</label>
<label><span x-text="t('country_of_work_88453b')"></span><select :aria-label="t('country_of_work_88453b')" x-model="form.country">
<template x-for="country in countries" :key="country.code">
<option :value="country.code" :selected="form.country===country.code" x-text="country.name">
</option>
</template>
</select>
</label>
<label><span x-text="t('display_timezone_2425c3')"></span><input list="timezones" x-model="form.timezone" :placeholder="t('browser_default_e90df2')+browserZone" :aria-label="t('display_timezone_2425c3')">
</label>
<button type="button" class="text" @click="form.timezone=''"><span x-text="t('use_browser_timezone_3f54ba')"></span></button>
<p class="info-note" x-text="t('dates_and_timestamps_are_displayed_in_77ec74')+(form.timezone || browserZone)+t('this_never_changes_the_workspace_timezone_or_shifts_1de864')">
</p>
<p class="muted"><span x-text="t('country_changes_update_your_holiday_calendar_in_team_1e4eb8')"></span></p>
<label><span x-text="t('current_password_19dff4')"></span><input type="password" autocomplete="current-password" x-model="form.current_password">
</label>
<label><span x-text="t('new_password_d850ee')"></span><input type="password" autocomplete="new-password" minlength="12" x-model="form.password">
</label>
</div>
</template>
<template x-if="modal==='person'">
<div>
<label><span x-text="t('name_709a23')"></span><input x-model="form.display_name" required>
</label>
<label><span x-text="t('email_for_jsm_lookup_193cfb')"></span><input type="email" x-model="form.email" :required="!form.user_id">
</label>
<label><span x-text="t('scheduling_country_393322')"></span><select :aria-label="t('scheduling_country_393322')" x-model="form.country">
<template x-for="country in countries" :key="country.code">
<option :value="country.code" :selected="form.country===country.code" x-text="country.name">
</option>
</template>
</select>
</label>
<p class="info-note"><span x-text="t('an_email_contact_can_be_scheduled_without_logging_in_34d74f')"></span></p>
</div>
</template>
<template x-if="modal==='working'">
<div>
<label><span x-text="t('workspace_iana_timezone_bea0e0')"></span><input list="timezones" x-model="form.timezone" required>
</label>
<div class="days-select">
<template x-for="(day,index) in [t('mon_24b2a0'),t('tue_529541'),t('wed_23408b'),t('thu_3593cc'),t('fri_bbd6e3'),t('sat_6b782d'),t('sun_48c98c')]">
<label class="check">
<input type="checkbox" :value="index+1" x-model="form.weekdays">
<span x-text="day">
</span>
</label>
</template>
</div>
<div class="two-columns">
<label><span x-text="t('working_day_starts_5418a6')"></span><input type="time" x-model="form.start" required>
</label>
<label><span x-text="t('working_day_ends_52d5f6')"></span><input type="time" x-model="form.end" required>
</label>
</div>
<p class="info-note"><span x-text="t('this_interval_is_shared_across_the_workspace_the_tim_07e765')"></span></p>
</div>
</template>
<template x-if="['rotation','priority'].includes(modal)">
<div>
<label><span x-text="t('schedule_name_fb63dc')"></span><input x-model="form.name" required>
</label>
<label><span x-text="t('handover_time_c9e315')"></span><input :aria-label="t('handover_time_c9e315')" type="time" x-model="form.handover" required>
</label>
<small x-text="t('schedule_time_in_911b55')+doc.timezone+t('each_shift_ends_when_the_next_begins_14ab16')">
</small>
<template x-if="['rotation','priority'].includes(modal)">
<div>
<template x-if="modal==='rotation'"><div class="two-columns">
<label><span x-text="t('anchor_date_e77bd6')"></span><span class="date-field" x-data="dateField" x-modelable="value" x-model="form.anchor"><input type="text" :aria-label="t('anchor_date_e77bd6')" x-ref="textDate" :value="text" @input="edit($event.target)" :placeholder="t('dd_mm_yyyy_d7e5fe')" inputmode="numeric" maxlength="10" required><button type="button" :aria-label="t('open_calendar_41547e')" @click="$refs.calendar.showPicker()"><span x-text="t('symbol_21887f')"></span></button><input class="native-date" type="date" x-ref="calendar" x-model="value" @input="picked()" tabindex="-1" aria-hidden="true"></span>
</label>
<label><span x-text="t('days_per_shift_83ec07')"></span><input type="number" min="1" max="366" x-model="form.shift_days" required>
</label>
</div></template>
<label><span x-text="modal==='priority'?t('responsible_people_in_priority_order_7205a5'):t('participants_in_rotation_order_4edee0')"></span></label>
<template x-for="(id,index) in form.participants" :key="id">
<div class="list-row">
<div class="grow">
<strong x-text="person(id)">
</strong>
<small x-text="countryName(people.find(p=>p.id===id)?.country)">
</small>
</div>
<button type="button" @click="editPerson(id)" :aria-label="t('edit_participant_details_73e491')"><span x-text="t('details_dc3dec')"></span></button>
<button type="button" @click="moveParticipant(index,-1)" :aria-label="t('move_earlier_07dd6f')"><span x-text="t('symbol_9555ff')"></span></button>
<button type="button" @click="moveParticipant(index,1)" :aria-label="t('move_later_bb4c84')"><span x-text="t('symbol_fc4866')"></span></button>
<button type="button" @click="form.participants.splice(index,1)" :aria-label="t('remove_participant_131344')"><span x-text="t('symbol_67fba2')"></span></button>
</div>
</template>
<button type="button" class="person-select wide" @click="choosePerson('participants')"><span x-text="t('add_participant_name_or_email_5d89ac')"></span></button>
<p class="muted" x-show="modal==='rotation'"><span x-text="t('the_first_participant_starts_on_the_anchor_date_at_t_d87af4')"></span></p>
</div>
</template>
<template x-if="modal==='priority'"><div><label><span x-text="t('fallback_schedule_7b7a21')"></span><select x-model="form.fallback" required>
<option value="" x-text="t('choose_a_schedule_0636f3')"></option>
<template x-for="s in doc.schedules.filter(s=>s.id!==form.id)">
<option :value="s.id" :selected="form.fallback===(s.id)" x-text="s.name">
</option>
</template>
</select>
</label></div></template>
</div>
</template>
<template x-if="modal==='day'"><div><p x-show="!form.assignments.length"><span x-text="t('no_assignment_for_this_day_95a9f7')"></span></p><template x-for="a in form.assignments"><button type="button" class="shift-option" @click="openDuty(form.day,a)"><strong x-text="person(a.user_id)"></strong><small x-text="assignmentInterval(a,form.day)"></small><span><span x-text="t('choose_755968')"></span></span></button></template></div></template>
<template x-if="modal==='request'"><div class="cover-sheet">
<div class="shift-ticket"><div class="person-line"><span class="avatar" :class="color(form.from_user)" x-text="initials(form.from_user)"></span><div><strong x-text="person(form.from_user)"></strong><small x-text="scheduleName(form.schedule_id)"></small></div><span class="badge" x-show="Date.parse(form.end)<Date.now()"><span x-text="t('past_shift_cd54e4')"></span></span></div><div class="shift-times" x-show="!form.custom_range && !form.swap"><div><small><span x-text="t('from_3f6605')"></span></small><strong x-text="Date.parse(form.start)<Date.now() && Date.parse(form.end)>Date.now()?t('now_e3b820'):formatTime(form.start)"></strong></div><span><span x-text="t('symbol_213edd')"></span></span><div><small><span x-text="t('until_96bdb6')"></span></small><strong x-text="formatTime(form.end)"></strong></div></div></div>
<template x-if="Date.parse(form.end)>Date.now() && (can('request_overrides') || can('force_overrides'))"><div>
<p class="action-comment" x-show="matchingRequest?.reason && !form.custom_range && !form.swap" x-text="matchingRequest?.reason"></p><div class="cover-self" x-show="form.from_user!==user?.id && !form.force_mode"><span class="avatar" :class="color(user?.id)" x-text="initials(user?.id)"></span><strong><span x-text="t('you_ll_cover_this_shift_afcf1e')"></span></strong><span class="badge" x-show="matchingRequest && !form.custom_range && !form.swap"><span x-text="t('requested_by_your_colleague_1320db')"></span></span></div>
<div x-show="form.from_user===user?.id || form.force_mode"><label><span x-text="t('who_s_covering_926c44')"></span><input type="search" :aria-label="t('search_colleagues_9c47ad')" x-model="coverQuery" :placeholder="t('search_by_name_or_email_eedfe2')" autocomplete="off"></label><div class="cover-options" role="group" :aria-label="t('covering_colleague_10b47f')"><template x-for="p in coverPeople" :key="p.id"><button type="button" :class="{chosen:form.to_user===p.id}" @click="form.to_user=p.id" :aria-pressed="form.to_user===p.id" :aria-label="p.display_name"><span class="avatar small" :class="color(p.id)" x-text="initials(p.id)"></span><span x-text="p.id===user?.id?t('me_94563e'):p.display_name"></span><span class="selection-tick" x-show="form.to_user===p.id"><span x-text="t('symbol_698a87')"></span></span></button></template><p class="muted" x-show="!coverPeople.length"><span x-text="t('no_matching_teammates_dac71f')"></span></p></div></div>
<div class="cover-range"><small x-text="t('cover_timezone',{zone:doc.timezone})"></small><div class="cover-boundary"><label><span x-text="t('cover_start_date')"></span><span class="date-field" x-data="dateField" x-modelable="value" x-model="form.start_day"><input type="text" :aria-label="t('cover_start_date')" x-ref="textDate" :value="text" @input="edit($event.target);coverRangeChanged()" :placeholder="t('dd_mm_yyyy_d7e5fe')" maxlength="10" required><button type="button" :aria-label="t('open_calendar_41547e')" @click="$refs.calendar.showPicker()"><span x-text="t('symbol_21887f')"></span></button><input class="native-date" type="date" x-ref="calendar" x-model="value" @input="picked();coverRangeChanged()" tabindex="-1" aria-hidden="true"></span></label><label><span x-text="t('cover_start_time')"></span><input type="time" :aria-label="t('cover_start_time')" x-model="form.start_time" @input="coverRangeChanged()" required></label></div><div class="cover-boundary"><label><span x-text="t('cover_end_date')"></span><span class="date-field" x-data="dateField" x-modelable="value" x-model="form.end_day"><input type="text" :aria-label="t('cover_end_date')" x-ref="textDate" :value="text" @input="edit($event.target);coverRangeChanged()" :placeholder="t('dd_mm_yyyy_d7e5fe')" maxlength="10" required><button type="button" :aria-label="t('open_calendar_41547e')" @click="$refs.calendar.showPicker()"><span x-text="t('symbol_21887f')"></span></button><input class="native-date" type="date" x-ref="calendar" x-model="value" @input="picked();coverRangeChanged()" tabindex="-1" aria-hidden="true"></span></label><label><span x-text="t('cover_end_time')"></span><input type="time" :aria-label="t('cover_end_time')" x-model="form.end_time" @input="coverRangeChanged()" required></label></div></div><details class="range-options"><summary x-text="t('cover_swap_optional')"></summary><label class="check"><input type="checkbox" x-model="form.swap"><span x-text="t('i_ll_take_a_shift_in_return_b47888')"></span></label><template x-if="form.swap"><div><div class="cover-boundary"><label><span x-text="t('cover_return_date')"></span><span class="date-field" x-data="dateField" x-modelable="value" x-model="form.return_start"><input type="text" :aria-label="t('cover_return_date')" x-ref="textDate" :value="text" @input="edit($event.target);coverRangeChanged()" :placeholder="t('dd_mm_yyyy_d7e5fe')" maxlength="10" required><button type="button" :aria-label="t('open_calendar_41547e')" @click="$refs.calendar.showPicker()"><span x-text="t('symbol_21887f')"></span></button><input class="native-date" type="date" x-ref="calendar" x-model="value" @input="picked();coverRangeChanged()" tabindex="-1" aria-hidden="true"></span></label><label><span x-text="t('cover_return_time')"></span><input type="time" :aria-label="t('cover_return_time')" x-model="form.return_start_time" @input="coverRangeChanged()" required></label></div><div class="cover-boundary"><label><span x-text="t('cover_return_end_date')"></span><span class="date-field" x-data="dateField" x-modelable="value" x-model="form.return_end"><input type="text" :aria-label="t('cover_return_end_date')" x-ref="textDate" :value="text" @input="edit($event.target);coverRangeChanged()" :placeholder="t('dd_mm_yyyy_d7e5fe')" maxlength="10" required><button type="button" :aria-label="t('open_calendar_41547e')" @click="$refs.calendar.showPicker()"><span x-text="t('symbol_21887f')"></span></button><input class="native-date" type="date" x-ref="calendar" x-model="value" @input="picked();coverRangeChanged()" tabindex="-1" aria-hidden="true"></span></label><label><span x-text="t('cover_return_end_time')"></span><input type="time" :aria-label="t('cover_return_end_time')" x-model="form.return_end_time" @input="coverRangeChanged()" required></label></div></div></template></details>
<label x-show="!matchingRequest || form.force_mode || form.custom_range || form.swap"><span x-text="t('comment_optional_f28415')"></span><input :aria-label="t('comment_optional_f28415')" x-model="form.reason" maxlength="1000" :placeholder="t('e_g_ops_123_vacation_befe39')"></label>
<p class="validation-error" role="status" x-show="requestError" x-text="l(requestError)"></p><p class="muted" x-show="requestLoading"><span x-text="t('checking_availability_24f241')"></span></p>
<div class="request-ready" x-show="requestResult && !requestLoading"><template x-for="leg in requestResult?.legs || []"><small x-text="person(leg.from_user)+' → '+person(leg.to_user)+' · '+formatTime(leg.start)+' → '+formatTime(leg.end)"></small></template><span x-text="form.force_mode?t('applies_immediately_no_approval_needed_842779'):matchingRequest?t('your_acceptance_completes_this_request_3713f7'):(form.from_user===user?.id?shortName(form.to_user):shortName(form.from_user))+t('will_confirm_in_meguri_a4a9b6')"></span></div>
</div></template>
</div></template>
<template x-if="modal==='force'">
<div>
<p class="info-note"><span x-text="t('applies_immediately_your_action_and_optional_comment_50c159')"></span></p>
<label><span x-text="t('comment_optional_f28415')"></span><textarea x-model="form.reason" maxlength="1000" :placeholder="t('e_g_ops_123_vacation_befe39')">
</textarea>
</label>
</div>
</template>
<template x-if="modal==='absence'">
<div>
<label><span x-text="t('employee_079711')"></span><button type="button" class="person-select" @click="choosePerson('user_id')">
<span x-text="form.user_id?person(form.user_id):t('choose_a_person_or_enter_email_bcface')">
</span>
<span><span x-text="t('symbol_00d74e')"></span></span>
</button>
</label>
<div class="two-columns">
<label><span x-text="t('first_absent_duty_day_b0b47f')"></span><span class="date-field" x-data="dateField" x-modelable="value" x-model="form.start"><input type="text" :aria-label="t('first_absent_duty_day_b0b47f')" x-ref="textDate" :value="text" @input="edit($event.target)" :placeholder="t('dd_mm_yyyy_d7e5fe')" inputmode="numeric" maxlength="10" required><button type="button" :aria-label="t('open_calendar_41547e')" @click="$refs.calendar.showPicker()"><span x-text="t('symbol_21887f')"></span></button><input class="native-date" type="date" x-ref="calendar" x-model="value" @input="picked()" tabindex="-1" aria-hidden="true"></span>
</label>
<label><span x-text="t('last_absent_duty_day_inclusive_6f2bf4')"></span><span class="date-field" x-data="dateField" x-modelable="value" x-model="form.end"><input type="text" :aria-label="t('last_absent_duty_day_inclusive_6f2bf4')" x-ref="textDate" :value="text" @input="edit($event.target)" :placeholder="t('dd_mm_yyyy_d7e5fe')" inputmode="numeric" maxlength="10" required><button type="button" :aria-label="t('open_calendar_41547e')" @click="$refs.calendar.showPicker()"><span x-text="t('symbol_21887f')"></span></button><input class="native-date" type="date" x-ref="calendar" x-model="value" @input="picked()" tabindex="-1" aria-hidden="true"></span>
</label>
</div>
<p class="info-note"><span x-text="t('dates_are_inclusive_duty_days_in_the_workspace_timez_b377b5')"></span></p>
<div class="section-line"><h3><span x-text="t('replacement_ranges_a8d04e')"></span></h3><button type="button" @click="addAbsenceRange()"><span x-text="t('replacement_range_f216ec')"></span></button></div>
<p class="muted" x-show="!form.ranges.length"><span x-text="t('no_explicit_substitutes_yet_the_preview_below_shows__07dfef')"></span></p>
<template x-for="(range,index) in form.ranges" :key="range.id"><div class="absence-range">
<label><span x-text="t('schedule_0a8ada')"></span><select x-model="range.schedule_id"><option value="" x-text="t('absence_all_schedules')"></option><template x-for="schedule in doc.schedules" :key="schedule.id"><option :value="schedule.id" x-text="schedule.name"></option></template></select></label><small class="absence-zone" x-text="t('absence_interval_zone',{zone:doc.timezone})"></small><div class="two-columns">
<label><span x-text="t('range_starts_3632e7')"></span><span class="date-field" x-data="dateField" x-modelable="value" x-model="range.start"><input type="text" :aria-label="t('range_starts_3632e7')" x-ref="textDate" :value="text" @input="edit($event.target)" :placeholder="t('dd_mm_yyyy_d7e5fe')" inputmode="numeric" maxlength="10" required><button type="button" :aria-label="t('open_calendar_41547e')" @click="$refs.calendar.showPicker()"><span x-text="t('symbol_21887f')"></span></button><input class="native-date" type="date" x-ref="calendar" x-model="value" @input="picked()" tabindex="-1" aria-hidden="true"></span><input type="time" step="1" x-model="range.start_time" :aria-label="t('start_time')" required></label>
<label><span x-text="t('absence_end_exclusive')"></span><span class="date-field" x-data="dateField" x-modelable="value" x-model="range.end"><input type="text" :aria-label="t('absence_end_exclusive')" x-ref="textDate" :value="text" @input="edit($event.target)" :placeholder="t('dd_mm_yyyy_d7e5fe')" inputmode="numeric" maxlength="10" required><button type="button" :aria-label="t('open_calendar_41547e')" @click="$refs.calendar.showPicker()"><span x-text="t('symbol_21887f')"></span></button><input class="native-date" type="date" x-ref="calendar" x-model="value" @input="picked()" tabindex="-1" aria-hidden="true"></span><input type="time" step="1" x-model="range.end_time" :aria-label="t('end_time')" required></label>
</div>
<label><span x-text="t('replacement_c385e3')"></span><button type="button" class="person-select" @click="choosePerson('absenceReplacement',index)"><span x-text="range.replacement?person(range.replacement):t('choose_a_person_or_enter_email_bcface')"></span><span><span x-text="t('symbol_00d74e')"></span></span></button></label>
<label x-show="!range.schedule_id"><span x-text="t('project_coverage_536388')"></span><select :value="range.replace_projects?'replace':'automatic'" @change="range.replace_projects=$event.target.value==='replace'"><option value="automatic" x-text="t('promote_secondary_automatically_3bee50')"></option><option value="replace" x-text="t('use_this_replacement_in_affected_projects_too_edf224')"></option></select></label>
<button type="button" class="text danger" @click="form.ranges.splice(index,1)"><span x-text="t('remove_range_55c658')"></span></button>
</div></template>
<label class="check" x-show="can('force_overrides')"><input type="checkbox" x-model="form.force"><span x-text="t('apply_replacements_without_confirmation_7c4a2e')"></span></label>
<label x-show="form.ranges.length || form.force"><span x-text="t('comment_optional_f28415')"></span><textarea x-model="form.reason" maxlength="1000" :placeholder="t('vacation_coverage_9d7295')"></textarea></label>
<div class="absence-preview" aria-live="polite">
<h3><span x-text="t('schedule_impact_0ae487')"></span></h3>
<p class="muted" x-show="absenceLoading"><span x-text="t('checking_affected_schedules_109ed9')"></span></p>
<p class="validation-error" x-show="absenceError" x-text="l(absenceError)"></p>
<template x-if="absenceResult"><div>
<p class="preview-count" x-text="absenceResult.plan.schedules+t('affected_schedules_c82c00')+absenceResult.plan.groups.length+t('replacement_requests_c81010')+absenceResult.plan.uncovered+t('uncovered_intervals_b5430f')"></p>
<template x-for="error in absenceResult.plan.errors"><p class="validation-error" x-text="l(error)"></p></template>
<template x-for="warning in absenceResult.plan.warnings"><p class="validation-warning" x-text="l(warning)"></p></template>
<p class="muted" x-show="!absenceResult.plan.rows.length"><span x-text="t('no_scheduled_shifts_change_in_this_interval_98915a')"></span></p>
<div class="impact-scroll" x-show="absenceResult.plan.rows.length"><table class="impact-table"><thead><tr><th><span x-text="t('schedule_interval_3cf85c')"></span></th><th><span x-text="t('coverage_80e135')"></span></th><th><span x-text="t('action_97c89a')"></span></th></tr></thead><tbody>
<template x-for="row in (showAllAbsence?absenceResult.plan.rows:absenceResult.plan.rows.slice(0,8))" :key="row.schedule_id+row.start"><tr><td class="impact-interval"><strong x-text="row.schedule_name"></strong><small><span x-text="formatTime(row.start)"></span><span x-text="t('symbol_213edd')"></span><span x-text="formatTime(row.end)"></span></small></td><td><span x-text="person(row.before)"></span> <span x-text="t('symbol_213edd')"></span> <strong x-text="row.after?person(row.after):t('uncovered_88710a')"></strong></td><td><button type="button" class="text" x-show="row.status==='uncovered'" @click="coverAbsenceRow(row)" x-text="t('choose_a_substitute_be8f67')"></button><span x-show="row.status!=='uncovered'" x-text="row.status==='replacement'?(form.force?t('apply_now_a356cc'):t('needs_confirmation_722b99')):t('automatic_secondary_fallback_7197c5')"></span></td></tr></template>
</tbody></table></div>
<button class="text" type="button" x-show="absenceResult.plan.rows.length>8" @click="showAllAbsence=!showAllAbsence" x-text="showAllAbsence?t('show_fewer_intervals_6d52e6'):t('show_all_382ebd')+absenceResult.plan.rows.length+t('intervals_d03402')"></button>
<p class="info-note" x-show="absenceResult.plan.groups.length && !form.force"><span x-text="t('absence_takes_effect_when_saved_each_replacement_con_a55d09')"></span></p>
<p class="validation-warning" x-show="absenceResult.plan.uncovered"><span x-text="t('some_shifts_have_no_available_replacement_they_are_s_c20d79')"></span></p>
</div></template>
</div>
</div>
</template>
<template x-if="modal==='holiday'">
<div>
<label><span x-text="t('country_d523eb')"></span><select :aria-label="t('country_d523eb')" x-model="form.country">
<template x-for="country in countries" :key="country.code">
<option :value="country.code" :selected="form.country===country.code" x-text="country.name">
</option>
</template>
</select>
</label>
<label><span x-text="t('date_in_country_calendar_fbc5e1')"></span><span class="date-field" x-data="dateField" x-modelable="value" x-model="form.date"><input type="text" :aria-label="t('date_in_country_calendar_fbc5e1')" x-ref="textDate" :value="text" @input="edit($event.target)" :placeholder="t('dd_mm_yyyy_d7e5fe')" inputmode="numeric" maxlength="10" required><button type="button" :aria-label="t('open_calendar_41547e')" @click="$refs.calendar.showPicker()"><span x-text="t('symbol_21887f')"></span></button><input class="native-date" type="date" x-ref="calendar" x-model="value" @input="picked()" tabindex="-1" aria-hidden="true"></span>
</label>
<label><span x-text="t('holiday_name_ceba88')"></span><input x-model="form.name" required maxlength="120">
</label>
</div>
</template>
<template x-if="modal==='holiday-import'">
<div>
<p class="muted"><span x-text="t('paste_a_csv_with_113f64')"></span> <code><span x-text="t('country_date_name_a98540')"></span></code> <span x-text="t('headers_use_country_codes_from_the_same_directory_as_abe68d')"></span> <strong><span x-text="t('cy_c44de8')"></span></strong> <span x-text="t('cyprus_and_834749')"></span> <strong><span x-text="t('am_80d305')"></span></strong> <span x-text="t('armenia_existing_dates_will_never_be_overwritten_5250bb')"></span></p>
<details>
<summary><span x-text="t('country_codes_csv_example_b6cb8a')"></span></summary>
<div class="two-columns">
<label><span x-text="t('find_a_country_bec93b')"></span><select @change="form.csvCountry=$event.target.value">
<option value="" x-text="t('choose_a_country_7843c8')"></option>
<template x-for="c in countries">
<option :value="c.code" x-text="c.name+' · '+c.code">
</option>
</template>
</select>
</label>
<p class="muted" x-text="form.csvCountry || t('cy_cyprus_am_armenia_8c0034')">
</p>
</div>
<pre><span x-text="t('country_date_name_cy_2026_10_01_independence_day_am__be8930')"></span></pre>
</details>
<label><span x-text="t('holiday_csv_222225')"></span><textarea class="csv-input" :aria-label="t('holiday_csv_222225')" x-model="csvText" @input="csvChanged()" :placeholder="t('country_date_name_cy_2026_12_25_christmas_day_b4543f')" spellcheck="false">
</textarea>
</label>
<div aria-live="polite">
<p class="muted" x-show="csvLoading"><span x-text="t('comparing_with_saved_holidays_ad992c')"></span></p>
<p class="danger-text" x-show="csvError" x-text="l(csvError)">
</p>
<template x-if="csvResult">
<div>
<div class="import-summary">
<strong x-text="t('holiday_count',{count:csvResult.preview.additions.length})">
</strong>
<span x-text="csvResult.preview.existing+t('already_saved_2ba31a')">
</span>
<span x-text="csvResult.preview.duplicates+t('repeated_rows_60187e')">
</span>
</div>
<p class="muted"><span x-text="t('nothing_is_saved_until_you_choose_import_996b73')"></span></p>
<template x-for="message in csvResult.preview.errors">
<p class="validation-error" x-text="l(message)">
</p>
</template>
<template x-for="message in csvResult.preview.conflicts">
<p class="validation-warning" x-text="l(message)">
</p>
</template>
<div class="import-table" x-show="csvResult.preview.additions.length">
<div class="import-table-head">
<span><span x-text="t('country_d523eb')"></span></span>
<span><span x-text="t('date_eb9a4b')"></span></span>
<span><span x-text="t('will_be_added_5399ac')"></span></span>
</div>
<template x-for="h in csvResult.preview.additions" :key="h.country+h.date">
<div>
<span x-text="countryName(h.country)">
</span>
<time x-text="formatDate(h.date)">
</time>
<span x-text="h.name">
</span>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</template>
<template x-if="modal==='member'">
<div>
<label><span x-text="t('registered_username_e29aa9')"></span><input x-model="form.username" required>
</label>
<p class="muted"><span x-text="t('your_teammate_needs_an_account_before_you_can_add_th_80fa32')"></span></p>
<template x-for="grant in grantOptions">
<label class="check">
<input type="checkbox" :value="grant.id" x-model="form.permissions" :disabled="grant.id==='view'">
<span x-text="grant.name">
</span>
</label>
</template>
</div>
</template>
<template x-if="modal==='preferences'">
<div>
<label><span x-text="t('team_name_9b11ed')"></span><input x-model="form.name" required>
</label>
<label><span x-text="t('planning_horizon_days_fc8284')"></span><input type="number" min="7" max="366" x-model="form.horizon_days" required>
</label>
<p class="info-note" x-text="t('schedule_timezone_003979')+doc.timezone+t('change_it_in_workspace_working_interval_settings_eac_1779f8')">
</p>
</div>
</template>
<template x-if="modal==='remove-schedule' && removal"><div class="removal-preview">
<p><span x-text="t('remove_e96390')"></span> <strong x-text="removal.plan.schedule.name"></strong> <span x-text="t('from_the_current_configuration_past_assignments_and__e2a3f7')"></span></p>
<p class="validation-error" x-show="removal.plan.active_replacements.length"><span x-text="t('this_schedule_has_current_or_future_approved_replace_6bfa3a')"></span></p>
<div class="dependency-list" x-show="removal.plan.escalation_steps.length"><h3><span x-text="t('used_by_escalation_steps_3e2542')"></span></h3><template x-for="step in removal.plan.escalation_steps" :key="step.policy_id+':'+step.step_number"><div class="list-row"><strong class="grow" x-text="step.policy_name"></strong><span class="badge" x-text="t('step_37d11c')+step.step_number+' · '+step.delay_minutes+t('min_f4e8a8')"></span></div></template></div>
<div class="dependency-list" x-show="removal.plan.coverage_fallbacks.length"><h3><span x-text="t('used_as_a_coverage_fallback_8955da')"></span></h3><template x-for="s in removal.plan.coverage_fallbacks" :key="s.id"><p x-text="s.name"></p></template></div>
<label><span x-text="t('existing_references_748fbb')"></span><select :aria-label="t('existing_references_748fbb')" x-model="form.redirect_to"><option value="" x-text="t('remove_the_affected_escalation_steps_8586b1')"></option><template x-for="s in doc.schedules.filter(s=>s.id!==form.id)" :key="s.id"><option :value="s.id" :selected="form.redirect_to===s.id" x-text="t('use_8023ca')+s.name+t('instead_cd9ec2')"></option></template></select></label>
<p class="info-note" x-show="form.redirect_to"><span x-text="t('all_escalation_steps_and_coverage_fallbacks_that_ref_8ca984')"></span></p>
<p class="validation-warning" x-show="!form.redirect_to && removal.plan.coverage_fallbacks.length"><span x-text="t('choose_another_schedule_for_the_coverage_fallbacks_b_61dbff')"></span></p>
<div class="validation-warning" x-show="!form.redirect_to && removal.plan.policies_emptied.length"><span x-text="t('these_policies_would_have_no_steps_left_choose_a_rep_2ce922')"></span><template x-for="p in removal.plan.policies_emptied" :key="p.id"><p x-text="p.name"></p></template></div>
<p class="info-note" x-show="!form.redirect_to && !removal.plan.escalation_steps.length && !removal.plan.coverage_fallbacks.length"><span x-text="t('no_escalation_or_fallback_references_need_changing_b2018c')"></span></p>
<p class="validation-warning" x-show="removal.plan.pending_requests.length" x-text="removal.plan.pending_requests.length+t('pending_replacement_requests_will_be_cancelled_in_fu_a1f178')"></p>
<p class="muted"><span x-text="t('the_schedule_and_these_dependency_changes_are_saved__42e786')"></span></p>
</div></template>
<template x-if="modal==='policy'">
<div>
<label><span x-text="t('policy_name_101bf6')"></span><input x-model="form.name" required>
</label>
<template x-for="(step,index) in form.steps">
<div class="step-editor">
<div class="flow-arrow" x-show="index"><span x-text="t('then_if_the_condition_still_holds_0fa74f')"></span></div>
<div class="two-columns">
<label><span x-text="t('after_minutes_d33505')"></span><input type="number" min="0" max="1440" x-model="step.delay_minutes" required>
</label>
<label><span x-text="t('condition_2f4979')"></span><select x-model="step.condition">
<option value="if-not-acked" x-text="t('if_not_acknowledged_5fc93a')"></option>
<option value="if-not-closed" x-text="t('if_not_closed_4369ad')"></option>
</select>
</label>
</div>
<label><span x-text="t('notify_6c03ee')"></span><select x-model="step.target">
<optgroup label="Schedules">
<template x-for="s in doc.schedules">
<option :value="'schedule:'+s.id" :selected="step.target===('schedule:'+s.id)" x-text="s.name">
</option>
</template>
</optgroup>
<optgroup label="Employees">
<template x-for="p in people">
<option :value="'user:'+p.id" :selected="step.target===('user:'+p.id)" x-text="p.display_name">
</option>
</template>
</optgroup>
<option value="team_all" x-text="t('everyone_on_the_team_235b55')"></option>
<option value="team_random" x-text="t('a_random_team_member_8f35f3')"></option>
</select>
</label>
<button type="button" class="text danger" @click="form.steps.splice(index,1)"><span x-text="t('remove_step_088d48')"></span></button>
</div>
</template>
<button type="button" @click="form.steps.push({delay_minutes:30,condition:'if-not-acked',target:'team_all'})"><span x-text="t('escalation_step_72b706')"></span></button>
<div class="two-columns spaced">
<label><span x-text="t('repeat_count_912e08')"></span><input type="number" min="0" max="20" x-model="form.repeat.count">
</label>
<label><span x-text="t('wait_between_repeats_0_disables_2e1378')"></span><input type="number" min="0" max="1440" x-model="form.repeat.wait_minutes">
</label>
</div>
<label class="check">
<input type="checkbox" x-model="form.repeat.reset_recipient_states"><span x-text="t('reset_recipient_states_on_repeat_8152c6')"></span></label>
<label class="check">
<input type="checkbox" x-model="form.repeat.close_alert_after_all"><span x-text="t('close_alert_after_all_repeats_2bec22')"></span></label>
</div>
</template>
<template x-if="modal==='route'">
<div>
<div class="two-columns">
<label><span x-text="t('rule_name_9e84d7')"></span><input x-model="form.name" required>
</label>
<label><span x-text="t('position_cf1c85')"></span><select :aria-label="t('position_cf1c85')" x-model.number="form.position" required>
<template x-for="position in (doc.routes.length+(doc.routes.some(r=>r.id===form.id)?0:1))"><option :value="position" :selected="form.position===position" x-text="position===1?t('1_first_6d7898'):position"></option></template>
</select>
</label>
</div>
<label><span x-text="t('escalation_policy_bbb0f7')"></span><select x-model="form.escalation_id">
<option value="" x-text="t('do_not_notify_1a59a6')"></option>
<template x-for="p in doc.escalations">
<option :value="p.id" :selected="form.escalation_id===(p.id)" x-text="p.name">
</option>
</template>
</select>
</label>
<div class="logic-editor">
<div class="logic-header">
<span class="logic-key"><span x-text="t('if_7be63e')"></span></span>
<strong><span x-text="t('all_conditions_match_e5187c')"></span></strong>
<span class="badge" x-show="!form.conditions.length"><span x-text="t('any_alert_4496c3')"></span></span>
</div>
<template x-for="(c,index) in form.conditions">
<div class="condition-editor">
<span class="logic-and" x-text="index?t('and_c2f934'):t('when_e3eafa')">
</span>
<select :aria-label="t('field_c326a4')" x-model="c.field">
<option value="tags" x-text="t('tags_848eed')"></option>
<option value="priority" x-text="t('priority_886cbf')"></option>
<option value="message" x-text="t('message_68f414')"></option>
<option value="description" x-text="t('description_55f8eb')"></option>
</select>
<select :aria-label="t('operation_430d32')" x-model="c.operation">
<option value="contains" x-text="t('contains_33e15d')"></option>
<option value="equals" x-text="t('equals_09b6a6')"></option>
<option value="starts-with" x-text="t('starts_with_2a61f5')"></option>
</select>
<input :aria-label="t('value_8dce17')" x-model="c.value" required>
<label class="check">
<input type="checkbox" x-model="c.negate"><span x-text="t('not_ca1dd3')"></span></label>
<button type="button" @click="form.conditions.splice(index,1)" :aria-label="t('remove_condition_858fb1')"><span x-text="t('symbol_67fba2')"></span></button>
</div>
</template>
<button type="button" class="text" @click="form.conditions.push({field:'tags',operation:'contains',value:'',negate:false})"><span x-text="t('and_condition_63ce48')"></span></button>
</div>
<label class="check spaced">
<input type="checkbox" x-model="form.working"><span x-text="t('only_during_working_hours_fa1eb6')"></span></label>
<p class="info-note" x-show="form.working">
<strong x-text="workingSummary()">
</strong>
<br><span x-text="t('uses_the_shared_workspace_interval_change_it_in_team_01fcc4')"></span></p>
</div>
</template>
<template x-if="modal==='integration'">
<div>
<label><span x-text="t('provider_7ceee3')"></span><select x-model="form.provider">
<option value="mock" x-text="t('local_preview_no_external_writes_0c302e')"></option>
<option value="jsm" :disabled="demo" x-text="t('jira_service_management_071f90')"></option>
</select>
</label>
<div x-show="form.provider==='jsm'">
<label><span x-text="t('atlassian_cloud_id_829908')"></span><input x-model="form.cloud_id" :required="form.provider==='jsm'">
</label>
<label><span x-text="t('jsm_team_id_70f05e')"></span><input x-model="form.team_id" :required="form.provider==='jsm'">
</label>
<label class="check">
<input type="checkbox" x-model="form.advanced_escalations"><span x-text="t('premium_enterprise_advanced_escalations_available_483732')"></span></label>
<label><span x-text="t('atlassian_accounts_a9aeb6')"></span></label>
<p class="muted"><span x-text="t('emails_resolve_automatically_to_existing_active_atla_e073f6')"></span></p>
<template x-for="p in people">
<label>
<span x-text="p.display_name+' · '+(p.email || t('no_email_configured_b309ce'))">
</span>
<input x-model="form.users[p.id]" :placeholder="t('account_id_optional_when_email_is_available_d135c3')">
</label>
</template>
<p class="info-note"><span x-text="t('ask_your_deployment_administrator_to_configure_crede_782891')"></span></p>
<code class="secret-key" x-text="secretPrefix()+'_EMAIL / _TOKEN'">
</code>
</div>
</div>
</template>
<!-- GENERATION_MODALS -->
<div class="modal-footer cover-footer" x-show="modal==='request' && Date.parse(form.end)>Date.now() && (can('request_overrides') || can('force_overrides'))"><div class="form-validation" x-data="{validationScope:'form'}"><!-- VALIDATION_STATUS --></div>
<button type="button" @click="modal=''"><span x-text="t('cancel_77dfd2')"></span></button>
<button type="button" class="force-action" x-show="can('force_overrides') && !form.force_mode" @click="form.force_mode=true"><span x-text="t('apply_without_approval_d72926')"></span></button>
<button type="button" x-show="form.force_mode && can('request_overrides')" @click="form.force_mode=false;form.to_user=form.from_user===user?.id?'':user?.id"><span x-text="t('request_approval_instead_b84265')"></span></button>
<button type="button" class="primary" @click="sendQuickRequest()" :disabled="busy || !canSendRequest" x-show="can('request_overrides') || form.force_mode" x-text="form.force_mode?t('apply_now_a356cc'):matchingRequest && !form.custom_range && !form.swap?t('accept_cover_d9a527'):form.from_user===user?.id?t('send_request_a89d64'):t('offer_to_cover_054c95')"></button>
</div>
<div class="modal-footer" x-show="!['request','day'].includes(modal)">
<div class="form-validation" x-data="{validationScope:'form'}"><!-- VALIDATION_STATUS --></div>
<button type="button" x-show="modal==='account'" @click="modal='';signOut()"><span x-text="t('sign_out_dc1649')"></span></button>
<button type="button" @click="modal=''"><span x-text="t('cancel_77dfd2')"></span></button>
<button type="button" class="primary" x-show="rulePeopleTarget" @click="closeRuleEditor()" x-text="t('done_e9b450')"></button>
<button x-show="!rulePeopleTarget" class="primary" :class="{'remove-confirm':modal==='remove-schedule'}" :disabled="busy || (modal==='holiday-import' && !canImport) || (modal==='absence' && !canSaveAbsence) || (modal==='remove-schedule' && !canRemoveSchedule)" x-text="busy?t('saving_56a228'):modal==='remove-schedule'?(form.redirect_to?t('remove_redirect_references_f1a241'):removal?.plan.escalation_steps.length?t('remove_schedule_affected_steps_7348af'):t('remove_schedule_ef0a8a')):modal==='holiday-import'?t('import_new_holidays_b36db6'):modal==='request'?t('send_for_confirmation_f3df0f'):modal==='force'?t('apply_replacement_9b0845'):['variable','generation-rule'].includes(modal)?t('use_in_draft_a8f78c'):t('save_changes_179359')">
</button>
</div>
</div>
</form>
</section>
</div>
<!-- GENERATION_MAP -->
</body>
</html>
File diff suppressed because it is too large. Load diff
+20
View File
@@ -0,0 +1,20 @@
<div class="resource-widget">
<template x-if="resourceKind==='schedule'"><div class="schedule-widget">
<div class="widget-meta"><span class="widget-type"><svg class="resource-icon" viewBox="0 0 24 24" aria-hidden="true"><path :d="resourceIcon(resource.type==='rotation'?'rotation':'schedule')"></path></svg><span x-text="label(resource.type)"></span></span><span x-show="resource.type==='rotation'" x-text="resource.type==='rotation'?t('resource_rotation_cycle',{days:resource.shift_days,date:formatDate(resource.anchor)}):''"></span><span x-text="resource.handover.slice(0,5)+' · '+doc.timezone"></span></div>
<div class="widget-chain" :class="{'rotation-chain':resource.type==='rotation'}">
<template x-for="(id,index) in resource.participants" :key="index"><div class="widget-node person-node"><span class="widget-order" x-text="index+1"></span><span x-text="person(id)"></span><small x-show="resource.type==='priority' && index===0" x-text="t('primary_a9a96e')"></small></div></template>
<template x-if="resource.type==='priority'"><div class="widget-node fallback-node"><svg class="resource-icon" viewBox="0 0 24 24" aria-hidden="true"><path :d="resourceIcon('fallback')"></path></svg><div><small x-text="t('resource_fallback_when')"></small><strong x-text="scheduleName(resource.fallback)"></strong></div></div></template>
</div>
</div></template>
<template x-if="resourceKind==='escalation'"><div class="escalation-widget">
<div class="widget-chain">
<div class="widget-node trigger-node"><svg class="resource-icon" viewBox="0 0 24 24" aria-hidden="true"><path :d="resourceIcon('escalation')"></path></svg><span x-text="t('resource_detail_trigger')"></span></div>
<template x-for="(step,index) in resource.steps" :key="index"><div class="widget-node escalation-node"><span class="widget-order" x-text="index+1"></span><div><small x-text="step.delay_minutes===0?t('immediately_21b60a'):step.delay_minutes+t('min_after_alert_8adeb2')"></small><strong x-text="describeRecipient(step.recipient)"></strong><small x-text="t(step.condition==='if-not-acked'?'if_unacknowledged_407d03':'if_still_open_c34949')"></small></div></div></template>
</div>
<div class="widget-meta widget-end"><svg class="resource-icon" viewBox="0 0 24 24" aria-hidden="true"><path :d="resourceIcon('rotation')"></path></svg><span x-text="resource.repeat.count && resource.repeat.wait_minutes?t('repeat_654579')+resource.repeat.count+t('times_wait_e1b163')+resource.repeat.wait_minutes+t('min_f4e8a8'):t('end_of_policy_no_repeats_c2ac4f')"></span></div>
</div></template>
<template x-if="resourceKind==='route'"><div class="routing-widget widget-chain">
<div class="widget-node routing-conditions"><svg class="resource-icon" viewBox="0 0 24 24" aria-hidden="true"><path :d="resourceIcon('condition')"></path></svg><div><small x-text="t('resource_all_conditions')"></small><div class="widget-condition-list"><span x-show="!resource.conditions.length" x-text="t('any_alert_4496c3')"></span><template x-for="(condition,index) in resource.conditions" :key="index"><span class="widget-condition" x-text="conditionText(condition)"></span></template><span x-show="resource.window" class="widget-condition" x-text="resource.window?workingSummary():''"></span></div></div></div>
<div class="widget-node route-destination"><svg class="resource-icon" viewBox="0 0 24 24" aria-hidden="true"><path :d="resourceIcon('route')"></path></svg><div><small x-text="t('resource_detail_destination')"></small><strong x-text="doc.escalations.find(p=>p.id===resource.escalation_id)?.name || t('do_not_notify_1a59a6')"></strong></div></div>
</div></template>
</div>
+43
View File
@@ -0,0 +1,43 @@
<section id="schedules" class="setup-section resource-inventory" x-show="view==='configuration'">
<div class="section-line"><h2><span x-text="t('setup_resources')"></span><span class="section-count" x-text="doc.schedules.length+doc.escalations.length+doc.routes.length"></span></h2>
<div class="actions resource-create" x-show="can('edit_schedules')">
<details class="schedule-create"><summary role="button" x-text="t('create_schedule')"></summary><div class="schedule-create-options"><button @click="open('rotation');$el.closest('details').open=false" x-text="t('rotation_b83874')"></button><button @click="open('priority');$el.closest('details').open=false" x-text="t('project_coverage_6d54f7')"></button></div></details>
<button @click="open('policy')" x-text="t('create_escalation')"></button><button @click="open('route')" x-text="t('create_routing')"></button>
</div></div>
<div class="resource-table" x-ref="routingList"><template x-for="isGenerated in [false,true]"><div class="resource-group" :class="{'generated-group':isGenerated}" x-show="[...doc.schedules,...doc.escalations,...doc.routes].some(r=>generated(r.id)===isGenerated)">
<h3 class="resource-group-label" x-text="isGenerated?t('generated_resources'):t('manual_resources')"></h3>
<template x-for="s in doc.schedules.filter(r=>generated(r.id)===isGenerated)" :key="s.id"><details class="resource-disclosure resource-row " :class="isGenerated?'generated-resource':'manual-resource'" data-resource-kind="schedules" >
<summary class="resource-summary"><span class="resource-kind"><svg class="resource-icon" viewBox="0 0 24 24" aria-hidden="true"><path :d="resourceIcon('schedule')"></path></svg><span x-text="t('resource_schedule')"></span></span><div class="resource-title"><strong class="resource-name" x-text="s.name"></strong><small class="resource-brief" x-text="resourceBrief(s,'schedule')" :title="resourceBrief(s,'schedule')"></small></div><span class="generated-mark" x-show="isGenerated" :title="t('generated_resource')" :aria-label="t('generated_resource')" x-text="t('generated_symbol')"></span></summary>
<div class="resource-body"><div class="resource-source" x-show="isGenerated"><button class="source-rule-link" :disabled="!can('edit_schedules')" @click="editGenerated(s.id)" x-text="t('generated_by_rule',{name:sourceRuleName(s.id)})"></button></div><div x-data="{resource:s,resourceKind:'schedule'}" x-effect="resource=s"><!-- RESOURCE_DETAILS --></div>
<div class="actions resource-actions" x-show="can('edit_schedules') && !isGenerated">
<button x-show="!generated(s.id)" @click="open(s.type,s)"><span x-text="t('edit_530164')"></span></button>
<button class="text danger" x-show="!generated(s.id)" @click="removeItem('schedules',s.id)"><span x-text="t('remove_e96390')"></span></button>
</div>
</div></details></template>
<template x-for="p in doc.escalations.filter(r=>generated(r.id)===isGenerated)" :key="p.id"><details class="resource-disclosure resource-row " :class="isGenerated?'generated-resource':'manual-resource'" data-resource-kind="escalations" >
<summary class="resource-summary"><span class="resource-kind"><svg class="resource-icon" viewBox="0 0 24 24" aria-hidden="true"><path :d="resourceIcon('escalation')"></path></svg><span x-text="t('resource_escalation')"></span></span><div class="resource-title"><strong class="resource-name" x-text="p.name"></strong><small class="resource-brief" x-text="resourceBrief(p,'escalation')" :title="resourceBrief(p,'escalation')"></small></div><span class="generated-mark" x-show="isGenerated" :title="t('generated_resource')" :aria-label="t('generated_resource')" x-text="t('generated_symbol')"></span></summary>
<div class="resource-body"><div class="resource-source" x-show="isGenerated"><button class="source-rule-link" :disabled="!can('edit_schedules')" @click="editGenerated(p.id)" x-text="t('generated_by_rule',{name:sourceRuleName(p.id)})"></button></div><div x-data="{resource:p,resourceKind:'escalation'}" x-effect="resource=p"><!-- RESOURCE_DETAILS --></div>
<div class="actions resource-actions" x-show="can('edit_schedules') && !isGenerated">
<button x-show="!generated(p.id)" @click="open('policy',p)"><span x-text="t('edit_530164')"></span></button>
<button class="text danger" x-show="!generated(p.id)" @click="removeItem('escalations',p.id)"><span x-text="t('remove_e96390')"></span></button>
</div>
</div></details></template>
<div class="routing-list" :aria-label="t('routing_rules_in_evaluation_order_582c46')"><template x-for="r in orderedRoutes.filter(r=>generated(r.id)===isGenerated)" :key="r.id"><details class="resource-disclosure resource-row routing-card" :class="{'generated-resource':isGenerated,'manual-resource':!isGenerated,'route-dragging':draggedRoute===r.id,'drop-before':routeDropTarget===r.id && routeDropSide==='before','drop-after':routeDropTarget===r.id && routeDropSide==='after'}" data-resource-kind="routes" :data-route-id="r.id" @dragover.prevent="overRoute($event,r.id)" @drop.prevent.stop="dropRoute($event,r.id)">
<summary class="resource-summary"><span class="resource-kind"><svg class="resource-icon" viewBox="0 0 24 24" aria-hidden="true"><path :d="resourceIcon('route')"></path></svg><span x-text="t('resource_routing')"></span></span><div class="resource-title"><strong class="resource-name"><span class="route-position" x-text="orderedRoutes.findIndex(item=>item.id===r.id)+1"></span><span x-text="r.name"></span></strong><small class="resource-brief" x-text="resourceBrief(r,'route')" :title="resourceBrief(r,'route')"></small></div><span class="generated-mark" x-show="isGenerated" :title="t('generated_resource')" :aria-label="t('generated_resource')" x-text="t('generated_symbol')"></span></summary>
<div class="resource-body"><div class="resource-source" x-show="isGenerated"><button class="source-rule-link" :disabled="!can('edit_schedules')" @click="editGenerated(r.id)" x-text="t('generated_by_rule',{name:sourceRuleName(r.id)})"></button></div><div x-data="{resource:r,resourceKind:'route'}" x-effect="resource=r"><!-- RESOURCE_DETAILS --></div>
<div class="route-heading" x-show="can('edit_schedules') && !generated(r.id)">
<button type="button" class="drag-handle" x-show="can('edit_schedules') && !generated(r.id)" :draggable="!busy" :disabled="busy" @dragstart="startRouteDrag($event,r.id)" @dragend="clearRouteDrag()" :aria-label="t('drag_72143a')+r.name+t('to_reorder_64a607')" :title="t('drag_to_reorder_e7541f')"><span x-text="t('symbol_40b24b')"></span></button>
<span class="minute route-position" x-text="orderedRoutes.findIndex(item=>item.id===r.id)+1" :aria-label="t('position_713830')+(orderedRoutes.findIndex(item=>item.id===r.id)+1)">
</span>
<div class="route-moves" x-show="can('edit_schedules') && !generated(r.id)">
<button type="button" class="move-route" @click="moveRoute(r.id,-1)" :disabled="busy || orderedRoutes.findIndex(item=>item.id===r.id)===0" :aria-label="t('move_e402ac')+r.name+t('up_459409')" :title="t('move_up_b4f57c')"><span x-text="t('symbol_9555ff')"></span></button>
<button type="button" class="move-route" @click="moveRoute(r.id,1)" :disabled="busy || orderedRoutes.findIndex(item=>item.id===r.id)===orderedRoutes.length-1" :aria-label="t('move_e402ac')+r.name+t('down_edbf4b')" :title="t('move_down_260ff8')"><span x-text="t('symbol_fc4866')"></span></button>
</div>
<button x-show="can('edit_schedules') && !generated(r.id)" @click="open('route',r)"><span x-text="t('edit_530164')"></span></button>
<button class="text danger" x-show="can('edit_schedules') && !generated(r.id)" @click="removeItem('routes',r.id)"><span x-text="t('remove_e96390')"></span></button>
</div>
</div></details></template></div>
</div></template></div>
</section>
+6
View File
@@ -0,0 +1,6 @@
<div class="validation-control" x-data="{detailsOpen:false,check:{issues:[],label:'',loading:false,checked:false}}" x-effect="check=validationState(validationScope)">
<button type="button" class="validation-toggle" :class="{'has-errors':check.issues.length,'is-checked':check.checked && !check.issues.length,'is-checking':check.loading}" @click="detailsOpen=!detailsOpen" :aria-expanded="detailsOpen" :aria-label="t('validation_details')"><span aria-live="polite" x-text="check.label"></span><span x-text="detailsOpen?t('generation_hide_errors'):t('generation_show_errors')"></span></button>
<template x-if="detailsOpen"><div class="validation-details">
<p class="muted" x-show="!check.issues.length" x-text="check.loading?t('cover_checking'):check.checked?t('validation_no_errors'):t('validation_on_save')"></p>
<template x-for="(issue,index) in check.issues" :key="index"><div class="validation-issue"><small x-show="issue.path" x-text="l(issue.path||'')"></small><strong x-text="l(issue.message)"></strong><small x-show="issue.secondary" x-text="t('validation_secondary')"></small><div class="actions"><template x-for="action in issueActions(issue)" :key="action.kind+action.id"><button type="button" @click="detailsOpen=false;openIssue(action)" x-text="t(action.kind==='rule'?'validation_edit_rule':'validation_edit_variable',{name:action.name})"></button></template><a x-show="issue.team_id && issue.team_id!==workspace?.id" :href="'?view=configuration&workspace='+issue.team_id" target="_blank" rel="noopener" x-text="t('validation_other_team')"></a></div></div></template>
</div></template></div>
+21
View File
@@ -0,0 +1,21 @@
# MIT License
Copyright © 2019-2021 Caleb Porzio and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+5
View File
@@ -0,0 +1,5 @@
Alpine.js 3.14.9, vendored from the npm package's `dist/cdn.min.js`.
Source: https://github.com/alpinejs/alpine/tree/v3.14.9
License: `ALPINE-LICENSE.md` (MIT).
The application serves this asset locally and has no runtime CDN dependency. Standard Alpine requires the documented CSP `unsafe-eval` allowance. A future move to Alpine's CSP build needs a browser regression run because expressions differ.
+5
View File
@@ -0,0 +1,5 @@
(()=>{var nt=!1,it=!1,W=[],ot=-1;function Ut(e){Rn(e)}function Rn(e){W.includes(e)||W.push(e),Mn()}function Wt(e){let t=W.indexOf(e);t!==-1&&t>ot&&W.splice(t,1)}function Mn(){!it&&!nt&&(nt=!0,queueMicrotask(Nn))}function Nn(){nt=!1,it=!0;for(let e=0;e<W.length;e++)W[e](),ot=e;W.length=0,ot=-1,it=!1}var T,N,$,at,st=!0;function Gt(e){st=!1,e(),st=!0}function Jt(e){T=e.reactive,$=e.release,N=t=>e.effect(t,{scheduler:r=>{st?Ut(r):r()}}),at=e.raw}function ct(e){N=e}function Yt(e){let t=()=>{};return[n=>{let i=N(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),$(i))},i},()=>{t()}]}function ve(e,t){let r=!0,n,i=N(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>$(i)}var Xt=[],Zt=[],Qt=[];function er(e){Qt.push(e)}function te(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Zt.push(t))}function Ae(e){Xt.push(e)}function Oe(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function lt(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function tr(e){for(e._x_effects?.forEach(Wt);e._x_cleanups?.length;)e._x_cleanups.pop()()}var ut=new MutationObserver(mt),ft=!1;function ue(){ut.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ft=!0}function dt(){kn(),ut.disconnect(),ft=!1}var le=[];function kn(){let e=ut.takeRecords();le.push(()=>e.length>0&&mt(e));let t=le.length;queueMicrotask(()=>{if(le.length===t)for(;le.length>0;)le.shift()()})}function m(e){if(!ft)return e();dt();let t=e();return ue(),t}var pt=!1,Se=[];function rr(){pt=!0}function nr(){pt=!1,mt(Se),Se=[]}function mt(e){if(pt){Se=Se.concat(e);return}let t=[],r=new Set,n=new Map,i=new Map;for(let o=0;o<e.length;o++)if(!e[o].target._x_ignoreMutationObserver&&(e[o].type==="childList"&&(e[o].removedNodes.forEach(s=>{s.nodeType===1&&s._x_marker&&r.add(s)}),e[o].addedNodes.forEach(s=>{if(s.nodeType===1){if(r.has(s)){r.delete(s);return}s._x_marker||t.push(s)}})),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{lt(s,o)}),n.forEach((o,s)=>{Xt.forEach(a=>a(s,o))});for(let o of r)t.some(s=>s.contains(o))||Zt.forEach(s=>s(o));for(let o of t)o.isConnected&&Qt.forEach(s=>s(o));t=null,r=null,n=null,i=null}function Ce(e){return z(B(e))}function k(e,t,r){return e._x_dataStack=[t,...B(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function B(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?B(e.host):e.parentNode?B(e.parentNode):[]}function z(e){return new Proxy({objects:e},Dn)}var Dn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?Pn:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function Pn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Te(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Re(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>In(n,i),s=>ht(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function In(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function ht(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),ht(e[t[0]],t.slice(1),r)}}var ir={};function y(e,t){ir[e]=t}function fe(e,t){let r=Ln(t);return Object.entries(ir).forEach(([n,i])=>{Object.defineProperty(e,`$${n}`,{get(){return i(t,r)},enumerable:!1})}),e}function Ln(e){let[t,r]=_t(e),n={interceptor:Re,...t};return te(e,r),n}function or(e,t,r,...n){try{return r(...n)}catch(i){re(i,e,t)}}function re(e,tLine truncated
${r?'Expression: "'+r+`"
`:""}`,t),setTimeout(()=>{throw e},0)}var Me=!0;function ke(e){let t=Me;Me=!1;let r=e();return Me=t,r}function R(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return sr(...e)}var sr=xt;function ar(e){sr=e}function xt(e,t){let r={};fe(r,e);let n=[r,...B(e)],i=typeof t=="function"?$n(n,t):Fn(n,t,e);return or.bind(null,e,t,i)}function $n(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(z([n,...e]),i);Ne(r,o)}}var gt={};function jn(e,t){if(gt[e])return gt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return re(s,t,e),Promise.resolve()}})();return gt[e]=o,o}function Fn(e,t,r){let n=jn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=z([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>re(l,r,t));n.finished?(Ne(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Ne(i,l,a,s,r)}).catch(l=>re(l,r,t)).finally(()=>n.result=void 0)}}}function Ne(e,t,r,n,i){if(Me&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Ne(e,s,r,n)).catch(s=>re(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var wt="x-";function C(e=""){return wt+e}function cr(e){wt=e}var De={};function d(e,t){return De[e]=t,{before(r){if(!De[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=G.indexOf(r);G.splice(n>=0?n:G.indexOf("DEFAULT"),0,e)}}}function lr(e){return Object.keys(De).includes(e)}function pe(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=Et(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(dr((o,s)=>n[o]=s)).filter(mr).map(zn(n,r)).sort(Kn).map(o=>Bn(e,o))}function Et(e){return Array.from(e).map(dr()).filter(t=>!mr(t))}var yt=!1,de=new Map,ur=Symbol();function fr(e){yt=!0;let t=Symbol();ur=t,de.set(t,[]);let r=()=>{for(;de.get(t).length;)de.get(t).shift()();de.delete(t)},n=()=>{yt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Yt(e);return t.push(i),[{Alpine:K,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:R.bind(R,e)},()=>t.forEach(a=>a())]}function Bn(e,t){let r=()=>{},n=De[t.type]||r,[i,o]=_t(e);Oe(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),yt?de.get(ur).push(n):n())};return s.runCleanups=o,s}var Pe=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Ie=e=>e;function dr(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=pr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var pr=[];function ne(e){pr.push(e)}function mr({name:e}){return hr().test(e)}var hr=()=>new RegExp(`^${wt}([^:^.]+)\\b`);function zn(e,t){return({name:r,value:n})=>{let i=r.match(hr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var bt="DEFAULT",G=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",bt,"teleport"];function Kn(e,t){let r=G.indexOf(e.type)===-1?bt:e.type,n=G.indexOf(t.type)===-1?bt:t.type;return G.indexOf(r)-G.indexOf(n)}function J(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function D(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>D(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)D(n,t,!1),n=n.nextElementSibling}function E(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var _r=!1;function gr(){_r&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),_r=!0,document.body||E("Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine's `<script>` tag?"),J(document,"alpine:init"),J(document,"alpine:initializing"),ue(),er(t=>S(t,D)),te(t=>P(t)),Ae((t,r)=>{pe(t,r).forEach(n=>n())});let e=t=>!Y(t.parentElement,!0);Array.from(document.querySelectorAll(br().join(","))).filter(e).forEach(t=>{S(t)}),J(document,"alpine:initialized"),setTimeout(()=>{Vn()})}var vt=[],xr=[];function yr(){return vt.map(e=>e())}function br(){return vt.concat(xr).map(e=>e())}function Le(e){vt.push(e)}function $e(e){xr.push(e)}function Y(e,t=!1){return j(e,r=>{if((t?br():yr()).some(i=>r.matches(i)))return!0})}function j(e,t){if(e){if(t(e))return e;if(e._x_teleportBack&&(e=e._x_teleportBack),!!e.parentElement)return j(e.parentElement,t)}}function wrLine truncated
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2016-2025, The Cytoscape Consortium.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+31
View File
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2016-2025, The Cytoscape Consortium.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the “Software”), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).cytoscape=t()}(this,(function(){"use strict";function e(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t,n){return t&&function(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,s(r.key),r)}}(e.prototype,t),Object.defineProperty(e,"prototype",{writable:!1}),e}function r(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=u(e))||t){n&&(e=n);var r=0,a=function(){};return{s:a,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,o=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,i=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw i}}}}function a(e,t,n){return(t=s(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var r,a,i,o,s=[],l=!0,u=!1;try{if(i=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;l=!1}else for(;!(l=(r=i.call(n)).done)&&(s.push(r.value),s.length!==t);l=!0);}catch(e){u=!0,a=e}finally{try{if(!l&&null!=n.return&&(o=n.return(),Object(o)!==o))return}finally{if(u)throw a}}return s}}(e,t)||u(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(t){return function(t){if(Array.isArray(t))return e(t)}(t)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(t)||u(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t);if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(e)}(e,"string");return"symbol"==typeof t?t:t+""}function l(e){return l="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},l(e)}function u(t,n){if(t){if("string"==typeof t)return e(t,n);var r={}.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?e(t,n):void 0}}var c="undefined"==typeof window?null:window,d=c?c.navigator:null;c&&c.document;var h,f,p,v,g,y,m,b,x,w,E,k,T,C,P,S,B,D,_,A,M,R,I,N,L,z,O,V,F=l(""),X=l({}),j=l((function(){})),Y="undefined"==typeof HTMLElement?"undefined":l(HTMLElement),q=function(e){return e&&e.instanceString&&U(e.instanceString)?e.instanceString():null},W=function(e){return null!=e&&l(e)==F},U=function(e){return null!=e&&l(e)===j},H=function(e){return!$(e)&&(Array.isArray?Array.isArray(e):null!=e&&e instanceof Array)},K=function(e){return null!=e&&l(e)===X&&!H(e)&&e.constructor===Object},G=function(e){return null!=e&&l(e)===l(1)&&!isNaN(e)},Z=function(e){return"undefined"===Y?void 0:null!=e&&e instanceof HTMLElement},$=function(e){return Q(e)||J(e)},Q=function(e){return"collection"===q(e)&&e._private.single},J=function(e){return"collection"===q(e)&&!e._private.single},ee=function(e){return"core"===q(e)},te=function(e){return"stylesheet"===q(e)},ne=function(e){return null==e||!(""!==e&&!e.match(/^\s+$/))},re=function(e){return function(e){return null!=e&&l(e)===X}(e)&&U(e.then)},ae=function(e,t){t||(t=function(){if(1===arguments.length)return arguments[0];if(0===arguments.length)return"undefined";for(var e=[],t=0;t<arguments.length;t++)e.push(arguments[t]);return e.join("$")});var n=function(){var r,a=arguments,i=t.apply(this,a),o=n.cache;return(r=o[i])||(r=o[i]=e.apply(this,a)),r};return n.cache={},n},ie=ae((function(e){return e.replace(/([A-Z])/g,(function(e){return"-"+e.toLowerCase()}))})),oe=ae((function(e){return e.replace(/(-\w)/g,(function(e){return e[1].toUpperCase()}))})),se=ae((function(e,t){return e+t[0].toUpperCase()+t.substring(1)}),(function(e,t){return e+"$"+t})),le=function(e){return ne(e)?e:e.charAt(0).toUpperCase()+e.substring(1)},ue=function(eLine truncated
/*!
Embeddable Minimum Strictly-Compliant Promises/A+ 1.1.1 Thenable
Copyright (c) 2013-2014 Ralf S. Engelschall (http://engelschall.com)
Licensed under The MIT License (http://opensource.org/licenses/MIT)
*/
var Xr=function(e){if(!(this instanceof Xr))return new Xr(e);this.id="Thenable/1.0.7",this.state=0,this.fulfillValue=void 0,this.rejectReason=void 0,this.onFulfilled=[],this.onRejected=[],this.proxy={then:this.then.bind(this)},"function"==typeof e&&e.call(this,this.fulfill.bind(this),this.reject.bind(this))};Xr.prototype={fulfill:function(e){return jr(this,1,"fulfillValue",e)},reject:function(e){return jr(this,2,"rejectReason",e)},then:function(e,t){var n=this,r=new Xr;return n.onFulfilled.push(Wr(e,r,"fulfill")),n.onRejected.push(Wr(t,r,"reject")),Yr(n),r.proxy}};var jr=function(e,t,n,r){return 0===e.state&&(e.state=t,e[n]=r,Yr(e)),e},Yr=function(e){1===e.state?qr(e,"onFulfilled",e.fulfillValue):2===e.state&&qr(e,"onRejected",e.rejectReason)},qr=function(e,t,n){if(0!==e[t].length){var r=e[t];e[t]=[];var a=function(){for(var e=0;e<r.length;e++)r[e](n)};"function"==typeof setImmediate?setImmediate(a):setTimeout(a,0)}},Wr=function(e,t,n){return function(r){if("function"!=typeof e)t[n].call(t,r);else{var a;try{a=e(r)}catch(e){return void t.reject(e)}Ur(t,a)}}},Ur=function(e,t){if(e!==t&&e.proxy!==t){var n;if("object"===l(t)&&null!==t||"function"==typeof t)try{n=t.then}catch(t){return void e.reject(t)}if("function"!=typeof n)e.fulfill(t);else{var r=!1;try{n.call(t,(function(n){r||(r=!0,n===t?e.reject(new TypeError("circular thenable chain")):Ur(e,n))}),(function(t){r||(r=!0,e.reject(t))}))}catch(t){r||e.reject(t)}}}else e.reject(new TypeError("cannot resolve promise with itself"))};Xr.all=function(e){return new Xr((function(t,n){for(var r=new Array(e.length),a=0,i=function(n,i){r[n]=i,++a===e.length&&t(r)},o=0;o<e.length;o++)!function(t){var r=e[t];null!=r&&null!=r.then?r.then((function(e){i(t,e)}),(function(e){n(e)})):i(t,r)}(o)}))},Xr.resolve=function(e){return new Xr((function(t,n){t(e)}))},Xr.reject=function(e){return new Xr((function(t,n){n(e)}))};var Hr="undefined"!=typeof Promise?Promise:Xr,Kr=function(e,t,n){var r=ee(e),a=!r,i=this._private=ge({duration:1e3},t,n);if(i.target=e,i.style=i.style||i.css,i.started=!1,i.playing=!1,i.hooked=!1,i.applying=!1,i.progress=0,i.completes=[],i.frames=[],i.complete&&U(i.complete)&&i.completes.push(i.complete),a){var o=e.position();i.startPosition=i.startPosition||{x:o.x,y:o.y},i.startStyle=i.startStyle||e.cy().style().getAnimationStartStyle(e,i.style)}if(r){var s=e.pan();i.startPan={x:s.x,y:s.y},i.startZoom=e.zoom()}this.length=1,this[0]=this},Gr=Kr.prototype;ge(Gr,{instanceString:function(){return"animation"},hook:function(){var e=this._private;if(!e.hooked){var t=e.target._private.animation;(e.queue?t.queue:t.current).push(this),$(e.target)&&e.target.cy().addToAnimationPool(e.target),e.hooked=!0}return this},play:function(){var e=this._private;return 1===e.progress&&(e.progress=0),e.playing=!0,e.started=!1,e.stopped=!1,this.hook(),this},playing:function(){return this._private.playing},apply:function(){var e=this._private;return e.applying=!0,e.started=!1,e.stopped=!1,this.hook(),this},applying:function(){return this._private.applying},pause:function(){var e=this._private;return e.playing=!1,e.started=!1,this},stop:function(){var e=this._private;return e.playing=!1,e.started=!1,e.stopped=!0,this},rewind:function(){return this.progress(0)},fastforward:function(){return this.progress(1)},time:function(e){var t=this._private;return void 0===e?t.progress*t.duration:this.progress(e/t.duration)},progress:function(e){var t=this._private,n=t.playing;return void 0===e?t.progress:(n&&this.pause(),t.progress=e,t.started=!1,n&&this.play(),this)},completed:function(){return 1===this._private.progress},reverse:function(){var e=this._private,t=e.playing;t&&this.pause(),e.progress=1-e.progress,e.started=!1;var n=function(t,n){var r=e[t];null!=r&&(e[t]=e[n],e[n]=r)};if(n("zoom","startZoom"),n("pan","startPan"),n("position","startPosition"),e.style)for(var r=0;r<e.style.length;r++){var a=e.style[r],i=a.name,o=e.startStyle[i];e.startStyle[i]=a,e.style[r]=o}return t&&this.play(),this},promise:function(e){var t,n=this._private;if("frame"===e)t=n.frames;else t=n.completes;return new Hr((function(e,n){t.push((function(){e()}))}))}}),Gr.complete=Gr.completed,Gr.run=Gr.play,Gr.running=Gr.playing;var Zr,$r,Qr,Jr,ea,ta,na,ra,aa,ia,oa,sa,la,ua,ca,da,ha,fa,pa,va,ga,ya,ma,ba,xa,wa,Ea,ka,Ta,Ca,Pa,Sa,Ba,Da,_a,Aa,Ma,Ra,Ia,Na,La,za,Oa,Va,Fa,Xa,ja,Ya,qa,Wa,Ua,Ha,Ka,Ga,Za,$a,Qa,Ja,ei,ti,ni,ri,ai,ii,oi,si,li,ui,ci,di,hi,fi,pi,vi,gi,yi,mi,bi,xi,wi,Ei,ki,Ti,Ci,Pi,Si,Bi={animated:function(){return function(){var e=this,t=void 0!==e.length?e:[e];if(!(this._private.cy||this).styleEnabled())return!1;var n=t[0];return n?n._private.animation.current.length>0:void 0}},clearQueue:function(){return function(){var e=this,t=void 0!==e.length?e:[e];if(!(this._private.cy||this).styleEnabled())return this;for(var n=0;n<t.length;n++){t[n]._private.animation.queue=[]}return this}},delay:function(){return function(e,t){return(this._private.cy||this).styleEnabled()?this.animate({delay:e,duration:e,complete:t}):this}},delaLine truncated
/*! Bezier curve function generator. Copyright Gaetan Renaudeau. MIT License: http://en.wikipedia.org/wiki/MIT_License */function Xl(e,t,n,r){var a=.1,i="undefined"!=typeof Float32Array;if(4!==arguments.length)return!1;for(var o=0;o<4;++o)if("number"!=typeof arguments[o]||isNaN(arguments[o])||!isFinite(arguments[o]))return!1;e=Math.min(e,1),n=Math.min(n,1),e=Math.max(e,0),n=Math.max(n,0);var s=i?new Float32Array(11):new Array(11);function l(e,t){return 1-3*t+3*e}function u(e,t){return 3*t-6*e}function c(e){return 3*e}function d(e,t,n){return((l(t,n)*e+u(t,n))*e+c(t))*e}function h(e,t,n){return 3*l(t,n)*e*e+2*u(t,n)*e+c(t)}function f(t){for(var r=0,i=1;10!==i&&s[i]<=t;++i)r+=a;--i;var o=r+(t-s[i])/(s[i+1]-s[i])*a,l=h(o,e,n);return l>=.001?function(t,r){for(var a=0;a<4;++a){var i=h(r,e,n);if(0===i)return r;r-=(d(r,e,n)-t)/i}return r}(t,o):0===l?o:function(t,r,a){var i,o,s=0;do{(i=d(o=r+(a-r)/2,e,n)-t)>0?a=o:r=o}while(Math.abs(i)>1e-7&&++s<10);return o}(t,r,r+a)}var p=!1;function v(){p=!0,e===t&&n===r||function(){for(var t=0;t<11;++t)s[t]=d(t*a,e,n)}()}var g=function(a){return p||v(),e===t&&n===r?a:0===a?0:1===a?1:d(f(a),t,r)};g.getControlPoints=function(){return[{x:e,y:t},{x:n,y:r}]};var y="generateBezier("+[e,t,n,r]+")";return g.toString=function(){return y},g}
/*! Runge-Kutta spring physics function generator. Adapted from Framer.js, copyright Koen Bok. MIT License: http://en.wikipedia.org/wiki/MIT_License */var jl=function(){function e(e){return-e.tension*e.x-e.friction*e.v}function t(t,n,r){var a={x:t.x+r.dx*n,v:t.v+r.dv*n,tension:t.tension,friction:t.friction};return{dx:a.v,dv:e(a)}}function n(n,r){var a={dx:n.v,dv:e(n)},i=t(n,.5*r,a),o=t(n,.5*r,i),s=t(n,r,o),l=1/6*(a.dx+2*(i.dx+o.dx)+s.dx),u=1/6*(a.dv+2*(i.dv+o.dv)+s.dv);return n.x=n.x+l*r,n.v=n.v+u*r,n}return function e(t,r,a){var i,o,s,l={x:-1,v:0,tension:null,friction:null},u=[0],c=0,d=1e-4;for(t=parseFloat(t)||500,r=parseFloat(r)||20,a=a||null,l.tension=t,l.friction=r,o=(i=null!==a)?(c=e(t,r))/a*.016:.016;s=n(s||l,o),u.push(1+s.x),c+=16,Math.abs(s.x)>d&&Math.abs(s.v)>d;);return i?function(e){return u[e*(u.length-1)|0]}:c}}(),Yl=function(e,t,n,r){var a=Xl(e,t,n,r);return function(e,t,n){return e+(t-e)*a(n)}},ql={linear:function(e,t,n){return e+(t-e)*n},ease:Yl(.25,.1,.25,1),"ease-in":Yl(.42,0,1,1),"ease-out":Yl(0,0,.58,1),"ease-in-out":Yl(.42,0,.58,1),"ease-in-sine":Yl(.47,0,.745,.715),"ease-out-sine":Yl(.39,.575,.565,1),"ease-in-out-sine":Yl(.445,.05,.55,.95),"ease-in-quad":Yl(.55,.085,.68,.53),"ease-out-quad":Yl(.25,.46,.45,.94),"ease-in-out-quad":Yl(.455,.03,.515,.955),"ease-in-cubic":Yl(.55,.055,.675,.19),"ease-out-cubic":Yl(.215,.61,.355,1),"ease-in-out-cubic":Yl(.645,.045,.355,1),"ease-in-quart":Yl(.895,.03,.685,.22),"ease-out-quart":Yl(.165,.84,.44,1),"ease-in-out-quart":Yl(.77,0,.175,1),"ease-in-quint":Yl(.755,.05,.855,.06),"ease-out-quint":Yl(.23,1,.32,1),"ease-in-out-quint":Yl(.86,0,.07,1),"ease-in-expo":Yl(.95,.05,.795,.035),"ease-out-expo":Yl(.19,1,.22,1),"ease-in-out-expo":Yl(1,0,0,1),"ease-in-circ":Yl(.6,.04,.98,.335),"ease-out-circ":Yl(.075,.82,.165,1),"ease-in-out-circ":Yl(.785,.135,.15,.86),spring:function(e,t,n){if(0===n)return ql.linear;var r=jl(e,t,n);return function(e,t,n){return e+(t-e)*r(n)}},"cubic-bezier":Yl};function Wl(e,t,n,r,a){if(1===r)return n;if(t===n)return n;var i=a(t,n,r);return null==e||((e.roundValue||e.color)&&(i=Math.round(i)),void 0!==e.min&&(i=Math.max(i,e.min)),void 0!==e.max&&(i=Math.min(i,e.max))),i}function Ul(e,t){return null!=e.pfValue||null!=e.value?null==e.pfValue||null!=t&&"%"===t.type.units?e.value:e.pfValue:e}function Hl(e,t,n,r,a){var i=null!=a?a.type:null;n<0?n=0:n>1&&(n=1);var o=Ul(e,a),s=Ul(t,a);if(G(o)&&G(s))return Wl(i,o,s,n,r);if(H(o)&&H(s)){for(var l=[],u=0;u<s.length;u++){var c=o[u],d=s[u];if(null!=c&&null!=d){var h=Wl(i,c,d,n,r);l.push(h)}else l.push(d)}return l}}function Kl(e,t,n,r){var a=!r,i=e._private,o=t._private,s=o.easing,l=o.startTime,u=(r?e:e.cy()).style();if(!o.easingImpl)if(null==s)o.easingImpl=ql.linear;else{var c,d,h;if(W(s))c=u.parse("transition-timing-function",s).value;else c=s;W(c)?(d=c,h=[]):(d=c[1],h=c.slice(2).map((function(e){return+e}))),h.length>0?("spring"===d&&h.push(o.duration),o.easingImpl=ql[d].apply(null,h)):o.easingImpl=ql[d]}var f,p=o.easingImpl;if(f=0===o.duration?1:(n-l)/o.duration,o.applying&&(f=o.progress),f<0?f=0:f>1&&(f=1),null==o.delay){var v=o.startPosition,g=o.position;if(g&&a&&!e.locked()){var y={};Gl(v.x,g.x)&&(y.x=Hl(v.x,g.x,f,p)),Gl(v.y,g.y)&&(y.y=Hl(v.y,g.y,f,p)),e.position(y)}var m=o.startPan,b=o.pan,x=i.pan,w=null!=b&&r;w&&(Gl(m.x,b.x)&&(x.x=Hl(m.x,b.x,f,p)),Gl(m.y,b.y)&&(x.y=Hl(m.y,b.y,f,p)),e.emit("pan"));var E=o.startZoom,k=o.zoom,T=null!=k&&r;T&&(Gl(E,k)&&(i.zoom=Qt(i.minZoom,Hl(E,k,f,p),i.maxZoom)),e.emit("zoom")),(w||T)&&e.emit("viewport");var C=o.style;if(C&&C.length>0&&a){for(var P=0;P<C.length;P++){var S=C[P],B=S.name,D=S,_=o.startStyle[B],A=Hl(_,D,f,p,u.properties[_.name]);u.overrideBypass(e,B,A)}e.emit("style")}}return o.progress=f,f}function Gl(e,t){return null!=e&&null!=t&&(!(!G(e)||!G(t))||!(!e||!t))}function Zl(e,t,n,r){var a=t._private;a.started=!0,a.startTime=n-a.progress*a.duration}function $l(e,t){var n=t._private.aniEles,r=[];function a(t,n){var a=t._private,i=a.animation.current,o=a.animation.queue,s=!1;if(0===i.length){var l=o.shift();l&&i.push(l)}for(var u=function(e){for(var t=e.length-1;t>=0;t--){(0,e[t])()}e.splice(0,e.length)},c=i.length-1;c>=0;c--){var d=i[c],h=d._private;h.stopped?(i.splice(c,1),h.hooked=!1,h.playing=!1,h.started=!1,u(h.frames)):(h.playing||h.applying)&&(h.playing&&h.applying&&(h.applying=!1),h.started||Zl(0,d,e),Kl(t,d,e,n),h.applying&&(h.applying=!1),u(h.frames),null!=h.step&&h.step(e),d.completed()&&(i.splice(c,1),h.hooked=!1,h.playing=!1,h.started=!1,u(h.completes)),s=!0)}return n||0!==i.length||0!==o.length||r.push(t),s}for(var i=!1,o=0;o<n.length;o++){var s=a(n[o]);i=i||s}var l=a(t,!0);(i||l)&&(n.length>0?t.notify("draw",n):t.notify("draw")),n.unmerge(r),t.emit("step")}var Ql={animate:xo.animate(),animation:xo.animation(),animated:xo.animated(),clearQueue:xo.clearQueue(),delay:xo.delay(),delayAnimation:xo.delayAnimation(),stop:xo.stop(),addToAnimationPool:function(e){this.styleEnabled()&&this._private.aniEles.merge(e)},stopAnimationLoop:function()Line truncated