Rapport2

{% comment %} ProActiveAir Rapport-App ONLY v7 Passwort: PAAi2020 Einbau: kompletten Inhalt dieser Datei in die Shopify-Seite bzw. Liquid-Section kopieren. Voraussetzung: proactiveair-logo.png ist als Shopify Asset vorhanden. {% endcomment %}
ProActiveAir

Rapport · Kundenbericht

Rapport erfassen

Datum, Zeiten, Arbeiten, Ware und Bilder direkt vor Ort sichern.

0%
01

Zuordnung

Kunde, Projekt und Datum

02

Zeiten & Team

Arbeitszeit und ein bis zwei Personen

Mitarbeitende auswählen
03

Arbeiten

Durchgeführte Tätigkeiten und Bild

04

Fahrtkosten & Ware

Fahrt, verbaute Ware und Bild

05

Abschluss

Speichern, PDF oder Export

Noch kein Rapport gespeichert

Speichern legt den Rapport lokal auf diesem Gerät ab. Sobald ein Backend vorhanden ist, kann der offene Sync-Status dafür genutzt werden.

Lokale Ablage

Gespeicherte Rapporte

Export

Auswertung

Projekt abrechnen

Stammdaten

Listen bearbeiten

ProActiveAir

Geschützter Bereich

ProActiveAir Apps

Bitte geben Sie das Passwort ein.

`; } function numberValue(value) { return Number(String(value ?? "").replace(",", ".")) || 0; } function percentLabel(value) { if (!Number.isFinite(value)) return ""; return `${value.toLocaleString("de-DE", { maximumFractionDigits: 1 })} %`; } function numberLabel(value, digits = 0) { return Number(value || 0).toLocaleString("de-DE", { maximumFractionDigits: digits, minimumFractionDigits: digits }); } function moneyLabel(value) { return `${Number(value || 0).toLocaleString("de-DE", { style: "currency", currency: "EUR" })}`; } function openPdf(data) { const win = window.open("", "_blank"); const html = reportHtml(data); if (!win) { downloadFile(html, "text/html;charset=utf-8", `rapport-${slug(data.customerShort || data.customerFull)}-${data.date || "ohne-datum"}.html`); return toast("Bericht als HTML gespeichert"); } win.document.open(); win.document.write(html); win.document.close(); } function renderBillingFilters() { const items = records(); const customers = [...new Map(items.map((item) => [item.data.customerId, item.data])).values()] .filter((data) => data.customerId) .sort((a, b) => String(a.customerShort).localeCompare(String(b.customerShort))); const customerSelect = document.querySelector("#billing-customer"); const previousCustomer = customerSelect.value; customerSelect.innerHTML = `${customers.map((data) => ``).join("")}`; if ([...customerSelect.options].some((option) => option.value === previousCustomer)) customerSelect.value = previousCustomer; renderBillingProjects(); } function renderBillingProjects() { const items = records(); const customerId = document.querySelector("#billing-customer").value; const projects = [...new Set(items.filter((item) => !customerId || item.data.customerId === customerId).map((item) => item.data.projectName).filter(Boolean))] .sort((a, b) => String(a).localeCompare(String(b))); const projectSelect = document.querySelector("#billing-project"); const previousProject = projectSelect.value; projectSelect.innerHTML = `${projects.map((project) => ``).join("")}`; if ([...projectSelect.options].some((option) => option.value === previousProject)) projectSelect.value = previousProject; renderBillingList(); } function billingCandidates() { const customerId = document.querySelector("#billing-customer").value; const project = document.querySelector("#billing-project").value; const status = document.querySelector("#billing-status").value; return records() .filter((item) => !customerId || item.data.customerId === customerId) .filter((item) => !project || item.data.projectName === project) .filter((item) => { if (status === "all") return true; if (status === "billed") return billedMinutes(item.data) > 0; return openMinutes(item.data) > 0; }) .sort((a, b) => String(a.data.date).localeCompare(String(b.data.date))); } function selectedBillingRows() { return [...document.querySelectorAll(".billing-check:checked")].map((checkbox) => { const item = records().find((record) => record.id === checkbox.value); const minutesInput = document.querySelector(`[data-billing-minutes="${checkbox.value}"]`); const minutes = Math.max(0, Math.min(Number(minutesInput.value || 0), openMinutes(item.data))); return { item, minutes }; }).filter((row) => row.item && row.minutes > 0); } function billingRange(rows) { const dates = rows.map((row) => row.item.data.date).filter(Boolean).sort(); return { first: dates[0] || "", last: dates[dates.length - 1] || "" }; } function updateBillingSummary() { const rows = selectedBillingRows(); const range = billingRange(rows); const total = rows.reduce((sum, row) => sum + row.minutes, 0); document.querySelector("#billing-summary").innerHTML = ` ${rows.length} Positionen ausgewählt

Zeitraum ${range.first ? `${formatDate(range.first)} bis ${formatDate(range.last)}` : "noch nicht gewählt"} · ${durationLabel(total) || "0 min"} abrechnen

`; } function renderBillingList() { const items = billingCandidates(); document.querySelector("#billing-list").innerHTML = items.length ? items.map((item) => { const total = Number(item.data.durationMinutes || 0); const billed = billedMinutes(item.data); const open = openMinutes(item.data); const disabled = open <= 0 ? "disabled" : ""; return `
`; }).join("") : "

Keine passenden Zeiten gefunden.

"; updateBillingSummary(); } function billingReportHtml(rows) { const range = billingRange(rows); const total = rows.reduce((sum, row) => sum + row.minutes, 0); const first = rows[0]?.item.data || {}; const logoUrl = (window.PROACTIVEAIR_LOGO_URL || new URL("proactiveair-logo.png", location.href).href); const rowsHtml = rows.map((row) => ` ${escapeHtml(formatDate(row.item.data.date))} ${escapeHtml(row.item.data.projectName || "")} ${escapeHtml((row.item.data.staff || []).join(", "))} ${escapeHtml(row.item.data.workText || "")} ${escapeHtml(durationLabel(row.minutes))} `).join(""); return `Abrechnung ${escapeHtml(first.customerShort || "")}
ProActiveAir

Abrechnungsanlage

${new Date().toLocaleDateString("de-DE")}

Kunde${escapeHtml(first.customerFull || "Mehrere Kunden")}
Zeitraum${escapeHtml(range.first ? `${formatDate(range.first)} bis ${formatDate(range.last)}` : "")}
Positionen${rows.length}
Zeit gesamt${escapeHtml(durationLabel(total) || "0 min")}
Notiz${escapeHtml(document.querySelector("#billing-note").value || "")}
${rowsHtml}
DatumProjektMitarbeitendeArbeitenZeit
`; } function openBillingReport() { const rows = selectedBillingRows(); if (!rows.length) return toast("Bitte Zeiten für die Auswertung auswählen"); const win = window.open("", "_blank"); const html = billingReportHtml(rows); if (!win) return downloadFile(html, "text/html;charset=utf-8", `abrechnung-${new Date().toISOString().slice(0, 10)}.html`); win.document.open(); win.document.write(html); win.document.close(); } function markBillingSelection() { const rows = selectedBillingRows(); if (!rows.length) return toast("Bitte Zeiten zum Abrechnen auswählen"); const note = document.querySelector("#billing-note").value.trim(); const billedAt = new Date().toISOString(); const billingId = crypto.randomUUID(); const items = records().map((record) => { const row = rows.find((entry) => entry.item.id === record.id); if (!row) return record; return { ...record, syncStatus: "pending", updatedAt: billedAt, data: { ...record.data, billings: [...(record.data.billings || []), { id: billingId, billedAt, note, minutes: row.minutes }] } }; }); saveRecords(items); renderRecords(); renderBillingList(); toast("Auswahl als abgerechnet markiert"); } function formatDate(value) { if (!value) return ""; return new Date(`${value}T12:00:00`).toLocaleDateString("de-DE"); } function renderListEditor() { if (!listsDialog.open) return; const master = loadMaster(); const editor = document.querySelector("#list-editor"); if (activeList === "customers") { editor.innerHTML = `
${master.customers.map((customer) => `
${escapeHtml(customer.short)}

${escapeHtml(customer.full)}

`).join("")}`; } if (activeList === "projects") { editor.innerHTML = master.customers.map((customer) => `

${escapeHtml(customer.short)}

${(customer.projects || []).map((project) => `
${escapeHtml(project)}

${escapeHtml(customer.full)}

`).join("") || "

Keine Projekte hinterlegt.

"}
`).join(""); } if (activeList === "staff") { editor.innerHTML = `
${master.staff.map((name) => `
${escapeHtml(name)}
`).join("")}`; } } function addStaff(name) { const clean = name.trim(); if (!clean) return; const master = loadMaster(); if (!master.staff.includes(clean)) master.staff.push(clean); saveMaster(master); const checkbox = [...form.querySelectorAll('input[name="staff"]')].find((input) => input.value === clean); if (checkbox) checkbox.checked = true; updateProgress(); } function updateSyncBadge() { const pending = records().filter((item) => item.syncStatus === "pending").length; const badge = document.querySelector("#sync-badge"); if (!navigator.onLine) { badge.textContent = pending ? `${pending} offen` : "Offline bereit"; return; } badge.textContent = pending ? `${pending} sync-bereit` : "Synchron"; } function toast(message) { const el = document.querySelector("#toast"); el.textContent = message; el.classList.add("show"); setTimeout(() => el.classList.remove("show"), 1900); } function unlockApp() { document.querySelector("#access-gate").classList.add("unlocked"); } function isLocalTest() { return location.protocol === "file:" || LOCAL_TEST_HOSTS.includes(location.hostname); } function passwordMatches(value) { return String(value || "").trim() === ACCESS_PASSWORD; } document.querySelector("#new-rapport").addEventListener("click", newRapport); document.querySelector("#prev-step").addEventListener("click", () => showStep(currentStep - 1)); document.querySelector("#next-step").addEventListener("click", () => currentStep === steps.length - 1 ? save() : showStep(currentStep + 1)); document.querySelector("#save-rapport").addEventListener("click", save); document.querySelector("#pdf-current").addEventListener("click", () => openPdf(values())); document.querySelector("#open-records").addEventListener("click", () => { renderRecords(); recordsDialog.showModal(); }); document.querySelector("#close-records").addEventListener("click", () => recordsDialog.close()); document.querySelector("#export-json").addEventListener("click", exportJson); document.querySelector("#export-csv").addEventListener("click", exportCsv); document.querySelector("#open-billing").addEventListener("click", () => { renderBillingFilters(); billingDialog.showModal(); }); document.querySelector("#close-billing").addEventListener("click", () => billingDialog.close()); document.querySelector("#billing-customer").addEventListener("change", renderBillingProjects); document.querySelector("#billing-project").addEventListener("change", renderBillingList); document.querySelector("#billing-status").addEventListener("change", renderBillingList); document.querySelector("#billing-list").addEventListener("input", updateBillingSummary); document.querySelector("#billing-list").addEventListener("change", updateBillingSummary); document.querySelector("#billing-report").addEventListener("click", openBillingReport); document.querySelector("#billing-mark").addEventListener("click", markBillingSelection); document.querySelector("#edit-lists").addEventListener("click", () => { renderListEditor(); listsDialog.showModal(); }); document.querySelector("#close-lists").addEventListener("click", () => listsDialog.close()); tabs.addEventListener("click", (event) => { if (event.target.dataset.index !== undefined) showStep(Number(event.target.dataset.index)); }); form.addEventListener("input", () => { updateCounters(); updateDuration(); updateSummary(); updateProgress(); }); form.addEventListener("change", () => { updateDuration(); updateSummary(); updateProgress(); }); form.elements.customerId.addEventListener("change", () => { renderProjectOptions(); updateNewFields(); }); form.elements.projectId.addEventListener("change", updateNewFields); document.querySelector("#staff-options").addEventListener("change", (event) => { if (event.target.name === "staff" && selectedStaff().length > 2) { event.target.checked = false; toast("Bitte maximal zwei Mitarbeitende auswählen"); } updateProgress(); }); document.querySelector("#add-staff").addEventListener("click", () => { addStaff(document.querySelector("#new-staff-name").value); document.querySelector("#new-staff-name").value = ""; }); document.querySelectorAll('input[type="file"]').forEach((input) => { input.addEventListener("change", async () => { if (!input.files?.[0]) return; const type = input.id.startsWith("work") ? "work" : "material"; imageStore[type] = await readImage(input.files[0]); setImagePreview(type, imageStore[type]); updateProgress(); }); }); document.querySelectorAll(".clear-image").forEach((button) => { button.addEventListener("click", () => { imageStore[button.dataset.image] = ""; setImagePreview(button.dataset.image, ""); updateProgress(); }); }); document.querySelector("#records-list").addEventListener("click", (event) => { const loadId = event.target.dataset.load; const pdfId = event.target.dataset.pdf; const deleteId = event.target.dataset.delete; if (loadId) { const item = records().find((record) => record.id === loadId); if (!item) return; editingId = item.id; fill(item.data); recordsDialog.close(); showView("rapport"); showStep(0); } if (pdfId) { const item = records().find((record) => record.id === pdfId); if (item) openPdf(item.data); } if (deleteId && confirm("Diesen Rapport löschen?")) { saveRecords(records().filter((record) => record.id !== deleteId)); renderRecords(); } }); document.querySelector(".list-tabs").addEventListener("click", (event) => { if (!event.target.dataset.list) return; activeList = event.target.dataset.list; document.querySelectorAll(".list-tab").forEach((tab) => tab.classList.toggle("active", tab.dataset.list === activeList)); renderListEditor(); }); document.querySelector("#list-editor").addEventListener("click", (event) => { const master = loadMaster(); if (event.target.id === "customer-add") { const short = document.querySelector("#customer-short-add").value.trim(); const full = document.querySelector("#customer-full-add").value.trim(); if (!short || !full) return toast("Bitte Kurz- und Vollname eingeben"); master.customers.push({ id: `c-${slug(`${short}-${full}`)}`, short, full, projects: [] }); saveMaster(master); } if (event.target.dataset.removeCustomer && confirm("Kunden aus der Liste entfernen?")) { master.customers = master.customers.filter((customer) => customer.id !== event.target.dataset.removeCustomer); saveMaster(master); } if (event.target.dataset.addProject) { const customer = master.customers.find((item) => item.id === event.target.dataset.addProject); const input = document.querySelector(`[data-project-input="${event.target.dataset.addProject}"]`); const project = input.value.trim(); if (customer && project && !customer.projects.includes(project)) customer.projects.push(project); saveMaster(master); } if (event.target.dataset.removeProject && confirm("Projekt aus der Liste entfernen?")) { const customer = master.customers.find((item) => item.id === event.target.dataset.removeProject); if (customer) customer.projects = customer.projects.filter((project) => project !== event.target.dataset.project); saveMaster(master); } if (event.target.id === "staff-add-list") { addStaff(document.querySelector("#staff-add-name").value); } if (event.target.dataset.removeStaff && confirm("Namen aus der Liste entfernen?")) { master.staff = master.staff.filter((name) => name !== event.target.dataset.removeStaff); saveMaster(master); } }); document.querySelector("#access-form").addEventListener("submit", async (event) => { event.preventDefault(); const password = document.querySelector("#access-password").value; if (passwordMatches(password) || (isLocalTest() && password.trim())) { sessionStorage.setItem(STORAGE_KEYS.access, "ok"); unlockApp(); } else { document.querySelector("#access-error").textContent = "Das Passwort ist nicht korrekt."; } }); window.addEventListener("online", updateSyncBadge); window.addEventListener("offline", updateSyncBadge); if (sessionStorage.getItem(STORAGE_KEYS.access) === "ok" || isLocalTest()) unlockApp(); renderCustomerOptions(); renderStaffOptions(); setDateBounds(); try { const draft = JSON.parse(localStorage.getItem(STORAGE_KEYS.draft) || "null"); if (draft) { editingId = draft.id; fill(draft.data); } else { fill(); } } catch { localStorage.removeItem(STORAGE_KEYS.draft); fill(); } showView("rapport"); updateSyncBadge(); // Service Worker wird im Shopify-Liquid-Einbau nicht registriert.
Schließen (Esc)

Lernen Sie uns mit unseren Leihgeräten kennen!

Noch nicht bereit für eine Bindung? Mit unseren Leihgeräten haben Sie die Möglichkeit uns 14 Tage lang kostenlos kennen zu lernen. Kontaktieren Sie uns einfach per Mail oder telefonisch.

Hört sich gut an

Age verification

By clicking enter you are verifying that you are old enough to consume alcohol.

Suchen

Einkaufswagen

Ihr Einkaufswagen ist im Moment leer.
Einkauf beginnen