Files
SPA-landing/fotoupload/index.html

856 lines
29 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>PowerToolsX - Foto-Upload</title>
<style>
:root {
--bg-primary: #1E1E1E;
--bg-secondary: #2D2D30;
--bg-tertiary: #3C3C3C;
--text-primary: #E0E0E0;
--text-secondary: #A0A0A0;
--accent: #0E639C;
--accent-hover: #1177BB;
--success: #00B454;
--warning: #FFB900;
--error: #E81123;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
padding: 16px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 24px;
}
header h1 {
font-size: 1.5rem;
margin-bottom: 4px;
}
header .projekt-name {
color: var(--accent);
font-size: 1.1rem;
}
.section {
background: var(--bg-secondary);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.section-title {
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* IFC-Hierarchie Dropdowns */
.ifc-select-group {
margin-bottom: 12px;
}
.ifc-select-group label {
display: block;
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 4px;
}
.ifc-select-group select {
width: 100%;
padding: 12px;
background: var(--bg-tertiary);
border: 1px solid #555;
border-radius: 6px;
color: var(--text-primary);
font-size: 1rem;
}
.ifc-select-group select:disabled {
opacity: 0.5;
}
.ifc-path {
background: var(--bg-tertiary);
padding: 8px 12px;
border-radius: 4px;
font-size: 0.85rem;
color: var(--text-secondary);
margin-top: 8px;
}
/* PropertySets */
.pset-group {
border: 1px solid #444;
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
}
.pset-group h4 {
font-size: 0.9rem;
margin-bottom: 8px;
color: var(--accent);
}
.pset-property {
margin-bottom: 8px;
}
.pset-property label {
display: block;
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: 2px;
}
.pset-property select,
.pset-property input {
width: 100%;
padding: 8px;
background: var(--bg-tertiary);
border: 1px solid #555;
border-radius: 4px;
color: var(--text-primary);
font-size: 0.95rem;
}
/* Foto-Capture */
.capture-area {
text-align: center;
}
.camera-preview {
width: 100%;
max-height: 300px;
background: #000;
border-radius: 8px;
margin-bottom: 12px;
display: none;
}
.preview-image {
width: 100%;
max-height: 300px;
object-fit: contain;
border-radius: 8px;
margin-bottom: 12px;
display: none;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 14px 24px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
gap: 8px;
}
.btn-primary {
background: var(--accent);
color: white;
width: 100%;
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-primary:disabled {
background: #555;
cursor: not-allowed;
}
.btn-success {
background: var(--success);
color: white;
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-row {
display: flex;
gap: 12px;
margin-top: 12px;
}
.btn-row .btn {
flex: 1;
}
/* Beschreibung */
textarea {
width: 100%;
padding: 12px;
background: var(--bg-tertiary);
border: 1px solid #555;
border-radius: 6px;
color: var(--text-primary);
font-size: 1rem;
resize: vertical;
min-height: 80px;
}
/* Status */
.status {
text-align: center;
padding: 12px;
border-radius: 6px;
margin-top: 16px;
}
.status.success {
background: rgba(0, 180, 84, 0.2);
color: var(--success);
}
.status.error {
background: rgba(232, 17, 35, 0.2);
color: var(--error);
}
.status.info {
background: rgba(14, 99, 156, 0.2);
color: var(--accent);
}
/* Upload Counter */
.upload-counter {
text-align: center;
font-size: 0.9rem;
color: var(--text-secondary);
margin-top: 16px;
}
.upload-counter strong {
color: var(--success);
font-size: 1.2rem;
}
/* Hidden file input */
#fileInput {
display: none;
}
/* Loading Overlay */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-overlay.active {
display: flex;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid var(--bg-tertiary);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* No Session */
.no-session {
text-align: center;
padding: 48px 24px;
}
.no-session h2 {
margin-bottom: 16px;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>PowerToolsX Foto-Upload</h1>
<div class="projekt-name" id="projektName">Wird geladen...</div>
</header>
<div id="noSession" class="no-session" style="display: none;">
<h2>Keine aktive Session</h2>
<p>Bitte scannen Sie den QR-Code in PowerToolsX um eine Upload-Session zu starten.</p>
</div>
<div id="mainContent" style="display: none;">
<!-- IFC-Struktur Auswahl -->
<div class="section">
<div class="section-title">Ort im Bauwerk</div>
<div class="ifc-select-group">
<label>Gebaude</label>
<select id="selectBuilding" onchange="onBuildingChanged()">
<option value="">-- Gebaude wahlen --</option>
</select>
</div>
<div class="ifc-select-group">
<label>Geschoss</label>
<select id="selectStorey" onchange="onStoreyChanged()" disabled>
<option value="">-- Geschoss wahlen --</option>
</select>
</div>
<div class="ifc-select-group">
<label>Raum</label>
<select id="selectSpace" onchange="onSpaceChanged()" disabled>
<option value="">-- Raum wahlen --</option>
</select>
</div>
<div class="ifc-path" id="ifcPath" style="display: none;">
<strong>Pfad:</strong> <span id="pathText"></span>
</div>
</div>
<!-- PropertySets -->
<div class="section" id="propertySetsSection">
<div class="section-title">Eigenschaften</div>
<div id="propertySetsContainer"></div>
</div>
<!-- Beschreibung -->
<div class="section">
<div class="section-title">Beschreibung</div>
<textarea id="description" placeholder="Optionale Beschreibung zum Foto..."></textarea>
</div>
<!-- Foto aufnehmen -->
<div class="section">
<div class="section-title">Foto</div>
<div class="capture-area">
<video id="cameraPreview" class="camera-preview" autoplay playsinline></video>
<img id="previewImage" class="preview-image" alt="Vorschau">
<input type="file" id="fileInput" accept="image/*" capture="environment" onchange="onFileSelected(event)">
<button class="btn btn-primary" id="btnCapture" onclick="openCamera()">
<span>Foto aufnehmen</span>
</button>
<div class="btn-row" id="previewButtons" style="display: none;">
<button class="btn btn-secondary" onclick="resetCapture()">Neu</button>
<button class="btn btn-success" onclick="uploadPhoto()">Hochladen</button>
</div>
</div>
</div>
<!-- Status -->
<div id="statusMessage" class="status info" style="display: none;"></div>
<!-- Upload Counter -->
<div class="upload-counter">
Hochgeladene Fotos: <strong id="uploadCount">0</strong>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div class="loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
</div>
<script>
// Konfiguration
const DATA_URL = 'https://docs.artetui.de/backups/fotoupload';
// Session-Daten
let sessionId = null;
let strukturData = null;
let selectedElement = null;
let capturedImageData = null;
let uploadCount = 0;
// Initialisierung
document.addEventListener('DOMContentLoaded', async () => {
// Session-ID aus URL holen
const params = new URLSearchParams(window.location.search);
sessionId = params.get('session');
if (!sessionId) {
document.getElementById('noSession').style.display = 'block';
return;
}
// Struktur laden
await loadStruktur();
});
// IFC-Struktur vom Server laden
async function loadStruktur() {
showLoading(true);
try {
const response = await fetch(`${DATA_URL}/struktur_${sessionId}.json`);
if (!response.ok) {
throw new Error('Struktur nicht gefunden');
}
strukturData = await response.json();
// UI aktualisieren
document.getElementById('projektName').textContent = strukturData.ProjektName || 'Projekt';
document.getElementById('mainContent').style.display = 'block';
// Gebaude-Dropdown fullen
populateBuildingSelect();
// PropertySets generieren
generatePropertySets();
} catch (error) {
console.error('Fehler beim Laden der Struktur:', error);
document.getElementById('projektName').textContent = 'Fehler beim Laden';
showStatus('Struktur konnte nicht geladen werden. Bitte erneut versuchen.', 'error');
document.getElementById('mainContent').style.display = 'block';
} finally {
showLoading(false);
}
}
// Gebaude-Dropdown fullen
function populateBuildingSelect() {
const select = document.getElementById('selectBuilding');
select.innerHTML = '<option value="">-- Gebaude wahlen --</option>';
if (!strukturData || !strukturData.Elemente) return;
// Finde alle IfcBuilding Elemente
const buildings = findElementsByType(strukturData.Elemente, 'IfcBuilding');
buildings.forEach(b => {
const option = document.createElement('option');
option.value = b.GlobalId;
option.textContent = `${b.Icon || ''} ${b.Name}`;
option.dataset.element = JSON.stringify(b);
select.appendChild(option);
});
}
// Rekursiv Elemente nach Typ finden
function findElementsByType(elements, type) {
let result = [];
for (const el of elements) {
if (el.IfcType === type) {
result.push(el);
}
if (el.Children && el.Children.length > 0) {
result = result.concat(findElementsByType(el.Children, type));
}
}
return result;
}
// Element nach GlobalId finden
function findElementById(elements, globalId) {
for (const el of elements) {
if (el.GlobalId === globalId) {
return el;
}
if (el.Children && el.Children.length > 0) {
const found = findElementById(el.Children, globalId);
if (found) return found;
}
}
return null;
}
// Gebaude geandert
function onBuildingChanged() {
const select = document.getElementById('selectBuilding');
const storeySelect = document.getElementById('selectStorey');
const spaceSelect = document.getElementById('selectSpace');
// Geschoss und Raum zurucksetzen
storeySelect.innerHTML = '<option value="">-- Geschoss wahlen --</option>';
storeySelect.disabled = true;
spaceSelect.innerHTML = '<option value="">-- Raum wahlen --</option>';
spaceSelect.disabled = true;
selectedElement = null;
updatePath();
if (!select.value) return;
// Gebaude finden
const building = findElementById(strukturData.Elemente, select.value);
if (!building) return;
// Geschosse dieses Gebaudes finden
const storeys = findElementsByType(building.Children || [], 'IfcBuildingStorey');
storeys.forEach(s => {
const option = document.createElement('option');
option.value = s.GlobalId;
option.textContent = `${s.Icon || ''} ${s.Name}`;
option.dataset.element = JSON.stringify(s);
storeySelect.appendChild(option);
});
storeySelect.disabled = storeys.length === 0;
}
// Geschoss geandert
function onStoreyChanged() {
const buildingSelect = document.getElementById('selectBuilding');
const storeySelect = document.getElementById('selectStorey');
const spaceSelect = document.getElementById('selectSpace');
// Raum zurucksetzen
spaceSelect.innerHTML = '<option value="">-- Raum wahlen --</option>';
spaceSelect.disabled = true;
if (!storeySelect.value) {
selectedElement = null;
updatePath();
return;
}
// Gebaude und Geschoss finden
const building = findElementById(strukturData.Elemente, buildingSelect.value);
const storey = findElementById(building?.Children || [], storeySelect.value);
if (!storey) {
selectedElement = null;
updatePath();
return;
}
// Raume dieses Geschosses finden
const spaces = findElementsByType(storey.Children || [], 'IfcSpace');
spaces.forEach(s => {
const option = document.createElement('option');
option.value = s.GlobalId;
option.textContent = `${s.Icon || ''} ${s.Name}`;
option.dataset.element = JSON.stringify(s);
spaceSelect.appendChild(option);
});
spaceSelect.disabled = spaces.length === 0;
// Geschoss als ausgewahltes Element setzen (falls kein Raum gewahlt)
selectedElement = storey;
updatePath();
}
// Raum geandert
function onSpaceChanged() {
const buildingSelect = document.getElementById('selectBuilding');
const storeySelect = document.getElementById('selectStorey');
const spaceSelect = document.getElementById('selectSpace');
if (!spaceSelect.value) {
// Zurück zum Geschoss
const building = findElementById(strukturData.Elemente, buildingSelect.value);
selectedElement = findElementById(building?.Children || [], storeySelect.value);
} else {
// Raum finden
const building = findElementById(strukturData.Elemente, buildingSelect.value);
const storey = findElementById(building?.Children || [], storeySelect.value);
selectedElement = findElementById(storey?.Children || [], spaceSelect.value);
}
updatePath();
}
// Pfad-Anzeige aktualisieren
function updatePath() {
const pathDiv = document.getElementById('ifcPath');
const pathText = document.getElementById('pathText');
if (!selectedElement) {
pathDiv.style.display = 'none';
return;
}
// Pfad aus den Dropdowns bauen
const parts = [];
const buildingSelect = document.getElementById('selectBuilding');
const storeySelect = document.getElementById('selectStorey');
const spaceSelect = document.getElementById('selectSpace');
if (buildingSelect.value) {
const opt = buildingSelect.selectedOptions[0];
parts.push(opt.textContent.trim());
}
if (storeySelect.value) {
const opt = storeySelect.selectedOptions[0];
parts.push(opt.textContent.trim());
}
if (spaceSelect.value) {
const opt = spaceSelect.selectedOptions[0];
parts.push(opt.textContent.trim());
}
pathText.textContent = parts.join(' > ');
pathDiv.style.display = 'block';
}
// PropertySets generieren
function generatePropertySets() {
const container = document.getElementById('propertySetsContainer');
container.innerHTML = '';
if (!strukturData || !strukturData.PropertySets) {
document.getElementById('propertySetsSection').style.display = 'none';
return;
}
strukturData.PropertySets.forEach(pset => {
const group = document.createElement('div');
group.className = 'pset-group';
group.innerHTML = `<h4>${pset.Label}</h4>`;
pset.Properties.forEach(prop => {
const propDiv = document.createElement('div');
propDiv.className = 'pset-property';
let inputHtml = '';
if (prop.Type === 'select' && prop.Options) {
inputHtml = `<select id="pset_${pset.Name}_${prop.Name}">
<option value="">-- Wahlen --</option>
${prop.Options.map(o => `<option value="${o}">${o}</option>`).join('')}
</select>`;
} else {
inputHtml = `<input type="text" id="pset_${pset.Name}_${prop.Name}" placeholder="${prop.Label}">`;
}
propDiv.innerHTML = `
<label>${prop.Label}</label>
${inputHtml}
`;
group.appendChild(propDiv);
});
container.appendChild(group);
});
document.getElementById('propertySetsSection').style.display = 'block';
}
// PropertySet-Werte sammeln
function collectPropertyValues() {
const values = {};
if (!strukturData || !strukturData.PropertySets) return values;
strukturData.PropertySets.forEach(pset => {
pset.Properties.forEach(prop => {
const input = document.getElementById(`pset_${pset.Name}_${prop.Name}`);
if (input && input.value) {
values[`${pset.Name}.${prop.Name}`] = input.value;
}
});
});
return values;
}
// Kamera offnen
function openCamera() {
document.getElementById('fileInput').click();
}
// Datei ausgewahlt
function onFileSelected(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
capturedImageData = e.target.result;
// Vorschau anzeigen
const img = document.getElementById('previewImage');
img.src = capturedImageData;
img.style.display = 'block';
// Buttons umschalten
document.getElementById('btnCapture').style.display = 'none';
document.getElementById('previewButtons').style.display = 'flex';
};
reader.readAsDataURL(file);
}
// Aufnahme zurucksetzen
function resetCapture() {
capturedImageData = null;
document.getElementById('previewImage').style.display = 'none';
document.getElementById('previewImage').src = '';
document.getElementById('btnCapture').style.display = 'block';
document.getElementById('previewButtons').style.display = 'none';
document.getElementById('fileInput').value = '';
}
// Foto hochladen
async function uploadPhoto() {
if (!capturedImageData) {
showStatus('Bitte erst ein Foto aufnehmen', 'error');
return;
}
showLoading(true);
try {
// Upload-Daten zusammenstellen
const timestamp = Date.now().toString(16);
const fileName = `${sessionId}_${timestamp}.json`;
// Pfad bauen
const pathParts = [];
const buildingSelect = document.getElementById('selectBuilding');
const storeySelect = document.getElementById('selectStorey');
const spaceSelect = document.getElementById('selectSpace');
if (buildingSelect.value) pathParts.push(buildingSelect.selectedOptions[0].textContent.trim());
if (storeySelect.value) pathParts.push(storeySelect.selectedOptions[0].textContent.trim());
if (spaceSelect.value) pathParts.push(spaceSelect.selectedOptions[0].textContent.trim());
const uploadData = {
Timestamp: new Date().toISOString(),
Filename: `foto_${Date.now()}.jpg`,
Description: document.getElementById('description').value,
Base64: capturedImageData,
Size: capturedImageData.length,
Type: 'image/jpeg',
ElementGlobalId: selectedElement?.GlobalId || null,
ElementPath: pathParts.join(' > '),
Properties: collectPropertyValues()
};
// Per WebDAV PUT hochladen
const response = await fetch(`${DATA_URL}/${fileName}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(uploadData)
});
if (!response.ok) {
throw new Error(`Upload fehlgeschlagen: ${response.status}`);
}
// Erfolg
uploadCount++;
document.getElementById('uploadCount').textContent = uploadCount;
showStatus('Foto erfolgreich hochgeladen!', 'success');
// Zurucksetzen
resetCapture();
document.getElementById('description').value = '';
} catch (error) {
console.error('Upload-Fehler:', error);
showStatus('Upload fehlgeschlagen: ' + error.message, 'error');
} finally {
showLoading(false);
}
}
// Status-Meldung anzeigen
function showStatus(message, type) {
const statusDiv = document.getElementById('statusMessage');
statusDiv.textContent = message;
statusDiv.className = `status ${type}`;
statusDiv.style.display = 'block';
// Nach 5 Sekunden ausblenden
setTimeout(() => {
statusDiv.style.display = 'none';
}, 5000);
}
// Loading-Overlay
function showLoading(show) {
document.getElementById('loadingOverlay').classList.toggle('active', show);
}
</script>
<footer style="text-align:center;padding:1rem;margin-top:2rem;border-top:1px solid #e5e7eb;font-size:0.85rem;color:#6b7280;">
<a href="#" onclick="openImpressum();return false;" style="color:#6b7280;text-decoration:none;">Impressum</a>
<span style="color:#d1d5db;margin:0 0.5rem;">|</span>
<a href="#" onclick="openDatenschutz();return false;" style="color:#6b7280;text-decoration:none;">Datenschutz</a>
</footer>
<!-- IMPRESSUM MODAL -->
<div class="legal-modal" id="impressumModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center;">
<div style="background:white;width:90%;max-width:900px;height:90vh;border-radius:12px;margin:20px;overflow:hidden;display:flex;flex-direction:column;">
<div style="padding:0.75rem 1.5rem;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<h2 style="margin:0;font-size:1.25rem;color:#1f2937;">Impressum</h2>
<button onclick="closeImpressum()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6b7280;line-height:1;">&times;</button>
</div>
<iframe src="/legal/impressum.html" style="flex:1;width:100%;border:none;"></iframe>
</div>
</div>
<!-- DATENSCHUTZ MODAL -->
<div class="legal-modal" id="datenschutzModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center;">
<div style="background:white;width:90%;max-width:900px;height:90vh;border-radius:12px;margin:20px;overflow:hidden;display:flex;flex-direction:column;">
<div style="padding:0.75rem 1.5rem;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<h2 style="margin:0;font-size:1.25rem;color:#1f2937;">Datenschutz</h2>
<button onclick="closeDatenschutz()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6b7280;line-height:1;">&times;</button>
</div>
<iframe src="/legal/datenschutz.html" style="flex:1;width:100%;border:none;"></iframe>
</div>
</div>
<script>
function openImpressum(){document.getElementById("impressumModal").style.display="flex";}
function closeImpressum(){document.getElementById("impressumModal").style.display="none";}
function openDatenschutz(){document.getElementById("datenschutzModal").style.display="flex";}
function closeDatenschutz(){document.getElementById("datenschutzModal").style.display="none";}
document.addEventListener("keydown",function(e){if(e.key==="Escape"){closeImpressum();closeDatenschutz();}});
["impressumModal","datenschutzModal"].forEach(function(id){
var el=document.getElementById(id);
if(el)el.addEventListener("click",function(e){if(e.target===this)this.style.display="none";});
});
</script>
</body>
</html>