Files
DigitalSignage/views/adportal_dashboard.xml
2026-07-23 08:54:44 +02:00

1080 lines
66 KiB
XML
Executable File

<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="adportal_dashboard" name="AD Portal Dashboard">
<t t-call="portal.portal_layout">
<t t-call="website.eingabe-header"/>
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<t t-call="DigitalSignage.invoiceinput_portal_searchbar">
<t t-set="title">AD Portal Dashboard</t>
</t>
<style>
.adportal-dashboard-layout {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.adportal-dashboard-main {
flex: 0 0 60%;
max-width: 60%;
}
.adportal-dashboard-side {
flex: 0 0 calc(40% - 1rem);
max-width: calc(40% - 1rem);
}
.adportal-stat-block {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 0.85rem 1rem;
}
@media (max-width: 991.98px) {
.adportal-dashboard-main,
.adportal-dashboard-side {
flex: 0 0 100%;
max-width: 100%;
}
}
</style>
<div class="adportal-dashboard-layout mb-4">
<div class="adportal-dashboard-main">
<div class="card mb-3" style="background-color: #ffffff !important; border: 1px solid #d8dee6; border-radius: 10px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);">
<div class="card-body" style="background-color: #ffffff !important;">
<h4 class="card-title mb-2"><i class="fa fa-list-alt me-1"/> Vertragsuebersicht</h4>
<p class="card-text text-muted mb-3">
Zeigt deine Vertraege mit Status und Schnellaktionen.
</p>
<t t-if="show_contract_overview">
<a href="/adportal/contracts" class="btn btn-primary">Zur Vertragsuebersicht</a>
</t>
<t t-else="">
<span class="text-muted">Kein Recht fuer Vertragsanzeige.</span>
</t>
</div>
</div>
<div class="card" style="background-color: #ffffff !important; border: 1px solid #d8dee6; border-radius: 10px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);">
<div class="card-body" style="background-color: #ffffff !important;">
<h4 class="card-title mb-2"><i class="fa fa-pencil-square-o me-1"/> Vertragseingabe</h4>
<p class="card-text text-muted mb-3">
Starte neue Eingaben oder arbeite an offenen Vertraegen weiter.
</p>
<a href="/my/invoiceinput" class="btn btn-outline-primary">Zur Vertragseingabe</a>
</div>
</div>
<t t-if="show_graphic_examples">
<div class="card" style="background-color: #ffffff !important; border: 1px solid #d8dee6; border-radius: 10px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);">
<div class="card-body" style="background-color: #ffffff !important;">
<h4 class="card-title mb-2"><i class="fa fa-picture-o me-1"/> Grafikbeispiele erstellen/anzeigen</h4>
<p class="card-text text-muted mb-3">
Vorhandene Grafikbeispiele ansehen oder neue anlegen.
</p>
<a href="/adportal/graphic-examples" class="btn btn-outline-primary">Grafikbeispiele oeffnen</a>
</div>
</div>
</t>
</div>
<div class="adportal-dashboard-side">
<div class="card mb-3" style="background-color: #ffffff !important; border: 1px solid #d8dee6; border-radius: 10px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);">
<div class="card-body" style="background-color: #ffffff !important;">
<h4 class="card-title mb-3"><i class="fa fa-bar-chart me-1"/> Statistik</h4>
<div class="adportal-stat-block mb-2">
<div class="text-muted">Nicht abgeschlossene Vertraege</div>
<div class="display-6 mb-0" t-esc="draft_count"/>
</div>
<div class="adportal-stat-block mb-2">
<div class="text-muted">Gesamte Vertraege</div>
<div class="display-6 mb-0" t-esc="total_count"/>
</div>
<div class="adportal-stat-block">
<div class="text-muted mb-1">Zuletzt bearbeitet</div>
<t t-if="recent_contracts">
<div><t t-esc="recent_contracts[0].contract_auto_id or '-'"/></div>
</t>
<t t-else="">
<div class="text-muted">Noch keine Vertraege vorhanden.</div>
</t>
</div>
</div>
</div>
<t t-if="show_ranking">
<div class="card" style="background-color: #ffffff !important; border: 1px solid #d8dee6; border-radius: 10px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);">
<div class="card-body" style="background-color: #ffffff !important;">
<h4 class="card-title mb-3"><i class="fa fa-star me-1"/> Ranking</h4>
<t t-if="not ranking_rows">
<div class="text-muted">Keine Benutzer im Ranking markiert.</div>
</t>
<t t-else="">
<div class="list-group list-group-flush">
<t t-foreach="ranking_rows" t-as="ranking_row" t-key="ranking_row['name']">
<div class="list-group-item px-0 d-flex justify-content-between align-items-center" style="background: transparent;">
<span><t t-esc="ranking_row['name']"/></span>
<span>
<span style="color: #f59e0b;"><t t-esc="'★' * ranking_row['stars']"/></span><span class="text-muted"><t t-esc="'☆' * (8 - ranking_row['stars'])"/></span>
<span class="visually-hidden">Summe: <t t-esc="ranking_row['sum']"/></span>
</span>
</div>
</t>
</div>
</t>
</div>
</div>
</t>
</div>
</div>
</t>
</template>
<template id="adportal_contracts" name="AD Portal Vertragsuebersicht">
<t t-call="portal.portal_layout">
<t t-call="website.eingabe-header"/>
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<t t-call="DigitalSignage.invoiceinput_portal_searchbar">
<t t-set="title">Projekte mit Vertraegen</t>
</t>
<t t-if="not project_cards">
<p>Es sind keine Projekte mit Vertraegen vorhanden.</p>
</t>
<t t-if="project_cards">
<div class="row g-3">
<t t-foreach="project_cards" t-as="project_card" t-key="project_card['id']">
<div class="col-12 col-md-6 col-xl-4">
<div class="card h-100" style="background-color: #ffffff !important; border: 1px solid #d8dee6; border-radius: 10px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);">
<div class="card-body" style="background-color: #ffffff !important;">
<h4 class="card-title mb-1" style="font-weight: 700; font-size: 1.1rem;">
<i class="fa fa-folder-open-o me-1"/><t t-esc="project_card['name']"/>
</h4>
<p class="text-muted mb-2 small">
Projekt <t t-esc="project_card['projectid'] or '-'"/>
</p>
<p class="mb-3">
<strong><t t-esc="project_card['count']"/></strong> Vertraege
</p>
<a t-att-href="'/adportal/contracts/project/' + str(project_card['id'])" class="btn btn-sm btn-primary">
Vertragsliste anzeigen
</a>
</div>
</div>
</div>
</t>
</div>
</t>
</t>
</template>
<template id="adportal_contracts_project" name="AD Portal Projekt Vertragsliste">
<t t-call="portal.portal_layout">
<t t-call="website.eingabe-header"/>
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<t t-call="DigitalSignage.invoiceinput_portal_searchbar">
<t t-set="title">Vertraege Projekt <t t-esc="project.projectid or '-'"/></t>
</t>
<div class="mb-3">
<a href="/adportal/contracts" class="btn btn-sm btn-outline-secondary">Zurueck zu Projekten</a>
</div>
<t t-if="not documents">
<p>Keine Vertraege fuer dieses Projekt vorhanden.</p>
</t>
<t t-if="documents" t-call="portal.portal_table">
<thead>
<tr class="thead-light">
<th class="text-left">Vertrag #</th>
<th class="text-left">Bezeichnung</th>
<th class="text-left">Werbefelder</th>
<th class="text-left">Vertragsabschluss</th>
<th class="text-left">Startdatum</th>
<th class="text-left">Vertragsverlaengerung</th>
<th class="text-left">Errechnetes Vertragsende</th>
<th class="text-left">Kunden Name</th>
<th class="text-left">Status</th>
<th class="text-center">Details</th>
</tr>
</thead>
<tbody>
<t t-foreach="documents" t-as="document" t-key="document.id">
<tr>
<td><t t-esc="document.contract_auto_id or '-'"/></td>
<td><t t-esc="document.contract_name or document.contract_auto_name or '-'"/></td>
<td>
<t t-if="document.werbe_feld_selected">
<div class="d-flex flex-wrap" style="gap: .25rem; max-width: 320px;">
<t t-foreach="document.werbe_feld_selected" t-as="wfeld" t-key="wfeld.id">
<span class="badge rounded-pill text-bg-light" style="border: 1px solid #d8dee6; color: #1f2937;">
<t t-esc="wfeld.feldname or wfeld.display_name or '-'"/>
</span>
</t>
</div>
</t>
<t t-else="">-</t>
</td>
<td><t t-esc="document.contract_date or '-'"/></td>
<td><t t-esc="document.start_date or '-'"/></td>
<td>
<t t-if="document.contract_auto_extend">
Ja (<t t-esc="document.contract_auto_extend_time or '-'"/> Monate)
</t>
<t t-else="">Nein</t>
</td>
<td>
<t t-if="computed_contract_end_by_id and document.id in computed_contract_end_by_id and computed_contract_end_by_id[document.id]">
<t t-esc="computed_contract_end_by_id[document.id]"/>
</t>
<t t-else="">-</t>
</td>
<td><t t-esc="document.client_short_name or '-'"/></td>
<td><t t-esc="document.contract_state_name or '-'"/></td>
<td style="text-align: center;">
<a t-att-href="'/adportal/contracts/' + (document.uuid or '')" title="Details" class="mr4">
<i class="fa fa-search fa-1x" role="img" aria-label="Details"/>
</a>
</td>
</tr>
</t>
</tbody>
</t>
</t>
</template>
<template id="adportal_contract_media_panel" name="AD Portal Medienpanel">
<t t-if="media_enabled">
<div class="row g-3 mt-0" data-media-panel="1" t-att-data-panel-url="media_panel_url">
<div class="col-12">
<div class="card" style="background-color: #ffffff !important; border: 1px solid #d8dee6; border-radius: 10px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);">
<div class="card-body" style="background-color: #ffffff !important;">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="card-title mb-0"><i class="fa fa-folder-open-o me-1"/> Mediendaten</h5>
<div class="d-flex" style="gap: .5rem;">
<button type="button" class="btn btn-sm btn-outline-secondary" t-att-data-media-refresh="media_current_path or ''">Aktualisieren</button>
<t t-if="media_parent_path != ''">
<a t-att-href="media_parent_href" t-att-data-media-path="media_parent_path" class="btn btn-sm btn-outline-secondary">
Zurueck
</a>
</t>
</div>
</div>
<div class="small mb-2">
<span class="text-muted me-1">Pfad:</span>
<t t-foreach="media_breadcrumbs" t-as="crumb" t-key="crumb['href']">
<span class="text-muted mx-1">/</span>
<t t-if="crumb['is_current']">
<strong><t t-esc="crumb['name']"/></strong>
</t>
<t t-else="">
<a t-att-href="crumb['href']" t-att-data-media-path="crumb['path']"><t t-esc="crumb['name']"/></a>
</t>
</t>
</div>
<div style="overflow-x: auto;">
<table class="table table-sm table-hover mb-0">
<thead style="background-color: #f8f9fa;">
<tr>
<th>Name</th>
<th>Typ</th>
<th class="text-end">Groesse (Bytes)</th>
<th class="text-center">Aktion</th>
</tr>
</thead>
<tbody>
<t t-if="not media_entries">
<tr>
<td colspan="4" class="text-muted">In diesem Ordner sind keine Dateien oder Unterordner vorhanden.</td>
</tr>
</t>
<t t-else="">
<t t-foreach="media_entries" t-as="entry" t-key="entry['href']">
<tr>
<td>
<i t-attf-class="fa #{'fa-folder' if entry['is_dir'] else 'fa-file-o'} me-1"/>
<t t-esc="entry['name']"/>
</td>
<td><t t-esc="'Ordner' if entry['is_dir'] else 'Datei'"/></td>
<td class="text-end"><t t-esc="entry['size'] or '-'"/></td>
<td class="text-center">
<t t-if="entry['is_dir']">
<a t-att-href="entry['nav_href']" t-att-data-media-path="entry['nav_path']" class="btn btn-sm btn-outline-primary">Oeffnen</a>
</t>
<t t-else="">
<a t-att-href="entry['href']" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">Anzeigen</a>
</t>
</td>
</tr>
</t>
</t>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
<template id="adportal_contract_detail" name="AD Portal Vertragsdetails">
<t t-call="portal.portal_layout">
<t t-call="website.eingabe-header"/>
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<t t-call="DigitalSignage.invoiceinput_portal_searchbar">
<t t-set="title">Vertragsdetails</t>
</t>
<div class="mb-3">
<div>
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">
<i class="fa fa-file-text-o me-1"/> Vertrag <t t-esc="contract.contract_auto_id or '-'"/>
</h4>
<a href="/adportal/contracts" class="btn btn-sm btn-outline-secondary">Zurueck zur Uebersicht</a>
</div>
<div class="card mb-3" style="background-color: #ffffff !important; border: 1px solid #d8dee6; border-radius: 10px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);">
<div class="card-body" style="background-color: #ffffff !important;">
<h5 class="card-title mb-3"><i class="fa fa-database me-1"/> Vertragsdaten</h5>
<div class="row g-3">
<div class="col-12 col-lg-6">
<div class="mb-3 p-3" style="background: #f8fafc; border: 1px solid #d8dee6; border-radius: 10px;">
<h5 class="mb-2" style="font-weight: 700;"><i class="fa fa-th-large me-1"/> Werbefelder</h5>
<t t-if="contract.werbe_feld_selected">
<div class="d-flex flex-wrap" style="gap: .45rem;">
<t t-foreach="contract.werbe_feld_selected" t-as="wfeld" t-key="wfeld.id">
<span class="badge rounded-pill" style="font-size: .95rem; padding: .55rem .8rem; background: #e2e8f0; color: #0f172a; border: 1px solid #cbd5e1;">
<t t-esc="wfeld.feldname or wfeld.display_name or '-'"/>
</span>
</t>
</div>
</t>
<t t-else="">
<div class="text-muted">Keine Werbefelder zugeordnet.</div>
</t>
</div>
<h6 class="text-muted mb-2">Grund- und Kundendaten</h6>
<div><strong>Projekt ID:</strong> <t t-esc="contract.project_id or '-'"/></div>
<div><strong>Projekt:</strong> <t t-esc="contract.project.display_name if contract.project else '-'"/></div>
<div><strong>Kunden ID:</strong> <t t-esc="contract.client_id or '-'"/></div>
<div><strong>Kundennummer:</strong> <t t-esc="contract.contract_auto_id or '-'"/></div>
<div><strong>Vertragskennung:</strong> <t t-esc="contract.contract_name or contract.contract_auto_name or '-'"/></div>
<div><strong>Vertragsstatus:</strong> <t t-esc="contract.contract_state_name or '-'"/></div>
<div><strong>Kampagnenstatus:</strong> <t t-esc="contract.ads_last_adtype or '-'"/></div>
<hr class="my-2"/>
<div><strong>Firmenname:</strong> <t t-esc="contract.client_short_company or '-'"/></div>
<div><strong>Vorname:</strong> <t t-esc="contract.client_short_vorname or '-'"/></div>
<div><strong>Name:</strong> <t t-esc="contract.client_short_name or '-'"/></div>
<div><strong>Strasse:</strong> <t t-esc="contract.client_short_strasse or '-'"/></div>
<div><strong>PLZ/Ort:</strong> <t t-esc="(contract.client_short_plz or '-') + ' ' + (contract.client_short_ort or '-')"/></div>
<div><strong>Telefon:</strong> <t t-esc="contract.client_short_telefon or '-'"/></div>
<div><strong>Mobil:</strong> <t t-esc="contract.client_short_mobil or '-'"/></div>
<div><strong>E-Mail:</strong> <t t-esc="contract.client_short_email or '-'"/></div>
<div><strong>RG-E-Mail:</strong> <t t-esc="contract.client_short_rgemail or '-'"/></div>
<div><strong>Webseite:</strong> <t t-esc="contract.client_short_website or '-'"/></div>
</div>
<div class="col-12 col-lg-6">
<h6 class="text-muted mb-2">Laufzeit, Hinweise und Summen</h6>
<div><strong>Zustaendigkeit (letzte Kamp.):</strong> <t t-esc="contract.ads_last_state.display_name if contract.ads_last_state else '-'"/></div>
<div><strong>Arbeitsstand (letzte Kamp.):</strong> <t t-esc="contract.ads_last_work_state.display_name if contract.ads_last_work_state else '-'"/></div>
<div><strong>Naechste Aufgabe:</strong> <t t-esc="contract.ads_last_todo_state.display_name if contract.ads_last_todo_state else '-'"/></div>
<div><strong>Aufgabe bis:</strong> <t t-esc="contract.ads_last_todo_state_until or '-'"/></div>
<div><strong>Hinweise Kampagne:</strong> <t t-esc="contract.ads_last_work_state_info or '-'"/></div>
<div><strong>Hinweise Vertrag:</strong> <t t-esc="contract.work_state_info or '-'"/></div>
<div><strong>Hinweise Allgemein:</strong> <t t-esc="contract.remark or '-'"/></div>
<hr class="my-2"/>
<div><strong>Vertragsdatum:</strong> <t t-esc="contract.contract_date or '-'"/></div>
<div><strong>Vertragsstart (1. Ausstrahlung geplant):</strong> <t t-esc="contract.start_date or '-'"/></div>
<div><strong>Aktueller wirklicher Start:</strong> <t t-esc="contract.ads_last_date_start_real or '-'"/></div>
<div><strong>Laufzeit-System:</strong> <t t-esc="contract.runtimesystem or '-'"/></div>
<div><strong>Laufzeit-Ende geplant:</strong> <t t-esc="contract.runtime_finish or '-'"/></div>
<div><strong>Enddatum berechnet:</strong> <t t-esc="contract.calc_end_date or '-'"/></div>
<div><strong>Laufzeit (Monate):</strong> <t t-esc="contract.runtime_m or '-'"/></div>
<div><strong>Automat. Verlängerung:</strong> <t t-esc="'Ja' if contract.contract_auto_extend else 'Nein'"/></div>
<div><strong>Verlängerungszeit (Monate):</strong> <t t-esc="contract.contract_auto_extend_time or '-'"/></div>
<div><strong>Kündigungsfrist (Monate):</strong> <t t-esc="contract.contract_cancel_mon or '-'"/></div>
<div><strong>Kündigungsfrist (Datum):</strong> <t t-esc="contract.contract_cancel_date or '-'"/></div>
<div><strong>Vertrag gekündigt:</strong> <t t-esc="'Ja' if contract.contract_iscanceled else 'Nein'"/></div>
<div><strong>Kündigungsdatum:</strong> <t t-esc="contract.ads_last_todo_state_until or contract.contract_iscanceled_date or '-'"/></div>
<hr class="my-2"/>
<div><strong>Gesamtsumme:</strong> <t t-esc="contract.web_contract_paysum_all or '-'"/></div>
<div><strong>Laufzeit-Summe:</strong> <t t-esc="contract.web_contract_paysum_lz or '-'"/></div>
<div><strong>Grafik-Summe:</strong> <t t-esc="contract.web_contract_paysum_gfx or '-'"/></div>
<div><strong>BIC:</strong> <t t-esc="contract.client_short_BIC or '-'"/></div>
<div><strong>IBAN:</strong> <t t-esc="contract.client_short_IBAN or '-'"/></div>
<div><strong>Letzte Aenderung:</strong> <t t-esc="contract.write_date or '-'"/></div>
</div>
</div>
</div>
</div>
<div class="row g-3 mt-0">
<div class="col-12">
<div class="card" style="background-color: #ffffff !important; border: 1px solid #d8dee6; border-radius: 10px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);">
<div class="card-body" style="background-color: #ffffff !important;">
<h5 class="card-title"><i class="fa fa-bullhorn me-1"/> Werbekampagnen</h5>
<t t-if="not contract.ads">
<p class="text-muted mb-0">Keine Werbekampagnen vorhanden.</p>
</t>
<t t-else="">
<div style="overflow-x: auto;">
<table class="table table-sm table-borderless">
<thead style="background-color: #f8f9fa;">
<tr>
<th>Kampagnennummer</th>
<th>Typ</th>
<th>Start geplant</th>
<th>Start real</th>
<th>Ende geplant</th>
<th>Ende real</th>
</tr>
</thead>
<tbody>
<t t-foreach="contract.ads" t-as="ad" t-key="ad.id">
<tr>
<td><strong><t t-esc="ad.adname or '-'"/></strong></td>
<td>
<span class="badge" t-attf-style="background-color: #{'#0d6efd' if ad.adtype == 'MAIN' else '#6c757d'};">
<t t-esc="ad.adtype or '-'"/>
</span>
</td>
<td><t t-esc="ad.date_start_planed or '-'"/></td>
<td><strong><t t-esc="ad.date_start_real or '-'"/></strong></td>
<td><t t-esc="ad.date_remove_planed or '-'"/></td>
<td><t t-esc="ad.date_remove_real or '-'"/></td>
</tr>
</t>
</tbody>
</table>
</div>
</t>
</div>
</div>
</div>
</div>
<div id="adportal-media-panel">
<t t-if="show_media">
<t t-call="DigitalSignage.adportal_contract_media_panel"/>
</t>
<t t-else="">
<div class="card" style="background-color: #ffffff !important; border: 1px solid #d8dee6; border-radius: 10px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);">
<div class="card-body" style="background-color: #ffffff !important;">
<h5 class="card-title mb-0"><i class="fa fa-folder-open-o me-1"/> Mediendaten</h5>
<p class="text-muted mb-0 mt-2">Kein Recht fuer Medienanzeige.</p>
</div>
</div>
</t>
</div>
<div t-if="contract.scan_vertrag" class="card mt-3" style="background-color: #ffffff !important; border: 1px solid #d8dee6; border-radius: 10px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);">
<t t-set="pdf_download_url" t-value="'/web/content?model=dss.contracts&amp;field=scan_vertrag&amp;id=%s&amp;filename_field=scan_vertrag_filename&amp;download=true' % contract.id"/>
<t t-set="pdf_viewer_src" t-value="'/web/static/lib/pdfjs/web/viewer.html?file=/web/content%3Fmodel%3Ddss.contracts%26field%3Dscan_vertrag%26id%3D' + str(contract.id) + '%26filename_field%3Dscan_vertrag_filename%26download%3Dfalse'"/>
<div class="card-body" style="background-color: #ffffff !important;">
<div class="d-flex justify-content-between align-items-center flex-wrap" style="gap: .5rem;">
<h5 class="card-title mb-0"><i class="fa fa-file-pdf-o me-1"/> Vertrag PDF</h5>
<a t-att-href="pdf_download_url" class="btn btn-sm btn-outline-secondary">
Download
</a>
</div>
<div class="mt-3" style="border: 1px solid #d8dee6; border-radius: 8px; overflow: hidden; background: #f8fafc;">
<iframe
t-att-src="pdf_viewer_src"
style="width: 100%; min-height: 720px; border: 0;"
title="Vertrag PDF Vorschau"
/>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
(function () {
function bindMediaPanelHandlers(root) {
if (!root) {
return;
}
var panel = root.querySelector('[data-media-panel]');
if (!panel) {
return;
}
var panelUrl = panel.getAttribute('data-panel-url');
if (!panelUrl) {
return;
}
function reloadMedia(path) {
var url = panelUrl;
if (path) {
url += '?media_path=' + encodeURIComponent(path);
}
fetch(url, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
}).then(function (response) {
return response.text();
}).then(function (html) {
root.innerHTML = html;
bindMediaPanelHandlers(root);
}).catch(function () {
window.location.reload();
});
}
panel.querySelectorAll('[data-media-path]').forEach(function (element) {
element.addEventListener('click', function (ev) {
ev.preventDefault();
reloadMedia(element.getAttribute('data-media-path') || '');
});
});
var refreshButton = panel.querySelector('[data-media-refresh]');
if (refreshButton) {
refreshButton.addEventListener('click', function () {
reloadMedia(refreshButton.getAttribute('data-media-refresh') || '');
});
}
}
bindMediaPanelHandlers(document.getElementById('adportal-media-panel'));
})();
</script>
</t>
</template>
<template id="adportal_graphic_examples" name="AD Portal Grafikbeispiele">
<t t-call="portal.portal_layout">
<t t-call="website.eingabe-header"/>
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<t t-call="DigitalSignage.invoiceinput_portal_searchbar">
<t t-set="title">Grafikbeispiele erstellen/anzeigen</t>
</t>
<div class="card mb-3" style="background-color: #ffffff !important; border: 1px solid #d8dee6; border-radius: 10px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);">
<div class="card-body" style="background-color: #ffffff !important;">
<div>
<h4 class="mb-1"><i class="fa fa-picture-o me-1"/> Grafikbeispiele</h4>
<p class="text-muted mb-3">Firma eingeben, Bild generieren und danach per Aenderungswunsch iterativ verbessern.</p>
</div>
<div class="row g-2 align-items-end mb-3">
<div class="col-12 col-md-6">
<label for="adportal-company-input" class="form-label">Firma</label>
<input id="adportal-company-input" list="adportal-company-list" class="form-control" type="text" placeholder="Firma eingeben oder aus Vorschlag waehlen"/>
<datalist id="adportal-company-list">
<t t-foreach="company_suggestions" t-as="company_name" t-key="company_name">
<option t-att-value="company_name"/>
</t>
</datalist>
</div>
<div class="col-12 col-md-4">
<label for="adportal-template-input" class="form-label">Medientyp</label>
<select id="adportal-template-input" class="form-select">
<option value="0">Standard</option>
<t t-foreach="graphic_templates" t-as="graphic_template" t-key="graphic_template.id">
<option t-att-value="graphic_template.id">
<t t-esc="graphic_template.medianame"/> - <t t-esc="graphic_template.maxsize_w or 1024"/> x <t t-esc="graphic_template.maxsize_h or 1024"/>
</option>
</t>
</select>
</div>
<div class="col-12 col-md-2 d-grid">
<button type="button" id="adportal-graphic-start" class="btn btn-primary">Start</button>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-12">
<label for="adportal-company-address-input" class="form-label">Adresse</label>
<input id="adportal-company-address-input" class="form-control" type="text" placeholder="Adresse wird nach Treffer automatisch eingetragen"/>
</div>
</div>
<div id="adportal-graphic-status" class="alert d-none mb-3" role="alert"/>
<div id="adportal-address-selection-panel" class="alert alert-info d-none mb-3" role="alert">
<div class="row g-2 align-items-end">
<div class="col-12 col-md-9">
<label for="adportal-address-select" class="form-label">Mehrere Adressen gefunden, bitte waehlen</label>
<select id="adportal-address-select" class="form-select"/>
</div>
<div class="col-12 col-md-3 d-grid">
<button type="button" id="adportal-address-confirm" class="btn btn-primary">Adresse uebernehmen</button>
</div>
</div>
</div>
<div class="mb-3" style="position: relative; border: 1px solid #d8dee6; border-radius: 10px; min-height: 340px; display: flex; align-items: center; justify-content: center; padding: 1rem; background: linear-gradient(135deg, #f8fafc 0%, #eef4ff 100%);">
<button type="button" id="adportal-graphic-debug-open" class="btn btn-light btn-sm" title="Prompt und API-Parameter anzeigen" style="position: absolute; top: 10px; right: 10px; width: 30px; height: 30px; border-radius: 50%; border: 1px solid #c8d2e0; padding: 0; font-weight: 700; z-index: 2;">i</button>
<img id="adportal-graphic-preview" alt="Grafikvorschau" style="max-width: 100%; max-height: 520px; border-radius: 8px; display: none;"/>
<div id="adportal-graphic-placeholder" class="text-muted text-center">
Noch kein Bild generiert. Bitte Firma eingeben und Start klicken.
</div>
</div>
<div id="adportal-graphic-debug-overlay" class="d-none" style="position: fixed; inset: 0; z-index: 1200; background: rgba(15, 23, 42, 0.62); padding: 1rem; overflow: auto;">
<div style="max-width: 960px; margin: 1rem auto; background: #ffffff; border-radius: 12px; box-shadow: 0 10px 30px rgba(15, 23, 42, 0.3); border: 1px solid #d8dee6;">
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0.9rem 1rem; border-bottom: 1px solid #e6ebf2;">
<h5 class="mb-0">Anfrage-Details</h5>
<button type="button" id="adportal-graphic-debug-close" class="btn btn-outline-secondary btn-sm">Schliessen</button>
</div>
<div style="padding: 1rem;">
<div class="mb-3">
<label class="form-label fw-bold mb-1">Prompt</label>
<pre id="adportal-graphic-debug-prompt" style="background: #f8fafc; border: 1px solid #d8dee6; border-radius: 8px; padding: 0.75rem; white-space: pre-wrap; word-break: break-word; min-height: 70px; margin: 0;"/>
</div>
<div class="mb-3">
<label class="form-label fw-bold mb-1">Parameter (Request)</label>
<pre id="adportal-graphic-debug-request" style="background: #f8fafc; border: 1px solid #d8dee6; border-radius: 8px; padding: 0.75rem; white-space: pre-wrap; word-break: break-word; min-height: 70px; margin: 0;"/>
</div>
<div class="mb-3">
<label class="form-label fw-bold mb-1">Antwort-Parameter (Response)</label>
<pre id="adportal-graphic-debug-response" style="background: #f8fafc; border: 1px solid #d8dee6; border-radius: 8px; padding: 0.75rem; white-space: pre-wrap; word-break: break-word; min-height: 70px; margin: 0;"/>
</div>
<div>
<label class="form-label fw-bold mb-1">Meldungen</label>
<pre id="adportal-graphic-debug-messages" style="background: #f8fafc; border: 1px solid #d8dee6; border-radius: 8px; padding: 0.75rem; white-space: pre-wrap; word-break: break-word; min-height: 70px; margin: 0;"/>
</div>
</div>
</div>
</div>
<div class="row g-2 align-items-end">
<div class="col-12 col-md-9">
<label for="adportal-graphic-instruction" class="form-label">Aenderungswunsch</label>
<input id="adportal-graphic-instruction" class="form-control" type="text" placeholder="z.B. Logo groesser, Hintergrund heller, CTA deutlicher"/>
</div>
<div class="col-12 col-md-3 d-grid">
<button type="button" id="adportal-graphic-send" class="btn btn-outline-primary" disabled="disabled">Senden</button>
</div>
</div>
</div>
</div>
<script type="text/javascript">
(function () {
var startButton = document.getElementById('adportal-graphic-start');
var sendButton = document.getElementById('adportal-graphic-send');
var companyInput = document.getElementById('adportal-company-input');
var addressInput = document.getElementById('adportal-company-address-input');
var templateInput = document.getElementById('adportal-template-input');
var addressSelectionPanel = document.getElementById('adportal-address-selection-panel');
var addressSelect = document.getElementById('adportal-address-select');
var addressConfirmButton = document.getElementById('adportal-address-confirm');
var instructionInput = document.getElementById('adportal-graphic-instruction');
var statusBox = document.getElementById('adportal-graphic-status');
var previewImage = document.getElementById('adportal-graphic-preview');
var placeholder = document.getElementById('adportal-graphic-placeholder');
var debugOpenButton = document.getElementById('adportal-graphic-debug-open');
var debugOverlay = document.getElementById('adportal-graphic-debug-overlay');
var debugCloseButton = document.getElementById('adportal-graphic-debug-close');
var debugPrompt = document.getElementById('adportal-graphic-debug-prompt');
var debugRequest = document.getElementById('adportal-graphic-debug-request');
var debugResponse = document.getElementById('adportal-graphic-debug-response');
var debugMessages = document.getElementById('adportal-graphic-debug-messages');
var currentImageRef = '';
var isAddressValidated = false;
var selectedAddressValue = '';
var addressCandidates = [];
var currentTemplateId = 0;
var currentResolutionLabel = '1024 x 1024';
var debugState = {
action: '',
request: {},
response: {},
prompt: '',
messages: []
};
if (!startButton || !sendButton || !companyInput || !addressInput || !templateInput || !addressSelectionPanel || !addressSelect || !addressConfirmButton || !instructionInput || !statusBox || !previewImage || !placeholder || !debugOpenButton || !debugOverlay || !debugCloseButton || !debugPrompt || !debugRequest || !debugResponse || !debugMessages) {
return;
}
function resetValidationState() {
isAddressValidated = false;
selectedAddressValue = '';
addressCandidates = [];
addressSelectionPanel.classList.add('d-none');
addressSelect.innerHTML = '';
sendButton.setAttribute('disabled', 'disabled');
}
function formatAddressOption(record) {
var location = [record.postal_code || '', record.city || ''].join(' ').trim();
var parts = [record.name || '', record.street || '', location, record.region || '', record.country || ''];
return parts.filter(function (part) { return !!part; }).join(' | ');
}
function getAddressValue(record) {
return (record.full_address || '').trim();
}
function showAddressSelection(records) {
addressSelect.innerHTML = '';
records.forEach(function (record, index) {
var option = document.createElement('option');
option.value = String(index);
option.textContent = formatAddressOption(record);
addressSelect.appendChild(option);
});
addressSelectionPanel.classList.remove('d-none');
}
function renderDebugState() {
debugPrompt.textContent = debugState.prompt || 'Noch kein Prompt vorhanden.';
debugRequest.textContent = Object.keys(debugState.request || {}).length ? JSON.stringify(debugState.request, null, 2) : 'Noch keine Request-Parameter.';
debugResponse.textContent = Object.keys(debugState.response || {}).length ? JSON.stringify(debugState.response, null, 2) : 'Noch keine Response-Parameter.';
debugMessages.textContent = (debugState.messages || []).length ? debugState.messages.join('\n') : 'Keine Meldungen.';
}
function openDebugOverlay() {
renderDebugState();
debugOverlay.classList.remove('d-none');
}
function closeDebugOverlay() {
debugOverlay.classList.add('d-none');
}
function pushDebugMessage(message) {
if (!message) {
return;
}
debugState.messages.push(message);
}
function logToConsole(eventName, payload) {
if (typeof console === 'undefined') {
return;
}
console.log('[AD Portal Grafik]', eventName, payload || {});
}
function setStatus(message, isError) {
statusBox.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-info');
statusBox.classList.add(isError ? 'alert-danger' : 'alert-success');
statusBox.textContent = message;
pushDebugMessage('Status: ' + message);
}
function setLoading(message) {
statusBox.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-info');
statusBox.classList.add('alert-info');
statusBox.textContent = message;
}
function updatePreview(payload) {
var imageRef = payload.image_data || payload.image_url || '';
if (!imageRef) {
return false;
}
previewImage.src = imageRef;
previewImage.style.display = 'block';
placeholder.style.display = 'none';
currentImageRef = imageRef;
currentTemplateId = payload.template_id || currentTemplateId || 0;
currentResolutionLabel = payload.resolution_label || currentResolutionLabel || '1024 x 1024';
return true;
}
debugOpenButton.addEventListener('click', function () {
openDebugOverlay();
});
debugCloseButton.addEventListener('click', function () {
closeDebugOverlay();
});
debugOverlay.addEventListener('click', function (ev) {
if (ev.target === debugOverlay) {
closeDebugOverlay();
}
});
function postFormEncoded(url, data) {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams(data)
}).then(function (response) {
return response.json().catch(function () {
return { ok: false, error: 'Antwort konnte nicht gelesen werden.' };
}).then(function (payload) {
return {
ok: response.ok &amp;&amp; payload.ok,
payload: payload
};
});
});
}
function runStartGeneration(company, selectedAddress) {
debugState = {
action: 'start',
request: {
route: '/adportal/graphic-examples/start',
company: company,
selected_address: selectedAddress,
template_id: templateInput.value || '0'
},
response: {},
prompt: '',
messages: []
};
setLoading('Grafik wird erstellt...');
logToConsole('start_request', {
company: company,
selected_address: selectedAddress,
template_id: templateInput.value || '0'
});
return postFormEncoded('/adportal/graphic-examples/start', {
company: company,
selected_address: selectedAddress,
template_id: templateInput.value || '0'
}).then(function (result) {
debugState.response = result.payload || {};
debugState.prompt = (result.payload &amp;&amp; result.payload.prompt) || '';
logToConsole('start_response', {
ok: result.ok,
payload: result.payload || {}
});
if (result.payload &amp;&amp; result.payload.chatgpt_warning) {
pushDebugMessage('ChatGPT Hinweis: ' + result.payload.chatgpt_warning);
}
if (result.payload &amp;&amp; result.payload.google_warning) {
pushDebugMessage('Google Hinweis: ' + result.payload.google_warning);
}
if (result.payload &amp;&amp; result.payload.error) {
pushDebugMessage('Fehler: ' + result.payload.error);
}
if (!result.ok) {
setStatus(result.payload.error || 'Grafik konnte nicht erstellt werden.', true);
return;
}
if (!updatePreview(result.payload)) {
setStatus('API hat kein Bild geliefert.', true);
return;
}
selectedAddressValue = selectedAddress;
isAddressValidated = true;
sendButton.removeAttribute('disabled');
setStatus('Grafik erfolgreich erstellt.', false);
});
}
startButton.addEventListener('click', function () {
var company = (companyInput.value || '').trim();
if (!company) {
setStatus('Bitte zuerst eine Firma eingeben.', true);
return;
}
setLoading('Adresse wird geprueft...');
startButton.setAttribute('disabled', 'disabled');
resetValidationState();
logToConsole('address_validation_request', {
company: company,
template_id: templateInput.value || '0'
});
postFormEncoded('/adportal/graphic-examples/validate-address', {
company: company
}).then(function (addressResult) {
var payload = addressResult.payload || {};
var addresses = payload.addresses || [];
logToConsole('address_validation_response', {
ok: addressResult.ok,
payload: payload
});
if (payload.error) {
pushDebugMessage('Adresspruefung Fehler: ' + payload.error);
}
if (!addressResult.ok) {
setStatus(payload.error || 'Adresspruefung fehlgeschlagen.', true);
return;
}
addressCandidates = addresses;
if (payload.requires_selection &amp;&amp; addresses.length &gt; 1) {
showAddressSelection(addresses);
setStatus('Mehrere Adressen gefunden. Bitte Adresse auswaehlen.', false);
return;
}
var single = payload.selected_address || (addresses.length === 1 ? addresses[0] : {});
var chosenAddress = getAddressValue(single);
var singleCompany = ((single &amp;&amp; single.name) || company || '').trim();
if (!chosenAddress) {
setStatus('Keine Adresse fuer die Firma gefunden.', true);
return;
}
companyInput.value = singleCompany;
addressInput.value = chosenAddress;
return runStartGeneration(singleCompany, chosenAddress);
}).catch(function () {
logToConsole('address_validation_error', {
message: 'Technischer Fehler bei der Adresspruefung.'
});
pushDebugMessage('Technischer Fehler bei der Adresspruefung.');
setStatus('Technischer Fehler bei der Adresspruefung.', true);
}).finally(function () {
renderDebugState();
startButton.removeAttribute('disabled');
});
});
addressConfirmButton.addEventListener('click', function () {
var company = (companyInput.value || '').trim();
var index = parseInt(addressSelect.value || '0', 10);
var selected = addressCandidates[index] || {};
var chosenAddress = getAddressValue(selected);
var selectedCompany = ((selected &amp;&amp; selected.name) || company || '').trim();
if (!selectedCompany) {
setStatus('Bitte zuerst eine Firma eingeben.', true);
return;
}
if (!chosenAddress) {
setStatus('Bitte eine gueltige Adresse auswaehlen.', true);
return;
}
companyInput.value = selectedCompany;
addressInput.value = chosenAddress;
addressSelectionPanel.classList.add('d-none');
startButton.setAttribute('disabled', 'disabled');
runStartGeneration(selectedCompany, chosenAddress).catch(function () {
logToConsole('start_error', {
message: 'Technischer Fehler beim Erstellen der Grafik.'
});
pushDebugMessage('Technischer Fehler beim Erstellen der Grafik.');
setStatus('Technischer Fehler beim Erstellen der Grafik.', true);
}).finally(function () {
renderDebugState();
startButton.removeAttribute('disabled');
});
});
sendButton.addEventListener('click', function () {
var company = (companyInput.value || '').trim();
var instruction = (instructionInput.value || '').trim();
if (!company) {
setStatus('Bitte zuerst eine Firma eingeben.', true);
return;
}
if (!isAddressValidated) {
setStatus('Bitte zuerst Firma pruefen, Adresse waehlen und Start ausfuehren.', true);
return;
}
if (!instruction) {
setStatus('Bitte einen Aenderungswunsch eingeben.', true);
return;
}
if (!currentImageRef) {
setStatus('Bitte zuerst per Start ein Bild erzeugen.', true);
return;
}
debugState = {
action: 'refine',
request: {
route: '/adportal/graphic-examples/refine',
company: company,
selected_address: selectedAddressValue,
instruction: instruction,
current_image_present: !!currentImageRef,
template_id: templateInput.value || currentTemplateId || '0'
},
response: {},
prompt: '',
messages: []
};
setLoading('Aenderungen werden angewendet...');
sendButton.setAttribute('disabled', 'disabled');
logToConsole('refine_request', {
company: company,
instruction: instruction,
template_id: templateInput.value || currentTemplateId || '0',
has_current_image: !!currentImageRef
});
postFormEncoded('/adportal/graphic-examples/refine', {
company: company,
selected_address: selectedAddressValue,
instruction: instruction,
current_image: currentImageRef,
template_id: templateInput.value || currentTemplateId || '0'
}).then(function (result) {
debugState.response = result.payload || {};
debugState.prompt = (result.payload &amp;&amp; result.payload.prompt) || '';
logToConsole('refine_response', {
ok: result.ok,
payload: result.payload || {}
});
if (result.payload &amp;&amp; result.payload.chatgpt_warning) {
pushDebugMessage('ChatGPT Hinweis: ' + result.payload.chatgpt_warning);
}
if (result.payload &amp;&amp; result.payload.google_warning) {
pushDebugMessage('Google Hinweis: ' + result.payload.google_warning);
}
if (result.payload &amp;&amp; result.payload.error) {
pushDebugMessage('Fehler: ' + result.payload.error);
}
if (!result.ok) {
setStatus(result.payload.error || 'Aenderung konnte nicht verarbeitet werden.', true);
return;
}
if (!updatePreview(result.payload)) {
setStatus('API hat kein Bild geliefert.', true);
return;
}
instructionInput.value = '';
setStatus('Aenderung erfolgreich uebernommen.', false);
}).catch(function () {
logToConsole('refine_error', {
message: 'Technischer Fehler beim Senden der Aenderung.'
});
pushDebugMessage('Technischer Fehler beim Senden der Aenderung.');
setStatus('Technischer Fehler beim Senden der Aenderung.', true);
}).finally(function () {
renderDebugState();
sendButton.removeAttribute('disabled');
});
});
companyInput.addEventListener('input', function () {
resetValidationState();
addressInput.value = '';
});
templateInput.addEventListener('change', function () {
resetValidationState();
});
})();
</script>
</t>
</template>
</odoo>