feat(i18n): add VAT labels and email sent messages in German and English
style(admin): increase max-width of admin page and adjust table styles fix(n8n): enhance workflow import and publishing process fix(workflows): update SQL queries for fetching and updating sales orders feat(migrations): normalize rental types and enhance email guard for individuell rentals feat(migrations): add RPC for updating deposit in sales orders fix(migrations): ensure individuell orders persist net/vat components and backfill existing records test: update last run status to failed
This commit is contained in:
+256
-134
@@ -70,6 +70,14 @@ const state = {
|
||||
forcedRotation: false,
|
||||
};
|
||||
|
||||
function notify(message, duration = 3000) {
|
||||
if (typeof window.showToast === "function") {
|
||||
window.showToast(message, duration);
|
||||
return;
|
||||
}
|
||||
alert(message);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AUTH FLOW
|
||||
// =========================================================================
|
||||
@@ -396,6 +404,7 @@ function renderLeads() {
|
||||
leadsEmpty.style.display = rows.length ? "none" : "block";
|
||||
leadsTableBody.innerHTML = "";
|
||||
for (const l of rows) {
|
||||
const rental = rentalTypeMeta(l.rental_type);
|
||||
const total = l.total_eur || 0;
|
||||
const totalStr = total > 0 ? "€ " + total.toLocaleString("de-DE") : "—";
|
||||
const tr = document.createElement("tr");
|
||||
@@ -404,7 +413,7 @@ function renderLeads() {
|
||||
<td><strong>${esc(l.name)}</strong><br /><span class="muted">${esc(l.email)}${l.phone ? " · " + esc(l.phone) : ""}</span></td>
|
||||
<td>${esc(l.vehicle_label || "—")}</td>
|
||||
<td>${esc(l.date_from || "—")} → ${esc(l.date_to || "—")}</td>
|
||||
<td style="white-space:nowrap;"><span class="pill pill-${esc(l.rental_type || 'weekend')}">${esc(l.rental_type || 'weekend')}</span></td>
|
||||
<td style="white-space:nowrap;"><span class="pill pill-${esc(rental.type)}">${esc(rental.label)}</span></td>
|
||||
<td style="font-weight:600;color:var(--accent-strong);">${totalStr}</td>
|
||||
<td><span class="pill pill-${esc(l.status)}">${esc(l.status)}</span></td>
|
||||
<td style="white-space:nowrap;">
|
||||
@@ -553,6 +562,7 @@ async function renderLeadTab(tab, l) {
|
||||
});
|
||||
leadDialogBody.appendChild(saveNoteBtn);
|
||||
} else if (tab === "pricing") {
|
||||
const rental = rentalTypeMeta(l.rental_type);
|
||||
const daily = l.daily_subtotal || 0;
|
||||
const weekend = l.weekend_subtotal || 0;
|
||||
const sub = l.subtotal_eur || 0;
|
||||
@@ -569,8 +579,8 @@ async function renderLeadTab(tab, l) {
|
||||
<div class="price-row total"><span>${lang === "de" ? t("adminTotalLabel") : t("adminTotalLabelEn")}</span><span>€ ${total.toLocaleString("de-DE")}</span></div>
|
||||
<div class="price-row muted" style="margin-top:0.8rem;"><span>${lang === "de" ? t("adminDepositLabel") : t("adminDepositLabelEn")}</span><span>€ ${deposit.toLocaleString("de-DE")}</span></div>
|
||||
<div class="price-row muted"><span>${lang === "de" ? t("adminIncludedKmLabel") : t("adminIncludedKmLabelEn")}</span><span>${(l.total_days || 0) * (state.vehicleMap.get(l.vehicle_id)?.included_km_per_day || 150)} km</span></div>
|
||||
<div class="price-row muted"><span>${lang === "de" ? t("adminTotalDaysLabel") : t("adminTotalDaysLabelEn")}</span><span>${l.total_days || 0}</span></div>
|
||||
<div class="price-row muted"><span>${lang === "de" ? t("adminRentalType") : t("adminRentalTypeEn")}</span><span><span class="pill pill-${esc(l.rental_type || 'weekend')}">${esc(l.rental_type || 'weekend')}</span></span></div>
|
||||
<div class="price-row muted"><span>${lang === "de" ? t("adminTotalDaysLabel") : t("adminTotalDaysLabelEn")}</span><span>${l.total_days || 0}</span></div>
|
||||
<div class="price-row muted"><span>${lang === "de" ? t("adminRentalType") : t("adminRentalTypeEn")}</span><span><span class="pill pill-${esc(rental.type)}">${esc(rental.label)}</span></span></div>
|
||||
</div>`;
|
||||
} else if (tab === "documents") {
|
||||
const docs = await loadLeadAttachments(l.id);
|
||||
@@ -687,6 +697,7 @@ function renderOrders() {
|
||||
ordersEmpty.style.display = state.salesOrders.length ? "none" : "block";
|
||||
ordersTableBody.innerHTML = "";
|
||||
for (const o of state.salesOrders) {
|
||||
const rental = rentalTypeMeta(o.rental_type);
|
||||
const total = o.total_eur || 0;
|
||||
const totalStr = total > 0 ? "€ " + total.toLocaleString("de-DE") : "—";
|
||||
const cust = state.customers.find(c => c.id === o.customer_id);
|
||||
@@ -696,7 +707,7 @@ function renderOrders() {
|
||||
<td>${cust ? `<strong>${esc(cust.name)}</strong><br><span class="muted">${esc(cust.email)}</span>` : `<span class="muted">${esc(o.customer_id?.slice(0, 8) || "—")}</span>`}</td>
|
||||
<td>${esc(o.vehicle_label || "—")}</td>
|
||||
<td>${esc(o.date_from || "—")} → ${esc(o.date_to || "—")}</td>
|
||||
<td style="white-space:nowrap;"><span class="pill pill-${esc(o.rental_type || 'weekend')}">${esc(o.rental_type || 'weekend')}</span></td>
|
||||
<td style="white-space:nowrap;"><span class="pill pill-${esc(rental.type)}">${esc(rental.label)}</span></td>
|
||||
<td style="font-weight:600;color:var(--accent-strong);">${totalStr}</td>
|
||||
<td><span class="pill pill-${o.kaution_paid ? "active" : "new"}">${o.kaution_paid ? "✓" : "—"}</span></td>
|
||||
<td><span class="pill pill-${o.rental_paid ? "active" : "new"}">${o.rental_paid ? "✓" : "—"}</span></td>
|
||||
@@ -712,13 +723,88 @@ function renderOrders() {
|
||||
// ----- ORDER DETAIL DIALOG -----
|
||||
const orderDialog = document.querySelector("#orderDialog");
|
||||
const orderDialogTitle = document.querySelector("#orderDialogTitle");
|
||||
const orderDialogTabs = document.querySelector("#orderDialogTabs");
|
||||
const orderDialogBody = document.querySelector("#orderDialogBody");
|
||||
const orderDialogFooter = document.querySelector("#orderDialogFooter");
|
||||
const orderDialogClose = document.querySelector("#orderDialogClose");
|
||||
|
||||
async function sendOrderEmailDirect(orderId) {
|
||||
const sendBtn = orderDialogBody.querySelector("[data-manual-email-send]");
|
||||
if (sendBtn) sendBtn.disabled = true;
|
||||
|
||||
const n8nUrl = (window.MCCARS_CONFIG?.N8N_WEBHOOK_URL || "http://localhost:55590") + "/webhook/manual-email-send";
|
||||
|
||||
try {
|
||||
// Use urlencoded payload to avoid browser preflight/CORS issues with JSON headers.
|
||||
const payload = new URLSearchParams({ sales_order_id: orderId });
|
||||
const res = await fetch(n8nUrl, {
|
||||
method: "POST",
|
||||
body: payload,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
notify(t("emailSentToast"), 5000);
|
||||
} catch (err) {
|
||||
console.error("Webhook error:", err);
|
||||
alert(`Email senden fehlgeschlagen: ${err?.message || err}`);
|
||||
}
|
||||
}
|
||||
|
||||
orderDialogBody.addEventListener("click", async (e) => {
|
||||
const sendBtn = e.target.closest("[data-manual-email-send]");
|
||||
if (!sendBtn) return;
|
||||
|
||||
e.preventDefault();
|
||||
const orderId = sendBtn.dataset.orderId;
|
||||
if (!orderId) return;
|
||||
|
||||
await sendOrderEmailDirect(orderId);
|
||||
await loadSalesOrders();
|
||||
const fresh = state.salesOrders.find(x => x.id === orderId);
|
||||
if (fresh) await renderOrderTab("general", fresh, orderId);
|
||||
});
|
||||
|
||||
const orderTabOrder = ["general", "pricing"];
|
||||
const orderTabLabels = {
|
||||
general: () => getLang() === "de" ? t("adminTabGeneral") : t("adminTabGeneralEn"),
|
||||
pricing: () => getLang() === "de" ? t("adminTabPricing") : t("adminTabPricingEn"),
|
||||
};
|
||||
|
||||
async function openOrder(id) {
|
||||
const o = state.salesOrders.find(x => x.id === id);
|
||||
if (!o) return;
|
||||
const cust = state.customers.find(c => c.id === o.customer_id);
|
||||
|
||||
orderDialogTitle.textContent = `${o.order_number || o.id.slice(0, 8)} · ${cust?.name || "—"}`;
|
||||
|
||||
// Build tabs
|
||||
orderDialogTabs.innerHTML = orderTabOrder.map((tab, i) =>
|
||||
`<button class="order-tab${i === 0 ? " active" : ""}" data-order-tab="${tab}">${orderTabLabels[tab]()}</button>`
|
||||
).join("");
|
||||
|
||||
// Render first tab
|
||||
await renderOrderTab("general", o, id);
|
||||
|
||||
orderDialog.showModal();
|
||||
|
||||
// Tab switching
|
||||
orderDialogTabs.querySelectorAll(".order-tab").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
orderDialogTabs.querySelectorAll(".order-tab").forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
renderOrderTab(btn.dataset.orderTab, o, id);
|
||||
});
|
||||
});
|
||||
|
||||
orderDialogClose.addEventListener("click", () => orderDialog.close(), { once: true });
|
||||
}
|
||||
|
||||
async function renderOrderTab(tab, o, id) {
|
||||
const rental = rentalTypeMeta(o.rental_type);
|
||||
const isIndividuell = rental.type === "individuell";
|
||||
const lang = getLang();
|
||||
const cust = state.customers.find(c => c.id === o.customer_id);
|
||||
const total = o.total_eur || 0;
|
||||
@@ -728,149 +814,168 @@ async function openOrder(id) {
|
||||
const emailSentPillClass = emailSent === 1 ? 'active' : emailSent === 2 ? 'disqualified' : 'new';
|
||||
const isEmailLocked = emailSent === 1;
|
||||
|
||||
orderDialogTitle.textContent = `${o.order_number || o.id.slice(0, 8)} · ${cust?.name || "—"}`;
|
||||
if (tab === "general") {
|
||||
// Load attachments for this order
|
||||
const { data: attachments } = await supabase
|
||||
.from("sales_order_attachments")
|
||||
.select("*")
|
||||
.eq("sales_order_id", id)
|
||||
.order("created_at", { ascending: false });
|
||||
const docs = attachments || [];
|
||||
|
||||
// Load attachments for this order
|
||||
const { data: attachments } = await supabase
|
||||
.from("sales_order_attachments")
|
||||
.select("*")
|
||||
.eq("sales_order_id", id)
|
||||
.order("created_at", { ascending: false });
|
||||
const docs = attachments || [];
|
||||
|
||||
orderDialogBody.innerHTML = `
|
||||
<dl class="kv">
|
||||
<dt>${lang === "de" ? "Kunde" : "Customer"}</dt><dd>${cust ? `<a href="#" class="link-lead" data-goto-cust="${cust.id}">${esc(cust.name)} (${esc(cust.email)})</a>` : esc(o.customer_id?.slice(0, 8) || "—")}</dd>
|
||||
<dt>${lang === "de" ? "Fahrzeug" : "Vehicle"}</dt><dd>${esc(o.vehicle_label || "—")}</dd>
|
||||
<dt>${lang === "de" ? "Zeitraum" : "Period"}</dt><dd>${esc(o.date_from || "—")} → ${esc(o.date_to || "—")}</dd>
|
||||
<dt>${t("adminTotalLabel")}</dt><dd id="orderTotalDisplay" style="font-weight:600;">
|
||||
${o.rental_type === 'individuell' && !isEmailLocked
|
||||
? `<input type="number" id="orderTotalInput" step="1" min="0" value="${o.total_eur || 0}" style="font-weight:600;width:120px;" ${isEmailLocked ? 'disabled' : ''} />
|
||||
<button class="btn small" id="orderTotalSave" style="margin-left:0.4rem;" ${isEmailLocked ? 'disabled' : ''}>${t("adminSave")}</button>`
|
||||
: '€ ' + total.toLocaleString("de-DE")
|
||||
}
|
||||
</dd>
|
||||
<dt>${t("adminDepositLabel")}</dt><dd>${isEmailLocked
|
||||
? '€ ' + deposit.toLocaleString("de-DE")
|
||||
: `<input type="number" id="orderDepositInput" step="1" min="0" value="${deposit}" style="width:120px;" ${isEmailLocked ? 'disabled' : ''} /><button class="btn small" id="orderDepositSave" style="margin-left:0.4rem;" ${isEmailLocked ? 'disabled' : ''}>${t("adminSave")}</button>`
|
||||
}</dd>
|
||||
<dt>${t("adminEmailSent")}</dt><dd><span class="pill pill-${emailSentPillClass}">${emailSentText}</span></dd>
|
||||
${o.rental_type === 'individuell' && !isEmailLocked ? `<dt></dt><dd><button class="btn small" id="manualEmailSend" style="background-color:var(--accent-strong);color:#fff;">${t("sendEmailButton")}</button></dd>` : ''}
|
||||
</dl>
|
||||
<div style="display:flex;gap:0.4rem;flex-wrap:wrap;margin:1rem 0;">
|
||||
<button class="btn small ${o.kaution_paid ? "ghost" : ""}" data-so-toggle="kaution" data-so-id="${o.id}">${o.kaution_paid ? t("adminKautionPaid") : t("adminKautionPending")}</button>
|
||||
<button class="btn small ${o.rental_paid ? "ghost" : ""}" data-so-toggle="rental" data-so-id="${o.id}">${o.rental_paid ? t("adminRentalPaid") : t("adminRentalPending")}</button>
|
||||
<button class="btn small ${o.rental_complete ? "ghost" : ""}" data-so-toggle="complete" data-so-id="${o.id}">${o.rental_complete ? t("adminCompleteDone") : t("adminCompletePending")}</button>
|
||||
</div>
|
||||
<h4 style="margin:1.2rem 0 0.6rem;font-size:0.9rem;color:var(--muted);">${t("adminTabDocuments")}</h4>
|
||||
${docs.length ? renderDocList(docs) : `<p class="muted" style="text-align:center;padding:1rem 0;">${t("adminNoDocuments")}</p>`}
|
||||
<div style="margin-top:0.8rem;">
|
||||
<label class="muted" style="display:block;margin-bottom:0.3rem;">${t("adminPrivateNotes")}</label>
|
||||
<textarea id="orderNote" rows="4" style="width:100%;resize:vertical;">${esc(o.private_notes || "")}</textarea>
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:0.4rem;">
|
||||
<button class="btn small" id="orderNoteSave">${t("adminSaveNotes")}</button>
|
||||
orderDialogBody.innerHTML = `
|
||||
<dl class="kv">
|
||||
<dt>${lang === "de" ? "Kunde" : "Customer"}</dt><dd>${cust ? `<a href="#" class="link-lead" data-goto-cust="${cust.id}">${esc(cust.name)} (${esc(cust.email)})</a>` : esc(o.customer_id?.slice(0, 8) || "—")}</dd>
|
||||
<dt>${lang === "de" ? "Fahrzeug" : "Vehicle"}</dt><dd>${esc(o.vehicle_label || "—")}</dd>
|
||||
<dt>${lang === "de" ? "Zeitraum" : "Period"}</dt><dd>${esc(o.date_from || "—")} → ${esc(o.date_to || "—")}</dd>
|
||||
<dt>${lang === "de" ? t("adminRentalType") : t("adminRentalTypeEn")}</dt><dd><span class="pill pill-${esc(rental.type)}">${esc(rental.label)}</span></dd>
|
||||
<dt>${t("adminEmailSent")}</dt><dd><span class="pill pill-${emailSentPillClass}">${emailSentText}</span></dd>
|
||||
</dl>
|
||||
<div style="display:flex;gap:0.4rem;flex-wrap:wrap;margin:1rem 0;">
|
||||
<button class="btn small ${o.kaution_paid ? "ghost" : ""}" data-so-toggle="kaution" data-so-id="${o.id}">${o.kaution_paid ? t("adminKautionPaid") : t("adminKautionPending")}</button>
|
||||
<button class="btn small ${o.rental_paid ? "ghost" : ""}" data-so-toggle="rental" data-so-id="${o.id}">${o.rental_paid ? t("adminRentalPaid") : t("adminRentalPending")}</button>
|
||||
<button class="btn small ${o.rental_complete ? "ghost" : ""}" data-so-toggle="complete" data-so-id="${o.id}">${o.rental_complete ? t("adminCompleteDone") : t("adminCompletePending")}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
<h4 style="margin:1.2rem 0 0.6rem;font-size:0.9rem;color:var(--muted);">${t("adminTabDocuments")}</h4>
|
||||
${docs.length ? renderDocList(docs) : `<p class="muted" style="text-align:center;padding:1rem 0;">${t("adminNoDocuments")}</p>`}
|
||||
<div style="margin-top:0.8rem;">
|
||||
<label class="muted" style="display:block;margin-bottom:0.3rem;">${t("adminPrivateNotes")}</label>
|
||||
<textarea id="orderNote" rows="4" style="width:100%;resize:vertical;">${esc(o.private_notes || "")}</textarea>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:0.4rem;">
|
||||
<div>${emailSent !== 1 ? `<button class="btn small" type="button" data-manual-email-send data-order-id="${o.id}" style="background-color:var(--accent-strong);color:#fff;">${t("sendEmailButton")}</button>` : ''}</div>
|
||||
<button class="btn small" id="orderNoteSave">${t("adminSaveNotes")}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Customer lookup link
|
||||
orderDialogBody.querySelectorAll("[data-goto-cust]").forEach(a => {
|
||||
a.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
orderDialog.close();
|
||||
openCustomer(a.dataset.gotoCust);
|
||||
// Customer lookup link
|
||||
orderDialogBody.querySelectorAll("[data-goto-cust]").forEach(a => {
|
||||
a.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
orderDialog.close();
|
||||
openCustomer(a.dataset.gotoCust);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Document open links
|
||||
orderDialogBody.querySelectorAll("[data-open-file]").forEach(btn => {
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
await openAttachmentInNewTab(btn.dataset.openFile, btn.dataset.openBucket || "customer-documents");
|
||||
// Document open links
|
||||
orderDialogBody.querySelectorAll("[data-open-file]").forEach(btn => {
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
await openAttachmentInNewTab(btn.dataset.openFile, btn.dataset.openBucket || "customer-documents");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle buttons
|
||||
orderDialogBody.querySelectorAll("[data-so-toggle]").forEach(btn => {
|
||||
btn.addEventListener("click", async () => {
|
||||
await toggleSalesOrderState(btn.dataset.soId, btn.dataset.soToggle);
|
||||
await openOrder(id); // re-render
|
||||
// Toggle buttons
|
||||
orderDialogBody.querySelectorAll("[data-so-toggle]").forEach(btn => {
|
||||
btn.addEventListener("click", async () => {
|
||||
await toggleSalesOrderState(btn.dataset.soId, btn.dataset.soToggle);
|
||||
await loadSalesOrders();
|
||||
const fresh = state.salesOrders.find(x => x.id === id);
|
||||
if (fresh) await renderOrderTab("general", fresh, id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Dirty form tracking
|
||||
let noteIsDirty = false;
|
||||
const orderNoteEl = document.querySelector("#orderNote");
|
||||
const originalNoteValue = o.private_notes || "";
|
||||
const saveBtn = document.querySelector("#orderNoteSave");
|
||||
if (orderNoteEl && saveBtn) {
|
||||
saveBtn.classList.add("ghost");
|
||||
orderNoteEl.addEventListener("input", () => {
|
||||
noteIsDirty = orderNoteEl.value !== originalNoteValue;
|
||||
if (noteIsDirty) {
|
||||
saveBtn.classList.remove("ghost");
|
||||
saveBtn.style.backgroundColor = "var(--accent-strong)";
|
||||
saveBtn.style.color = "#fff";
|
||||
saveBtn.textContent = t("adminSaveNotes") + " (unsaved)";
|
||||
} else {
|
||||
saveBtn.classList.add("ghost");
|
||||
saveBtn.style.backgroundColor = "";
|
||||
saveBtn.style.color = "";
|
||||
saveBtn.textContent = t("adminSaveNotes");
|
||||
// Dirty form tracking
|
||||
let noteIsDirty = false;
|
||||
const orderNoteEl = document.querySelector("#orderNote");
|
||||
const originalNoteValue = o.private_notes || "";
|
||||
const saveBtn = document.querySelector("#orderNoteSave");
|
||||
if (orderNoteEl && saveBtn) {
|
||||
saveBtn.classList.add("ghost");
|
||||
orderNoteEl.addEventListener("input", () => {
|
||||
noteIsDirty = orderNoteEl.value !== originalNoteValue;
|
||||
if (noteIsDirty) {
|
||||
saveBtn.classList.remove("ghost");
|
||||
saveBtn.style.backgroundColor = "var(--accent-strong)";
|
||||
saveBtn.style.color = "#fff";
|
||||
saveBtn.textContent = t("adminSaveNotes") + " (unsaved)";
|
||||
} else {
|
||||
saveBtn.classList.add("ghost");
|
||||
saveBtn.style.backgroundColor = "";
|
||||
saveBtn.style.color = "";
|
||||
saveBtn.textContent = t("adminSaveNotes");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Save notes
|
||||
document.querySelector("#orderNoteSave")?.addEventListener("click", async () => {
|
||||
const ok = await saveSalesOrderPrivateNotes(o.id, document.querySelector("#orderNote").value);
|
||||
if (ok) {
|
||||
noteIsDirty = false;
|
||||
document.querySelector("#orderNoteSave").textContent = "✓";
|
||||
setTimeout(() => { document.querySelector("#orderNoteSave").textContent = t("adminSaveNotes"); }, 1500);
|
||||
}
|
||||
});
|
||||
|
||||
} else if (tab === "pricing") {
|
||||
let daily = o.daily_subtotal || 0;
|
||||
let weekend = o.weekend_subtotal || 0;
|
||||
let sub = o.subtotal_eur || 0;
|
||||
let vat = o.vat_eur || 0;
|
||||
const days = o.total_days || 0;
|
||||
const inclVatLabel = lang === "de" ? t("adminInclVat") : t("adminInclVatEn");
|
||||
|
||||
// For individuell: derive pricing from total (which includes VAT)
|
||||
if (isIndividuell && total > 0) {
|
||||
sub = Math.round(total / 1.2 * 100) / 100;
|
||||
vat = Math.round((total - sub) * 100) / 100;
|
||||
daily = sub; // all days counted as weekdays
|
||||
weekend = 0;
|
||||
}
|
||||
|
||||
const weekdayCount = isIndividuell ? days : (o.weekday_count || 0);
|
||||
const weekendCount = isIndividuell ? 0 : (o.weekend_day_count || 0);
|
||||
const perDay = daily && weekdayCount ? Math.round(daily / weekdayCount) : 0;
|
||||
const perWeekend = weekend && weekendCount ? Math.round(weekend / weekendCount) : 0;
|
||||
|
||||
orderDialogBody.innerHTML = `
|
||||
<div class="pricing-card">
|
||||
<div class="price-row"><span>${lang === "de" ? t("adminWeekdays") : t("adminWeekdaysEn")} (${weekdayCount} × € ${perDay || "—"})</span><span>€ ${daily.toLocaleString("de-DE")}</span></div>
|
||||
<div class="price-row"><span>${lang === "de" ? t("adminWeekendRateLabel") : t("adminWeekendRateLabelEn")} (${weekendCount} × € ${perWeekend || "—"})</span><span>€ ${weekend.toLocaleString("de-DE")}</span></div>
|
||||
<div class="price-row divider"><span>${lang === "de" ? t("adminSubtotalLabel") : t("adminSubtotalLabelEn")}</span><span>€ ${sub.toLocaleString("de-DE")}</span></div>
|
||||
<div class="price-row muted"><span>${lang === "de" ? t("adminVatLabel") : t("adminVatLabelEn")}</span><span>€ ${vat.toLocaleString("de-DE")}</span></div>
|
||||
<div class="price-row total"><span>${lang === "de" ? t("adminTotalLabel") : t("adminTotalLabelEn")}</span>
|
||||
<span>${isIndividuell && !isEmailLocked
|
||||
? `<input type="number" id="orderTotalInput" step="1" min="0" value="${total}" style="font-weight:600;width:120px;" />`
|
||||
: '€ ' + total.toLocaleString("de-DE")
|
||||
}</span>
|
||||
</div>
|
||||
${!isEmailLocked ? `<div class="price-row muted" id="inclVatHint"><span></span><span style="font-size:0.78rem;">${inclVatLabel}</span></div>` : ''}
|
||||
<div class="price-row muted" style="margin-top:0.8rem;"><span>${lang === "de" ? t("adminDepositLabel") : t("adminDepositLabelEn")}</span>
|
||||
<span>${isEmailLocked || !isIndividuell
|
||||
? '€ ' + deposit.toLocaleString("de-DE")
|
||||
: `<input type="number" id="orderDepositInput" step="1" min="0" value="${deposit}" style="width:120px;" />`
|
||||
}</span>
|
||||
</div>
|
||||
<div class="price-row muted"><span>${lang === "de" ? t("adminIncludedKmLabel") : t("adminIncludedKmLabelEn")}</span><span>${days * (state.vehicleMap.get(o.vehicle_id)?.included_km_per_day || 150)} km</span></div>
|
||||
<div class="price-row muted"><span>${lang === "de" ? t("adminTotalDaysLabel") : t("adminTotalDaysLabelEn")}</span><span>${days}</span></div>
|
||||
<div class="price-row muted"><span>${lang === "de" ? t("adminRentalType") : t("adminRentalTypeEn")}</span><span><span class="pill pill-${esc(rental.type)}">${esc(rental.label)}</span></span></div>
|
||||
</div>
|
||||
${isIndividuell && !isEmailLocked ? `<div style="display:flex;justify-content:flex-end;margin-top:0.8rem;"><button class="btn small" id="orderPricingSave">${t("adminSave")}</button></div>` : ''}`;
|
||||
|
||||
// Single save for both total + deposit
|
||||
document.querySelector("#orderPricingSave")?.addEventListener("click", async () => {
|
||||
const btn = document.querySelector("#orderPricingSave");
|
||||
btn.disabled = true;
|
||||
const totalInput = document.querySelector("#orderTotalInput");
|
||||
const depositInput = document.querySelector("#orderDepositInput");
|
||||
const errors = [];
|
||||
if (totalInput) {
|
||||
const { error } = await supabase.rpc("sales_order_set_total", { p_so_id: o.id, p_total_eur: +totalInput.value });
|
||||
if (error) errors.push(error.message);
|
||||
}
|
||||
if (depositInput) {
|
||||
const { error } = await supabase.rpc("sales_order_set_deposit", { p_so_id: o.id, p_deposit_eur: +depositInput.value });
|
||||
if (error) errors.push(error.message);
|
||||
}
|
||||
if (errors.length) { alert(errors.join("\n")); btn.disabled = false; return; }
|
||||
await loadSalesOrders();
|
||||
const fresh = state.salesOrders.find(x => x.id === id);
|
||||
if (fresh) await renderOrderTab("pricing", fresh, id);
|
||||
});
|
||||
}
|
||||
|
||||
// Save notes
|
||||
document.querySelector("#orderNoteSave")?.addEventListener("click", async () => {
|
||||
const ok = await saveSalesOrderPrivateNotes(o.id, document.querySelector("#orderNote").value);
|
||||
if (ok) {
|
||||
noteIsDirty = false;
|
||||
document.querySelector("#orderNoteSave").textContent = "✓";
|
||||
setTimeout(() => { document.querySelector("#orderNoteSave").textContent = t("adminSaveNotes"); }, 1500);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector("#orderTotalSave")?.addEventListener("click", async () => {
|
||||
const input = document.querySelector("#orderTotalInput");
|
||||
if (!input) return;
|
||||
const { error } = await supabase.rpc("sales_order_set_total", {
|
||||
p_so_id: o.id,
|
||||
p_total_eur: +input.value,
|
||||
});
|
||||
if (error) {
|
||||
alert(error.message);
|
||||
return;
|
||||
}
|
||||
await loadSalesOrders();
|
||||
await openOrder(id); // re-render
|
||||
});
|
||||
|
||||
document.querySelector("#manualEmailSend")?.addEventListener("click", async () => {
|
||||
const btn = document.querySelector("#manualEmailSend");
|
||||
btn.disabled = true;
|
||||
|
||||
showToast(t("emailSentToast"), 3000);
|
||||
|
||||
const n8nUrl = window.MCCARS_CONFIG?.N8N_WEBHOOK_URL || "http://localhost:55590/webhook/manual-email-send";
|
||||
try {
|
||||
await fetch(n8nUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sales_order_id: o.id })
|
||||
});
|
||||
} catch (webhookError) {
|
||||
console.error("Webhook error:", webhookError);
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
await loadSalesOrders();
|
||||
await openOrder(id);
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
orderDialogFooter.innerHTML = "";
|
||||
orderDialog.showModal();
|
||||
orderDialogClose.addEventListener("click", () => orderDialog.close(), { once: true });
|
||||
}
|
||||
|
||||
async function toggleSalesOrderState(orderId, action) {
|
||||
@@ -1013,6 +1118,7 @@ async function renderCustomerTab(tab, c) {
|
||||
let html = "";
|
||||
if (orders.length) {
|
||||
for (const o of orders) {
|
||||
const rental = rentalTypeMeta(o.rental_type);
|
||||
const total = o.total_eur || 0;
|
||||
html += `
|
||||
<div class="pricing-card" style="margin-bottom:0.9rem;">
|
||||
@@ -1022,7 +1128,7 @@ async function renderCustomerTab(tab, c) {
|
||||
</div>
|
||||
<div class="price-row"><span>${lang === "de" ? "Fahrzeug" : "Vehicle"}</span><span>${esc(o.vehicle_label || "—")}</span></div>
|
||||
<div class="price-row"><span>${lang === "de" ? "Zeitraum" : "Period"}</span><span>${esc(o.date_from || "—")} → ${esc(o.date_to || "—")}</span></div>
|
||||
<div class="price-row"><span>${lang === "de" ? "Miettyp" : "Rental type"}</span><span><span class="pill pill-${esc(o.rental_type || 'weekend')}">${esc(o.rental_type || 'weekend')}</span></span></div>
|
||||
<div class="price-row"><span>${lang === "de" ? "Miettyp" : "Rental type"}</span><span><span class="pill pill-${esc(rental.type)}">${esc(rental.label)}</span></span></div>
|
||||
<div style="display:flex;gap:0.4rem;flex-wrap:wrap;margin-top:0.7rem;">
|
||||
<button class="btn small ${o.kaution_paid ? "ghost" : ""}" data-so-toggle="kaution" data-so-id="${o.id}">${o.kaution_paid ? t("adminKautionPaid") : t("adminKautionPending")}</button>
|
||||
<button class="btn small ${o.rental_paid ? "ghost" : ""}" data-so-toggle="rental" data-so-id="${o.id}">${o.rental_paid ? t("adminRentalPaid") : t("adminRentalPending")}</button>
|
||||
@@ -1180,6 +1286,22 @@ function attachRealtime() {
|
||||
// =========================================================================
|
||||
function esc(s) { return String(s ?? "").replace(/[&<>"']/g, c => ({ "&":"&","<":"<",">":">",'"':""","'":"'" })[c]); }
|
||||
function attr(s) { return esc(s); }
|
||||
function normalizeRentalType(rawType) {
|
||||
const value = String(rawType ?? "").trim().toLowerCase();
|
||||
if (!value) return "weekend";
|
||||
if (value === "individual" || value === "custom") return "individuell";
|
||||
if (value === "day" || value === "daily" || value === "1 tag" || value === "1_tag" || value === "single_day") return "single_day";
|
||||
if (value === "weekend") return "weekend";
|
||||
return value;
|
||||
}
|
||||
function rentalTypeMeta(rawType) {
|
||||
const type = normalizeRentalType(rawType);
|
||||
const lang = getLang();
|
||||
if (type === "single_day") return { type, label: lang === "de" ? "1 Tag" : "1 Day" };
|
||||
if (type === "individuell") return { type, label: lang === "de" ? "individuell" : "individual" };
|
||||
if (type === "weekend") return { type, label: "weekend" };
|
||||
return { type: "weekend", label: type };
|
||||
}
|
||||
function fmtDate(iso) {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
|
||||
Reference in New Issue
Block a user