344 lines
15 KiB
JavaScript
344 lines
15 KiB
JavaScript
|
|
|
||
|
|
let reminderUserId = null;
|
||
|
|
const remindersCaseId = {{ case.id }};
|
||
|
|
|
||
|
|
function getReminderUserId() {
|
||
|
|
const token = localStorage.getItem('access_token') || sessionStorage.getItem('access_token');
|
||
|
|
if (token) {
|
||
|
|
try {
|
||
|
|
const payload = JSON.parse(atob(token.split('.')[1]));
|
||
|
|
return payload.sub || payload.user_id;
|
||
|
|
} catch (e) {
|
||
|
|
console.warn('Could not decode token for reminder user_id');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const metaTag = document.querySelector('meta[name="user-id"]');
|
||
|
|
if (metaTag) return metaTag.getAttribute('content');
|
||
|
|
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) {
|
||
|
|
if (!value) return '-';
|
||
|
|
const date = new Date(value);
|
||
|
|
if (Number.isNaN(date.getTime())) return '-';
|
||
|
|
return date.toLocaleString('da-DK', { hour12: false });
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateReminderTriggerFields() {
|
||
|
|
const triggerType = document.getElementById('rem_trigger_type')?.value;
|
||
|
|
const timeWrap = document.getElementById('rem_trigger_time_wrap');
|
||
|
|
const statusWrap = document.getElementById('rem_trigger_status_wrap');
|
||
|
|
if (timeWrap && statusWrap) {
|
||
|
|
if (triggerType === 'status_change') {
|
||
|
|
timeWrap.classList.add('d-none');
|
||
|
|
statusWrap.classList.remove('d-none');
|
||
|
|
} else {
|
||
|
|
timeWrap.classList.remove('d-none');
|
||
|
|
statusWrap.classList.add('d-none');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateReminderRecurrenceFields() {
|
||
|
|
const recurrenceType = document.getElementById('rem_recurrence_type')?.value;
|
||
|
|
const dowWrap = document.getElementById('rem_recurrence_dow_wrap');
|
||
|
|
const domWrap = document.getElementById('rem_recurrence_dom_wrap');
|
||
|
|
if (!dowWrap || !domWrap) return;
|
||
|
|
dowWrap.classList.toggle('d-none', recurrenceType !== 'weekly');
|
||
|
|
domWrap.classList.toggle('d-none', recurrenceType !== 'monthly');
|
||
|
|
}
|
||
|
|
|
||
|
|
function openCreateReminderModal(defaultEventType) {
|
||
|
|
reminderUserId = getReminderUserId();
|
||
|
|
const warning = document.getElementById('rem_user_warning');
|
||
|
|
if (warning) warning.classList.toggle('d-none', !!reminderUserId);
|
||
|
|
|
||
|
|
const form = document.getElementById('createReminderForm');
|
||
|
|
if (form) form.reset();
|
||
|
|
document.getElementById('rem_notify_frontend').checked = true;
|
||
|
|
document.getElementById('rem_priority').value = 'normal';
|
||
|
|
document.getElementById('rem_event_type').value = defaultEventType || 'reminder';
|
||
|
|
document.getElementById('rem_trigger_type').value = 'time_based';
|
||
|
|
document.getElementById('rem_recurrence_type').value = 'once';
|
||
|
|
updateReminderTriggerFields();
|
||
|
|
updateReminderRecurrenceFields();
|
||
|
|
new bootstrap.Modal(document.getElementById('createReminderModal')).show();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadReminders() {
|
||
|
|
const list = document.getElementById('remindersList');
|
||
|
|
if (!list) return;
|
||
|
|
reminderUserId = await ensureReminderUserId();
|
||
|
|
|
||
|
|
if (!reminderUserId) {
|
||
|
|
list.innerHTML = '<div class="p-4 text-center text-muted">Kunne ikke finde bruger-id.</div>';
|
||
|
|
setModuleContentState('reminders', true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
list.innerHTML = '<div class="p-4 text-center text-muted"><span class="spinner-border spinner-border-sm"></span> Henter reminders...</div>';
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch(`/api/v1/sag/${remindersCaseId}/reminders?user_id=${reminderUserId}`);
|
||
|
|
if (!res.ok) throw new Error('Kunne ikke hente reminders');
|
||
|
|
const reminders = await res.json();
|
||
|
|
renderReminders(reminders);
|
||
|
|
} catch (e) {
|
||
|
|
console.error(e);
|
||
|
|
list.innerHTML = '<div class="p-4 text-center text-danger">Fejl ved hentning af reminders</div>';
|
||
|
|
setModuleContentState('reminders', true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderReminders(reminders) {
|
||
|
|
const list = document.getElementById('remindersList');
|
||
|
|
if (!list) return;
|
||
|
|
if (!reminders || reminders.length === 0) {
|
||
|
|
list.innerHTML = '<div class="p-4 text-center text-muted">Ingen reminders endnu.</div>';
|
||
|
|
setModuleContentState('reminders', false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const triggerLabels = {
|
||
|
|
time_based: 'Tidspunkt',
|
||
|
|
status_change: 'Status ændring',
|
||
|
|
deadline_approaching: 'Deadline'
|
||
|
|
};
|
||
|
|
|
||
|
|
const eventTypeLabels = {
|
||
|
|
reminder: 'Reminder',
|
||
|
|
meeting: 'Moede',
|
||
|
|
technician_visit: 'Teknikerbesoeg',
|
||
|
|
obs: 'OBS',
|
||
|
|
deadline: 'Deadline'
|
||
|
|
};
|
||
|
|
|
||
|
|
const recurrenceLabels = {
|
||
|
|
once: 'Én gang',
|
||
|
|
daily: 'Dagligt',
|
||
|
|
weekly: 'Ugentligt',
|
||
|
|
monthly: 'Månedligt'
|
||
|
|
};
|
||
|
|
|
||
|
|
list.innerHTML = reminders.map(reminder => {
|
||
|
|
const nextCheck = formatReminderDate(reminder.next_check_at);
|
||
|
|
const createdAt = formatReminderDate(reminder.created_at);
|
||
|
|
const isActive = reminder.is_active;
|
||
|
|
const statusBadge = isActive
|
||
|
|
? '<span class="badge bg-success">Aktiv</span>'
|
||
|
|
: '<span class="badge bg-secondary">Inaktiv</span>';
|
||
|
|
|
||
|
|
return `
|
||
|
|
<div class="list-group-item">
|
||
|
|
<div class="d-flex justify-content-between align-items-start">
|
||
|
|
<div class="me-3">
|
||
|
|
<div class="fw-bold">${reminder.title}</div>
|
||
|
|
<div class="text-muted small">${reminder.message || '-'} </div>
|
||
|
|
<div class="small text-muted mt-1">
|
||
|
|
Type: ${eventTypeLabels[reminder.event_type] || reminder.event_type || 'Reminder'} · Trigger: ${triggerLabels[reminder.trigger_type] || reminder.trigger_type} · Gentagelse: ${recurrenceLabels[reminder.recurrence_type] || reminder.recurrence_type}
|
||
|
|
</div>
|
||
|
|
<div class="small text-muted">Næste: ${nextCheck} · Oprettet: ${createdAt}</div>
|
||
|
|
</div>
|
||
|
|
<div class="d-flex flex-column align-items-end gap-2">
|
||
|
|
${statusBadge}
|
||
|
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteReminder(${reminder.id})">
|
||
|
|
<i class="bi bi-trash"></i>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}).join('');
|
||
|
|
setModuleContentState('reminders', true);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function saveReminder() {
|
||
|
|
reminderUserId = await ensureReminderUserId();
|
||
|
|
if (!reminderUserId) {
|
||
|
|
alert('Mangler bruger-id. Log ind igen.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const title = document.getElementById('rem_title').value.trim();
|
||
|
|
const message = document.getElementById('rem_message').value.trim();
|
||
|
|
const priority = document.getElementById('rem_priority').value;
|
||
|
|
const eventType = document.getElementById('rem_event_type').value;
|
||
|
|
const triggerType = document.getElementById('rem_trigger_type').value;
|
||
|
|
const scheduledAtValue = document.getElementById('rem_scheduled_at').value;
|
||
|
|
const targetStatus = document.getElementById('rem_target_status').value;
|
||
|
|
const recurrenceType = document.getElementById('rem_recurrence_type').value;
|
||
|
|
const recurrenceDow = document.getElementById('rem_recurrence_dow').value;
|
||
|
|
const recurrenceDom = document.getElementById('rem_recurrence_dom').value;
|
||
|
|
const notifyFrontend = document.getElementById('rem_notify_frontend').checked;
|
||
|
|
const notifyEmail = document.getElementById('rem_notify_email').checked;
|
||
|
|
const notifyMattermost = document.getElementById('rem_notify_mattermost').checked;
|
||
|
|
const overridePrefs = document.getElementById('rem_override_prefs').checked;
|
||
|
|
|
||
|
|
if (!title) {
|
||
|
|
alert('Titel er påkrævet');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let triggerConfig = {};
|
||
|
|
let scheduledAt = null;
|
||
|
|
|
||
|
|
if (triggerType === 'status_change') {
|
||
|
|
if (!targetStatus) {
|
||
|
|
alert('Vælg en status for statusændring');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
triggerConfig = { target_status: targetStatus };
|
||
|
|
} else {
|
||
|
|
if (!scheduledAtValue) {
|
||
|
|
alert('Vælg et tidspunkt');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
scheduledAt = new Date(scheduledAtValue).toISOString();
|
||
|
|
}
|
||
|
|
|
||
|
|
const payload = {
|
||
|
|
title,
|
||
|
|
message: message || null,
|
||
|
|
priority,
|
||
|
|
event_type: eventType,
|
||
|
|
trigger_type: triggerType,
|
||
|
|
trigger_config: triggerConfig,
|
||
|
|
recipient_user_ids: [Number(reminderUserId)],
|
||
|
|
recipient_emails: [],
|
||
|
|
notify_mattermost: notifyMattermost,
|
||
|
|
notify_email: notifyEmail,
|
||
|
|
notify_frontend: notifyFrontend,
|
||
|
|
override_user_preferences: overridePrefs,
|
||
|
|
recurrence_type: recurrenceType,
|
||
|
|
recurrence_day_of_week: recurrenceType === 'weekly' ? Number(recurrenceDow) : null,
|
||
|
|
recurrence_day_of_month: recurrenceType === 'monthly' ? Number(recurrenceDom) : null,
|
||
|
|
scheduled_at: scheduledAt
|
||
|
|
};
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch(`/api/v1/sag/${remindersCaseId}/reminders?user_id=${reminderUserId}`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(payload)
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
const err = await res.json();
|
||
|
|
throw new Error(err.detail || 'Kunne ikke oprette reminder');
|
||
|
|
}
|
||
|
|
bootstrap.Modal.getInstance(document.getElementById('createReminderModal')).hide();
|
||
|
|
await loadReminders();
|
||
|
|
await loadCaseCalendar();
|
||
|
|
} catch (e) {
|
||
|
|
alert('Fejl: ' + e.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function deleteReminder(reminderId) {
|
||
|
|
if (!confirm('Vil du slette denne reminder?')) return;
|
||
|
|
try {
|
||
|
|
const res = await fetch(`/api/v1/sag/reminders/${reminderId}`, { method: 'DELETE' });
|
||
|
|
if (!res.ok) throw new Error('Kunne ikke slette reminder');
|
||
|
|
await loadReminders();
|
||
|
|
await loadCaseCalendar();
|
||
|
|
} catch (e) {
|
||
|
|
alert('Fejl: ' + e.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatCalendarEvent(event) {
|
||
|
|
const dateLabel = formatReminderDate(event.start);
|
||
|
|
const typeLabelMap = {
|
||
|
|
reminder: 'Reminder',
|
||
|
|
meeting: 'Moede',
|
||
|
|
technician_visit: 'Teknikerbesoeg',
|
||
|
|
obs: 'OBS',
|
||
|
|
deadline: 'Deadline',
|
||
|
|
deferred: 'Deferred'
|
||
|
|
};
|
||
|
|
const typeLabel = typeLabelMap[event.event_kind] || event.event_kind || 'Reminder';
|
||
|
|
return `
|
||
|
|
<a href="${event.url}" class="list-group-item list-group-item-action">
|
||
|
|
<div class="d-flex justify-content-between">
|
||
|
|
<div>
|
||
|
|
<div class="fw-semibold">${event.title || 'Aftale'}</div>
|
||
|
|
<div class="text-muted small">${typeLabel} · ${dateLabel}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</a>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadCaseCalendar() {
|
||
|
|
const currentList = document.getElementById('caseCalendarCurrent');
|
||
|
|
const childrenList = document.getElementById('caseCalendarChildren');
|
||
|
|
if (!currentList || !childrenList) return;
|
||
|
|
|
||
|
|
currentList.innerHTML = '<div class="text-muted small">Indlæser aftaler...</div>';
|
||
|
|
childrenList.innerHTML = '<div class="text-muted small">Indlæser børnesager...</div>';
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch(`/api/v1/sag/${remindersCaseId}/calendar-events?include_children=true`);
|
||
|
|
if (!res.ok) throw new Error('Kunne ikke hente kalenderaftaler');
|
||
|
|
const data = await res.json();
|
||
|
|
|
||
|
|
const currentEvents = data.current || [];
|
||
|
|
const childGroups = data.children || [];
|
||
|
|
const childCount = childGroups.reduce((sum, child) => sum + (child.events || []).length, 0);
|
||
|
|
const hasAnyEvents = currentEvents.length > 0 || childCount > 0;
|
||
|
|
|
||
|
|
if (!currentEvents.length) {
|
||
|
|
currentList.innerHTML = '<div class="text-muted small">Ingen aftaler for denne sag.</div>';
|
||
|
|
} else {
|
||
|
|
currentList.innerHTML = currentEvents
|
||
|
|
.map(formatCalendarEvent)
|
||
|
|
.join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!childGroups.length) {
|
||
|
|
childrenList.innerHTML = '<div class="text-muted small">Ingen børnesager.</div>';
|
||
|
|
} else {
|
||
|
|
childrenList.innerHTML = childGroups.map(child => {
|
||
|
|
const eventsHtml = (child.events || []).length
|
||
|
|
? child.events.map(formatCalendarEvent).join('')
|
||
|
|
: '<div class="text-muted small">Ingen aftaler.</div>';
|
||
|
|
return `
|
||
|
|
<div class="mb-3">
|
||
|
|
<div class="fw-semibold mb-1">${child.case_title}</div>
|
||
|
|
<div class="list-group">
|
||
|
|
${eventsHtml}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
setModuleContentState('calendar', hasAnyEvents);
|
||
|
|
} catch (e) {
|
||
|
|
console.error(e);
|
||
|
|
currentList.innerHTML = '<div class="text-danger small">Fejl ved hentning af aftaler.</div>';
|
||
|
|
childrenList.innerHTML = '';
|
||
|
|
setModuleContentState('calendar', true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
updateReminderTriggerFields();
|
||
|
|
updateReminderRecurrenceFields();
|
||
|
|
loadReminders();
|
||
|
|
loadCaseCalendar();
|
||
|
|
});
|
||
|
|
|