feat: Equipment-System, Buchungsbearbeitung, Kundenadresse, LexOffice-Fix

- Vintage Modell hinzugefuegt
- Equipment Multi-Select (Neue Buchung + Bearbeitung)
- Kundenadresse in Formularen
- Bearbeiten-Seite fuer Buchungen
- Abbau-Zeiten in Formularen und Uebersicht
- Vertrag PDF nur bei Privatkunden
- LexOffice Kontakt-Erstellung Fix (BUSINESS)
- Zurueck-Pfeil auf Touren-Seite
This commit is contained in:
Julia Wehden
2026-03-19 16:21:55 +01:00
parent 0b6e429329
commit a2c95c70e7
79 changed files with 7396 additions and 538 deletions

209
sync-nextcloud-bookings.js Normal file
View File

@@ -0,0 +1,209 @@
const { PrismaClient } = require('@prisma/client');
const { createDAVClient } = require('tsdav');
const fs = require('fs');
const path = require('path');
const prisma = new PrismaClient();
function loadEnv() {
const envPath = path.join(__dirname, '.env');
const envContent = fs.readFileSync(envPath, 'utf-8');
const env = {};
envContent.split('\n').forEach(line => {
const match = line.match(/^([^=:#]+)=(.*)$/);
if (match) {
const key = match[1].trim();
let value = match[2].trim();
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
value = value.replace(/\\"/g, '"');
value = value.replace(/\\\\/g, '\\');
} else if (value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1);
}
env[key] = value;
}
});
return env;
}
async function syncBookingToCalendar(client, booking, env) {
try {
const calendars = await client.fetchCalendars();
if (calendars.length === 0) {
throw new Error('No calendars found in Nextcloud');
}
// Suche nach "Buchungen" Kalender, sonst verwende ersten
let calendar = calendars.find((cal) =>
cal.displayName?.toLowerCase().includes('buchung')
);
if (!calendar) {
console.log(` ⚠️ Kein "Buchungen"-Kalender gefunden, verwende: ${calendars[0].displayName}`);
calendar = calendars[0];
}
const startDate = new Date(booking.eventDate);
const endDate = new Date(startDate.getTime() + 4 * 60 * 60 * 1000);
const event = {
summary: `${booking.customerName} - ${booking.location?.name || 'Unbekannt'}`,
description: `
Buchung #${booking.bookingNumber || booking.id}
Kunde: ${booking.customerName}
E-Mail: ${booking.customerEmail}
Telefon: ${booking.customerPhone || 'N/A'}
Event-Location: ${booking.eventLocation || booking.eventAddress}
Status: ${booking.status}
Fotobox: ${booking.photobox?.model || 'Keine Box'}
Standort: ${booking.location?.name || 'Unbekannt'}
Preis: ${booking.calculatedPrice || 0}
`.trim(),
location: `${booking.eventAddress || ''}, ${booking.eventZip || ''} ${booking.eventCity || ''}`.trim(),
start: startDate.toISOString(),
end: endDate.toISOString(),
uid: `savethemoment-booking-${booking.id}`,
};
// Create iCalendar format
const icsContent = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//SaveTheMoment Atlas//EN
BEGIN:VEVENT
UID:${event.uid}
DTSTAMP:${new Date().toISOString().replace(/[-:]/g, '').split('.')[0]}Z
DTSTART:${startDate.toISOString().replace(/[-:]/g, '').split('.')[0]}Z
DTEND:${endDate.toISOString().replace(/[-:]/g, '').split('.')[0]}Z
SUMMARY:${event.summary}
DESCRIPTION:${event.description.replace(/\n/g, '\\n')}
LOCATION:${event.location}
STATUS:CONFIRMED
END:VEVENT
END:VCALENDAR`;
// Check if event already exists
const calendarObjects = await client.fetchCalendarObjects({
calendar,
});
const existingEvent = calendarObjects.find(obj =>
obj.data && obj.data.includes(`UID:${event.uid}`)
);
if (existingEvent) {
// Update existing event
await client.updateCalendarObject({
calendarObject: {
url: existingEvent.url,
data: icsContent,
etag: existingEvent.etag,
},
});
} else {
// Create new event
await client.createCalendarObject({
calendar,
filename: `${event.uid}.ics`,
iCalString: icsContent,
});
}
return true;
} catch (error) {
throw error;
}
}
async function syncExistingBookings() {
console.log('🔄 Synchronisiere bestehende Buchungen mit Nextcloud...\n');
const env = loadEnv();
const serverUrl = env.NEXTCLOUD_URL;
const username = env.NEXTCLOUD_USERNAME;
const password = env.NEXTCLOUD_PASSWORD;
if (!serverUrl || !username || !password) {
console.error('❌ Missing Nextcloud credentials in .env file!');
process.exit(1);
}
try {
console.log('⏳ Verbinde mit Nextcloud...');
const client = await createDAVClient({
serverUrl: `${serverUrl}/remote.php/dav`,
credentials: {
username,
password,
},
authMethod: 'Basic',
defaultAccountType: 'caldav',
});
console.log('✅ Nextcloud-Verbindung hergestellt!\n');
// Hole alle bestätigten Buchungen
const bookings = await prisma.booking.findMany({
where: {
status: {
in: ['RESERVED', 'CONFIRMED'],
},
},
include: {
location: true,
photobox: true,
},
orderBy: {
eventDate: 'asc',
},
});
console.log(`📊 Gefunden: ${bookings.length} Buchungen\n`);
if (bookings.length === 0) {
console.log(' Keine Buchungen zum Synchronisieren gefunden.');
return;
}
let synced = 0;
let failed = 0;
for (const booking of bookings) {
try {
console.log(`📅 Synchronisiere: ${booking.bookingNumber} - ${booking.customerName}`);
console.log(` Event: ${new Date(booking.eventDate).toLocaleDateString('de-DE')}`);
console.log(` Standort: ${booking.location?.name || 'Unbekannt'}`);
await syncBookingToCalendar(client, booking, env);
synced++;
console.log(` ✅ Erfolgreich!\n`);
} catch (error) {
failed++;
console.error(` ❌ Fehler: ${error.message}\n`);
}
}
console.log('─'.repeat(50));
console.log(`✅ Erfolgreich synchronisiert: ${synced}`);
console.log(`❌ Fehlgeschlagen: ${failed}`);
console.log(`📊 Gesamt: ${bookings.length}`);
console.log('\n🎉 Synchronisation abgeschlossen!');
console.log(' Prüfen Sie Nextcloud → Kalender "Buchungen (Dennis Forte)"');
} catch (error) {
console.error('❌ Fehler beim Synchronisieren:', error);
throw error;
} finally {
await prisma.$disconnect();
}
}
syncExistingBookings()
.catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});