bmc_hub/static/js/absence-report.js

52 lines
6.4 KiB
JavaScript
Raw Permalink 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.

(() => {
'use strict';
const $=id=>document.getElementById(id),form=$('absenceFilters');
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const number=v=>Number(v).toLocaleString('da-DK',{maximumFractionDigits:2});
const hours=v=>number(v/60)+' t';
const date=v=>new Intl.DateTimeFormat('da-DK',{dateStyle:'short',timeStyle:'short',timeZone:'Europe/Copenhagen'}).format(new Date(v));
const day=v=>`${v.getFullYear()}-${String(v.getMonth()+1).padStart(2,'0')}-${String(v.getDate()).padStart(2,'0')}`;
let offset=0,requestVersion=0,filters=null,data=null;
const now=new Date();form.elements.start_date.value=day(new Date(now.getFullYear(),now.getMonth(),1));form.elements.end_date.value=day(new Date(now.getFullYear(),now.getMonth()+1,0));
window.BMCPlanningDates?.mount(form);
async function get(params) {
const response=await fetch('/api/v1/planner/absence-report?'+params,{headers:{Accept:'application/json'},credentials:'same-origin'});
const result=await response.json();if(!response.ok)throw new Error(typeof result.detail==='string'?result.detail:'Rapporten kunne ikke indlæses');return result;
}
function fail(err){$('absenceError').textContent=err.message;$('absenceError').hidden=false;}
async function load(newFilters=false) {
const version=++requestVersion;$('absenceError').hidden=true;$('absenceExport').disabled=true;$('absenceStatus').textContent='Henter fravær…';
if(newFilters){offset=0;filters=new URLSearchParams(new FormData(form));if(!filters.get('user_id'))filters.delete('user_id');filters.set('group_by','type');}
const params=new URLSearchParams(filters);params.set('offset',offset);params.set('limit',50);
try {
const result=await get(params);if(version!==requestVersion)return;data=result;
if(!form.elements.user_id.dataset.loaded){form.elements.user_id.insertAdjacentHTML('beforeend',result.employees.map(u=>`<option value="${u.id}">${esc(u.name)}</option>`).join(''));form.elements.user_id.dataset.loaded='1';}
$('absenceTotals').innerHTML=[['Arbejdsdage',number(result.totals.planned_days)],['Planlagte timer',hours(result.totals.planned_minutes)],['Faktisk registreret fravær',hours(result.totals.actual_minutes)]].map(([label,value])=>`<div class="col-sm-4"><div class="card h-100"><div class="card-body"><p class="small text-secondary mb-1">${label}</p><strong class="h4">${value}</strong></div></div></div>`).join('');
const values=r=>`<td>${r.norm_configured?number(r.planned_days):'Ukendt'}</td><td>${r.norm_configured?hours(r.planned_minutes):'Ukendt'}</td><td>${hours(r.actual_minutes)}</td>`;
$('absenceSummary').innerHTML=(result.type_summary||[]).map(group=>`<tr class="table-light"><th scope="row">${esc(group.kind_label)} <span class="small text-secondary fw-normal">· ${group.periods} perioder</span></th>${values(group)}</tr>`+group.employees.map(r=>`<tr><td class="ps-4">${esc(r.name)}${r.norm_configured?'':' <span class="small text-secondary">· arbejdstid mangler</span>'}</td>${values(r)}</tr>`).join('')).join('')||'<tr><td colspan="4" class="text-secondary p-3">Intet fravær i den valgte periode.</td></tr>';
let previousKind=null;
$('absenceRows').innerHTML=result.items.map(r=>{
const heading=previousKind!==r.kind?`<tr class="table-light"><th scope="rowgroup" colspan="8">${esc(r.kind_label)}</th></tr>`:'';previousKind=r.kind;
return heading+`<tr><td>${esc(r.name)}</td><td>${esc(r.kind_label)}</td><td>${date(r.starts_at)}</td><td>${date(r.ends_at)}</td><td>${r.norm_configured?number(r.planned_days):'Ukendt'}</td><td>${r.norm_configured?hours(r.planned_minutes):'Ukendt'}</td><td>${hours(r.actual_minutes)}</td><td><span class="badge ${r.registered?'bg-success':'bg-secondary'}">${r.registered?'Tid registreret':'Planlagt'}</span></td></tr>`;
}).join('')||'<tr><td colspan="8" class="text-secondary text-center p-4">Intet fravær i den valgte periode.</td></tr>';
$('absencePrev').disabled=offset===0;$('absenceNext').disabled=offset+result.items.length>=result.total;
$('absencePage').textContent=`${number(result.totals.planned_days)} arbejdsdage i alt · `+(result.total?`${offset+1}–${offset+result.items.length} af ${result.total} perioder`:'0 perioder');
$('absenceStatus').textContent=`${result.total} fraværsperioder. Planlagt og faktisk fravær vises separat.`;$('absenceExport').disabled=false;
}catch(err){if(version===requestVersion){data=null;$('absenceTotals').innerHTML='';$('absenceSummary').innerHTML='';$('absenceRows').innerHTML='';$('absencePrev').disabled=true;$('absenceNext').disabled=true;$('absenceStatus').textContent='';fail(err);}}
}
function csvCell(value) {
let text=String(value??'');if(/^[\s]*[=+\-@]/.test(text))text="'"+text;return '"'+text.replaceAll('"','""')+'"';
}
$('absenceExport').onclick=async()=>{
if(!data||!filters)return;$('absenceExport').disabled=true;
const exportFilters=new URLSearchParams(filters);
try {
let rows=[],page=0,total;
do {const params=new URLSearchParams(exportFilters);params.set('limit',500);params.set('offset',page);const result=await get(params);rows.push(...result.items);total=result.total;page+=result.items.length;if(!result.items.length&&page<total)throw new Error('Listen blev ændret. Hent rapporten igen.');}while(page<total);
const content=[['Fraværstype','Medarbejder','Fra','Til','Arbejdsdage','Planlagte timer','Faktisk registrerede timer','Status'],...rows.map(r=>[r.kind_label,r.name,date(r.starts_at),date(r.ends_at),r.norm_configured?number(r.planned_days):'Ukendt',r.norm_configured?number(r.planned_minutes/60):'Ukendt',number(r.actual_minutes/60),r.registered?'Tid registreret':'Planlagt'])].map(r=>r.map(csvCell).join(';')).join('\r\n');
const url=URL.createObjectURL(new Blob(['\uFEFF'+content],{type:'text/csv;charset=utf-8'})),a=document.createElement('a');a.href=url;a.download=`fravaer-${exportFilters.get('start_date')}-${exportFilters.get('end_date')}.csv`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
}catch(err){fail(err);}finally{$('absenceExport').disabled=!data;}
};
form.onsubmit=e=>{e.preventDefault();load(true);};$('absencePrev').onclick=()=>{offset=Math.max(0,offset-50);load();};$('absenceNext').onclick=()=>{offset+=50;load();};load(true);
})();