bmc_hub/app/modules/planner/templates/work.html
Christian 1fb5b4a191 Add comprehensive tests for email and drift functionalities
- Introduced tests for drift operations UI and backend to ensure proper handling of ignore rules and UI elements.
- Added tests for email identity API to validate email linking and identity retrieval without updates.
- Implemented tests for email purchase link service to verify correct handling of purchase mail links and conflict scenarios.
- Created tests for email reply automation to ensure proper routing and processing of replies based on classification.
- Developed tests for email sender creation to validate suggestions and handling of existing contacts and companies.
- Added tests for email sender identity to ensure correct matching of contacts and domains.
- Implemented tests for work desk functionalities to validate task management, meeting registrations, and timer functionalities.
2026-11-03 01:16:12 +01:00

124 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);display:flex;align-items:center;gap:.4rem}
.work-tabs .nav-link span{font-size:.75rem;opacity:.78}
.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}.work-tabs .nav-link{flex:1 1 42%;justify-content:center;text-align:center}.work-tabs .nav-link span{display:none}.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">
<a class="nav-link work-tab-link" href="/planning?view=queue"><i class="bi bi-person-workspace" aria-hidden="true"></i> Mit arbejde <span>Min dag og min kø</span></a>
<button class="nav-link" data-work-tab="tasks"><i class="bi bi-list-task" aria-hidden="true"></i> Opgaver <span>Fordel og planlæg</span></button>
<button class="nav-link" data-work-tab="team"><i class="bi bi-people" aria-hidden="true"></i> Team og kapacitet <span>Se belastning og ledig tid</span></button>
</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 %}