<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Campaign Exposure Calculator</title>
<script src="/_sdk/element_sdk.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Raleway:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
body {
box-sizing: border-box;
}
* {
box-sizing: border-box;
}
</style>
<style>@view-transition { navigation: auto; }</style>
<script src="/_sdk/data_sdk.js" type="text/javascript"></script>
</head>
<body class="min-h-full flex items-center justify-center p-8">
<div class="w-full max-w-4xl">
<div id="calculator-card" class="rounded-2xl shadow-2xl p-8"><!-- Logo Section -->
<div class="flex justify-center mb-6"><img src="https://usercontent.one/wp/syntesedigital.dk/wp-content/uploads/2025/01/Orange.png?media=1769522210" alt="Syntese Digital Logo" style="max-width: 200px; height: auto;">
</div>
<h1 id="calculator-title" class="text-3xl font-bold text-center mb-8"></h1>
<div class="space-y-6"><!-- Customer Information Section -->
<div id="customer-info-section" class="p-6 rounded-xl border-2">
<h2 class="text-xl font-bold mb-4">Kundeoplysninger</h2>
<div class="grid grid-cols-2 gap-4 mb-4">
<div><label for="company-name" class="block text-sm font-semibold mb-2">Firmanavn</label> <input type="text" id="company-name" placeholder="Indtast firmanavn" class="w-full px-4 py-2 rounded-lg border-2 focus:outline-none focus:ring-2 text-sm">
</div>
<div><label for="address" class="block text-sm font-semibold mb-2">Adresse</label> <input type="text" id="address" placeholder="Indtast adresse" class="w-full px-4 py-2 rounded-lg border-2 focus:outline-none focus:ring-2 text-sm">
</div>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div><label for="cvr" class="block text-sm font-semibold mb-2">CVR</label> <input type="text" id="cvr" placeholder="Indtast CVR" class="w-full px-4 py-2 rounded-lg border-2 focus:outline-none focus:ring-2 text-sm">
</div>
<div><label for="contact-person" class="block text-sm font-semibold mb-2">Kontaktperson</label> <input type="text" id="contact-person" placeholder="Indtast kontaktperson" class="w-full px-4 py-2 rounded-lg border-2 focus:outline-none focus:ring-2 text-sm">
</div>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div><label for="email" class="block text-sm font-semibold mb-2">E-mail</label> <input type="email" id="email" placeholder="Indtast e-mail" class="w-full px-4 py-2 rounded-lg border-2 focus:outline-none focus:ring-2 text-sm">
</div>
<div><label for="website" class="block text-sm font-semibold mb-2">Hjemmeside</label> <input type="text" id="website" placeholder="Indtast hjemmeside" class="w-full px-4 py-2 rounded-lg border-2 focus:outline-none focus:ring-2 text-sm">
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div><label for="phone" class="block text-sm font-semibold mb-2">Telefonnummer</label> <input type="tel" id="phone" placeholder="Indtast telefonnummer" class="w-full px-4 py-2 rounded-lg border-2 focus:outline-none focus:ring-2 text-sm">
</div>
<div><label for="mobile" class="block text-sm font-semibold mb-2">Mobilnummer</label> <input type="tel" id="mobile" placeholder="Indtast mobilnummer" class="w-full px-4 py-2 rounded-lg border-2 focus:outline-none focus:ring-2 text-sm">
</div>
</div>
</div>
<div id="products-container" class="space-y-4"><!-- Products will be added here dynamically -->
</div><button id="add-product-button" class="w-full py-3 rounded-lg font-semibold transition-all hover:opacity-90 active:scale-95 flex items-center justify-center gap-2"> <span class="text-2xl">+</span> <span>Tilføj Produkt</span> </button>
<div id="price-container" class="mt-6 p-6 rounded-xl">
<h3 class="text-lg font-bold mb-4 text-center">Pris Oversigt</h3>
<div id="products-summary" class="space-y-2 mb-4"><!-- Product summaries will be added here -->
</div>
<div class="border-t-2 pt-3 space-y-3">
<div class="flex justify-between items-center"><span class="font-bold">Samlede Eksponeringer:</span> <span id="total-exposures" class="font-bold text-xl">0 visninger</span>
</div>
<div class="flex justify-between items-center text-sm opacity-60"><span>Normalpris (Listepris):</span> <span id="normal-price" class="line-through">kr 0</span>
</div>
<div class="flex justify-between items-center"><span class="text-sm font-semibold">Rabat:</span> <span id="discount-amount" class="font-bold text-lg text-green-600">kr 0</span>
</div>
<div class="border-t-2 pt-3 flex justify-between items-center"><span class="font-bold">Din pris (ekskl. Tech Fee):</span> <span id="total-price" class="font-bold text-xl">kr 0</span>
</div>
<div class="flex justify-between items-center text-sm opacity-80"><span>Tech Fee (13,5%):</span> <span id="vat-amount">kr 0</span>
</div>
<div class="border-t-2 pt-3 flex justify-between items-center"><span class="font-bold">Total pris (inkl. Tech Fee):</span> <span id="grand-total" class="font-bold text-xl">kr 0</span>
</div>
</div>
</div><!-- Disclaimer Section -->
<div id="disclaimer-section" class="p-4 rounded-lg text-xs">
<p class="font-semibold mb-2">Forretnings- og betalingsbetingelser:</p>
<p class="opacity-90 mb-2">For aftaler over 6 måneder, faktureres Tech fee og de første 3 måneder upfront. Efterfølgende faktureres man månedligt forud. For aftaler under 6 måneder, faktureres hele beløbet upfront. Banner produktion betales upfront.</p>
<p class="opacity-90">Der henvises til vores forretningsbetingelser: <a href="https://syntesedigital.dk/forretningsbetingelser" target="_blank" rel="noopener noreferrer" class="underline hover:opacity-70">syntesedigital.dk/forretningsbetingelser</a></p>
</div><!-- Comments Section -->
<div><label for="comments" class="block text-sm font-semibold mb-2">Kommentarer / Særaftaler</label>
<div id="payment-summary" class="mb-3 p-3 rounded-lg text-sm font-semibold" style="background: #374151; color: #ffffff;"><!-- Payment summary will be inserted here -->
</div><textarea id="comments" rows="4" placeholder="Indtast eventuelle kommentarer eller særaftaler..." class="w-full px-4 py-3 rounded-lg border-2 focus:outline-none focus:ring-2 text-sm resize-none">Kampagnen vises på Top 30 sites med mindre andet er aftalt.</textarea>
</div><!-- Date Section -->
<div><label for="contract-date" class="block text-sm font-semibold mb-2">Dato for aftale</label> <input type="date" id="contract-date" class="w-full px-4 py-3 rounded-lg border-2 focus:outline-none focus:ring-2 text-lg">
</div><!-- Signature Section -->
<div id="signature-section" class="p-6 rounded-xl border-2">
<h2 class="text-xl font-bold mb-4">Underskrifter</h2>
<p class="text-xs mb-6 opacity-75 leading-relaxed">Kunden bekræfter med sin underskrift at være berettiget til tegning af aftalen, og at informationerne i kontrakten er korrekte. Kunden bekræfter ligeledes at have modtaget og forstået forretningsbetingelser og accepterer hermed disse. Alle priser er angivet ekskl. moms.</p><!-- Seller Signature -->
<div class="mb-6"><label class="block text-sm font-semibold mb-2">På vegne af Syntese Digital ApS</label>
<canvas id="seller-signature" class="w-full border-2 rounded-lg cursor-crosshair" style="height: 120px; background: white;"></canvas><button id="clear-seller" class="mt-2 text-xs px-3 py-1 rounded hover:opacity-80">Ryd</button>
<div class="mt-3"><label for="seller-name" class="block text-xs font-semibold mb-1">Navn</label> <input type="text" id="seller-name" placeholder="Indtast navn" class="w-full px-3 py-2 rounded-lg border-2 focus:outline-none focus:ring-2 text-sm">
</div>
</div><!-- Customer Signature -->
<div><label class="block text-sm font-semibold mb-2">Kunde Underskrift</label>
<canvas id="customer-signature" class="w-full border-2 rounded-lg cursor-crosshair" style="height: 120px; background: white;"></canvas><button id="clear-customer" class="mt-2 text-xs px-3 py-1 rounded hover:opacity-80">Ryd</button>
<div class="mt-3"><label for="customer-name-sig" class="block text-xs font-semibold mb-1">Kundens Navn</label> <input type="text" id="customer-name-sig" placeholder="Indtast navn" class="w-full px-3 py-2 rounded-lg border-2 focus:outline-none focus:ring-2 text-sm">
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4"><button id="download-pdf-button" class="py-3 rounded-lg font-semibold transition-all hover:opacity-90 active:scale-95"> 📄 Download PDF </button> <button id="reset-button" class="py-3 rounded-lg font-semibold transition-all hover:opacity-90 active:scale-95"> 🔄 Nulstil </button>
</div>
</div>
</div>
</div>
<script>
const defaultConfig = {
background_color: "#f0f4f8",
card_color: "#ffffff",
primary_color: "#3b82f6",
text_color: "#1e293b",
accent_color: "#f97316",
font_family: "Raleway",
font_size: 16,
calculator_title: "Kampagne Aftale",
budget_label: "Månedligt Budget",
cmp_label: "CPM (Pris Per 1.000 Visninger)",
result_label: "Samlede Eksponeringer"
};
let config = {};
let products = [];
let productIdCounter = 0;
const campaignTypes = [
{ name: "Display Kampagne - DV360 - Standard Formater", cpm: 70, type: "campaign" },
{ name: "Display Kampagne - DV360 - Topscroll Formater", cpm: 100, type: "campaign" },
{ name: "Display Kampagne - DV360 - Midtscroll Formater", cpm: 100, type: "campaign" },
{ name: "Re-targeting Kampagne - DV360", cpm: 70, type: "campaign" },
{ name: "Youtube Kampagne - DV360", cpm: 100, type: "campaign" },
{ name: "Banner produktion - Standard formater", cpm: 0, listPrice: 3000, type: "production" },
{ name: "Banner produktion - High Impact formater", cpm: 0, listPrice: 5000, type: "production" }
];
function createProduct() {
const productId = productIdCounter++;
const firstType = campaignTypes[0];
const product = {
id: productId,
name: firstType.name,
budget: firstType.type === 'production' ? (firstType.listPrice || 0) : 0,
cpm: firstType.cpm,
months: 1,
quantity: 1,
listPrice: firstType.listPrice || firstType.cpm,
type: firstType.type
};
products.push(product);
renderProducts();
calculateTotals();
}
function removeProduct(productId) {
products = products.filter(p => p.id !== productId);
renderProducts();
calculateTotals();
}
function updateProduct(productId, field, value) {
const product = products.find(p => p.id === productId);
if (product) {
product[field] = value;
calculateTotals();
}
}
function renderProducts() {
const container = document.getElementById('products-container');
container.innerHTML = '';
products.forEach(product => {
const productDiv = document.createElement('div');
productDiv.className = 'product-card p-6 rounded-xl border-2';
productDiv.dataset.productId = product.id;
const isProduction = product.type === 'production';
productDiv.innerHTML = `
<div class="flex justify-between items-center mb-4">
<button class="remove-product-btn text-red-500 hover:text-red-700 font-bold text-xl" data-product-id="${product.id}">×</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-semibold mb-2">Kampagnetype</label>
<select
class="product-type-select w-full px-4 py-2 rounded-lg border-2 focus:outline-none focus:ring-2 text-sm font-semibold"
data-product-id="${product.id}"
>
${campaignTypes.map((type, index) => `
<option value="${index}" ${product.name === type.name ? 'selected' : ''}>
${type.name}
</option>
`).join('')}
</select>
</div>
<div>
<label class="block text-sm font-semibold mb-2">${isProduction ? 'Pris' : 'Månedligt Budget'}</label>
<div class="relative">
<span class="absolute left-4 top-1/2 transform -translate-y-1/2 text-lg font-semibold">kr</span>
<input
type="text"
class="product-budget w-full pl-10 pr-4 py-2 rounded-lg border-2 focus:outline-none focus:ring-2"
value="${product.budget.toLocaleString('da-DK')}"
data-product-id="${product.id}"
>
</div>
</div>
${isProduction ? `
<div>
<label class="block text-sm font-semibold mb-2">Antal</label>
<input
type="number"
class="product-quantity w-full px-4 py-2 rounded-lg border-2 focus:outline-none focus:ring-2"
min="1"
step="1"
value="${product.quantity || 1}"
data-product-id="${product.id}"
>
<div class="product-discount mt-2 p-2 rounded-lg text-center text-sm font-semibold" style="background: #10b981; color: white; display: none;">
Rabat: 0%
</div>
</div>
` : ''}
${!isProduction ? `
<div>
<label class="block text-sm font-semibold mb-2">CPM (Pris Per 1.000 Visninger)</label>
<div class="relative">
<span class="absolute left-4 top-1/2 transform -translate-y-1/2 text-lg font-semibold">kr</span>
<input
type="text"
class="product-cpm w-full pl-10 pr-4 py-2 rounded-lg border-2 focus:outline-none focus:ring-2"
value="${product.cpm.toLocaleString('da-DK')}"
data-product-id="${product.id}"
>
</div>
<div class="product-discount mt-2 p-2 rounded-lg text-center text-sm font-semibold" style="background: #10b981; color: white; display: none;">
Rabat: 0%
</div>
</div>
<div>
<label class="block text-sm font-semibold mb-2">Periode (måneder)</label>
<input
type="number"
class="product-months w-full px-4 py-2 rounded-lg border-2 focus:outline-none focus:ring-2"
min="1"
step="1"
value="${product.months}"
data-product-id="${product.id}"
>
</div>
<div class="product-result p-4 rounded-lg text-center">
<p class="text-sm font-semibold mb-1">Eksponeringer</p>
<p class="text-2xl font-bold">0 visninger</p>
</div>
` : ''}
</div>
`;
container.appendChild(productDiv);
});
// Add event listeners
document.querySelectorAll('.product-type-select').forEach(select => {
select.addEventListener('change', (e) => {
const productId = parseInt(e.target.dataset.productId);
const selectedIndex = parseInt(e.target.value);
const selectedType = campaignTypes[selectedIndex];
const product = products.find(p => p.id === productId);
if (product) {
product.name = selectedType.name;
product.cpm = selectedType.cpm;
product.listPrice = selectedType.listPrice || selectedType.cpm;
product.type = selectedType.type;
// Set budget to list price for production items
if (selectedType.type === 'production') {
product.budget = selectedType.listPrice || 0;
}
// Re-render products to show/hide fields based on type
renderProducts();
calculateTotals();
}
});
});
document.querySelectorAll('.product-budget').forEach(input => {
input.addEventListener('input', (e) => {
const productId = parseInt(e.target.dataset.productId);
const rawValue = e.target.value.replace(/\./g, '').replace(',', '.');
const numValue = parseFloat(rawValue) || 0;
updateProduct(productId, 'budget', numValue);
e.target.value = numValue.toLocaleString('da-DK');
});
input.addEventListener('focus', (e) => {
const productId = parseInt(e.target.dataset.productId);
const product = products.find(p => p.id === productId);
if (product && product.budget === 0) {
e.target.value = '';
}
});
input.addEventListener('blur', (e) => {
const productId = parseInt(e.target.dataset.productId);
const product = products.find(p => p.id === productId);
if (product) {
e.target.value = product.budget.toLocaleString('da-DK');
}
});
});
document.querySelectorAll('.product-cpm').forEach(input => {
input.addEventListener('input', (e) => {
const productId = parseInt(e.target.dataset.productId);
const rawValue = e.target.value.replace(/\./g, '').replace(',', '.');
const numValue = parseFloat(rawValue) || 0;
updateProduct(productId, 'cpm', numValue);
e.target.value = numValue.toLocaleString('da-DK');
});
input.addEventListener('focus', (e) => {
const productId = parseInt(e.target.dataset.productId);
const product = products.find(p => p.id === productId);
if (product && product.cpm === 0) {
e.target.value = '';
}
});
input.addEventListener('blur', (e) => {
const productId = parseInt(e.target.dataset.productId);
const product = products.find(p => p.id === productId);
if (product) {
e.target.value = product.cpm.toLocaleString('da-DK');
}
});
});
document.querySelectorAll('.product-months').forEach(input => {
input.addEventListener('input', (e) => {
const productId = parseInt(e.target.dataset.productId);
updateProduct(productId, 'months', parseInt(e.target.value) || 1);
});
});
document.querySelectorAll('.product-quantity').forEach(input => {
input.addEventListener('input', (e) => {
const productId = parseInt(e.target.dataset.productId);
updateProduct(productId, 'quantity', parseInt(e.target.value) || 1);
});
});
document.querySelectorAll('.remove-product-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const productId = parseInt(e.target.dataset.productId);
removeProduct(productId);
});
});
applyStylesToProducts();
}
function calculateTotals() {
let totalExposures = 0;
let totalPrice = 0;
let totalNormalPrice = 0;
let totalCampaignMonthlyPrice = 0;
let totalProductionPrice = 0;
let maxMonths = 0;
const productsSummary = document.getElementById('products-summary');
productsSummary.innerHTML = '';
products.forEach(product => {
const budget = product.budget;
const cpm = product.cpm;
const months = product.months;
const quantity = product.quantity || 1;
const listPrice = product.listPrice || 70;
const isProduction = product.type === 'production';
let exposures = 0;
let productPrice = 0;
let normalPriceForProduct = 0;
if (isProduction) {
// For production items, multiply by quantity
productPrice = budget * quantity;
normalPriceForProduct = listPrice * quantity;
totalProductionPrice += productPrice;
} else {
// For campaign items, calculate based on CPM
if (cpm > 0) {
exposures = (budget / cpm) * 1000 * months;
}
totalExposures += exposures;
productPrice = budget * months;
normalPriceForProduct = (exposures / 1000) * listPrice;
totalCampaignMonthlyPrice += budget;
if (months > maxMonths) {
maxMonths = months;
}
}
totalPrice += productPrice;
totalNormalPrice += normalPriceForProduct;
// Update product result display
const productCard = document.querySelector(`[data-product-id="${product.id}"]`);
if (productCard) {
const resultDiv = productCard.querySelector('.product-result p:last-child');
if (resultDiv) {
resultDiv.textContent = `${exposures.toLocaleString('en-US', { maximumFractionDigits: 0 })} visninger`;
}
// Update discount percentage - only show if there's a discount
const discountDiv = productCard.querySelector('.product-discount');
if (discountDiv) {
const discountPercent = normalPriceForProduct > 0 ? ((normalPriceForProduct - productPrice) / normalPriceForProduct * 100) : 0;
if (discountPercent > 0.01) {
discountDiv.textContent = `Rabat: ${discountPercent.toFixed(0)}%`;
discountDiv.style.display = 'block';
} else {
discountDiv.style.display = 'none';
}
}
}
// Add to summary
if (!isProduction) {
// Campaign items - show exposures
if (exposures > 0) {
const summaryItem = document.createElement('div');
summaryItem.className = 'flex justify-between items-center text-sm';
summaryItem.innerHTML = `
<span>${product.name}:</span>
<span>${exposures.toLocaleString('en-US', { maximumFractionDigits: 0 })} visninger</span>
`;
productsSummary.appendChild(summaryItem);
}
} else {
// Production items - show price and discount
const productDiscount = normalPriceForProduct - productPrice;
const summaryItem = document.createElement('div');
summaryItem.className = 'flex justify-between items-center text-sm';
summaryItem.innerHTML = `
<span>${product.name}:</span>
<span>kr ${productPrice.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} (Rabat: kr ${productDiscount.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })})</span>
`;
productsSummary.appendChild(summaryItem);
}
});
const discountAmount = totalNormalPrice - totalPrice;
const vatAmount = totalPrice * 0.135;
const grandTotal = totalPrice + vatAmount;
document.getElementById('total-exposures').textContent = `${totalExposures.toLocaleString('en-US', { maximumFractionDigits: 0 })} visninger`;
const normalPriceElement = document.getElementById('normal-price').parentElement;
const discountElement = document.getElementById('discount-amount').parentElement;
// Only show discount lines if there's an actual discount (more than 1 kr)
if (discountAmount > 1) {
document.getElementById('normal-price').textContent = `kr ${totalNormalPrice.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
document.getElementById('discount-amount').textContent = `kr ${discountAmount.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
normalPriceElement.style.display = 'flex';
discountElement.style.display = 'flex';
} else {
normalPriceElement.style.display = 'none';
discountElement.style.display = 'none';
}
document.getElementById('total-price').textContent = `kr ${totalPrice.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
document.getElementById('vat-amount').textContent = `kr ${vatAmount.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
document.getElementById('grand-total').textContent = `kr ${grandTotal.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
// Calculate payment summary
const paymentSummary = document.getElementById('payment-summary');
if (products.length > 0) {
let upfrontPayment = 0;
let monthlyPayment = 0;
// Add production costs to upfront
upfrontPayment += totalProductionPrice;
// Calculate campaign payments
if (maxMonths > 6) {
// Over 6 months: Tech fee + first 3 months upfront, then monthly
const techFee = totalPrice * 0.135;
const first3Months = totalCampaignMonthlyPrice * 3;
upfrontPayment += techFee + first3Months;
monthlyPayment = totalCampaignMonthlyPrice;
} else if (maxMonths > 0) {
// Under 6 months: everything upfront
upfrontPayment += totalCampaignMonthlyPrice * maxMonths + (totalCampaignMonthlyPrice * maxMonths * 0.135);
monthlyPayment = 0;
}
let summaryText = '';
if (upfrontPayment > 0) {
summaryText += `Upfront betaling: kr ${upfrontPayment.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
if (monthlyPayment > 0) {
if (summaryText) summaryText += '\n';
summaryText += `Månedlig betaling: kr ${monthlyPayment.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
paymentSummary.textContent = summaryText || 'Tilføj produkter for at se betalingsoversigt';
paymentSummary.style.whiteSpace = 'pre-line';
} else {
paymentSummary.textContent = 'Tilføj produkter for at se betalingsoversigt';
}
}
function validateForm() {
const requiredFields = [
{ id: 'company-name', label: 'Firmanavn' },
{ id: 'address', label: 'Adresse' },
{ id: 'cvr', label: 'CVR' },
{ id: 'contact-person', label: 'Kontaktperson' },
{ id: 'email', label: 'E-mail' },
{ id: 'phone', label: 'Telefonnummer' }
];
const missingFields = [];
const invalidFields = [];
// Check required fields
requiredFields.forEach(field => {
const element = document.getElementById(field.id);
const value = element.value.trim();
// Remove any existing error styling
element.style.borderColor = '#e5e7eb';
element.style.boxShadow = 'none';
if (!value) {
missingFields.push(field.label);
// Add error styling
element.style.borderColor = '#ef4444';
element.style.boxShadow = '0 0 0 3px rgba(239, 68, 68, 0.1)';
}
});
// Check if at least one product exists
if (products.length === 0) {
missingFields.push('Mindst ét produkt');
}
return {
isValid: missingFields.length === 0,
missingFields: missingFields
};
}
function showValidationError(missingFields) {
// Create modal overlay
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
// Create modal content
const modal = document.createElement('div');
modal.style.cssText = `
background: white;
padding: 30px;
border-radius: 12px;
max-width: 500px;
width: 90%;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
text-align: center;
`;
modal.innerHTML = `
<div style="color: #ef4444; font-size: 48px; margin-bottom: 20px;">⚠️</div>
<h3 style="color: #1e293b; margin: 0 0 15px 0; font-size: 24px;">Manglende oplysninger</h3>
<p style="color: #64748b; margin: 0 0 20px 0; line-height: 1.6;">
Du skal udfylde følgende felter før du kan downloade PDF'en:
</p>
<ul style="color: #ef4444; text-align: left; margin: 0 0 25px 0; padding-left: 20px;">
${missingFields.map(field => `<li style="margin-bottom: 5px;"><strong>${field}</strong></li>`).join('')}
</ul>
<button id="close-validation-modal" style="
background: #3b82f6;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
font-size: 16px;
transition: all 0.2s;
">OK, jeg forstår</button>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Add hover effect to button
const closeBtn = modal.querySelector('#close-validation-modal');
closeBtn.addEventListener('mouseenter', () => {
closeBtn.style.background = '#2563eb';
});
closeBtn.addEventListener('mouseleave', () => {
closeBtn.style.background = '#3b82f6';
});
// Close modal functionality
function closeModal() {
document.body.removeChild(overlay);
}
closeBtn.addEventListener('click', closeModal);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeModal();
}
});
// Focus first missing field (if it's an input)
setTimeout(() => {
const firstMissingField = document.querySelector('input[style*="border-color: rgb(239, 68, 68)"]');
if (firstMissingField) {
firstMissingField.focus();
}
}, 100);
}
async function downloadPDF() {
// Validate form before proceeding
const validation = validateForm();
if (!validation.isValid) {
showValidationError(validation.missingFields);
return;
}
// Show loading state
const downloadButton = document.getElementById('download-pdf-button');
const originalText = downloadButton.textContent;
downloadButton.textContent = '⏳ Genererer PDF...';
downloadButton.disabled = true;
// Generate contract ID with company_cvr_date format
const companyName = document.getElementById('company-name').value || 'FIRMA';
const cvr = document.getElementById('cvr').value || 'CVR';
const contractDate = document.getElementById('contract-date').value || new Date().toISOString().split('T')[0];
// Clean company name and CVR for filename (remove spaces and special characters)
const cleanCompanyName = companyName.replace(/[^a-zA-Z0-9æøåÆØÅ]/g, '').toUpperCase();
const cleanCvr = cvr.replace(/[^0-9]/g, '');
// Convert date from YYYY-MM-DD to DDMMYYYY format
const dateParts = contractDate.split('-');
const cleanDate = dateParts.length === 3 ? `${dateParts[2]}${dateParts[1]}${dateParts[0]}` : contractDate.replace(/-/g, '');
const contractId = `${cleanCompanyName}_${cleanCvr}_${cleanDate}`;
try {
// Get all data
const companyName = document.getElementById('company-name').value || 'N/A';
const address = document.getElementById('address').value || 'N/A';
const cvr = document.getElementById('cvr').value || 'N/A';
const contactPerson = document.getElementById('contact-person').value || 'N/A';
const email = document.getElementById('email').value || 'N/A';
const website = document.getElementById('website').value || 'N/A';
const phone = document.getElementById('phone').value || 'N/A';
const mobile = document.getElementById('mobile').value || 'N/A';
const totalExposures = document.getElementById('total-exposures').textContent;
const normalPrice = document.getElementById('normal-price').textContent;
const discountAmount = document.getElementById('discount-amount').textContent;
const totalPrice = document.getElementById('total-price').textContent;
const vatAmount = document.getElementById('vat-amount').textContent;
const grandTotal = document.getElementById('grand-total').textContent;
const comments = document.getElementById('comments').value || 'Ingen kommentarer';
const sellerName = document.getElementById('seller-name').value || 'N/A';
const customerNameSig = document.getElementById('customer-name-sig').value || 'N/A';
const contractDate = document.getElementById('contract-date').value || new Date().toLocaleDateString('da-DK');
const paymentSummary = document.getElementById('payment-summary').textContent;
// Get signature images
const sellerCanvas = document.getElementById('seller-signature');
const customerCanvas = document.getElementById('customer-signature');
const sellerSignature = sellerCanvas.toDataURL('image/png');
const customerSignature = customerCanvas.toDataURL('image/png');
// Check if there's a discount to show
const totalDiscountAmount = parseFloat(discountAmount.replace('kr ', '').replace(/\./g, '').replace(',', '.'));
const showDiscount = totalDiscountAmount > 1;
// Build products HTML
let productsHTML = '';
products.forEach(product => {
const isProduction = product.type === 'production';
const quantity = product.quantity || 1;
const exposures = !isProduction && product.cpm > 0 ? ((product.budget / product.cpm) * 1000 * product.months) : 0;
const listPrice = product.listPrice || 70;
const productPrice = isProduction ? product.budget * quantity : product.budget * product.months;
const normalPrice = isProduction ? listPrice * quantity : (exposures / 1000) * listPrice;
const discount = normalPrice - productPrice;
const discountPercent = normalPrice > 0 ? ((discount / normalPrice) * 100) : 0;
productsHTML += `
<div style="margin-bottom: 20px; padding: 15px; border: 2px solid #e5e7eb; border-radius: 8px;">
<h4 style="margin: 0 0 10px 0; color: #1e293b; font-size: 16px;">${product.name}</h4>
${isProduction ? `
<p style="margin: 5px 0;"><strong>Pris:</strong> kr ${product.budget.toLocaleString('da-DK')}</p>
<p style="margin: 5px 0;"><strong>Antal:</strong> ${quantity}</p>
<p style="margin: 5px 0;"><strong>Listepris:</strong> kr ${listPrice.toLocaleString('da-DK')} pr. stk.</p>
<p style="margin: 5px 0;"><strong>Total Listepris:</strong> kr ${normalPrice.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</p>
${discountPercent > 0.01 ? `<p style="margin: 5px 0; color: #10b981;"><strong>Rabat:</strong> ${discountPercent.toFixed(0)}% - Spar kr ${discount.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</p>` : ''}
` : `
<p style="margin: 5px 0;"><strong>Månedligt Budget:</strong> kr ${product.budget.toLocaleString('da-DK')}</p>
<p style="margin: 5px 0;"><strong>CPM:</strong> kr ${product.cpm.toLocaleString('da-DK')}</p>
<p style="margin: 5px 0;"><strong>Listepris CPM:</strong> kr ${listPrice.toLocaleString('da-DK')}</p>
<p style="margin: 5px 0;"><strong>Periode:</strong> ${product.months} måneder</p>
${discountPercent > 0.01 ? `<p style="margin: 5px 0; color: #10b981;"><strong>Rabat:</strong> ${discountPercent.toFixed(0)}% - Spar kr ${discount.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</p>` : ''}
<p style="margin: 5px 0; color: #f97316;"><strong>Eksponeringer:</strong> ${exposures.toLocaleString('en-US', { maximumFractionDigits: 0 })} visninger</p>
`}
</div>
`;
});
// Create HTML document
const htmlContent = `<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kampagne Aftale - ${companyName}</title>
<style>
body {
font-family: 'Raleway', Arial, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 40px 20px;
color: #1e293b;
line-height: 1.6;
}
h1 {
text-align: center;
color: #1e293b;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
color: #64748b;
margin-bottom: 10px;
}
.contract-id {
text-align: center;
color: #f97316;
font-weight: bold;
font-size: 14px;
margin-bottom: 30px;
padding: 8px 16px;
background: #fef3e2;
border-radius: 6px;
display: inline-block;
width: 100%;
box-sizing: border-box;
}
.section {
margin-bottom: 30px;
padding: 20px;
border: 2px solid #e5e7eb;
border-radius: 8px;
}
.section h2 {
margin-top: 0;
color: #1e293b;
border-bottom: 2px solid #f97316;
padding-bottom: 10px;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.info-item {
margin-bottom: 10px;
}
.info-item strong {
display: block;
color: #64748b;
font-size: 14px;
margin-bottom: 3px;
}
.price-summary {
background: #f0f4f8;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
}
.price-row {
display: flex;
justify-content: space-between;
margin: 10px 0;
padding: 8px 0;
}
.price-row.total {
border-top: 2px solid #1e293b;
font-weight: bold;
font-size: 18px;
color: #3b82f6;
}
.signature-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-top: 20px;
}
.signature-box {
text-align: center;
}
.signature-box img {
width: 100%;
max-width: 300px;
height: 120px;
border: none;
margin: 10px 0;
}
.signature-line {
border-top: 2px solid #1e293b;
margin-top: 10px;
padding-top: 5px;
}
.disclaimer {
background: #64748b;
color: white;
padding: 15px;
border-radius: 8px;
font-size: 12px;
margin-top: 20px;
}
.payment-info {
background: #374151;
color: white;
padding: 15px;
border-radius: 8px;
margin: 15px 0;
white-space: pre-line;
}
@media print {
body {
padding: 20px;
}
.no-print {
display: none;
}
}
</style>
</head>
<body>
<h1>Kampagne Aftale</h1>
<p class="subtitle">Syntese Digital ApS</p>
<div class="contract-id">Kontrakt ID: ${contractId}</div>
<div class="section">
<h2>Kundeoplysninger</h2>
<div class="info-grid">
<div class="info-item">
<strong>Firmanavn</strong>
${companyName}
</div>
<div class="info-item">
<strong>Adresse</strong>
${address}
</div>
<div class="info-item">
<strong>CVR</strong>
${cvr}
</div>
<div class="info-item">
<strong>Kontaktperson</strong>
${contactPerson}
</div>
<div class="info-item">
<strong>E-mail</strong>
${email}
</div>
<div class="info-item">
<strong>Hjemmeside</strong>
${website}
</div>
<div class="info-item">
<strong>Telefonnummer</strong>
${phone}
</div>
<div class="info-item">
<strong>Mobilnummer</strong>
${mobile}
</div>
</div>
</div>
<div class="section">
<h2>Produkter</h2>
${productsHTML}
</div>
<div class="section">
<h2>Pris Oversigt</h2>
<div class="price-summary">
<div class="price-row">
<span><strong>Samlede Eksponeringer:</strong></span>
<span style="color: #f97316; font-weight: bold;">${totalExposures}</span>
</div>
${showDiscount ? `
<div class="price-row" style="font-size: 14px; opacity: 0.7;">
<span>Normalpris (Listepris):</span>
<span style="text-decoration: line-through;">${normalPrice}</span>
</div>
<div class="price-row">
<span>Rabat:</span>
<span style="color: #10b981; font-weight: bold;">${discountAmount}</span>
</div>
` : ''}
<div class="price-row">
<span><strong>Din pris (ekskl. Tech Fee):</strong></span>
<span><strong>${totalPrice}</strong></span>
</div>
<div class="price-row" style="font-size: 14px;">
<span>Tech Fee (13,5%):</span>
<span>${vatAmount}</span>
</div>
<div class="price-row total">
<span style="color: #f97316;">Total pris (inkl. Tech Fee):</span>
<span style="color: #f97316;">${grandTotal}</span>
</div>
</div>
<div class="payment-info">
${paymentSummary}
</div>
</div>
<div class="disclaimer">
<p style="margin: 0 0 10px 0;"><strong>Forretnings- og betalingsbetingelser:</strong></p>
<p style="margin: 0 0 10px 0;">For aftaler over 6 måneder, faktureres Tech fee og de første 3 måneder upfront. Efterfølgende faktureres man månedligt forud. For aftaler under 6 måneder, faktureres hele beløbet upfront. Banner produktion betales upfront.</p>
<p style="margin: 0;">Der henvises til vores forretningsbetingelser: <a href="https://syntesedigital.dk/forretningsbetingelser" target="_blank" style="color: white; text-decoration: underline;">syntesedigital.dk/forretningsbetingelser</a></p>
</div>
<div class="section">
<h2>Kommentarer / Særaftaler</h2>
<p style="white-space: pre-wrap;">${comments}</p>
</div>
<div class="section">
<h2>Dato for aftale</h2>
<p><strong>${contractDate}</strong></p>
</div>
<div class="section">
<h2>Underskrifter</h2>
<p style="font-size: 12px; color: #64748b; margin-bottom: 20px;">
Kunden bekræfter med sin underskrift at være berettiget til tegning af aftalen, og at informationerne i kontrakten er korrekte. Kunden bekræfter ligeledes at have modtaget og forstået forretningsbetingelser og accepterer hermed disse. Alle priser er angivet ekskl. moms.
</p>
<div class="signature-container">
<div class="signature-box">
<p><strong>På vegne af Syntese Digital ApS</strong></p>
<img src="${sellerSignature}" alt="Sælger underskrift">
<div class="signature-line">${sellerName}</div>
</div>
<div class="signature-box">
<p><strong>Kunde Underskrift</strong></p>
<img src="${customerSignature}" alt="Kunde underskrift">
<div class="signature-line">${customerNameSig}</div>
</div>
</div>
</div>
<div class="no-print" style="text-align: center; margin-top: 40px; padding: 20px; background: #f0f4f8; border-radius: 8px;">
<p style="margin-bottom: 0; font-weight: bold;">Tryk Ctrl+P (Windows) eller Cmd+P (Mac) for at gemme som PDF</p>
</div>
<p style="text-align: center; color: #94a3b8; font-size: 12px; margin-top: 40px;">
Genereret af Syntese Digital Kampagne Aftale
</p>
</body>
</html>
`;
// Open in new window
const newWindow = window.open('', '_blank');
if (newWindow) {
newWindow.document.write(htmlContent);
newWindow.document.close();
// Show success message
const successMsg = document.createElement('div');
successMsg.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #10b981; color: white; padding: 16px 24px; border-radius: 8px; z-index: 9999; font-weight: bold;';
successMsg.textContent = '✅ PDF åbnet i nyt vindue - Tryk Ctrl+P for at gemme';
document.body.appendChild(successMsg);
setTimeout(() => successMsg.remove(), 4000);
} else {
// Show error if popup was blocked
const errorMsg = document.createElement('div');
errorMsg.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #ef4444; color: white; padding: 16px 24px; border-radius: 8px; z-index: 9999; font-weight: bold;';
errorMsg.textContent = '❌ Tillad pop-up vinduer i din browser';
document.body.appendChild(errorMsg);
setTimeout(() => errorMsg.remove(), 4000);
}
} catch (error) {
console.error('Error generating PDF:', error);
const errorMsg = document.createElement('div');
errorMsg.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #ef4444; color: white; padding: 16px 24px; border-radius: 8px; z-index: 9999; font-weight: bold;';
errorMsg.textContent = '❌ Fejl ved generering af PDF';
document.body.appendChild(errorMsg);
setTimeout(() => errorMsg.remove(), 4000);
} finally {
// Reset button state
downloadButton.textContent = originalText;
downloadButton.disabled = false;
}
}
function applyStylesToProducts() {
const textColor = config.text_color || defaultConfig.text_color;
const accentColor = config.accent_color || defaultConfig.accent_color;
document.querySelectorAll('.product-card').forEach(card => {
card.style.borderColor = '#e5e7eb';
card.style.color = textColor;
});
document.querySelectorAll('.product-type-select').forEach(select => {
select.style.borderColor = '#e5e7eb';
select.style.color = textColor;
});
document.querySelectorAll('.product-budget, .product-cpm, .product-months').forEach(input => {
input.style.borderColor = '#e5e7eb';
input.style.color = textColor;
});
document.querySelectorAll('.product-result').forEach(result => {
result.style.background = accentColor;
result.style.color = '#ffffff';
});
}
async function onConfigChange(newConfig) {
const baseFont = newConfig.font_family || defaultConfig.font_family;
const baseFontStack = 'Raleway, system-ui, -apple-system, sans-serif';
const fontSize = newConfig.font_size || defaultConfig.font_size;
document.body.style.background = newConfig.background_color || defaultConfig.background_color;
document.body.style.fontFamily = `${baseFont}, ${baseFontStack}`;
const card = document.getElementById('calculator-card');
card.style.background = newConfig.card_color || defaultConfig.card_color;
const title = document.getElementById('calculator-title');
title.textContent = newConfig.calculator_title || defaultConfig.calculator_title;
title.style.color = newConfig.text_color || defaultConfig.text_color;
title.style.fontSize = `${fontSize * 1.875}px`;
title.style.fontFamily = `${baseFont}, ${baseFontStack}`;
const priceContainer = document.getElementById('price-container');
priceContainer.style.background = newConfig.card_color || defaultConfig.card_color;
priceContainer.style.borderColor = '#e5e7eb';
priceContainer.style.borderWidth = '2px';
priceContainer.style.borderStyle = 'solid';
priceContainer.style.color = newConfig.text_color || defaultConfig.text_color;
const resetButton = document.getElementById('reset-button');
const accentColor = newConfig.accent_color || defaultConfig.accent_color;
resetButton.style.background = accentColor + '33';
resetButton.style.color = accentColor;
resetButton.style.border = `2px solid ${accentColor}`;
const downloadButton = document.getElementById('download-pdf-button');
downloadButton.style.background = accentColor;
downloadButton.style.color = '#ffffff';
downloadButton.style.border = `2px solid ${accentColor}`;
const addProductButton = document.getElementById('add-product-button');
addProductButton.style.background = accentColor;
addProductButton.style.color = '#ffffff';
addProductButton.style.border = `2px solid ${accentColor}`;
const customerInfoSection = document.getElementById('customer-info-section');
customerInfoSection.style.borderColor = '#e5e7eb';
customerInfoSection.style.color = newConfig.text_color || defaultConfig.text_color;
const customerInputs = customerInfoSection.querySelectorAll('input');
customerInputs.forEach(input => {
input.style.borderColor = '#e5e7eb';
input.style.color = newConfig.text_color || defaultConfig.text_color;
input.style.background = '#ffffff';
});
const disclaimerSection = document.getElementById('disclaimer-section');
disclaimerSection.style.background = '#64748b';
disclaimerSection.style.color = '#ffffff';
const commentsTextarea = document.getElementById('comments');
commentsTextarea.style.borderColor = '#e5e7eb';
commentsTextarea.style.color = newConfig.text_color || defaultConfig.text_color;
const contractDateInput = document.getElementById('contract-date');
contractDateInput.style.borderColor = '#e5e7eb';
contractDateInput.style.color = newConfig.text_color || defaultConfig.text_color;
const signatureSection = document.getElementById('signature-section');
signatureSection.style.borderColor = '#e5e7eb';
signatureSection.style.color = newConfig.text_color || defaultConfig.text_color;
const signatureInputs = signatureSection.querySelectorAll('input');
signatureInputs.forEach(input => {
input.style.borderColor = '#e5e7eb';
input.style.color = newConfig.text_color || defaultConfig.text_color;
});
const signatureCanvases = signatureSection.querySelectorAll('canvas');
signatureCanvases.forEach(canvas => {
canvas.style.borderColor = '#e5e7eb';
});
const clearButtons = signatureSection.querySelectorAll('button');
clearButtons.forEach(btn => {
btn.style.background = newConfig.accent_color || defaultConfig.accent_color;
btn.style.color = '#ffffff';
});
applyStylesToProducts();
}
// Signature functionality
function setupSignaturePad(canvasId, clearButtonId) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
const clearBtn = document.getElementById(clearButtonId);
let isDrawing = false;
let lastX = 0;
let lastY = 0;
function resizeCanvas() {
const rect = canvas.getBoundingClientRect();
const containerWidth = canvas.parentElement.offsetWidth;
// Calculate responsive height based on screen size
let canvasHeight = 120;
if (window.innerWidth < 768) {
canvasHeight = 180;
}
if (window.innerHeight > window.innerWidth) {
canvasHeight = 220;
}
// Save current drawing
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Resize canvas
canvas.width = containerWidth;
canvas.height = canvasHeight;
// Fill with white background
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Restore drawing if it existed
if (imageData.width > 0) {
ctx.putImageData(imageData, 0, 0);
}
}
resizeCanvas();
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(resizeCanvas, 100);
});
function getCoordinates(e) {
const rect = canvas.getBoundingClientRect();
let x, y;
if (e.touches && e.touches.length > 0) {
x = e.touches[0].clientX - rect.left;
y = e.touches[0].clientY - rect.top;
} else {
x = e.clientX - rect.left;
y = e.clientY - rect.top;
}
return { x, y };
}
function startDrawing(e) {
e.preventDefault();
isDrawing = true;
const coords = getCoordinates(e);
lastX = coords.x;
lastY = coords.y;
}
function draw(e) {
if (!isDrawing) return;
e.preventDefault();
const coords = getCoordinates(e);
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(coords.x, coords.y);
ctx.stroke();
lastX = coords.x;
lastY = coords.y;
}
function stopDrawing() {
isDrawing = false;
}
// Mouse events
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseleave', stopDrawing);
// Touch events
canvas.addEventListener('touchstart', startDrawing, { passive: false });
canvas.addEventListener('touchmove', draw, { passive: false });
canvas.addEventListener('touchend', stopDrawing);
canvas.addEventListener('touchcancel', stopDrawing);
clearBtn.addEventListener('click', () => {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
});
}
function resetCalculator() {
products = [];
productIdCounter = 0;
renderProducts();
calculateTotals();
document.getElementById('company-name').value = '';
document.getElementById('cvr').value = '';
document.getElementById('address').value = '';
document.getElementById('contact-person').value = '';
document.getElementById('email').value = '';
document.getElementById('website').value = '';
document.getElementById('phone').value = '';
document.getElementById('mobile').value = '';
// Sync contact person name to signature field
document.getElementById('contact-person').addEventListener('input', (e) => {
document.getElementById('customer-name-sig').value = e.target.value;
});
document.getElementById('comments').value = '';
document.getElementById('seller-name').value = '';
document.getElementById('customer-name-sig').value = '';
const today = new Date().toISOString().split('T')[0];
document.getElementById('contract-date').value = today;
// Clear signatures
const sellerCanvas = document.getElementById('seller-signature');
const customerCanvas = document.getElementById('customer-signature');
sellerCanvas.getContext('2d').clearRect(0, 0, sellerCanvas.width, sellerCanvas.height);
customerCanvas.getContext('2d').clearRect(0, 0, customerCanvas.width, customerCanvas.height);
}
async function initializeApp() {
// Initialize Element SDK
if (window.elementSdk) {
window.elementSdk.init({
defaultConfig,
onConfigChange,
mapToCapabilities: (config) => ({
recolorables: [
{
get: () => config.background_color || defaultConfig.background_color,
set: (value) => {
config.background_color = value;
window.elementSdk.setConfig({ background_color: value });
}
},
{
get: () => config.card_color || defaultConfig.card_color,
set: (value) => {
config.card_color = value;
window.elementSdk.setConfig({ card_color: value });
}
},
{
get: () => config.primary_color || defaultConfig.primary_color,
set: (value) => {
config.primary_color = value;
window.elementSdk.setConfig({ primary_color: value });
}
},
{
get: () => config.text_color || defaultConfig.text_color,
set: (value) => {
config.text_color = value;
window.elementSdk.setConfig({ text_color: value });
}
},
{
get: () => config.accent_color || defaultConfig.accent_color,
set: (value) => {
config.accent_color = value;
window.elementSdk.setConfig({ accent_color: value });
}
}
],
borderables: [],
fontEditable: {
get: () => config.font_family || defaultConfig.font_family,
set: (value) => {
config.font_family = value;
window.elementSdk.setConfig({ font_family: value });
}
},
fontSizeable: {
get: () => config.font_size || defaultConfig.font_size,
set: (value) => {
config.font_size = value;
window.elementSdk.setConfig({ font_size: value });
}
}
}),
mapToEditPanelValues: (config) => new Map([
["calculator_title", config.calculator_title || defaultConfig.calculator_title],
["budget_label", config.budget_label || defaultConfig.budget_label],
["cpm_label", config.cpm_label || defaultConfig.cpm_label],
["result_label", config.result_label || defaultConfig.result_label]
])
});
config = window.elementSdk.config;
onConfigChange(config);
} else {
config = defaultConfig;
onConfigChange(config);
}
}
// Initialize the application
initializeApp();
// Set today's date as default
const today = new Date().toISOString().split('T')[0];
document.getElementById('contract-date').value = today;
// Sync contact person name to customer signature name field
document.getElementById('contact-person').addEventListener('input', (e) => {
document.getElementById('customer-name-sig').value = e.target.value;
});
document.getElementById('add-product-button').addEventListener('click', createProduct);
document.getElementById('reset-button').addEventListener('click', resetCalculator);
document.getElementById('download-pdf-button').addEventListener('click', downloadPDF);
// Initialize signature pads
setupSignaturePad('seller-signature', 'clear-seller');
setupSignaturePad('customer-signature', 'clear-customer');
</script>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'99df4b5126999302',t:'MTc2MzA0NzY1Ny4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>