Files
Messe-Lotse/index.html
T
2026-09-29 09:40:32 +00:00

6205 lines
277 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>24HRS - Messe-Lotse</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- QRCode.js Library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<style>
:root {
--primary-yellow: #FFE000;
--primary-black: #111111;
--bg-light: #F5F5F5;
--white: #FFFFFF;
--text-primary: #111111;
--text-secondary: #555555;
--border-color: #E0E0E0;
--hover-yellow: #FFC800;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-light);
color: var(--text-primary);
font-size: 14px;
}
.app {
display: flex;
min-height: 100vh;
}
/* Sidebar */
.sidebar {
width: 260px;
background-color: var(--primary-black);
color: var(--white);
padding: 24px;
position: fixed;
height: 100vh;
}
.logo {
font-weight: 800;
font-size: 1.8rem;
color: var(--primary-yellow);
margin-bottom: 8px;
}
.subtitle {
font-size: 0.7rem;
color: var(--primary-yellow);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 32px;
}
.nav-item {
padding: 14px;
color: var(--white);
cursor: pointer;
margin-bottom: 8px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 12px;
font-weight: 600;
font-size: 0.85rem;
transition: all 0.2s;
}
.nav-item:hover {
background-color: #333;
}
.nav-item.active {
background-color: var(--primary-yellow);
color: var(--primary-black);
}
/* Main Content */
.main-content {
flex: 1;
margin-left: 260px;
padding: 32px;
}
h1 {
font-size: 2rem;
font-weight: 800;
text-transform: uppercase;
margin-bottom: 8px;
}
.subtitle-text {
color: var(--text-secondary);
margin-bottom: 32px;
}
/* Stats */
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 32px;
}
.stat-card {
background: var(--primary-black);
color: var(--white);
padding: 24px;
border-radius: 4px;
}
.stat-icon {
font-size: 2rem;
color: var(--primary-yellow);
margin-bottom: 12px;
}
.stat-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 8px;
color: #999;
}
.stat-value {
font-size: 2.5rem;
font-weight: 800;
color: var(--primary-yellow);
}
/* Buttons */
.btn {
padding: 12px 24px;
border: none;
border-radius: 4px;
font-family: 'Inter', sans-serif;
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: var(--primary-yellow);
color: var(--primary-black);
}
.btn-primary:hover {
background: var(--hover-yellow);
}
.btn-secondary {
background: var(--primary-black);
color: var(--white);
}
.btn-secondary:hover {
background: #333;
}
/* Toolbar */
.toolbar {
display: flex;
justify-content: space-between;
margin-bottom: 24px;
gap: 16px;
}
.search-box input {
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: 4px;
width: 300px;
font-family: 'Inter', sans-serif;
}
/* Jobs Grid */
.jobs-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.job-card {
background: var(--white);
border-radius: 4px;
overflow: hidden;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border-top: 4px solid var(--primary-yellow);
}
.job-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0,0,0,0.12);
}
.job-card-header {
padding: 20px;
border-bottom: 1px solid var(--border-color);
}
.job-id {
font-size: 0.7rem;
text-transform: uppercase;
color: var(--text-secondary);
margin-bottom: 8px;
}
.job-title {
font-size: 1.2rem;
font-weight: 800;
margin-bottom: 4px;
}
.job-subtitle {
color: var(--text-secondary);
font-size: 0.9rem;
}
.job-card-body {
padding: 20px;
}
.meta-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
font-size: 0.9rem;
}
.meta-item i {
color: var(--primary-yellow);
}
.progress-bar-container {
background: var(--border-color);
height: 8px;
border-radius: 4px;
overflow: hidden;
margin-top: 16px;
}
.progress-bar {
background: var(--primary-yellow);
height: 100%;
transition: width 0.3s;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 80px 20px;
}
.empty-state i {
font-size: 4rem;
color: var(--border-color);
margin-bottom: 16px;
}
.hidden {
display: none;
}
/* Tabs */
.tabs {
background: white;
border-radius: 4px;
overflow: hidden;
margin-bottom: 24px;
}
.tab-nav {
display: flex;
border-bottom: 2px solid var(--border-color);
background: var(--white);
}
.tab-btn {
flex: 1;
padding: 16px 24px;
background: transparent;
border: none;
font-family: 'Inter', sans-serif;
font-size: 0.85rem;
font-weight: 700;
text-transform: uppercase;
color: var(--text-secondary);
cursor: pointer;
position: relative;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.tab-btn:hover {
color: var(--text-primary);
}
.tab-btn.active {
color: var(--text-primary);
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 3px;
background: var(--primary-yellow);
}
.tab-content {
padding: 32px;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
/* Form Styles */
.form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 24px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group.full-width {
grid-column: 1 / -1;
}
label {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 8px;
color: var(--text-primary);
}
input, select, textarea {
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-family: 'Inter', sans-serif;
font-size: 0.9rem;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--primary-yellow);
}
textarea {
min-height: 100px;
resize: vertical;
}
.section-title {
font-size: 1.1rem;
font-weight: 800;
text-transform: uppercase;
margin: 32px 0 16px;
color: var(--text-primary);
}
/* Dynamic Lists */
.list-item {
background: var(--bg-light);
padding: 16px;
border-radius: 4px;
margin-bottom: 12px;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.btn-small {
padding: 8px 16px;
font-size: 0.8rem;
}
.icon-btn {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: all 0.2s;
}
.icon-btn:hover {
background: var(--border-color);
}
.icon-btn.danger:hover {
background: #dc3545;
color: white;
}
/* Checklist */
.checklist-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: var(--bg-light);
border-radius: 4px;
margin-bottom: 8px;
}
.checklist-item input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.checklist-item label {
flex: 1;
margin: 0;
font-size: 0.9rem;
font-weight: 400;
text-transform: none;
letter-spacing: normal;
cursor: pointer;
}
.checklist-item.completed label {
text-decoration: line-through;
color: var(--text-secondary);
}
/* Toast */
.toast {
position: fixed;
bottom: 24px;
right: 24px;
background: var(--primary-black);
color: var(--white);
padding: 16px 24px;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
display: none;
align-items: center;
gap: 12px;
border-left: 4px solid var(--primary-yellow);
z-index: 1000;
}
.toast.show {
display: flex;
animation: slideIn 0.3s;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Gallery Styles */
.gallery-upload {
background: var(--bg-light);
padding: 24px;
border-radius: 4px;
border: 2px dashed #CCCCCC;
text-align: center;
margin-bottom: 24px;
transition: all 0.2s;
}
.gallery-upload:hover {
border-color: var(--primary-yellow);
background: #FAFAFA;
}
.gallery-upload input[type="file"] {
display: none;
}
.gallery-upload-label {
display: inline-block;
padding: 12px 24px;
background: var(--primary-yellow);
color: var(--primary-black);
font-weight: 700;
text-transform: uppercase;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.gallery-upload-label:hover {
background: var(--hover-yellow);
}
.gallery-upload-info {
margin-top: 12px;
color: var(--text-secondary);
font-size: 0.85rem;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
margin-top: 24px;
}
.gallery-item {
position: relative;
background: var(--white);
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: all 0.2s;
cursor: pointer;
}
.gallery-item:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
transform: translateY(-2px);
}
.gallery-item-image {
width: 100%;
height: 200px;
object-fit: cover;
display: block;
}
.gallery-item-info {
padding: 12px;
background: var(--white);
}
.gallery-item-name {
font-size: 0.85rem;
color: var(--text-primary);
font-weight: 600;
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gallery-item-actions {
display: flex;
gap: 8px;
}
.gallery-item-btn {
flex: 1;
padding: 6px 12px;
background: var(--bg-light);
border: none;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
cursor: pointer;
transition: all 0.2s;
}
.gallery-item-btn:hover {
background: #E0E0E0;
}
.gallery-item-btn.delete {
color: #dc3545;
}
.gallery-item-btn.delete:hover {
background: #dc3545;
color: white;
}
/* Lightbox */
.lightbox {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
z-index: 10000;
align-items: center;
justify-content: center;
}
.lightbox.active {
display: flex;
}
.lightbox-content {
position: relative;
max-width: 90%;
max-height: 90%;
}
.lightbox-image {
max-width: 100%;
max-height: 90vh;
object-fit: contain;
border-radius: 4px;
}
.lightbox-close {
position: absolute;
top: -40px;
right: 0;
background: var(--primary-yellow);
color: var(--primary-black);
border: none;
width: 40px;
height: 40px;
border-radius: 4px;
font-size: 1.5rem;
cursor: pointer;
transition: all 0.2s;
}
.lightbox-close:hover {
background: var(--hover-yellow);
}
.lightbox-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: var(--primary-yellow);
color: var(--primary-black);
border: none;
width: 50px;
height: 50px;
border-radius: 4px;
font-size: 1.5rem;
cursor: pointer;
transition: all 0.2s;
}
.lightbox-nav:hover {
background: var(--hover-yellow);
}
.lightbox-nav.prev {
left: -60px;
}
.lightbox-nav.next {
right: -60px;
}
.gallery-empty {
text-align: center;
padding: 48px 24px;
color: var(--text-secondary);
}
.gallery-empty i {
font-size: 3rem;
color: #CCCCCC;
margin-bottom: 16px;
}
/* Calendar Styles */
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: #E0E0E0;
border: 1px solid #E0E0E0;
border-radius: 4px;
overflow: hidden;
}
.calendar-day-header {
background: var(--primary-black);
color: white;
padding: 12px;
text-align: center;
font-weight: 700;
font-size: 0.75rem;
text-transform: uppercase;
}
.calendar-day {
background: white;
min-height: 100px;
padding: 8px;
position: relative;
}
.calendar-day.other-month {
background: #F9F9F9;
color: #CCCCCC;
}
.calendar-day.today {
background: #FFF9E6;
border: 2px solid var(--primary-yellow);
}
.calendar-day-number {
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 4px;
color: var(--text-primary);
}
.calendar-event {
background: #3498db;
color: white;
padding: 4px 6px;
border-radius: 3px;
font-size: 0.7rem;
margin-bottom: 2px;
cursor: pointer;
transition: all 0.2s;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.calendar-event:hover {
opacity: 0.8;
transform: translateX(2px);
}
.calendar-event.setup {
background: #3498db;
}
.calendar-event.event {
background: var(--primary-yellow);
color: var(--primary-black);
font-weight: 600;
}
.calendar-event.teardown {
background: #e74c3c;
}
.calendar-event.deadline {
background: #FF6B35;
color: white;
font-weight: 600;
border-left: 3px solid #C44D2C;
}
.calendar-event-badge {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 4px;
}
</style>
</head>
<body>
<div class="app">
<!-- Sidebar -->
<nav class="sidebar">
<div class="logo">24HRS</div>
<div class="subtitle">Messe-Lotse</div>
<div class="nav-item active" id="nav-dashboard">
<i class="fas fa-th-large"></i>
<span>Dashboard</span>
</div>
<div class="nav-item" id="nav-calendar">
<i class="fas fa-calendar-alt"></i>
<span>Kalender</span>
</div>
</nav>
<!-- Main Content -->
<main class="main-content">
<h1>Dashboard</h1>
<p class="subtitle-text">Übersicht aller Messeprojekte</p>
<!-- Stats -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-briefcase"></i></div>
<div class="stat-label">Gesamt</div>
<div class="stat-value" id="stat-total">0</div>
</div>
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-clock"></i></div>
<div class="stat-label">Offen</div>
<div class="stat-value" id="stat-open">0</div>
</div>
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-calendar-week"></i></div>
<div class="stat-label">Diese Woche</div>
<div class="stat-value" id="stat-week">0</div>
</div>
</div>
<!-- Toolbar -->
<div class="toolbar">
<div class="search-box">
<input type="text" id="search-input" placeholder="Jobs durchsuchen...">
</div>
<button class="btn btn-primary" id="new-job-btn">
<i class="fas fa-plus"></i> Neuer Job
</button>
</div>
<!-- Dashboard View -->
<div id="dashboard-view">
<!-- Jobs Grid -->
<div id="jobs-container" class="jobs-grid"></div>
<!-- Empty State -->
<div id="empty-state" class="empty-state">
<i class="fas fa-briefcase"></i>
<h3>Keine Jobs vorhanden</h3>
<p>Erstellen Sie Ihren ersten Messejob</p>
<button class="btn btn-primary" id="empty-new-job-btn">
<i class="fas fa-plus"></i> Neuer Job
</button>
</div>
</div>
<!-- Calendar View -->
<div id="calendar-view" class="hidden">
<h1>Kalender</h1>
<p class="subtitle-text">Zeitplanung aller Messeprojekte</p>
<!-- Calendar Toolbar -->
<div class="calendar-toolbar" style="background: white; padding: 16px; border-radius: 4px; margin-bottom: 24px; display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; gap: 16px; align-items: center;">
<button class="btn btn-secondary btn-small" id="calendar-prev-month">
<i class="fas fa-chevron-left"></i>
</button>
<h2 id="calendar-month-year" style="margin: 0; font-size: 1.2rem;">Januar 2026</h2>
<button class="btn btn-secondary btn-small" id="calendar-next-month">
<i class="fas fa-chevron-right"></i>
</button>
<button class="btn btn-secondary btn-small" id="calendar-today">
Heute
</button>
</div>
<div style="display: flex; gap: 8px; align-items: center;">
<div style="display: flex; align-items: center; gap: 4px; font-size: 0.85rem;">
<span style="display: inline-block; width: 12px; height: 12px; background: #3498db; border-radius: 2px;"></span>
<span>Aufbau</span>
</div>
<div style="display: flex; align-items: center; gap: 4px; font-size: 0.85rem;">
<span style="display: inline-block; width: 12px; height: 12px; background: #FFE000; border-radius: 2px;"></span>
<span>Messe</span>
</div>
<div style="display: flex; align-items: center; gap: 4px; font-size: 0.85rem;">
<span style="display: inline-block; width: 12px; height: 12px; background: #e74c3c; border-radius: 2px;"></span>
<span>Abbau</span>
</div>
</div>
</div>
<!-- Calendar Container -->
<div id="calendar-container" style="background: white; padding: 24px; border-radius: 4px;">
<!-- Calendar will be rendered here -->
</div>
</div>
<!-- Job Detail View -->
<div id="job-detail-view" class="hidden">
<div style="background: var(--primary-black); color: white; padding: 24px; border-radius: 4px; margin-bottom: 24px; display: flex; justify-content: space-between; align-items: center;">
<div>
<button class="btn btn-secondary" id="back-btn" style="margin-bottom: 16px;">
<i class="fas fa-arrow-left"></i> Zurück
</button>
<h2 id="job-detail-title">Job Details</h2>
</div>
<div style="display: flex; gap: 12px;">
<button class="btn btn-primary" id="save-job-btn">
<i class="fas fa-save"></i> Speichern
</button>
<button class="btn btn-secondary" id="duplicate-job-btn">
<i class="fas fa-copy"></i> Duplizieren
</button>
<button class="btn btn-secondary" id="export-job-btn">
<i class="fas fa-file-pdf"></i> PDF Export
</button>
<button class="btn btn-secondary" id="delete-job-btn">
<i class="fas fa-trash"></i> Löschen
</button>
</div>
</div>
<!-- Tabs -->
<div class="tabs">
<div class="tab-nav">
<button class="tab-btn active" data-tab="0">
<i class="fas fa-info-circle"></i> Stammdaten & Termine
</button>
<button class="tab-btn" data-tab="1">
<i class="fas fa-file-alt"></i> Briefing
</button>
<button class="tab-btn" data-tab="2">
<i class="fas fa-truck"></i> Organisation
</button>
<button class="tab-btn" data-tab="3">
<i class="fas fa-tools"></i> Produktion
</button>
<button class="tab-btn" data-tab="4">
<i class="fas fa-tasks"></i> Dokumente
</button>
<button class="tab-btn" data-tab="5">
<i class="fas fa-images"></i> Standfotos
</button>
<button class="tab-btn" data-tab="6">
<i class="fas fa-clipboard-check"></i> Standabnahme
</button>
<button class="tab-btn" data-tab="7">
<i class="fas fa-calendar-week"></i> Weekly
</button>
</div>
<div class="tab-content">
<!-- Tab 1: Stammdaten & Termine -->
<div class="tab-pane active" id="tab-0">
<h3 class="section-title">Stammdaten</h3>
<div class="form-grid">
<div class="form-group">
<label>Kunde</label>
<input type="text" id="edit-customer">
</div>
<div class="form-group">
<label>Messe / Event</label>
<input type="text" id="edit-event">
</div>
<div class="form-group">
<label>Ort</label>
<div style="display: flex; gap: 8px; align-items: center;">
<input type="text" id="edit-location" style="flex: 1;">
<button type="button" class="btn btn-secondary btn-small" onclick="openGoogleMaps()" title="In Google Maps öffnen">
<i class="fas fa-map-marker-alt"></i>
</button>
</div>
</div>
<div class="form-group">
<label>Projekt</label>
<input type="text" id="edit-project">
</div>
<div class="form-group">
<label>Standart</label>
<select id="edit-stand-type">
<option value="">Bitte wählen</option>
<option value="Systemstand">Systemstand</option>
<option value="Reihstand">Reihstand</option>
<option value="Eckstand">Eckstand</option>
<option value="Kopfstand">Kopfstand</option>
<option value="Blockstand">Blockstand</option>
<option value="Inselstand">Inselstand</option>
</select>
</div>
<div class="form-group">
<label>Standgröße (B×T in m)</label>
<input type="text" id="edit-stand-size" placeholder="z.B. 6x4">
</div>
<div class="form-group">
<label>Standhöhe (m)</label>
<input type="number" step="0.1" id="edit-stand-height" placeholder="z.B. 2.5">
</div>
<div class="form-group">
<label>Halle / Stand</label>
<input type="text" id="edit-hall">
</div>
<div class="form-group">
<label>Standfläche (m²)</label>
<input type="number" id="edit-area">
</div>
<div class="form-group">
<label>Status</label>
<select id="edit-status">
<option value="Offen">Offen</option>
<option value="In Bearbeitung">In Bearbeitung</option>
<option value="Abgeschlossen">Abgeschlossen</option>
</select>
</div>
</div>
<h3 class="section-title">Zeitplanung</h3>
<div class="form-grid">
<div class="form-group">
<label>Aufbau Start</label>
<input type="datetime-local" id="edit-setup-start">
</div>
<div class="form-group">
<label>Aufbau Ende</label>
<input type="datetime-local" id="edit-setup-end">
</div>
<div class="form-group">
<label>Messe Start</label>
<input type="datetime-local" id="edit-event-start">
</div>
<div class="form-group">
<label>Messe Ende</label>
<input type="datetime-local" id="edit-event-end">
</div>
<div class="form-group">
<label>Abbau Start</label>
<input type="datetime-local" id="edit-teardown-start">
</div>
<div class="form-group">
<label>Abbau Ende</label>
<input type="datetime-local" id="edit-teardown-end">
</div>
</div>
<h3 class="section-title">Ansprechpartner</h3>
<div class="form-grid">
<div class="form-group">
<label>Name</label>
<input type="text" id="edit-contact-name">
</div>
<div class="form-group">
<label>Telefon</label>
<input type="tel" id="edit-contact-phone">
</div>
<div class="form-group full-width">
<label>E-Mail</label>
<input type="email" id="edit-contact-email">
</div>
</div>
<h3 class="section-title">Messestand-Bild</h3>
<div class="form-group">
<label>Bild vom Messestand hochladen</label>
<input type="file" id="stand-image-upload" accept="image/*" style="margin-bottom: 12px;">
<div id="stand-image-preview" style="display: none; background: var(--bg-light); padding: 16px; border-radius: 4px; text-align: center; margin-top: 12px;">
<img id="stand-image-preview-img" style="max-width: 100%; max-height: 300px; border-radius: 4px; margin-bottom: 12px;">
<div style="display: flex; gap: 8px; justify-content: center;">
<button type="button" class="btn btn-secondary btn-small" id="download-stand-image-btn">
<i class="fas fa-download"></i> Herunterladen
</button>
<button type="button" class="btn btn-secondary btn-small" id="remove-stand-image-btn">
<i class="fas fa-trash"></i> Entfernen
</button>
</div>
</div>
</div>
<div class="form-group">
<label>Notizen / Besonderheiten</label>
<textarea id="edit-notes"></textarea>
</div>
</div>
<!-- Tab 2: Briefing -->
<div class="tab-pane" id="tab-1">
<h3 class="section-title">Briefing</h3>
<p style="color: #666; margin-bottom: 32px;">
Laden Sie alle wichtigen Briefing-Dokumente für das Projekt hoch. Unterstützt werden PDFs und Bilder.
</p>
<!-- 1. Briefing Kunde -->
<div style="margin-bottom: 32px;">
<h4 style="font-size: 16px; font-weight: 600; color: #111111; margin-bottom: 12px;">
<i class="fas fa-file-pdf"></i> Briefing Kunde
</h4>
<p style="font-size: 14px; color: #666; margin-bottom: 12px;">
Kundenbriefing mit Anforderungen, Wünschen und Spezifikationen
</p>
<div class="upload-section">
<input type="file" id="briefing-customer-upload" accept=".pdf,.doc,.docx,image/*" style="display: none;">
<label for="briefing-customer-upload" class="btn btn-secondary" style="display: inline-block; cursor: pointer;">
<i class="fas fa-upload"></i> Briefing hochladen
</label>
<span style="font-size: 12px; color: #999; margin-left: 12px;">Max. 10 MB (PDF, Word, Bilder)</span>
</div>
<div id="briefing-customer-preview" style="display: none; margin-top: 16px; padding: 16px; background: #F5F5F5; border-radius: 6px;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 12px;">
<i class="fas fa-file-pdf" style="font-size: 24px; color: #D32F2F;"></i>
<div>
<div style="font-weight: 600; color: #111111;" id="briefing-customer-name">Dokument.pdf</div>
<div style="font-size: 12px; color: #666;" id="briefing-customer-size">0 KB</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-small" onclick="downloadBriefingCustomer()">
<i class="fas fa-download"></i> Download
</button>
<button class="btn btn-secondary btn-small" onclick="removeBriefingCustomer()">
<i class="fas fa-trash"></i> Löschen
</button>
</div>
</div>
</div>
</div>
<!-- 2. Brandbook / Logo -->
<div style="margin-bottom: 32px;">
<h4 style="font-size: 16px; font-weight: 600; color: #111111; margin-bottom: 12px;">
<i class="fas fa-palette"></i> Brandbook / Logo
</h4>
<p style="font-size: 14px; color: #666; margin-bottom: 12px;">
Corporate Identity, Brandbook, Logo-Dateien
</p>
<div class="upload-section">
<input type="file" id="briefing-brand-upload" accept=".pdf,.ai,.eps,.svg,.png,.jpg,.jpeg" style="display: none;">
<label for="briefing-brand-upload" class="btn btn-secondary" style="display: inline-block; cursor: pointer;">
<i class="fas fa-upload"></i> Brandbook hochladen
</label>
<span style="font-size: 12px; color: #999; margin-left: 12px;">Max. 10 MB (PDF, AI, EPS, SVG, PNG, JPG)</span>
</div>
<div id="briefing-brand-preview" style="display: none; margin-top: 16px; padding: 16px; background: #F5F5F5; border-radius: 6px;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 12px;">
<i class="fas fa-palette" style="font-size: 24px; color: #7B1FA2;"></i>
<div>
<div style="font-weight: 600; color: #111111;" id="briefing-brand-name">Brandbook.pdf</div>
<div style="font-size: 12px; color: #666;" id="briefing-brand-size">0 KB</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-small" onclick="downloadBriefingBrand()">
<i class="fas fa-download"></i> Download
</button>
<button class="btn btn-secondary btn-small" onclick="removeBriefingBrand()">
<i class="fas fa-trash"></i> Löschen
</button>
</div>
</div>
</div>
</div>
<!-- 3. Standplan -->
<div style="margin-bottom: 32px;">
<h4 style="font-size: 16px; font-weight: 600; color: #111111; margin-bottom: 12px;">
<i class="fas fa-map"></i> Standplan
</h4>
<p style="font-size: 14px; color: #666; margin-bottom: 12px;">
Technische Zeichnung, Grundriss, Standplan
</p>
<div class="upload-section">
<input type="file" id="briefing-plan-upload" accept=".pdf,.dwg,.dxf,image/*" style="display: none;">
<label for="briefing-plan-upload" class="btn btn-secondary" style="display: inline-block; cursor: pointer;">
<i class="fas fa-upload"></i> Standplan hochladen
</label>
<span style="font-size: 12px; color: #999; margin-left: 12px;">Max. 10 MB (PDF, DWG, DXF, Bilder)</span>
</div>
<div id="briefing-plan-preview" style="display: none; margin-top: 16px; padding: 16px; background: #F5F5F5; border-radius: 6px;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 12px;">
<i class="fas fa-map" style="font-size: 24px; color: #1976D2;"></i>
<div>
<div style="font-weight: 600; color: #111111;" id="briefing-plan-name">Standplan.pdf</div>
<div style="font-size: 12px; color: #666;" id="briefing-plan-size">0 KB</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-small" onclick="downloadBriefingPlan()">
<i class="fas fa-download"></i> Download
</button>
<button class="btn btn-secondary btn-small" onclick="removeBriefingPlan()">
<i class="fas fa-trash"></i> Löschen
</button>
</div>
</div>
</div>
</div>
<!-- 4. Weitere Dokumente -->
<div style="margin-bottom: 32px;">
<h4 style="font-size: 16px; font-weight: 600; color: #111111; margin-bottom: 12px;">
<i class="fas fa-folder-open"></i> Weitere Dokumente
</h4>
<p style="font-size: 14px; color: #666; margin-bottom: 12px;">
Zusätzliche Dateien, Verträge, Spezifikationen
</p>
<div class="upload-section">
<input type="file" id="briefing-additional-upload" accept="*" style="display: none;">
<label for="briefing-additional-upload" class="btn btn-secondary" style="display: inline-block; cursor: pointer;">
<i class="fas fa-upload"></i> Dokument hochladen
</label>
<span style="font-size: 12px; color: #999; margin-left: 12px;">Max. 10 MB (alle Dateitypen)</span>
</div>
<div id="briefing-additional-preview" style="display: none; margin-top: 16px; padding: 16px; background: #F5F5F5; border-radius: 6px;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 12px;">
<i class="fas fa-file" style="font-size: 24px; color: #616161;"></i>
<div>
<div style="font-weight: 600; color: #111111;" id="briefing-additional-name">Dokument.pdf</div>
<div style="font-size: 12px; color: #666;" id="briefing-additional-size">0 KB</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-small" onclick="downloadBriefingAdditional()">
<i class="fas fa-download"></i> Download
</button>
<button class="btn btn-secondary btn-small" onclick="removeBriefingAdditional()">
<i class="fas fa-trash"></i> Löschen
</button>
</div>
</div>
</div>
</div>
<div style="margin-top: 32px; padding: 16px; background: #FFFDE7; border-left: 4px solid #FFE000; border-radius: 4px;">
<div style="display: flex; gap: 12px;">
<i class="fas fa-info-circle" style="color: #F57F17; margin-top: 2px;"></i>
<div style="font-size: 14px; color: #333;">
<strong>Hinweis:</strong> Alle Dokumente werden verschlüsselt im Browser gespeichert (Base64).
Beachten Sie das LocalStorage-Limit von ca. 5-10 MB. Bei großen Dateien empfehlen wir Cloud-Speicher.
</div>
</div>
</div>
</div>
<!-- Tab 3: Organisation -->
<div class="tab-pane" id="tab-2">
<h3 class="section-title">Buchungsplattform</h3>
<div class="form-group">
<div class="checklist-item">
<input type="checkbox" id="edit-booking-platform" onchange="toggleBookingPlatformFields()">
<label for="edit-booking-platform">Buchungsplattform vorhanden</label>
</div>
</div>
<div id="booking-platform-fields" style="display: none; margin-top: 16px;">
<div class="form-grid">
<div class="form-group" style="grid-column: 1 / -1;">
<label>Portal-Link</label>
<input type="url" id="edit-booking-portal-link" placeholder="https://...">
</div>
<div class="form-group">
<label>E-Mail</label>
<input type="email" id="edit-booking-email" placeholder="beispiel@firma.de">
</div>
<div class="form-group">
<label>Passwort</label>
<input type="password" id="edit-booking-password" placeholder="••••••••">
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label>🗓️ Bestelldeadline</label>
<input type="date" id="edit-booking-deadline">
</div>
</div>
</div>
<h3 class="section-title">Strombuchung</h3>
<div class="form-group">
<div class="checklist-item">
<input type="checkbox" id="edit-power-booking" onchange="togglePowerBookingFields()">
<label for="edit-power-booking">Strom gebucht</label>
</div>
</div>
<div id="power-booking-fields" style="display: none; margin-top: 16px;">
<div class="form-grid">
<div class="form-group">
<label>Leistung (kW)</label>
<input type="number" id="edit-power-kw" placeholder="z.B. 3.5" step="0.1" min="0">
</div>
<div class="form-group">
<label>Stromverteiler</label>
<input type="text" id="edit-power-distributor" placeholder="z.B. Verteiler A12">
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label>🗓️ Bestelldeadline</label>
<input type="date" id="edit-power-deadline">
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label>Buchungsbestätigung (PDF)</label>
<input type="file" id="edit-power-confirmation-upload" accept=".pdf" onchange="handlePowerConfirmationUpload(event)" style="display: none;">
<button class="btn btn-secondary btn-small" onclick="document.getElementById('edit-power-confirmation-upload').click()">
<i class="fas fa-upload"></i> PDF hochladen
</button>
<div id="power-confirmation-preview" style="display: none; margin-top: 12px; padding: 12px; background: #F5F5F5; border-radius: 6px; border: 2px solid #FFE000;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 8px;">
<i class="fas fa-file-pdf" style="color: #E74C3C; font-size: 24px;"></i>
<div>
<div style="font-weight: 600; color: #111111;" id="power-confirmation-name">Bestätigung.pdf</div>
<div style="font-size: 12px; color: #666;" id="power-confirmation-size">0 KB</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-small" onclick="downloadPowerConfirmation()" title="Herunterladen">
<i class="fas fa-download"></i>
</button>
<button class="btn btn-secondary btn-small" onclick="removePowerConfirmation()" title="Entfernen">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<h3 class="section-title">WLAN</h3>
<div class="form-group">
<div class="checklist-item">
<input type="checkbox" id="edit-wlan" onchange="toggleWlanFields()">
<label for="edit-wlan">WLAN gebucht</label>
</div>
</div>
<div id="wlan-fields" style="display: none; margin-top: 16px;">
<div class="form-grid">
<div class="form-group">
<label>Art</label>
<input type="text" id="edit-wlan-type" placeholder="z.B. Standard, Premium, Business">
</div>
<div class="form-group">
<label>🗓️ Bestelldeadline</label>
<input type="date" id="edit-wlan-deadline">
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label>Buchungsbestätigung (PDF)</label>
<input type="file" id="edit-wlan-confirmation-upload" accept=".pdf" onchange="handleWlanConfirmationUpload(event)" style="display: none;">
<button class="btn btn-secondary btn-small" onclick="document.getElementById('edit-wlan-confirmation-upload').click()">
<i class="fas fa-upload"></i> PDF hochladen
</button>
<div id="wlan-confirmation-preview" style="display: none; margin-top: 12px; padding: 12px; background: #F5F5F5; border-radius: 6px; border: 2px solid #FFE000;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 8px;">
<i class="fas fa-file-pdf" style="color: #E74C3C; font-size: 24px;"></i>
<div>
<div style="font-weight: 600; color: #111111;" id="wlan-confirmation-name">Bestätigung.pdf</div>
<div style="font-size: 12px; color: #666;" id="wlan-confirmation-size">0 KB</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-small" onclick="downloadWlanConfirmation()" title="Herunterladen">
<i class="fas fa-download"></i>
</button>
<button class="btn btn-secondary btn-small" onclick="removeWlanConfirmation()" title="Entfernen">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<h3 class="section-title">Reinigungsservice</h3>
<div class="form-group">
<div class="checklist-item">
<input type="checkbox" id="edit-cleaning" onchange="toggleCleaningFields()">
<label for="edit-cleaning">Reinigungsservice gebucht</label>
</div>
</div>
<div id="cleaning-fields" style="display: none; margin-top: 16px;">
<div class="form-grid">
<div class="form-group">
<label>🗓️ Bestelldeadline</label>
<input type="date" id="edit-cleaning-deadline">
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label>Buchungsbestätigung (PDF)</label>
<input type="file" id="edit-cleaning-confirmation-upload" accept=".pdf" onchange="handleCleaningConfirmationUpload(event)" style="display: none;">
<button class="btn btn-secondary btn-small" onclick="document.getElementById('edit-cleaning-confirmation-upload').click()">
<i class="fas fa-upload"></i> PDF hochladen
</button>
<div id="cleaning-confirmation-preview" style="display: none; margin-top: 12px; padding: 12px; background: #F5F5F5; border-radius: 6px; border: 2px solid #FFE000;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 8px;">
<i class="fas fa-file-pdf" style="color: #E74C3C; font-size: 24px;"></i>
<div>
<div style="font-weight: 600; color: #111111;" id="cleaning-confirmation-name">Bestätigung.pdf</div>
<div style="font-size: 12px; color: #666;" id="cleaning-confirmation-size">0 KB</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-small" onclick="downloadCleaningConfirmation()" title="Herunterladen">
<i class="fas fa-download"></i>
</button>
<button class="btn btn-secondary btn-small" onclick="removeCleaningConfirmation()" title="Entfernen">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<h3 class="section-title">Rigging / Truss</h3>
<div class="form-group">
<div class="checklist-item">
<input type="checkbox" id="edit-rigging" onchange="toggleRiggingFields()">
<label for="edit-rigging">Rigging / Truss vorhanden</label>
</div>
</div>
<div id="rigging-fields" style="display: none; margin-top: 16px;">
<h4 style="font-size: 14px; font-weight: 600; color: #111111; margin-bottom: 12px;">Lieferant Kontaktdaten</h4>
<div class="form-grid">
<div class="form-group">
<label>Vorname</label>
<input type="text" id="edit-rigging-firstname" placeholder="Max">
</div>
<div class="form-group">
<label>Nachname</label>
<input type="text" id="edit-rigging-lastname" placeholder="Mustermann">
</div>
<div class="form-group">
<label>E-Mail</label>
<input type="email" id="edit-rigging-email" placeholder="max@rigging-firma.de">
</div>
<div class="form-group">
<label>Telefon</label>
<input type="tel" id="edit-rigging-phone" placeholder="+49 123 456789">
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label>🗓️ Bestelldeadline</label>
<input type="date" id="edit-rigging-deadline">
</div>
</div>
<h4 style="font-size: 14px; font-weight: 600; color: #111111; margin: 20px 0 12px 0;">Dokumente</h4>
<div class="form-grid">
<!-- Angebot Upload -->
<div class="form-group" style="grid-column: 1 / -1;">
<label>Angebot (PDF)</label>
<input type="file" id="edit-rigging-quote-upload" accept=".pdf" onchange="handleRiggingQuoteUpload(event)" style="display: none;">
<button class="btn btn-secondary btn-small" onclick="document.getElementById('edit-rigging-quote-upload').click()">
<i class="fas fa-upload"></i> Angebot hochladen
</button>
<div id="rigging-quote-preview" style="display: none; margin-top: 12px; padding: 12px; background: #F5F5F5; border-radius: 6px; border: 2px solid #FFE000;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 8px;">
<i class="fas fa-file-pdf" style="color: #E74C3C; font-size: 24px;"></i>
<div>
<div style="font-weight: 600; color: #111111;" id="rigging-quote-name">Angebot.pdf</div>
<div style="font-size: 12px; color: #666;" id="rigging-quote-size">0 KB</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-small" onclick="downloadRiggingQuote()" title="Herunterladen">
<i class="fas fa-download"></i>
</button>
<button class="btn btn-secondary btn-small" onclick="removeRiggingQuote()" title="Entfernen">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Technische Zeichnung Upload -->
<div class="form-group" style="grid-column: 1 / -1;">
<label>Technische Zeichnung (PDF)</label>
<input type="file" id="edit-rigging-drawing-upload" accept=".pdf" onchange="handleRiggingDrawingUpload(event)" style="display: none;">
<button class="btn btn-secondary btn-small" onclick="document.getElementById('edit-rigging-drawing-upload').click()">
<i class="fas fa-upload"></i> Zeichnung hochladen
</button>
<div id="rigging-drawing-preview" style="display: none; margin-top: 12px; padding: 12px; background: #F5F5F5; border-radius: 6px; border: 2px solid #FFE000;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 8px;">
<i class="fas fa-file-pdf" style="color: #E74C3C; font-size: 24px;"></i>
<div>
<div style="font-weight: 600; color: #111111;" id="rigging-drawing-name">Zeichnung.pdf</div>
<div style="font-size: 12px; color: #666;" id="rigging-drawing-size">0 KB</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-small" onclick="downloadRiggingDrawing()" title="Herunterladen">
<i class="fas fa-download"></i>
</button>
<button class="btn btn-secondary btn-small" onclick="removeRiggingDrawing()" title="Entfernen">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Auftragsbestätigung Messe Upload -->
<div class="form-group" style="grid-column: 1 / -1;">
<label>Auftragsbestätigung Messe (PDF)</label>
<input type="file" id="edit-rigging-messe-confirmation-upload" accept=".pdf" onchange="handleRiggingMesseConfirmationUpload(event)" style="display: none;">
<button class="btn btn-secondary btn-small" onclick="document.getElementById('edit-rigging-messe-confirmation-upload').click()">
<i class="fas fa-upload"></i> Auftragsbestätigung hochladen
</button>
<div id="rigging-messe-confirmation-preview" style="display: none; margin-top: 12px; padding: 12px; background: #F5F5F5; border-radius: 6px; border: 2px solid #FFE000;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 8px;">
<i class="fas fa-file-pdf" style="color: #E74C3C; font-size: 24px;"></i>
<div>
<div style="font-weight: 600; color: #111111;" id="rigging-messe-confirmation-name">Auftragsbestätigung.pdf</div>
<div style="font-size: 12px; color: #666;" id="rigging-messe-confirmation-size">0 KB</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-small" onclick="downloadRiggingMesseConfirmation()" title="Herunterladen">
<i class="fas fa-download"></i>
</button>
<button class="btn btn-secondary btn-small" onclick="removeRiggingMesseConfirmation()" title="Entfernen">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<h4 style="font-size: 14px; font-weight: 600; color: #111111; margin: 20px 0 12px 0;">Messe Kontaktdaten</h4>
<div class="form-grid">
<div class="form-group">
<label>Vorname</label>
<input type="text" id="edit-rigging-messe-firstname" placeholder="Max">
</div>
<div class="form-group">
<label>Nachname</label>
<input type="text" id="edit-rigging-messe-lastname" placeholder="Mustermann">
</div>
<div class="form-group">
<label>E-Mail</label>
<input type="email" id="edit-rigging-messe-email" placeholder="max@messe.de">
</div>
<div class="form-group">
<label>Telefon</label>
<input type="tel" id="edit-rigging-messe-phone" placeholder="+49 123 456789">
</div>
</div>
</div>
<h3 class="section-title">Standfreigabe der Messe</h3>
<div class="form-group">
<div class="checklist-item">
<input type="checkbox" id="edit-stand-approval" onchange="toggleStandApprovalFields()">
<label for="edit-stand-approval">Standfreigabe vorhanden</label>
</div>
</div>
<div id="stand-approval-fields" style="display: none; margin-top: 16px;">
<div class="form-group">
<label>🗓️ Bestelldeadline</label>
<input type="date" id="edit-stand-approval-deadline">
</div>
<div class="form-group">
<label>Freigabedokument (PDF)</label>
<input type="file" id="edit-stand-approval-upload" accept=".pdf" onchange="handleStandApprovalUpload(event)" style="display: none;">
<button class="btn btn-secondary btn-small" onclick="document.getElementById('edit-stand-approval-upload').click()">
<i class="fas fa-upload"></i> PDF hochladen
</button>
<div id="stand-approval-preview" style="display: none; margin-top: 12px; padding: 12px; background: #F5F5F5; border-radius: 6px; border: 2px solid #FFE000;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 8px;">
<i class="fas fa-file-pdf" style="color: #E74C3C; font-size: 24px;"></i>
<div>
<div style="font-weight: 600; color: #111111;" id="stand-approval-name">Freigabe.pdf</div>
<div style="font-size: 12px; color: #666;" id="stand-approval-size">0 KB</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-small" onclick="downloadStandApproval()" title="Herunterladen">
<i class="fas fa-download"></i>
</button>
<button class="btn btn-secondary btn-small" onclick="removeStandApproval()" title="Entfernen">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<h3 class="section-title">Logistik</h3>
<div id="vehicles-container"></div>
<button class="btn btn-secondary btn-small" id="add-vehicle-btn">
<i class="fas fa-plus"></i> Fahrzeug hinzufügen
</button>
<h3 class="section-title">Hotelbuchungen</h3>
<div id="hotels-container"></div>
<button class="btn btn-secondary btn-small" id="add-hotel-btn">
<i class="fas fa-plus"></i> Hotel hinzufügen
</button>
<h3 class="section-title">
Personalplanung
<span style="font-size: 0.9rem; font-weight: 600; color: var(--text-secondary);" id="team-count">(0 Personen)</span>
</h3>
<div id="team-container"></div>
<button class="btn btn-secondary btn-small" id="add-team-btn">
<i class="fas fa-plus"></i> Mitarbeiter hinzufügen
</button>
</div>
<!-- Tab 4: Produktion -->
<div class="tab-pane" id="tab-3">
<h3 class="section-title">Materialien</h3>
<div class="list-header">
<strong>Bodenbeläge</strong>
<button class="btn btn-secondary btn-small" onclick="addMaterial('flooring')">
<i class="fas fa-plus"></i> Hinzufügen
</button>
</div>
<div id="flooring-container"></div>
<div class="list-header" style="margin-top: 24px;">
<strong>Standsystem / Messebau-Elemente</strong>
<button class="btn btn-secondary btn-small" onclick="addMaterial('standsystem')">
<i class="fas fa-plus"></i> Hinzufügen
</button>
</div>
<div id="standsystem-container"></div>
<!-- Druckproduktion -->
<h3 class="section-title" style="margin-top: 32px;">Druckproduktion</h3>
<div class="form-group">
<div class="checklist-item">
<input type="checkbox" id="druckproduktion-enabled" onchange="toggleDruckproduktion()">
<label for="druckproduktion-enabled">Druckproduktion vorhanden</label>
</div>
</div>
<div id="druckproduktion-fields" style="display: none; margin-top: 16px; padding: 16px; background: #F5F5F5; border-radius: 6px;">
<div class="form-grid">
<div class="form-group">
<label>Quadratmeter (m²)</label>
<input type="number" id="druckproduktion-qm" placeholder="0" min="0" step="0.01">
</div>
<div class="form-group">
<label>Blockout</label>
<input type="text" id="druckproduktion-blockout" placeholder="z.B. 270g/m²">
</div>
<div class="form-group">
<label>Anzahl Forexplatten</label>
<input type="number" id="druckproduktion-forexplatten" placeholder="0" min="0" step="1">
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label>🗓️ Bestelldeadline</label>
<input type="date" id="druckproduktion-deadline">
</div>
</div>
</div>
<div class="list-header" style="margin-top: 24px;">
<strong>Hardware / Technik</strong>
<button class="btn btn-secondary btn-small" onclick="addMaterial('hardware')">
<i class="fas fa-plus"></i> Hinzufügen
</button>
</div>
<div id="hardware-container"></div>
<div class="list-header" style="margin-top: 24px;">
<strong>Möbel & Ausstattung</strong>
<button class="btn btn-secondary btn-small" onclick="addMaterial('furniture')">
<i class="fas fa-plus"></i> Hinzufügen
</button>
</div>
<div id="furniture-container"></div>
<!-- Catering -->
<h3 class="section-title" style="margin-top: 32px;">Catering</h3>
<div class="form-group">
<div class="checklist-item">
<input type="checkbox" id="catering-barista" onchange="toggleBaristaFields()">
<label for="catering-barista">Barista-Service</label>
</div>
</div>
<div id="barista-fields" style="display: none; margin-top: 16px; padding: 16px; background: #F5F5F5; border-radius: 6px;">
<div class="form-grid">
<div class="form-group">
<label>Service-Paket</label>
<select id="catering-barista-package">
<option value="">-- Bitte wählen --</option>
<option value="1-pax-150">1 PAX - 150 Kaffee/pro Tag</option>
<option value="1-pax-300">1 PAX - 300 Kaffee/pro Tag</option>
<option value="2-pax-150">2 PAX - 150 Kaffee/pro Tag</option>
<option value="2-pax-300">2 PAX - 300 Kaffee/pro Tag</option>
</select>
</div>
<div class="form-group">
<label>Anzahl Messetage</label>
<input type="number" id="catering-days" min="1" placeholder="z.B. 3">
</div>
</div>
<div style="margin-top: 16px;">
<div class="list-header">
<strong>Catering-Personal</strong>
<button class="btn btn-secondary btn-small" onclick="addCateringStaff()">
<i class="fas fa-plus"></i> Person hinzufügen
</button>
</div>
<div id="catering-staff-container" style="margin-top: 12px;"></div>
</div>
</div>
<!-- Getränke -->
<div class="form-group" style="margin-top: 24px;">
<div class="checklist-item">
<input type="checkbox" id="catering-drinks" onchange="toggleDrinksFields()">
<label for="catering-drinks">Getränke</label>
</div>
</div>
<div id="drinks-fields" style="display: none; margin-top: 16px; padding: 16px; background: #F5F5F5; border-radius: 6px;">
<div class="form-group">
<label>Getränke-Bestellungen</label>
<textarea id="catering-drinks-details" rows="4" placeholder="z.B. 100 Flaschen Wasser, 50 Flaschen Cola, 30 Flaschen Saft..." style="width: 100%; padding: 12px; border: 1px solid #E0E0E0; border-radius: 6px; font-size: 14px; font-family: Inter, sans-serif; resize: vertical;"></textarea>
</div>
</div>
<!-- Food -->
<div class="form-group" style="margin-top: 24px;">
<div class="checklist-item">
<input type="checkbox" id="catering-food" onchange="toggleFoodFields()">
<label for="catering-food">Food</label>
</div>
</div>
<div id="food-fields" style="display: none; margin-top: 16px; padding: 16px; background: #F5F5F5; border-radius: 6px;">
<div class="form-group">
<label>Food-Bestellungen</label>
<textarea id="catering-food-details" rows="4" placeholder="z.B. 50 Belegte Brötchen, 30 Wraps, Obstplatten..." style="width: 100%; padding: 12px; border: 1px solid #E0E0E0; border-radius: 6px; font-size: 14px; font-family: Inter, sans-serif; resize: vertical;"></textarea>
</div>
</div>
<h3 class="section-title">Leadmagneten</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
<div class="checklist-item">
<input type="checkbox" id="lead-flyer">
<label for="lead-flyer">Flyer / Broschüren</label>
</div>
<div class="checklist-item">
<input type="checkbox" id="lead-giveaway">
<label for="lead-giveaway">Giveaways / Geschenke</label>
</div>
<div class="checklist-item">
<input type="checkbox" id="lead-tablet">
<label for="lead-tablet">Tablet für Datenerfassung</label>
</div>
<div class="checklist-item">
<input type="checkbox" id="lead-scanner">
<label for="lead-scanner">Badge-Scanner</label>
</div>
<div class="checklist-item">
<input type="checkbox" id="lead-contest">
<label for="lead-contest">Gewinnspiel</label>
</div>
<div class="checklist-item">
<input type="checkbox" id="lead-demo">
<label for="lead-demo">Produkt-Demos</label>
</div>
<div class="checklist-item">
<input type="checkbox" id="lead-photo-booth">
<label for="lead-photo-booth">KI Fotobox</label>
</div>
<div class="checklist-item">
<input type="checkbox" id="lead-vending-machine">
<label for="lead-vending-machine">Vending Machine</label>
</div>
<div class="checklist-item">
<input type="checkbox" id="lead-grab-arm">
<label for="lead-grab-arm">Greif-Arm</label>
</div>
</div>
</div>
<!-- Tab 5: Dokumente -->
<div class="tab-pane" id="tab-4">
<h3 class="section-title">Checkliste</h3>
<div id="checklist-container"></div>
<button class="btn btn-secondary btn-small" style="margin-top: 16px;" id="add-task-btn">
<i class="fas fa-plus"></i> Eigene Aufgabe hinzufügen
</button>
</div>
<!-- Tab 6: Standfotos -->
<div class="tab-pane" id="tab-5">
<h3 class="section-title">Standfotos</h3>
<div class="gallery-upload">
<input type="file" id="gallery-upload-input" accept="image/*" multiple>
<label for="gallery-upload-input" class="gallery-upload-label">
<i class="fas fa-cloud-upload-alt"></i> Bilder hochladen
</label>
<div class="gallery-upload-info">
<i class="fas fa-info-circle"></i> Max. 10 Bilder, je max. 2 MB (JPG, PNG, GIF)
</div>
</div>
<div id="gallery-container">
<div class="gallery-empty">
<i class="fas fa-images"></i>
<p>Noch keine Standfotos hochgeladen</p>
<p style="font-size: 0.85rem; margin-top: 8px;">Laden Sie Bilder Ihres Messestandes hoch, um eine Galerie zu erstellen.</p>
</div>
</div>
</div>
<!-- Tab 7: Standabnahme -->
<div class="tab-pane" id="tab-6">
<h3 class="section-title">Standabnahme</h3>
<p style="color: #666; margin-bottom: 24px;">
Dokumentieren Sie die Abnahme des fertigen Standes mit Fotos, Unterschrift und Bemerkungen.
</p>
<!-- Abnahmefotos Upload -->
<h4 style="font-size: 16px; font-weight: 600; color: #111111; margin-bottom: 12px;">
<i class="fas fa-camera"></i> Abnahmefotos
</h4>
<div class="gallery-upload" style="margin-bottom: 24px;">
<input type="file" id="acceptance-upload-input" accept="image/*" multiple>
<label for="acceptance-upload-input" class="gallery-upload-label">
<i class="fas fa-cloud-upload-alt"></i> Fotos hochladen
</label>
<div class="gallery-upload-info">
<i class="fas fa-info-circle"></i> Max. 10 Bilder, je max. 2 MB (JPG, PNG, GIF)
</div>
</div>
<div id="acceptance-gallery-container">
<div class="gallery-empty">
<i class="fas fa-camera"></i>
<p>Noch keine Abnahmefotos hochgeladen</p>
<p style="font-size: 0.85rem; margin-top: 8px;">Fotografieren Sie den fertigen Stand für die Abnahme.</p>
</div>
</div>
<!-- Unterschrift -->
<h4 style="font-size: 16px; font-weight: 600; color: #111111; margin: 32px 0 12px 0;">
<i class="fas fa-signature"></i> Unterschrift Kunde
</h4>
<div style="background: #F5F5F5; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
<canvas id="signature-canvas" width="600" height="200" style="width: 100%; max-width: 600px; height: auto; border: 2px solid #E0E0E0; background: white; border-radius: 6px; cursor: crosshair; display: block; touch-action: none;"></canvas>
<div style="display: flex; gap: 8px; margin-top: 12px;">
<button class="btn btn-secondary btn-small" onclick="clearSignature()">
<i class="fas fa-eraser"></i> Löschen
</button>
<button class="btn btn-secondary btn-small" onclick="saveSignature()">
<i class="fas fa-save"></i> Unterschrift speichern
</button>
</div>
<div id="signature-saved-indicator" style="display: none; margin-top: 12px; padding: 8px; background: #E8F5E9; border-radius: 4px; color: #2E7D32;">
<i class="fas fa-check-circle"></i> Unterschrift gespeichert
</div>
</div>
<!-- Bemerkungen -->
<h4 style="font-size: 16px; font-weight: 600; color: #111111; margin-bottom: 12px;">
<i class="fas fa-comment-alt"></i> Bemerkungen zur Abnahme
</h4>
<textarea id="acceptance-remarks" rows="6" placeholder="Notizen, Mängel, Besonderheiten..." style="width: 100%; padding: 12px; border: 1px solid #E0E0E0; border-radius: 6px; font-size: 14px; font-family: Inter, sans-serif; resize: vertical;"></textarea>
<!-- Abnahmedatum -->
<div style="margin-top: 24px;">
<label style="display: block; font-weight: 600; margin-bottom: 8px;">Abnahmedatum</label>
<input type="datetime-local" id="acceptance-date" style="padding: 8px; border: 1px solid #E0E0E0; border-radius: 4px; font-size: 14px;">
</div>
</div>
<!-- Tab 8: Weekly -->
<div class="tab-pane" id="tab-7">
<h3 class="section-title">Weekly Protokolle</h3>
<p style="color: #666; margin-bottom: 24px;">
Laden Sie wöchentliche Protokolle und Berichte hoch. Mehrere Dateien können gleichzeitig hochgeladen werden.
</p>
<!-- Protokoll Upload -->
<div class="form-group" style="margin-bottom: 24px;">
<label style="display: block; font-weight: 600; margin-bottom: 12px;">
<i class="fas fa-file-upload"></i> Protokolle hochladen (PDF)
</label>
<input type="file" id="weekly-protocol-upload" accept=".pdf" multiple style="display: none;">
<button class="btn btn-secondary" onclick="document.getElementById('weekly-protocol-upload').click()">
<i class="fas fa-cloud-upload-alt"></i> Protokolle hochladen
</button>
<div style="margin-top: 8px; font-size: 13px; color: #666;">
<i class="fas fa-info-circle"></i> Mehrere PDF-Dateien auswählbar, je max. 10 MB
</div>
</div>
<!-- Protokoll Liste -->
<h4 style="font-size: 16px; font-weight: 600; color: #111111; margin-bottom: 16px;">
<i class="fas fa-list"></i> Hochgeladene Protokolle
</h4>
<div id="weekly-protocols-container">
<div class="gallery-empty">
<i class="fas fa-file-pdf"></i>
<p>Noch keine Protokolle hochgeladen</p>
<p style="font-size: 0.85rem; margin-top: 8px;">Laden Sie wöchentliche Berichte und Protokolle hoch.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Toast -->
<div id="toast" class="toast">
<i class="fas fa-check-circle"></i>
<span id="toast-message">Gespeichert!</span>
</div>
<!-- Lightbox -->
<div id="lightbox" class="lightbox">
<div class="lightbox-content">
<button class="lightbox-close" id="lightbox-close">
<i class="fas fa-times"></i>
</button>
<button class="lightbox-nav prev" id="lightbox-prev">
<i class="fas fa-chevron-left"></i>
</button>
<button class="lightbox-nav next" id="lightbox-next">
<i class="fas fa-chevron-right"></i>
</button>
<img id="lightbox-image" class="lightbox-image" src="" alt="Standfoto">
</div>
</div>
<script>
// Test ob JavaScript läuft
console.log('🚀 24HRS Plattform startet...');
// Global Variables (use var to attach to window automatically)
var jobs = [];
var currentJobId = null;
// Initialize
document.addEventListener('DOMContentLoaded', function() {
console.log('✅ DOM geladen');
// Load jobs from localStorage
loadJobs();
console.log('✅ Jobs geladen:', jobs.length);
// Render dashboard
updateStats();
renderJobs();
console.log('✅ Dashboard gerendert');
// Back button
const backBtn = document.getElementById('back-btn');
if (backBtn) {
backBtn.addEventListener('click', showDashboard);
}
// Save button
const saveBtn = document.getElementById('save-job-btn');
if (saveBtn) {
saveBtn.addEventListener('click', saveCurrentJob);
}
// Duplicate button
const duplicateBtn = document.getElementById('duplicate-job-btn');
if (duplicateBtn) {
duplicateBtn.addEventListener('click', duplicateCurrentJob);
}
// Export button
const exportBtn = document.getElementById('export-job-btn');
if (exportBtn) {
exportBtn.addEventListener('click', exportJobToPDF);
}
// Delete button
const deleteBtn = document.getElementById('delete-job-btn');
if (deleteBtn) {
deleteBtn.addEventListener('click', deleteCurrentJob);
}
// New Job buttons - SIMPLIFIED VERSION
const newJobBtn = document.getElementById('new-job-btn');
if (newJobBtn) {
newJobBtn.onclick = function() {
console.log('🖱️ Neuer Job Button geklickt!');
if (typeof window.createNewJob === 'function') {
window.createNewJob();
} else {
console.error('❌ createNewJob Funktion nicht gefunden!');
}
};
console.log('✅ Button Event Listener registriert');
} else {
console.error('❌ Button nicht gefunden!');
}
const emptyNewJobBtn = document.getElementById('empty-new-job-btn');
if (emptyNewJobBtn) {
emptyNewJobBtn.onclick = function() {
console.log('🖱️ Empty Neuer Job Button geklickt!');
if (typeof window.createNewJob === 'function') {
window.createNewJob();
} else {
console.error('❌ createNewJob Funktion nicht gefunden!');
}
};
}
// Tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', function() {
const tabIndex = this.getAttribute('data-tab');
switchTab(tabIndex);
});
});
// Add hotel button
const addHotelBtn = document.getElementById('add-hotel-btn');
if (addHotelBtn) {
addHotelBtn.addEventListener('click', addHotel);
}
// Add vehicle button
const addVehicleBtn = document.getElementById('add-vehicle-btn');
if (addVehicleBtn) {
addVehicleBtn.addEventListener('click', addVehicle);
}
// Add team button
const addTeamBtn = document.getElementById('add-team-btn');
if (addTeamBtn) {
addTeamBtn.addEventListener('click', addTeamMember);
}
// Add task button
const addTaskBtn = document.getElementById('add-task-btn');
if (addTaskBtn) {
addTaskBtn.addEventListener('click', addChecklistItem);
}
// Stand image upload
const standImageUpload = document.getElementById('stand-image-upload');
if (standImageUpload) {
standImageUpload.addEventListener('change', handleStandImageUpload);
}
// Download stand image button
const downloadStandImageBtn = document.getElementById('download-stand-image-btn');
if (downloadStandImageBtn) {
downloadStandImageBtn.addEventListener('click', downloadStandImage);
}
// Remove stand image button
const removeStandImageBtn = document.getElementById('remove-stand-image-btn');
if (removeStandImageBtn) {
removeStandImageBtn.addEventListener('click', removeStandImage);
}
// Gallery upload
const galleryUploadInput = document.getElementById('gallery-upload-input');
if (galleryUploadInput) {
galleryUploadInput.addEventListener('change', handleGalleryUpload);
}
// Acceptance upload
const acceptanceUploadInput = document.getElementById('acceptance-upload-input');
if (acceptanceUploadInput) {
acceptanceUploadInput.addEventListener('change', handleAcceptancePhotosUpload);
}
// Weekly protocol upload
const weeklyProtocolUpload = document.getElementById('weekly-protocol-upload');
if (weeklyProtocolUpload) {
weeklyProtocolUpload.addEventListener('change', handleWeeklyProtocolUpload);
}
// Lightbox close
const lightboxClose = document.getElementById('lightbox-close');
if (lightboxClose) {
lightboxClose.addEventListener('click', closeLightbox);
}
// Lightbox navigation
const lightboxPrev = document.getElementById('lightbox-prev');
const lightboxNext = document.getElementById('lightbox-next');
if (lightboxPrev) {
lightboxPrev.addEventListener('click', () => navigateLightbox(-1));
}
if (lightboxNext) {
lightboxNext.addEventListener('click', () => navigateLightbox(1));
}
// Lightbox background click to close
const lightbox = document.getElementById('lightbox');
if (lightbox) {
lightbox.addEventListener('click', function(e) {
if (e.target === lightbox) {
closeLightbox();
}
});
}
// Navigation items
const navDashboard = document.getElementById('nav-dashboard');
const navCalendar = document.getElementById('nav-calendar');
if (navDashboard) {
navDashboard.addEventListener('click', showDashboardView);
}
if (navCalendar) {
navCalendar.addEventListener('click', showCalendarView);
}
// Calendar navigation
const calendarPrevBtn = document.getElementById('calendar-prev-month');
const calendarNextBtn = document.getElementById('calendar-next-month');
const calendarTodayBtn = document.getElementById('calendar-today');
if (calendarPrevBtn) {
calendarPrevBtn.addEventListener('click', () => changeMonth(-1));
}
if (calendarNextBtn) {
calendarNextBtn.addEventListener('click', () => changeMonth(1));
}
if (calendarTodayBtn) {
calendarTodayBtn.addEventListener('click', goToToday);
}
// Briefing upload event listeners
const briefingCustomerUpload = document.getElementById('briefing-customer-upload');
if (briefingCustomerUpload) {
briefingCustomerUpload.addEventListener('change', handleBriefingCustomerUpload);
}
const briefingBrandUpload = document.getElementById('briefing-brand-upload');
if (briefingBrandUpload) {
briefingBrandUpload.addEventListener('change', handleBriefingBrandUpload);
}
const briefingPlanUpload = document.getElementById('briefing-plan-upload');
if (briefingPlanUpload) {
briefingPlanUpload.addEventListener('change', handleBriefingPlanUpload);
}
const briefingAdditionalUpload = document.getElementById('briefing-additional-upload');
if (briefingAdditionalUpload) {
briefingAdditionalUpload.addEventListener('change', handleBriefingAdditionalUpload);
}
// Initialize signature pad for acceptance tab
initSignaturePad();
showToast('Plattform bereit! ✓');
});
// Load jobs from localStorage
function loadJobs() {
try {
const saved = localStorage.getItem('messebau_jobs');
if (saved) {
const parsed = JSON.parse(saved);
// Validate and fix loaded jobs
if (Array.isArray(parsed)) {
jobs = parsed.filter(job => {
// Ensure job has minimum required fields
if (!job || typeof job !== 'object') return false;
if (!job.id) {
console.warn('Job ohne ID gefunden, wird übersprungen');
return false;
}
return true;
});
console.log('Geladene Jobs:', jobs.length);
} else {
console.warn('Ungültiges Jobs-Array, wird zurückgesetzt');
jobs = [];
}
}
} catch (e) {
console.error('Fehler beim Laden:', e);
console.warn('LocalStorage wird zurückgesetzt');
jobs = [];
localStorage.removeItem('messebau_jobs');
}
}
// Save jobs to localStorage
function saveJobs() {
try {
localStorage.setItem('messebau_jobs', JSON.stringify(jobs));
console.log('Jobs gespeichert:', jobs.length);
} catch (e) {
console.error('Fehler beim Speichern:', e);
}
}
// Update statistics
function updateStats() {
const total = jobs.length;
const open = jobs.filter(j => j.status !== 'Abgeschlossen').length;
const thisWeek = 0; // Simplified for now
document.getElementById('stat-total').textContent = total;
document.getElementById('stat-open').textContent = open;
document.getElementById('stat-week').textContent = thisWeek;
}
// Render jobs
function renderJobs() {
const container = document.getElementById('jobs-container');
const emptyState = document.getElementById('empty-state');
if (!container || !emptyState) {
console.error('Jobs container oder empty state nicht gefunden!');
return;
}
if (jobs.length === 0) {
container.classList.add('hidden');
emptyState.classList.remove('hidden');
return;
}
container.classList.remove('hidden');
emptyState.classList.add('hidden');
container.innerHTML = jobs.map(job => `
<div class="job-card" onclick="openJob('${job.id}')">
${job.standImage ? `
<div class="job-card-image" style="height: 180px; overflow: hidden; background: var(--bg-light); display: flex; align-items: center; justify-content: center; border-bottom: 3px solid var(--primary-yellow);">
<img src="${job.standImage}" style="width: 100%; height: 100%; object-fit: cover;" alt="Messestand">
</div>
` : ''}
<div class="job-card-header">
<div class="job-id">Job #${job.id ? job.id.substring(0, 8) : 'unknown'}</div>
${job.project ? `<div class="job-project" style="font-size: 0.75rem; font-weight: 600; color: var(--primary-yellow); text-transform: uppercase; margin-bottom: 4px;"><i class="fas fa-folder"></i> ${job.project}</div>` : ''}
<div class="job-title">${job.customerName || 'Neuer Job'}</div>
<div class="job-subtitle">${job.eventName || 'Keine Messe'}</div>
</div>
<div class="job-card-body">
<div class="meta-item">
<i class="fas fa-map-marker-alt"></i>
<span>${job.location || 'Kein Ort'}</span>
</div>
<div class="meta-item">
<i class="fas fa-calendar"></i>
<span>${job.eventStart || 'Kein Datum'}</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar" style="width: 0%"></div>
</div>
</div>
</div>
`).join('');
}
// Default checklist items
const defaultChecklist = [
'Standaufbau-Genehmigung einholen',
'Technische Anschlüsse bestellen',
'Ausstellerausweise bestellen',
'Hotelbuchungen bestätigen',
'Team-Einweisung durchführen',
'Material & Werkzeug einpacken',
'Grafiken zur Produktion freigeben',
'Transport & Logistik koordinieren'
];
// Create new job (GLOBAL FUNCTION)
window.createNewJob = function() {
console.log('🆕 Erstelle neuen Job...');
try {
const newJob = {
id: Date.now().toString(36) + Math.random().toString(36).substring(2),
customerName: 'Neuer Kunde',
eventName: 'Neue Messe',
location: '',
project: '',
standType: '',
standSize: '',
standHeight: '',
status: 'Offen',
eventStart: '',
eventEnd: '',
setupStart: '',
setupEnd: '',
teardownStart: '',
teardownEnd: '',
standImage: null,
standImageName: null,
standGallery: [],
bookingPlatform: {
enabled: false,
portalLink: '',
email: '',
password: '',
deadline: ''
},
powerBooking: {
enabled: false,
kw: '',
distributor: '',
confirmationPdf: null,
confirmationPdfName: null,
confirmationPdfSize: null,
deadline: ''
},
rigging: {
enabled: false,
supplierFirstname: '',
supplierLastname: '',
supplierEmail: '',
supplierPhone: '',
quotePdf: null,
quotePdfName: null,
quotePdfSize: null,
drawingPdf: null,
drawingPdfName: null,
drawingPdfSize: null,
messeConfirmationPdf: null,
messeConfirmationPdfName: null,
messeConfirmationPdfSize: null,
messeFirstname: '',
messeLastname: '',
messeEmail: '',
messePhone: '',
deadline: ''
},
wlan: {
enabled: false,
type: '',
confirmationPdf: null,
confirmationPdfName: null,
confirmationPdfSize: null,
deadline: ''
},
cleaning: {
enabled: false,
confirmationPdf: null,
confirmationPdfName: null,
confirmationPdfSize: null,
deadline: ''
},
standApproval: {
enabled: false,
pdf: null,
pdfName: null,
pdfSize: null,
deadline: ''
},
acceptance: {
photos: [],
signature: null,
remarks: '',
date: ''
},
weekly: {
protocols: []
},
briefing: {
customerDoc: null,
customerDocName: null,
customerDocSize: null,
brandDoc: null,
brandDocName: null,
brandDocSize: null,
planDoc: null,
planDocName: null,
planDocSize: null,
additionalDoc: null,
additionalDocName: null,
additionalDocSize: null
},
catering: {
baristaEnabled: false,
baristaPackage: '',
days: '',
staff: [],
drinksEnabled: false,
drinksDetails: '',
foodEnabled: false,
foodDetails: ''
},
druckproduktion: {
enabled: false,
qm: '',
blockout: '',
forexplatten: '',
deadline: ''
},
createdAt: new Date().toISOString(),
// Initialize other fields
vehicles: [],
hotels: [],
team: [],
materials: {
bodenbelag: [],
standsystem: [],
hardware: [],
moebel: []
},
leadMagnets: {
flyer: false,
giveaway: false,
tablet: false,
scanner: false,
gewinnspiel: false,
demo: false,
photobooth: false,
vendingMachine: false,
grabArm: false
},
checklist: defaultChecklist.map(item => ({ text: item, checked: false }))
};
jobs.push(newJob);
console.log('✅ Job erstellt:', newJob.id);
saveJobs();
updateStats();
renderJobs();
showToast('✅ Neuer Job erstellt!');
// Open the job immediately for editing
openJob(newJob.id);
} catch (error) {
console.error('❌ Fehler:', error);
alert('Fehler beim Erstellen des Jobs: ' + error.message);
}
}
// Open job for editing
window.openJob = function(jobId) {
console.log('📂 Öffne Job:', jobId);
currentJobId = jobId;
const job = jobs.find(j => j.id === jobId);
if (!job) {
console.error('Job nicht gefunden:', jobId);
return;
}
// Hide dashboard, show detail view
const dashboardView = document.getElementById('dashboard-view');
const calendarView = document.getElementById('calendar-view');
const jobDetailView = document.getElementById('job-detail-view');
if (dashboardView) dashboardView.classList.add('hidden');
if (calendarView) calendarView.classList.add('hidden');
if (jobDetailView) jobDetailView.classList.remove('hidden');
const navDashboard = document.getElementById('nav-dashboard');
const navCalendar = document.getElementById('nav-calendar');
if (navDashboard) navDashboard.classList.remove('active');
if (navCalendar) navCalendar.classList.remove('active');
// Fill form
document.getElementById('job-detail-title').textContent = job.customerName || 'Neuer Job';
document.getElementById('edit-customer').value = job.customerName || '';
document.getElementById('edit-event').value = job.eventName || '';
document.getElementById('edit-location').value = job.location || '';
document.getElementById('edit-project').value = job.project || '';
document.getElementById('edit-stand-type').value = job.standType || '';
document.getElementById('edit-stand-size').value = job.standSize || '';
document.getElementById('edit-stand-height').value = job.standHeight || '';
document.getElementById('edit-status').value = job.status || 'Offen';
document.getElementById('edit-event-start').value = job.eventStart || '';
// Load time schedule fields
document.getElementById('edit-setup-start').value = job.setupStart || '';
document.getElementById('edit-setup-end').value = job.setupEnd || '';
document.getElementById('edit-event-end').value = job.eventEnd || '';
document.getElementById('edit-teardown-start').value = job.teardownStart || '';
document.getElementById('edit-teardown-end').value = job.teardownEnd || '';
// Load stand image if exists
const preview = document.getElementById('stand-image-preview');
const previewImg = document.getElementById('stand-image-preview-img');
if (job.standImage) {
previewImg.src = job.standImage;
preview.style.display = 'block';
} else {
preview.style.display = 'none';
}
// Load gallery
renderGallery();
// Load booking platform data
const bookingPlatform = job.bookingPlatform || { enabled: false, portalLink: '', email: '', password: '' };
document.getElementById('edit-booking-platform').checked = bookingPlatform.enabled || false;
document.getElementById('edit-booking-portal-link').value = bookingPlatform.portalLink || '';
document.getElementById('edit-booking-email').value = bookingPlatform.email || '';
document.getElementById('edit-booking-password').value = bookingPlatform.password || '';
document.getElementById('edit-booking-deadline').value = bookingPlatform.deadline || '';
// Show/hide booking platform fields based on checkbox
toggleBookingPlatformFields();
// Load power booking data
const powerBooking = job.powerBooking || { enabled: false, kw: '', distributor: '', confirmationPdf: null };
document.getElementById('edit-power-booking').checked = powerBooking.enabled || false;
document.getElementById('edit-power-kw').value = powerBooking.kw || '';
document.getElementById('edit-power-distributor').value = powerBooking.distributor || '';
document.getElementById('edit-power-deadline').value = powerBooking.deadline || '';
// Show/hide power booking fields and PDF preview
togglePowerBookingFields();
if (powerBooking.confirmationPdf) {
document.getElementById('power-confirmation-preview').style.display = 'block';
document.getElementById('power-confirmation-name').textContent = powerBooking.confirmationPdfName || 'Bestätigung.pdf';
document.getElementById('power-confirmation-size').textContent = ((powerBooking.confirmationPdfSize || 0) / 1024).toFixed(1) + ' KB';
} else {
document.getElementById('power-confirmation-preview').style.display = 'none';
}
// Load rigging data
const rigging = job.rigging || { enabled: false, supplierFirstname: '', supplierLastname: '', supplierEmail: '', supplierPhone: '' };
document.getElementById('edit-rigging').checked = rigging.enabled || false;
document.getElementById('edit-rigging-firstname').value = rigging.supplierFirstname || '';
document.getElementById('edit-rigging-lastname').value = rigging.supplierLastname || '';
document.getElementById('edit-rigging-email').value = rigging.supplierEmail || '';
document.getElementById('edit-rigging-phone').value = rigging.supplierPhone || '';
document.getElementById('edit-rigging-deadline').value = rigging.deadline || '';
// Show/hide rigging fields and PDF previews
toggleRiggingFields();
if (rigging.quotePdf) {
document.getElementById('rigging-quote-preview').style.display = 'block';
document.getElementById('rigging-quote-name').textContent = rigging.quotePdfName || 'Angebot.pdf';
document.getElementById('rigging-quote-size').textContent = ((rigging.quotePdfSize || 0) / 1024).toFixed(1) + ' KB';
} else {
document.getElementById('rigging-quote-preview').style.display = 'none';
}
if (rigging.drawingPdf) {
document.getElementById('rigging-drawing-preview').style.display = 'block';
document.getElementById('rigging-drawing-name').textContent = rigging.drawingPdfName || 'Zeichnung.pdf';
document.getElementById('rigging-drawing-size').textContent = ((rigging.drawingPdfSize || 0) / 1024).toFixed(1) + ' KB';
} else {
document.getElementById('rigging-drawing-preview').style.display = 'none';
}
if (rigging.messeConfirmationPdf) {
document.getElementById('rigging-messe-confirmation-preview').style.display = 'block';
document.getElementById('rigging-messe-confirmation-name').textContent = rigging.messeConfirmationPdfName || 'Auftragsbestätigung.pdf';
document.getElementById('rigging-messe-confirmation-size').textContent = ((rigging.messeConfirmationPdfSize || 0) / 1024).toFixed(1) + ' KB';
} else {
document.getElementById('rigging-messe-confirmation-preview').style.display = 'none';
}
// Load rigging messe contact data
document.getElementById('edit-rigging-messe-firstname').value = rigging.messeFirstname || '';
document.getElementById('edit-rigging-messe-lastname').value = rigging.messeLastname || '';
document.getElementById('edit-rigging-messe-email').value = rigging.messeEmail || '';
document.getElementById('edit-rigging-messe-phone').value = rigging.messePhone || '';
// Load WLAN data
const wlan = job.wlan || { enabled: false, type: '', confirmationPdf: null };
document.getElementById('edit-wlan').checked = wlan.enabled || false;
document.getElementById('edit-wlan-type').value = wlan.type || '';
document.getElementById('edit-wlan-deadline').value = wlan.deadline || '';
toggleWlanFields();
if (wlan.confirmationPdf) {
document.getElementById('wlan-confirmation-preview').style.display = 'block';
document.getElementById('wlan-confirmation-name').textContent = wlan.confirmationPdfName || 'Bestätigung.pdf';
document.getElementById('wlan-confirmation-size').textContent = ((wlan.confirmationPdfSize || 0) / 1024).toFixed(1) + ' KB';
} else {
document.getElementById('wlan-confirmation-preview').style.display = 'none';
}
// Load Cleaning data
const cleaning = job.cleaning || { enabled: false, confirmationPdf: null };
document.getElementById('edit-cleaning').checked = cleaning.enabled || false;
document.getElementById('edit-cleaning-deadline').value = cleaning.deadline || '';
toggleCleaningFields();
if (cleaning.confirmationPdf) {
document.getElementById('cleaning-confirmation-preview').style.display = 'block';
document.getElementById('cleaning-confirmation-name').textContent = cleaning.confirmationPdfName || 'Bestätigung.pdf';
document.getElementById('cleaning-confirmation-size').textContent = ((cleaning.confirmationPdfSize || 0) / 1024).toFixed(1) + ' KB';
} else {
document.getElementById('cleaning-confirmation-preview').style.display = 'none';
}
// Load Stand Approval data
const standApproval = job.standApproval || { enabled: false, pdf: null };
document.getElementById('edit-stand-approval').checked = standApproval.enabled || false;
document.getElementById('edit-stand-approval-deadline').value = standApproval.deadline || '';
toggleStandApprovalFields();
if (standApproval.pdf) {
document.getElementById('stand-approval-preview').style.display = 'block';
document.getElementById('stand-approval-name').textContent = standApproval.pdfName || 'Freigabe.pdf';
document.getElementById('stand-approval-size').textContent = ((standApproval.pdfSize || 0) / 1024).toFixed(1) + ' KB';
} else {
document.getElementById('stand-approval-preview').style.display = 'none';
}
// Render dynamic lists
renderVehicles(job.vehicles || []);
renderHotels(job.hotels || []);
renderTeam(job.team || []);
// Render materials
const materials = job.materials || {};
renderMaterials('flooring', materials.bodenbelag || materials.flooring || []);
renderMaterials('standsystem', materials.standsystem || []);
renderMaterials('hardware', materials.hardware || []);
renderMaterials('furniture', materials.moebel || materials.furniture || []);
// Load lead magnets
const leadMagnets = job.leadMagnets || {};
document.getElementById('lead-flyer').checked = leadMagnets.flyer || false;
document.getElementById('lead-giveaway').checked = leadMagnets.giveaway || false;
document.getElementById('lead-tablet').checked = leadMagnets.tablet || false;
document.getElementById('lead-scanner').checked = leadMagnets.scanner || false;
document.getElementById('lead-contest').checked = leadMagnets.gewinnspiel || false;
document.getElementById('lead-demo').checked = leadMagnets.demo || false;
document.getElementById('lead-photo-booth').checked = leadMagnets.photobooth || false;
document.getElementById('lead-vending-machine').checked = leadMagnets.vendingMachine || false;
document.getElementById('lead-grab-arm').checked = leadMagnets.grabArm || false;
// Load catering data
const catering = job.catering || { baristaEnabled: false, baristaPackage: '', days: '', staff: [] };
document.getElementById('catering-barista').checked = catering.baristaEnabled || false;
document.getElementById('catering-barista-package').value = catering.baristaPackage || '';
document.getElementById('catering-days').value = catering.days || '';
toggleBaristaFields();
renderCateringStaff();
// Load drinks data
document.getElementById('catering-drinks').checked = catering.drinksEnabled || false;
document.getElementById('catering-drinks-details').value = catering.drinksDetails || '';
toggleDrinksFields();
// Load food data
document.getElementById('catering-food').checked = catering.foodEnabled || false;
document.getElementById('catering-food-details').value = catering.foodDetails || '';
toggleFoodFields();
// Load druckproduktion data
const druckproduktion = job.druckproduktion || { enabled: false, qm: '', blockout: '', forexplatten: '' };
document.getElementById('druckproduktion-enabled').checked = druckproduktion.enabled || false;
document.getElementById('druckproduktion-qm').value = druckproduktion.qm || '';
document.getElementById('druckproduktion-blockout').value = druckproduktion.blockout || '';
document.getElementById('druckproduktion-forexplatten').value = druckproduktion.forexplatten || '';
document.getElementById('druckproduktion-deadline').value = druckproduktion.deadline || '';
toggleDruckproduktion();
// Load acceptance data
const acceptance = job.acceptance || { photos: [], signature: null, remarks: '', date: '' };
document.getElementById('acceptance-remarks').value = acceptance.remarks || '';
document.getElementById('acceptance-date').value = acceptance.date || '';
// Load signature if exists
if (acceptance.signature) {
const canvas = document.getElementById('signature-canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0);
document.getElementById('signature-saved-indicator').style.display = 'block';
};
img.src = acceptance.signature;
} else {
clearSignature();
}
// Render acceptance gallery
renderAcceptanceGallery();
// Render weekly protocols
renderWeeklyProtocols();
// Load briefing data
const briefing = job.briefing || {};
// Customer briefing
if (briefing.customerDoc) {
document.getElementById('briefing-customer-preview').style.display = 'block';
document.getElementById('briefing-customer-name').textContent = briefing.customerDocName || 'Dokument.pdf';
document.getElementById('briefing-customer-size').textContent = ((briefing.customerDocSize || 0) / 1024).toFixed(1) + ' KB';
} else {
document.getElementById('briefing-customer-preview').style.display = 'none';
}
// Brandbook
if (briefing.brandDoc) {
document.getElementById('briefing-brand-preview').style.display = 'block';
document.getElementById('briefing-brand-name').textContent = briefing.brandDocName || 'Brandbook.pdf';
document.getElementById('briefing-brand-size').textContent = ((briefing.brandDocSize || 0) / 1024).toFixed(1) + ' KB';
} else {
document.getElementById('briefing-brand-preview').style.display = 'none';
}
// Standplan
if (briefing.planDoc) {
document.getElementById('briefing-plan-preview').style.display = 'block';
document.getElementById('briefing-plan-name').textContent = briefing.planDocName || 'Standplan.pdf';
document.getElementById('briefing-plan-size').textContent = ((briefing.planDocSize || 0) / 1024).toFixed(1) + ' KB';
} else {
document.getElementById('briefing-plan-preview').style.display = 'none';
}
// Additional documents
if (briefing.additionalDoc) {
document.getElementById('briefing-additional-preview').style.display = 'block';
document.getElementById('briefing-additional-name').textContent = briefing.additionalDocName || 'Dokument.pdf';
document.getElementById('briefing-additional-size').textContent = ((briefing.additionalDocSize || 0) / 1024).toFixed(1) + ' KB';
} else {
document.getElementById('briefing-additional-preview').style.display = 'none';
}
console.log('✅ Job geöffnet');
}
// Show dashboard
window.showDashboard = function() {
console.log('🏠 Zurück zum Dashboard');
const jobDetailView = document.getElementById('job-detail-view');
const calendarView = document.getElementById('calendar-view');
const dashboardView = document.getElementById('dashboard-view');
const navDashboard = document.getElementById('nav-dashboard');
const navCalendar = document.getElementById('nav-calendar');
if (jobDetailView) jobDetailView.classList.add('hidden');
if (calendarView) calendarView.classList.add('hidden');
if (dashboardView) dashboardView.classList.remove('hidden');
if (navDashboard) navDashboard.classList.add('active');
if (navCalendar) navCalendar.classList.remove('active');
currentJobId = null;
updateStats();
renderJobs();
}
// Save current job
window.saveCurrentJob = function() {
if (!currentJobId) return;
console.log('💾 Speichere Job:', currentJobId);
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
// Update job data
job.customerName = document.getElementById('edit-customer').value;
job.eventName = document.getElementById('edit-event').value;
job.location = document.getElementById('edit-location').value;
job.project = document.getElementById('edit-project').value;
job.standType = document.getElementById('edit-stand-type').value;
job.standSize = document.getElementById('edit-stand-size').value;
job.standHeight = document.getElementById('edit-stand-height').value;
job.status = document.getElementById('edit-status').value;
job.eventStart = document.getElementById('edit-event-start').value;
// Update time schedule
job.setupStart = document.getElementById('edit-setup-start').value;
job.setupEnd = document.getElementById('edit-setup-end').value;
job.eventEnd = document.getElementById('edit-event-end').value;
job.teardownStart = document.getElementById('edit-teardown-start').value;
job.teardownEnd = document.getElementById('edit-teardown-end').value;
// Update booking platform data
if (!job.bookingPlatform) {
job.bookingPlatform = {};
}
job.bookingPlatform.enabled = document.getElementById('edit-booking-platform').checked;
job.bookingPlatform.portalLink = document.getElementById('edit-booking-portal-link').value;
job.bookingPlatform.email = document.getElementById('edit-booking-email').value;
job.bookingPlatform.password = document.getElementById('edit-booking-password').value;
job.bookingPlatform.deadline = document.getElementById('edit-booking-deadline').value;
// Update power booking data
if (!job.powerBooking) {
job.powerBooking = {};
}
job.powerBooking.enabled = document.getElementById('edit-power-booking').checked;
job.powerBooking.kw = document.getElementById('edit-power-kw').value;
job.powerBooking.distributor = document.getElementById('edit-power-distributor').value;
job.powerBooking.deadline = document.getElementById('edit-power-deadline').value;
// PDF is handled separately in handlePowerConfirmationUpload
// Update rigging data
if (!job.rigging) {
job.rigging = {};
}
job.rigging.enabled = document.getElementById('edit-rigging').checked;
job.rigging.supplierFirstname = document.getElementById('edit-rigging-firstname').value;
job.rigging.supplierLastname = document.getElementById('edit-rigging-lastname').value;
job.rigging.supplierEmail = document.getElementById('edit-rigging-email').value;
job.rigging.supplierPhone = document.getElementById('edit-rigging-phone').value;
job.rigging.deadline = document.getElementById('edit-rigging-deadline').value;
job.rigging.messeFirstname = document.getElementById('edit-rigging-messe-firstname').value;
job.rigging.messeLastname = document.getElementById('edit-rigging-messe-lastname').value;
job.rigging.messeEmail = document.getElementById('edit-rigging-messe-email').value;
job.rigging.messePhone = document.getElementById('edit-rigging-messe-phone').value;
// PDFs are handled separately in upload functions
// Update WLAN data
if (!job.wlan) {
job.wlan = {};
}
job.wlan.enabled = document.getElementById('edit-wlan').checked;
job.wlan.type = document.getElementById('edit-wlan-type').value;
job.wlan.deadline = document.getElementById('edit-wlan-deadline').value;
// PDF is handled separately
// Update Cleaning data
if (!job.cleaning) {
job.cleaning = {};
}
job.cleaning.enabled = document.getElementById('edit-cleaning').checked;
job.cleaning.deadline = document.getElementById('edit-cleaning-deadline').value;
// PDF is handled separately
// Update Stand Approval data
if (!job.standApproval) {
job.standApproval = {};
}
job.standApproval.enabled = document.getElementById('edit-stand-approval').checked;
job.standApproval.deadline = document.getElementById('edit-stand-approval-deadline').value;
// PDF is handled separately
// Update lead magnets
if (!job.leadMagnets) {
job.leadMagnets = {};
}
job.leadMagnets.flyer = document.getElementById('lead-flyer').checked;
job.leadMagnets.giveaway = document.getElementById('lead-giveaway').checked;
job.leadMagnets.tablet = document.getElementById('lead-tablet').checked;
job.leadMagnets.scanner = document.getElementById('lead-scanner').checked;
job.leadMagnets.gewinnspiel = document.getElementById('lead-contest').checked;
job.leadMagnets.demo = document.getElementById('lead-demo').checked;
job.leadMagnets.photobooth = document.getElementById('lead-photo-booth').checked;
job.leadMagnets.vendingMachine = document.getElementById('lead-vending-machine').checked;
job.leadMagnets.grabArm = document.getElementById('lead-grab-arm').checked;
// Update catering data
if (!job.catering) {
job.catering = { baristaEnabled: false, baristaPackage: '', days: '', staff: [] };
}
job.catering.baristaEnabled = document.getElementById('catering-barista').checked;
job.catering.baristaPackage = document.getElementById('catering-barista-package').value;
job.catering.days = document.getElementById('catering-days').value;
job.catering.drinksEnabled = document.getElementById('catering-drinks').checked;
job.catering.drinksDetails = document.getElementById('catering-drinks-details').value;
job.catering.foodEnabled = document.getElementById('catering-food').checked;
job.catering.foodDetails = document.getElementById('catering-food-details').value;
// Staff is handled separately in updateCateringStaff
// Update druckproduktion data
if (!job.druckproduktion) {
job.druckproduktion = { enabled: false, qm: '', blockout: '', forexplatten: '' };
}
job.druckproduktion.enabled = document.getElementById('druckproduktion-enabled').checked;
job.druckproduktion.qm = document.getElementById('druckproduktion-qm').value;
job.druckproduktion.blockout = document.getElementById('druckproduktion-blockout').value;
job.druckproduktion.forexplatten = document.getElementById('druckproduktion-forexplatten').value;
job.druckproduktion.deadline = document.getElementById('druckproduktion-deadline').value;
// Update Acceptance data
if (!job.acceptance) {
job.acceptance = { photos: [], signature: null, remarks: '', date: '' };
}
job.acceptance.remarks = document.getElementById('acceptance-remarks').value;
job.acceptance.date = document.getElementById('acceptance-date').value;
// Photos and signature are handled separately in their functions
saveJobs();
showToast('✅ Job gespeichert!');
console.log('✅ Job gespeichert:', job);
}
// Delete current job
window.deleteCurrentJob = function() {
if (!currentJobId) return;
if (!confirm('Job wirklich löschen?')) return;
console.log('🗑️ Lösche Job:', currentJobId);
jobs = jobs.filter(j => j.id !== currentJobId);
saveJobs();
showDashboard();
showToast('✅ Job gelöscht!');
}
// Duplicate Current Job
window.duplicateCurrentJob = function() {
if (!currentJobId) return;
console.log('📋 Dupliziere Job:', currentJobId);
// Finde den aktuellen Job
const originalJob = jobs.find(j => j.id === currentJobId);
if (!originalJob) {
showToast('❌ Job nicht gefunden');
return;
}
// Speichere aktuellen Job zuerst
saveCurrentJob();
// Erstelle eine tiefe Kopie des Jobs
const duplicatedJob = JSON.parse(JSON.stringify(originalJob));
// Generiere neue ID
duplicatedJob.id = Date.now().toString(36) + Math.random().toString(36).substring(2);
// Aktualisiere Namen für Kopie
duplicatedJob.customerName = (duplicatedJob.customerName || 'Neuer Kunde') + ' (Kopie)';
// Aktualisiere Zeitstempel
duplicatedJob.createdAt = new Date().toISOString();
// Füge duplizierten Job zur Liste hinzu
jobs.push(duplicatedJob);
console.log('✅ Job dupliziert:', duplicatedJob.id);
// Speichere alle Jobs
saveJobs();
// Aktualisiere UI
updateStats();
renderJobs();
// Öffne den duplizierten Job
showToast('✅ Job dupliziert!');
openJob(duplicatedJob.id);
}
// Export Job to PDF
function exportJobToPDF() {
const job = jobs.find(j => j.id === currentJobId);
if (!job) {
showToast('❌ Kein Job ausgewählt');
return;
}
console.log('📄 Exportiere Job zu PDF:', job.id);
// Zuerst Job speichern
saveCurrentJob();
// Neues Fenster für PDF-Ansicht erstellen
const printWindow = window.open('', '_blank');
// HTML-Inhalt generieren
let html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Jobzettel - ${job.customerName} - ${job.eventName}</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
font-size: 11pt;
line-height: 1.5;
color: #111111;
padding: 20mm;
background: white;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 3px solid #FFE000;
}
.logo {
font-size: 36pt;
font-weight: 700;
color: #111111;
letter-spacing: 2px;
}
.job-title {
font-size: 18pt;
font-weight: 600;
color: #111111;
margin-bottom: 10px;
}
.section {
margin-bottom: 30px;
page-break-inside: avoid;
}
.section-title {
font-size: 14pt;
font-weight: 600;
color: #111111;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 2px solid #FFE000;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 15px;
}
.info-item {
padding: 8px;
background: #F5F5F5;
border-radius: 4px;
}
.info-label {
font-weight: 600;
font-size: 9pt;
color: #666;
margin-bottom: 2px;
}
.info-value {
font-size: 11pt;
color: #111111;
}
.list-item {
padding: 8px;
background: #F5F5F5;
border-radius: 4px;
margin-bottom: 8px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
}
th, td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #E0E0E0;
}
th {
background: #FFE000;
font-weight: 600;
font-size: 10pt;
}
td {
font-size: 10pt;
}
.photo-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 15px;
}
.photo-item {
break-inside: avoid;
}
.photo-item img {
width: 100%;
height: auto;
border-radius: 4px;
border: 1px solid #E0E0E0;
}
.checkbox {
display: inline-block;
width: 12pt;
height: 12pt;
border: 2px solid #111111;
border-radius: 2px;
margin-right: 8px;
vertical-align: middle;
}
.checkbox.checked {
background: #FFE000;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 2px solid #E0E0E0;
text-align: center;
font-size: 9pt;
color: #666;
}
@media print {
body {
padding: 15mm;
}
.section {
page-break-inside: avoid;
}
.no-print {
display: none;
}
}
.print-button {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 24px;
background: #FFE000;
color: #111111;
border: none;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
font-size: 12pt;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
z-index: 1000;
}
.print-button:hover {
background: #FFD700;
}
</style>
</head>
<body>
<button class="print-button no-print" onclick="window.print()">🖨️ Drucken / Als PDF speichern</button>
<div class="header">
<div class="logo">24HRS</div>
<div>
<div class="job-title">${job.customerName || 'Neuer Kunde'}</div>
<div style="font-size: 12pt; color: #666;">${job.eventName || 'Neue Messe'}</div>
</div>
</div>
`;
// TITELBILD - Messestand Hauptbild (falls vorhanden)
if (job.standImage) {
html += `
<div class="section" style="text-align: center; margin-bottom: 40px; page-break-inside: avoid;">
<img src="${job.standImage}" alt="Messestand" style="max-width: 100%; height: auto; max-height: 400px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); border: 3px solid #FFE000;">
${job.standImageName ? `<p style="margin-top: 10px; font-size: 10pt; color: #666; font-style: italic;">${job.standImageName}</p>` : ''}
</div>
`;
}
// 1. STAMMDATEN & TERMINE
html += `
<div class="section">
<h2 class="section-title">1. Stammdaten & Termine</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Kunde</div>
<div class="info-value">${job.customerName || '-'}</div>
</div>
<div class="info-item">
<div class="info-label">Messe</div>
<div class="info-value">${job.eventName || '-'}</div>
</div>
<div class="info-item">
<div class="info-label">Ort</div>
<div class="info-value">${job.location || '-'}</div>
</div>
<div class="info-item">
<div class="info-label">Projekt</div>
<div class="info-value">${job.project || '-'}</div>
</div>
<div class="info-item">
<div class="info-label">Standtyp</div>
<div class="info-value">${job.standType || '-'}</div>
</div>
<div class="info-item">
<div class="info-label">Standgröße</div>
<div class="info-value">${job.standSize || '-'}</div>
</div>
<div class="info-item">
<div class="info-label">Standhöhe</div>
<div class="info-value">${job.standHeight || '-'}</div>
</div>
<div class="info-item">
<div class="info-label">Status</div>
<div class="info-value">${job.status || '-'}</div>
</div>
</div>
<h3 style="font-size: 12pt; font-weight: 600; margin: 20px 0 10px;">Termine</h3>
<table>
<thead>
<tr>
<th>Phase</th>
<th>Von</th>
<th>Bis</th>
</tr>
</thead>
<tbody>
<tr>
<td>Aufbau</td>
<td>${job.setupStart ? new Date(job.setupStart).toLocaleDateString('de-DE') : '-'}</td>
<td>${job.setupEnd ? new Date(job.setupEnd).toLocaleDateString('de-DE') : '-'}</td>
</tr>
<tr>
<td>Messe</td>
<td>${job.eventStart ? new Date(job.eventStart).toLocaleDateString('de-DE') : '-'}</td>
<td>${job.eventEnd ? new Date(job.eventEnd).toLocaleDateString('de-DE') : '-'}</td>
</tr>
<tr>
<td>Abbau</td>
<td>${job.teardownStart ? new Date(job.teardownStart).toLocaleDateString('de-DE') : '-'}</td>
<td>${job.teardownEnd ? new Date(job.teardownEnd).toLocaleDateString('de-DE') : '-'}</td>
</tr>
</tbody>
</table>
${job.notes ? `
<h3 style="font-size: 12pt; font-weight: 600; margin: 20px 0 10px;">Notizen</h3>
<div class="info-item">
<div class="info-value" style="white-space: pre-wrap;">${job.notes}</div>
</div>
` : ''}
</div>
`;
// 2. ORGANISATION
html += `<div class="section">`;
html += `<h2 class="section-title">2. Organisation</h2>`;
// Buchungsplattform
if (job.bookingPlatform && job.bookingPlatform.enabled) {
html += `
<h3 style="font-size: 12pt; font-weight: 600; margin: 15px 0 10px;">Buchungsplattform</h3>
<div class="info-item">
<div class="info-label">Portal-Link</div>
<div class="info-value">${job.bookingPlatform.portalLink || '-'}</div>
</div>
<div class="info-item" style="margin-top: 8px;">
<div class="info-label">Login</div>
<div class="info-value">${job.bookingPlatform.email || '-'}</div>
</div>
`;
}
// Strombuchung
if (job.powerBooking && job.powerBooking.enabled) {
html += `
<h3 style="font-size: 12pt; font-weight: 600; margin: 15px 0 10px;">Strombuchung</h3>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Leistung</div>
<div class="info-value">${job.powerBooking.kw || '-'} kW</div>
</div>
<div class="info-item">
<div class="info-label">Verteiler</div>
<div class="info-value">${job.powerBooking.distributor || '-'}</div>
</div>
</div>
`;
}
// WLAN
if (job.wlan && job.wlan.enabled) {
html += `
<h3 style="font-size: 12pt; font-weight: 600; margin: 15px 0 10px;">WLAN</h3>
<div class="info-item">
<div class="info-label">Typ</div>
<div class="info-value">${job.wlan.type || '-'}</div>
</div>
`;
}
// Rigging
if (job.rigging && job.rigging.enabled) {
html += `
<h3 style="font-size: 12pt; font-weight: 600; margin: 15px 0 10px;">Rigging / Truss</h3>
<div class="info-item">
<div class="info-label">Lieferant</div>
<div class="info-value">${job.rigging.supplierFirstname || ''} ${job.rigging.supplierLastname || ''}</div>
</div>
${job.rigging.messeFirstname || job.rigging.messeLastname ? `
<div class="info-item" style="margin-top: 8px;">
<div class="info-label">Messe-Kontakt</div>
<div class="info-value">${job.rigging.messeFirstname || ''} ${job.rigging.messeLastname || ''}</div>
</div>
` : ''}
`;
}
// Fahrzeuge
if (job.vehicles && job.vehicles.length > 0) {
html += `
<h3 style="font-size: 12pt; font-weight: 600; margin: 15px 0 10px;">Logistik / Fahrzeuge</h3>
<table>
<thead>
<tr>
<th>Typ</th>
<th>Kennzeichen</th>
<th>Fahrer</th>
<th>Abfahrt</th>
</tr>
</thead>
<tbody>
`;
job.vehicles.forEach(vehicle => {
html += `
<tr>
<td>${vehicle.type || '-'}</td>
<td>${vehicle.plate || '-'}</td>
<td>${vehicle.driver || '-'}</td>
<td>${vehicle.departure ? new Date(vehicle.departure).toLocaleDateString('de-DE') : '-'}</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
}
// Hotels
if (job.hotels && job.hotels.length > 0) {
html += `
<h3 style="font-size: 12pt; font-weight: 600; margin: 15px 0 10px;">Hotelbuchungen</h3>
<table>
<thead>
<tr>
<th>Mitarbeiter</th>
<th>Hotel</th>
<th>Check-in</th>
<th>Check-out</th>
</tr>
</thead>
<tbody>
`;
job.hotels.forEach(hotel => {
html += `
<tr>
<td>${hotel.employee || '-'}</td>
<td>${hotel.name || '-'}</td>
<td>${hotel.checkin ? new Date(hotel.checkin).toLocaleDateString('de-DE') : '-'}</td>
<td>${hotel.checkout ? new Date(hotel.checkout).toLocaleDateString('de-DE') : '-'}</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
}
// Team
if (job.team && job.team.length > 0) {
html += `
<h3 style="font-size: 12pt; font-weight: 600; margin: 15px 0 10px;">Personalplanung</h3>
<table>
<thead>
<tr>
<th>Name</th>
<th>Funktion</th>
<th>Telefon</th>
</tr>
</thead>
<tbody>
`;
job.team.forEach(member => {
html += `
<tr>
<td>${member.name || '-'}</td>
<td>${member.role || '-'}</td>
<td>${member.phone || '-'}</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
}
html += `</div>`; // Ende Organisation
// 3. PRODUKTION
html += `<div class="section">`;
html += `<h2 class="section-title">3. Produktion</h2>`;
// Materialien
const materialCategories = [
{ key: 'bodenbelag', title: 'Bodenbeläge' },
{ key: 'standsystem', title: 'Standsystem / Messebau-Elemente' },
{ key: 'hardware', title: 'Hardware / Technik' },
{ key: 'moebel', title: 'Möbel & Ausstattung' }
];
materialCategories.forEach(category => {
const materials = job.materials && job.materials[category.key];
if (materials && materials.length > 0) {
html += `
<h3 style="font-size: 12pt; font-weight: 600; margin: 15px 0 10px;">${category.title}</h3>
<table>
<thead>
<tr>
<th>Artikel</th>
<th>Menge</th>
<th>Einheit</th>
</tr>
</thead>
<tbody>
`;
materials.forEach(material => {
html += `
<tr>
<td>${material.name || '-'}</td>
<td>${material.quantity || '-'}</td>
<td>${material.unit || '-'}</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
}
});
// Druckproduktion
if (job.druckproduktion && job.druckproduktion.enabled) {
html += `
<h3 style="font-size: 12pt; font-weight: 600; margin: 15px 0 10px;">Druckproduktion</h3>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Quadratmeter</div>
<div class="info-value">${job.druckproduktion.qm || '-'} m²</div>
</div>
<div class="info-item">
<div class="info-label">Blockout</div>
<div class="info-value">${job.druckproduktion.blockout || '-'}</div>
</div>
<div class="info-item">
<div class="info-label">Forexplatten</div>
<div class="info-value">${job.druckproduktion.forexplatten || '-'}</div>
</div>
</div>
`;
}
// Catering
if (job.catering) {
let hasCatering = false;
let cateringHtml = `<h3 style="font-size: 12pt; font-weight: 600; margin: 15px 0 10px;">Catering</h3>`;
if (job.catering.baristaEnabled) {
hasCatering = true;
cateringHtml += `
<div class="info-item">
<div class="info-label">Barista-Service</div>
<div class="info-value">${job.catering.baristaPackage || '-'} (${job.catering.days || '-'} Tage)</div>
</div>
`;
}
if (job.catering.drinksEnabled && job.catering.drinksDetails) {
hasCatering = true;
cateringHtml += `
<div class="info-item" style="margin-top: 8px;">
<div class="info-label">Getränke</div>
<div class="info-value" style="white-space: pre-wrap;">${job.catering.drinksDetails}</div>
</div>
`;
}
if (job.catering.foodEnabled && job.catering.foodDetails) {
hasCatering = true;
cateringHtml += `
<div class="info-item" style="margin-top: 8px;">
<div class="info-label">Food</div>
<div class="info-value" style="white-space: pre-wrap;">${job.catering.foodDetails}</div>
</div>
`;
}
if (hasCatering) {
html += cateringHtml;
}
}
// Lead-Magnets
if (job.leadMagnets) {
const activeMagnets = [];
if (job.leadMagnets.flyer) activeMagnets.push('Flyer / Broschüren');
if (job.leadMagnets.giveaway) activeMagnets.push('Giveaways');
if (job.leadMagnets.tablet) activeMagnets.push('Tablet');
if (job.leadMagnets.scanner) activeMagnets.push('Badge-Scanner');
if (job.leadMagnets.gewinnspiel) activeMagnets.push('Gewinnspiel');
if (job.leadMagnets.demo) activeMagnets.push('Produkt-Demos');
if (activeMagnets.length > 0) {
html += `
<h3 style="font-size: 12pt; font-weight: 600; margin: 15px 0 10px;">Lead-Magneten</h3>
<div class="info-item">
<div class="info-value">${activeMagnets.join(', ')}</div>
</div>
`;
}
}
html += `</div>`; // Ende Produktion
// 4. STANDFOTOS
if (job.standGallery && job.standGallery.length > 0) {
html += `
<div class="section">
<h2 class="section-title">4. Standfotos</h2>
<div class="photo-grid">
`;
job.standGallery.forEach((photo, index) => {
html += `
<div class="photo-item">
<img src="${photo.data}" alt="Standfoto ${index + 1}">
</div>
`;
});
html += `
</div>
</div>
`;
}
// 5. DOKUMENTE (Checkliste)
if (job.checklist && job.checklist.length > 0) {
html += `
<div class="section">
<h2 class="section-title">5. Dokumente & Checkliste</h2>
`;
job.checklist.forEach(item => {
html += `
<div class="list-item">
<span class="checkbox ${item.checked ? 'checked' : ''}"></span>
${item.text}
</div>
`;
});
html += `
</div>
`;
}
// Footer
html += `
<div class="footer">
<div>24HRS Messebau Projektmanagement</div>
<div style="margin-top: 8px;">Erstellt am: ${new Date().toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}</div>
</div>
</body>
</html>
`;
// HTML in neues Fenster schreiben
printWindow.document.write(html);
printWindow.document.close();
// Warten bis Bilder geladen sind, dann Druckdialog öffnen
printWindow.onload = function() {
setTimeout(() => {
printWindow.focus();
}, 500);
};
showToast('✅ PDF-Ansicht geöffnet');
}
// Tab switching
function switchTab(tabIndex) {
// Convert to number if string
const targetIndex = parseInt(tabIndex, 10);
document.querySelectorAll('.tab-btn').forEach((btn, index) => {
if (index === targetIndex) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
document.querySelectorAll('.tab-pane').forEach((pane, index) => {
if (index === targetIndex) {
pane.classList.add('active');
} else {
pane.classList.remove('active');
}
});
}
// Hotels
function addHotel() {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.hotels) job.hotels = [];
job.hotels.push({
id: Date.now().toString(36) + Math.random().toString(36).substr(2),
employee: '',
hotel: '',
checkin: '',
checkout: '',
confirmationFile: null,
confirmationFileName: ''
});
renderHotels(job.hotels);
saveJobs();
}
function renderHotels(hotels) {
const container = document.getElementById('hotels-container');
if (!hotels || hotels.length === 0) {
container.innerHTML = '<p style="color: var(--text-secondary); padding: 12px;">Keine Hotels hinzugefügt</p>';
return;
}
container.innerHTML = hotels.map((hotel, index) => `
<div class="list-item">
<div class="form-grid">
<div class="form-group">
<label>Mitarbeiter</label>
<input type="text" value="${hotel.employee || ''}" onchange="updateHotel(${index}, 'employee', this.value)">
</div>
<div class="form-group">
<label>Hotel</label>
<input type="text" value="${hotel.hotel || ''}" onchange="updateHotel(${index}, 'hotel', this.value)">
</div>
<div class="form-group">
<label>Check-in</label>
<input type="date" value="${hotel.checkin || ''}" onchange="updateHotel(${index}, 'checkin', this.value)">
</div>
<div class="form-group">
<label>Check-out</label>
<input type="date" value="${hotel.checkout || ''}" onchange="updateHotel(${index}, 'checkout', this.value)">
</div>
</div>
<button class="icon-btn danger" onclick="removeHotel(${index})" style="margin-top: 8px;">
<i class="fas fa-trash"></i> Entfernen
</button>
</div>
`).join('');
}
function updateHotel(index, field, value) {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.hotels[index]) return;
job.hotels[index][field] = value;
saveJobs();
}
function removeHotel(index) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
job.hotels.splice(index, 1);
renderHotels(job.hotels);
saveJobs();
}
// Team
function addTeamMember() {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.team) job.team = [];
job.team.push({
id: Date.now().toString(36) + Math.random().toString(36).substr(2),
name: '',
role: '',
phone: ''
});
renderTeam(job.team);
saveJobs();
}
function renderTeam(team) {
const container = document.getElementById('team-container');
const countEl = document.getElementById('team-count');
countEl.textContent = `(${team ? team.length : 0} Personen)`;
if (!team || team.length === 0) {
container.innerHTML = '<p style="color: var(--text-secondary); padding: 12px;">Keine Mitarbeiter hinzugefügt</p>';
return;
}
container.innerHTML = team.map((member, index) => `
<div class="list-item">
<div class="form-grid">
<div class="form-group">
<label>Name</label>
<input type="text" value="${member.name || ''}" onchange="updateTeamMember(${index}, 'name', this.value)">
</div>
<div class="form-group">
<label>Funktion</label>
<input type="text" value="${member.role || ''}" onchange="updateTeamMember(${index}, 'role', this.value)">
</div>
<div class="form-group">
<label>Telefon</label>
<input type="tel" value="${member.phone || ''}" onchange="updateTeamMember(${index}, 'phone', this.value)">
</div>
</div>
<button class="icon-btn danger" onclick="removeTeamMember(${index})" style="margin-top: 8px;">
<i class="fas fa-trash"></i> Entfernen
</button>
</div>
`).join('');
}
function updateTeamMember(index, field, value) {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.team[index]) return;
job.team[index][field] = value;
saveJobs();
}
function removeTeamMember(index) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
job.team.splice(index, 1);
renderTeam(job.team);
saveJobs();
}
// Materials
function addMaterial(category) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.materials) job.materials = {};
if (!job.materials[category]) job.materials[category] = [];
const newMaterial = {
id: Date.now().toString(36) + Math.random().toString(36).substr(2),
name: '',
quantity: '',
unit: 'Stk'
};
if (category === 'flooring') {
newMaterial.winkelleistenLFM = '';
newMaterial.dekorQM = '';
}
job.materials[category].push(newMaterial);
renderMaterials(category, job.materials[category]);
saveJobs();
}
function renderMaterials(category, materials) {
const container = document.getElementById(`${category}-container`);
// Null-Check: Element muss existieren
if (!container) {
console.warn(`⚠️ Container nicht gefunden: ${category}-container`);
return;
}
if (!materials || materials.length === 0) {
container.innerHTML = '<p style="color: var(--text-secondary); padding: 12px;">Keine Materialien</p>';
return;
}
container.innerHTML = materials.map((material, index) => {
let fieldsHTML = `
<div class="form-group">
<label>Artikel</label>
<input type="text" value="${material.name || ''}" onchange="updateMaterial('${category}', ${index}, 'name', this.value)">
</div>
<div class="form-group">
<label>Menge</label>
<input type="number" value="${material.quantity || ''}" onchange="updateMaterial('${category}', ${index}, 'quantity', this.value)">
</div>
<div class="form-group">
<label>Einheit</label>
<select onchange="updateMaterial('${category}', ${index}, 'unit', this.value)">
<option value="Stk" ${material.unit === 'Stk' ? 'selected' : ''}>Stk</option>
<option value="m" ${material.unit === 'm' ? 'selected' : ''}>m</option>
<option value="m²" ${material.unit === 'm²' ? 'selected' : ''}>m²</option>
<option value="kg" ${material.unit === 'kg' ? 'selected' : ''}>kg</option>
<option value="Palette" ${material.unit === 'Palette' ? 'selected' : ''}>Palette</option>
</select>
</div>
`;
if (category === 'flooring') {
fieldsHTML += `
<div class="form-group">
<label>Winkelleisten (LFM)</label>
<input type="number" step="0.01" value="${material.winkelleistenLFM || ''}" onchange="updateMaterial('${category}', ${index}, 'winkelleistenLFM', this.value)">
</div>
<div class="form-group">
<label>Dekor (m²)</label>
<input type="number" step="0.01" value="${material.dekorQM || ''}" onchange="updateMaterial('${category}', ${index}, 'dekorQM', this.value)">
</div>
`;
}
return `
<div class="list-item">
<div class="form-grid">
${fieldsHTML}
</div>
<button class="icon-btn danger" onclick="removeMaterial('${category}', ${index})" style="margin-top: 8px;">
<i class="fas fa-trash"></i> Entfernen
</button>
</div>
`;
}).join('');
}
function updateMaterial(category, index, field, value) {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.materials[category][index]) return;
job.materials[category][index][field] = value;
saveJobs();
}
function removeMaterial(category, index) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
job.materials[category].splice(index, 1);
renderMaterials(category, job.materials[category]);
saveJobs();
}
// Druckproduktion functions
function toggleDruckproduktion() {
const checkbox = document.getElementById('druckproduktion-enabled');
const fields = document.getElementById('druckproduktion-fields');
fields.style.display = checkbox.checked ? 'block' : 'none';
}
// Catering functions
function toggleBaristaFields() {
const checkbox = document.getElementById('catering-barista');
const fields = document.getElementById('barista-fields');
fields.style.display = checkbox.checked ? 'block' : 'none';
}
function toggleDrinksFields() {
const checkbox = document.getElementById('catering-drinks');
const fields = document.getElementById('drinks-fields');
fields.style.display = checkbox.checked ? 'block' : 'none';
}
function toggleFoodFields() {
const checkbox = document.getElementById('catering-food');
const fields = document.getElementById('food-fields');
fields.style.display = checkbox.checked ? 'block' : 'none';
}
function addCateringStaff() {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.catering) {
job.catering = {
baristaEnabled: false,
baristaPackage: '',
days: '',
staff: []
};
}
if (!job.catering.staff) {
job.catering.staff = [];
}
const newStaff = {
id: Date.now().toString(36) + Math.random().toString(36).substr(2),
firstname: '',
lastname: '',
email: ''
};
job.catering.staff.push(newStaff);
renderCateringStaff();
saveJobs();
}
function renderCateringStaff() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.catering || !job.catering.staff) {
document.getElementById('catering-staff-container').innerHTML = '<p style="color: #999; font-size: 14px;">Noch kein Personal hinzugefügt</p>';
return;
}
const container = document.getElementById('catering-staff-container');
if (job.catering.staff.length === 0) {
container.innerHTML = '<p style="color: #999; font-size: 14px;">Noch kein Personal hinzugefügt</p>';
return;
}
container.innerHTML = job.catering.staff.map((person, index) => `
<div class="list-item">
<div class="form-grid">
<div class="form-group">
<label>Vorname</label>
<input type="text" value="${person.firstname || ''}" onchange="updateCateringStaff(${index}, 'firstname', this.value)">
</div>
<div class="form-group">
<label>Nachname</label>
<input type="text" value="${person.lastname || ''}" onchange="updateCateringStaff(${index}, 'lastname', this.value)">
</div>
<div class="form-group">
<label>E-Mail</label>
<input type="email" value="${person.email || ''}" onchange="updateCateringStaff(${index}, 'email', this.value)">
</div>
</div>
<button class="icon-btn danger" onclick="removeCateringStaff(${index})" style="margin-top: 8px;">
<i class="fas fa-trash"></i> Entfernen
</button>
</div>
`).join('');
}
function updateCateringStaff(index, field, value) {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.catering || !job.catering.staff) return;
job.catering.staff[index][field] = value;
saveJobs();
}
function removeCateringStaff(index) {
if (!confirm('Person wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.catering || !job.catering.staff) return;
job.catering.staff.splice(index, 1);
renderCateringStaff();
saveJobs();
}
// Checklist
function addChecklistItem() {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
const text = prompt('Neue Aufgabe:');
if (!text) return;
if (!job.checklist) job.checklist = [];
job.checklist.push({
id: Date.now().toString(36) + Math.random().toString(36).substr(2),
text: text,
completed: false,
custom: true
});
renderChecklist(job.checklist);
saveJobs();
}
function renderChecklist(checklist) {
const container = document.getElementById('checklist-container');
if (!checklist || checklist.length === 0) {
container.innerHTML = '<p style="color: var(--text-secondary); padding: 12px;">Keine Aufgaben</p>';
return;
}
container.innerHTML = checklist.map((item, index) => `
<div class="checklist-item ${item.completed ? 'completed' : ''}">
<input type="checkbox" id="check-${index}" ${item.completed ? 'checked' : ''} onchange="toggleChecklistItem(${index})">
<label for="check-${index}">${item.text}</label>
${item.custom ? `<button class="icon-btn danger" onclick="removeChecklistItem(${index})"><i class="fas fa-trash"></i></button>` : ''}
</div>
`).join('');
}
function toggleChecklistItem(index) {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.checklist[index]) return;
job.checklist[index].completed = !job.checklist[index].completed;
renderChecklist(job.checklist);
saveJobs();
}
function removeChecklistItem(index) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
job.checklist.splice(index, 1);
renderChecklist(job.checklist);
saveJobs();
}
// Stand image upload functions
function handleStandImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
// Check file size (max 2MB)
if (file.size > 2 * 1024 * 1024) {
showToast('❌ Bild zu groß! Max. 2 MB');
event.target.value = '';
return;
}
// Check file type
if (!file.type.startsWith('image/')) {
showToast('❌ Nur Bilddateien erlaubt!');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
job.standImage = e.target.result;
job.standImageName = file.name;
// Show preview
const preview = document.getElementById('stand-image-preview');
const previewImg = document.getElementById('stand-image-preview-img');
previewImg.src = e.target.result;
preview.style.display = 'block';
saveJobs();
showToast('✅ Bild hochgeladen');
console.log('✅ Messestand-Bild hochgeladen:', file.name);
};
reader.onerror = function() {
showToast('❌ Fehler beim Hochladen');
console.error('❌ Fehler beim Lesen der Bilddatei');
};
reader.readAsDataURL(file);
}
// Toggle booking platform fields
function toggleBookingPlatformFields() {
const checkbox = document.getElementById('edit-booking-platform');
const fields = document.getElementById('booking-platform-fields');
if (checkbox.checked) {
fields.style.display = 'block';
} else {
fields.style.display = 'none';
}
}
// Toggle power booking fields
function togglePowerBookingFields() {
const checkbox = document.getElementById('edit-power-booking');
const fields = document.getElementById('power-booking-fields');
if (checkbox.checked) {
fields.style.display = 'block';
} else {
fields.style.display = 'none';
}
}
// Power confirmation PDF upload
function handlePowerConfirmationUpload(event) {
const file = event.target.files[0];
if (!file) return;
// Check file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
showToast('❌ PDF zu groß! Max. 5 MB');
event.target.value = '';
return;
}
// Check file type
if (file.type !== 'application/pdf') {
showToast('❌ Nur PDF-Dateien erlaubt!');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.powerBooking) {
job.powerBooking = {};
}
job.powerBooking.confirmationPdf = e.target.result;
job.powerBooking.confirmationPdfName = file.name;
job.powerBooking.confirmationPdfSize = file.size;
// Show preview
document.getElementById('power-confirmation-preview').style.display = 'block';
document.getElementById('power-confirmation-name').textContent = file.name;
document.getElementById('power-confirmation-size').textContent = (file.size / 1024).toFixed(1) + ' KB';
saveJobs();
showToast('✅ PDF hochgeladen');
console.log('✅ Strombuchungs-PDF hochgeladen:', file.name);
};
reader.onerror = function() {
showToast('❌ Fehler beim Hochladen');
console.error('❌ Fehler beim Lesen der PDF-Datei');
};
reader.readAsDataURL(file);
}
function downloadPowerConfirmation() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.powerBooking || !job.powerBooking.confirmationPdf) return;
const link = document.createElement('a');
link.href = job.powerBooking.confirmationPdf;
link.download = job.powerBooking.confirmationPdfName || 'strombuchung.pdf';
link.click();
showToast('✅ PDF heruntergeladen');
}
function removePowerConfirmation() {
if (!confirm('PDF wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.powerBooking) return;
job.powerBooking.confirmationPdf = null;
job.powerBooking.confirmationPdfName = null;
job.powerBooking.confirmationPdfSize = null;
document.getElementById('power-confirmation-preview').style.display = 'none';
document.getElementById('edit-power-confirmation-upload').value = '';
saveJobs();
showToast('✅ PDF entfernt');
}
// Toggle rigging fields
function toggleRiggingFields() {
const checkbox = document.getElementById('edit-rigging');
const fields = document.getElementById('rigging-fields');
if (checkbox.checked) {
fields.style.display = 'block';
} else {
fields.style.display = 'none';
}
}
// Rigging quote PDF upload
function handleRiggingQuoteUpload(event) {
const file = event.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) {
showToast('❌ PDF zu groß! Max. 5 MB');
event.target.value = '';
return;
}
if (file.type !== 'application/pdf') {
showToast('❌ Nur PDF-Dateien erlaubt!');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.rigging) {
job.rigging = {};
}
job.rigging.quotePdf = e.target.result;
job.rigging.quotePdfName = file.name;
job.rigging.quotePdfSize = file.size;
document.getElementById('rigging-quote-preview').style.display = 'block';
document.getElementById('rigging-quote-name').textContent = file.name;
document.getElementById('rigging-quote-size').textContent = (file.size / 1024).toFixed(1) + ' KB';
saveJobs();
showToast('✅ Angebot hochgeladen');
};
reader.readAsDataURL(file);
}
function downloadRiggingQuote() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.rigging || !job.rigging.quotePdf) return;
const link = document.createElement('a');
link.href = job.rigging.quotePdf;
link.download = job.rigging.quotePdfName || 'rigging-angebot.pdf';
link.click();
showToast('✅ Angebot heruntergeladen');
}
function removeRiggingQuote() {
if (!confirm('Angebot wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.rigging) return;
job.rigging.quotePdf = null;
job.rigging.quotePdfName = null;
job.rigging.quotePdfSize = null;
document.getElementById('rigging-quote-preview').style.display = 'none';
document.getElementById('edit-rigging-quote-upload').value = '';
saveJobs();
showToast('✅ Angebot entfernt');
}
// Rigging drawing PDF upload
function handleRiggingDrawingUpload(event) {
const file = event.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) {
showToast('❌ PDF zu groß! Max. 5 MB');
event.target.value = '';
return;
}
if (file.type !== 'application/pdf') {
showToast('❌ Nur PDF-Dateien erlaubt!');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.rigging) {
job.rigging = {};
}
job.rigging.drawingPdf = e.target.result;
job.rigging.drawingPdfName = file.name;
job.rigging.drawingPdfSize = file.size;
document.getElementById('rigging-drawing-preview').style.display = 'block';
document.getElementById('rigging-drawing-name').textContent = file.name;
document.getElementById('rigging-drawing-size').textContent = (file.size / 1024).toFixed(1) + ' KB';
saveJobs();
showToast('✅ Zeichnung hochgeladen');
};
reader.readAsDataURL(file);
}
function downloadRiggingDrawing() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.rigging || !job.rigging.drawingPdf) return;
const link = document.createElement('a');
link.href = job.rigging.drawingPdf;
link.download = job.rigging.drawingPdfName || 'rigging-zeichnung.pdf';
link.click();
showToast('✅ Zeichnung heruntergeladen');
}
function removeRiggingDrawing() {
if (!confirm('Zeichnung wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.rigging) return;
job.rigging.drawingPdf = null;
job.rigging.drawingPdfName = null;
job.rigging.drawingPdfSize = null;
document.getElementById('rigging-drawing-preview').style.display = 'none';
document.getElementById('edit-rigging-drawing-upload').value = '';
saveJobs();
showToast('✅ Zeichnung entfernt');
}
function handleRiggingMesseConfirmationUpload(event) {
const file = event.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) {
showToast('❌ PDF zu groß! Max. 5 MB');
event.target.value = '';
return;
}
if (file.type !== 'application/pdf') {
showToast('❌ Nur PDF-Dateien erlaubt!');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.rigging) {
job.rigging = {};
}
job.rigging.messeConfirmationPdf = e.target.result;
job.rigging.messeConfirmationPdfName = file.name;
job.rigging.messeConfirmationPdfSize = file.size;
document.getElementById('rigging-messe-confirmation-preview').style.display = 'block';
document.getElementById('rigging-messe-confirmation-name').textContent = file.name;
document.getElementById('rigging-messe-confirmation-size').textContent = (file.size / 1024).toFixed(1) + ' KB';
saveJobs();
showToast('✅ Auftragsbestätigung hochgeladen');
};
reader.readAsDataURL(file);
}
function downloadRiggingMesseConfirmation() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.rigging || !job.rigging.messeConfirmationPdf) return;
const link = document.createElement('a');
link.href = job.rigging.messeConfirmationPdf;
link.download = job.rigging.messeConfirmationPdfName || 'auftragsbestaetigung-messe.pdf';
link.click();
showToast('✅ Auftragsbestätigung heruntergeladen');
}
function removeRiggingMesseConfirmation() {
if (!confirm('Auftragsbestätigung wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.rigging) return;
job.rigging.messeConfirmationPdf = null;
job.rigging.messeConfirmationPdfName = null;
job.rigging.messeConfirmationPdfSize = null;
document.getElementById('rigging-messe-confirmation-preview').style.display = 'none';
document.getElementById('edit-rigging-messe-confirmation-upload').value = '';
saveJobs();
showToast('✅ Auftragsbestätigung entfernt');
}
// Vehicles / Logistics
function addVehicle() {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.vehicles) job.vehicles = [];
job.vehicles.push({
id: Date.now().toString(36) + Math.random().toString(36).substr(2),
type: 'Eigene FZGs',
license: '',
driver: '',
departure: '',
return: '',
confirmationPdf: null,
confirmationPdfName: null,
confirmationPdfSize: null
});
renderVehicles(job.vehicles);
saveJobs();
}
function renderVehicles(vehicles) {
const container = document.getElementById('vehicles-container');
if (!vehicles || vehicles.length === 0) {
container.innerHTML = '<p style="color: var(--text-secondary); padding: 12px;">Keine Fahrzeuge hinzugefügt</p>';
return;
}
container.innerHTML = vehicles.map((vehicle, index) => `
<div class="list-item" style="margin-bottom: 16px;">
<div class="form-grid">
<div class="form-group">
<label>Fahrzeug</label>
<select onchange="updateVehicle(${index}, 'type', this.value)" style="padding: 8px; border: 1px solid #E0E0E0; border-radius: 4px; font-size: 14px; width: 100%;">
<option value="Eigene FZGs" ${vehicle.type === 'Eigene FZGs' ? 'selected' : ''}>Eigene FZGs</option>
<option value="Arndt" ${vehicle.type === 'Arndt' ? 'selected' : ''}>Arndt</option>
<option value="AVIS" ${vehicle.type === 'AVIS' ? 'selected' : ''}>AVIS</option>
<option value="Andere" ${vehicle.type === 'Andere' ? 'selected' : ''}>Andere</option>
</select>
</div>
<div class="form-group">
<label>Kennzeichen</label>
<input type="text" value="${vehicle.license || ''}" onchange="updateVehicle(${index}, 'license', this.value)" placeholder="z.B. B-AB 1234">
</div>
<div class="form-group">
<label>Fahrer</label>
<input type="text" value="${vehicle.driver || ''}" onchange="updateVehicle(${index}, 'driver', this.value)" placeholder="Name des Fahrers">
</div>
<div class="form-group">
<label>Abfahrt</label>
<input type="datetime-local" value="${vehicle.departure || ''}" onchange="updateVehicle(${index}, 'departure', this.value)">
</div>
<div class="form-group">
<label>Rückkehr</label>
<input type="datetime-local" value="${vehicle.return || ''}" onchange="updateVehicle(${index}, 'return', this.value)">
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label>Buchungsbestätigung (PDF)</label>
<input type="file" id="vehicle-confirmation-upload-${index}" accept=".pdf" onchange="handleVehicleConfirmationUpload(event, ${index})" style="display: none;">
<button class="btn btn-secondary btn-small" onclick="document.getElementById('vehicle-confirmation-upload-${index}').click()">
<i class="fas fa-upload"></i> PDF hochladen
</button>
${vehicle.confirmationPdf ? `
<div style="margin-top: 12px; padding: 12px; background: #F5F5F5; border-radius: 6px; border: 2px solid #FFE000;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 8px;">
<i class="fas fa-file-pdf" style="color: #E74C3C; font-size: 24px;"></i>
<div>
<div style="font-weight: 600; color: #111111;">${vehicle.confirmationPdfName || 'Bestätigung.pdf'}</div>
<div style="font-size: 12px; color: #666;">${((vehicle.confirmationPdfSize || 0) / 1024).toFixed(1)} KB</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-small" onclick="downloadVehicleConfirmation(${index})" title="Herunterladen">
<i class="fas fa-download"></i>
</button>
<button class="btn btn-secondary btn-small" onclick="removeVehicleConfirmation(${index})" title="Entfernen">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
` : ''}
</div>
</div>
<button class="icon-btn danger" onclick="removeVehicle(${index})" style="margin-top: 8px;">
<i class="fas fa-trash"></i> Fahrzeug entfernen
</button>
</div>
`).join('');
}
function updateVehicle(index, field, value) {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.vehicles || !job.vehicles[index]) return;
job.vehicles[index][field] = value;
saveJobs();
}
function removeVehicle(index) {
if (!confirm('Fahrzeug wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
job.vehicles.splice(index, 1);
renderVehicles(job.vehicles);
saveJobs();
}
function handleVehicleConfirmationUpload(event, index) {
const file = event.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) {
showToast('❌ PDF zu groß! Max. 5 MB');
event.target.value = '';
return;
}
if (file.type !== 'application/pdf') {
showToast('❌ Nur PDF-Dateien erlaubt!');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.vehicles || !job.vehicles[index]) return;
job.vehicles[index].confirmationPdf = e.target.result;
job.vehicles[index].confirmationPdfName = file.name;
job.vehicles[index].confirmationPdfSize = file.size;
renderVehicles(job.vehicles);
saveJobs();
showToast('✅ PDF hochgeladen');
};
reader.readAsDataURL(file);
}
function downloadVehicleConfirmation(index) {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.vehicles || !job.vehicles[index] || !job.vehicles[index].confirmationPdf) return;
const link = document.createElement('a');
link.href = job.vehicles[index].confirmationPdf;
link.download = job.vehicles[index].confirmationPdfName || 'fahrzeug-bestaetigung.pdf';
link.click();
showToast('✅ PDF heruntergeladen');
}
function removeVehicleConfirmation(index) {
if (!confirm('PDF wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.vehicles || !job.vehicles[index]) return;
job.vehicles[index].confirmationPdf = null;
job.vehicles[index].confirmationPdfName = null;
job.vehicles[index].confirmationPdfSize = null;
renderVehicles(job.vehicles);
saveJobs();
showToast('✅ PDF entfernt');
}
// WLAN functions
function toggleWlanFields() {
const checkbox = document.getElementById('edit-wlan');
const fields = document.getElementById('wlan-fields');
if (checkbox.checked) {
fields.style.display = 'block';
} else {
fields.style.display = 'none';
}
}
function handleWlanConfirmationUpload(event) {
const file = event.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) {
showToast('❌ PDF zu groß! Max. 5 MB');
event.target.value = '';
return;
}
if (file.type !== 'application/pdf') {
showToast('❌ Nur PDF-Dateien erlaubt!');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.wlan) {
job.wlan = {};
}
job.wlan.confirmationPdf = e.target.result;
job.wlan.confirmationPdfName = file.name;
job.wlan.confirmationPdfSize = file.size;
document.getElementById('wlan-confirmation-preview').style.display = 'block';
document.getElementById('wlan-confirmation-name').textContent = file.name;
document.getElementById('wlan-confirmation-size').textContent = (file.size / 1024).toFixed(1) + ' KB';
saveJobs();
showToast('✅ PDF hochgeladen');
};
reader.readAsDataURL(file);
}
function downloadWlanConfirmation() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.wlan || !job.wlan.confirmationPdf) return;
const link = document.createElement('a');
link.href = job.wlan.confirmationPdf;
link.download = job.wlan.confirmationPdfName || 'wlan-bestaetigung.pdf';
link.click();
showToast('✅ PDF heruntergeladen');
}
function removeWlanConfirmation() {
if (!confirm('PDF wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.wlan) return;
job.wlan.confirmationPdf = null;
job.wlan.confirmationPdfName = null;
job.wlan.confirmationPdfSize = null;
document.getElementById('wlan-confirmation-preview').style.display = 'none';
document.getElementById('edit-wlan-confirmation-upload').value = '';
saveJobs();
showToast('✅ PDF entfernt');
}
// Cleaning service functions
function toggleCleaningFields() {
const checkbox = document.getElementById('edit-cleaning');
const fields = document.getElementById('cleaning-fields');
if (checkbox.checked) {
fields.style.display = 'block';
} else {
fields.style.display = 'none';
}
}
function handleCleaningConfirmationUpload(event) {
const file = event.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) {
showToast('❌ PDF zu groß! Max. 5 MB');
event.target.value = '';
return;
}
if (file.type !== 'application/pdf') {
showToast('❌ Nur PDF-Dateien erlaubt!');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.cleaning) {
job.cleaning = {};
}
job.cleaning.confirmationPdf = e.target.result;
job.cleaning.confirmationPdfName = file.name;
job.cleaning.confirmationPdfSize = file.size;
document.getElementById('cleaning-confirmation-preview').style.display = 'block';
document.getElementById('cleaning-confirmation-name').textContent = file.name;
document.getElementById('cleaning-confirmation-size').textContent = (file.size / 1024).toFixed(1) + ' KB';
saveJobs();
showToast('✅ PDF hochgeladen');
};
reader.readAsDataURL(file);
}
function downloadCleaningConfirmation() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.cleaning || !job.cleaning.confirmationPdf) return;
const link = document.createElement('a');
link.href = job.cleaning.confirmationPdf;
link.download = job.cleaning.confirmationPdfName || 'reinigung-bestaetigung.pdf';
link.click();
showToast('✅ PDF heruntergeladen');
}
function removeCleaningConfirmation() {
if (!confirm('PDF wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.cleaning) return;
job.cleaning.confirmationPdf = null;
job.cleaning.confirmationPdfName = null;
job.cleaning.confirmationPdfSize = null;
document.getElementById('cleaning-confirmation-preview').style.display = 'none';
document.getElementById('edit-cleaning-confirmation-upload').value = '';
saveJobs();
showToast('✅ PDF entfernt');
}
// ========== Standfreigabe Functions ==========
function toggleStandApprovalFields() {
const checkbox = document.getElementById('edit-stand-approval');
const fields = document.getElementById('stand-approval-fields');
if (checkbox.checked) {
fields.style.display = 'block';
} else {
fields.style.display = 'none';
}
}
function handleStandApprovalUpload(event) {
const file = event.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) {
showToast('❌ PDF zu groß! Max. 5 MB');
event.target.value = '';
return;
}
if (file.type !== 'application/pdf') {
showToast('❌ Nur PDF-Dateien erlaubt!');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.standApproval) {
job.standApproval = {};
}
job.standApproval.pdf = e.target.result;
job.standApproval.pdfName = file.name;
job.standApproval.pdfSize = file.size;
document.getElementById('stand-approval-preview').style.display = 'block';
document.getElementById('stand-approval-name').textContent = file.name;
document.getElementById('stand-approval-size').textContent = (file.size / 1024).toFixed(1) + ' KB';
saveJobs();
showToast('✅ Standfreigabe hochgeladen');
};
reader.readAsDataURL(file);
}
function downloadStandApproval() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.standApproval || !job.standApproval.pdf) return;
const link = document.createElement('a');
link.href = job.standApproval.pdf;
link.download = job.standApproval.pdfName || 'standfreigabe.pdf';
link.click();
showToast('✅ PDF heruntergeladen');
}
function removeStandApproval() {
if (!confirm('PDF wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.standApproval) return;
job.standApproval.pdf = null;
job.standApproval.pdfName = null;
job.standApproval.pdfSize = null;
document.getElementById('stand-approval-preview').style.display = 'none';
document.getElementById('edit-stand-approval-upload').value = '';
saveJobs();
showToast('✅ PDF entfernt');
}
function downloadStandImage() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.standImage) return;
const link = document.createElement('a');
link.href = job.standImage;
link.download = job.standImageName || 'messestand.jpg';
link.click();
showToast('✅ Bild heruntergeladen');
}
function removeStandImage() {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
job.standImage = null;
job.standImageName = null;
// Hide preview
const preview = document.getElementById('stand-image-preview');
preview.style.display = 'none';
// Clear input
const input = document.getElementById('stand-image-upload');
if (input) input.value = '';
saveJobs();
showToast('✅ Bild entfernt');
console.log('✅ Messestand-Bild entfernt');
}
// Gallery functions
let currentLightboxIndex = 0;
function handleGalleryUpload(event) {
const files = Array.from(event.target.files);
if (!files.length) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
// Initialize gallery array if not exists
if (!job.standGallery) {
job.standGallery = [];
}
// Check if adding these files would exceed 10 images
const totalImages = job.standGallery.length + files.length;
if (totalImages > 10) {
showToast(`❌ Max. 10 Bilder erlaubt! (${job.standGallery.length}/10 bereits vorhanden)`);
event.target.value = '';
return;
}
let processedCount = 0;
let errorCount = 0;
files.forEach((file, index) => {
// Check file size (max 2MB)
if (file.size > 2 * 1024 * 1024) {
showToast(`❌ "${file.name}" zu groß! Max. 2 MB`);
errorCount++;
processedCount++;
if (processedCount === files.length) {
finishGalleryUpload(files.length - errorCount);
}
return;
}
// Check file type
if (!file.type.startsWith('image/')) {
showToast(`❌ "${file.name}" ist keine Bilddatei!`);
errorCount++;
processedCount++;
if (processedCount === files.length) {
finishGalleryUpload(files.length - errorCount);
}
return;
}
const reader = new FileReader();
reader.onload = function(e) {
job.standGallery.push({
id: Date.now().toString(36) + Math.random().toString(36).substring(2),
name: file.name,
data: e.target.result,
uploadedAt: new Date().toISOString()
});
processedCount++;
if (processedCount === files.length) {
finishGalleryUpload(files.length - errorCount);
}
};
reader.onerror = function() {
showToast(`❌ Fehler beim Hochladen von "${file.name}"`);
errorCount++;
processedCount++;
if (processedCount === files.length) {
finishGalleryUpload(files.length - errorCount);
}
};
reader.readAsDataURL(file);
});
}
function finishGalleryUpload(successCount) {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
saveJobs();
renderGallery();
// Clear input
const input = document.getElementById('gallery-upload-input');
if (input) input.value = '';
if (successCount > 0) {
showToast(`✅ ${successCount} Bild${successCount > 1 ? 'er' : ''} hochgeladen`);
console.log(`✅ ${successCount} Standfotos hochgeladen`);
}
}
function renderGallery() {
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
const container = document.getElementById('gallery-container');
// Initialize gallery array if not exists
if (!job.standGallery) {
job.standGallery = [];
}
if (job.standGallery.length === 0) {
container.innerHTML = `
<div class="gallery-empty">
<i class="fas fa-images"></i>
<p>Noch keine Standfotos hochgeladen</p>
<p style="font-size: 0.85rem; margin-top: 8px;">Laden Sie Bilder Ihres Messestandes hoch, um eine Galerie zu erstellen.</p>
</div>
`;
return;
}
container.innerHTML = `
<div class="gallery-grid">
${job.standGallery.map((image, index) => `
<div class="gallery-item">
<img src="${image.data}"
alt="${image.name}"
class="gallery-item-image"
onclick="openLightbox(${index})">
<div class="gallery-item-info">
<div class="gallery-item-name" title="${image.name}">
${image.name}
</div>
<div class="gallery-item-actions">
<button class="gallery-item-btn" onclick="downloadGalleryImage('${image.id}')">
<i class="fas fa-download"></i> Download
</button>
<button class="gallery-item-btn delete" onclick="removeGalleryImage('${image.id}')">
<i class="fas fa-trash"></i> Löschen
</button>
</div>
</div>
</div>
`).join('')}
</div>
`;
}
function openLightbox(index) {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.standGallery || job.standGallery.length === 0) return;
currentLightboxIndex = index;
const lightbox = document.getElementById('lightbox');
const lightboxImage = document.getElementById('lightbox-image');
lightboxImage.src = job.standGallery[index].data;
lightbox.classList.add('active');
// Disable scrolling
document.body.style.overflow = 'hidden';
}
function closeLightbox() {
const lightbox = document.getElementById('lightbox');
lightbox.classList.remove('active');
// Enable scrolling
document.body.style.overflow = 'auto';
}
function navigateLightbox(direction) {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.standGallery || job.standGallery.length === 0) return;
currentLightboxIndex += direction;
// Loop around
if (currentLightboxIndex < 0) {
currentLightboxIndex = job.standGallery.length - 1;
} else if (currentLightboxIndex >= job.standGallery.length) {
currentLightboxIndex = 0;
}
const lightboxImage = document.getElementById('lightbox-image');
lightboxImage.src = job.standGallery[currentLightboxIndex].data;
}
function downloadGalleryImage(imageId) {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.standGallery) return;
const image = job.standGallery.find(img => img.id === imageId);
if (!image) return;
const link = document.createElement('a');
link.href = image.data;
link.download = image.name;
link.click();
showToast('✅ Bild heruntergeladen');
}
function removeGalleryImage(imageId) {
if (!confirm('Bild wirklich löschen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.standGallery) return;
job.standGallery = job.standGallery.filter(img => img.id !== imageId);
saveJobs();
renderGallery();
showToast('✅ Bild gelöscht');
console.log('✅ Standfoto gelöscht');
}
// Calendar functions
let currentCalendarDate = new Date();
function showDashboardView() {
document.getElementById('dashboard-view').classList.remove('hidden');
document.getElementById('calendar-view').classList.add('hidden');
document.getElementById('job-detail-view').classList.add('hidden');
document.getElementById('nav-dashboard').classList.add('active');
document.getElementById('nav-calendar').classList.remove('active');
updateStats();
renderJobs();
}
function showCalendarView() {
document.getElementById('dashboard-view').classList.add('hidden');
document.getElementById('calendar-view').classList.remove('hidden');
document.getElementById('job-detail-view').classList.add('hidden');
document.getElementById('nav-dashboard').classList.remove('active');
document.getElementById('nav-calendar').classList.add('active');
renderCalendar();
}
function changeMonth(delta) {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() + delta);
renderCalendar();
}
function goToToday() {
currentCalendarDate = new Date();
renderCalendar();
}
function renderCalendar() {
const container = document.getElementById('calendar-container');
const monthYearEl = document.getElementById('calendar-month-year');
const year = currentCalendarDate.getFullYear();
const month = currentCalendarDate.getMonth();
// Update month/year display
const monthNames = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
monthYearEl.textContent = `${monthNames[month]} ${year}`;
// Get first and last day of month
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
// Get day of week for first day (0 = Sunday, adjust to Monday = 0)
let firstDayOfWeek = firstDay.getDay() - 1;
if (firstDayOfWeek === -1) firstDayOfWeek = 6;
// Get events for this month
const events = getMonthEvents(year, month);
// Build calendar HTML
let html = '<div class="calendar-grid">';
// Day headers
const dayNames = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
dayNames.forEach(day => {
html += `<div class="calendar-day-header">${day}</div>`;
});
// Empty cells for days before month starts
for (let i = 0; i < firstDayOfWeek; i++) {
const prevMonthDay = new Date(year, month, -(firstDayOfWeek - i - 1));
html += `<div class="calendar-day other-month">
<div class="calendar-day-number">${prevMonthDay.getDate()}</div>
</div>`;
}
// Days of current month
const today = new Date();
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day);
const dateStr = date.toISOString().split('T')[0];
const isToday = date.toDateString() === today.toDateString();
const dayEvents = events.filter(e => e.date === dateStr);
html += `<div class="calendar-day ${isToday ? 'today' : ''}">
<div class="calendar-day-number">${day}</div>`;
dayEvents.forEach(event => {
if (event.jobIds && event.jobIds.length > 1) {
// Mehrere Jobs - zeige Auswahl-Dialog
const jobIdsStr = event.jobIds.join(',');
html += `<div class="calendar-event calendar-event-multi ${event.type}" data-job-ids="${jobIdsStr}" data-event-name="${event.eventName}" title="${event.title}">
${event.title}
</div>`;
} else {
// Einzelner Job - direkt öffnen
html += `<div class="calendar-event calendar-event-single ${event.type}" data-job-id="${event.jobId}" title="${event.title}">
${event.title}
</div>`;
}
});
html += '</div>';
}
// Fill remaining cells
const totalCells = firstDayOfWeek + daysInMonth;
const remainingCells = 7 - (totalCells % 7);
if (remainingCells < 7) {
for (let i = 1; i <= remainingCells; i++) {
html += `<div class="calendar-day other-month">
<div class="calendar-day-number">${i}</div>
</div>`;
}
}
html += '</div>';
container.innerHTML = html;
// Event Listener für Kalender-Events
document.querySelectorAll('.calendar-event-multi').forEach(eventEl => {
eventEl.addEventListener('click', function() {
const jobIdsStr = this.getAttribute('data-job-ids');
const eventName = this.getAttribute('data-event-name');
showJobSelector(jobIdsStr, eventName);
});
});
document.querySelectorAll('.calendar-event-single').forEach(eventEl => {
eventEl.addEventListener('click', function() {
const jobId = this.getAttribute('data-job-id');
openJob(jobId);
});
});
}
// Show job selector for multi-job events
function showJobSelector(jobIdsStr, eventName) {
const jobIds = jobIdsStr.split(',');
const eventJobs = jobs.filter(j => jobIds.includes(j.id));
if (eventJobs.length === 0) return;
// Erstelle HTML für Job-Auswahl
let html = `
<div class="job-selector-modal" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 10000; display: flex; align-items: center; justify-content: center;" onclick="this.remove()">
<div style="background: white; border-radius: 8px; padding: 30px; max-width: 500px; width: 90%; box-shadow: 0 10px 40px rgba(0,0,0,0.3);" onclick="event.stopPropagation()">
<h3 style="margin: 0 0 20px 0; font-size: 20px; color: #111111;">
📅 ${eventName}
</h3>
<p style="color: #666; margin-bottom: 20px; font-size: 14px;">
Wählen Sie einen Stand aus:
</p>
<div style="display: flex; flex-direction: column; gap: 10px;">
`;
eventJobs.forEach(job => {
const customerName = job.customerName || 'Unbenannt';
const projektNr = job.projektNr || '';
const standInfo = job.standSize || job.hall || '';
html += `
<button data-job-id="${job.id}" class="job-selector-btn"
style="
background: white;
border: 2px solid #E0E0E0;
border-radius: 6px;
padding: 15px;
text-align: left;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
"
onmouseover="this.style.borderColor='#FFE000'; this.style.background='#FFFBEA';"
onmouseout="this.style.borderColor='#E0E0E0'; this.style.background='white';">
<div style="font-weight: 600; color: #111111; margin-bottom: 5px;">
${customerName}
</div>
<div style="font-size: 12px; color: #666;">
${projektNr ? 'Projekt: ' + projektNr : ''}
${standInfo ? ' • ' + standInfo : ''}
</div>
</button>
`;
});
html += `
</div>
<button onclick="this.parentElement.parentElement.remove()"
style="
margin-top: 20px;
width: 100%;
padding: 12px;
background: #F5F5F5;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
color: #666;
"
onmouseover="this.style.background='#E0E0E0';"
onmouseout="this.style.background='#F5F5F5';">
Abbrechen
</button>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', html);
// Event Listener für Job-Auswahl-Buttons
document.querySelectorAll('.job-selector-btn').forEach(btn => {
btn.addEventListener('click', function() {
const jobId = this.getAttribute('data-job-id');
// Modal entfernen
this.closest('.job-selector-modal').remove();
// Job öffnen
openJob(jobId);
});
});
}
// Helper function to get all dates between start and end
function getDatesBetween(startDate, endDate) {
const dates = [];
const current = new Date(startDate);
const end = new Date(endDate);
// Set to start of day to avoid time zone issues
current.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
while (current <= end) {
dates.push(current.toISOString().split('T')[0]);
current.setDate(current.getDate() + 1);
}
return dates;
}
function getMonthEvents(year, month) {
const eventsMap = new Map(); // Für Duplikat-Prüfung
jobs.forEach(job => {
// Setup events (Aufbau)
if (job.setupStart) {
const startDate = new Date(job.setupStart);
const endDate = job.setupEnd ? new Date(job.setupEnd) : startDate;
const allDates = getDatesBetween(startDate, endDate);
allDates.forEach(dateStr => {
const date = new Date(dateStr);
if (date.getFullYear() === year && date.getMonth() === month) {
const key = `${dateStr}-setup-${job.customerName}`;
if (!eventsMap.has(key)) {
eventsMap.set(key, {
date: dateStr,
title: `🔧 ${job.customerName || 'Aufbau'}`,
type: 'setup',
jobId: job.id,
jobIds: [job.id]
});
}
}
});
}
// Event/Messe (Messe/Event) - Gruppierung nach eventName
if (job.eventStart) {
const startDate = new Date(job.eventStart);
const endDate = job.eventEnd ? new Date(job.eventEnd) : startDate;
const allDates = getDatesBetween(startDate, endDate);
allDates.forEach(dateStr => {
const date = new Date(dateStr);
if (date.getFullYear() === year && date.getMonth() === month) {
const eventName = job.eventName || 'Messe';
const key = `${dateStr}-event-${eventName}`;
if (eventsMap.has(key)) {
// Event existiert bereits - Job hinzufügen
const existingEvent = eventsMap.get(key);
existingEvent.jobIds.push(job.id);
existingEvent.count = existingEvent.jobIds.length;
existingEvent.title = `📅 ${eventName} (${existingEvent.count} Stände)`;
} else {
// Neues Event erstellen
eventsMap.set(key, {
date: dateStr,
title: `📅 ${eventName}`,
type: 'event',
jobId: job.id,
jobIds: [job.id],
count: 1,
eventName: eventName
});
}
}
});
}
// Teardown events (Abbau)
if (job.teardownStart) {
const startDate = new Date(job.teardownStart);
const endDate = job.teardownEnd ? new Date(job.teardownEnd) : startDate;
const allDates = getDatesBetween(startDate, endDate);
allDates.forEach(dateStr => {
const date = new Date(dateStr);
if (date.getFullYear() === year && date.getMonth() === month) {
const key = `${dateStr}-teardown-${job.customerName}`;
if (!eventsMap.has(key)) {
eventsMap.set(key, {
date: dateStr,
title: `🔨 ${job.customerName || 'Abbau'}`,
type: 'teardown',
jobId: job.id,
jobIds: [job.id]
});
}
}
});
}
});
// Deadline events (Bestelldeadlines) - Organisation & Produktion
jobs.forEach(job => {
const deadlines = [
{ date: job.bookingPlatform?.deadline, title: '⏰ Buchungsplattform-Deadline', type: 'deadline', enabled: job.bookingPlatform?.enabled },
{ date: job.powerBooking?.deadline, title: '⏰ Strom-Deadline', type: 'deadline', enabled: job.powerBooking?.enabled },
{ date: job.rigging?.deadline, title: '⏰ Rigging-Deadline', type: 'deadline', enabled: job.rigging?.enabled },
{ date: job.wlan?.deadline, title: '⏰ WLAN-Deadline', type: 'deadline', enabled: job.wlan?.enabled },
{ date: job.cleaning?.deadline, title: '⏰ Reinigung-Deadline', type: 'deadline', enabled: job.cleaning?.enabled },
{ date: job.standApproval?.deadline, title: '⏰ Standfreigabe-Deadline', type: 'deadline', enabled: job.standApproval?.enabled },
{ date: job.druckproduktion?.deadline, title: '⏰ Druckproduktion-Deadline', type: 'deadline', enabled: job.druckproduktion?.enabled }
];
deadlines.forEach(deadline => {
if (deadline.date && deadline.enabled) {
const deadlineDate = new Date(deadline.date);
if (deadlineDate.getFullYear() === year && deadlineDate.getMonth() === month) {
const dateStr = deadlineDate.toISOString().split('T')[0];
const key = `${dateStr}-deadline-${deadline.title}-${job.id}`;
if (!eventsMap.has(key)) {
eventsMap.set(key, {
date: dateStr,
title: `${deadline.title} (${job.customerName || 'Job'})`,
type: 'deadline',
jobId: job.id,
jobIds: [job.id]
});
}
}
}
});
});
// Map zurück in Array konvertieren
return Array.from(eventsMap.values());
}
// Acceptance (Standabnahme) functions
let signatureCanvas, signatureCtx, isDrawing = false;
let lastX = 0, lastY = 0;
function initSignaturePad() {
signatureCanvas = document.getElementById('signature-canvas');
if (!signatureCanvas) return;
signatureCtx = signatureCanvas.getContext('2d');
// Set actual canvas size
const rect = signatureCanvas.getBoundingClientRect();
signatureCanvas.width = 600;
signatureCanvas.height = 200;
// Mouse events
signatureCanvas.addEventListener('mousedown', startDrawing);
signatureCanvas.addEventListener('mousemove', draw);
signatureCanvas.addEventListener('mouseup', stopDrawing);
signatureCanvas.addEventListener('mouseout', stopDrawing);
// Touch events for mobile
signatureCanvas.addEventListener('touchstart', handleTouchStart, { passive: false });
signatureCanvas.addEventListener('touchmove', handleTouchMove, { passive: false });
signatureCanvas.addEventListener('touchend', stopDrawing);
}
function startDrawing(e) {
isDrawing = true;
const rect = signatureCanvas.getBoundingClientRect();
lastX = e.clientX - rect.left;
lastY = e.clientY - rect.top;
}
function draw(e) {
if (!isDrawing) return;
const rect = signatureCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
signatureCtx.beginPath();
signatureCtx.moveTo(lastX * (signatureCanvas.width / rect.width), lastY * (signatureCanvas.height / rect.height));
signatureCtx.lineTo(x * (signatureCanvas.width / rect.width), y * (signatureCanvas.height / rect.height));
signatureCtx.strokeStyle = '#111111';
signatureCtx.lineWidth = 2;
signatureCtx.lineCap = 'round';
signatureCtx.stroke();
lastX = x;
lastY = y;
}
function stopDrawing() {
isDrawing = false;
}
function handleTouchStart(e) {
e.preventDefault();
const touch = e.touches[0];
const rect = signatureCanvas.getBoundingClientRect();
isDrawing = true;
lastX = touch.clientX - rect.left;
lastY = touch.clientY - rect.top;
}
function handleTouchMove(e) {
e.preventDefault();
if (!isDrawing) return;
const touch = e.touches[0];
const rect = signatureCanvas.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
signatureCtx.beginPath();
signatureCtx.moveTo(lastX * (signatureCanvas.width / rect.width), lastY * (signatureCanvas.height / rect.height));
signatureCtx.lineTo(x * (signatureCanvas.width / rect.width), y * (signatureCanvas.height / rect.height));
signatureCtx.strokeStyle = '#111111';
signatureCtx.lineWidth = 2;
signatureCtx.lineCap = 'round';
signatureCtx.stroke();
lastX = x;
lastY = y;
}
function clearSignature() {
if (!signatureCanvas || !signatureCtx) return;
signatureCtx.clearRect(0, 0, signatureCanvas.width, signatureCanvas.height);
document.getElementById('signature-saved-indicator').style.display = 'none';
}
function saveSignature() {
if (!signatureCanvas) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.acceptance) {
job.acceptance = {};
}
job.acceptance.signature = signatureCanvas.toDataURL('image/png');
saveJobs();
document.getElementById('signature-saved-indicator').style.display = 'block';
showToast('✅ Unterschrift gespeichert');
}
// Acceptance photos
function handleAcceptancePhotosUpload(event) {
const files = Array.from(event.target.files);
if (!files.length) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.acceptance) {
job.acceptance = {};
}
if (!job.acceptance.photos) {
job.acceptance.photos = [];
}
if (job.acceptance.photos.length + files.length > 10) {
showToast('❌ Max. 10 Bilder erlaubt!');
return;
}
let successCount = 0;
files.forEach((file, index) => {
if (file.size > 2 * 1024 * 1024) {
showToast(`❌ ${file.name} zu groß! Max. 2 MB`);
return;
}
if (!file.type.startsWith('image/')) {
showToast(`❌ ${file.name} ist kein Bild!`);
return;
}
const reader = new FileReader();
reader.onload = function(e) {
job.acceptance.photos.push({
id: Date.now().toString(36) + Math.random().toString(36).substring(2),
data: e.target.result,
name: file.name,
size: file.size
});
successCount++;
if (successCount === files.length) {
renderAcceptanceGallery();
saveJobs();
showToast(`✅ ${successCount} Foto(s) hochgeladen`);
}
};
reader.readAsDataURL(file);
});
event.target.value = '';
}
function renderAcceptanceGallery() {
const container = document.getElementById('acceptance-gallery-container');
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.acceptance || !job.acceptance.photos || job.acceptance.photos.length === 0) {
container.innerHTML = `
<div class="gallery-empty">
<i class="fas fa-camera"></i>
<p>Noch keine Abnahmefotos hochgeladen</p>
<p style="font-size: 0.85rem; margin-top: 8px;">Fotografieren Sie den fertigen Stand für die Abnahme.</p>
</div>
`;
return;
}
container.innerHTML = `
<div class="gallery-grid">
${job.acceptance.photos.map((photo, index) => `
<div class="gallery-item">
<img src="${photo.data}" alt="${photo.name}">
<div class="gallery-item-actions">
<button class="icon-btn" onclick="downloadAcceptancePhoto(${index})" title="Herunterladen">
<i class="fas fa-download"></i>
</button>
<button class="icon-btn danger" onclick="removeAcceptancePhoto(${index})" title="Löschen">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`).join('')}
</div>
`;
}
function downloadAcceptancePhoto(index) {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.acceptance || !job.acceptance.photos || !job.acceptance.photos[index]) return;
const photo = job.acceptance.photos[index];
const link = document.createElement('a');
link.href = photo.data;
link.download = photo.name || `abnahme-foto-${index + 1}.jpg`;
link.click();
showToast('✅ Foto heruntergeladen');
}
function removeAcceptancePhoto(index) {
if (!confirm('Foto wirklich löschen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.acceptance || !job.acceptance.photos) return;
job.acceptance.photos.splice(index, 1);
renderAcceptanceGallery();
saveJobs();
showToast('✅ Foto gelöscht');
}
// Weekly Protocol functions
function handleWeeklyProtocolUpload(event) {
const files = Array.from(event.target.files);
if (files.length === 0) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (!job.weekly) {
job.weekly = { protocols: [] };
}
let uploadedCount = 0;
let errorCount = 0;
files.forEach(file => {
// Validate file type
if (file.type !== 'application/pdf') {
showToast('❌ Nur PDF-Dateien erlaubt: ' + file.name);
errorCount++;
return;
}
// Validate file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
showToast('❌ Datei zu groß (max. 10 MB): ' + file.name);
errorCount++;
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const protocol = {
id: Date.now().toString(36) + Math.random().toString(36).substring(2),
name: file.name,
size: file.size,
data: e.target.result,
uploadDate: new Date().toISOString()
};
job.weekly.protocols.push(protocol);
uploadedCount++;
// Render after last file
if (uploadedCount + errorCount === files.length) {
renderWeeklyProtocols();
saveJobs();
showToast(`✅ ${uploadedCount} Protokoll(e) hochgeladen`);
}
};
reader.readAsDataURL(file);
});
event.target.value = '';
}
function renderWeeklyProtocols() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.weekly || !job.weekly.protocols || job.weekly.protocols.length === 0) {
document.getElementById('weekly-protocols-container').innerHTML = `
<div class="gallery-empty">
<i class="fas fa-file-pdf"></i>
<p>Noch keine Protokolle hochgeladen</p>
<p style="font-size: 0.85rem; margin-top: 8px;">Laden Sie wöchentliche Berichte und Protokolle hoch.</p>
</div>
`;
return;
}
const html = job.weekly.protocols.map((protocol, index) => `
<div style="padding: 16px; background: #F5F5F5; border-radius: 8px; border: 2px solid #FFE000; margin-bottom: 12px;">
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;">
<div style="display: flex; align-items: center; gap: 12px; flex: 1; min-width: 200px;">
<i class="fas fa-file-pdf" style="color: #E74C3C; font-size: 32px;"></i>
<div style="flex: 1;">
<div style="font-weight: 600; color: #111111; margin-bottom: 4px;">${protocol.name}</div>
<div style="font-size: 13px; color: #666;">
${(protocol.size / 1024).toFixed(1)} KB •
${new Date(protocol.uploadDate).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-small" onclick="downloadWeeklyProtocol(${index})" title="Herunterladen">
<i class="fas fa-download"></i>
</button>
<button class="btn btn-secondary btn-small" onclick="removeWeeklyProtocol(${index})" title="Löschen">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`).join('');
document.getElementById('weekly-protocols-container').innerHTML = html;
}
function downloadWeeklyProtocol(index) {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.weekly || !job.weekly.protocols || !job.weekly.protocols[index]) return;
const protocol = job.weekly.protocols[index];
const link = document.createElement('a');
link.href = protocol.data;
link.download = protocol.name;
link.click();
showToast('✅ Protokoll heruntergeladen');
}
function removeWeeklyProtocol(index) {
if (!confirm('Protokoll wirklich löschen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.weekly || !job.weekly.protocols) return;
const protocolName = job.weekly.protocols[index].name;
job.weekly.protocols.splice(index, 1);
renderWeeklyProtocols();
saveJobs();
showToast(`✅ ${protocolName} gelöscht`);
}
// Briefing document upload functions
// 1. Briefing Customer
function handleBriefingCustomerUpload(event) {
const file = event.target.files[0];
if (!file) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
// Check file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
showToast('❌ Datei zu groß! Max. 10 MB');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
if (!job.briefing) {
job.briefing = {};
}
job.briefing.customerDoc = e.target.result;
job.briefing.customerDocName = file.name;
job.briefing.customerDocSize = file.size;
// Show preview
document.getElementById('briefing-customer-preview').style.display = 'block';
document.getElementById('briefing-customer-name').textContent = file.name;
document.getElementById('briefing-customer-size').textContent = (file.size / 1024).toFixed(1) + ' KB';
saveJobs();
showToast('✅ Briefing Kunde hochgeladen');
console.log('✅ Briefing Kunde hochgeladen:', file.name);
};
reader.onerror = function() {
showToast('❌ Fehler beim Lesen der Datei');
console.error('❌ FileReader Fehler');
};
reader.readAsDataURL(file);
}
function downloadBriefingCustomer() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.briefing || !job.briefing.customerDoc) return;
const link = document.createElement('a');
link.href = job.briefing.customerDoc;
link.download = job.briefing.customerDocName || 'briefing-kunde.pdf';
link.click();
showToast('✅ Briefing heruntergeladen');
}
function removeBriefingCustomer() {
if (!confirm('Briefing Kunde wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.briefing) return;
job.briefing.customerDoc = null;
job.briefing.customerDocName = null;
job.briefing.customerDocSize = null;
document.getElementById('briefing-customer-preview').style.display = 'none';
document.getElementById('briefing-customer-upload').value = '';
saveJobs();
showToast('✅ Briefing entfernt');
}
// 2. Brandbook / Logo
function handleBriefingBrandUpload(event) {
const file = event.target.files[0];
if (!file) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (file.size > 10 * 1024 * 1024) {
showToast('❌ Datei zu groß! Max. 10 MB');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
if (!job.briefing) {
job.briefing = {};
}
job.briefing.brandDoc = e.target.result;
job.briefing.brandDocName = file.name;
job.briefing.brandDocSize = file.size;
document.getElementById('briefing-brand-preview').style.display = 'block';
document.getElementById('briefing-brand-name').textContent = file.name;
document.getElementById('briefing-brand-size').textContent = (file.size / 1024).toFixed(1) + ' KB';
saveJobs();
showToast('✅ Brandbook hochgeladen');
console.log('✅ Brandbook hochgeladen:', file.name);
};
reader.onerror = function() {
showToast('❌ Fehler beim Lesen der Datei');
};
reader.readAsDataURL(file);
}
function downloadBriefingBrand() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.briefing || !job.briefing.brandDoc) return;
const link = document.createElement('a');
link.href = job.briefing.brandDoc;
link.download = job.briefing.brandDocName || 'brandbook.pdf';
link.click();
showToast('✅ Brandbook heruntergeladen');
}
function removeBriefingBrand() {
if (!confirm('Brandbook wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.briefing) return;
job.briefing.brandDoc = null;
job.briefing.brandDocName = null;
job.briefing.brandDocSize = null;
document.getElementById('briefing-brand-preview').style.display = 'none';
document.getElementById('briefing-brand-upload').value = '';
saveJobs();
showToast('✅ Brandbook entfernt');
}
// 3. Standplan
function handleBriefingPlanUpload(event) {
const file = event.target.files[0];
if (!file) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (file.size > 10 * 1024 * 1024) {
showToast('❌ Datei zu groß! Max. 10 MB');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
if (!job.briefing) {
job.briefing = {};
}
job.briefing.planDoc = e.target.result;
job.briefing.planDocName = file.name;
job.briefing.planDocSize = file.size;
document.getElementById('briefing-plan-preview').style.display = 'block';
document.getElementById('briefing-plan-name').textContent = file.name;
document.getElementById('briefing-plan-size').textContent = (file.size / 1024).toFixed(1) + ' KB';
saveJobs();
showToast('✅ Standplan hochgeladen');
console.log('✅ Standplan hochgeladen:', file.name);
};
reader.onerror = function() {
showToast('❌ Fehler beim Lesen der Datei');
};
reader.readAsDataURL(file);
}
function downloadBriefingPlan() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.briefing || !job.briefing.planDoc) return;
const link = document.createElement('a');
link.href = job.briefing.planDoc;
link.download = job.briefing.planDocName || 'standplan.pdf';
link.click();
showToast('✅ Standplan heruntergeladen');
}
function removeBriefingPlan() {
if (!confirm('Standplan wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.briefing) return;
job.briefing.planDoc = null;
job.briefing.planDocName = null;
job.briefing.planDocSize = null;
document.getElementById('briefing-plan-preview').style.display = 'none';
document.getElementById('briefing-plan-upload').value = '';
saveJobs();
showToast('✅ Standplan entfernt');
}
// 4. Weitere Dokumente
function handleBriefingAdditionalUpload(event) {
const file = event.target.files[0];
if (!file) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job) return;
if (file.size > 10 * 1024 * 1024) {
showToast('❌ Datei zu groß! Max. 10 MB');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
if (!job.briefing) {
job.briefing = {};
}
job.briefing.additionalDoc = e.target.result;
job.briefing.additionalDocName = file.name;
job.briefing.additionalDocSize = file.size;
document.getElementById('briefing-additional-preview').style.display = 'block';
document.getElementById('briefing-additional-name').textContent = file.name;
document.getElementById('briefing-additional-size').textContent = (file.size / 1024).toFixed(1) + ' KB';
saveJobs();
showToast('✅ Dokument hochgeladen');
console.log('✅ Weiteres Dokument hochgeladen:', file.name);
};
reader.onerror = function() {
showToast('❌ Fehler beim Lesen der Datei');
};
reader.readAsDataURL(file);
}
function downloadBriefingAdditional() {
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.briefing || !job.briefing.additionalDoc) return;
const link = document.createElement('a');
link.href = job.briefing.additionalDoc;
link.download = job.briefing.additionalDocName || 'dokument.pdf';
link.click();
showToast('✅ Dokument heruntergeladen');
}
function removeBriefingAdditional() {
if (!confirm('Dokument wirklich entfernen?')) return;
const job = jobs.find(j => j.id === currentJobId);
if (!job || !job.briefing) return;
job.briefing.additionalDoc = null;
job.briefing.additionalDocName = null;
job.briefing.additionalDocSize = null;
document.getElementById('briefing-additional-preview').style.display = 'none';
document.getElementById('briefing-additional-upload').value = '';
saveJobs();
showToast('✅ Dokument entfernt');
}
// Open Google Maps with location
function openGoogleMaps() {
const location = document.getElementById('edit-location').value;
if (!location || location.trim() === '') {
showToast('❌ Bitte geben Sie einen Ort ein');
return;
}
// Encode location for URL
const encodedLocation = encodeURIComponent(location.trim());
// Open Google Maps in new tab
const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${encodedLocation}`;
window.open(mapsUrl, '_blank');
showToast('✅ Google Maps geöffnet');
}
// Show toast notification
function showToast(message) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toast-message');
toastMessage.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// Test-Funktion die wir direkt aufrufen können
window.testButton = function() {
alert('Button funktioniert!');
console.log('✅ Button Test erfolgreich!');
};
</script>
</body>
</html>