354 lines
46 KiB
JavaScript
354 lines
46 KiB
JavaScript
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: '', version: '', ready: false, busy: false, error: '', toast: '', demo: false, demoLifecycle:null, auth:{oidc:false,password:true}, loginError:null, admin:null, events:[], eventsMore:false, eventSearch:'', eventsLoading:false,
|
||
loginForm: { username: '', password: '' },
|
||
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: '⚙' }, { id: 'instance', name: tr('access_instance_nav'), 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 employeeGrantOptions(){return this.grantOptions.filter(g=>!['manage_members','manage_integration'].includes(g.id));},
|
||
canEditUserGroups(u){return !u.sso && !u.bootstrap && u.id!==this.user?.id;},
|
||
async loadAdmin(){this.admin=await this.api('/api/admin/overview');await this.loadEvents();},
|
||
async loadEvents(){const query=this.eventSearch.trim(),sequence=++this.eventSequence;this.eventsLoading=true;try{const result=await this.api('/api/admin/events'+(query?'?search='+encodeURIComponent(query):''));if(sequence===this.eventSequence){this.events=result.events;this.eventsMore=!!result.more;}}finally{if(sequence===this.eventSequence)this.eventsLoading=false;}},
|
||
eventSequence:0,
|
||
isFailedLogin(event){return event.action==='login.failed';},
|
||
eventLabel(key){return MEGURI_MESSAGES['access_event_key_'+key]?tr('access_event_key_'+key):key;},
|
||
eventDetails(event){const details=event.details;if(!details || typeof details!=='object')return [];const order=['reference','code','detail','username','email','subject','issuer','groups'];const keys=[...order.filter(k=>k in details),...Object.keys(details).filter(k=>!order.includes(k))];return keys.map(key=>{const value=details[key];return Array.isArray(value)?{key,chips:value.map(String),empty:!value.length}:{key,value:typeof value==='object' && value!==null?JSON.stringify(value):String(value??'')};});},
|
||
useDemoAccount(account){this.loginForm.username=account.username;this.loginForm.password=this.demoLifecycle?.password||'';},
|
||
async copyDemoPassword(){try{await navigator.clipboard.writeText(this.demoLifecycle?.password||'');this.toast=tr('access_demo_password_copied');setTimeout(()=>this.toast='',4500);}catch(e){this.loginForm.password=this.demoLifecycle?.password||'';}},
|
||
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); },
|
||
personById(id) { return this.people.find(p=>p.id===id) || this.people.find(p=>p.aliases?.includes(id)); },
|
||
person(id) { return this.personById(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 || p.aliases?.includes(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==='instance' && this.user && !this.user.instance_admin?'calendar':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==='instance'){if(this.user?.instance_admin)this.run(()=>this.loadAdmin());}if(v==='configuration')this.loadGenerationDraft();if(this.setupView)this.setupOpen=true;this.writeNavigation();});
|
||
this.consumeLoginError();
|
||
{const query=new URLSearchParams(location.search);this.eventSearch=(query.get('ref')||query.get('search')||'').slice(0,200);}
|
||
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.version = session.version || ''; this.user = session.user; this.csrf = session.csrf || ''; this.auth = session.auth || {oidc:false,password:true}; this.demo = session.demo;this.demoLifecycle=session.demo_lifecycle; if (this.user) { if(this.view==='instance' && !this.user.instance_admin)this.view='calendar'; await this.loadWorkspaces(); if(this.view==='instance')await this.loadAdmin(); } });
|
||
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);
|
||
},
|
||
consumeLoginError(){const url=new URL(location.href),code=url.searchParams.get('login_error');if(code===null)return;const ref=url.searchParams.get('ref')||'';this.loginError={code:['no_access','failed','unavailable'].includes(code)?code:'failed',ref:ref.slice(0,64)};url.searchParams.delete('login_error');url.searchParams.delete('ref');history.replaceState({},'',url.pathname+url.search+url.hash);},
|
||
async signIn() { await this.run(async () => { this.loginError=null; const s = await this.api('/api/login', 'POST', this.loginForm); this.user = s.user; this.csrf = s.csrf; this.loginForm.password = ''; await this.loadWorkspaces(); if(this.view==='instance'){if(this.user.instance_admin)await this.loadAdmin();else this.view='calendar';} }); },
|
||
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.admin = 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-permissions') this.form = { permissions: [...(this.doc.member_permissions || ['view','request_overrides','edit_schedules'])] };
|
||
if (type === 'admin-user') this.form = { username: '', display_name: '', password: '', email: '', groups: [] };
|
||
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 === '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-permissions') await this.api(this.endpoint('/member-permissions'), 'PUT', { revision, permissions: ['view', ...f.permissions.filter(p=>p!=='view')] });
|
||
else if (kind === 'admin-user') { const body={username:f.username,display_name:f.display_name,password:f.password,groups:f.groups};if(f.email.trim())body.email=f.email.trim();await this.api('/api/admin/users','POST',body);await this.loadAdmin(); }
|
||
else if (kind === 'admin-groups') { await this.api(`/api/admin/users/${f.id}/groups`,'PUT',{groups:f.groups});await this.loadAdmin(); }
|
||
else if (kind === 'admin-password') { await this.api(`/api/admin/users/${f.id}/password`,'POST',{password:f.password});await this.loadAdmin(); }
|
||
else if (kind === 'admin-delete') { await this.api(`/api/admin/workspaces/${f.id}`,'DELETE',{confirm:f.confirm});await this.loadAdmin(); }
|
||
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') : this.savedMessage(this.modal));
|
||
},
|
||
savedMessage(kind){const key={'admin-user':'access_user_created','admin-groups':'access_groups_saved','admin-password':'access_password_reset_done','admin-delete':'access_workspace_deleted'}[kind];return tr(key||'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(); },
|
||
}));
|
||
});
|
||
|