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

View File

@@ -0,0 +1,73 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function checkEmailSync() {
console.log('🔍 Prüfe E-Mail-Sync Status...\n');
const luebeck = await prisma.location.findUnique({
where: { slug: 'luebeck' },
select: {
name: true,
emailSyncEnabled: true,
lastEmailSync: true,
imapHost: true,
imapUser: true,
},
});
if (!luebeck) {
console.log('❌ Lübeck Location nicht gefunden!');
return;
}
console.log('📍 Lübeck:');
console.log(` E-Mail-Sync: ${luebeck.emailSyncEnabled ? '✅ Aktiviert' : '❌ Deaktiviert'}`);
console.log(` IMAP konfiguriert: ${luebeck.imapHost ? '✅ Ja' : '❌ Nein'}`);
console.log(` Letzter Sync: ${luebeck.lastEmailSync ? new Date(luebeck.lastEmailSync).toLocaleString('de-DE') : 'Noch nie'}`);
if (!luebeck.emailSyncEnabled) {
console.log('\n⚠ E-Mail-Sync ist deaktiviert!');
console.log(' Buchungen werden NICHT automatisch erfasst.');
console.log(' → Entweder manuell im Dashboard anlegen');
console.log(' → Oder E-Mail-Sync aktivieren\n');
} else {
console.log('\n✅ E-Mail-Sync ist aktiviert!');
console.log(' → E-Mails werden automatisch abgerufen (Cron-Job oder manuell)');
console.log(' → Buchungen erscheinen im Dashboard\n');
}
// Prüfe ob es neue Buchungen gibt
const recentBookings = await prisma.booking.findMany({
where: {
locationId: luebeck ? undefined : undefined,
},
orderBy: {
createdAt: 'desc',
},
take: 5,
select: {
id: true,
bookingNumber: true,
customerName: true,
createdAt: true,
location: {
select: { name: true },
},
},
});
if (recentBookings.length > 0) {
console.log('📊 Letzte 5 Buchungen:');
recentBookings.forEach((booking, idx) => {
console.log(` ${idx + 1}. ${booking.bookingNumber} - ${booking.customerName} (${booking.location.name})`);
console.log(` Erstellt: ${new Date(booking.createdAt).toLocaleString('de-DE')}`);
});
} else {
console.log('📊 Keine Buchungen gefunden.');
}
await prisma.$disconnect();
}
checkEmailSync();

View File

@@ -0,0 +1,31 @@
import { prisma } from '../lib/prisma';
async function main() {
console.log('🔍 Prüfe Equipment-Einträge...\n');
const count = await prisma.equipment.count();
console.log(`📊 Anzahl Equipment-Einträge: ${count}`);
if (count > 0) {
const equipment = await prisma.equipment.findMany();
console.log('\n📦 Vorhandene Equipment:');
console.table(equipment.map(e => ({
id: e.id.slice(0, 8) + '...',
name: e.name,
price: e.price,
lexofficeId: e.lexofficeArticleId?.slice(0, 8) + '...' || 'null',
})));
} else {
console.log('\n⚠ Keine Equipment-Einträge in der Datenbank!');
console.log(' Du musst zuerst Equipment-Artikel anlegen.');
}
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -0,0 +1,56 @@
import { prisma } from '../lib/prisma';
import { lexofficeService } from '../lib/lexoffice';
async function main() {
console.log('🔍 Prüfe LexOffice Quotation Status...\n');
// Hole die Test-Buchung
const booking = await prisma.booking.findFirst({
where: { bookingNumber: 'STM-2511-9237' },
select: {
id: true,
bookingNumber: true,
lexofficeOfferId: true,
},
});
if (!booking) {
console.log('❌ Buchung nicht gefunden');
return;
}
console.log('📋 Buchung:', booking.bookingNumber);
console.log('🆔 LexOffice Offer ID:', booking.lexofficeOfferId);
if (!booking.lexofficeOfferId) {
console.log('❌ Keine LexOffice Angebots-ID vorhanden');
return;
}
try {
console.log('\n🔍 Lade Quotation Details von LexOffice...');
const quotation = await lexofficeService.getQuotation(booking.lexofficeOfferId);
console.log('\n📊 Quotation Details:');
console.log(' Voucher Number:', quotation.voucherNumber);
console.log(' Created:', quotation.createdDate);
console.log(' Updated:', quotation.updatedDate);
console.log('\n Full Response:', JSON.stringify(quotation, null, 2));
console.log('\n📄 Versuche PDF Download...');
const pdf = await lexofficeService.getQuotationPDF(booking.lexofficeOfferId);
console.log('✅ PDF erfolgreich heruntergeladen! Größe:', pdf.length, 'bytes');
} catch (error: any) {
console.error('\n❌ Fehler:', error.message);
}
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -0,0 +1,40 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🧹 Entferne LexOffice-Artikel-IDs...');
const result = await prisma.priceConfig.updateMany({
data: {
lexofficeArticleId: null,
lexofficeArticleIdWithFlat: null,
lexofficeKmFlatArticleId: null,
lexofficeKmExtraArticleId: null,
},
});
console.log(`${result.count} PriceConfigs aktualisiert`);
const configs = await prisma.priceConfig.findMany({
select: {
id: true,
model: true,
basePrice: true,
lexofficeArticleId: true,
lexofficeArticleIdWithFlat: true,
},
});
console.log('\n📊 Aktuelle PriceConfigs:');
console.table(configs);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -0,0 +1,77 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function configureLuebeckLocation() {
console.log('🔧 Konfiguriere Lübeck Location & Kilometerpauschalen...\n');
try {
const location = await prisma.location.findUnique({
where: { slug: 'luebeck' },
});
if (!location) {
console.error('❌ Lübeck Location nicht gefunden!');
process.exit(1);
}
await prisma.location.update({
where: { id: location.id },
data: {
warehouseAddress: 'Wahmstraße 83',
warehouseZip: '23552',
warehouseCity: 'Lübeck',
},
});
console.log('✅ Lager-Adresse gesetzt: Wahmstraße 83, 23552 Lübeck\n');
const models = ['VINTAGE_SMILE', 'VINTAGE_PHOTOS', 'NOSTALGIE', 'MAGIC_MIRROR'];
for (const model of models) {
const existing = await prisma.priceConfig.findUnique({
where: {
locationId_model: {
locationId: location.id,
model: model as any,
},
},
});
if (existing) {
await prisma.priceConfig.update({
where: {
locationId_model: {
locationId: location.id,
model: model as any,
},
},
data: {
kmFlatRate: 60.0,
kmFlatRateUpTo: 15,
pricePerKm: 0.40,
kmMultiplier: 4,
},
});
console.log(`${model}: Kilometerpauschale aktualisiert`);
} else {
console.log(`⚠️ ${model}: Keine PriceConfig gefunden, überspringe...`);
}
}
console.log('\n🎉 Lübeck Location erfolgreich konfiguriert!');
console.log('\nKonfiguration:');
console.log(' 📍 Lager: Wahmstraße 83, 23552 Lübeck');
console.log(' 💰 Pauschale bis 15km: 60,00€ brutto');
console.log(' 💰 Darüber: 0,40€ netto/km × 4 Strecken');
} catch (error) {
console.error('❌ Fehler:', error);
throw error;
} finally {
await prisma.$disconnect();
}
}
configureLuebeckLocation();

View File

@@ -0,0 +1,35 @@
import { prisma } from '../lib/prisma';
import { lexofficeService } from '../lib/lexoffice';
async function main() {
console.log('🔄 Finalisiere bestehende Quotation...\n');
const quotationId = 'c66b3347-4411-449c-897f-e0d84cb42601';
try {
console.log('📤 Rufe PUT /quotations/{id}/pursue auf...');
const result = await lexofficeService.finalizeQuotation(quotationId);
console.log('✅ Erfolgreich!', result);
console.log('\n🔍 Prüfe neuen Status...');
const quotation = await lexofficeService.getQuotation(quotationId);
console.log('Status:', quotation.voucherStatus);
console.log('\n📄 Versuche PDF Download...');
const pdf = await lexofficeService.getQuotationPDF(quotationId);
console.log('✅ PDF erfolgreich! Größe:', pdf.length, 'bytes');
} catch (error: any) {
console.error('❌ Fehler:', error.message);
console.error('Details:', error);
}
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -0,0 +1,89 @@
const fs = require('fs');
const path = require('path');
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);
} else if (value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1);
}
env[key] = value;
}
});
return env;
}
async function listLexOfficeArticles() {
console.log('🔍 Lade LexOffice Artikel...\n');
const env = loadEnv();
const apiKey = env.LEXOFFICE_API_KEY;
if (!apiKey) {
console.error('❌ LEXOFFICE_API_KEY nicht in .env gefunden!');
process.exit(1);
}
try {
const response = await fetch('https://api.lexoffice.io/v1/articles?page=0&size=100', {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Accept': 'application/json',
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`LexOffice API Error: ${response.status} - ${errorText}`);
}
const data = await response.json();
if (!data.content || data.content.length === 0) {
console.log('⚠️ Keine Artikel gefunden.');
return;
}
console.log(`✅ Gefunden: ${data.content.length} Artikel\n`);
console.log('─'.repeat(80));
data.content.forEach((article, index) => {
console.log(`\n${index + 1}. ${article.title || article.articleNumber || 'Unbekannt'}`);
console.log(` ID: ${article.id}`);
console.log(` Artikelnummer: ${article.articleNumber || 'N/A'}`);
console.log(` Typ: ${article.type || 'N/A'}`);
if (article.price) {
const netAmount = article.price.netAmount || 0;
console.log(` Preis: ${netAmount.toFixed(2)}€ netto`);
}
if (article.description) {
const desc = article.description.substring(0, 60);
console.log(` Beschreibung: ${desc}${article.description.length > 60 ? '...' : ''}`);
}
});
console.log('\n' + '─'.repeat(80));
console.log('\n💡 Kopieren Sie die IDs für Ihr Produkt-Mapping!');
console.log('📝 Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\n');
} catch (error) {
console.error('❌ Fehler:', error.message);
}
}
listLexOfficeArticles();

View File

@@ -0,0 +1,53 @@
import { config } from 'dotenv';
config();
async function listLexOfficeArticles() {
console.log('🔍 Lade LexOffice Artikel...\n');
try {
const response = await fetch('https://api.lexoffice.io/v1/articles?page=0&size=100', {
method: 'GET',
headers: {
'Authorization': `Bearer ${process.env.LEXOFFICE_API_KEY}`,
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`LexOffice API Error: ${response.status}`);
}
const data = await response.json();
if (!data.content || data.content.length === 0) {
console.log('⚠️ Keine Artikel gefunden.');
return;
}
console.log(`✅ Gefunden: ${data.content.length} Artikel\n`);
console.log('─'.repeat(80));
data.content.forEach((article: any, index: number) => {
console.log(`\n${index + 1}. ${article.title || article.articleNumber || 'Unbekannt'}`);
console.log(` ID: ${article.id}`);
console.log(` Artikelnummer: ${article.articleNumber || 'N/A'}`);
console.log(` Typ: ${article.type || 'N/A'}`);
if (article.price) {
console.log(` Preis: ${article.price.netAmount || 0}€ netto`);
}
if (article.description) {
console.log(` Beschreibung: ${article.description.substring(0, 60)}...`);
}
});
console.log('\n' + '─'.repeat(80));
console.log('\n💡 Kopieren Sie die IDs für Ihr Produkt-Mapping!');
} catch (error: any) {
console.error('❌ Fehler:', error.message);
}
}
listLexOfficeArticles();

View File

@@ -0,0 +1,69 @@
import { PrismaClient } from '@prisma/client';
import { emailSyncService } from '../lib/email-sync';
const prisma = new PrismaClient();
async function manualEmailSync() {
console.log('🔄 Manueller E-Mail-Sync gestartet...\n');
try {
const locations = await prisma.location.findMany({
where: { emailSyncEnabled: true },
select: { id: true, name: true, slug: true },
});
if (locations.length === 0) {
console.log('⚠️ Keine Locations mit aktiviertem E-Mail-Sync gefunden.\n');
console.log('💡 Bitte aktivieren Sie E-Mail-Sync in den Location-Einstellungen.\n');
return;
}
console.log(`📍 Gefunden: ${locations.length} Location(s) mit E-Mail-Sync\n`);
let totalNewEmails = 0;
let totalNewBookings = 0;
const results = [];
for (const location of locations) {
console.log(`🔄 ${location.name} (${location.slug})...`);
const result = await emailSyncService.syncLocationEmails(location.id);
if (result.success) {
console.log(`${result.newEmails} neue E-Mails`);
console.log(`${result.newBookings} neue Buchungen\n`);
totalNewEmails += result.newEmails;
totalNewBookings += result.newBookings;
} else {
console.log(` ❌ Fehler:`);
result.errors.forEach(err => console.log(` - ${err}`));
console.log('');
}
results.push({
location: location.name,
...result,
});
}
console.log('─'.repeat(60));
console.log('📊 Zusammenfassung:');
console.log(` Locations: ${locations.length}`);
console.log(` Neue E-Mails: ${totalNewEmails}`);
console.log(` Neue Buchungen: ${totalNewBookings}`);
console.log('─'.repeat(60));
if (totalNewBookings > 0) {
console.log('\n✅ Neue Buchungen im Dashboard verfügbar!');
console.log(' → http://localhost:3000/dashboard/bookings\n');
}
} catch (error: any) {
console.error('❌ Fehler beim E-Mail-Sync:', error.message);
throw error;
} finally {
await prisma.$disconnect();
}
}
manualEmailSync();

View File

@@ -0,0 +1,42 @@
import { prisma } from '../lib/prisma';
async function main() {
console.log('🗑️ Lösche alte LexOffice IDs für erneuten Test...\n');
const booking = await prisma.booking.findFirst({
where: { bookingNumber: 'STM-2511-9237' },
});
if (!booking) {
console.log('❌ Buchung nicht gefunden');
return;
}
console.log('📋 Buchung:', booking.bookingNumber);
console.log('🆔 Aktuelle LexOffice Offer ID:', booking.lexofficeOfferId);
if (booking.lexofficeOfferId) {
await prisma.booking.update({
where: { id: booking.id },
data: {
lexofficeOfferId: null,
lexofficeContactId: null,
},
});
console.log('\n✅ LexOffice IDs gelöscht!');
console.log(' Der "Automation starten" Button sollte jetzt wieder erscheinen.');
console.log('⚠️ WICHTIG: Lösche die alte Quotation in LexOffice manuell (AN-221646)');
} else {
console.log('\n✅ Keine LexOffice IDs vorhanden - nichts zu löschen.');
}
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -0,0 +1,68 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🔄 Stelle LexOffice-Artikel-IDs wieder her...\n');
// 1. Fotobox-Artikel-IDs für ALLE PriceConfigs setzen
const photoboxUpdate = await prisma.priceConfig.updateMany({
data: {
lexofficeArticleId: '5d9d3716-f81e-4e46-b5cf-13988f489cc2', // ohne Druckflatrate
lexofficeArticleIdWithFlat: '3f26c02c-d705-41a6-9b49-2e2e96e77036', // mit Druckflatrate
},
});
console.log(`${photoboxUpdate.count} PriceConfigs mit Fotobox-Artikel-IDs aktualisiert`);
// 2. Kilometerpauschale für Lübeck
const luebeckLocation = await prisma.location.findFirst({
where: { city: 'Lübeck' },
});
if (luebeckLocation) {
const kmUpdate = await prisma.priceConfig.updateMany({
where: { locationId: luebeckLocation.id },
data: {
lexofficeKmFlatArticleId: 'd3e2b21b-e899-412d-b53e-c82a0a94fcfa',
},
});
console.log(`${kmUpdate.count} PriceConfigs für Lübeck mit Kilometerpauschale-ID aktualisiert`);
}
// 3. Equipment-IDs prüfen
const equipment = await prisma.equipment.findMany({
select: {
id: true,
name: true,
lexofficeArticleId: true,
},
});
console.log('\n📦 Vorhandene Equipment-Artikel:');
console.table(equipment);
// 4. Finale Übersicht
console.log('\n📊 Finale PriceConfig-Übersicht:');
const configs = await prisma.priceConfig.findMany({
include: {
location: true,
},
});
console.table(configs.map(c => ({
model: c.model,
city: c.location?.city,
articleId: c.lexofficeArticleId?.slice(0, 8) + '...',
articleIdWithFlat: c.lexofficeArticleIdWithFlat?.slice(0, 8) + '...',
kmFlatArticleId: c.lexofficeKmFlatArticleId?.slice(0, 8) + '...' || 'null',
})));
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -0,0 +1,114 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🔄 Setze korrekte LexOffice-Artikel-IDs pro Modell...\n');
// Mapping: Modell -> Artikel-IDs
const modelArticleIds: Record<string, { withoutFlat: string; withFlat: string }> = {
'VINTAGE_SMILE': {
withoutFlat: '5d9d3716-f81e-4e46-b5cf-13988f489cc2',
withFlat: '3f26c02c-d705-41a6-9b49-2e2e96e77036',
},
'VINTAGE_PHOTOS': {
withoutFlat: '5d9d3716-f81e-4e46-b5cf-13988f489cc2',
withFlat: '3f26c02c-d705-41a6-9b49-2e2e96e77036',
},
'NOSTALGIE': {
withoutFlat: '8954bd20-570c-4a7d-9ac3-8ee756652a89',
withFlat: '701bd150-48c0-4937-b628-f4a754d86264',
},
'MAGIC_MIRROR': {
withoutFlat: '72bbe51b-c0bb-437a-963b-248cb105553a',
withFlat: '39ec59e7-57a6-4d6d-80f6-645e589c4b2c',
},
};
// Update pro Modell
for (const [model, ids] of Object.entries(modelArticleIds)) {
const result = await prisma.priceConfig.updateMany({
where: { model },
data: {
lexofficeArticleId: ids.withoutFlat,
lexofficeArticleIdWithFlat: ids.withFlat,
},
});
console.log(`${model}: ${result.count} PriceConfigs aktualisiert`);
}
// Kilometerpauschale für Lübeck
const luebeck = await prisma.location.findFirst({ where: { city: 'Lübeck' } });
if (luebeck) {
const kmResult = await prisma.priceConfig.updateMany({
where: { locationId: luebeck.id },
data: { lexofficeKmFlatArticleId: 'd3e2b21b-e899-412d-b53e-c82a0a94fcfa' },
});
console.log(`✅ Lübeck Kilometerpauschale: ${kmResult.count} PriceConfigs aktualisiert`);
}
console.log('\n📦 Equipment-Artikel-IDs aktualisieren...\n');
// Equipment-Mapping: Name -> Artikel-ID
const equipmentArticleIds: Record<string, string> = {
'Accessoires': 'c62d4dad-4f04-4330-9019-f9804bb43ddc',
'VIP-Bänder': 'e3942394-94d7-45ea-a31b-a1b035f6f34e',
'Erweiterte Druck-Flat DIY': '774aba7f-2c20-4d65-b8d0-4793f27d2d71',
'Kartenspiel Fotoboxaufgaben': '17d563fe-5f00-4591-a414-d013d7ce68e0',
'Roter Teppich': 'd138da65-4e23-4a88-813a-1f3725a75a15',
'Service-Techniker vor Ort': 'ec972616-8bc5-4334-876d-b442838a8bbf',
};
for (const [name, articleId] of Object.entries(equipmentArticleIds)) {
try {
const equipment = await prisma.equipment.findFirst({
where: { name: { contains: name, mode: 'insensitive' } },
});
if (equipment) {
await prisma.equipment.update({
where: { id: equipment.id },
data: { lexofficeArticleId: articleId },
});
console.log(`${name}: Artikel-ID gesetzt`);
} else {
console.log(`⚠️ ${name}: Equipment nicht gefunden - übersprungen`);
}
} catch (error) {
console.log(`⚠️ ${name}: Fehler - ${error}`);
}
}
console.log('\n📊 Finale Übersicht:');
const configs = await prisma.priceConfig.findMany({
include: { location: true },
orderBy: [{ location: { city: 'asc' } }, { model: 'asc' }],
});
console.table(configs.map(c => ({
city: c.location?.city,
model: c.model,
withoutFlat: c.lexofficeArticleId?.slice(0, 8) + '...',
withFlat: c.lexofficeArticleIdWithFlat?.slice(0, 8) + '...',
kmFlat: c.lexofficeKmFlatArticleId?.slice(0, 8) + '...' || 'null',
})));
const equipment = await prisma.equipment.findMany({
select: { name: true, lexofficeArticleId: true },
});
console.log('\n📦 Equipment mit Artikel-IDs:');
console.table(equipment.map(e => ({
name: e.name,
articleId: e.lexofficeArticleId?.slice(0, 8) + '...' || 'null',
})));
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -0,0 +1,113 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// LexOffice Artikel-IDs (von Dennis bereitgestellt)
const LEXOFFICE_ARTICLES = {
// Fotoboxen
VINTAGE: {
withFlat: '3f26c02c-d705-41a6-9b49-2e2e96e77036', // Fotobox Vintage Flat
withoutFlat: '5d9d3716-f81e-4e46-b5cf-13988f489cc2', // Fotobox Vintage
},
NOSTALGIE: {
withFlat: '701bd150-48c0-4937-b628-f4a754d86264', // Fotobox Nostalgie Flat
withoutFlat: '8954bd20-570c-4a7d-9ac3-8ee756652a89', // Fotobox Nostalgie
},
MAGIC_MIRROR: {
withFlat: '39ec59e7-57a6-4d6d-80f6-645e589c4b2c', // Fotobox Magic Mirror Flat
withoutFlat: '72bbe51b-c0bb-437a-963b-248cb105553a', // Fotobox Magic Mirror
},
// Equipment/Extras
ACCESSORIES: 'c62d4dad-4f04-4330-9019-f9804bb43ddc',
VIP_BARRIER: 'e3942394-94d7-45ea-a31b-a1b035f6f34e',
PRINT_FLAT_DIY: '774aba7f-2c20-4d65-b8d0-4793f27d2d71',
CARD_GAME: '17d563fe-5f00-4591-a414-d013d7ce68e0',
RED_CARPET: 'd138da65-4e23-4a88-813a-1f3725a75a15',
SERVICE_TECH: 'ec972616-8bc5-4334-876d-b442838a8bbf',
};
async function setupLexOfficeMapping() {
console.log('🔧 Konfiguriere LexOffice Artikel-IDs...\n');
try {
const locations = await prisma.location.findMany();
for (const location of locations) {
console.log(`📍 ${location.name}:`);
// VINTAGE_SMILE / VINTAGE_PHOTOS (gleiche Box)
const vintageConfigs = await prisma.priceConfig.findMany({
where: {
locationId: location.id,
model: {
in: ['VINTAGE_SMILE', 'VINTAGE_PHOTOS'],
},
},
});
for (const config of vintageConfigs) {
await prisma.priceConfig.update({
where: { id: config.id },
data: {
lexofficeArticleId: LEXOFFICE_ARTICLES.VINTAGE.withoutFlat,
lexofficeArticleIdWithFlat: LEXOFFICE_ARTICLES.VINTAGE.withFlat,
},
});
console.log(`${config.model}: LexOffice IDs gesetzt`);
}
// NOSTALGIE
const nostalgieConfig = await prisma.priceConfig.findFirst({
where: {
locationId: location.id,
model: 'NOSTALGIE',
},
});
if (nostalgieConfig) {
await prisma.priceConfig.update({
where: { id: nostalgieConfig.id },
data: {
lexofficeArticleId: LEXOFFICE_ARTICLES.NOSTALGIE.withoutFlat,
lexofficeArticleIdWithFlat: LEXOFFICE_ARTICLES.NOSTALGIE.withFlat,
},
});
console.log(` ✅ NOSTALGIE: LexOffice IDs gesetzt`);
}
// MAGIC_MIRROR
const magicMirrorConfig = await prisma.priceConfig.findFirst({
where: {
locationId: location.id,
model: 'MAGIC_MIRROR',
},
});
if (magicMirrorConfig) {
await prisma.priceConfig.update({
where: { id: magicMirrorConfig.id },
data: {
lexofficeArticleId: LEXOFFICE_ARTICLES.MAGIC_MIRROR.withoutFlat,
lexofficeArticleIdWithFlat: LEXOFFICE_ARTICLES.MAGIC_MIRROR.withFlat,
},
});
console.log(` ✅ MAGIC_MIRROR: LexOffice IDs gesetzt`);
}
}
console.log('\n🎉 LexOffice Artikel-Mapping erfolgreich konfiguriert!');
console.log('\n📝 Nächste Schritte:');
console.log(' 1. Testbuchung erstellen (mit/ohne Druckflatrate)');
console.log(' 2. LexOffice Angebot generieren');
console.log(' 3. Prüfen ob korrekte Artikel-IDs verwendet werden\n');
} catch (error) {
console.error('❌ Fehler:', error);
throw error;
} finally {
await prisma.$disconnect();
}
}
setupLexOfficeMapping();

View File

@@ -0,0 +1,73 @@
import { config } from 'dotenv';
config(); // Load .env file
import { PrismaClient } from '@prisma/client';
import { nextcloudCalendar } from '../lib/nextcloud-calendar.ts';
const prisma = new PrismaClient();
async function syncExistingBookings() {
console.log('🔄 Synchronisiere bestehende Buchungen mit Nextcloud...\n');
try {
// 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 nextcloudCalendar.syncBookingToCalendar(booking);
synced++;
console.log(` ✅ Erfolgreich!\n`);
} catch (error: any) {
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: any) {
console.error('❌ Fehler beim Synchronisieren:', error);
throw error;
} finally {
await prisma.$disconnect();
}
}
syncExistingBookings()
.catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

View File

@@ -0,0 +1,85 @@
import { lexofficeService } from '../lib/lexoffice';
async function main() {
console.log('🔍 Teste LexOffice Artikel-Zugriff...\n');
const testArticleIds = [
'5d9d3716-f81e-4e46-b5cf-13988f489cc2', // Vintage ohne Flat
'3f26c02c-d705-41a6-9b49-2e2e96e77036', // Vintage mit Flat
'8954bd20-570c-4a7d-9ac3-8ee756652a89', // Nostalgie ohne Flat
'701bd150-48c0-4937-b628-f4a754d86264', // Nostalgie mit Flat
];
for (const articleId of testArticleIds) {
try {
console.log(`\n📦 Teste Artikel-ID: ${articleId}`);
// Versuche den Artikel abzurufen
const response = await fetch(`https://api.lexoffice.io/v1/articles/${articleId}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${process.env.LEXOFFICE_API_KEY}`,
'Accept': 'application/json',
},
});
if (response.ok) {
const article = await response.json();
console.log('✅ Artikel gefunden:', article.title || article.name);
console.log(' Preis:', article.price?.netAmount, 'EUR');
} else {
const error = await response.text();
console.log('❌ Artikel nicht gefunden:', response.status, error);
}
} catch (error: any) {
console.log('❌ Fehler:', error.message);
}
}
console.log('\n\n🔍 Teste Quotation mit custom lineItem (ohne Artikel-ID)...\n');
try {
const testQuotation = {
voucherDate: new Date().toISOString().split('T')[0] + 'T00:00:00.000+01:00',
expirationDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] + 'T00:00:00.000+01:00',
address: {
name: 'Test Kunde',
countryCode: 'DE',
},
lineItems: [
{
type: 'custom' as const,
name: 'Test Fotobox VINTAGE_PHOTOS mit Druckflatrate',
description: 'Test Beschreibung',
quantity: 1,
unitName: 'Stück',
unitPrice: {
currency: 'EUR',
netAmount: 449,
taxRatePercentage: 19,
},
},
],
totalPrice: {
currency: 'EUR',
},
taxConditions: {
taxType: 'net' as const,
},
};
console.log('📤 Erstelle Test-Quotation mit custom lineItem...');
const result = await lexofficeService.createQuotation(testQuotation, true);
console.log('✅ Erfolgreich erstellt:', result.id);
console.log(' Voucher Number:', result.voucherNumber);
console.log('\n📄 Teste PDF Download...');
const pdf = await lexofficeService.getQuotationPDF(result.id);
console.log('✅ PDF erfolgreich heruntergeladen! Größe:', pdf.length, 'bytes');
} catch (error: any) {
console.log('❌ Fehler:', error.message);
}
}
main();

View File

@@ -0,0 +1,53 @@
import { PrismaClient } from '@prisma/client';
import { bookingAutomationService } from '../lib/booking-automation.ts';
const prisma = new PrismaClient();
async function testAutomation() {
console.log('🧪 Teste Booking Automation...\n');
try {
// Hole neueste Buchung
const latestBooking = await prisma.booking.findFirst({
orderBy: { createdAt: 'desc' },
select: {
id: true,
bookingNumber: true,
customerName: true,
eventDate: true,
},
});
if (!latestBooking) {
console.log('❌ Keine Buchung gefunden!');
return;
}
console.log(`📋 Buchung: ${latestBooking.bookingNumber} - ${latestBooking.customerName}`);
console.log(` Event: ${new Date(latestBooking.eventDate).toLocaleDateString('de-DE')}\n`);
console.log('🤖 Starte automatische Aktionen...\n');
const result = await bookingAutomationService.runPostBookingActions(latestBooking.id);
console.log('\n' + '─'.repeat(60));
console.log('📊 Ergebnis:');
console.log(` ✅ E-Mail gesendet: ${result.emailSent ? 'Ja' : 'Nein'}`);
console.log(` ✅ Kalender synchronisiert: ${result.calendarSynced ? 'Ja' : 'Nein'}`);
if (result.errors.length > 0) {
console.log(`\n❌ Fehler (${result.errors.length}):`);
result.errors.forEach(err => console.log(` - ${err}`));
} else {
console.log('\n✅ Alle Aktionen erfolgreich!');
}
} catch (error: any) {
console.error('\n❌ Fehler:', error.message);
throw error;
} finally {
await prisma.$disconnect();
}
}
testAutomation();

View File

@@ -0,0 +1,54 @@
import { LexOfficeService } from '../lib/lexoffice';
async function main() {
const lexoffice = new LexOfficeService();
console.log('🧪 Teste LexOffice Quotation finalize Parameter...\n');
// Einfaches Test-Angebot erstellen
const testQuotation = {
voucherDate: new Date().toISOString().split('T')[0] + 'T00:00:00.000+01:00',
expirationDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] + 'T00:00:00.000+01:00',
address: {
name: 'Test Kunde',
countryCode: 'DE',
},
lineItems: [
{
type: 'custom' as const,
name: 'Test Artikel',
quantity: 1,
unitName: 'Stück',
unitPrice: {
currency: 'EUR',
netAmount: 100,
taxRatePercentage: 19,
},
},
],
totalPrice: {
currency: 'EUR',
},
taxConditions: {
taxType: 'net' as const,
},
};
try {
console.log('📤 Erstelle Quotation MIT finalize=true...');
const result = await lexoffice.createQuotation(testQuotation, true);
console.log('✅ Quotation erstellt:', result);
console.log('\n🔍 Lade Quotation Details...');
const details = await lexoffice.getQuotation(result.id);
console.log('📊 Quotation Status:', JSON.stringify(details, null, 2));
console.log('\n📄 Versuche PDF Download...');
const pdf = await lexoffice.getQuotationPDF(result.id);
console.log('✅ PDF erfolgreich heruntergeladen! Größe:', pdf.length, 'bytes');
} catch (error: any) {
console.error('❌ Fehler:', error.message);
}
}
main();