Initial commit - docs.artetui.de SPAs (ohne Subprojekte)

This commit is contained in:
2025-12-14 19:42:32 +00:00
commit d56a39abab
13 changed files with 17994 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# OS files
.DS_Store
Thumbs.db
# Editor files
*.swp
*.swo
*~
# Node modules (falls vorhanden)
node_modules/
# Temp files
*.tmp
*.bak
symbols/
kostenschaetzung/
schadendokumentation/
zeitwert/

5492
angebotsdatenbank/Pos.json Normal file

File diff suppressed because it is too large Load Diff

4071
angebotsdatenbank/index.html Normal file

File diff suppressed because it is too large Load Diff

115
api/backup.php Normal file
View File

@@ -0,0 +1,115 @@
<?php
/**
* SPA Backup API - Speichert SPA-Backups in Nextcloud
* URL: https://docs.artetui.de/api/backup.php
*/
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://docs.artetui.de');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Preflight für CORS
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// Nur POST erlauben
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
// Konfiguration aus .env laden
$envFile = __DIR__ . '/spa-backup-api.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
list($key, $value) = explode('=', $line, 2);
putenv(trim($key) . '=' . trim($value));
}
}
$NEXTCLOUD_URL = 'https://cloud.artetui.de';
$NEXTCLOUD_USER = getenv('NEXTCLOUD_USER') ?: 'lars.munkes';
$NEXTCLOUD_PASS = getenv('NEXTCLOUD_PASS');
// Prüfen ob Credentials konfiguriert sind
if (!$NEXTCLOUD_PASS) {
http_response_code(500);
echo json_encode(['error' => 'Nextcloud credentials not configured. Create spa-backup-api.env file.']);
exit;
}
// Request-Daten lesen
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['app']) || !isset($input['data'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing app or data parameter']);
exit;
}
$app = $input['app'];
$backupData = $input['data'];
// Validierung: Nur erlaubte Apps
$allowedApps = ['schadenprofi', 'angebotsdatenbank', 'kostenschaetzung', 'zeitwert'];
if (!in_array($app, $allowedApps)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid app name']);
exit;
}
// Backup-Dateiname mit Timestamp
$timestamp = date('Y-m-d_H-i-s');
$filename = "{$app}-backup-{$timestamp}.json";
// Nextcloud WebDAV Upload
$webdavUrl = "{$NEXTCLOUD_URL}/remote.php/dav/files/{$NEXTCLOUD_USER}/Backups/{$app}/{$filename}";
// Verzeichnis erstellen falls nicht existiert
$dirUrl = "{$NEXTCLOUD_URL}/remote.php/dav/files/{$NEXTCLOUD_USER}/Backups/{$app}";
$ch = curl_init($dirUrl);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'MKCOL');
curl_setopt($ch, CURLOPT_USERPWD, "{$NEXTCLOUD_USER}:{$NEXTCLOUD_PASS}");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);
// Backup hochladen
$ch = curl_init($webdavUrl);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_USERPWD, "{$NEXTCLOUD_USER}:{$NEXTCLOUD_PASS}");
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($backupData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Erfolg prüfen
if ($httpCode >= 200 && $httpCode < 300) {
echo json_encode([
'success' => true,
'message' => 'Backup erfolgreich gespeichert',
'filename' => $filename,
'path' => "/Backups/{$app}/{$filename}",
'timestamp' => $timestamp,
'url' => "{$NEXTCLOUD_URL}/f/" // Nextcloud sharing link base
]);
} else {
http_response_code(500);
echo json_encode([
'error' => 'Nextcloud upload failed',
'http_code' => $httpCode
]);
}
?>

View File

@@ -0,0 +1,11 @@
# Nextcloud Credentials für SPA Backup API
# Kopiere diese Datei zu spa-backup-api.env und fülle die Werte aus
# Nextcloud Username
NEXTCLOUD_USER=lars.munkes
# Nextcloud App Password (NICHT dein Login-Passwort!)
# Erstelle ein App-Password unter: https://cloud.artetui.de/settings/user/security
# -> Geräte & Sitzungen -> Neues App-Passwort erstellen
# Name: "SPA Backup API"
NEXTCLOUD_PASS=xxxx-xxxx-xxxx-xxxx

1367
dictation/index.html Normal file

File diff suppressed because it is too large Load Diff

855
fotoupload/index.html Normal file
View File

@@ -0,0 +1,855 @@
<!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>

BIN
geo-calc/dokumentation.pdf Normal file

Binary file not shown.

2908
geo-calc/index.html Normal file

File diff suppressed because it is too large Load Diff

337
index.html Normal file
View File

@@ -0,0 +1,337 @@
<!DOCTYPE html>
<html lang="de" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPA Platform - artetui.de</title>
<style>
:root {
--bg-primary: #f5f1e8;
--bg-secondary: #ffffff;
--bg-card: #ffffff;
--text-primary: #3e3126;
--text-secondary: #6b5d4f;
--text-muted: #9d8b7a;
--accent-primary: #c47b5a;
--accent-secondary: #d4a574;
--accent-hover: #b36848;
--border: #e6dfd5;
--shadow: rgba(62, 49, 38, 0.08);
--shadow-hover: rgba(62, 49, 38, 0.15);
}
[data-theme="dark"] {
--bg-primary: #1a1612;
--bg-secondary: #2a2319;
--bg-card: #2a2319;
--text-primary: #f5f1e8;
--text-secondary: #c9bfb3;
--text-muted: #9d8b7a;
--accent-primary: #d4a574;
--accent-secondary: #c47b5a;
--accent-hover: #e6b889;
--border: #3e3126;
--shadow: rgba(0, 0, 0, 0.3);
--shadow-hover: rgba(0, 0, 0, 0.5);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
.header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
padding: 1.5rem 2rem;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 2px 8px var(--shadow);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo-section h1 { font-size: 2rem; font-weight: 700; }
.logo-section p { color: var(--text-secondary); font-size: 0.95rem; }
.theme-toggle {
background: var(--bg-card);
border: 2px solid var(--border);
border-radius: 2rem;
padding: 0.5rem 1rem;
cursor: pointer;
color: var(--text-primary);
font-size: 0.9rem;
}
.container { max-width: 1400px; margin: 0 auto; padding: 3rem 2rem; }
.apps-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}
.app-card {
background: var(--bg-card);
border-radius: 1rem;
border: 1px solid var(--border);
padding: 2rem;
text-decoration: none;
color: var(--text-primary);
display: flex;
flex-direction: column;
transition: all 0.3s ease;
box-shadow: 0 2px 8px var(--shadow);
}
.app-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px var(--shadow-hover);
border-color: var(--accent-primary);
}
.app-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
.app-icon {
width: 3rem; height: 3rem;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
border-radius: 0.75rem;
display: flex; align-items: center; justify-content: center;
font-size: 1.5rem;
}
.app-card h2 { font-size: 1.5rem; font-weight: 600; }
.app-card p { color: var(--text-secondary); line-height: 1.7; margin: 1rem 0; flex-grow: 1; }
.app-meta {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid var(--border);
font-size: 0.875rem;
}
.app-status {
background: linear-gradient(135deg, #4ade80, #22c55e);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-weight: 500;
font-size: 0.8rem;
}
.app-version { color: var(--text-muted); }
.doc-link {
display: inline-block;
margin-top: 1rem;
padding: 0.5rem 1rem;
background: #007acc;
color: white !important;
border-radius: 0.5rem;
text-decoration: none;
font-size: 0.85rem;
text-align: center;
}
.doc-link:hover { background: #005fa3; }
.info-section {
background: var(--bg-card);
border-radius: 1rem;
border: 1px solid var(--border);
padding: 2rem;
box-shadow: 0 2px 8px var(--shadow);
}
.info-section h3 {
color: var(--accent-primary);
font-size: 1.25rem;
margin-bottom: 1.5rem;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.info-item {
padding: 1rem;
background: var(--bg-primary);
border-radius: 0.5rem;
border: 1px solid var(--border);
}
.info-item strong { display: block; margin-bottom: 0.5rem; }
.info-item p { color: var(--text-secondary); font-size: 0.9rem; }
.footer {
text-align: center;
padding: 2rem;
color: var(--text-muted);
font-size: 0.875rem;
border-top: 1px solid var(--border);
margin-top: 3rem;
}
</style>
</head>
<body>
<header class="header">
<div class="header-content">
<div class="logo-section">
<h1>SPA Platform</h1>
<p>Zentrale Anwendungsplattform</p>
</div>
<button class="theme-toggle" onclick="toggleTheme()">🌓 Theme</button>
</div>
</header>
<main class="container">
<div class="apps-grid">
<a href="/schadendokumentation/" class="app-card">
<div class="app-header">
<div class="app-icon">📋</div>
<h2>Schadendokumentation</h2>
</div>
<p>Erfassung und Verwaltung von Schadenfällen mit umfassender Dokumentation, Bildverwaltung und Exportfunktionen.</p>
<div class="app-meta">
<span class="app-status">Aktiv</span>
<span class="app-version">Build 24.11.2025</span>
</div>
</a>
<a href="/kostenschaetzung/" class="app-card">
<div class="app-header">
<div class="app-icon">💰</div>
<h2>Kostenschätzung</h2>
</div>
<p>Professionelles Tool zur Erstellung und Verwaltung von Kostenschätzungen für Bauprojekte und Sanierungen.</p>
<div class="app-meta">
<span class="app-status">Aktiv</span>
<span class="app-version">Build 09.11.2025</span>
</div>
</a>
<a href="/zeitwert/" class="app-card">
<div class="app-header">
<div class="app-icon">⏱️</div>
<h2>Zeitwert</h2>
</div>
<p>Berechnung und Dokumentation von Zeitwerten für Versicherungen und Gutachten mit automatischer Wertermittlung.</p>
<div class="app-meta">
<span class="app-status">Aktiv</span>
<span class="app-version">Build 22.11.2025</span>
</div>
</a>
<a href="/angebotsdatenbank/" class="app-card">
<div class="app-header">
<div class="app-icon">💼</div>
<h2>Angebotsdatenbank</h2>
</div>
<p>Intelligente Positionsverwaltung mit STLB-Codes, DIN 276 Klassifikation und umfassender Suchfunktion für Bauleistungen.</p>
<div class="app-meta">
<span class="app-status">Aktiv</span>
<span class="app-version">Build 25.11.2025</span>
</div>
</a>
<a href="/zeitutility/" class="app-card">
<div class="app-header">
<div class="app-icon">🕐</div>
<h2>ZeitUtility</h2>
</div>
<p>Zeit- und Datumsmanagement mit Zeitstempel-Generator, Zeitzonen-Konverter und Datumsrechner für präzise Zeitberechnungen.</p>
<div class="app-meta">
<span class="app-status">Aktiv</span>
<span class="app-version">Build 25.11.2025</span>
</div>
</a>
<div class="app-card" style="cursor:default;">
<a href="/geo-calc/" style="text-decoration:none;color:inherit;">
<div class="app-header">
<div class="app-icon">📐</div>
<h2>GEO-Calc</h2>
</div>
<p>3D Geometrie-Rechner für rechtwinklige Dreiecke mit interaktiver Visualisierung und automatischer Berechnung aller Maße.</p>
<div class="app-meta">
<span class="app-status">Aktiv</span>
<span class="app-version">V7</span>
</div>
</a>
<a href="/geo-calc/dokumentation.pdf" target="_blank" class="doc-link">📄 Anleitung (PDF)</a>
</div>
<a href="/symbols/" class="app-card">
<div class="app-header">
<div class="app-icon">🔣</div>
<h2>Symbolbibliothek</h2>
</div>
<p>148+ SVG & DXF Symbole für Schadensgutachten und Vermessung. Kategorien: Schäden, Werkzeuge, Bauteile, Möbel, Vermessung und mehr.</p>
<div class="app-meta">
<span class="app-status">Aktiv</span>
<span class="app-version">v2.0</span>
</div>
</a>
</div>
<section class="info-section">
<h3>📦 Backup & Datenverwaltung</h3>
<div class="info-grid">
<div class="info-item">
<strong>Lokale Speicherung</strong>
<p>Alle Daten werden sicher in deinem Browser gespeichert (IndexedDB). Niemand außer dir hat Zugriff auf deine Daten.</p>
</div>
<div class="info-item">
<strong>Export & Backup</strong>
<p>Nutze die Export-Funktion in jeder App, um Backups herunterzuladen. Daten als JSON oder CSV exportierbar.</p>
</div>
<div class="info-item">
<strong>Import & Restore</strong>
<p>Lade Backups direkt in der App hoch, um Daten wiederherzustellen. Vollständiger Datenimport möglich.</p>
</div>
</div>
</section>
</main>
<footer class="footer">
<p>artetui.de · SPA Platform · Alle Daten werden lokal gespeichert</p>
<p style="text-align:center;margin-top:0.5rem;font-size:0.85rem;">
<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>
</p>
</footer>
<script>
function toggleTheme() {
const html = document.documentElement;
const newTheme = html.getAttribute("data-theme") === "light" ? "dark" : "light";
html.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
}
(function() {
const savedTheme = localStorage.getItem("theme") || "light";
document.documentElement.setAttribute("data-theme", savedTheme);
})();
</script>
<!-- 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>

83
legal/datenschutz.html Normal file
View File

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Datenschutz - Architekturbuero Lars Munkes</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.7;
color: #374151;
background: #fff;
padding: 2rem;
max-width: 800px;
margin: 0 auto;
}
h1 { color: #1f2937; margin-bottom: 1.5rem; font-size: 2rem; }
h2 { color: #2563eb; margin: 2rem 0 1rem 0; font-size: 1.25rem; }
h3 { color: #2563eb; margin: 1.5rem 0 0.75rem 0; font-size: 1.1rem; }
p { margin-bottom: 1rem; }
ul { margin: 0 0 1rem 1.5rem; }
li { margin-bottom: 0.5rem; }
a { color: #2563eb; text-decoration: none; }
a:hover { text-decoration: underline; }
hr { border: none; border-top: 1px solid #e5e7eb; margin: 2rem 0; }
.updated { text-align: center; color: #9ca3af; font-size: 0.9rem; margin-top: 2rem; }
</style>
</head>
<body>
<h1>Datenschutzerklaerung</h1>
<h2>1. Verantwortlicher</h2>
<p>Verantwortlich fuer die Datenverarbeitung auf dieser Website ist:</p>
<p>Architekturbuero Lars Munkes<br>
Oldenstorf 3<br>
18276 Lohmen<br>
Deutschland<br>
E-Mail: <a href="mailto:info@artetui.de">info@artetui.de</a></p>
<h2>2. Erhebung und Speicherung personenbezogener Daten</h2>
<h3>Server-Logfiles</h3>
<p>Bei jedem Zugriff auf diese Website werden durch den Webserver automatisch Daten erfasst und in Server-Logfiles gespeichert:</p>
<ul>
<li>IP-Adresse des anfragenden Rechners</li>
<li>Datum und Uhrzeit des Zugriffs</li>
<li>Name und URL der abgerufenen Datei</li>
<li>Website, von der aus der Zugriff erfolgt (Referrer-URL)</li>
<li>Verwendeter Browser und ggf. das Betriebssystem</li>
</ul>
<p>Diese Daten werden zur Gewaehrleistung eines reibungslosen Verbindungsaufbaus und zur Systemsicherheit verarbeitet. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO.</p>
<h3>Lokale Speicherung (localStorage)</h3>
<p>Diese Webanwendung speichert Daten ausschliesslich lokal in Ihrem Browser (localStorage). Es findet keine Uebertragung dieser Daten an unsere Server oder Dritte statt. Sie koennen die lokal gespeicherten Daten jederzeit in Ihren Browsereinstellungen loeschen.</p>
<h2>3. Cookies</h2>
<p>Diese Website verwendet keine Tracking-Cookies. Es werden ausschliesslich technisch notwendige Browser-Funktionen genutzt.</p>
<h2>4. Weitergabe von Daten</h2>
<p>Eine Uebermittlung Ihrer persoenlichen Daten an Dritte zu anderen als den im Folgenden aufgefuehrten Zwecken findet nicht statt.</p>
<h2>5. Ihre Rechte</h2>
<p>Sie haben gegenueber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:</p>
<ul>
<li>Recht auf Auskunft (Art. 15 DSGVO)</li>
<li>Recht auf Berichtigung (Art. 16 DSGVO)</li>
<li>Recht auf Loeschung (Art. 17 DSGVO)</li>
<li>Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO)</li>
<li>Recht auf Datenuebertragbarkeit (Art. 20 DSGVO)</li>
<li>Recht auf Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)</li>
</ul>
<p>Sie haben zudem das Recht, sich bei einer Datenschutz-Aufsichtsbehoerde ueber die Verarbeitung Ihrer personenbezogenen Daten zu beschweren.</p>
<h2>6. Datensicherheit</h2>
<p>Diese Website nutzt aus Sicherheitsgruenden eine SSL- bzw. TLS-Verschluesselung. Eine verschluesselte Verbindung erkennen Sie daran, dass die Adresszeile des Browsers von "http://" auf "https://" wechselt und an dem Schloss-Symbol in Ihrer Browserzeile.</p>
<h2>7. Aktualitaet und Aenderung dieser Datenschutzerklaerung</h2>
<p>Diese Datenschutzerklaerung ist aktuell gueltig und hat den Stand Dezember 2024. Durch die Weiterentwicklung unserer Website oder aufgrund geaenderter gesetzlicher bzw. behoerdlicher Vorgaben kann es notwendig werden, diese Datenschutzerklaerung zu aendern.</p>
<p class="updated">Stand: Dezember 2024</p>
</body>
</html>

76
legal/impressum.html Normal file
View File

@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impressum - Architekturbuero Lars Munkes</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.7;
color: #374151;
background: #fff;
padding: 2rem;
max-width: 800px;
margin: 0 auto;
}
h1 { color: #1f2937; margin-bottom: 1.5rem; font-size: 2rem; }
h2 { color: #2563eb; margin: 2rem 0 1rem 0; font-size: 1.25rem; }
h3 { color: #2563eb; margin: 1.5rem 0 0.75rem 0; font-size: 1.1rem; }
p { margin-bottom: 1rem; }
a { color: #2563eb; text-decoration: none; }
a:hover { text-decoration: underline; }
hr { border: none; border-top: 1px solid #e5e7eb; margin: 2rem 0; }
.company { font-size: 1.25rem; font-weight: 600; color: #1f2937; margin-bottom: 1.5rem; }
.section { margin-bottom: 1.5rem; }
.label { font-weight: 600; color: #1f2937; }
.copyright { text-align: center; color: #9ca3af; font-size: 0.9rem; margin-top: 2rem; }
</style>
</head>
<body>
<h1>Impressum</h1>
<p class="company">Architekturbuero Lars Munkes</p>
<div class="section">
<p class="label">Anschrift:</p>
<p>Oldenstorf 3<br>18276 Lohmen<br>Deutschland</p>
</div>
<div class="section">
<p class="label">Kontakt:</p>
<p>E-Mail: <a href="mailto:info@artetui.de">info@artetui.de</a></p>
</div>
<div class="section">
<p class="label">Umsatzsteuer-Identifikationsnummer:</p>
<p>DE233951802</p>
</div>
<div class="section">
<p class="label">Berufsbezeichnung:</p>
<p>Architekt<br>Verliehen in Deutschland</p>
</div>
<div class="section">
<p class="label">Zustaendige Kammer:</p>
<p>Architektenkammer Mecklenburg-Vorpommern</p>
</div>
<hr>
<h2>Haftungsausschluss</h2>
<h3>Haftung fuer Inhalte</h3>
<p>Die Inhalte dieser Seiten wurden mit groesster Sorgfalt erstellt. Fuer die Richtigkeit, Vollstaendigkeit und Aktualitaet der Inhalte kann jedoch keine Gewaehr uebernommen werden. Als Diensteanbieter sind wir gemaess § 7 Abs. 1 TMG fuer eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich.</p>
<h3>Haftung fuer Links</h3>
<p>Diese Seiten enthalten Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb koennen wir fuer diese fremden Inhalte auch keine Gewaehr uebernehmen. Fuer die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.</p>
<h3>Urheberrecht</h3>
<p>Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfaeltigung, Bearbeitung, Verbreitung und jede Art der Verwertung ausserhalb der Grenzen des Urheberrechtes beduerfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.</p>
<p class="copyright">&copy; 2024 Architekturbuero Lars Munkes</p>
</body>
</html>

2660
zeitutility/index.html Normal file

File diff suppressed because it is too large Load Diff