commit
a47c4a0398
96 files changed
+25721
No files matched your search
+196
@@ -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
@@ -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(); },
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;},
|
||||
};}
|
||||
@@ -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);}
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Vendored
+21
@@ -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.
|
||||
Vendored
+5
@@ -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.
|
||||
Vendored
+5
@@ -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
|
||||
Vendored
+19
@@ -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.
|
||||
Vendored
+31
@@ -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
|
||||
Reference in new issue
Block a user