a6c0f3de-eeb0-4d76-a57f-f600dd133c06
This commit is contained in:
1 parent
2aa3496a90
commit
762f3d7373
141 files changed
+4485
-69
No files matched your search
@@ -0,0 +1,8 @@
|
||||
---PORT---
|
||||
LISTEN 0 511 *:3000 *:* users:(("next-server (v1",pid=4467,fd=22))
|
||||
---GET /---
|
||||
} catch (n) {}
|
||||
})("class","theme","light",null,["light","dark"],null,false,true)</script><div class="flex min-h-screen w-full bg-background"><aside class="hidden w-64 shrink-0 border-r border-border md:block"><div class="sticky top-0 h-screen"><div class="flex h-full flex-col bg-sidebar text-sidebar-foreground"><div class="flex h-16 items-center gap-2.5 px-5 border-b border-sidebar-border"><div class="flex size-9 items-center justify-center rounded-md bg-primary text-primary-foreground font-bold tracking-tight">A</div><div class="flex flex-col leading-tight"><span class="text-sm font-semibold">AOS</span><span class="text-[11px] text-muted-foreground">Nordlicht Digital</span></div></div><div class="px-3 pt-3"><button class="flex w-full items-center gap-2 rounded-md border border-sidebar-border bg-background/50 px-3 py-2 text-sm text-muted-foreground transition hover:bg-background"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search size-3.5" aria-hidden="true"><path d="m21 21-4.34-4.34"></path><circle cx="11" cy="11" r="8"></circle></svg><span>Suchen…</span><kbd class="ml-auto inline-flex items-center gap-0.5 rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-mono"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-command size-2.5" aria-hidden="true"><path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"></path></svg>K</kbd></button></div><nav class="flex-1 overflow-y-auto px-2 py-3 [scrollbar-width:thin]"><div class="mb-4"><p class="px-3 pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">Übersicht</p><div class="space-y-0.5"><button class="group flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition bg-sidebar-accent text-sidebar-accent-foreground" aria-current="page"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-layout-dashboard size-4 shrink-0 text-foreground" aria-hidden="true"><rect width="7" height="9" x="3" y="3" rx="1"></rect><rect width="7" height="5" x="14" y="3" rx="1"></rect><rect width="7" height="9" x="14" y="12" rx="1"></rect><rect width="7" height="5" x="3" y="16" rx="1"></rect></svg><span class="truncate">Dashboard</span></button><button class="group flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition text-muted-foreground hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar-days size-4 shrink-0 text-muted-foreground group-hover:text-foreground" aria-hidden="true"><path d="M8 2v4"></path><path d="M16 2v4"></path><rect width="18" height="18" x="3" y="4" rx="2"></rect><path d="M3 10h18"></path><path d="M8 14h.01"></path><path d="M12 14h.01"></path><path d="M16 14h.01"></path><path d="M8 18h.01"></path><path d="M12 18h.01"></path><path d="M16 18h.01"></path></svg><span class="truncate">Fristen</span></button><button class="group flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition text-muted-foreground hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chart-column size-4 shrink-0 text-muted-foreground group-hover:text-foreground" aria-hidden="true"><path d="M3 3v16a2 2 0 0 0 2 2h16"></path><path d="M18 17V9"></path><path d="M13 17V5"></path><path d="M8 17v-3"></path></svg><span class="truncate">Reporting</span></button></div></div><div class="mb-4"><p class="px-3 pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">Vertrieb</p><div class="space-y-0.5"><button class="group flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition text-muted-foreground hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users size-4 shrink-0 text-muted-foreground group-hover:text-foreground" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><path d="M16 3.128a4 4 0 0 1 0 7.744"></path><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><circle cx="9" cy="7" r="4"></circle></svg><span claLine truncated
|
||||
HTTP 200 | time=0.074956s
|
||||
---PORT after /---
|
||||
LISTEN 0 511 *:3000 *:* users:(("next-server (v1",pid=4467,fd=22))
|
||||
@@ -0,0 +1,895 @@
|
||||
1→// AOS — Agency Operating System
|
||||
2→// One deployment = one agency (single tenant). SQLite for this build.
|
||||
3→// Money is integer cents, tax rates are basis points. No floats for money.
|
||||
4→
|
||||
5→generator client {
|
||||
6→ provider = "prisma-client-js"
|
||||
7→}
|
||||
8→
|
||||
9→datasource db {
|
||||
10→ provider = "sqlite"
|
||||
11→ url = env("DATABASE_URL")
|
||||
12→}
|
||||
13→
|
||||
14→// ---------------------------------------------------------------------------
|
||||
15→// Platform
|
||||
16→// ---------------------------------------------------------------------------
|
||||
17→
|
||||
18→model User {
|
||||
19→ id String @id @default(cuid())
|
||||
20→ email String @unique
|
||||
21→ name String
|
||||
22→ role String @default("owner") // owner|admin|sales|pm|developer|finance|readonly|client
|
||||
23→ createdAt DateTime @default(now())
|
||||
24→ updatedAt DateTime @updatedAt
|
||||
25→}
|
||||
26→
|
||||
27→// Module registry — installed/enabled/disabled state of each domain module
|
||||
28→model Module {
|
||||
29→ id String @id // crm|leads|projects|licenses|sales-docs|finance|support|...
|
||||
30→ version String @default("1.0.0")
|
||||
31→ enabled Boolean @default(true)
|
||||
32→ installed Boolean @default(true)
|
||||
33→ nav String? // JSON: [{label,route,permission}]
|
||||
34→ events String? // JSON: {emits:[],listens:[]}
|
||||
35→ updatedAt DateTime @updatedAt
|
||||
36→ createdAt DateTime @default(now())
|
||||
37→}
|
||||
38→
|
||||
39→model Setting {
|
||||
40→ key String @id
|
||||
41→ value String // typed at app layer (encrypted secrets handled separately)
|
||||
42→ updatedAt DateTime @updatedAt
|
||||
43→}
|
||||
44→
|
||||
45→model AuditLog {
|
||||
46→ id String @id @default(cuid())
|
||||
47→ actor String? // user id or "system"
|
||||
48→ action String // e.g. "crm.deal:update"
|
||||
49→ entity String
|
||||
50→ entityId String
|
||||
51→ before String? // JSON
|
||||
52→ after String? // JSON
|
||||
53→ hash String? // hash chain
|
||||
54→ createdAt DateTime @default(now())
|
||||
55→}
|
||||
56→
|
||||
57→// ---------------------------------------------------------------------------
|
||||
58→// CRM (Phase 1)
|
||||
59→// ---------------------------------------------------------------------------
|
||||
60→
|
||||
61→model Company {
|
||||
62→ id String @id @default(cuid())
|
||||
63→ name String
|
||||
64→ legalForm String? // GmbH, UG, e.K., GbR, AG, Einzelunternehmen...
|
||||
65→ vatId String? // USt-IdNr
|
||||
66→ registerCourt String?
|
||||
67→ registerNumber String? // HRB ...
|
||||
68→ website String?
|
||||
69→ email String?
|
||||
70→ phone String?
|
||||
71→ city String?
|
||||
72→ zip String?
|
||||
73→ street String?
|
||||
74→ source String? // lead-finder|import|manual|referral|agency|self-signup
|
||||
75→ tags String? // comma separated
|
||||
76→
|
||||
77→ // --- B2B Agency / Reseller (PRD §6.5) ---
|
||||
78→ // A company may be a regular client OR an agency (reseller). Agencies
|
||||
79→ // onboard their own clients and license them; they cannot create invoices
|
||||
80→ // (they must use their own accounting system, e.g. lexoffice/sevdesk via REST API).
|
||||
81→ isAgency Boolean @default(false)
|
||||
82→ // Agency approval: pending (just signed up) → approved (admin allowed) → rejected.
|
||||
83→ agencyStatus String? // null when not an agency; "pending"|"approved"|"rejected"
|
||||
84→ agencyAppliedAt DateTime?
|
||||
85→ agencyApprovedAt DateTime?
|
||||
86→ agencyRejectedAt DateTime?
|
||||
87→ agencyRejectReason String?
|
||||
88→ // Admin-set discount percentage applied to all licenses this agency buys
|
||||
89→ // from us. Range 0-20 (admin decision only). Default 0.
|
||||
90→ agencyDiscountPct Int @default(0)
|
||||
91→ // Self-serve clients belong to an agency. NULL = direct client of ours.
|
||||
92→ parentAgencyId String? // FK to Company(id) — the agency that owns this client
|
||||
93→ parentAgency Company? @relation("AgencyClients", fields: [parentAgencyId], references: [id])
|
||||
94→ agencyClients Company[] @relation("AgencyClients")
|
||||
95→
|
||||
96→ createdAt DateTime @default(now())
|
||||
97→ updatedAt DateTime @updatedAt
|
||||
98→ persons Person[]
|
||||
99→ deals Deal[]
|
||||
100→ leads Lead[]
|
||||
101→ licenses License[]
|
||||
102→ invoices Invoice[]
|
||||
103→ projects Project[]
|
||||
104→ contracts Contract[]
|
||||
105→ expenses Expense[]
|
||||
106→ tickets Ticket[]
|
||||
107→ agencyApplication AgencyApplication?
|
||||
108→}
|
||||
109→
|
||||
110→model Person {
|
||||
111→ id String @id @default(cuid())
|
||||
112→ companyId String?
|
||||
113→ company Company? @relation(fields: [companyId], references: [id])
|
||||
114→ firstName String
|
||||
115→ lastName String
|
||||
116→ email String?
|
||||
117→ phone String?
|
||||
118→ role String? // Geschäftsführer, Marketing, IT...
|
||||
119→ createdAt DateTime @default(now())
|
||||
120→ updatedAt DateTime @updatedAt
|
||||
121→}
|
||||
122→
|
||||
123→model Deal {
|
||||
124→ id String @id @default(cuid())
|
||||
125→ companyId String
|
||||
126→ company Company @relation(fields: [companyId], references: [id])
|
||||
127→ title String
|
||||
128→ valueCents Int @default(0)
|
||||
129→ probability Int @default(0) // 0-100
|
||||
130→ stage String @default("lead") // pipeline stage key
|
||||
131→ expectedClose DateTime?
|
||||
132→ lossReason String?
|
||||
133→ ownerId String?
|
||||
134→ createdAt DateTime @default(now())
|
||||
135→ updatedAt DateTime @updatedAt
|
||||
136→}
|
||||
137→
|
||||
138→// ---------------------------------------------------------------------------
|
||||
139→// Leads Finder (Phase 2)
|
||||
140→// ---------------------------------------------------------------------------
|
||||
141→
|
||||
142→model Lead {
|
||||
143→ id String @id @default(cuid())
|
||||
144→ // discovered company info (may become a Company when approved)
|
||||
145→ companyName String
|
||||
146→ website String?
|
||||
147→ city String?
|
||||
148→ branch String?
|
||||
149→ // audit + scoring
|
||||
150→ score Int @default(0) // 0-100 weighted rule score
|
||||
151→ scoreLabel String? // new-site|redesign|seo|aeo|shop|maintenance|accessibility
|
||||
152→ evidence String? // JSON array of {id,check,status,detail}
|
||||
153→ explanation String? // LLM draft "why this lead" — every claim links to an evidence id
|
||||
154→ suggestedAngle String? // proposed outreach angle
|
||||
155→ status String @default("pending") // pending|approved|rejected|snoozed|converted
|
||||
156→ reviewReason String?
|
||||
157→ source String? // serpapi|tavily|apify|firecrawl
|
||||
158→ radiusKm Int?
|
||||
159→ // when converted, link to CRM company + deal
|
||||
160→ companyId String?
|
||||
161→ company Company? @relation(fields: [companyId], references: [id])
|
||||
162→ createdAt DateTime @default(now())
|
||||
163→ reviewedAt DateTime?
|
||||
164→ updatedAt DateTime @updatedAt
|
||||
165→}
|
||||
166→
|
||||
167→// ---------------------------------------------------------------------------
|
||||
168→// Projects (Phase 3)
|
||||
169→// ---------------------------------------------------------------------------
|
||||
170→
|
||||
171→model Project {
|
||||
172→ id String @id @default(cuid())
|
||||
173→ companyId String
|
||||
174→ company Company @relation(fields: [companyId], references: [id])
|
||||
175→ name String
|
||||
176→ type String // website|shop|app|retainer
|
||||
177→ status String @default("planning") // planning|active|review|done|on-hold|cancelled
|
||||
178→ budgetType String @default("fixed") // fixed|time-materials|retainer
|
||||
179→ budgetHours Int?
|
||||
180→ spentHours Int @default(0)
|
||||
181→ fixedPriceCents Int?
|
||||
182→ startDate DateTime?
|
||||
183→ dueDate DateTime?
|
||||
184→ progress Int @default(0) // 0-100
|
||||
185→ createdAt DateTime @default(now())
|
||||
186→ updatedAt DateTime @updatedAt
|
||||
187→ tasks Task[]
|
||||
188→ timeEntries TimeEntry[]
|
||||
189→ approvals PortalApproval[]
|
||||
190→}
|
||||
191→
|
||||
192→model Task {
|
||||
193→ id String @id @default(cuid())
|
||||
194→ projectId String
|
||||
195→ project Project @relation(fields: [projectId], references: [id])
|
||||
196→ title String
|
||||
197→ description String?
|
||||
198→ status String @default("todo") // todo|in-progress|review|done
|
||||
199→ assignee String?
|
||||
200→ estimateMin Int?
|
||||
201→ priority String @default("medium") // low|medium|high
|
||||
202→ createdAt DateTime @default(now())
|
||||
203→ updatedAt DateTime @updatedAt
|
||||
204→ comments TaskComment[]
|
||||
205→}
|
||||
206→
|
||||
207→model TaskComment {
|
||||
208→ id String @id @default(cuid())
|
||||
209→ taskId String
|
||||
210→ task Task @relation(fields: [taskId], references: [id])
|
||||
211→ author String
|
||||
212→ body String
|
||||
213→ createdAt DateTime @default(now())
|
||||
214→}
|
||||
215→
|
||||
216→model TimeEntry {
|
||||
217→ id String @id @default(cuid())
|
||||
218→ projectId String
|
||||
219→ project Project @relation(fields: [projectId], references: [id])
|
||||
220→ taskId String? // optional link to a specific task
|
||||
221→ userId String?
|
||||
222→ description String
|
||||
223→ minutes Int
|
||||
224→ billable Boolean @default(true)
|
||||
225→ rateCentsPerHour Int?
|
||||
226→ invoiced Boolean @default(false)
|
||||
227→ locked Boolean @default(false) // locked after PM approval
|
||||
228→ date DateTime @default(now())
|
||||
229→ createdAt DateTime @default(now())
|
||||
230→}
|
||||
231→
|
||||
232→// ---------------------------------------------------------------------------
|
||||
233→// Licenses (Phase 4)
|
||||
234→// ---------------------------------------------------------------------------
|
||||
235→
|
||||
236→model License {
|
||||
237→ id String @id @default(cuid())
|
||||
238→ key String @unique
|
||||
239→ companyId String
|
||||
240→ company Company @relation(fields: [companyId], references: [id])
|
||||
241→ product String // e.g. aos-cms, restov1, wp-plugin-xyz
|
||||
242→ plan String // starter|pro|enterprise
|
||||
243→ status String @default("active") // active|suspended|revoked|expired|cancelled
|
||||
244→ // environment: demo licenses are feature-limited / watermarked / time-boxed
|
||||
245→ // and can be promoted to production via the license server.
|
||||
246→ environment String @default("production") // demo|production
|
||||
247→ // Modular licensing: a license may be the "core" license, or a module add-on.
|
||||
248→ // Module licenses reference their parent core license via parentId.
|
||||
249→ parentId String? // null = core license; non-null = module add-on
|
||||
250→ parent License? @relation("LicenseModules", fields: [parentId], references: [id])
|
||||
251→ modules License[] @relation("LicenseModules")
|
||||
252→ // The module key this license grants (only when parentId is set).
|
||||
253→ // e.g. "online-order", "payment", "reservation", "delivery", "table-service"
|
||||
254→ module String? // null for core licenses
|
||||
255→ maxActivations Int @default(1)
|
||||
256→ validUntil DateTime?
|
||||
257→ token String? // Ed25519-signed token (mocked)
|
||||
258→ // Suspend/bloke reason + timestamp — set when status -> suspended.
|
||||
259→ suspendReason String?
|
||||
260→ suspendedAt DateTime?
|
||||
261→
|
||||
262→ // --- B2B Subscription & Billing (German law) ---
|
||||
263→ // Kündigung frist: 3 months (90 days) notice period before the next renewal.
|
||||
264→ // A client can cancel, but the cancellation only takes effect at the end of
|
||||
265→ // the notice period. Set on issuance, defaults to 90 days.
|
||||
266→ noticePeriodDays Int @default(90)
|
||||
267→ // Trial: first 14 days are a free trial. Subscription can be cancelled within
|
||||
268→ // these 14 days at any time with immediate effect (no charge). Day 15 the
|
||||
269→ // paid subscription starts.
|
||||
270→ trialEndsAt DateTime? // null = no trial (e.g. agency-issued)
|
||||
271→ subscriptionStartsAt DateTime? // when the paid subscription begins (trial + 1 day)
|
||||
272→ // Billing cycle: monthly or yearly. Yearly gets a discount on the total.
|
||||
273→ billingCycle String @default("monthly") // monthly|yearly
|
||||
274→ // The price in cents per billing cycle BEFORE any discount.
|
||||
275→ // For monthly: this is the monthly price (e.g. 5000 = 50 €).
|
||||
276→ // For yearly: this is the yearly total (e.g. 60000 = 600 € before yearly discount).
|
||||
277→ priceCents Int @default(0)
|
||||
278→ // Discount percentage applied to this license (0-100).
|
||||
279→ // For agencies: copied from Company.agencyDiscountPct at issue time.
|
||||
280→ // For yearly billing: a yearly discount (e.g. 10%) is applied on top.
|
||||
281→ discountPct Int @default(0)
|
||||
282→ // The effective amount actually charged (priceCents - all discounts).
|
||||
283→ // Computed at issue time and stored for audit/GoBD.
|
||||
284→ netPriceCents Int @default(0)
|
||||
285→ // Cancellation tracking (Kündigung).
|
||||
286→ cancelledAt DateTime? // when the client cancelled
|
||||
287→ cancellationEffectiveAt DateTime? // when the cancellation takes effect (notice period end)
|
||||
288→ cancellationReason String?
|
||||
289→ // Stripe integration (optional — populated when paid via Stripe).
|
||||
290→ stripeCustomerId String?
|
||||
291→ stripeSubscriptionId String?
|
||||
292→ stripePriceId String?
|
||||
293→
|
||||
294→ createdAt DateTime @default(now())
|
||||
295→ activations LicenseActivation[]
|
||||
296→}
|
||||
297→
|
||||
298→model LicenseActivation {
|
||||
299→ id String @id @default(cuid())
|
||||
300→ licenseId String
|
||||
301→ license License @relation(fields: [licenseId], references: [id])
|
||||
302→ domain String
|
||||
303→ fingerprint String?
|
||||
304→ environment String @default("production") // production|staging
|
||||
305→ version String?
|
||||
306→ lastCheck DateTime @default(now())
|
||||
307→ createdAt DateTime @default(now())
|
||||
308→}
|
||||
309→
|
||||
310→// Asset & renewal register (what the agency manages/pays for on behalf of clients)
|
||||
311→model Asset {
|
||||
312→ id String @id @default(cuid())
|
||||
313→ name String
|
||||
314→ type String // domain|hosting|ssl|plugin-seat|saas
|
||||
315→ provider String? // Ionos, Strato, Cloudflare...
|
||||
316→ companyId String? // may be internal asset
|
||||
317→ costCentsPerMonth Int?
|
||||
318→ renewalDate DateTime
|
||||
319→ rebill Boolean @default(false)
|
||||
320→ markupPct Int @default(0)
|
||||
321→ status String @default("active") // active|expiring|expired|cancelled
|
||||
322→ createdAt DateTime @default(now())
|
||||
323→ updatedAt DateTime @updatedAt
|
||||
324→}
|
||||
325→
|
||||
326→// ---------------------------------------------------------------------------
|
||||
327→// Finance (Phase 5)
|
||||
328→// ---------------------------------------------------------------------------
|
||||
329→
|
||||
330→model Invoice {
|
||||
331→ id String @id @default(cuid())
|
||||
332→ number String @unique // gapless, e.g. RE-2026-0001
|
||||
333→ companyId String
|
||||
334→ company Company @relation(fields: [companyId], references: [id])
|
||||
335→ status String @default("draft") // draft|sent|paid|overdue|cancelled|storno
|
||||
336→ type String @default("final") // final|partial|recurring|credit
|
||||
337→ issueDate DateTime @default(now())
|
||||
338→ dueDate DateTime
|
||||
339→ paidDate DateTime?
|
||||
340→ netCents Int @default(0)
|
||||
341→ taxBp Int @default(1900) // 19% as basis points
|
||||
342→ grossCents Int @default(0)
|
||||
343→ // E-Rechnung / compliance
|
||||
344→ zugferd Boolean @default(false)
|
||||
345→ xrechnung Boolean @default(false)
|
||||
346→ datevExported Boolean @default(false)
|
||||
347→ projectId String?
|
||||
348→ createdAt DateTime @default(now())
|
||||
349→ updatedAt DateTime @updatedAt
|
||||
350→ lines InvoiceLine[]
|
||||
351→ dunningRuns DunningRun[]
|
||||
352→}
|
||||
353→
|
||||
354→model InvoiceLine {
|
||||
355→ id String @id @default(cuid())
|
||||
356→ invoiceId String
|
||||
357→ invoice Invoice @relation(fields: [invoiceId], references: [id])
|
||||
358→ description String
|
||||
359→ quantity Int @default(1)
|
||||
360→ unitCents Int
|
||||
361→ totalCents Int
|
||||
362→}
|
||||
363→
|
||||
364→// ---------------------------------------------------------------------------
|
||||
365→// Contracts / Verträge (Phase 5 — adapted from PerfEx Contracts, Germanized)
|
||||
366→// German contract types: Werkvertrag, Dienstvertrag, Wartungsvertrag/SLA,
|
||||
367→// AVV (Auftragsverarbeitungsvertrag), NDA, AVV
|
||||
368→// ---------------------------------------------------------------------------
|
||||
369→
|
||||
370→model Contract {
|
||||
371→ id String @id @default(cuid())
|
||||
372→ number String @unique // gapless, e.g. VER-2026-0001
|
||||
373→ companyId String
|
||||
374→ company Company @relation(fields: [companyId], references: [id])
|
||||
375→ type String // werkvertrag|dienstvertrag|wartung-sla|avv|nda|dpa
|
||||
376→ title String
|
||||
377→ status String @default("draft") // draft|sent|signed|active|expired|terminated
|
||||
378→ valueCents Int @default(0)
|
||||
379→ startDate DateTime?
|
||||
380→ endDate DateTime? // null = unbefristet
|
||||
381→ templateId String? // reference to a stored template
|
||||
382→ dealId String? // optional link to originating deal
|
||||
383→ projectId String? // optional link to project
|
||||
384→ // E-signature hook (mock)
|
||||
385→ signedAt DateTime?
|
||||
386→ signedBy String? // signer name/email
|
||||
387→ signedIp String?
|
||||
388→ signToken String? // unique token for the signing link
|
||||
389→ notes String?
|
||||
390→ createdAt DateTime @default(now())
|
||||
391→ updatedAt DateTime @updatedAt
|
||||
392→}
|
||||
393→
|
||||
394→// ---------------------------------------------------------------------------
|
||||
395→// Expenses / Ausgaben (adapted from PerfEx Expenses, Germanized for DATEV)
|
||||
396→// ---------------------------------------------------------------------------
|
||||
397→
|
||||
398→model Expense {
|
||||
399→ id String @id @default(cuid())
|
||||
400→ number String @unique // gapless, e.g. AUS-2026-0001
|
||||
401→ categoryId String // reise|software|hardware|fremdleistung|buero|marketing
|
||||
402→ vendor String? // supplier / merchant
|
||||
403→ description String
|
||||
404→ amountCents Int // net amount
|
||||
405→ taxBp Int @default(1900)
|
||||
406→ grossCents Int @default(0)
|
||||
407→ date DateTime @default(now())
|
||||
408→ // linking
|
||||
409→ companyId String? // customer this is billable to
|
||||
410→ company Company? @relation(fields: [companyId], references: [id])
|
||||
411→ projectId String? // project this relates to
|
||||
412→ billable Boolean @default(false) // convert to customer invoice
|
||||
413→ markupPct Int @default(0) // markup when re-billed
|
||||
414→ // recurring
|
||||
415→ recurring Boolean @default(false)
|
||||
416→ recurringInterval String? // monthly|quarterly|yearly
|
||||
417→ // receipt + export
|
||||
418→ receiptHash String? // SHA-256 of stored receipt file (content-addressed)
|
||||
419→ datevExported Boolean @default(false)
|
||||
420→ // conversion
|
||||
421→ convertedInvoiceId String? // link to the invoice created from this expense
|
||||
422→ status String @default("recorded") // recorded|approved|invoiced|rejected
|
||||
423→ createdAt DateTime @default(now())
|
||||
424→ updatedAt DateTime @updatedAt
|
||||
425→}
|
||||
426→
|
||||
427→// ---------------------------------------------------------------------------
|
||||
428→// Support Tickets (PRD §6.7 + PerfEx Support Tickets + AI Integration)
|
||||
429→// ---------------------------------------------------------------------------
|
||||
430→
|
||||
431→model Ticket {
|
||||
432→ id String @id @default(cuid())
|
||||
433→ number String @unique // gapless, e.g. TKT-2026-0001
|
||||
434→ subject String
|
||||
435→ status String @default("open") // open|pending|resolved|closed
|
||||
436→ priority String @default("normal") // low|normal|high|urgent
|
||||
437→ categoryId String // bug|anfrage|aenderung|konto|rechnung|hosting
|
||||
438→ companyId String
|
||||
439→ company Company @relation(fields: [companyId], references: [id])
|
||||
440→ projectId String?
|
||||
441→ licenseId String?
|
||||
442→ // --- Ticket routing (agency model) ---
|
||||
443→ // If the client belongs to an agency, the ticket is routed to that agency
|
||||
444→ // (agencyCompanyId). The agency handles it; only if the agency escalates
|
||||
445→ // does the ticket reach us (routedToAgency = false, escalatedToAdmin = true).
|
||||
446→ agencyCompanyId String? // the reseller that owns this client (if any)
|
||||
447→ routedToAgency Boolean @default(false) // ticket sent to agency inbox
|
||||
448→ escalatedToAdmin Boolean @default(false) // agency escalated to us
|
||||
449→ escalatedAt DateTime?
|
||||
450→ // SLA
|
||||
451→ slaResponseHours Int @default(8) // first response target
|
||||
452→ slaResolveHours Int @default(48) // resolution target
|
||||
453→ slaRespondedAt DateTime?
|
||||
454→ slaResolvedAt DateTime?
|
||||
455→ // AI
|
||||
456→ aiSummary String? // LLM-generated summary of the conversation
|
||||
457→ // assignee
|
||||
458→ assignee String?
|
||||
459→ createdAt DateTime @default(now())
|
||||
460→ updatedAt DateTime @updatedAt
|
||||
461→ messages TicketMessage[]
|
||||
462→}
|
||||
463→
|
||||
464→model TicketMessage {
|
||||
465→ id String @id @default(cuid())
|
||||
466→ ticketId String
|
||||
467→ ticket Ticket @relation(fields: [ticketId], references: [id])
|
||||
468→ fromType String // customer|staff|system|agency
|
||||
469→ fromName String
|
||||
470→ body String
|
||||
471→ createdAt DateTime @default(now())
|
||||
472→}
|
||||
473→
|
||||
474→// ---------------------------------------------------------------------------
|
||||
475→// Agency Applications (PRD §6.5 — reseller onboarding)
|
||||
476→// ---------------------------------------------------------------------------
|
||||
477→// When a company signs up, they may choose to become an agency (reseller).
|
||||
478→// The application is stored here and awaits admin approval. On approval,
|
||||
479→// the Company.isAgency flag is set, Company.agencyStatus = "approved", and
|
||||
480→// an email is sent to the applicant with their discount percentage.
|
||||
481→model AgencyApplication {
|
||||
482→ id String @id @default(cuid())
|
||||
483→ companyId String @unique
|
||||
484→ company Company @relation(fields: [companyId], references: [id])
|
||||
485→ status String @default("pending") // pending|approved|rejected
|
||||
486→ // Discount the admin grants this agency (0-20%). Applied to all licenses
|
||||
487→ // the agency buys from us. Admin-only decision.
|
||||
488→ discountPct Int @default(0) // 0-20
|
||||
489→ // Business justification fields (filled by the applicant)
|
||||
490→ businessModel String? // e.g. "Webdesign agency", "IT consultant"
|
||||
491→ expectedClientVolume String? // e.g. "10-20 clients"
|
||||
492→ referredBy String? // how did they hear about us
|
||||
493→ // Review
|
||||
494→ reviewedBy String?
|
||||
495→ reviewedAt DateTime?
|
||||
496→ rejectReason String?
|
||||
497→ appliedAt DateTime @default(now())
|
||||
498→ createdAt DateTime @default(now())
|
||||
499→ updatedAt DateTime @updatedAt
|
||||
500→}
|
||||
501→
|
||||
502→// ---------------------------------------------------------------------------
|
||||
503→// Estimates / Angebote (PRD §6.5 sales-docs — quotes that convert to invoice)
|
||||
504→// ---------------------------------------------------------------------------
|
||||
505→
|
||||
506→model Estimate {
|
||||
507→ id String @id @default(cuid())
|
||||
508→ number String @unique // gapless, e.g. AN-2026-0001
|
||||
509→ companyId String
|
||||
510→ dealId String? // originating deal
|
||||
511→ projectId String? // creates project when accepted
|
||||
512→ subject String
|
||||
513→ status String @default("draft") // draft|sent|accepted|rejected|expired
|
||||
514→ validUntil DateTime?
|
||||
515→ issueDate DateTime @default(now())
|
||||
516→ acceptedAt DateTime?
|
||||
517→ acceptedBy String?
|
||||
518→ notes String?
|
||||
519→ netCents Int @default(0)
|
||||
520→ taxBp Int @default(1900)
|
||||
521→ grossCents Int @default(0)
|
||||
522→ // AI
|
||||
523→ aiDraftFromBrief String? // flag: was this drafted from a brief via AI
|
||||
524→ createdAt DateTime @default(now())
|
||||
525→ updatedAt DateTime @updatedAt
|
||||
526→ lines EstimateLine[]
|
||||
527→}
|
||||
528→
|
||||
529→model EstimateLine {
|
||||
530→ id String @id @default(cuid())
|
||||
531→ estimateId String
|
||||
532→ estimate Estimate @relation(fields: [estimateId], references: [id])
|
||||
533→ description String
|
||||
534→ quantity Int @default(1)
|
||||
535→ unitCents Int
|
||||
536→ totalCents Int
|
||||
537→}
|
||||
538→
|
||||
539→// ---------------------------------------------------------------------------
|
||||
540→// Accounting / Buchhaltung (adapted from Haben, German-law compliant)
|
||||
541→// VAT return (Umsatzsteuer-Voranmeldung), EÜR, bank reconciliation, dunning.
|
||||
542→// PRD §6.6: double-entry + ELSTER out of scope; this covers the agency-facing
|
||||
543→// compliance layer (compute, validate, mock-submit) + bank matching.
|
||||
544→// ---------------------------------------------------------------------------
|
||||
545→
|
||||
546→// Bank transactions imported from CAMT.053 or PSD2 (FinTS).
|
||||
547→model BankTransaction {
|
||||
548→ id String @id @default(cuid())
|
||||
549→ amountCents Int // signed: + credit (incoming), - debit (outgoing)
|
||||
550→ bookingDate DateTime
|
||||
551→ valueDate DateTime
|
||||
552→ partnerName String?
|
||||
553→ partnerIban String?
|
||||
554→ reference String?
|
||||
555→ purpose String?
|
||||
556→ matchedInvoiceId String?
|
||||
557→ matchedStatus String @default("unmatched")
|
||||
558→ matchReason String?
|
||||
559→ importBatchId String?
|
||||
560→ importedAt DateTime @default(now())
|
||||
561→ createdAt DateTime @default(now())
|
||||
562→}
|
||||
563→
|
||||
564→// VAT pre-registration periods (Umsatzsteuer-Voranmeldung)
|
||||
565→model VatReturn {
|
||||
566→ id String @id @default(cuid())
|
||||
567→ periodYear Int
|
||||
568→ periodMonth Int
|
||||
569→ periodType String @default("monthly")
|
||||
570→ status String @default("draft")
|
||||
571→ k81StandardNetCents Int @default(0)
|
||||
572→ k86ReducedNetCents Int @default(0)
|
||||
573→ k66TaxFreeCents Int @default(0)
|
||||
574→ k83TaxStandardCents Int @default(0)
|
||||
575→ k65TaxReducedCents Int @default(0)
|
||||
576→ k37InputTaxCents Int @default(0)
|
||||
577→ outputTaxCents Int @default(0)
|
||||
578→ inputTaxCents Int @default(0)
|
||||
579→ payableCents Int @default(0)
|
||||
580→ submittedAt DateTime?
|
||||
581→ elsterRef String?
|
||||
582→ acceptedAt DateTime?
|
||||
583→ notes String?
|
||||
584→ createdAt DateTime @default(now())
|
||||
585→ updatedAt DateTime @updatedAt
|
||||
586→}
|
||||
587→
|
||||
588→// Dunning levels for overdue invoices (Mahnwesen)
|
||||
589→model DunningRun {
|
||||
590→ id String @id @default(cuid())
|
||||
591→ invoiceId String
|
||||
592→ invoice Invoice @relation(fields: [invoiceId], references: [id])
|
||||
593→ level Int
|
||||
594→ status String @default("sent")
|
||||
595→ feesCents Int @default(0)
|
||||
596→ sentAt DateTime @default(now())
|
||||
597→ createdAt DateTime @default(now())
|
||||
598→}
|
||||
599→
|
||||
600→// ---------------------------------------------------------------------------
|
||||
601→// Public API (PRD §5.5) — API tokens with scopes, webhooks with HMAC signing
|
||||
602→// ---------------------------------------------------------------------------
|
||||
603→
|
||||
604→model ApiToken {
|
||||
605→ id String @id @default(cuid())
|
||||
606→ name String
|
||||
607→ // The token shown to the user ONCE. We store only its SHA-256 hash so a
|
||||
608→ // compromised DB does not leak usable tokens (like GitHub PATs).
|
||||
609→ tokenHash String @unique
|
||||
610→ tokenPrefix String // first 8 chars for identification (e.g. "aos_abc1…")
|
||||
611→ scopes String // JSON array of scope strings, e.g. ["crm.company:read","finance.invoice:read"]
|
||||
612→ lastUsedAt DateTime?
|
||||
613→ expiresAt DateTime?
|
||||
614→ revokedAt DateTime?
|
||||
615→ createdAt DateTime @default(now())
|
||||
616→}
|
||||
617→
|
||||
618→model Webhook {
|
||||
619→ id String @id @default(cuid())
|
||||
620→ url String // https endpoint to deliver to
|
||||
621→ events String // JSON array, e.g. ["invoice.paid","contract.signed"]
|
||||
622→ secret String // HMAC signing secret (stored, needed to verify signatures)
|
||||
623→ active Boolean @default(true)
|
||||
624→ // delivery stats
|
||||
625→ sentCount Int @default(0)
|
||||
626→ failCount Int @default(0)
|
||||
627→ lastSentAt DateTime?
|
||||
628→ lastStatus Int? // last HTTP status code received
|
||||
629→ createdAt DateTime @default(now())
|
||||
630→ updatedAt DateTime @updatedAt
|
||||
631→}
|
||||
632→
|
||||
633→// ---------------------------------------------------------------------------
|
||||
634→// Client Portal (PRD §6.3) — milestone approvals with evidence
|
||||
635→// ---------------------------------------------------------------------------
|
||||
636→
|
||||
637→model PortalApproval {
|
||||
638→ id String @id @default(cuid())
|
||||
639→ projectId String
|
||||
640→ project Project @relation(fields: [projectId], references: [id])
|
||||
641→ title String // e.g. "Design-Entwürfe freigeben"
|
||||
642→ description String?
|
||||
643→ status String @default("pending") // pending|approved|rejected|changes-requested
|
||||
644→ // evidence (PRD §6.3: "approvals with timestamp and IP evidence")
|
||||
645→ decidedAt DateTime?
|
||||
646→ decidedBy String? // client name
|
||||
647→ decidedIp String? // real client IP at decision time
|
||||
648→ decision String? // free-text reason/feedback
|
||||
649→ // content hash for tamper-evidence
|
||||
650→ contentHash String?
|
||||
651→ createdAt DateTime @default(now())
|
||||
652→ updatedAt DateTime @updatedAt
|
||||
653→}
|
||||
654→
|
||||
655→// ---------------------------------------------------------------------------
|
||||
656→// Knowledge Base (PRD §6.7) — internal + client-facing docs with versioning
|
||||
657→// ---------------------------------------------------------------------------
|
||||
658→
|
||||
659→model Article {
|
||||
660→ id String @id @default(cuid())
|
||||
661→ slug String @unique
|
||||
662→ title String
|
||||
663→ category String // onboarding|launch-checklist|templates|handbuch|faq
|
||||
664→ visibility String @default("internal") // internal|client|public
|
||||
665→ excerpt String?
|
||||
666→ content String // markdown
|
||||
667→ tags String? // comma separated
|
||||
668→ version Int @default(1)
|
||||
669→ authorId String?
|
||||
670→ authorName String?
|
||||
671→ published Boolean @default(true)
|
||||
672→ viewCount Int @default(0)
|
||||
673→ createdAt DateTime @default(now())
|
||||
674→ updatedAt DateTime @updatedAt
|
||||
675→}
|
||||
676→
|
||||
677→// ---------------------------------------------------------------------------
|
||||
678→// Haben-cloned features: Belege, Pauschalen, Anlagen/AfA, Kasse
|
||||
679→// ---------------------------------------------------------------------------
|
||||
680→
|
||||
681→// Belege (receipts/documents) — content-addressed storage (PRD §5.5)
|
||||
682→model Beleg {
|
||||
683→ id String @id @default(cuid())
|
||||
684→ number String @unique // gapless, e.g. BEL-2026-0001
|
||||
685→ type String // rechnung-eingang|rechnung-ausgang|quittung|kassenbeleg|bankbeleg|sonstiges
|
||||
686→ // content-addressed (SHA-256 hash of the file content)
|
||||
687→ fileHash String @unique
|
||||
688→ fileName String
|
||||
689→ fileType String // application/pdf, image/jpeg, etc.
|
||||
690→ fileSizeBytes Int
|
||||
691→ // metadata extracted from the document
|
||||
692→ vendor String? // Rechnungssteller
|
||||
693→ invoiceNumber String? // Rechnungsnummer des Lieferanten
|
||||
694→ amountCents Int? // Bruttobetrag auf dem Beleg
|
||||
695→ amountNetCents Int? // Netto (falls extrahiert)
|
||||
696→ taxBp Int? // USt %
|
||||
697→ bookingDate DateTime?
|
||||
698→ // linking
|
||||
699→ expenseId String? // link to Expense if billable
|
||||
700→ invoiceId String? // link to Invoice if outgoing
|
||||
701→ // OCR / AI
|
||||
702→ ocrText String? // extracted text (optional, via Jina/AI)
|
||||
703→ // GoBD
|
||||
704→ archivedAt DateTime?
|
||||
705→ retentionUntil DateTime? // Aufbewahrung bis
|
||||
706→ createdAt DateTime @default(now())
|
||||
707→}
|
||||
708→
|
||||
709→// Pauschalen (flat-rate allowances) — Homeoffice, Kilometer, Verpflegung
|
||||
710→model Pauschale {
|
||||
711→ id String @id @default(cuid())
|
||||
712→ type String // homeoffice|kilometer|verpflegung
|
||||
713→ // homeoffice: €4.17/day (2026: €6.92/day up to €1.260/year)
|
||||
714→ // kilometer: €0.30/km (car), €0.38/km (car w/ pass), or actual
|
||||
715→ // verpflegung: €14 (8-14h), €24 (>14h, same day), €28 (overnight)
|
||||
716→ date DateTime
|
||||
717→ userId String? // employee
|
||||
718→ amountCents Int // computed amount
|
||||
719→ // details per type
|
||||
720→ days Int? // homeoffice: 1; verpflegung: 1
|
||||
721→ km Int? // kilometer: distance
|
||||
722→ vehicleType String? // kilometer: auto|rad|fuss
|
||||
723→ tripPurpose String? // kilometer: geschäftlich|fortbildung|umzug
|
||||
724→ hoursAway Int? // verpflegung: hours away from office
|
||||
725→ overnight Boolean @default(false) // verpflegung: overnight stay
|
||||
726→ description String?
|
||||
727→ projectId String? // link to project if billable
|
||||
728→ expenseId String? // link to generated expense
|
||||
729→ createdAt DateTime @default(now())
|
||||
730→}
|
||||
731→
|
||||
732→// Anlagenverzeichnis & AfA (asset register + depreciation)
|
||||
733→model Anlage {
|
||||
734→ id String @id @default(cuid())
|
||||
735→ number String @unique // e.g. ANL-2026-0001
|
||||
736→ name String // "MacBook Pro 16"
|
||||
737→ category String // hardware|software|mobiliar|fahrzeug|immobilie
|
||||
738→ // acquisition
|
||||
739→ purchaseDate DateTime
|
||||
740→ purchaseCostCents Int // Anschaffungskosten (netto)
|
||||
741→ taxBp Int @default(1900) // USt bei Anschaffung
|
||||
742→ // AfA (depreciation)
|
||||
743→ afaMethod String @default("linear") // linear|degressiv
|
||||
744→ usefulLifeYears Int @default(3) // Nutzungsdauer (AfA-Tabelle)
|
||||
745→ salvageValueCents Int @default(0) // Restwert
|
||||
746→ // GWG (Geringwertiger Wirtschaftsgüter ≤ €800 net → Sofortabschreibung)
|
||||
747→ isGWG Boolean @default(false) // GWG ≤ €800 → immediate expense
|
||||
748→ // Sammelposten (€251-€1000 → Sammelabschreibung über 5 Jahre)
|
||||
749→ isSammelposten Boolean @default(false)
|
||||
750→ // computed
|
||||
751→ annualAfaCents Int @default(0) // jährliche AfA
|
||||
752→ accumulatedAfaCents Int @default(0) // kumulierte AfA
|
||||
753→ bookValueCents Int @default(0) // Buchwert (purchase - accumulated AfA)
|
||||
754→ // status
|
||||
755→ status String @default("active") // active|sold|scrapped|fully-depreciated
|
||||
756→ soldDate DateTime?
|
||||
757→ soldPriceCents Int?
|
||||
758→ createdAt DateTime @default(now())
|
||||
759→ updatedAt DateTime @updatedAt
|
||||
760→}
|
||||
761→
|
||||
762→// Kassenbuch (cash book for Barzahlungen — § 146 AO, § 22 UStG)
|
||||
763→model KassenbuchEntry {
|
||||
764→ id String @id @default(cuid())
|
||||
765→ number String @unique // gapless, e.g. KAS-2026-0001
|
||||
766→ date DateTime
|
||||
767→ type String // einnahme|ausgabe
|
||||
768→ description String
|
||||
769→ amountCents Int // signed: + for einnahme, - for ausgabe
|
||||
770→ taxBp Int? // USt % (if applicable)
|
||||
771→ taxFree Boolean @default(false) // steuerfrei
|
||||
772→ // receipt link
|
||||
773→ belegId String? // link to Beleg (Kassenbeleg)
|
||||
774→ // balance after this entry (laufender Kassenstand)
|
||||
775→ balanceCents Int @default(0)
|
||||
776→ // GoBD: Kassenbuch ist unveränderlich (elektronische Aufzeichnung)
|
||||
777→ createdAt DateTime @default(now())
|
||||
778→}
|
||||
779→
|
||||
780→
|
||||
781→// ---------------------------------------------------------------------------
|
||||
782→// SKR03/SKR04 Kontenrahmen + doppelte Buchführung (Haben clone)
|
||||
783→// ---------------------------------------------------------------------------
|
||||
784→
|
||||
785→model Konto {
|
||||
786→ id String @id @default(cuid())
|
||||
787→ nummer String @unique
|
||||
788→ name String
|
||||
789→ rahmen String @default("skr04")
|
||||
790→ art String // aktiv|passiv|ertrag|aufwand
|
||||
791→ parentId String?
|
||||
792→ locked Boolean @default(false)
|
||||
793→ sollBuchungen Buchungssatz[] @relation("SollKonto")
|
||||
794→ habenBuchungen Buchungssatz[] @relation("HabenKonto")
|
||||
795→ createdAt DateTime @default(now())
|
||||
796→}
|
||||
797→
|
||||
798→model Buchungssatz {
|
||||
799→ id String @id @default(cuid())
|
||||
800→ number String @unique
|
||||
801→ belegdatum DateTime
|
||||
802→ buchungsdatum DateTime @default(now())
|
||||
803→ sollKontoId String
|
||||
804→ sollKonto Konto @relation("SollKonto", fields: [sollKontoId], references: [id])
|
||||
805→ habenKontoId String
|
||||
806→ habenKonto Konto @relation("HabenKonto", fields: [habenKontoId], references: [id])
|
||||
807→ betragCents Int
|
||||
808→ buchungstext String
|
||||
809→ belegNummer String?
|
||||
810→ steuerBp Int?
|
||||
811→ steuerKontoId String?
|
||||
812→ festgeschrieben Boolean @default(false)
|
||||
813→ festgeschriebenAt DateTime?
|
||||
814→ festgeschriebenHash String?
|
||||
815→ belegId String?
|
||||
816→ invoiceId String?
|
||||
817→ expenseId String?
|
||||
818→ reverseCharge Boolean @default(false)
|
||||
819→ par19UStG Boolean @default(false)
|
||||
820→ createdAt DateTime @default(now())
|
||||
821→}
|
||||
822→
|
||||
823→model EuerEntry {
|
||||
824→ id String @id @default(cuid())
|
||||
825→ jahr Int
|
||||
826→ zeile String
|
||||
827→ label String
|
||||
828→ art String
|
||||
829→ betragCents Int
|
||||
830→ kontoNummern String?
|
||||
831→ createdAt DateTime @default(now())
|
||||
832→}
|
||||
833→
|
||||
834→model Jahreserklaerung {
|
||||
835→ id String @id @default(cuid())
|
||||
836→ jahr Int
|
||||
837→ art String
|
||||
838→ status String @default("draft")
|
||||
839→ kennzahlen String?
|
||||
840→ elsterRef String?
|
||||
841→ submittedAt DateTime?
|
||||
842→ acceptedAt DateTime?
|
||||
843→ notes String?
|
||||
844→ createdAt DateTime @default(now())
|
||||
845→ updatedAt DateTime @updatedAt
|
||||
846→}
|
||||
847→
|
||||
848→model Jahresarchiv {
|
||||
849→ id String @id @default(cuid())
|
||||
850→ jahr Int
|
||||
851→ prevArchivId String?
|
||||
852→ archivHash String @unique
|
||||
853→ buchungenCount Int
|
||||
854→ belegeCount Int
|
||||
855→ rechnungenCount Int
|
||||
856→ zipFileName String
|
||||
857→ zipSizeBytes Int
|
||||
858→ createdDate DateTime @default(now())
|
||||
859→ createdBy String
|
||||
860→ notes String?
|
||||
861→}
|
||||
862→
|
||||
863→// ---------------------------------------------------------------------------
|
||||
864→// Custom Fields (PRD §5.5: "Custom fields: JSONB with a schema table per entity")
|
||||
865→// ---------------------------------------------------------------------------
|
||||
866→
|
||||
867→model CustomFieldDef {
|
||||
868→ id String @id @default(cuid())
|
||||
869→ entityType String // company|person|deal|project|invoice|ticket|contract
|
||||
870→ key String // field key, e.g. "branche", "lead_source_detail"
|
||||
871→ label String // display label, e.g. "Branche"
|
||||
872→ type String // string|number|boolean|date|select|multiselect
|
||||
873→ options String? // JSON array for select/multiselect
|
||||
874→ required Boolean @default(false)
|
||||
875→ sortOrder Int @default(0)
|
||||
876→ values CustomFieldValue[]
|
||||
877→ createdAt DateTime @default(now())
|
||||
878→ updatedAt DateTime @updatedAt
|
||||
879→
|
||||
880→ @@unique([entityType, key])
|
||||
881→}
|
||||
882→
|
||||
883→model CustomFieldValue {
|
||||
884→ id String @id @default(cuid())
|
||||
885→ fieldDefId String
|
||||
886→ fieldDef CustomFieldDef @relation(fields: [fieldDefId], references: [id])
|
||||
887→ entityType String // denormalized for fast lookup
|
||||
888→ entityId String // the entity's ID
|
||||
889→ value String? // JSON-encoded value (string, number, bool, array)
|
||||
890→ createdAt DateTime @default(now())
|
||||
891→ updatedAt DateTime @updatedAt
|
||||
892→
|
||||
893→ @@unique([entityType, entityId, fieldDefId])
|
||||
894→}
|
||||
895→
|
||||
Reference in new issue
Block a user