b6167bcb-7363-4695-9481-addd81de4367

This commit is contained in:
Z User committed 2026-10-26 13:40:52 +00:00
1 parent fdc3b82d10
commit d226dabba3
136 files changed
+13904 -2

No files matched your search

+412
View File
@@ -0,0 +1,412 @@
// AOS seed script — realistic German software agency dataset
// Run with: bun run src/lib/seed.ts
import { db } from './db'
const CENTS = (euro: number) => Math.round(euro * 100)
const daysFromNow = (d: number) => new Date(Date.now() + d * 86400000)
async function main() {
console.log('Seeding AOS…')
// --- Users / RBAC -------------------------------------------------------
const users = [
{ email: 'max@nordlicht.digital', name: 'Max Brandt', role: 'owner' },
{ email: 'lena@nordlicht.digital', name: 'Lena Vogt', role: 'admin' },
{ email: 'sven@nordlicht.digital', name: 'Sven Hoffmann', role: 'sales' },
{ email: 'aylin@nordlicht.digital', name: 'Aylin Demir', role: 'pm' },
{ email: 'tom@nordlicht.digital', name: 'Tom Becker', role: 'developer' },
{ email: 'nora@nordlicht.digital', name: 'Nora Klein', role: 'finance' },
{ email: 'portal@bauer-handwerk.de', name: 'Klaus Bauer', role: 'client' },
]
for (const u of users) {
await db.user.upsert({
where: { email: u.email },
update: {},
create: u,
})
}
// --- Modules registry ---------------------------------------------------
const modules = [
{ id: 'crm', enabled: true, version: '1.0.0' },
{ id: 'leads', enabled: true, version: '1.0.0' },
{ id: 'projects', enabled: true, version: '1.0.0' },
{ id: 'licenses', enabled: true, version: '1.0.0' },
{ id: 'sales-docs', enabled: false, version: '0.9.0' },
{ id: 'finance', enabled: true, version: '1.0.0' },
{ id: 'support', enabled: false, version: '0.5.0' },
{ id: 'reporting', enabled: false, version: '0.5.0' },
]
for (const m of modules) {
await db.module.upsert({
where: { id: m.id },
update: {},
create: m,
})
}
// --- Settings (German defaults) ----------------------------------------
const settings: Record<string, string> = {
'company.name': 'Nordlicht Digital GmbH',
'company.legalForm': 'GmbH',
'company.vatId': 'DE284617302',
'company.registerCourt': 'Amtsgericht Hamburg',
'company.registerNumber': 'HRB 142887',
'company.city': 'Hamburg',
'company.bank': 'DE89 3704 0044 0532 0130 00',
'invoice.numberPrefix': 'RE',
'invoice.numberYear': '2026',
'invoice.numberNext': '0042',
'tax.standardBp': '1900',
'tax.reducedBp': '700',
'leadFinder.radiusKm': '100',
'leadFinder.costCapEur': '20',
'compliance.goBD': 'true',
'compliance.eRechnung': 'true',
}
for (const [key, value] of Object.entries(settings)) {
await db.setting.upsert({
where: { key },
update: { value },
create: { key, value },
})
}
// --- CRM: Companies ----------------------------------------------------
const companies = [
{ name: 'Bauer Handwerk GmbH', legalForm: 'GmbH', vatId: 'DE301884012', registerCourt: 'Amtsgericht Lübeck', registerNumber: 'HRB 8842', website: 'bauer-handwerk.de', email: 'info@bauer-handwerk.de', phone: '+49 451 8842', city: 'Lübeck', zip: '23552', street: 'Mühlendamm 14', source: 'lead-finder', tags: 'handwerk,website-neu' },
{ name: 'Ostsee Hotel & Spa AG', legalForm: 'AG', vatId: 'DE815491002', registerCourt: 'Amtsgericht Rostock', registerNumber: 'HRB 4012', website: 'ostsee-hotel-spa.de', email: 'kontakt@ostsee-hotel-spa.de', phone: '+49 381 4012', city: 'Rostock', zip: '18055', street: 'Lange Straße 8', source: 'lead-finder', tags: 'hotel,redesign' },
{ name: 'Müller Bäckerei e.K.', legalForm: 'e.K.', vatId: 'DE192003411', website: 'mueller-baeckerei.de', email: 'hallo@mueller-baeckerei.de', phone: '+49 40 8841 22', city: 'Hamburg', zip: '20095', street: 'Spitalerstraße 33', source: 'import', tags: 'food,shop' },
{ name: 'TechStart UG (haftungsbeschränkt)', legalForm: 'UG', vatId: 'DE312045519', registerCourt: 'Amtsgericht Berlin-Charlottenburg', registerNumber: 'HRB 220934', website: 'techstart.berlin', email: 'hello@techstart.berlin', phone: '+49 30 8841 200', city: 'Berlin', zip: '10115', street: 'Torstraße 100', source: 'lead-finder', tags: 'startup,saas' },
{ name: 'Nordwind Logistik GmbH', legalForm: 'GmbH', vatId: 'DE440281115', registerCourt: 'Amtsgericht Bremen', registerNumber: 'HRB 22119', website: 'nordwind-logistik.de', email: 'info@nordwind-logistik.de', phone: '+49 421 500 88', city: 'Bremen', zip: '28195', street: 'Am Markt 12', source: 'referral', tags: 'logistik,maintenance' },
{ name: 'Atelier Wendland GbR', legalForm: 'GbR', website: 'atelier-wendland.de', email: 'studio@atelier-wendland.de', city: 'Lüneburg', zip: '21335', street: 'Am Sande 4', source: 'manual', tags: 'design,redesign' },
{ name: 'Klinik am Park MV GmbH', legalForm: 'GmbH', vatId: 'DE293041855', registerCourt: 'Amtsgericht Schwerin', registerNumber: 'HRB 5512', website: 'klinik-am-park-mv.de', email: 'kontakt@klinik-am-park-mv.de', phone: '+49 385 2241', city: 'Schwerin', zip: '19053', street: 'Gartenstraße 21', source: 'lead-finder', tags: 'gesundheit,seo,accessibility' },
{ name: 'HanseMöbel Manufaktur GmbH', legalForm: 'GmbH', vatId: 'DE118823440', registerCourt: 'Amtsgericht Hamburg', registerNumber: 'HRB 161204', website: 'hansemoebel.de', email: 'verkauf@hansemoebel.de', phone: '+49 40 7710 00', city: 'Hamburg', zip: '21107', street: 'Stuhlrother Str. 5', source: 'lead-finder', tags: 'shop,woocommerce' },
]
const companyMap: Record<string, string> = {}
for (const c of companies) {
const created = await db.company.create({ data: c })
companyMap[c.name] = created.id
}
// --- Persons ------------------------------------------------------------
const persons = [
{ companyId: companyMap['Bauer Handwerk GmbH'], firstName: 'Klaus', lastName: 'Bauer', email: 'klaus@bauer-handwerk.de', phone: '+49 451 8842 0', role: 'Geschäftsführer' },
{ companyId: companyMap['Bauer Handwerk GmbH'], firstName: 'Sabine', lastName: 'Bauer', email: 'sabine@bauer-handwerk.de', role: 'Geschäftsführung' },
{ companyId: companyMap['Ostsee Hotel & Spa AG'], firstName: 'Henrik', lastName: 'Olsen', email: 'henrik@ostsee-hotel-spa.de', role: 'Direktor' },
{ companyId: companyMap['Müller Bäckerei e.K.'], firstName: 'Greta', lastName: 'Müller', email: 'greta@mueller-baeckerei.de', role: 'Inhaberin' },
{ companyId: companyMap['TechStart UG (haftungsbeschränkt)'], firstName: 'Jonas', lastName: 'Weiß', email: 'jonas@techstart.berlin', role: 'CEO' },
{ companyId: companyMap['Nordwind Logistik GmbH'], firstName: 'Petra', lastName: 'Hahn', email: 'petra.hahn@nordwind-logistik.de', role: 'Leitung IT' },
{ companyId: companyMap['Klinik am Park MV GmbH'], firstName: 'Dr. med. Iris', lastName: 'Brandt', email: 'i.brandt@klinik-am-park-mv.de', role: 'Ärztliche Leitung' },
{ companyId: companyMap['HanseMöbel Manufaktur GmbH'], firstName: 'Frank', lastName: 'Hartmann', email: 'f.hartmann@hansemoebel.de', role: 'Verkaufsleiter' },
]
for (const p of persons) {
await db.person.create({ data: p })
}
// --- Deals / pipeline ---------------------------------------------------
const pipelineStages = [
{ key: 'lead', label: 'Lead', probability: 10 },
{ key: 'qualified', label: 'Qualifiziert', probability: 25 },
{ key: 'proposal', label: 'Angebot gesendet', probability: 50 },
{ key: 'negotiation', label: 'Verhandlung', probability: 70 },
{ key: 'won', label: 'Gewonnen', probability: 100 },
{ key: 'lost', label: 'Verloren', probability: 0 },
]
void pipelineStages
const deals = [
{ company: 'Bauer Handwerk GmbH', title: 'Neue Website inkl. CMS', valueCents: CENTS(9800), probability: 70, stage: 'proposal', expectedClose: daysFromNow(14) },
{ company: 'Ostsee Hotel & Spa AG', title: 'Website-Redesign + Buchungsintegration', valueCents: CENTS(24500), probability: 50, stage: 'qualified', expectedClose: daysFromNow(35) },
{ company: 'Müller Bäckerei e.K.', title: 'WooCommerce Online-Shop', valueCents: CENTS(7200), probability: 25, stage: 'lead', expectedClose: daysFromNow(50) },
{ company: 'TechStart UG (haftungsbeschränkt)', title: 'SaaS MVP Entwicklung', valueCents: CENTS(48000), probability: 70, stage: 'negotiation', expectedClose: daysFromNow(21) },
{ company: 'Nordwind Logistik GmbH', title: 'Wartungsvertrag 12 Monate', valueCents: CENTS(6600), probability: 100, stage: 'won', expectedClose: daysFromNow(-3) },
{ company: 'Atelier Wendland GbR', title: 'Portfolio-Website', valueCents: CENTS(3900), probability: 10, stage: 'lead', expectedClose: daysFromNow(60) },
{ company: 'Klinik am Park MV GmbH', title: 'SEO + Barrierefreiheit BFSG', valueCents: CENTS(12500), probability: 50, stage: 'proposal', expectedClose: daysFromNow(28) },
{ company: 'HanseMöbel Manufaktur GmbH', title: 'WooCommerce Migration + Anbindung ERP', valueCents: CENTS(31000), probability: 25, stage: 'qualified', expectedClose: daysFromNow(45) },
{ company: 'Nordwind Logistik GmbH', title: 'Intranet-Portal', valueCents: CENTS(18000), probability: 0, stage: 'lost', lossReason: 'Budget gekürzt', expectedClose: daysFromNow(-10) },
]
for (const d of deals) {
const { company, ...rest } = d
await db.deal.create({ data: { ...rest, companyId: companyMap[company] } })
}
// --- Leads Finder -------------------------------------------------------
const evidence = (items: { id: string; check: string; status: 'pass' | 'warn' | 'fail'; detail: string }[]) => JSON.stringify(items)
const leads = [
{
companyName: 'Fahrrad Manufaktur Altona', website: 'fahrrad-altona.de', city: 'Hamburg', branch: 'Einzelhandel',
score: 84, scoreLabel: 'redesign',
evidence: evidence([
{ id: 'e1', check: 'HTTPS / Zertifikat', status: 'warn', detail: 'Zertifikat läuft in 9 Tagen ab (Let\'s Encrypt, zuletzt erneuert 2026-01-12)' },
{ id: 'e2', check: 'Mobile Viewport', status: 'pass', detail: 'meta viewport vorhanden, Layout aber 410px zu breit auf 375px Bildschirm' },
{ id: 'e3', check: 'Core Web Vitals (LCP)', status: 'fail', detail: 'LCP 4,8s (Ziel < 2,5s), gemessen via PageSpeed API am 2026-10-24' },
{ id: 'e4', check: 'CMS / Plugin-Versionen', status: 'warn', detail: 'WordPress 6.2 (veraltet, aktuell 6.5), WooCommerce 7.4 (2 Major-Versionen hinter aktuell)' },
{ id: 'e5', check: 'Impressum vorhanden', status: 'pass', detail: 'Impressum gefunden, enthält Angaben gem. § 5 TMG/DDG' },
{ id: 'e6', check: 'Datenschutz / Cookie Banner', status: 'warn', detail: 'Datenschutzerklärung vorhanden, Cookie-Banner initialisiert erst nach Scroll (DSGVO-Risiko)' },
{ id: 'e7', check: 'Strukturierte Daten', status: 'fail', detail: 'Keine Schema.org-Produkt-Markup trotz Online-Shop' },
{ id: 'e8', check: 'Letzte Aktualisierung', status: 'warn', detail: 'Copyright-Zeile zeigt 2023, sitemap.xml nicht gefunden' },
]),
explanation: 'Fahrrad Manufaktur Altona betreibt einen WooCommerce-Shop auf einer veralteten WordPress-Installation (e4) mit schlechten Core Web Vitals (e3) und fehlendem Produktmarkup (e7). Das SSL-Zertifikat läuft in 9 Tagen ab (e1) — ein konkreter, zeitkritischer Ansatzpunkt. Vorgeschlagener Winkel: "Wir sichern Ihren Shop vor dem Zertifikat-Ablauf und holen die Ladezeit in den grünen Bereich."',
suggestedAngle: 'SSL-Ablauf + Performance-Optimierung als Türöffner, danach WooCommerce-Upgrade & SEO-AEO.',
status: 'pending', source: 'serpapi', radiusKm: 100,
},
{
companyName: 'Ostsee Catering Berg', website: 'ostsee-catering-berg.de', city: 'Rostock', branch: 'Gastronomie',
score: 76, scoreLabel: 'new-site',
evidence: evidence([
{ id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültiges Zertifikat bis 2026-04-18' },
{ id: 'e2', check: 'Mobile Viewport', status: 'pass', detail: 'Korrekt konfiguriert' },
{ id: 'e3', check: 'Core Web Vitals (LCP)', status: 'warn', detail: 'LCP 2,9s, knapp über Ziel' },
{ id: 'e4', check: 'CMS / Plugin-Versionen', status: 'warn', detail: 'Contao 4.13, Support-Ende erreicht' },
{ id: 'e5', check: 'Impressum vorhanden', status: 'fail', detail: 'Keine USt-IdNr. angegeben, fehlende Handelsregister-Nummer' },
{ id: 'e6', check: 'Datenschutz / Cookie Banner', status: 'warn', detail: 'YouTube-Videos ohne lokale Einbindung, Cookie-Banner nicht CMP-zertifiziert' },
{ id: 'e7', check: 'AEO / GEO (LLM-Zugänglichkeit)', status: 'fail', detail: 'Keine llms.txt, keine strukturierten Daten zu Speisekarten oder Preisen' },
]),
explanation: 'Ostsee Catering Berg nutzt Contao 4.13, dessen Support-Ende erreicht ist (e4). Das Impressum ist unvollständig und damit abmahngefährdet (e5). Für eine Catering-Spitze sind LLM-gestützte Suchen ("Catering Rostock Hochzeit") relevant, aber es fehlen strukturierte Daten (e7).',
suggestedAngle: 'Rechtscheck Impressum/Datenschutz + AEO für "Catering + Stadt"-Anfragen.',
status: 'pending', source: 'tavily', radiusKm: 100,
},
{
companyName: 'Schiffswerft Hansen', website: 'werft-hansen.de', city: 'Kiel', branch: 'Industrie',
score: 62, scoreLabel: 'maintenance',
evidence: evidence([
{ id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' },
{ id: 'e2', check: 'Core Web Vitals (LCP)', status: 'pass', detail: 'LCP 1,9s' },
{ id: 'e3', check: 'Letzte Aktualisierung', status: 'fail', detail: 'Letzter News-Eintrag 2021, kein CMS erkennbar, statische HTML-Seite' },
{ id: 'e4', check: 'Strukturierte Daten', status: 'fail', detail: 'Keine Organisation/LocalBusiness-Markup' },
{ id: 'e5', check: 'Impressum vorhanden', status: 'warn', detail: 'Vorhanden, aber veraltete Telefonnummer' },
]),
explanation: 'Schiffswerft Hansen hat eine schnelle, aber statische Seite ohne CMS und seit 2021 keine Aktualisierung (e3). Potenzial für ein Redesign mit redaktionellem CMS und LocalBusiness-Markup (e4), um in regionalen Suchen besser gefunden zu werden.',
suggestedAngle: 'Redaktionelle Unabhängigkeit durch CMS + Local-SEO für Werft-Dienstleistungen.',
status: 'pending', source: 'apify', radiusKm: 100,
},
{
companyName: 'Yoga Studio Mitte', website: 'yoga-mitte-berlin.de', city: 'Berlin', branch: 'Dienstleistung',
score: 71, scoreLabel: 'seo',
evidence: evidence([
{ id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' },
{ id: 'e2', check: 'Core Web Vitals (LCP)', status: 'warn', detail: 'LCP 3,4s, Bilder nicht optimiert' },
{ id: 'e3', check: 'Strukturierte Daten', status: 'fail', detail: 'Keine Event-/Course-Markup für Stundenplan' },
{ id: 'e4', check: 'Sitemap / robots', status: 'warn', detail: 'Sitemap leer, robots blockiert /wp-admin korrekt' },
{ id: 'e5', check: 'Accessibility (BFSG)', status: 'warn', detail: 'Kontrast zu gering auf Buttons, keine Skip-Links' },
]),
explanation: 'Yoga Studio Mitte erreicht mobil nur mäßige Ladezeiten (e2) und nutzt kein Event-Markup für seinen Stundenplan (e3). Da das Studio eine App-ähnliche Buchung anbietet, ist BFSG-Relevanz wahrscheinlich (e5) — ein Ansatzpunkt für Barrierefreiheit.',
suggestedAngle: 'Performance + strukturierte Kursdaten + BFSG-Barrierefreiheit.',
status: 'pending', source: 'serpapi', radiusKm: 300,
},
{
companyName: 'Bauhof Schleswig', website: 'bauhof-schleswig.de', city: 'Schleswig', branch: 'Handwerk',
score: 58, scoreLabel: 'redesign',
evidence: evidence([
{ id: 'e1', check: 'HTTPS / Zertifikat', status: 'fail', detail: 'Nur HTTP, automatischer Redirect fehlt' },
{ id: 'e2', check: 'Mobile Viewport', status: 'fail', detail: 'Kein meta viewport, Desktop-Layout auf Mobilgeräten' },
{ id: 'e3', check: 'Impressum vorhanden', status: 'warn', detail: 'Vorhanden, aber keine Datenschutzerklärung' },
]),
explanation: 'Bauhof Schleswig betreibt eine reine HTTP-Seite ohne mobile Optimierung (e1, e2) — ein klarer technischer Notfall und gleichzeitig der einfachste Einstieg für eine komplette Neuentwicklung. Datenschutz fehlt (e3).',
suggestedAngle: 'Sicherheits-Upgrade (HTTPS) + mobilfähige Neuentwicklung + Rechtskonformität.',
status: 'pending', source: 'firecrawl', radiusKm: 100,
},
{
companyName: 'Weingut Moseltraum', website: 'moseltraum-weingut.de', city: 'Traben-Trarbach', branch: 'Landwirtschaft',
score: 49, scoreLabel: 'shop',
evidence: evidence([
{ id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' },
{ id: 'e2', check: 'Mobile Viewport', status: 'pass', detail: 'Korrekt' },
{ id: 'e3', check: 'Shop vorhanden', status: 'fail', detail: 'Kein Online-Shop, obwohl Weinversand angeboten wird (Impressum erwähnt Versand)' },
{ id: 'e4', check: 'Altersprüfung', status: 'fail', detail: 'Kein Altersverifikations-Plugin für alkoholhaltige Produkte' },
]),
explanation: 'Weingut Moseltraum bewirbt Weinversand, hat aber keinen Online-Shop (e3) und keine Altersprüfung (e4) — sowohl eine Umsatzchance als auch eine Compliance-Lücke beim Versand alkoholhaltiger Getränke.',
suggestedAngle: 'WooCommerce-Shop + rechtssichere Altersverifikation.',
status: 'pending', source: 'tavily', radiusKm: 500,
},
{
companyName: 'Tierarztpraxis Eulenflucht', website: 'tierarzt-eulenflucht.de', city: 'Flensburg', branch: 'Gesundheit',
score: 67, scoreLabel: 'accessibility',
evidence: evidence([
{ id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' },
{ id: 'e2', check: 'Accessibility (BFSG)', status: 'fail', detail: 'Formularfelder ohne Label, keine ARIA-Rollen, Kontrast 3:1 (Ziel 4,5:1)' },
{ id: 'e3', check: 'Online-Terminbuchung', status: 'warn', detail: 'Nur Telefonnummer, keine digitale Buchung' },
{ id: 'e4', check: 'Strukturierte Daten', status: 'warn', detail: 'Keine LocalBusiness-Markup, obwohl Praxis-Standort relevant' },
]),
explanation: 'Tierarztpraxis Eulenflucht hat gravierende Accessibility-Mängel (e2) und bietet keine Online-Terminbuchung (e3). Da Gesundheitsdienstleister unter BFSG fallen können, ist die Barrierefreiheit hier nicht optional.',
suggestedAngle: 'BFSG-konforme Barrierefreiheit + digitale Terminbuchung.',
status: 'pending', source: 'serpapi', radiusKm: 100,
},
{
companyName: 'Dachdecker Meister Petersen', website: 'dachdecker-petersen.de', city: 'Kiel', branch: 'Handwerk',
score: 73, scoreLabel: 'new-site',
evidence: evidence([
{ id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' },
{ id: 'e2', check: 'Mobile Viewport', status: 'pass', detail: 'Korrekt' },
{ id: 'e3', check: 'Letzte Aktualisierung', status: 'warn', detail: 'Referenzen zuletzt 2022, keine aktuellen Projekte' },
{ id: 'e4', check: 'Strukturierte Daten', status: 'fail', detail: 'Kein LocalBusiness-Markup, keine Öffnungszeiten ausgezeichnet' },
{ id: 'e5', check: 'Anfrageformular', status: 'warn', detail: 'Formular ohne spam-Schutz, keine Erfolgsbestätigung' },
]),
explanation: 'Dachdecker Meister Petersen hat eine technisch solide Basis, aber die Referenzen sind veraltet (e3) und Local-SEO-Markup fehlt (e4). Für Handwerksbetriebe ist Local-SEO der wichtigste Kanal.',
suggestedAngle: 'Aktuelle Referenzen + Local-SEO-Markup + funktionierendes Anfrageformular.',
status: 'pending', source: 'apify', radiusKm: 100,
},
{
companyName: 'Galerie Nordlicht', website: 'galerie-nordlicht.de', city: 'Hamburg', branch: 'Kultur',
score: 41, scoreLabel: 'seo',
evidence: evidence([
{ id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' },
{ id: 'e2', check: 'Bilder', status: 'warn', detail: 'Große unoptimierte Bilder, keine WebP, kein Lazy Loading' },
{ id: 'e3', check: 'Strukturierte Daten', status: 'fail', detail: 'Keine VisualArtwork-Markup für Ausstellungsstücke' },
]),
explanation: 'Galerie Nordlicht lädt langsam wegen unoptimierter Bilder (e2) und nutzt kein Markup für Kunstwerke (e3). Für eine Galerie sind Bildsuche und VisualArtwork-Markup ein starker Hebel.',
suggestedAngle: 'Bildoptimierung + VisualArtwork-Markup für bessere Sichtbarkeit in der Bildersuche.',
status: 'pending', source: 'tavily', radiusKm: 50,
},
{
companyName: 'Steuerberatung Knoop & Partner', website: 'knoop-steuerberatung.de', city: 'Hamburg', branch: 'Dienstleistung',
score: 55, scoreLabel: 'maintenance',
evidence: evidence([
{ id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' },
{ id: 'e2', check: 'CMS / Plugin-Versionen', status: 'warn', detail: 'TYPO3 11, Support bis 2026-10, Migration nötig' },
{ id: 'e3', check: 'Accessibility (BFSG)', status: 'warn', detail: 'Kontrast knapp unter Ziel, kein Skip-Link' },
{ id: 'e4', check: 'DSGVO / TDDDG', status: 'warn', detail: 'Google Maps Einbindung ohne lokalen Proxy, IP übertragen an Google' },
]),
explanation: 'Steuerberatung Knoop & Partner läuft auf TYPO3 11, dessen Support im Oktober 2026 endet (e2). Die Google-Maps-Einbindung überträgt IPs an Google, was nach TDDDG ein Risiko darstellt (e4).',
suggestedAngle: 'TYPO3-Upgrade + DSGVO-konforme Karten-Einbindung + BFSG-Barrierefreiheit.',
status: 'pending', source: 'serpapi', radiusKm: 50,
},
]
for (const l of leads) {
await db.lead.create({ data: l })
}
// --- Projects -----------------------------------------------------------
const projects = [
{ company: 'Nordwind Logistik GmbH', name: 'Intranet-Portal Q4', type: 'app', status: 'active', budgetType: 'fixed', fixedPriceCents: CENTS(18000), spentHours: 64, budgetHours: 120, startDate: daysFromNow(-20), dueDate: daysFromNow(18), progress: 55 },
{ company: 'Bauer Handwerk GmbH', name: 'Corporate Website Relaunch', type: 'website', status: 'planning', budgetType: 'fixed', fixedPriceCents: CENTS(9800), spentHours: 4, budgetHours: 60, startDate: daysFromNow(-2), dueDate: daysFromNow(40), progress: 8 },
{ company: 'TechStart UG (haftungsbeschränkt)', name: 'SaaS MVP — Auth & Billing', type: 'app', status: 'active', budgetType: 'time-materials', spentHours: 88, budgetHours: 160, startDate: daysFromNow(-30), dueDate: daysFromNow(30), progress: 48 },
{ company: 'HanseMöbel Manufaktur GmbH', name: 'WooCommerce Wartung Q4/2026', type: 'retainer', status: 'active', budgetType: 'retainer', budgetHours: 20, spentHours: 14, startDate: daysFromNow(-25), dueDate: daysFromNow(65), progress: 70 },
{ company: 'Klinik am Park MV GmbH', name: 'SEO-Optimierung & BFGG-Check', type: 'website', status: 'review', budgetType: 'fixed', fixedPriceCents: CENTS(12500), spentHours: 38, budgetHours: 40, startDate: daysFromNow(-15), dueDate: daysFromNow(5), progress: 92 },
{ company: 'Ostsee Hotel & Spa AG', name: 'Website-Redesign Konzept', type: 'website', status: 'planning', budgetType: 'time-materials', spentHours: 6, budgetHours: 0, startDate: daysFromNow(-1), dueDate: daysFromNow(21), progress: 5 },
]
const projectMap: Record<string, string> = {}
for (const p of projects) {
const { company, ...rest } = p
const created = await db.project.create({ data: { ...rest, companyId: companyMap[company] } })
projectMap[p.name] = created.id
}
// --- Tasks --------------------------------------------------------------
const tasks = [
{ project: 'Intranet-Portal Q4', title: 'SSO über Better Auth einrichten', status: 'in-progress', assignee: 'Tom Becker', estimateMin: 480, priority: 'high' },
{ project: 'Intranet-Portal Q4', title: 'Dashboard-Komponenten (Kanban, Timeline)', status: 'done', assignee: 'Tom Becker', estimateMin: 360, priority: 'medium' },
{ project: 'Intranet-Portal Q4', title: 'Dokumenten-Upload (SHA-256)', status: 'todo', assignee: 'Tom Becker', estimateMin: 300, priority: 'high' },
{ project: 'Corporate Website Relaunch', title: 'Impressum & Datenschutz finalisieren', status: 'todo', assignee: 'Aylin Demir', estimateMin: 120, priority: 'high' },
{ project: 'Corporate Website Relaunch', title: 'Design-Tokens abnehmen', status: 'done', assignee: 'Aylin Demir', estimateMin: 90, priority: 'medium' },
{ project: 'SaaS MVP — Auth & Billing', title: 'Stripe-Webhook + Rechnungsnummern', status: 'review', assignee: 'Tom Becker', estimateMin: 240, priority: 'high' },
{ project: 'SaaS MVP — Auth & Billing', title: 'Onboarding-Flow (3 Schritte)', status: 'in-progress', assignee: 'Tom Becker', estimateMin: 360, priority: 'medium' },
{ project: 'WooCommerce Wartung Q4/2026', title: 'Plugin-Updates + Backup-Check', status: 'done', assignee: 'Tom Becker', estimateMin: 60, priority: 'medium' },
{ project: 'SEO-Optimierung & BFGG-Check', title: 'BFSG-Audit (axe)', status: 'done', assignee: 'Aylin Demir', estimateMin: 180, priority: 'high' },
{ project: 'SEO-Optimierung & BFGG-Check', title: 'Schema.org LocalBusiness einbauen', status: 'done', assignee: 'Tom Becker', estimateMin: 120, priority: 'medium' },
{ project: 'SEO-Optimierung & BFGG-Check', title: 'Kunden-Freigabe einholen', status: 'in-progress', assignee: 'Aylin Demir', estimateMin: 30, priority: 'high' },
{ project: 'Website-Redesign Konzept', title: 'Moodboard + Wireframes Startseite', status: 'in-progress', assignee: 'Aylin Demir', estimateMin: 300, priority: 'high' },
{ project: 'Website-Redesign Konzept', title: 'Konkurrenzanalyse (5 Hotels)', status: 'todo', assignee: 'Sven Hoffmann', estimateMin: 150, priority: 'medium' },
]
for (const t of tasks) {
const { project, ...rest } = t
await db.task.create({ data: { ...rest, projectId: projectMap[project] } })
}
// --- Time entries -------------------------------------------------------
const timeEntries = [
{ project: 'Intranet-Portal Q4', description: 'Better Auth SSO Integration', minutes: 180, billable: true, rateCentsPerHour: CENTS(95), date: daysFromNow(-1) },
{ project: 'Intranet-Portal Q4', description: 'Kanban-Drag-Drop implementiert', minutes: 240, billable: true, rateCentsPerHour: CENTS(95), date: daysFromNow(-2) },
{ project: 'SaaS MVP — Auth & Billing', description: 'Stripe Webhook Handler', minutes: 150, billable: true, rateCentsPerHour: CENTS(110), date: daysFromNow(-1) },
{ project: 'SEO-Optimierung & BFGG-Check', description: 'axe Audit + Report', minutes: 120, billable: true, rateCentsPerHour: CENTS(85), date: daysFromNow(-3) },
{ project: 'WooCommerce Wartung Q4/2026', description: 'Plugin-Updates + Smoke-Test', minutes: 60, billable: true, rateCentsPerHour: CENTS(90), date: daysFromNow(-2) },
{ project: 'Website-Redesign Konzept', description: 'Moodboard Mood-Boards erstellt', minutes: 180, billable: true, rateCentsPerHour: CENTS(85), date: daysFromNow(-1) },
]
for (const te of timeEntries) {
const { project, ...rest } = te
await db.timeEntry.create({ data: { ...rest, projectId: projectMap[project] } })
}
// --- Licenses (issued) --------------------------------------------------
const licenseProducts = [
{ company: 'Nordwind Logistik GmbH', product: 'aos-intranet', plan: 'pro', status: 'active', key: 'AOS-PRO-7F3K-9Q2L-NWX4', maxActivations: 2, validUntil: daysFromNow(280) },
{ company: 'HanseMöbel Manufaktur GmbH', product: 'wp-plugin-erp-sync', plan: 'enterprise', status: 'active', key: 'AOS-ENT-3KP9-5RT8-VBN2', maxActivations: 3, validUntil: daysFromNow(120) },
{ company: 'TechStart UG (haftungsbeschränkt)', product: 'aos-cms', plan: 'starter', status: 'active', key: 'AOS-ST-9MN2-4QR7-LPZ8', maxActivations: 1, validUntil: daysFromNow(45) },
{ company: 'Bauer Handwerk GmbH', product: 'aos-cms', plan: 'pro', status: 'suspended', key: 'AOS-PRO-2JD8-6HJ5-KFR3', maxActivations: 1, validUntil: daysFromNow(-5) },
{ company: 'Klinik am Park MV GmbH', product: 'wp-plugin-bfsg-check', plan: 'pro', status: 'active', key: 'AOS-PRO-1ZA4-9XP2-MNB7', maxActivations: 2, validUntil: daysFromNow(200) },
{ company: 'Müller Bäckerei e.K.', product: 'woo-plugin-click-collect', plan: 'starter', status: 'expired', key: 'AOS-ST-5RC3-8BV1-QWE6', maxActivations: 1, validUntil: daysFromNow(-30) },
]
for (const lic of licenseProducts) {
const { company, ...rest } = lic
const created = await db.license.create({ data: { ...rest, companyId: companyMap[company], token: 'sig_' + rest.key.toLowerCase().replace(/-/g, '') } })
await db.licenseActivation.create({ data: { licenseId: created.id, domain: lic.product.includes('wp') ? 'wp.' + (lic.company === 'Nordwind Logistik GmbH' ? 'nordwind-logistik.de' : 'hansemoebel.de') : lic.company.split(' ')[0].toLowerCase() + '.de', environment: 'production', version: '1.4.2' } })
}
// --- Assets / renewal register -----------------------------------------
const assets = [
{ name: 'Domain nordwind-logistik.de', type: 'domain', provider: 'Ionos', companyId: companyMap['Nordwind Logistik GmbH'], costCentsPerMonth: CENTS(8), renewalDate: daysFromNow(35), rebill: true, markupPct: 50, status: 'active' },
{ name: 'Hosting VPS Strato', type: 'hosting', provider: 'Strato', companyId: companyMap['Nordwind Logistik GmbH'], costCentsPerMonth: CENTS(24), renewalDate: daysFromNow(12), rebill: true, markupPct: 100, status: 'expiring' },
{ name: 'SSL Wildcard *.hansemoebel.de', type: 'ssl', provider: 'Sectigo', companyId: companyMap['HanseMöbel Manufaktur GmbH'], costCentsPerMonth: CENTS(15), renewalDate: daysFromNow(7), rebill: true, markupPct: 30, status: 'expiring' },
{ name: 'WPML Multilingual CMS', type: 'plugin-seat', provider: 'OnTheGoSystems', companyId: companyMap['HanseMöbel Manufaktur GmbH'], costCentsPerMonth: CENTS(9), renewalDate: daysFromNow(90), rebill: true, markupPct: 0, status: 'active' },
{ name: 'Cloudflare Pro', type: 'saas', provider: 'Cloudflare', companyId: companyMap['TechStart UG (haftungsbeschränkt)'], costCentsPerMonth: CENTS(20), renewalDate: daysFromNow(60), rebill: false, markupPct: 0, status: 'active' },
{ name: 'Domain klinik-am-park-mv.de', type: 'domain', provider: 'Netcup', companyId: companyMap['Klinik am Park MV GmbH'], costCentsPerMonth: CENTS(5), renewalDate: daysFromNow(-2), rebill: true, markupPct: 50, status: 'expired' },
{ name: 'Managed WordPress Hosting', type: 'hosting', provider: 'Raidboxes', companyId: companyMap['Bauer Handwerk GmbH'], costCentsPerMonth: CENTS(29), renewalDate: daysFromNow(150), rebill: true, markupPct: 60, status: 'active' },
]
for (const a of assets) {
await db.asset.create({ data: a })
}
// --- Invoices -----------------------------------------------------------
let invCounter = 38
const invoices = [
{ company: 'Nordwind Logistik GmbH', number: `RE-2026-00${++invCounter}`, status: 'paid', type: 'final', issueDate: daysFromNow(-30), dueDate: daysFromNow(-14), paidDate: daysFromNow(-12), netCents: CENTS(1500), zugferd: true, xrechnung: true, datevExported: true, projectId: projectMap['Intranet-Portal Q4'] },
{ company: 'Nordwind Logistik GmbH', number: `RE-2026-00${++invCounter}`, status: 'sent', type: 'partial', issueDate: daysFromNow(-7), dueDate: daysFromNow(14), netCents: CENTS(5400), zugferd: true, xrechnung: false, datevExported: false, projectId: projectMap['Intranet-Portal Q4'] },
{ company: 'HanseMöbel Manufaktur GmbH', number: `RE-2026-00${++invCounter}`, status: 'sent', type: 'recurring', issueDate: daysFromNow(-3), dueDate: daysFromNow(24), netCents: CENTS(420), zugferd: true, xrechnung: true, datevExported: false },
{ company: 'TechStart UG (haftungsbeschränkt)', number: `RE-2026-00${++invCounter}`, status: 'overdue', type: 'final', issueDate: daysFromNow(-40), dueDate: daysFromNow(-10), netCents: CENTS(8800), zugferd: false, xrechnung: false, datevExported: false, projectId: projectMap['SaaS MVP — Auth & Billing'] },
{ company: 'Klinik am Park MV GmbH', number: `RE-2026-00${++invCounter}`, status: 'draft', type: 'final', issueDate: daysFromNow(0), dueDate: daysFromNow(14), netCents: CENTS(3200), zugferd: false, xrechnung: false, datevExported: false, projectId: projectMap['SEO-Optimierung & BFGG-Check'] },
{ company: 'Bauer Handwerk GmbH', number: `RE-2026-00${++invCounter}`, status: 'draft', type: 'partial', issueDate: daysFromNow(0), dueDate: daysFromNow(14), netCents: CENTS(1960), zugferd: false, xrechnung: false, datevExported: false, projectId: projectMap['Corporate Website Relaunch'] },
{ company: 'Ostsee Hotel & Spa AG', number: `RE-2026-00${++invCounter}`, status: 'cancelled', type: 'final', issueDate: daysFromNow(-50), dueDate: daysFromNow(-20), netCents: CENTS(1200), zugferd: false, xrechnung: false, datevExported: false },
]
for (const inv of invoices) {
const { company, ...rest } = inv
const taxBp = 1900
const grossCents = Math.round(rest.netCents * (1 + taxBp / 10000))
const created = await db.invoice.create({ data: { ...rest, companyId: companyMap[company], taxBp, grossCents } })
// one line per invoice for simplicity
await db.invoiceLine.create({
data: {
invoiceId: created.id,
description: inv.type === 'partial' ? 'Abschlagszahlung Projekt' : inv.type === 'recurring' ? 'Wartungspauschale monatlich' : 'Leistungen Projekt',
quantity: 1,
unitCents: rest.netCents,
totalCents: rest.netCents,
}
})
}
// --- Audit log entries ---------------------------------------------------
const auditEntries = [
{ actor: 'max@nordlicht.digital', action: 'crm.deal:update', entity: 'Deal', entityId: 'seed', before: '{"stage":"proposal"}', after: '{"stage":"won"}' },
{ actor: 'lena@nordlicht.digital', action: 'settings.update', entity: 'Setting', entityId: 'invoice.numberNext', before: '"0041"', after: '"0042"' },
{ actor: 'system', action: 'leads.job:score', entity: 'Lead', entityId: 'seed', after: '{"count":10}' },
{ actor: 'nora@nordlicht.digital', action: 'invoice.finalize', entity: 'Invoice', entityId: 'seed', before: '{"status":"draft"}', after: '{"status":"sent","zugferd":true}' },
{ actor: 'sven@nordlicht.digital', action: 'leads.review:approve', entity: 'Lead', entityId: 'seed', after: '{"status":"approved"}' },
]
for (const a of auditEntries) {
await db.auditLog.create({ data: { ...a, hash: 'sha256:' + Math.random().toString(36).slice(2) } })
}
console.log('Seed complete.')
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await db.$disconnect()
})