refactor: professional code quality audit
- Fix all TypeScript compilation errors (0 errors in src/) - Add try/catch error handling to ALL API routes (38 handlers) - Fix empty catch blocks (api-auth.ts now logs warnings) - Add aria-labels to all icon-only buttons (WCAG 2.1 AA) - Fix Prisma type mismatches (OR filter, Person.companyId, Estimate.projectId, Ticket.createdAt) - Fix webhook PATCH route (moved to [id]/route.ts) - Fix mobile-bottom-nav type safety - Fix KB route where-clause type - Fix test-mail banner null vs undefined - Fix task comment fetch: add cancellation + error toast - ESLint: 0 errors, 0 warnings - 175 files, 24.741 lines of code
This commit is contained in:
1 parent
969202764a
commit
fa8dd46571
175 files changed
+24741
-2
No files matched your search
Executable
+569
@@ -0,0 +1,569 @@
|
||||
// AOS — Agency Operating System
|
||||
// One deployment = one agency (single tenant). SQLite for this build.
|
||||
// Money is integer cents, tax rates are basis points. No floats for money.
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Platform
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String
|
||||
role String @default("owner") // owner|admin|sales|pm|developer|finance|readonly|client
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// Module registry — installed/enabled/disabled state of each domain module
|
||||
model Module {
|
||||
id String @id // crm|leads|projects|licenses|sales-docs|finance|support|...
|
||||
version String @default("1.0.0")
|
||||
enabled Boolean @default(true)
|
||||
installed Boolean @default(true)
|
||||
nav String? // JSON: [{label,route,permission}]
|
||||
events String? // JSON: {emits:[],listens:[]}
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Setting {
|
||||
key String @id
|
||||
value String // typed at app layer (encrypted secrets handled separately)
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
actor String? // user id or "system"
|
||||
action String // e.g. "crm.deal:update"
|
||||
entity String
|
||||
entityId String
|
||||
before String? // JSON
|
||||
after String? // JSON
|
||||
hash String? // hash chain
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CRM (Phase 1)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Company {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
legalForm String? // GmbH, UG, e.K., GbR, AG, Einzelunternehmen...
|
||||
vatId String? // USt-IdNr
|
||||
registerCourt String?
|
||||
registerNumber String? // HRB ...
|
||||
website String?
|
||||
email String?
|
||||
phone String?
|
||||
city String?
|
||||
zip String?
|
||||
street String?
|
||||
source String? // lead-finder|import|manual|referral
|
||||
tags String? // comma separated
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
persons Person[]
|
||||
deals Deal[]
|
||||
leads Lead[]
|
||||
licenses License[]
|
||||
invoices Invoice[]
|
||||
projects Project[]
|
||||
}
|
||||
|
||||
model Person {
|
||||
id String @id @default(cuid())
|
||||
companyId String?
|
||||
company Company? @relation(fields: [companyId], references: [id])
|
||||
firstName String
|
||||
lastName String
|
||||
email String?
|
||||
phone String?
|
||||
role String? // Geschäftsführer, Marketing, IT...
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Deal {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id])
|
||||
title String
|
||||
valueCents Int @default(0)
|
||||
probability Int @default(0) // 0-100
|
||||
stage String @default("lead") // pipeline stage key
|
||||
expectedClose DateTime?
|
||||
lossReason String?
|
||||
ownerId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Leads Finder (Phase 2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Lead {
|
||||
id String @id @default(cuid())
|
||||
// discovered company info (may become a Company when approved)
|
||||
companyName String
|
||||
website String?
|
||||
city String?
|
||||
branch String?
|
||||
// audit + scoring
|
||||
score Int @default(0) // 0-100 weighted rule score
|
||||
scoreLabel String? // new-site|redesign|seo|aeo|shop|maintenance|accessibility
|
||||
evidence String? // JSON array of {id,check,status,detail}
|
||||
explanation String? // LLM draft "why this lead" — every claim links to an evidence id
|
||||
suggestedAngle String? // proposed outreach angle
|
||||
status String @default("pending") // pending|approved|rejected|snoozed|converted
|
||||
reviewReason String?
|
||||
source String? // serpapi|tavily|apify|firecrawl
|
||||
radiusKm Int?
|
||||
// when converted, link to CRM company + deal
|
||||
companyId String?
|
||||
company Company? @relation(fields: [companyId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
reviewedAt DateTime?
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Projects (Phase 3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id])
|
||||
name String
|
||||
type String // website|shop|app|retainer
|
||||
status String @default("planning") // planning|active|review|done|on-hold|cancelled
|
||||
budgetType String @default("fixed") // fixed|time-materials|retainer
|
||||
budgetHours Int?
|
||||
spentHours Int @default(0)
|
||||
fixedPriceCents Int?
|
||||
startDate DateTime?
|
||||
dueDate DateTime?
|
||||
progress Int @default(0) // 0-100
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
tasks Task[]
|
||||
timeEntries TimeEntry[]
|
||||
approvals PortalApproval[]
|
||||
}
|
||||
|
||||
model Task {
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
title String
|
||||
description String?
|
||||
status String @default("todo") // todo|in-progress|review|done
|
||||
assignee String?
|
||||
estimateMin Int?
|
||||
priority String @default("medium") // low|medium|high
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
comments TaskComment[]
|
||||
}
|
||||
|
||||
model TaskComment {
|
||||
id String @id @default(cuid())
|
||||
taskId String
|
||||
task Task @relation(fields: [taskId], references: [id])
|
||||
author String
|
||||
body String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model TimeEntry {
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
taskId String? // optional link to a specific task
|
||||
userId String?
|
||||
description String
|
||||
minutes Int
|
||||
billable Boolean @default(true)
|
||||
rateCentsPerHour Int?
|
||||
invoiced Boolean @default(false)
|
||||
locked Boolean @default(false) // locked after PM approval
|
||||
date DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Licenses (Phase 4)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model License {
|
||||
id String @id @default(cuid())
|
||||
key String @unique
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id])
|
||||
product String // e.g. aos-cms, wp-plugin-xyz
|
||||
plan String // starter|pro|enterprise
|
||||
status String @default("active") // active|suspended|revoked|expired
|
||||
maxActivations Int @default(1)
|
||||
validUntil DateTime?
|
||||
token String? // Ed25519-signed token (mocked)
|
||||
createdAt DateTime @default(now())
|
||||
activations LicenseActivation[]
|
||||
}
|
||||
|
||||
model LicenseActivation {
|
||||
id String @id @default(cuid())
|
||||
licenseId String
|
||||
license License @relation(fields: [licenseId], references: [id])
|
||||
domain String
|
||||
fingerprint String?
|
||||
environment String @default("production") // production|staging
|
||||
version String?
|
||||
lastCheck DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
// Asset & renewal register (what the agency manages/pays for on behalf of clients)
|
||||
model Asset {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
type String // domain|hosting|ssl|plugin-seat|saas
|
||||
provider String? // Ionos, Strato, Cloudflare...
|
||||
companyId String? // may be internal asset
|
||||
costCentsPerMonth Int?
|
||||
renewalDate DateTime
|
||||
rebill Boolean @default(false)
|
||||
markupPct Int @default(0)
|
||||
status String @default("active") // active|expiring|expired|cancelled
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Finance (Phase 5)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Invoice {
|
||||
id String @id @default(cuid())
|
||||
number String @unique // gapless, e.g. RE-2026-0001
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id])
|
||||
status String @default("draft") // draft|sent|paid|overdue|cancelled|storno
|
||||
type String @default("final") // final|partial|recurring|credit
|
||||
issueDate DateTime @default(now())
|
||||
dueDate DateTime
|
||||
paidDate DateTime?
|
||||
netCents Int @default(0)
|
||||
taxBp Int @default(1900) // 19% as basis points
|
||||
grossCents Int @default(0)
|
||||
// E-Rechnung / compliance
|
||||
zugferd Boolean @default(false)
|
||||
xrechnung Boolean @default(false)
|
||||
datevExported Boolean @default(false)
|
||||
projectId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
lines InvoiceLine[]
|
||||
dunningRuns DunningRun[]
|
||||
}
|
||||
|
||||
model InvoiceLine {
|
||||
id String @id @default(cuid())
|
||||
invoiceId String
|
||||
invoice Invoice @relation(fields: [invoiceId], references: [id])
|
||||
description String
|
||||
quantity Int @default(1)
|
||||
unitCents Int
|
||||
totalCents Int
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Contracts / Verträge (Phase 5 — adapted from PerfEx Contracts, Germanized)
|
||||
// German contract types: Werkvertrag, Dienstvertrag, Wartungsvertrag/SLA,
|
||||
// AVV (Auftragsverarbeitungsvertrag), NDA, AVV
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Contract {
|
||||
id String @id @default(cuid())
|
||||
number String @unique // gapless, e.g. VER-2026-0001
|
||||
companyId String
|
||||
type String // werkvertrag|dienstvertrag|wartung-sla|avv|nda|dpa
|
||||
title String
|
||||
status String @default("draft") // draft|sent|signed|active|expired|terminated
|
||||
valueCents Int @default(0)
|
||||
startDate DateTime?
|
||||
endDate DateTime? // null = unbefristet
|
||||
templateId String? // reference to a stored template
|
||||
dealId String? // optional link to originating deal
|
||||
projectId String? // optional link to project
|
||||
// E-signature hook (mock)
|
||||
signedAt DateTime?
|
||||
signedBy String? // signer name/email
|
||||
signedIp String?
|
||||
signToken String? // unique token for the signing link
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Expenses / Ausgaben (adapted from PerfEx Expenses, Germanized for DATEV)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Expense {
|
||||
id String @id @default(cuid())
|
||||
number String @unique // gapless, e.g. AUS-2026-0001
|
||||
categoryId String // reise|software|hardware|fremdleistung|buero|marketing
|
||||
vendor String? // supplier / merchant
|
||||
description String
|
||||
amountCents Int // net amount
|
||||
taxBp Int @default(1900)
|
||||
grossCents Int @default(0)
|
||||
date DateTime @default(now())
|
||||
// linking
|
||||
companyId String? // customer this is billable to
|
||||
projectId String? // project this relates to
|
||||
billable Boolean @default(false) // convert to customer invoice
|
||||
markupPct Int @default(0) // markup when re-billed
|
||||
// recurring
|
||||
recurring Boolean @default(false)
|
||||
recurringInterval String? // monthly|quarterly|yearly
|
||||
// receipt + export
|
||||
receiptHash String? // SHA-256 of stored receipt file (content-addressed)
|
||||
datevExported Boolean @default(false)
|
||||
// conversion
|
||||
convertedInvoiceId String? // link to the invoice created from this expense
|
||||
status String @default("recorded") // recorded|approved|invoiced|rejected
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Support Tickets (PRD §6.7 + PerfEx Support Tickets + AI Integration)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Ticket {
|
||||
id String @id @default(cuid())
|
||||
number String @unique // gapless, e.g. TKT-2026-0001
|
||||
subject String
|
||||
status String @default("open") // open|pending|resolved|closed
|
||||
priority String @default("normal") // low|normal|high|urgent
|
||||
categoryId String // bug|anfrage|aenderung|konto|rechnung|hosting
|
||||
companyId String
|
||||
projectId String?
|
||||
licenseId String?
|
||||
// SLA
|
||||
slaResponseHours Int @default(8) // first response target
|
||||
slaResolveHours Int @default(48) // resolution target
|
||||
slaRespondedAt DateTime?
|
||||
slaResolvedAt DateTime?
|
||||
// AI
|
||||
aiSummary String? // LLM-generated summary of the conversation
|
||||
// assignee
|
||||
assignee String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
messages TicketMessage[]
|
||||
}
|
||||
|
||||
model TicketMessage {
|
||||
id String @id @default(cuid())
|
||||
ticketId String
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id])
|
||||
fromType String // customer|staff|system
|
||||
fromName String
|
||||
body String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Estimates / Angebote (PRD §6.5 sales-docs — quotes that convert to invoice)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Estimate {
|
||||
id String @id @default(cuid())
|
||||
number String @unique // gapless, e.g. AN-2026-0001
|
||||
companyId String
|
||||
dealId String? // originating deal
|
||||
projectId String? // creates project when accepted
|
||||
subject String
|
||||
status String @default("draft") // draft|sent|accepted|rejected|expired
|
||||
validUntil DateTime?
|
||||
issueDate DateTime @default(now())
|
||||
acceptedAt DateTime?
|
||||
acceptedBy String?
|
||||
notes String?
|
||||
netCents Int @default(0)
|
||||
taxBp Int @default(1900)
|
||||
grossCents Int @default(0)
|
||||
// AI
|
||||
aiDraftFromBrief String? // flag: was this drafted from a brief via AI
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
lines EstimateLine[]
|
||||
}
|
||||
|
||||
model EstimateLine {
|
||||
id String @id @default(cuid())
|
||||
estimateId String
|
||||
estimate Estimate @relation(fields: [estimateId], references: [id])
|
||||
description String
|
||||
quantity Int @default(1)
|
||||
unitCents Int
|
||||
totalCents Int
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Accounting / Buchhaltung (adapted from Haben, German-law compliant)
|
||||
// VAT return (Umsatzsteuer-Voranmeldung), EÜR, bank reconciliation, dunning.
|
||||
// PRD §6.6: double-entry + ELSTER out of scope; this covers the agency-facing
|
||||
// compliance layer (compute, validate, mock-submit) + bank matching.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Bank transactions imported from CAMT.053 or PSD2 (FinTS).
|
||||
model BankTransaction {
|
||||
id String @id @default(cuid())
|
||||
amountCents Int // signed: + credit (incoming), - debit (outgoing)
|
||||
bookingDate DateTime
|
||||
valueDate DateTime
|
||||
partnerName String?
|
||||
partnerIban String?
|
||||
reference String?
|
||||
purpose String?
|
||||
matchedInvoiceId String?
|
||||
matchedStatus String @default("unmatched")
|
||||
matchReason String?
|
||||
importBatchId String?
|
||||
importedAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
// VAT pre-registration periods (Umsatzsteuer-Voranmeldung)
|
||||
model VatReturn {
|
||||
id String @id @default(cuid())
|
||||
periodYear Int
|
||||
periodMonth Int
|
||||
periodType String @default("monthly")
|
||||
status String @default("draft")
|
||||
k81StandardNetCents Int @default(0)
|
||||
k86ReducedNetCents Int @default(0)
|
||||
k66TaxFreeCents Int @default(0)
|
||||
k83TaxStandardCents Int @default(0)
|
||||
k65TaxReducedCents Int @default(0)
|
||||
k37InputTaxCents Int @default(0)
|
||||
outputTaxCents Int @default(0)
|
||||
inputTaxCents Int @default(0)
|
||||
payableCents Int @default(0)
|
||||
submittedAt DateTime?
|
||||
elsterRef String?
|
||||
acceptedAt DateTime?
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// Dunning levels for overdue invoices (Mahnwesen)
|
||||
model DunningRun {
|
||||
id String @id @default(cuid())
|
||||
invoiceId String
|
||||
invoice Invoice @relation(fields: [invoiceId], references: [id])
|
||||
level Int
|
||||
status String @default("sent")
|
||||
feesCents Int @default(0)
|
||||
sentAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API (PRD §5.5) — API tokens with scopes, webhooks with HMAC signing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model ApiToken {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
// The token shown to the user ONCE. We store only its SHA-256 hash so a
|
||||
// compromised DB does not leak usable tokens (like GitHub PATs).
|
||||
tokenHash String @unique
|
||||
tokenPrefix String // first 8 chars for identification (e.g. "aos_abc1…")
|
||||
scopes String // JSON array of scope strings, e.g. ["crm.company:read","finance.invoice:read"]
|
||||
lastUsedAt DateTime?
|
||||
expiresAt DateTime?
|
||||
revokedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Webhook {
|
||||
id String @id @default(cuid())
|
||||
url String // https endpoint to deliver to
|
||||
events String // JSON array, e.g. ["invoice.paid","contract.signed"]
|
||||
secret String // HMAC signing secret (stored, needed to verify signatures)
|
||||
active Boolean @default(true)
|
||||
// delivery stats
|
||||
sentCount Int @default(0)
|
||||
failCount Int @default(0)
|
||||
lastSentAt DateTime?
|
||||
lastStatus Int? // last HTTP status code received
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Client Portal (PRD §6.3) — milestone approvals with evidence
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model PortalApproval {
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
title String // e.g. "Design-Entwürfe freigeben"
|
||||
description String?
|
||||
status String @default("pending") // pending|approved|rejected|changes-requested
|
||||
// evidence (PRD §6.3: "approvals with timestamp and IP evidence")
|
||||
decidedAt DateTime?
|
||||
decidedBy String? // client name
|
||||
decidedIp String? // real client IP at decision time
|
||||
decision String? // free-text reason/feedback
|
||||
// content hash for tamper-evidence
|
||||
contentHash String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Knowledge Base (PRD §6.7) — internal + client-facing docs with versioning
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Article {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
title String
|
||||
category String // onboarding|launch-checklist|templates|handbuch|faq
|
||||
visibility String @default("internal") // internal|client|public
|
||||
excerpt String?
|
||||
content String // markdown
|
||||
tags String? // comma separated
|
||||
version Int @default(1)
|
||||
authorId String?
|
||||
authorName String?
|
||||
published Boolean @default(true)
|
||||
viewCount Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user