release: v2.2.57 email+sag tab stability

This commit is contained in:
Christian 2026-03-18 07:33:32 +01:00
parent eb5e14e2a1
commit 9a3ada380f
3 changed files with 122 additions and 7 deletions

18
RELEASE_NOTES_v2.2.57.md Normal file
View File

@ -0,0 +1,18 @@
# Release Notes v2.2.57
Dato: 2026-03-18
## Fokus
Stabilisering af UI i Email- og SAG-modulerne.
## Aendringer
- Email-visning: yderligere hardening af HTML-tabeller i mail-body, inklusive normalisering af inline styles for at undgaa layout break.
- Email-visning: forbedret overflow-haandtering for bredt indhold (tabeller, celler og media).
- SAG detaljeside: forbedret tab-loading, saa data hentes ved faneskift for Varekob & Salg, Abonnement og Paamindelser.
- SAG detaljeside: robust fallback for reminder user-id via `/api/v1/auth/me`.
- SAG detaljeside: rettet API-kald for reminders og kalender til stabil case-id reference.
## Berorte filer
- app/emails/frontend/emails.html
- app/modules/sag/templates/detail.html
- RELEASE_NOTES_v2.2.57.md

View File

@ -323,7 +323,38 @@
.email-html-body table { .email-html-body table {
display: block; display: block;
overflow-x: auto; overflow-x: auto;
width: 100%; width: 100% !important;
max-width: 100% !important;
table-layout: auto !important;
border-collapse: collapse;
}
.email-html-body tbody,
.email-html-body thead,
.email-html-body tfoot,
.email-html-body tr,
.email-html-body td,
.email-html-body th {
max-width: 100% !important;
}
.email-html-body td,
.email-html-body th {
white-space: normal !important;
word-break: break-word !important;
overflow-wrap: anywhere !important;
}
.email-html-body img,
.email-html-body video,
.email-html-body iframe {
max-width: 100% !important;
height: auto !important;
}
.email-html-body [style*="position:fixed"],
.email-html-body [style*="position: fixed"] {
position: static !important;
} }
.email-body iframe { .email-body iframe {
@ -1886,9 +1917,39 @@ function renderEmailDetail(email) {
// If HTML, inject it as innerHTML after rendering // If HTML, inject it as innerHTML after rendering
if (email.body_html) { if (email.body_html) {
const htmlDiv = pane.querySelector('.email-html-body'); const htmlDiv = pane.querySelector('.email-html-body');
if (htmlDiv) htmlDiv.innerHTML = email.body_html; if (htmlDiv) {
htmlDiv.innerHTML = email.body_html;
normalizeEmailHtmlLayout(htmlDiv);
} }
} }
}
function normalizeEmailHtmlLayout(container) {
if (!container) return;
const tables = container.querySelectorAll('table');
tables.forEach((table) => {
table.style.maxWidth = '100%';
table.style.width = '100%';
table.style.tableLayout = 'auto';
table.removeAttribute('width');
});
const cells = container.querySelectorAll('td, th');
cells.forEach((cell) => {
cell.style.whiteSpace = 'normal';
cell.style.wordBreak = 'break-word';
cell.style.overflowWrap = 'anywhere';
});
const images = container.querySelectorAll('img, iframe, video');
images.forEach((el) => {
el.style.maxWidth = '100%';
if (el.tagName === 'IMG' || el.tagName === 'VIDEO') {
el.style.height = 'auto';
}
});
}
function renderEmailAnalysis(email) { function renderEmailAnalysis(email) {
const aiAnalysisTab = document.getElementById('aiAnalysisTab'); const aiAnalysisTab = document.getElementById('aiAnalysisTab');

View File

@ -2286,6 +2286,27 @@
todoForm.addEventListener('submit', createTodoStep); todoForm.addEventListener('submit', createTodoStep);
} }
const caseTabs = document.getElementById('caseTabs');
if (caseTabs) {
caseTabs.addEventListener('shown.bs.tab', async (event) => {
const targetSelector = event?.target?.getAttribute('data-bs-target') || '';
const tabId = targetSelector.startsWith('#') ? targetSelector.slice(1) : targetSelector;
try {
if (tabId === 'sales' && typeof loadVarekobSalg === 'function') {
await loadVarekobSalg();
} else if (tabId === 'subscription' && typeof loadSubscriptionForCase === 'function') {
await loadSubscriptionForCase();
} else if (tabId === 'reminders') {
if (typeof loadReminders === 'function') await loadReminders();
if (typeof loadCaseCalendar === 'function') await loadCaseCalendar();
}
} catch (tabLoadError) {
console.error('Tab data reload failed:', tabLoadError);
}
});
}
// Focus on title when create modal opens // Focus on title when create modal opens
const createModalEl = document.getElementById('createRelatedCaseModal'); const createModalEl = document.getElementById('createRelatedCaseModal');
if (createModalEl) { if (createModalEl) {
@ -4570,6 +4591,7 @@
<script> <script>
let reminderUserId = null; let reminderUserId = null;
const remindersCaseId = {{ case.id }};
function getReminderUserId() { function getReminderUserId() {
const token = localStorage.getItem('access_token') || sessionStorage.getItem('access_token'); const token = localStorage.getItem('access_token') || sessionStorage.getItem('access_token');
@ -4586,6 +4608,20 @@
return null; return null;
} }
async function ensureReminderUserId() {
const localId = getReminderUserId();
if (localId) return localId;
try {
const res = await fetch('/api/v1/auth/me', { credentials: 'include' });
if (!res.ok) return null;
const me = await res.json();
return me?.id || me?.user_id || null;
} catch (err) {
return null;
}
}
function formatReminderDate(value) { function formatReminderDate(value) {
if (!value) return '-'; if (!value) return '-';
const date = new Date(value); const date = new Date(value);
@ -4637,7 +4673,7 @@
async function loadReminders() { async function loadReminders() {
const list = document.getElementById('remindersList'); const list = document.getElementById('remindersList');
if (!list) return; if (!list) return;
reminderUserId = getReminderUserId(); reminderUserId = await ensureReminderUserId();
if (!reminderUserId) { if (!reminderUserId) {
list.innerHTML = '<div class="p-4 text-center text-muted">Kunne ikke finde bruger-id.</div>'; list.innerHTML = '<div class="p-4 text-center text-muted">Kunne ikke finde bruger-id.</div>';
@ -4648,7 +4684,7 @@
list.innerHTML = '<div class="p-4 text-center text-muted"><span class="spinner-border spinner-border-sm"></span> Henter reminders...</div>'; list.innerHTML = '<div class="p-4 text-center text-muted"><span class="spinner-border spinner-border-sm"></span> Henter reminders...</div>';
try { try {
const res = await fetch(`/api/v1/sag/${caseIds}/reminders?user_id=${reminderUserId}`); const res = await fetch(`/api/v1/sag/${remindersCaseId}/reminders?user_id=${reminderUserId}`);
if (!res.ok) throw new Error('Kunne ikke hente reminders'); if (!res.ok) throw new Error('Kunne ikke hente reminders');
const reminders = await res.json(); const reminders = await res.json();
renderReminders(reminders); renderReminders(reminders);
@ -4722,7 +4758,7 @@
} }
async function saveReminder() { async function saveReminder() {
reminderUserId = getReminderUserId(); reminderUserId = await ensureReminderUserId();
if (!reminderUserId) { if (!reminderUserId) {
alert('Mangler bruger-id. Log ind igen.'); alert('Mangler bruger-id. Log ind igen.');
return; return;
@ -4785,7 +4821,7 @@
}; };
try { try {
const res = await fetch(`/api/v1/sag/${caseIds}/reminders?user_id=${reminderUserId}`, { const res = await fetch(`/api/v1/sag/${remindersCaseId}/reminders?user_id=${reminderUserId}`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload) body: JSON.stringify(payload)
@ -4846,7 +4882,7 @@
childrenList.innerHTML = '<div class="text-muted small">Indlæser børnesager...</div>'; childrenList.innerHTML = '<div class="text-muted small">Indlæser børnesager...</div>';
try { try {
const res = await fetch(`/api/v1/sag/${caseIds}/calendar-events?include_children=true`); const res = await fetch(`/api/v1/sag/${remindersCaseId}/calendar-events?include_children=true`);
if (!res.ok) throw new Error('Kunne ikke hente kalenderaftaler'); if (!res.ok) throw new Error('Kunne ikke hente kalenderaftaler');
const data = await res.json(); const data = await res.json();