Initial commit - SaveTheMoment Atlas Basis-Setup

This commit is contained in:
Dennis Forte
2025-11-12 20:21:32 +01:00
commit 0b6e429329
167 changed files with 30843 additions and 0 deletions

424
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,424 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserRole {
ADMIN
DRIVER
}
enum BookingStatus {
RESERVED // Initial (nach E-Mail-Eingang)
CONFIRMED // Admin hat bestätigt & gesendet
READY_FOR_ASSIGNMENT // Vertrag unterschrieben → Admin-Freigabe
OPEN_FOR_DRIVERS // Admin hat freigegeben → Fahrer können sich melden
ASSIGNED // Admin hat Fahrer zugewiesen & Tour erstellt
COMPLETED // Event abgeschlossen
CANCELLED // Storniert
}
enum PhotoboxStatus {
AVAILABLE
IN_USE
MAINTENANCE
DAMAGED
}
enum PhotoboxModel {
VINTAGE_SMILE
VINTAGE_PHOTOS
NOSTALGIE
MAGIC_MIRROR
}
enum InvoiceType {
PRIVATE
BUSINESS
}
enum EquipmentType {
PRINTER
CARPET
VIP_BARRIER
ACCESSORIES_KIT
PRINTER_PAPER
TRIPOD
OTHER
}
enum EquipmentStatus {
AVAILABLE
IN_USE
MAINTENANCE
DAMAGED
RESERVED
}
model User {
id String @id @default(cuid())
email String @unique
name String
password String
role UserRole @default(DRIVER)
phoneNumber String?
vehiclePlate String?
vehicleModel String?
active Boolean @default(true)
available Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
driverTours Tour[]
notifications Notification[]
driverAvailability DriverAvailability[]
}
model Location {
id String @id @default(cuid())
name String
city String
slug String @unique
websiteUrl String
contactEmail String
active Boolean @default(true)
imapHost String?
imapPort Int?
imapUser String?
imapPassword String?
imapSecure Boolean @default(true)
smtpHost String?
smtpPort Int?
smtpUser String?
smtpPassword String?
smtpSecure Boolean @default(true)
emailSyncEnabled Boolean @default(false)
lastEmailSync DateTime?
priceConfig PriceConfig[]
photoboxes Photobox[]
bookings Booking[]
equipment Equipment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([slug])
}
model PriceConfig {
id String @id @default(cuid())
locationId String
location Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
model PhotoboxModel
basePrice Float
pricePerKm Float
includedKm Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([locationId, model])
}
model Photobox {
id String @id @default(cuid())
locationId String
location Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
model PhotoboxModel
serialNumber String @unique
status PhotoboxStatus @default(AVAILABLE)
active Boolean @default(true)
description String?
purchaseDate DateTime?
lastMaintenance DateTime?
bookings Booking[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([locationId, model])
@@index([status])
}
model Booking {
id String @id @default(cuid())
bookingNumber String @unique
locationId String
location Location @relation(fields: [locationId], references: [id])
photoboxId String?
photobox Photobox? @relation(fields: [photoboxId], references: [id])
status BookingStatus @default(RESERVED)
customerName String
customerEmail String
customerPhone String
customerAddress String?
customerCity String?
customerZip String?
invoiceType InvoiceType @default(PRIVATE)
companyName String?
eventDate DateTime
eventAddress String
eventCity String
eventZip String
eventLocation String?
setupTimeStart DateTime
setupTimeLatest DateTime
dismantleTimeEarliest DateTime?
dismantleTimeLatest DateTime?
distance Float?
calculatedPrice Float?
// Contract Management
contractSigned Boolean @default(false)
contractSignedAt DateTime?
contractGenerated Boolean @default(false)
contractGeneratedAt DateTime?
contractSentAt DateTime?
contractSignedOnline Boolean @default(false)
contractPdfUrl String?
contractSignatureData String? @db.Text
contractSignedBy String?
contractSignedIp String?
contractUploadedBy String?
lexofficeOfferId String?
lexofficeInvoiceId String?
lexofficeContactId String?
lexofficeConfirmationId String?
confirmationSentAt DateTime?
// KI-Analyse
aiParsed Boolean @default(false)
aiResponseDraft String? @db.Text
aiProcessedAt DateTime?
// Freigabe-Status
readyForAssignment Boolean @default(false)
openForDrivers Boolean @default(false)
// Kalender-Sync (Nextcloud)
calendarEventId String?
calendarSynced Boolean @default(false)
calendarSyncedAt DateTime?
tourId String?
tour Tour? @relation(fields: [tourId], references: [id])
notes String?
internalNotes String?
emails Email[]
bookingEquipment BookingEquipment[]
driverAvailability DriverAvailability[]
setupWindows SetupWindow[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([eventDate])
@@index([status])
@@index([locationId])
}
model SetupWindow {
id String @id @default(cuid())
bookingId String
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
setupDate DateTime
setupTimeStart DateTime
setupTimeEnd DateTime
preferred Boolean @default(false)
selected Boolean @default(false)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([bookingId])
@@index([setupDate])
}
enum TourStatus {
PLANNED
IN_PROGRESS
COMPLETED
CANCELLED
}
model Tour {
id String @id @default(cuid())
tourDate DateTime
tourNumber String @unique
driverId String?
driver User? @relation(fields: [driverId], references: [id])
bookings Booking[]
routeOptimized Json?
totalDistance Float?
estimatedDuration Int?
status TourStatus @default(PLANNED)
startedAt DateTime?
completedAt DateTime?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([tourDate])
@@index([driverId])
@@index([status])
}
model Notification {
id String @id @default(cuid())
userId String?
user User? @relation(fields: [userId], references: [id])
type String
title String
message String
read Boolean @default(false)
metadata Json?
createdAt DateTime @default(now())
@@index([userId, read])
}
model Email {
id String @id @default(cuid())
locationSlug String?
from String
to String
subject String
textBody String?
htmlBody String?
messageId String? @unique
inReplyTo String?
bookingId String?
booking Booking? @relation(fields: [bookingId], references: [id])
parsed Boolean @default(false)
parsedData Json?
direction String @default("INBOUND")
receivedAt DateTime @default(now())
createdAt DateTime @default(now())
@@index([locationSlug])
@@index([bookingId])
@@index([receivedAt])
}
model Project {
id String @id @default(cuid())
name String
description String?
active Boolean @default(true)
equipment Equipment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Equipment {
id String @id @default(cuid())
name String
type EquipmentType
brand String?
model String?
serialNumber String? @unique
quantity Int @default(1)
status EquipmentStatus @default(AVAILABLE)
locationId String?
location Location? @relation(fields: [locationId], references: [id])
projectId String?
project Project? @relation(fields: [projectId], references: [id])
notes String?
purchaseDate DateTime?
purchasePrice Decimal?
minStockLevel Int?
currentStock Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
bookingEquipment BookingEquipment[]
@@index([type])
@@index([status])
@@index([locationId])
}
model BookingEquipment {
id String @id @default(cuid())
bookingId String
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
equipmentId String
equipment Equipment @relation(fields: [equipmentId], references: [id])
quantity Int @default(1)
createdAt DateTime @default(now())
@@unique([bookingId, equipmentId])
@@index([bookingId])
@@index([equipmentId])
}
model DriverAvailability {
id String @id @default(cuid())
bookingId String
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
driverId String
driver User @relation(fields: [driverId], references: [id], onDelete: Cascade)
available Boolean @default(true)
message String? @db.Text
createdAt DateTime @default(now())
@@unique([bookingId, driverId])
@@index([bookingId])
@@index([driverId])
}

179
prisma/seed-equipment.ts Normal file
View File

@@ -0,0 +1,179 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('Starting equipment seed...');
const locations = await prisma.location.findMany();
const luebeck = locations.find(l => l.slug === 'luebeck');
const berlin = locations.find(l => l.slug === 'berlin');
const rostock = locations.find(l => l.slug === 'rostock');
let dieFotoboxJungs = await prisma.project.findFirst({
where: { name: 'Die Fotobox Jungs' },
});
if (!dieFotoboxJungs) {
dieFotoboxJungs = await prisma.project.create({
data: {
name: 'Die Fotobox Jungs',
description: 'Aufgekauftes Projekt mit eigenem Equipment',
active: true,
},
});
console.log('Created project: Die Fotobox Jungs');
}
const equipment = [];
for (let i = 1; i <= 10; i++) {
equipment.push({
name: `Citizen CX-02 #${i}`,
type: 'PRINTER',
brand: 'Citizen',
model: 'CX-02',
serialNumber: `CX02-LUE-${String(i).padStart(3, '0')}`,
quantity: 1,
status: 'AVAILABLE',
locationId: luebeck?.id,
});
}
equipment.push({
name: 'Citizen CX-02 Berlin',
type: 'PRINTER',
brand: 'Citizen',
model: 'CX-02',
serialNumber: 'CX02-BER-001',
quantity: 1,
status: 'AVAILABLE',
locationId: berlin?.id,
});
equipment.push({
name: 'Citizen CX-02 Rostock',
type: 'PRINTER',
brand: 'Citizen',
model: 'CX-02',
serialNumber: 'CX02-ROS-001',
quantity: 1,
status: 'AVAILABLE',
locationId: rostock?.id,
});
for (let i = 1; i <= 4; i++) {
equipment.push({
name: `DNP 620 #${i} (Fotobox Jungs)`,
type: 'PRINTER',
brand: 'DNP',
model: '620',
serialNumber: `DNP620-FBJ-${String(i).padStart(3, '0')}`,
quantity: 1,
status: 'AVAILABLE',
projectId: dieFotoboxJungs.id,
});
}
equipment.push({
name: 'DNP DS-RX1 HS Rostock',
type: 'PRINTER',
brand: 'DNP',
model: 'DS-RX1 HS',
serialNumber: 'DSRX1HS-ROS-001',
quantity: 1,
status: 'AVAILABLE',
locationId: rostock?.id,
});
equipment.push({
name: 'Roter Teppich Set 1',
type: 'CARPET',
quantity: 1,
status: 'AVAILABLE',
locationId: luebeck?.id,
notes: '3m x 1m, inkl. Befestigung',
});
equipment.push({
name: 'VIP Absperrbänder Set',
type: 'VIP_BARRIER',
quantity: 2,
status: 'AVAILABLE',
locationId: luebeck?.id,
notes: '2x Ständer mit rotem Samt-Band',
});
equipment.push({
name: 'Accessoires-Koffer Standard',
type: 'ACCESSORIES_KIT',
quantity: 1,
status: 'AVAILABLE',
locationId: luebeck?.id,
notes: 'Hüte, Brillen, Schnurrbärte, Schilder',
});
equipment.push({
name: 'Druckerpapier Citizen 10x15',
type: 'PRINTER_PAPER',
brand: 'Citizen',
model: '10x15cm',
quantity: 1,
currentStock: 50,
minStockLevel: 10,
status: 'AVAILABLE',
locationId: luebeck?.id,
notes: 'Für Citizen CX-02 Drucker',
});
equipment.push({
name: 'Druckerpapier DNP 15x20',
type: 'PRINTER_PAPER',
brand: 'DNP',
model: '15x20cm',
quantity: 1,
currentStock: 8,
minStockLevel: 10,
status: 'AVAILABLE',
locationId: luebeck?.id,
notes: 'Für DNP 620 Drucker',
});
equipment.push({
name: 'Stativ Manfrotto #1',
type: 'TRIPOD',
brand: 'Manfrotto',
model: 'Compact Advanced',
quantity: 1,
status: 'AVAILABLE',
locationId: luebeck?.id,
});
equipment.push({
name: 'Stativ Manfrotto #2',
type: 'TRIPOD',
brand: 'Manfrotto',
model: 'Compact Advanced',
quantity: 1,
status: 'AVAILABLE',
locationId: berlin?.id,
});
for (const item of equipment) {
await prisma.equipment.create({
data: item as any,
});
}
console.log(`Created ${equipment.length} equipment items`);
console.log('Equipment seed completed!');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

179
prisma/seed.ts Normal file
View File

@@ -0,0 +1,179 @@
import { PrismaClient } from '@prisma/client';
import { hash } from 'bcryptjs';
const prisma = new PrismaClient();
async function main() {
console.log('Seeding database...');
const adminPassword = await hash('admin123', 12);
const driverPassword = await hash('driver123', 12);
const admin = await prisma.user.upsert({
where: { email: 'admin@savethemoment.de' },
update: {},
create: {
email: 'admin@savethemoment.de',
name: 'Dennis Forte',
password: adminPassword,
role: 'ADMIN',
phoneNumber: '+49 123 456789',
},
});
const driver1 = await prisma.user.upsert({
where: { email: 'fahrer1@savethemoment.de' },
update: {},
create: {
email: 'fahrer1@savethemoment.de',
name: 'Max Mustermann',
password: driverPassword,
role: 'DRIVER',
phoneNumber: '+49 123 456780',
},
});
const driver2 = await prisma.user.upsert({
where: { email: 'fahrer2@savethemoment.de' },
update: {},
create: {
email: 'fahrer2@savethemoment.de',
name: 'Anna Schmidt',
password: driverPassword,
role: 'DRIVER',
phoneNumber: '+49 123 456781',
},
});
const luebeck = await prisma.location.upsert({
where: { slug: 'luebeck' },
update: {},
create: {
name: 'Lübeck',
city: 'Lübeck',
slug: 'luebeck',
websiteUrl: 'https://fotobox-luebeck.de',
contactEmail: 'info@fotobox-luebeck.de',
},
});
const hamburg = await prisma.location.upsert({
where: { slug: 'hamburg' },
update: {},
create: {
name: 'Hamburg',
city: 'Hamburg',
slug: 'hamburg',
websiteUrl: 'https://hamburg-fotobox.de',
contactEmail: 'info@hamburg-fotobox.de',
},
});
const kiel = await prisma.location.upsert({
where: { slug: 'kiel' },
update: {},
create: {
name: 'Kiel',
city: 'Kiel',
slug: 'kiel',
websiteUrl: 'https://fotobox-kiel.de',
contactEmail: 'info@fotobox-kiel.de',
},
});
const berlin = await prisma.location.upsert({
where: { slug: 'berlin' },
update: {},
create: {
name: 'Berlin',
city: 'Berlin',
slug: 'berlin',
websiteUrl: 'https://fotobox-potsdam.de',
contactEmail: 'info@fotobox-potsdam.de',
},
});
const rostock = await prisma.location.upsert({
where: { slug: 'rostock' },
update: {},
create: {
name: 'Rostock',
city: 'Rostock',
slug: 'rostock',
websiteUrl: 'https://fotobox-rostock.de',
contactEmail: 'info@fotobox-rostock.de',
},
});
await prisma.priceConfig.createMany({
data: [
{ locationId: luebeck.id, model: 'VINTAGE_SMILE', basePrice: 399, pricePerKm: 0.8, includedKm: 30 },
{ locationId: luebeck.id, model: 'VINTAGE_PHOTOS', basePrice: 449, pricePerKm: 0.8, includedKm: 30 },
{ locationId: luebeck.id, model: 'NOSTALGIE', basePrice: 499, pricePerKm: 0.8, includedKm: 30 },
{ locationId: luebeck.id, model: 'MAGIC_MIRROR', basePrice: 599, pricePerKm: 0.8, includedKm: 30 },
{ locationId: hamburg.id, model: 'VINTAGE_SMILE', basePrice: 419, pricePerKm: 0.9, includedKm: 25 },
{ locationId: hamburg.id, model: 'VINTAGE_PHOTOS', basePrice: 469, pricePerKm: 0.9, includedKm: 25 },
{ locationId: hamburg.id, model: 'NOSTALGIE', basePrice: 519, pricePerKm: 0.9, includedKm: 25 },
{ locationId: hamburg.id, model: 'MAGIC_MIRROR', basePrice: 619, pricePerKm: 0.9, includedKm: 25 },
{ locationId: kiel.id, model: 'VINTAGE_SMILE', basePrice: 389, pricePerKm: 0.75, includedKm: 35 },
{ locationId: kiel.id, model: 'VINTAGE_PHOTOS', basePrice: 439, pricePerKm: 0.75, includedKm: 35 },
{ locationId: kiel.id, model: 'NOSTALGIE', basePrice: 489, pricePerKm: 0.75, includedKm: 35 },
{ locationId: kiel.id, model: 'MAGIC_MIRROR', basePrice: 589, pricePerKm: 0.75, includedKm: 35 },
{ locationId: berlin.id, model: 'VINTAGE_SMILE', basePrice: 409, pricePerKm: 0.85, includedKm: 30 },
{ locationId: berlin.id, model: 'VINTAGE_PHOTOS', basePrice: 459, pricePerKm: 0.85, includedKm: 30 },
{ locationId: berlin.id, model: 'NOSTALGIE', basePrice: 509, pricePerKm: 0.85, includedKm: 30 },
{ locationId: berlin.id, model: 'MAGIC_MIRROR', basePrice: 609, pricePerKm: 0.85, includedKm: 30 },
{ locationId: rostock.id, model: 'VINTAGE_SMILE', basePrice: 379, pricePerKm: 0.75, includedKm: 35 },
{ locationId: rostock.id, model: 'VINTAGE_PHOTOS', basePrice: 429, pricePerKm: 0.75, includedKm: 35 },
{ locationId: rostock.id, model: 'NOSTALGIE', basePrice: 479, pricePerKm: 0.75, includedKm: 35 },
{ locationId: rostock.id, model: 'MAGIC_MIRROR', basePrice: 579, pricePerKm: 0.75, includedKm: 35 },
],
skipDuplicates: true,
});
await prisma.photobox.createMany({
data: [
{ locationId: luebeck.id, model: 'VINTAGE_SMILE', serialNumber: 'LUE-VS-001' },
{ locationId: luebeck.id, model: 'VINTAGE_SMILE', serialNumber: 'LUE-VS-002' },
{ locationId: luebeck.id, model: 'VINTAGE_PHOTOS', serialNumber: 'LUE-VP-001' },
{ locationId: luebeck.id, model: 'NOSTALGIE', serialNumber: 'LUE-NOS-001' },
{ locationId: luebeck.id, model: 'MAGIC_MIRROR', serialNumber: 'LUE-MM-001' },
{ locationId: hamburg.id, model: 'VINTAGE_SMILE', serialNumber: 'HAM-VS-001' },
{ locationId: hamburg.id, model: 'VINTAGE_SMILE', serialNumber: 'HAM-VS-002' },
{ locationId: hamburg.id, model: 'VINTAGE_PHOTOS', serialNumber: 'HAM-VP-001' },
{ locationId: hamburg.id, model: 'NOSTALGIE', serialNumber: 'HAM-NOS-001' },
{ locationId: hamburg.id, model: 'MAGIC_MIRROR', serialNumber: 'HAM-MM-001' },
{ locationId: kiel.id, model: 'VINTAGE_SMILE', serialNumber: 'KIEL-VS-001' },
{ locationId: kiel.id, model: 'VINTAGE_PHOTOS', serialNumber: 'KIEL-VP-001' },
{ locationId: kiel.id, model: 'NOSTALGIE', serialNumber: 'KIEL-NOS-001' },
{ locationId: berlin.id, model: 'VINTAGE_SMILE', serialNumber: 'BER-VS-001' },
{ locationId: berlin.id, model: 'MAGIC_MIRROR', serialNumber: 'BER-MM-001' },
{ locationId: rostock.id, model: 'VINTAGE_SMILE', serialNumber: 'ROS-VS-001' },
{ locationId: rostock.id, model: 'NOSTALGIE', serialNumber: 'ROS-NOS-001' },
],
skipDuplicates: true,
});
console.log('Database seeded successfully!');
console.log('\nTest Accounts:');
console.log('Admin: admin@savethemoment.de / admin123');
console.log('Fahrer 1: fahrer1@savethemoment.de / driver123');
console.log('Fahrer 2: fahrer2@savethemoment.de / driver123');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});