Files
meguri/static/generation.js
T
ab e37d583179
Checks / test (push) Failing after 3m0s
Fixed CI
2026-09-24 16:24:50 +03:00

250 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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:typeof value?.name==='string'?value.name:typeof value?.title==='string'?value.title:Array.isArray(source)?String(Number(key)+1):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;},
};}