- Implemented a new endpoint for searching webshop products with filters for visibility and configuration. - Enhanced the webshop frontend to include a customer search feature for improved user experience. - Added opportunity line items management with CRUD operations and comments functionality. - Created database migrations for opportunity line items and comments, including necessary triggers and indexes.
669 lines
29 KiB
HTML
669 lines
29 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Webshop Administration - BMC Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.webshop-card {
|
|
transition: all 0.2s;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.webshop-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 0.375rem 0.75rem;
|
|
border-radius: 20px;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.color-preview {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 50%;
|
|
border: 2px solid rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.form-label-required::after {
|
|
content: " *";
|
|
color: #dc3545;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-5">
|
|
<div>
|
|
<h2 class="fw-bold mb-1">Webshop Administration</h2>
|
|
<p class="text-muted mb-0">Administrer kunde-webshops og konfigurationer</p>
|
|
</div>
|
|
<div class="d-flex gap-3">
|
|
<button class="btn btn-primary" onclick="openCreateModal()">
|
|
<i class="bi bi-plus-lg me-2"></i>Opret Webshop
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4" id="webshopsGrid">
|
|
<div class="col-12 text-center py-5">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create/Edit Modal -->
|
|
<div class="modal fade" id="webshopModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="modalTitle">Opret Webshop</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="webshopForm">
|
|
<input type="hidden" id="configId">
|
|
|
|
<!-- Kunde Search -->
|
|
<div class="mb-3">
|
|
<label for="customerSearch" class="form-label">Søg kunde</label>
|
|
<input type="search" class="form-control" id="customerSearch"
|
|
placeholder="Søg efter navn, CVR eller email">
|
|
<small class="form-text text-muted">
|
|
Begynd at skrive for at indsnævre listen og find den rigtige kunde hurtigt.
|
|
</small>
|
|
</div>
|
|
|
|
<!-- Kunde Selection -->
|
|
<div class="mb-3">
|
|
<label for="customerId" class="form-label form-label-required">Kunde</label>
|
|
<select class="form-select" id="customerId" required>
|
|
<option value="">Vælg kunde...</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Webshop Navn -->
|
|
<div class="mb-3">
|
|
<label for="webshopName" class="form-label form-label-required">Webshop Navn</label>
|
|
<input type="text" class="form-control" id="webshopName" required
|
|
placeholder="fx 'Advokatfirmaet A/S Webshop'">
|
|
</div>
|
|
|
|
<!-- Email Domæner -->
|
|
<div class="mb-3">
|
|
<label for="emailDomains" class="form-label form-label-required">Tilladte Email Domæner</label>
|
|
<input type="text" class="form-control" id="emailDomains" required
|
|
placeholder="fx 'firma.dk,firma.com' (komma-separeret)">
|
|
<small class="form-text text-muted">Kun brugere med disse email-domæner kan logge ind</small>
|
|
</div>
|
|
|
|
<!-- Header & Intro Text -->
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="headerText" class="form-label">Header Tekst</label>
|
|
<input type="text" class="form-control" id="headerText"
|
|
placeholder="fx 'Velkommen til vores webshop'">
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label for="introText" class="form-label">Intro Tekst</label>
|
|
<textarea class="form-control" id="introText" rows="2"
|
|
placeholder="Kort introduktion til webshoppen"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Colors -->
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="primaryColor" class="form-label">Primær Farve</label>
|
|
<div class="input-group">
|
|
<input type="color" class="form-control form-control-color"
|
|
id="primaryColor" value="#0f4c75">
|
|
<input type="text" class="form-control" id="primaryColorHex"
|
|
value="#0f4c75" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label for="accentColor" class="form-label">Accent Farve</label>
|
|
<div class="input-group">
|
|
<input type="color" class="form-control form-control-color"
|
|
id="accentColor" value="#3282b8">
|
|
<input type="text" class="form-control" id="accentColorHex"
|
|
value="#3282b8" maxlength="7">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pricing -->
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label for="defaultMargin" class="form-label">Standard Avance (%)</label>
|
|
<input type="number" class="form-control" id="defaultMargin"
|
|
value="10" min="0" max="100" step="0.1">
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label for="minOrderAmount" class="form-label">Min. Ordre Beløb (DKK)</label>
|
|
<input type="number" class="form-control" id="minOrderAmount"
|
|
value="0" min="0" step="0.01">
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label for="shippingCost" class="form-label">Forsendelse (DKK)</label>
|
|
<input type="number" class="form-control" id="shippingCost"
|
|
value="0" min="0" step="0.01">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Enabled -->
|
|
<div class="mb-3 form-check">
|
|
<input type="checkbox" class="form-check-input" id="enabled" checked>
|
|
<label class="form-check-label" for="enabled">
|
|
Webshop aktiveret
|
|
</label>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
|
|
<button type="button" class="btn btn-primary" onclick="saveWebshop()">
|
|
<i class="bi bi-check-lg me-2"></i>Gem Webshop
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Products Modal -->
|
|
<div class="modal fade" id="productsModal" tabindex="-1">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Produkter - <span id="productsModalWebshopName"></span></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<div>
|
|
<p class="mb-0 text-muted">Administrer tilladte produkter for denne webshop</p>
|
|
</div>
|
|
<button class="btn btn-sm btn-primary" onclick="openAddProductModal()">
|
|
<i class="bi bi-plus-lg me-2"></i>Tilføj Produkt
|
|
</button>
|
|
</div>
|
|
|
|
<input type="hidden" id="currentConfigId">
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Varenr</th>
|
|
<th>Navn</th>
|
|
<th>EAN</th>
|
|
<th>Basispris</th>
|
|
<th>Avance %</th>
|
|
<th>Salgspris</th>
|
|
<th>Synlig</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="productsTableBody">
|
|
<tr>
|
|
<td colspan="8" class="text-center py-4 text-muted">Ingen produkter endnu</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Luk</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Product Modal -->
|
|
<div class="modal fade" id="addProductModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Tilføj Produkt</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="addProductForm">
|
|
<div class="mb-3">
|
|
<label for="productNumber" class="form-label form-label-required">Varenummer</label>
|
|
<input type="text" class="form-control" id="productNumber" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="productEan" class="form-label">EAN</label>
|
|
<input type="text" class="form-control" id="productEan">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="productName" class="form-label form-label-required">Navn</label>
|
|
<input type="text" class="form-control" id="productName" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="productDescription" class="form-label">Beskrivelse</label>
|
|
<textarea class="form-control" id="productDescription" rows="2"></textarea>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="productBasePrice" class="form-label form-label-required">Basispris (DKK)</label>
|
|
<input type="number" class="form-control" id="productBasePrice" required step="0.01">
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label for="productCustomMargin" class="form-label">Custom Avance (%)</label>
|
|
<input type="number" class="form-control" id="productCustomMargin" step="0.1" placeholder="Standard bruges">
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="productCategory" class="form-label">Kategori</label>
|
|
<input type="text" class="form-control" id="productCategory" placeholder="fx 'Network Security'">
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
|
|
<button type="button" class="btn btn-primary" onclick="addProduct()">
|
|
<i class="bi bi-plus-lg me-2"></i>Tilføj
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let webshopsData = [];
|
|
let currentWebshopConfig = null;
|
|
let webshopModal, productsModal, addProductModal;
|
|
let customerSearchTimeout;
|
|
|
|
// Load on page ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Initialize Bootstrap modals after DOM is loaded
|
|
webshopModal = new bootstrap.Modal(document.getElementById('webshopModal'));
|
|
productsModal = new bootstrap.Modal(document.getElementById('productsModal'));
|
|
addProductModal = new bootstrap.Modal(document.getElementById('addProductModal'));
|
|
|
|
loadWebshops();
|
|
loadCustomers();
|
|
initCustomerSearch();
|
|
|
|
// Color picker sync
|
|
document.getElementById('primaryColor').addEventListener('input', (e) => {
|
|
document.getElementById('primaryColorHex').value = e.target.value;
|
|
});
|
|
document.getElementById('primaryColorHex').addEventListener('input', (e) => {
|
|
document.getElementById('primaryColor').value = e.target.value;
|
|
});
|
|
document.getElementById('accentColor').addEventListener('input', (e) => {
|
|
document.getElementById('accentColorHex').value = e.target.value;
|
|
});
|
|
document.getElementById('accentColorHex').addEventListener('input', (e) => {
|
|
document.getElementById('accentColor').value = e.target.value;
|
|
});
|
|
});
|
|
|
|
async function loadWebshops() {
|
|
try {
|
|
const response = await fetch('/api/v1/webshop/configs');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
webshopsData = data.configs;
|
|
renderWebshops();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading webshops:', error);
|
|
showToast('Fejl ved indlæsning af webshops', 'danger');
|
|
}
|
|
}
|
|
|
|
function renderWebshops() {
|
|
const grid = document.getElementById('webshopsGrid');
|
|
|
|
if (webshopsData.length === 0) {
|
|
grid.innerHTML = `
|
|
<div class="col-12 text-center py-5">
|
|
<i class="bi bi-shop display-1 text-muted mb-3"></i>
|
|
<p class="text-muted">Ingen webshops oprettet endnu</p>
|
|
<button class="btn btn-primary" onclick="openCreateModal()">
|
|
<i class="bi bi-plus-lg me-2"></i>Opret Din Første Webshop
|
|
</button>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = webshopsData.map(ws => `
|
|
<div class="col-md-6 col-lg-4">
|
|
<div class="card webshop-card h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
<div>
|
|
<h5 class="card-title mb-1">${ws.name}</h5>
|
|
<small class="text-muted">${ws.customer_name || 'Ingen kunde'}</small>
|
|
</div>
|
|
<span class="status-badge ${ws.enabled ? 'bg-success bg-opacity-10 text-success' : 'bg-danger bg-opacity-10 text-danger'}">
|
|
${ws.enabled ? 'Aktiv' : 'Inaktiv'}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<small class="text-muted d-block mb-1">Branding</small>
|
|
<div class="d-flex gap-2">
|
|
<div class="color-preview" style="background-color: ${ws.primary_color}"></div>
|
|
<div class="color-preview" style="background-color: ${ws.accent_color}"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<small class="text-muted d-block mb-1">Email Domæner</small>
|
|
<div class="small">${ws.allowed_email_domains}</div>
|
|
</div>
|
|
|
|
<div class="row g-2 mb-3">
|
|
<div class="col-6">
|
|
<small class="text-muted d-block">Produkter</small>
|
|
<strong>${ws.product_count || 0}</strong>
|
|
</div>
|
|
<div class="col-6">
|
|
<small class="text-muted d-block">Avance</small>
|
|
<strong>${ws.default_margin_percent}%</strong>
|
|
</div>
|
|
</div>
|
|
|
|
${ws.last_published_at ? `
|
|
<div class="mb-3">
|
|
<small class="text-muted">Sidst publiceret: ${new Date(ws.last_published_at).toLocaleString('da-DK')}</small>
|
|
</div>
|
|
` : '<div class="mb-3"><small class="text-warning">⚠️ Ikke publiceret endnu</small></div>'}
|
|
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-sm btn-outline-primary flex-fill" onclick="openEditModal(${ws.id})">
|
|
<i class="bi bi-pencil me-1"></i>Rediger
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="openProductsModal(${ws.id})">
|
|
<i class="bi bi-box-seam"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-success" onclick="publishWebshop(${ws.id})"
|
|
${!ws.enabled ? 'disabled' : ''}>
|
|
<i class="bi bi-cloud-upload"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function initCustomerSearch() {
|
|
const searchInput = document.getElementById('customerSearch');
|
|
if (!searchInput) return;
|
|
|
|
searchInput.addEventListener('input', (event) => {
|
|
const term = event.target.value.trim();
|
|
clearTimeout(customerSearchTimeout);
|
|
customerSearchTimeout = setTimeout(() => loadCustomers(term), 300);
|
|
});
|
|
}
|
|
|
|
async function loadCustomers(searchTerm = '') {
|
|
try {
|
|
const params = new URLSearchParams({ limit: '1000' });
|
|
if (searchTerm) {
|
|
params.set('search', searchTerm);
|
|
}
|
|
|
|
const response = await fetch(`/api/v1/customers?${params.toString()}`);
|
|
const data = await response.json();
|
|
const customers = Array.isArray(data) ? data : (data.customers || []);
|
|
|
|
const select = document.getElementById('customerId');
|
|
select.innerHTML = '<option value="">Vælg kunde...</option>' +
|
|
customers.map(c => `<option value="${c.id}">${c.name}</option>`).join('');
|
|
} catch (error) {
|
|
console.error('Error loading customers:', error);
|
|
}
|
|
}
|
|
|
|
async function openCreateModal() {
|
|
document.getElementById('modalTitle').textContent = 'Opret Webshop';
|
|
document.getElementById('webshopForm').reset();
|
|
document.getElementById('configId').value = '';
|
|
document.getElementById('enabled').checked = true;
|
|
const searchInput = document.getElementById('customerSearch');
|
|
if (searchInput) searchInput.value = '';
|
|
await loadCustomers();
|
|
webshopModal.show();
|
|
}
|
|
|
|
async function openEditModal(configId) {
|
|
const ws = webshopsData.find(w => w.id === configId);
|
|
if (!ws) return;
|
|
|
|
document.getElementById('modalTitle').textContent = 'Rediger Webshop';
|
|
document.getElementById('configId').value = ws.id;
|
|
const searchInput = document.getElementById('customerSearch');
|
|
if (searchInput) searchInput.value = '';
|
|
await loadCustomers();
|
|
document.getElementById('customerId').value = ws.customer_id;
|
|
document.getElementById('webshopName').value = ws.name;
|
|
document.getElementById('emailDomains').value = ws.allowed_email_domains;
|
|
document.getElementById('headerText').value = ws.header_text || '';
|
|
document.getElementById('introText').value = ws.intro_text || '';
|
|
document.getElementById('primaryColor').value = ws.primary_color;
|
|
document.getElementById('primaryColorHex').value = ws.primary_color;
|
|
document.getElementById('accentColor').value = ws.accent_color;
|
|
document.getElementById('accentColorHex').value = ws.accent_color;
|
|
document.getElementById('defaultMargin').value = ws.default_margin_percent;
|
|
document.getElementById('minOrderAmount').value = ws.min_order_amount;
|
|
document.getElementById('shippingCost').value = ws.shipping_cost;
|
|
document.getElementById('enabled').checked = ws.enabled;
|
|
|
|
webshopModal.show();
|
|
}
|
|
|
|
async function saveWebshop() {
|
|
const configId = document.getElementById('configId').value;
|
|
const isEdit = !!configId;
|
|
|
|
const payload = {
|
|
customer_id: parseInt(document.getElementById('customerId').value),
|
|
name: document.getElementById('webshopName').value,
|
|
allowed_email_domains: document.getElementById('emailDomains').value,
|
|
header_text: document.getElementById('headerText').value || null,
|
|
intro_text: document.getElementById('introText').value || null,
|
|
primary_color: document.getElementById('primaryColorHex').value,
|
|
accent_color: document.getElementById('accentColorHex').value,
|
|
default_margin_percent: parseFloat(document.getElementById('defaultMargin').value),
|
|
min_order_amount: parseFloat(document.getElementById('minOrderAmount').value),
|
|
shipping_cost: parseFloat(document.getElementById('shippingCost').value),
|
|
enabled: document.getElementById('enabled').checked
|
|
};
|
|
|
|
try {
|
|
const url = isEdit ? `/api/v1/webshop/configs/${configId}` : '/api/v1/webshop/configs';
|
|
const method = isEdit ? 'PUT' : 'POST';
|
|
|
|
const response = await fetch(url, {
|
|
method: method,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast(data.message || 'Webshop gemt', 'success');
|
|
webshopModal.hide();
|
|
loadWebshops();
|
|
} else {
|
|
showToast(data.message || 'Fejl ved gemning', 'danger');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving webshop:', error);
|
|
showToast('Fejl ved gemning af webshop', 'danger');
|
|
}
|
|
}
|
|
|
|
async function openProductsModal(configId) {
|
|
currentWebshopConfig = configId;
|
|
document.getElementById('currentConfigId').value = configId;
|
|
|
|
const ws = webshopsData.find(w => w.id === configId);
|
|
document.getElementById('productsModalWebshopName').textContent = ws ? ws.name : '';
|
|
|
|
productsModal.show();
|
|
loadProducts(configId);
|
|
}
|
|
|
|
async function loadProducts(configId) {
|
|
try {
|
|
const response = await fetch(`/api/v1/webshop/configs/${configId}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
renderProducts(data.products, data.config.default_margin_percent);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading products:', error);
|
|
showToast('Fejl ved indlæsning af produkter', 'danger');
|
|
}
|
|
}
|
|
|
|
function renderProducts(products, defaultMargin) {
|
|
const tbody = document.getElementById('productsTableBody');
|
|
|
|
if (!products || products.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-4 text-muted">Ingen produkter endnu</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = products.map(p => {
|
|
const margin = p.custom_margin_percent || defaultMargin;
|
|
const salePrice = p.base_price * (1 + margin / 100);
|
|
|
|
return `
|
|
<tr>
|
|
<td><code>${p.product_number}</code></td>
|
|
<td>${p.name}</td>
|
|
<td>${p.ean || '-'}</td>
|
|
<td>${p.base_price.toFixed(2)} kr</td>
|
|
<td>${margin.toFixed(1)}%</td>
|
|
<td><strong>${salePrice.toFixed(2)} kr</strong></td>
|
|
<td>
|
|
<span class="badge ${p.visible ? 'bg-success' : 'bg-secondary'}">
|
|
${p.visible ? 'Ja' : 'Nej'}
|
|
</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-outline-danger" onclick="removeProduct(${p.id})">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function openAddProductModal() {
|
|
document.getElementById('addProductForm').reset();
|
|
addProductModal.show();
|
|
}
|
|
|
|
async function addProduct() {
|
|
const configId = document.getElementById('currentConfigId').value;
|
|
|
|
const payload = {
|
|
webshop_config_id: parseInt(configId),
|
|
product_number: document.getElementById('productNumber').value,
|
|
ean: document.getElementById('productEan').value || null,
|
|
name: document.getElementById('productName').value,
|
|
description: document.getElementById('productDescription').value || null,
|
|
base_price: parseFloat(document.getElementById('productBasePrice').value),
|
|
custom_margin_percent: document.getElementById('productCustomMargin').value ?
|
|
parseFloat(document.getElementById('productCustomMargin').value) : null,
|
|
category: document.getElementById('productCategory').value || null,
|
|
visible: true,
|
|
sort_order: 0
|
|
};
|
|
|
|
try {
|
|
const response = await fetch('/api/v1/webshop/products', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast('Produkt tilføjet', 'success');
|
|
addProductModal.hide();
|
|
loadProducts(configId);
|
|
} else {
|
|
showToast(data.detail || 'Fejl ved tilføjelse', 'danger');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error adding product:', error);
|
|
showToast('Fejl ved tilføjelse af produkt', 'danger');
|
|
}
|
|
}
|
|
|
|
async function removeProduct(productId) {
|
|
if (!confirm('Er du sikker på at du vil fjerne dette produkt?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/v1/webshop/products/${productId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast('Produkt fjernet', 'success');
|
|
const configId = document.getElementById('currentConfigId').value;
|
|
loadProducts(configId);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error removing product:', error);
|
|
showToast('Fejl ved fjernelse', 'danger');
|
|
}
|
|
}
|
|
|
|
async function publishWebshop(configId) {
|
|
if (!confirm('Vil du publicere denne webshop til Gateway?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/v1/webshop/configs/${configId}/publish`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast('Webshop publiceret til Gateway!', 'success');
|
|
loadWebshops();
|
|
} else {
|
|
showToast(data.detail || 'Fejl ved publicering', 'danger');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error publishing webshop:', error);
|
|
showToast('Fejl ved publicering', 'danger');
|
|
}
|
|
}
|
|
|
|
function showToast(message, type = 'info') {
|
|
// Reuse existing toast system if available
|
|
console.log(`[${type.toUpperCase()}] ${message}`);
|
|
alert(message); // Replace with proper toast when available
|
|
}
|
|
</script>
|
|
|
|
{% endblock %}
|