- Implemented subscription creation, updating, and rendering in script_9.js. - Added functions for handling subscription line items, product selection, and total calculations. - Integrated AnyDesk API for session management in test_anydesk.py. - Created REST client test requests for API endpoints in api.http. - Developed a script to check ESET machine status and save details in tmp_check_eset_machine.py.
208 lines
10 KiB
JavaScript
208 lines
10 KiB
JavaScript
|
|
const salesCaseId = {{ case.id }};
|
|
|
|
function formatCurrency(value) {
|
|
const num = Number(value || 0);
|
|
return new Intl.NumberFormat('da-DK', { style: 'currency', currency: 'DKK' }).format(num);
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
const num = Number(value || 0);
|
|
return new Intl.NumberFormat('da-DK', { minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(num);
|
|
}
|
|
|
|
let saleItemsCache = [];
|
|
|
|
async function loadVarekobSalg() {
|
|
try {
|
|
const res = await fetch(`/api/v1/sag/${salesCaseId}/varekob-salg?include_subcases=true`);
|
|
if (!res.ok) throw new Error('Failed to load aggregated data');
|
|
const data = await res.json();
|
|
|
|
document.getElementById('salesTotalPurchase').textContent = formatCurrency(data?.totals?.purchase_total);
|
|
document.getElementById('salesTotalSale').textContent = formatCurrency(data?.totals?.sale_total);
|
|
document.getElementById('salesTotalNet').textContent = formatCurrency(data?.totals?.net_total);
|
|
document.getElementById('salesTotalHours').textContent = formatNumber(data?.totals?.total_hours) + ' t';
|
|
document.getElementById('salesBillableHours').textContent = formatNumber(data?.totals?.billable_hours) + ' t';
|
|
|
|
saleItemsCache = data.sale_items || [];
|
|
renderSaleItems(saleItemsCache);
|
|
renderTimeEntries(data.time_entries || []);
|
|
const hasSalesData = (data.sale_items || []).length > 0 || (data.time_entries || []).length > 0;
|
|
setModuleContentState('sales', hasSalesData);
|
|
} catch (error) {
|
|
console.error(error);
|
|
const saleBody = document.getElementById('saleItemsBody');
|
|
if (saleBody) {
|
|
saleBody.innerHTML = '<tr><td colspan="10" class="text-center py-4 text-muted">Kunne ikke hente data</td></tr>';
|
|
}
|
|
const timeBody = document.getElementById('salesTimeBody');
|
|
if (timeBody) {
|
|
timeBody.innerHTML = '<tr><td colspan="3" class="text-center py-4 text-muted">Kunne ikke hente data</td></tr>';
|
|
}
|
|
setModuleContentState('sales', true);
|
|
}
|
|
}
|
|
|
|
function renderSaleItems(items) {
|
|
const salesBody = document.getElementById('saleItemsSalesBody');
|
|
const purchaseBody = document.getElementById('saleItemsPurchaseBody');
|
|
const salesSubtotal = document.getElementById('salesLinesSubtotal');
|
|
const purchaseSubtotal = document.getElementById('purchaseLinesSubtotal');
|
|
if (!salesBody || !purchaseBody) return;
|
|
|
|
const salesItems = items.filter(item => (item.type || '').toLowerCase() !== 'purchase');
|
|
const purchaseItems = items.filter(item => (item.type || '').toLowerCase() === 'purchase');
|
|
|
|
const renderRows = (list) => {
|
|
if (!list.length) {
|
|
return '<tr><td colspan="9" class="text-center py-4 text-muted">Ingen linjer</td></tr>';
|
|
}
|
|
|
|
return list.map(item => {
|
|
const statusLabel = item.status || 'draft';
|
|
const isSubcase = item.sag_id && item.sag_id !== salesCaseId;
|
|
const sourceBadge = isSubcase
|
|
? `<span class="badge bg-warning text-dark ms-2">Under-sag</span>`
|
|
: `<span class="badge bg-light text-dark border ms-2">Denne sag</span>`;
|
|
return `
|
|
<tr>
|
|
<td class="ps-4">${item.line_date || '-'}</td>
|
|
<td>${item.description || '-'}</td>
|
|
<td>${item.quantity ?? '-'}</td>
|
|
<td>${item.unit || '-'}</td>
|
|
<td>${item.unit_price != null ? formatCurrency(item.unit_price) : '-'}</td>
|
|
<td class="fw-bold">${formatCurrency(item.amount)}</td>
|
|
<td>${item.source_sag_titel || '-'}${sourceBadge}</td>
|
|
<td><span class="badge bg-light text-dark border">${statusLabel}</span></td>
|
|
<td class="text-end pe-4">
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<button class="btn btn-outline-secondary" onclick='openSaleItemModalById(${item.id})'><i class="bi bi-pencil"></i></button>
|
|
<button class="btn btn-outline-danger" onclick='deleteSaleItem(${item.id})'><i class="bi bi-trash"></i></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
};
|
|
|
|
salesBody.innerHTML = renderRows(salesItems);
|
|
purchaseBody.innerHTML = renderRows(purchaseItems);
|
|
|
|
const salesSum = salesItems.reduce((sum, item) => sum + Number(item.amount || 0), 0);
|
|
const purchaseSum = purchaseItems.reduce((sum, item) => sum + Number(item.amount || 0), 0);
|
|
if (salesSubtotal) salesSubtotal.textContent = formatCurrency(salesSum);
|
|
if (purchaseSubtotal) purchaseSubtotal.textContent = formatCurrency(purchaseSum);
|
|
}
|
|
|
|
function renderTimeEntries(entries) {
|
|
const tbody = document.getElementById('salesTimeBody');
|
|
if (!tbody) return;
|
|
if (!entries.length) {
|
|
tbody.innerHTML = '<tr><td colspan="3" class="text-center py-4 text-muted">Ingen tid registreret</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = entries.map(entry => {
|
|
const hours = entry.approved_hours || entry.original_hours || 0;
|
|
const isSubcase = entry.sag_id && entry.sag_id !== salesCaseId;
|
|
const sourceBadge = isSubcase
|
|
? `<span class="badge bg-warning text-dark ms-2">Under-sag</span>`
|
|
: `<span class="badge bg-light text-dark border ms-2">Denne sag</span>`;
|
|
return `
|
|
<tr>
|
|
<td class="ps-3">${entry.worked_date || '-'}</td>
|
|
<td>${formatNumber(hours)} t</td>
|
|
<td>${entry.source_sag_titel || '-'}${sourceBadge}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function openSaleItemModal(item = null) {
|
|
document.getElementById('sale_item_id').value = item?.id || '';
|
|
document.getElementById('sale_type').value = item?.type || 'sale';
|
|
document.getElementById('sale_status').value = item?.status || 'draft';
|
|
document.getElementById('sale_date').value = item?.line_date || '';
|
|
document.getElementById('sale_description').value = item?.description || '';
|
|
document.getElementById('sale_quantity').value = item?.quantity ?? '';
|
|
document.getElementById('sale_unit').value = item?.unit || '';
|
|
document.getElementById('sale_unit_price').value = item?.unit_price ?? '';
|
|
document.getElementById('sale_amount').value = item?.amount ?? '';
|
|
document.getElementById('sale_currency').value = item?.currency || 'DKK';
|
|
document.getElementById('sale_external_ref').value = item?.external_ref || '';
|
|
|
|
new bootstrap.Modal(document.getElementById('saleItemModal')).show();
|
|
}
|
|
|
|
function openSaleItemModalById(itemId) {
|
|
const item = saleItemsCache.find((entry) => entry.id === itemId);
|
|
openSaleItemModal(item || null);
|
|
}
|
|
|
|
function updateSaleAmount() {
|
|
const qty = parseFloat(document.getElementById('sale_quantity').value || 0);
|
|
const price = parseFloat(document.getElementById('sale_unit_price').value || 0);
|
|
if (qty && price) {
|
|
document.getElementById('sale_amount').value = (qty * price).toFixed(2);
|
|
}
|
|
}
|
|
|
|
async function saveSaleItem() {
|
|
const itemId = document.getElementById('sale_item_id').value;
|
|
const payload = {
|
|
type: document.getElementById('sale_type').value,
|
|
status: document.getElementById('sale_status').value,
|
|
line_date: document.getElementById('sale_date').value || null,
|
|
description: document.getElementById('sale_description').value,
|
|
quantity: document.getElementById('sale_quantity').value || null,
|
|
unit: document.getElementById('sale_unit').value || null,
|
|
unit_price: document.getElementById('sale_unit_price').value || null,
|
|
amount: document.getElementById('sale_amount').value,
|
|
currency: document.getElementById('sale_currency').value || 'DKK',
|
|
external_ref: document.getElementById('sale_external_ref').value || null
|
|
};
|
|
|
|
if (!payload.description || !payload.amount) {
|
|
alert('Beskrivelse og linjesum er påkrævet.');
|
|
return;
|
|
}
|
|
|
|
const method = itemId ? 'PATCH' : 'POST';
|
|
const url = itemId
|
|
? `/api/v1/sag/${salesCaseId}/sale-items/${itemId}`
|
|
: `/api/v1/sag/${salesCaseId}/sale-items`;
|
|
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!res.ok) {
|
|
alert('Kunne ikke gemme varelinje');
|
|
return;
|
|
}
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('saleItemModal')).hide();
|
|
await loadVarekobSalg();
|
|
}
|
|
|
|
async function deleteSaleItem(itemId) {
|
|
if (!confirm('Vil du slette denne varelinje?')) return;
|
|
const res = await fetch(`/api/v1/sag/${salesCaseId}/sale-items/${itemId}`, { method: 'DELETE' });
|
|
if (!res.ok) {
|
|
alert('Kunne ikke slette varelinje');
|
|
return;
|
|
}
|
|
await loadVarekobSalg();
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const qtyInput = document.getElementById('sale_quantity');
|
|
const priceInput = document.getElementById('sale_unit_price');
|
|
if (qtyInput) qtyInput.addEventListener('input', updateSaleAmount);
|
|
if (priceInput) priceInput.addEventListener('input', updateSaleAmount);
|
|
loadVarekobSalg();
|
|
});
|
|
|