31 lines
14 KiB
JavaScript
31 lines
14 KiB
JavaScript
|
|
(()=>{'use strict';
|
||
|
|
function init(){
|
||
|
|
const main=document.getElementById('createForm');if(!main)return;
|
||
|
|
const trigger=document.createElement('button');trigger.type='button';trigger.className='btn btn-outline-primary px-4 fw-bold';trigger.textContent='Kun tid';trigger.title='Kun tid · minus (-)';const caseCard=main.closest('.card');const modes=document.createElement('div');modes.className='d-flex gap-2 mb-3';modes.setAttribute('aria-label','Vælg oprettelse');const caseMode=document.createElement('button');caseMode.type='button';caseMode.className='btn btn-primary px-4 fw-bold';caseMode.textContent='Opret sag';caseMode.setAttribute('aria-pressed','true');trigger.setAttribute('aria-pressed','false');modes.append(caseMode,trigger);caseCard.before(modes);
|
||
|
|
const panel=document.createElement('section');panel.id='quickTimeOnly';panel.className='card mt-4';panel.hidden=true;
|
||
|
|
panel.innerHTML=`<div class="card-body"><div class="d-flex justify-content-between mb-3"><div><h4 class="mb-1">Kun tid</h4><p class="small text-muted mb-0">Små opgaver uden en sag. Firma og kontakt er valgfrie.</p></div><button type="button" class="btn btn-sm btn-outline-secondary" data-close>Luk</button></div><form data-form><div class="row g-3"><div class="col-md-6"><label class="form-label" for="qtMode">Handling</label><input type="hidden" name="mode" value="registered"><div class="d-flex flex-wrap gap-2" data-choice="mode" role="group" aria-label="Handling"><button type="button" class="btn btn-sm btn-outline-primary" data-value="registered" aria-pressed="true">Brugt tid</button><button type="button" class="btn btn-sm btn-outline-primary" data-value="planned" aria-pressed="false">Planlægning</button><button type="button" class="btn btn-sm btn-outline-primary" data-value="convert" aria-pressed="false">Konvertér planlagt tid</button></div></div><div class="col-md-6" data-plan-wrap hidden><label class="form-label" for="qtPlan">Planlagt opgave</label><input type="hidden" name="plan"><div data-plans data-choice="plan" class="d-flex flex-wrap gap-2" role="group" aria-label="Planlagt opgave"></div></div><div class="col-12"><label class="form-label" for="qtDescription">Hvad er tiden brugt på?</label><input id="qtDescription" name="description" class="form-control" placeholder="Fx internt møde" maxlength="2000" required></div><div class="col-md-3"><label class="form-label" for="qtMinutes">Minutter</label><input id="qtMinutes" name="minutes" type="number" min="1" max="1440" value="30" class="form-control" required></div><div class="col-md-5"><label class="form-label" for="qtStart">Dato og starttid</label><input id="qtStart" name="start" type="datetime-local" class="form-control" required></div><div class="col-md-4"><label class="form-label" for="qtPriority">Prioritet</label><input type="hidden" name="priority" value="normal"><div class="d-flex flex-wrap gap-2" data-choice="priority" role="group" aria-label="Prioritet"><button type="button" class="btn btn-sm btn-outline-primary" data-value="low" aria-pressed="false">Lav</button><button type="button" class="btn btn-sm btn-outline-primary" data-value="normal" aria-pressed="true">Normal</button><button type="button" class="btn btn-sm btn-outline-primary" data-value="high" aria-pressed="false">Høj</button><button type="button" class="btn btn-sm btn-outline-primary" data-value="urgent" aria-pressed="false">Haster</button></div></div><div class="col-md-6"><label class="form-label" for="qtCustomer">Firma · valgfrit</label><input id="qtCustomer" class="form-control" type="search" placeholder="Søg firma…" autocomplete="off"><div data-customers class="list-group mt-1"></div></div><div class="col-md-6"><label class="form-label" for="qtContact">Kontakt · valgfrit</label><input id="qtContact" class="form-control" type="search" placeholder="Søg kontakt…" autocomplete="off"><div data-contacts class="list-group mt-1"></div></div><div class="col-12"><span class="form-label d-block">Medarbejdere</span><div data-users class="d-flex gap-2 flex-wrap" role="group" aria-label="Medarbejdere"></div><div class="form-text">Hver valgt medarbejder får det angivne antal minutter. Registreringer gemmes til chefkontrol. Tab skifter felt, piletaster skifter knap, Enter vælger. Cmd/Ctrl+Enter gemmer.</div></div></div><input type="hidden" name="confirm" value="false"><button type="button" data-conflict hidden class="btn btn-outline-warning mt-3" aria-pressed="false">Bekræft overlap i planlægningen</button><div data-status role="status" class="small mt-3"></div><button type="submit" class="btn btn-primary mt-3" data-save>Gem tid</button></form></div>`;
|
||
|
|
modes.after(panel);const q=s=>panel.querySelector(s),form=q('[data-form]');let options=null,customer=null,contact=null,request=crypto.randomUUID(),pending=null;
|
||
|
|
const state=q('[data-status]');
|
||
|
|
async function api(url,opts){const r=await fetch(url,{credentials:'include',...opts});const d=await r.json();if(!r.ok){const e=new Error(typeof d.detail==='string'?d.detail:d.detail?.message||'Kunne ikke gemme');e.status=r.status;e.detail=d.detail;throw e;}return d;}
|
||
|
|
function local(date){return new Date(date.getTime()-date.getTimezoneOffset()*60000).toISOString().slice(0,16);}
|
||
|
|
function paint(button,selected){button.setAttribute('aria-pressed',String(selected));button.classList.toggle('btn-primary',selected);button.classList.toggle('btn-outline-primary',!selected);}
|
||
|
|
function syncChoices(){panel.querySelectorAll('[data-choice]').forEach(group=>group.querySelectorAll('button').forEach(button=>paint(button,button.dataset.value===form.elements[group.dataset.choice].value)));}
|
||
|
|
panel.addEventListener('click',e=>{const button=e.target.closest('[data-choice] button');if(!button||button.disabled)return;const group=button.closest('[data-choice]'),field=form.elements[group.dataset.choice];field.value=button.dataset.value;field.dispatchEvent(new Event('change'));syncChoices();if(!pending)request=crypto.randomUUID();});
|
||
|
|
panel.addEventListener('keydown',e=>{if(!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Home','End'].includes(e.key))return;const button=e.target.closest('button'),group=button?.closest('[role="group"],.list-group');if(!group)return;const buttons=[...group.querySelectorAll('button:not(:disabled)')];if(!buttons.length)return;e.preventDefault();const index=buttons.indexOf(button),step=['ArrowLeft','ArrowUp'].includes(e.key)?-1:1;buttons[e.key==='Home'?0:e.key==='End'?buttons.length-1:(index+step+buttons.length)%buttons.length].focus();});
|
||
|
|
q('[data-conflict]').addEventListener('click',()=>{const selected=form.elements.confirm.value!=='true';form.elements.confirm.value=String(selected);q('[data-conflict]').setAttribute('aria-pressed',String(selected));q('[data-conflict]').classList.toggle('btn-warning',selected);if(!pending)request=crypto.randomUUID();});
|
||
|
|
async function load(){options=await api('/api/v1/timetracking/quick-time/options');const host=q('[data-plans]');host.replaceChildren();for(const p of options.plans){const button=document.createElement('button');button.type='button';button.className='btn btn-sm btn-outline-primary text-start';button.dataset.value=String(p.id);button.textContent=`${p.description} · ${p.user_name} · ${new Date(p.starts_at).toLocaleString('da-DK')}`;host.append(button);}if(!options.plans.length)host.textContent='Ingen planlagte opgaver at konvertere.';users();syncChoices();}
|
||
|
|
function users(){const converting=form.elements.mode.value==='convert',planning=form.elements.mode.value==='planned';const host=q('[data-users]');host.replaceChildren();for(const u of options?.users||[]){if(planning?!u.can_plan:!u.can_register)continue;const button=document.createElement('button');button.type='button';button.className='btn btn-sm btn-outline-primary';button.dataset.userId=u.id;button.textContent=u.name;paint(button,u.id===options.current_user_id);button.disabled=converting;button.addEventListener('click',()=>{paint(button,button.getAttribute('aria-pressed')!=='true');if(!pending)request=crypto.randomUUID();});host.append(button);}}
|
||
|
|
async function open(){caseCard.classList.add('d-none');panel.hidden=false;caseMode.className='btn btn-outline-primary px-4 fw-bold';trigger.className='btn btn-primary px-4 fw-bold';caseMode.setAttribute('aria-pressed','false');trigger.setAttribute('aria-pressed','true');if(pending){panel.scrollIntoView({behavior:'smooth',block:'start'});return;}panel.scrollIntoView({behavior:'smooth',block:'start'});form.elements.start.value=local(new Date(Date.now()-Number(form.elements.minutes.value||30)*60000));q('#qtDescription').focus();state.textContent='Indlæser…';try{await load();state.textContent='';}catch(e){state.textContent=e.message;}}
|
||
|
|
document.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='Enter'&&!panel.hidden){e.preventDefault();e.stopImmediatePropagation();if(!q('[data-save]').disabled)form.requestSubmit();}},true);
|
||
|
|
trigger.addEventListener('click',open);function close(){panel.hidden=true;caseCard.classList.remove('d-none');caseMode.className='btn btn-primary px-4 fw-bold';trigger.className='btn btn-outline-primary px-4 fw-bold';caseMode.setAttribute('aria-pressed','true');trigger.setAttribute('aria-pressed','false');caseMode.focus();}caseMode.addEventListener('click',close);q('[data-close]').textContent='Tilbage til sag';q('[data-close]').addEventListener('click',close);
|
||
|
|
window.openQuickTimeOnly=open;
|
||
|
|
form.elements.mode.addEventListener('change',()=>{form.elements.start.value=local(new Date(Date.now()-(form.elements.mode.value==='registered'?Number(form.elements.minutes.value||30)*60000:0)));q('[data-plan-wrap]').hidden=form.elements.mode.value!=='convert';q('[data-save]').textContent=form.elements.mode.value==='planned'?'Gem planlægning':form.elements.mode.value==='convert'?'Konvertér til registreret tid':'Gem tid';q('[data-conflict]').hidden=true;form.elements.confirm.value='false';q('[data-conflict]').setAttribute('aria-pressed','false');q('[data-conflict]').classList.remove('btn-warning');users();customer=null;contact=null;q('#qtCustomer').disabled=q('#qtContact').disabled=form.elements.mode.value==='convert';q('#qtCustomer').value=q('#qtContact').value='';});
|
||
|
|
form.elements.plan.addEventListener('change',()=>{const p=options.plans.find(x=>x.id===Number(form.elements.plan.value));if(!p)return;form.elements.description.value=p.description;form.elements.minutes.value=Math.round((new Date(p.ends_at)-new Date(p.starts_at))/60000);form.elements.start.value=local(new Date(p.starts_at));form.elements.priority.value=p.priority;customer=p.customer_id;contact=p.contact_id;q('#qtCustomer').value=p.customer_name||'';q('#qtContact').value=p.contact_id?`Kontakt #${p.contact_id}`:'';q('[data-users]').querySelectorAll('button').forEach(x=>paint(x,Number(x.dataset.userId)===p.user_id));syncChoices();});
|
||
|
|
function search(input,list,type){let timer=0,generation=0;input.addEventListener('keydown',e=>{if(e.key==='ArrowDown'&&list.firstElementChild){e.preventDefault();list.firstElementChild.focus();}});input.addEventListener('input',()=>{const current=++generation;if(type==='customers')customer=null;else contact=null;clearTimeout(timer);list.replaceChildren();const term=input.value.trim();if(term.length<2)return;timer=setTimeout(async()=>{try{const data=await api(`/api/v1/search/${type}?q=${encodeURIComponent(term)}`);if(current!==generation)return;const rows=Array.isArray(data)?data:data.results||data.items||[];for(const r of rows.slice(0,10)){const b=document.createElement('button');b.type='button';b.className='list-group-item list-group-item-action small';const name=r.name||r.contact_name||[r.first_name,r.last_name].filter(Boolean).join(' ')||r.label;b.textContent=name;b.addEventListener('click',()=>{if(type==='customers')customer=r.id;else contact=r.id;input.value=name;list.replaceChildren();input.focus();});list.append(b);}}catch(e){state.textContent=e.message;}},250);});}
|
||
|
|
search(q('#qtCustomer'),q('[data-customers]'),'customers');search(q('#qtContact'),q('[data-contacts]'),'contacts');
|
||
|
|
form.addEventListener('input',()=>{if(!pending)request=crypto.randomUUID();});
|
||
|
|
form.addEventListener('submit',async e=>{e.preventDefault();if(!options){state.textContent='Muligheder er ikke indlæst.';return;}const ids=[...q('[data-users]').querySelectorAll('button[aria-pressed="true"]')].map(x=>Number(x.dataset.userId));if(!ids.length){state.textContent='Vælg mindst én medarbejder.';return;}const mode=form.elements.mode.value;if(mode!=='convert'&&((q('#qtCustomer').value.trim()&&!customer)||(q('#qtContact').value.trim()&&!contact))){state.textContent='Vælg firma/kontakt fra søgeresultaterne, eller ryd felterne.';return;}if(mode==='convert'&&!form.elements.plan.value){state.textContent='Vælg en planlagt opgave.';return;}pending=pending||{request_id:request,mode,description:form.elements.description.value,minutes:Number(form.elements.minutes.value),starts_at:new Date(form.elements.start.value).toISOString(),priority:form.elements.priority.value,user_ids:ids,customer_id:customer,contact_id:contact,plan_ids:mode==='convert'?[Number(form.elements.plan.value)]:[],confirm_conflicts:form.elements.confirm.value==='true'};const controls=[...form.querySelectorAll('input,select,button')],disabled=controls.map(x=>x.disabled);controls.forEach(x=>x.disabled=true);state.textContent='Gemmer…';try{await api('/api/v1/timetracking/quick-time',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(pending)});pending=null;request=crypto.randomUUID();try{await load();}catch{options=null;}state.textContent=mode==='planned'?'Planlægning gemt uden sag.':'Tid registreret uden sag og sendt til chefkontrol.';form.elements.description.value='';q('[data-conflict]').hidden=true;form.elements.confirm.value='false';q('[data-conflict]').setAttribute('aria-pressed','false');q('[data-conflict]').classList.remove('btn-warning');}catch(error){state.textContent=error.message;if(error.status&&error.status<500){pending=null;request=crypto.randomUUID();if(error.detail?.confirm_conflicts)q('[data-conflict]').hidden=false;}else state.textContent+=' Tryk Gem igen for at genprøve samme registrering.';}finally{controls.forEach((x,i)=>x.disabled=disabled[i]);if(pending){controls.forEach(x=>x.disabled=true);q('[data-save]').disabled=false;}}});
|
||
|
|
const params=new URLSearchParams(location.search);if(params.get('quick_time')==='1')open();const planParam=params.get('quick_plan');if(planParam){open().then(()=>{form.elements.mode.value='convert';form.elements.mode.dispatchEvent(new Event('change'));syncChoices();form.elements.plan.value=planParam;form.elements.plan.dispatchEvent(new Event('change'));});}
|
||
|
|
}
|
||
|
|
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();})();
|