bmc_hub/app/modules/planner/templates/work.html

123 lines
19 KiB
HTML

{% extends "shared/frontend/base.html" %}
{% block title %}Planlægning · BMC Hub{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="/static/vendor/flatpickr/flatpickr.min.css"><link rel="stylesheet" href="/static/css/planning-dates.css?v=20261018-1">
<style>
.work-page{width:100%;color:var(--text-primary)}
.work-tabs{margin:1rem 0;flex-wrap:wrap;gap:.25rem;border-color:var(--frame-border)}
.work-tabs .nav-link{color:var(--text-secondary)}
.work-tabs .nav-link.active{color:var(--accent);background:var(--bg-card);border-color:var(--frame-border) var(--frame-border) var(--bg-card);font-weight:600}
.work-grid{display:grid;grid-template-columns:minmax(280px,1fr) minmax(360px,2fr);gap:1rem}
.work-card{padding:1rem;margin-bottom:.75rem}
.work-row{display:flex;align-items:center;justify-content:space-between;gap:.5rem;flex-wrap:wrap}
.work-actions{display:flex;gap:.35rem;flex-wrap:wrap}
.work-muted{color:var(--text-secondary);font-size:.85rem}
.work-page dialog{width:min(540px,95vw);max-height:90vh;overflow:auto;border:2px solid var(--frame-border-strong);border-top:4px solid var(--accent);border-radius:var(--border-radius);box-shadow:var(--frame-shadow);background:var(--bg-card);color:var(--text-primary);padding:1.25rem}
.work-page dialog::backdrop{background:rgba(0,0,0,.5)}
.work-page label{display:block;margin:.5rem 0}
.work-page input,.work-page select,.work-page textarea{max-width:100%}
.work-page .btn-outline-primary{--bs-btn-color:var(--accent);--bs-btn-border-color:var(--accent);--bs-btn-hover-bg:var(--accent);--bs-btn-hover-border-color:var(--accent);--bs-btn-active-bg:var(--accent);--bs-btn-active-border-color:var(--accent)}
#workCalendar{min-height:550px;padding:1rem;--fc-border-color:var(--frame-border);--fc-page-bg-color:var(--bg-card);--fc-neutral-bg-color:var(--accent-light);--fc-button-bg-color:var(--accent);--fc-button-border-color:var(--accent);--fc-button-hover-bg-color:color-mix(in srgb,var(--accent) 85%,black);--fc-button-hover-border-color:var(--accent);--fc-button-active-bg-color:color-mix(in srgb,var(--accent) 75%,black);--fc-button-active-border-color:var(--accent)}
.work-help{position:relative;display:inline-block;vertical-align:middle;color:var(--text-secondary)}
.work-help summary{display:inline-flex;align-items:center;justify-content:center;cursor:pointer;list-style:none;min-width:28px;min-height:28px;border-radius:50%}
.work-help summary::-webkit-details-marker{display:none}
.work-help summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.work-help[open] summary{color:var(--accent);background:var(--accent-light)}
.work-help>div{position:absolute;z-index:10;right:0;top:100%;width:min(320px,75vw);padding:.85rem;font-size:.85rem;font-weight:400;line-height:1.5;color:var(--text-primary);background:var(--bg-card);border:1px solid var(--frame-border-strong);border-radius:var(--border-radius);box-shadow:var(--frame-shadow)}
.work-page dialog .work-help>div{position:static;width:auto;margin-top:.5rem}
.unassigned-table td{vertical-align:middle;padding:.85rem .65rem}
.unassigned-table .case-name{font-weight:600;text-decoration:none}
.unassigned-table .case-number{color:var(--text-secondary);font-weight:400;margin-right:.4rem}
.unassigned-empty{text-align:center;padding:2rem 1rem;color:var(--text-secondary)}
.unassigned-empty .bi{font-size:2rem;color:var(--accent)}
@media(max-width:800px){.unassigned-table thead{display:none}.unassigned-table,.unassigned-table tbody,.unassigned-table tr,.unassigned-table td{display:block;width:100%}.unassigned-table tr{border-bottom:1px solid var(--frame-border);padding:.6rem 0}.unassigned-table td{border:0;padding:.35rem .25rem}.unassigned-table td[data-label]::before{content:attr(data-label) ": ";color:var(--text-secondary);font-size:.85rem}.unassigned-table td:last-child .btn{width:100%}}
.work-task-values{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:.5rem;margin-top:.85rem!important}
.work-value-button{--value-accent:var(--accent);display:flex;flex-direction:column;justify-content:space-between;gap:.5rem;min-height:76px;min-width:0;text-align:left;padding:.65rem .7rem;border:1px solid var(--frame-border);border-radius:10px;background:var(--bg-card);color:var(--text-primary);cursor:grab;-webkit-user-drag:element;box-shadow:0 1px 2px rgba(15,47,72,.03);transition:border-color .15s,box-shadow .15s,background .15s}
.work-value-button[data-task-kind=deadline]{--value-accent:#a16b16}
.work-value-button[data-task-kind=slot]{--value-accent:#336699}
.work-value-button:hover{border-color:color-mix(in srgb,var(--value-accent) 55%,var(--frame-border));background:color-mix(in srgb,var(--value-accent) 3%,var(--bg-card));box-shadow:0 3px 9px rgba(15,47,72,.08)}
.work-value-button:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.work-value-button:active{cursor:grabbing}
.work-value-button:disabled{opacity:.55;cursor:default;-webkit-user-drag:none;box-shadow:none}
.work-value-label{display:flex;align-items:center;gap:.4rem;font-size:.76rem;font-weight:500;color:var(--text-secondary);width:100%}
.work-value-icon{display:inline-flex;align-items:center;justify-content:center;flex:0 0 24px;height:24px;border-radius:7px;background:color-mix(in srgb,var(--value-accent) 9%,var(--bg-card));color:var(--value-accent);font-size:.85rem}
.work-value-grip{margin-left:auto;font-size:.7rem;opacity:.35}
.work-value-button:hover .work-value-grip{opacity:.85}
.work-value-content{font-size:.82rem;font-weight:600;line-height:1.45;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}
.work-slot-values{font-size:.78rem;line-height:1.5;padding-top:.3rem}
.work-slot-values>span{font-size:.72rem}
.work-slot-values ul{display:flex;flex-wrap:wrap;gap:.35rem}
.work-slot-values li{padding:.3rem .55rem;border:1px solid var(--frame-border);border-radius:7px;background:color-mix(in srgb,var(--accent) 3%,var(--bg-card));font-variant-numeric:tabular-nums}
.work-slot-values li .bi{color:var(--accent)}
.work-page .work-card:hover{transform:none}
@media(max-width:420px){.work-task-values{gap:.35rem}.work-value-button{padding:.55rem .4rem}.work-value-label{gap:.25rem;font-size:.72rem}.work-value-icon{flex-basis:20px;height:20px}.work-value-grip{display:none}.work-value-content{font-size:.76rem}}
#workCalendar .fc-bg-event.work-reservation{--reservation-ink:var(--accent);opacity:1;background-color:color-mix(in srgb,var(--reservation-ink) 12%,transparent)!important;border-left:2px solid color-mix(in srgb,var(--reservation-ink) 38%,transparent)}
#workCalendar .fc-bg-event.work-reservation-conflict{--reservation-ink:#b42318}
#workCalendar .fc-bg-event.work-reservation .fc-event-title{display:none}
.work-reservation-label{font-style:normal;position:absolute;top:3px;left:5px;max-width:calc(100% - 10px);padding:1px 3px;border:0;border-radius:3px;background:transparent;color:var(--reservation-ink,var(--accent));font-size:.7rem;font-weight:500;line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:left;pointer-events:auto}
button.work-reservation-label{cursor:pointer}
button.work-reservation-label:hover,button.work-reservation-label:focus-visible{background:var(--bg-card);outline:1px solid var(--frame-border)}
#workCalendar .fc-event.work-reservation:not(.fc-bg-event){background-color:color-mix(in srgb,var(--accent) 12%,var(--bg-card))!important;border-color:var(--frame-border)!important;color:var(--text-primary)}
#workCalendar .fc-event.work-reservation:not(.fc-bg-event) .fc-event-main{color:var(--text-primary)}
.work-calendar-legend{display:flex;flex-wrap:wrap;gap:1rem;align-items:center;font-size:.78rem;color:var(--text-secondary);margin-bottom:.5rem}
.work-calendar-legend span{display:inline-flex;align-items:center;gap:.35rem}
.work-calendar-legend i{display:inline-block;width:12px;height:12px;border-radius:3px}
.work-legacy-deadline{display:none!important}
.work-employee-drops{display:flex;flex-wrap:wrap;gap:.5rem}
.work-employee-drop{border:1px dashed var(--frame-border-strong);border-radius:var(--border-radius);padding:.65rem .85rem;background:var(--bg-card);color:var(--text-primary);min-height:44px}
.work-employee-drop.is-drag-over,.work-page [data-date].is-drag-over{outline:3px solid var(--accent);background:var(--accent-light)!important}
.work-drag-hint{color:var(--text-secondary);font-size:.8rem;cursor:grab}
.work-page [data-task-id][draggable=true]{cursor:grab}
.work-page .is-dragging{opacity:.5}
.work-task-mode #workSourcePane{max-height:78vh;overflow:auto;padding:2px}
.work-task-mode .unassigned-table,.work-task-mode .unassigned-table tbody,.work-task-mode .unassigned-table tr,.work-task-mode .unassigned-table td{display:block;width:100%}
.work-task-mode .unassigned-table thead{display:none}.work-task-mode .unassigned-table tr{border-bottom:1px solid var(--frame-border);padding:.6rem 0}.work-task-mode .unassigned-table td{border:0;padding:.3rem .1rem}
.work-task-mode .unassigned-table td[data-label]::before{content:attr(data-label) ": ";color:var(--text-secondary)}
.work-task-mode #workSourcePane .work-help>div{position:static;width:auto}
.work-task-mode #workUnassigned .card-header p,.work-task-mode #workUnassigned .work-row.gap-3>p{display:none}
.work-task-mode .unassigned-table .text-md-end{text-align:left!important}
@media(max-width:800px){.work-task-mode #workSourcePane{max-height:none}.work-employee-drop{flex:1 1 130px}}
#workTeam{width:100%;height:80vh;border:0}
.work-row[draggable=true]{cursor:grab}
.work-row.is-drag-over{outline:2px solid var(--accent)}
@media(max-width:800px){.work-grid{grid-template-columns:1fr}.fc .fc-toolbar{display:flex;flex-wrap:wrap;gap:.4rem}.fc .fc-toolbar-title{font-size:1.1rem}#workCalendar{padding:.75rem}}
</style>
{% endblock %}
{% block content %}
<main class="work-page" data-user-id="{{ current_user.id }}">
<div class="work-row"><h1 class="h3 mb-0"><i class="bi bi-calendar2-week me-2" aria-hidden="true"></i>Planlægning</h1><div class="work-actions"><button class="btn btn-primary" data-work-next>Næste opgave</button><button class="btn btn-outline-primary" id="workCreate" disabled>Ny aftale</button><button class="btn btn-outline-secondary" id="workAbsence" disabled>Fravær</button><a href="/planning/absence" id="workAbsenceReport" class="btn btn-outline-secondary" hidden>Fraværsstatus og rapport</a></div></div>
<div id="workEmployeeToolbar" class="work-row my-2"><label>Medarbejder <select id="workEmployee" class="form-select form-select-sm"></select></label><button class="btn btn-sm btn-outline-secondary" id="workSettings" hidden>Indstillinger</button></div>
<nav class="work-tabs nav nav-tabs" aria-label="Planlægningsvisninger">
{% for key,label in [('work','Mit arbejde'),('tasks','Opgaver'),('team','Team og kapacitet')] %}
<button class="nav-link" data-work-tab="{{ key }}">{{ label }}</button>
{% endfor %}
</nav>
<p id="workError" class="alert alert-danger" role="alert" hidden></p>
<section id="workTaskBar" class="card p-3 mb-3" hidden>
<div class="work-row"><div><h2 class="h5 mb-1">Fordel opgaver og sæt datoer</h2><p class="work-muted mb-2">Træk Start, Deadline eller Allokeret fra opgavekortet til kalenderen. Allokeret tilføjer en ny tid hver gang.</p></div><details class="work-help"><summary aria-label="Hjælp til at trække opgaver"><i class="bi bi-info-circle" aria-hidden="true"></i></summary><div>Uden medarbejder: Træk til et navn for at tildele. Fra en medarbejders kø: Træk til et andet navn for at overdrage arbejdet. Træk inden for køen for at ændre rækkefølgen. Træk Start for at sætte sagsstart, Deadline for at sætte fristen og Allokeret for at tilføje et nyt tidsrum. Tidligere tidsrum bevares. Du kan også bruge knapperne på opgaven.</div></details></div>
<div id="workEmployeeDrops" class="work-employee-drops" aria-label="Træk opgaver til en medarbejder"></div><p id="workTaskStatus" class="small mt-2 mb-0" role="status" aria-live="polite"></p>
</section>
<section id="workDaily" class="work-grid"><div id="workSourcePane"><div id="workSourceFilter" class="work-actions mb-2" hidden><button type="button" class="btn btn-sm btn-outline-primary" data-task-source="inbox">Uden medarbejder</button><button type="button" class="btn btn-sm btn-outline-primary" data-task-source="queue">Medarbejderens kø</button></div><div id="workQueuePanel" class="card p-3 align-self-start"><div class="work-row"><h2 id="workQueueHeading" class="h5">Min arbejdskø</h2><button class="btn btn-sm btn-outline-secondary" id="workRefresh">Opdatér</button></div><label>Filter <select id="workFilter" class="form-select form-select-sm"><option value="">Alle</option><option value="ready">Klar til arbejde</option><option value="working">I gang</option><option value="paused">På pause</option><option value="waiting">Afventer</option><option value="high">Høj prioritet</option><option value="today">Deadline i dag</option><option value="overdue">Forfalden</option></select></label><input id="workSearch" class="form-control form-control-sm mb-2" type="search" placeholder="Søg efter sag, kunde, sagstype eller tags" aria-label="Søg i kø"><div id="workQueue"></div><button id="workMore" class="btn btn-sm btn-outline-secondary" hidden>Vis flere</button></div></div><div id="workCalendarPane"><div class="work-calendar-legend"><span><i style="background:color-mix(in srgb,var(--accent) 12%,var(--bg-card));border:1px solid var(--frame-border)" aria-hidden="true"></i>Allokeret · reserveret tid</span><span><i style="background:#7c3aed" aria-hidden="true"></i>Registreret · faktisk brugt tid</span></div><section id="workAbsenceSummary" class="card p-3 mb-2" aria-label="Fravær i den viste periode"></section><div id="workCalendar" class="card"></div></div></section>
<iframe id="workTeam" title="Medarbejderplanlægning" hidden loading="lazy"></iframe>
<section id="workCapacity" hidden></section><section id="workUnassigned" hidden aria-labelledby="unassignedHeading">
<div class="card">
<header class="card-header p-3 work-row"><div><h2 id="unassignedHeading" class="h5 mb-1">Sager der mangler en medarbejder</h2><p class="work-muted mb-0">Vælg, hvem der skal udføre arbejdet. Sagen kommer derefter i medarbejderens arbejdskø.</p></div>
<details class="work-help"><summary aria-label="Hvad betyder ikke allokeret?"><i class="bi bi-info-circle" aria-hidden="true"></i></summary><div>En sag er ikke allokeret, når ingen medarbejder har den i sin arbejdskø. Sagen kan godt have en ansvarlig: Den ansvarlige følger sagen, mens en tildelt medarbejder udfører arbejdet.</div></details>
</header>
<div class="card-body">
<div class="work-row gap-3 mb-3"><p class="small text-secondary mb-0"><strong>1.</strong> Find sagen <span class="mx-2" aria-hidden="true">→</span> <strong>2.</strong> Vælg medarbejder <span class="mx-2" aria-hidden="true">→</span> <strong>3.</strong> Tilføj til kø</p><button id="unassignedRefresh" type="button" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-clockwise me-1" aria-hidden="true"></i>Opdatér listen</button></div>
<label for="unassignedSearch" class="form-label">Find en sag</label><input id="unassignedSearch" type="search" class="form-control mb-3" placeholder="Søg på sagsnummer, titel, kunde eller ansvarlig…">
<p id="unassignedStatus" role="status" aria-live="polite" class="small mb-2"></p>
<div id="unassignedError" role="alert" class="alert alert-danger" hidden></div>
<div id="unassignedList" aria-live="polite"></div>
<nav class="work-row mt-3" aria-label="Sider med ikke-allokerede sager"><button id="unassignedPrev" class="btn btn-sm btn-outline-secondary" type="button" disabled>Forrige</button><span id="unassignedPage" class="work-muted"></span><button id="unassignedNext" class="btn btn-sm btn-outline-secondary" type="button" disabled>Næste</button></nav>
</div>
</div>
</section>
<dialog id="workEventDialog"><form id="workEventForm"><h2 class="h5">Fast aftale</h2><input name="event_id" type="hidden"><input name="version" type="hidden"><label>Titel<input name="title" class="form-control" required maxlength="255"></label><label>Sag (valgfrit)<input name="case_search" type="search" list="workCaseSuggestions" class="form-control" placeholder="Søg efter sagsnummer, titel eller kunde"><datalist id="workCaseSuggestions"></datalist><input name="sag_id" type="hidden"></label><label>Deltagere<select name="user_ids" class="form-select" multiple required></select></label><label>Start<input name="starts_at" type="datetime-local" class="form-control" required></label><label>Slut<input name="ends_at" type="datetime-local" class="form-control" required></label><label>Type<select name="kind" class="form-select"><option value="work">Arbejde</option><option value="meeting">Møde</option><option value="internal">Intern aktivitet</option></select></label><label>Beskrivelse<textarea name="description" class="form-control"></textarea></label><div class="work-actions mt-3"><button class="btn btn-primary">Gem</button><button class="btn btn-outline-danger" type="button" id="workDeleteEvent" hidden>Annullér aftale</button><button class="btn btn-light" type="button" data-work-close>Luk</button></div><p class="work-dialog-error text-danger" role="alert"></p></form></dialog>
<dialog id="workAbsenceDialog"><form id="workAbsenceForm"><input name="absence_id" type="hidden"><input name="version" type="hidden"><h2 class="h5">Registrer planlagt fravær</h2><label>Medarbejder<select name="user_id" class="form-select" required></select></label><label>Type<select name="kind" class="form-select" required></select></label><label class="form-check"><input name="whole_days" type="checkbox" class="form-check-input"> Hele dage</label><label>Fra og med<input name="starts_at" type="datetime-local" class="form-control" required></label><label>Til og med<input name="ends_at" type="datetime-local" class="form-control" required></label><p class="small text-secondary">Ved hele dage medregnes både første og sidste dato. Delvise dage angives med klokkeslæt.</p><label>Privat note<textarea name="note" class="form-control"></textarea></label><div class="work-actions mt-3"><button class="btn btn-primary">Gem</button><button type="button" class="btn btn-outline-primary" id="workRegisterAbsence" hidden>Registrer som afholdt</button><button type="button" class="btn btn-outline-danger" id="workCancelAbsence" hidden>Annullér fravær</button><button class="btn btn-light" type="button" data-work-close>Luk</button></div><p class="work-dialog-error text-danger" role="alert"></p></form></dialog>
<dialog id="workNextDialog"><h2 class="h5">Næste opgave</h2><p id="workNextMessage"></p><div id="workNextActions" class="work-actions"></div><button class="btn btn-light mt-3" data-work-close>Luk</button></dialog>
</main>
{% endblock %}
{% block extra_js %}<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.js"></script><script src="/static/vendor/flatpickr/flatpickr.min.js"></script><script src="/static/vendor/flatpickr/da.js"></script><script src="/static/js/planning-dates.js?v=20261018-1"></script><script src="/static/js/planning-work.js?v=20261018-9"></script>{% endblock %}