bmc_hub/script_9.js

544 lines
24 KiB
JavaScript
Raw Permalink Normal View History

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 = `
<tr>
<td>
<select class="form-select form-select-sm subscriptionProductSelect" onchange="applySubscriptionProduct(this)">
<option value="">Vælg produkt</option>
</select>
</td>
<td><input type="text" class="form-control form-control-sm" placeholder="Managed Backup"></td>
<td><input type="number" class="form-control form-control-sm" min="0.01" step="0.01" value="1" oninput="updateSubscriptionLineTotals()"></td>
<td><input type="number" class="form-control form-control-sm" min="0" step="0.01" value="0" oninput="updateSubscriptionLineTotals()"></td>
<td class="text-end"><span class="subscriptionLineTotal">0,00 kr</span></td>
<td class="text-end">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeSubscriptionLine(this)"><i class="bi bi-x"></i></button>
</td>
</tr>
`;
}
populateSubscriptionProductSelects();
updateSubscriptionLineTotals();
}
function populateSubscriptionProductSelects() {
const selects = document.querySelectorAll('.subscriptionProductSelect');
selects.forEach(select => {
const currentValue = select.value;
select.innerHTML = '<option value="">Vælg produkt</option>';
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 = `
<td>
<select class="form-select form-select-sm subscriptionProductSelect" onchange="applySubscriptionProduct(this)">
<option value="">Vælg produkt</option>
</select>
</td>
<td><input type="text" class="form-control form-control-sm" placeholder="Beskrivelse"></td>
<td><input type="number" class="form-control form-control-sm" min="0.01" step="0.01" value="1" oninput="updateSubscriptionLineTotals()"></td>
<td><input type="number" class="form-control form-control-sm" min="0" step="0.01" value="0" oninput="updateSubscriptionLineTotals()"></td>
<td class="text-end"><span class="subscriptionLineTotal">0,00 kr</span></td>
<td class="text-end">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeSubscriptionLine(this)"><i class="bi bi-x"></i></button>
</td>
`;
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} <span class="badge bg-warning text-dark">Om ${daysUntil} dage</span>`;
}
}
}
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 = '<tr><td colspan="5" class="text-center text-muted">Ingen linjer</td></tr>';
} else {
itemsBody.innerHTML = items.map(item => `
<tr>
<td>${item.product_name || '-'}</td>
<td>${item.description}</td>
<td class="text-end">${parseFloat(item.quantity).toFixed(2)}</td>
<td class="text-end">${formatSubscriptionCurrency(item.unit_price)}</td>
<td class="text-end">${formatSubscriptionCurrency(item.line_total)}</td>
</tr>
`).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(`<button class="btn btn-sm btn-success" onclick="updateSubscriptionStatus('active')"><i class="bi bi-play-circle me-1"></i>Aktiver</button>`);
}
if (subscription.status === 'active') {
buttons.push(`<button class="btn btn-sm btn-warning" onclick="updateSubscriptionStatus('paused')"><i class="bi bi-pause-circle me-1"></i>Pause</button>`);
}
if (subscription.status !== 'cancelled') {
buttons.push(`<button class="btn btn-sm btn-outline-danger" onclick="updateSubscriptionStatus('cancelled')"><i class="bi bi-x-circle me-1"></i>Opsig</button>`);
}
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);
}
}
});