const subscriptionCaseId = {{ case.id }}; let currentSubscription = null; let subscriptionProducts = []; let lastCreatedSubscriptionProductId = null; function formatSubscriptionInterval(interval) { const map = { 'daily': 'Daglig', 'biweekly': '14-dage', 'monthly': 'Maaned', 'quarterly': 'Kvartal', 'yearly': 'Aar' }; return map[interval] || interval || '-'; } function formatSubscriptionCurrency(amount) { return new Intl.NumberFormat('da-DK', { style: 'currency', currency: 'DKK', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(amount || 0); } function formatSubscriptionDate(dateStr) { if (!dateStr) return '-'; const date = new Date(dateStr); return date.toLocaleDateString('da-DK'); } function setSubscriptionBadge(status) { const badge = document.getElementById('subscriptionStatusBadge'); if (!badge) return; const classes = { 'draft': 'bg-light text-dark', 'active': 'bg-success', 'paused': 'bg-warning', 'cancelled': 'bg-secondary' }; const label = { 'draft': 'Kladde', 'active': 'Aktiv', 'paused': 'Pauset', 'cancelled': 'Opsagt' }; badge.className = `badge ${classes[status] || 'bg-light text-dark'}`; badge.textContent = label[status] || status || 'Ingen'; } function showSubscriptionCreateForm() { const empty = document.getElementById('subscriptionEmpty'); const form = document.getElementById('subscriptionCreateForm'); const details = document.getElementById('subscriptionDetails'); if (empty) empty.classList.remove('d-none'); if (form) form.classList.remove('d-none'); if (details) details.classList.add('d-none'); setSubscriptionBadge(null); const startDateInput = document.getElementById('subscriptionStartDateInput'); if (startDateInput && !startDateInput.value) { startDateInput.value = new Date().toISOString().split('T')[0]; } const body = document.getElementById('subscriptionLineItemsBody'); if (body) { body.innerHTML = ` 0,00 kr `; } populateSubscriptionProductSelects(); updateSubscriptionLineTotals(); } function populateSubscriptionProductSelects() { const selects = document.querySelectorAll('.subscriptionProductSelect'); selects.forEach(select => { const currentValue = select.value; select.innerHTML = ''; subscriptionProducts.forEach(product => { const option = document.createElement('option'); option.value = product.id; option.textContent = product.name; option.dataset.salesPrice = product.sales_price ?? ''; option.dataset.description = product.short_description ?? ''; select.appendChild(option); }); if (currentValue) { select.value = currentValue; } else if (lastCreatedSubscriptionProductId) { select.value = String(lastCreatedSubscriptionProductId); } }); lastCreatedSubscriptionProductId = null; } function applySubscriptionProduct(select) { const row = select.closest('tr'); if (!row) return; const descriptionInput = row.querySelector('input[type="text"]'); const unitPriceInput = row.querySelectorAll('input[type="number"]')[1]; const selected = select.options[select.selectedIndex]; if (!selected) return; const description = selected.dataset.description || selected.textContent || ''; const salesPrice = selected.dataset.salesPrice; if (descriptionInput && !descriptionInput.value.trim()) { descriptionInput.value = description; } if (unitPriceInput && salesPrice !== '') { unitPriceInput.value = salesPrice; } updateSubscriptionLineTotals(); } function addSubscriptionLine() { const body = document.getElementById('subscriptionLineItemsBody'); if (!body) return; const row = document.createElement('tr'); row.innerHTML = ` 0,00 kr `; body.appendChild(row); populateSubscriptionProductSelects(); updateSubscriptionLineTotals(); } function removeSubscriptionLine(button) { const row = button.closest('tr'); const body = document.getElementById('subscriptionLineItemsBody'); if (!row || !body) return; if (body.children.length <= 1) { row.querySelectorAll('input').forEach(input => { input.value = input.type === 'number' ? 0 : ''; }); } else { row.remove(); } updateSubscriptionLineTotals(); } function updateSubscriptionLineTotals() { const body = document.getElementById('subscriptionLineItemsBody'); const totalEl = document.getElementById('subscriptionLinesTotal'); if (!body || !totalEl) return; let total = 0; Array.from(body.querySelectorAll('tr')).forEach(row => { const inputs = row.querySelectorAll('input'); const description = inputs[0]?.value || ''; const qty = parseFloat(inputs[1]?.value || 0); const unit = parseFloat(inputs[2]?.value || 0); const lineTotal = (qty > 0 ? qty : 0) * (unit > 0 ? unit : 0); total += lineTotal; const lineTotalEl = row.querySelector('.subscriptionLineTotal'); if (lineTotalEl) { lineTotalEl.textContent = formatSubscriptionCurrency(lineTotal); } if (!description && qty === 0 && unit === 0) { if (lineTotalEl) { lineTotalEl.textContent = formatSubscriptionCurrency(0); } } }); totalEl.textContent = formatSubscriptionCurrency(total); } function collectSubscriptionLineItems() { const body = document.getElementById('subscriptionLineItemsBody'); if (!body) return []; const items = []; Array.from(body.querySelectorAll('tr')).forEach(row => { const productSelect = row.querySelector('.subscriptionProductSelect'); const inputs = row.querySelectorAll('input'); const description = (inputs[0]?.value || '').trim(); const quantity = parseFloat(inputs[1]?.value || 0); const unitPrice = parseFloat(inputs[2]?.value || 0); if (!description && quantity === 0 && unitPrice === 0) { return; } items.push({ product_id: productSelect && productSelect.value ? parseInt(productSelect.value, 10) : null, description, quantity, unit_price: unitPrice }); }); return items; } async function loadSubscriptionProducts() { try { const res = await fetch('/api/v1/products'); if (!res.ok) { throw new Error('Kunne ikke hente produkter'); } subscriptionProducts = await res.json(); } catch (e) { console.error('Error loading products:', e); subscriptionProducts = []; } populateSubscriptionProductSelects(); } function openSubscriptionProductModal() { const form = document.getElementById('subscriptionProductForm'); if (form) form.reset(); new bootstrap.Modal(document.getElementById('subscriptionProductModal')).show(); } async function createSubscriptionProduct() { const payload = { name: document.getElementById('subscriptionProductName').value.trim(), type: document.getElementById('subscriptionProductType').value.trim() || null, status: document.getElementById('subscriptionProductStatus').value, sales_price: document.getElementById('subscriptionProductSalesPrice').value || null, billing_period: document.getElementById('subscriptionProductBillingPeriod').value || null, short_description: document.getElementById('subscriptionProductDescription').value.trim() || null }; if (!payload.name) { alert('Navn er paakraevet'); return; } const res = await fetch('/api/v1/products', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) { const error = await res.json(); alert(error.detail || 'Kunne ikke oprette produkt'); return; } const product = await res.json(); lastCreatedSubscriptionProductId = product.id; bootstrap.Modal.getInstance(document.getElementById('subscriptionProductModal')).hide(); await loadSubscriptionProducts(); updateSubscriptionLineTotals(); } function renderSubscription(subscription) { currentSubscription = subscription; const empty = document.getElementById('subscriptionEmpty'); const form = document.getElementById('subscriptionCreateForm'); const details = document.getElementById('subscriptionDetails'); if (empty) empty.classList.add('d-none'); if (form) form.classList.add('d-none'); if (details) details.classList.remove('d-none'); document.getElementById('subscriptionNumber').textContent = subscription.subscription_number || `#${subscription.id}`; document.getElementById('subscriptionProduct').textContent = subscription.product_name || '-'; document.getElementById('subscriptionInterval').textContent = formatSubscriptionInterval(subscription.billing_interval); document.getElementById('subscriptionPrice').textContent = formatSubscriptionCurrency(subscription.price); document.getElementById('subscriptionStartDate').textContent = formatSubscriptionDate(subscription.start_date); document.getElementById('subscriptionStatusText').textContent = subscription.status || '-'; // New fields const periodStartEl = document.getElementById('subscriptionPeriodStart'); const nextInvoiceEl = document.getElementById('subscriptionNextInvoice'); if (periodStartEl) { periodStartEl.textContent = subscription.period_start ? formatSubscriptionDate(subscription.period_start) : '-'; } if (nextInvoiceEl) { const nextDate = subscription.next_invoice_date ? formatSubscriptionDate(subscription.next_invoice_date) : '-'; nextInvoiceEl.textContent = nextDate; // Highlight if invoice is due soon if (subscription.next_invoice_date) { const daysUntil = Math.ceil((new Date(subscription.next_invoice_date) - new Date()) / (1000 * 60 * 60 * 24)); if (daysUntil <= 7 && daysUntil >= 0) { nextInvoiceEl.innerHTML = `${nextDate} Om ${daysUntil} dage`; } } } setSubscriptionBadge(subscription.status); const itemsBody = document.getElementById('subscriptionItemsBody'); const itemsTotal = document.getElementById('subscriptionItemsTotal'); if (itemsBody) { const items = subscription.line_items || []; if (!items.length) { itemsBody.innerHTML = 'Ingen linjer'; } else { itemsBody.innerHTML = items.map(item => ` ${item.product_name || '-'} ${item.description} ${parseFloat(item.quantity).toFixed(2)} ${formatSubscriptionCurrency(item.unit_price)} ${formatSubscriptionCurrency(item.line_total)} `).join(''); } } if (itemsTotal) { itemsTotal.textContent = formatSubscriptionCurrency(subscription.price || 0); } const actions = document.getElementById('subscriptionActions'); if (!actions) return; const buttons = []; if (subscription.status === 'draft' || subscription.status === 'paused') { buttons.push(``); } if (subscription.status === 'active') { buttons.push(``); } if (subscription.status !== 'cancelled') { buttons.push(``); } actions.innerHTML = buttons.join(' '); } async function loadSubscriptionForCase() { try { const res = await fetch(`/api/v1/sag-subscriptions/by-sag/${subscriptionCaseId}`); if (res.status === 404) { showSubscriptionCreateForm(); setModuleContentState('subscription', false); return; } if (!res.ok) { throw new Error('Kunne ikke hente abonnement'); } const subscription = await res.json(); renderSubscription(subscription); setModuleContentState('subscription', true); } catch (e) { console.error('Error loading subscription:', e); showSubscriptionCreateForm(); setModuleContentState('subscription', true); } } async function createSubscription() { const billingInterval = document.getElementById('subscriptionIntervalInput').value; const billingDay = parseInt(document.getElementById('subscriptionBillingDayInput').value, 10); const startDate = document.getElementById('subscriptionStartDateInput').value; const notes = document.getElementById('subscriptionNotesInput').value.trim(); const lineItems = collectSubscriptionLineItems(); if (!billingInterval || !billingDay || !startDate) { alert('Udfyld venligst alle paakraevet felter'); return; } if (!lineItems.length) { alert('Du skal angive mindst en varelinje'); return; } try { const res = await fetch('/api/v1/sag-subscriptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sag_id: subscriptionCaseId, billing_interval: billingInterval, billing_day: billingDay, start_date: startDate, notes: notes || null, line_items: lineItems }) }); if (!res.ok) { const error = await res.json(); throw new Error(error.detail || 'Fejl ved oprettelse'); } const subscription = await res.json(); renderSubscription(subscription); } catch (e) { alert(e.message || e); } } async function updateSubscriptionStatus(status) { if (!currentSubscription) return; if (status === 'cancelled' && !confirm('Er du sikker paa, at abonnementet skal opsiges?')) { return; } try { const res = await fetch(`/api/v1/sag-subscriptions/${currentSubscription.id}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (!res.ok) { const error = await res.json(); throw new Error(error.detail || 'Kunne ikke opdatere status'); } const updated = await res.json(); renderSubscription(updated); } catch (e) { alert(e.message || e); } } document.addEventListener('DOMContentLoaded', () => { loadSubscriptionProducts(); loadSubscriptionForCase(); }); // === Quick Time Entry Functions (for inline time tracking) === function toggleQuickTimeForm() { const container = document.getElementById('quickTimeFormContainer'); if (container) { container.classList.remove('d-none'); } } // Make function globally available for onclick handler window.toggleQuickTimeForm = toggleQuickTimeForm; async function quickAddTime(event) { event.preventDefault(); const form = document.getElementById('quickAddTimeForm'); const formData = new FormData(form); // Parse hours and minutes const hours = parseInt(formData.get('hours')) || 0; const minutes = parseInt(formData.get('minutes')) || 0; const totalHours = hours + (minutes / 60); if (totalHours === 0) { alert('Angiv venligst timer eller minutter'); return; } const billingSelect = document.getElementById('quickTimeBillingMethod'); let billingMethod = billingSelect ? billingSelect.value : 'invoice'; let prepaidCardId = null; let fixedPriceAgreementId = null; if (billingMethod.startsWith('card_')) { prepaidCardId = parseInt(billingMethod.split('_')[1]); billingMethod = 'prepaid'; } if (billingMethod.startsWith('fpa_')) { fixedPriceAgreementId = parseInt(billingMethod.split('_')[1]); billingMethod = 'fixed_price'; } const isInternal = billingMethod === 'internal'; // Build payload const payload = { sag_id: {{ case.id }}, worked_date: formData.get('date'), original_hours: totalHours, description: formData.get('description'), billing_method: billingMethod, is_internal: isInternal }; if (prepaidCardId) { payload.prepaid_card_id = prepaidCardId; } if (fixedPriceAgreementId) { payload.fixed_price_agreement_id = fixedPriceAgreementId; } try { const response = await fetch('/api/v1/timetracking/entries/internal', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Kunne ikke gemme tidsregistrering'); } // Success - reload page to show new entry window.location.reload(); } catch (error) { alert('Fejl: ' + error.message); console.error('Quick add time error:', error); } } // Set today's date as default for quick time form document.addEventListener('DOMContentLoaded', function() { const dateInput = document.getElementById('quickTimeDate'); if (dateInput && !dateInput.value) { const today = new Date().toISOString().split('T')[0]; dateInput.value = today; } // Activate tab from ?tab= URL parameter (used when navigating from relation tree QA menu) const tabParam = new URLSearchParams(window.location.search).get('tab'); if (tabParam) { const tabBtn = document.getElementById(tabParam + '-tab') || document.querySelector(`[data-module-tab="${tabParam}"]`); if (tabBtn) { setTimeout(() => { bootstrap.Tab.getOrCreateInstance(tabBtn).show(); forceCaseTabActivation(tabParam); }, 300); } } });