b6167bcb-7363-4695-9481-addd81de4367
This commit is contained in:
1 parent
fdc3b82d10
commit
d226dabba3
136 files changed
+13904
-2
No files matched your search
@@ -0,0 +1,10 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/audit — audit log (hash-chain evidence)
|
||||
export async function GET() {
|
||||
const entries = await db.auditLog.findMany({ orderBy: { createdAt: 'desc' }, take: 50 })
|
||||
return NextResponse.json({ entries })
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/crm — companies, persons, deals grouped by stage
|
||||
export async function GET() {
|
||||
const [companies, deals, persons] = await Promise.all([
|
||||
db.company.findMany({ orderBy: { createdAt: 'desc' }, include: { persons: true, deals: true } }),
|
||||
db.deal.findMany({ include: { company: true }, orderBy: { updatedAt: 'desc' } }),
|
||||
db.person.findMany({ include: { company: true } }),
|
||||
])
|
||||
|
||||
const stages = ['lead', 'qualified', 'proposal', 'negotiation', 'won', 'lost']
|
||||
const stageLabels: Record<string, string> = {
|
||||
lead: 'Lead', qualified: 'Qualifiziert', proposal: 'Angebot', negotiation: 'Verhandlung', won: 'Gewonnen', lost: 'Verloren',
|
||||
}
|
||||
|
||||
const pipeline = stages.map((s) => ({
|
||||
stage: s,
|
||||
label: stageLabels[s],
|
||||
deals: deals.filter((d) => d.stage === s),
|
||||
}))
|
||||
|
||||
return NextResponse.json({ companies, deals, persons, pipeline })
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/dashboard — agency KPI overview
|
||||
export async function GET() {
|
||||
const [deals, leads, projects, licenses, assets, invoices, audit, modules, users] = await Promise.all([
|
||||
db.deal.findMany({ include: { company: true } }),
|
||||
db.lead.findMany(),
|
||||
db.project.findMany(),
|
||||
db.license.findMany({ include: { activations: true, company: true } }),
|
||||
db.asset.findMany(),
|
||||
db.invoice.findMany(),
|
||||
db.auditLog.findMany({ orderBy: { createdAt: 'desc' }, take: 8 }),
|
||||
db.module.findMany(),
|
||||
db.user.findMany(),
|
||||
])
|
||||
|
||||
const pipelineValue = deals
|
||||
.filter((d) => d.stage !== 'lost' && d.stage !== 'won')
|
||||
.reduce((s, d) => s + d.valueCents, 0)
|
||||
|
||||
const wonValue = deals
|
||||
.filter((d) => d.stage === 'won')
|
||||
.reduce((s, d) => s + d.valueCents, 0)
|
||||
|
||||
const pendingLeads = leads.filter((l) => l.status === 'pending').length
|
||||
const approvedLeads = leads.filter((l) => l.status === 'approved').length
|
||||
const rejectedLeads = leads.filter((l) => l.status === 'rejected').length
|
||||
const acceptRate = approvedLeads + rejectedLeads > 0
|
||||
? Math.round((approvedLeads / (approvedLeads + rejectedLeads)) * 100)
|
||||
: 0
|
||||
|
||||
const openProjects = projects.filter((p) => p.status === 'active' || p.status === 'planning' || p.status === 'review').length
|
||||
|
||||
const activeLicenses = licenses.filter((l) => l.status === 'active').length
|
||||
const expiringAssets = assets.filter((a) => a.status === 'expiring' || a.status === 'expired').length
|
||||
|
||||
const outstandingInvoices = invoices
|
||||
.filter((i) => i.status === 'sent' || i.status === 'overdue')
|
||||
.reduce((s, i) => s + i.grossCents, 0)
|
||||
|
||||
const paidThisYear = invoices
|
||||
.filter((i) => i.status === 'paid')
|
||||
.reduce((s, i) => s + i.grossCents, 0)
|
||||
|
||||
const stages = ['lead', 'qualified', 'proposal', 'negotiation', 'won', 'lost']
|
||||
const stageLabels: Record<string, string> = {
|
||||
lead: 'Lead', qualified: 'Qualifiziert', proposal: 'Angebot', negotiation: 'Verhandlung', won: 'Gewonnen', lost: 'Verloren',
|
||||
}
|
||||
const funnel = stages.map((s) => ({
|
||||
stage: s,
|
||||
label: stageLabels[s],
|
||||
count: deals.filter((d) => d.stage === s).length,
|
||||
valueCents: deals.filter((d) => d.stage === s).reduce((sum, d) => sum + d.valueCents, 0),
|
||||
}))
|
||||
|
||||
const now = new Date()
|
||||
const months: { label: string; cents: number }[] = []
|
||||
for (let i = 5; i >= 0; i--) {
|
||||
const m = new Date(now.getFullYear(), now.getMonth() - i, 1)
|
||||
const label = m.toLocaleDateString('de-DE', { month: 'short' })
|
||||
const cents = invoices
|
||||
.filter((inv) => {
|
||||
const d = inv.issueDate
|
||||
return d.getMonth() === m.getMonth() && d.getFullYear() === m.getFullYear() && inv.status === 'paid'
|
||||
})
|
||||
.reduce((s, i) => s + i.grossCents, 0)
|
||||
months.push({ label, cents })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
kpis: {
|
||||
pipelineValue,
|
||||
wonValue,
|
||||
pendingLeads,
|
||||
acceptRate,
|
||||
openProjects,
|
||||
activeLicenses,
|
||||
expiringAssets,
|
||||
outstandingInvoices,
|
||||
paidThisYear,
|
||||
auditEntries: audit.length,
|
||||
enabledModules: modules.filter((m) => m.enabled).length,
|
||||
totalModules: modules.length,
|
||||
teamMembers: users.filter((u) => u.role !== 'client').length,
|
||||
},
|
||||
funnel,
|
||||
revenue: months,
|
||||
audit,
|
||||
modules,
|
||||
users,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/finance — invoices with lines + company
|
||||
export async function GET() {
|
||||
const invoices = await db.invoice.findMany({
|
||||
orderBy: { issueDate: 'desc' },
|
||||
include: { company: true, lines: true },
|
||||
})
|
||||
|
||||
const total = invoices.filter((i) => i.status !== 'cancelled' && i.status !== 'storno').length
|
||||
const draft = invoices.filter((i) => i.status === 'draft').length
|
||||
const sent = invoices.filter((i) => i.status === 'sent').length
|
||||
const paid = invoices.filter((i) => i.status === 'paid').length
|
||||
const overdue = invoices.filter((i) => i.status === 'overdue').length
|
||||
const zugferdCount = invoices.filter((i) => i.zugferd).length
|
||||
const xrechnungCount = invoices.filter((i) => i.xrechnung).length
|
||||
const datevCount = invoices.filter((i) => i.datevExported).length
|
||||
const outstandingCents = invoices
|
||||
.filter((i) => i.status === 'sent' || i.status === 'overdue')
|
||||
.reduce((s, i) => s + i.grossCents, 0)
|
||||
|
||||
return NextResponse.json({
|
||||
invoices,
|
||||
stats: { total, draft, sent, paid, overdue, zugferdCount, xrechnungCount, datevCount, outstandingCents },
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// PATCH /api/aos/leads/[id] — human review: approve / reject / snooze / convert
|
||||
export async function PATCH(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
const body = await req.json()
|
||||
const { status, reviewReason } = body as { status: string; reviewReason?: string }
|
||||
|
||||
if (!['pending', 'approved', 'rejected', 'snoozed', 'converted'].includes(status)) {
|
||||
return NextResponse.json({ error: 'invalid status' }, { status: 400 })
|
||||
}
|
||||
|
||||
const lead = await db.lead.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status,
|
||||
reviewReason: reviewReason ?? null,
|
||||
reviewedAt: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
// write an audit entry
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: 'sven@nordlicht.digital',
|
||||
action: 'leads.review:' + (status === 'approved' ? 'approve' : status === 'rejected' ? 'reject' : 'update'),
|
||||
entity: 'Lead',
|
||||
entityId: id,
|
||||
after: JSON.stringify({ status, reviewReason }),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ lead })
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import ZAI from 'z-ai-web-dev-sdk'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const maxDuration = 60
|
||||
|
||||
// POST /api/aos/leads/explain
|
||||
// Generates an evidence-grounded "why this lead" draft via LLM.
|
||||
// Every claim in the output must reference an evidence id.
|
||||
export async function POST(req: Request) {
|
||||
const { leadId } = (await req.json()) as { leadId: string }
|
||||
|
||||
const lead = await db.lead.findUnique({ where: { id: leadId } })
|
||||
if (!lead) return NextResponse.json({ error: 'lead not found' }, { status: 404 })
|
||||
|
||||
const evidence = lead.evidence ? JSON.parse(lead.evidence) as { id: string; check: string; status: string; detail: string }[] : []
|
||||
|
||||
const evidenceBlock = evidence
|
||||
.map((e) => `- [${e.id}] ${e.check} (${e.status}): ${e.detail}`)
|
||||
.join('\n')
|
||||
|
||||
const systemPrompt = `Du bist ein Business-Development-Assistent für eine deutsche Software-Agentur (Nordlicht Digital GmbH).
|
||||
Deine Aufgabe: Aus einem Website-Audit eine kurze, evidenzbasierte Begründung ("warum dieser Lead") und einen vorgeschlagenen Ansatzwinkel formulieren.
|
||||
|
||||
Regeln:
|
||||
- Schreibe auf Deutsch.
|
||||
- Jede Behauptung MUSS in Klammern auf die zugehörige Evidence-ID verweisen, z.B. "(e3)".
|
||||
- Erfinde nichts. Beziehe dich nur auf die gelisteten Audit-Ergebnisse.
|
||||
- Maximal 3 Sätze Begründung, danach ein kurzer "Winkel"-Vorschlag.
|
||||
- KEINE automatischen Handlungen andeuten. Der Mensch entscheidet über den Kanal.`
|
||||
|
||||
const userPrompt = `Unternehmen: ${lead.companyName}
|
||||
Website: ${lead.website}
|
||||
Branche: ${lead.branch}
|
||||
Stadt: ${lead.city}
|
||||
Score: ${lead.score}/100 (Label: ${lead.scoreLabel})
|
||||
|
||||
Audit-Ergebnisse:
|
||||
${evidenceBlock}
|
||||
|
||||
Formuliere Begründung und Winkel.`
|
||||
|
||||
try {
|
||||
const zai = await ZAI.create()
|
||||
const res = await zai.chat.completions.create({
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
temperature: 0.4,
|
||||
max_tokens: 400,
|
||||
})
|
||||
|
||||
const explanation = res.choices?.[0]?.message?.content ?? ''
|
||||
|
||||
// log the AI action (prompt hash + actor) per AI_RULES
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: 'system:llm',
|
||||
action: 'ai.leads:explain',
|
||||
entity: 'Lead',
|
||||
entityId: leadId,
|
||||
after: JSON.stringify({ model: 'glm', promptHash: hash(userPrompt), length: explanation.length }),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
|
||||
// persist the (re)generated explanation
|
||||
const updated = await db.lead.update({
|
||||
where: { id: leadId },
|
||||
data: { explanation },
|
||||
})
|
||||
|
||||
return NextResponse.json({ explanation, lead: updated })
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'unknown error'
|
||||
return NextResponse.json({ error: msg }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
function hash(s: string) {
|
||||
let h = 0
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
h = (Math.imul(31, h) + s.charCodeAt(i)) | 0
|
||||
}
|
||||
return ('00000000' + (h >>> 0).toString(16)).slice(-8)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/leads — leads finder review queue
|
||||
export async function GET() {
|
||||
const leads = await db.lead.findMany({
|
||||
orderBy: [{ status: 'asc' }, { score: 'desc' }],
|
||||
})
|
||||
// parse evidence JSON for convenience
|
||||
const withEvidence = leads.map((l) => ({
|
||||
...l,
|
||||
evidence: l.evidence ? JSON.parse(l.evidence) : [],
|
||||
}))
|
||||
return NextResponse.json({ leads: withEvidence })
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/licenses — issued licenses + asset/renewal register
|
||||
export async function GET() {
|
||||
const [licenses, assets] = await Promise.all([
|
||||
db.license.findMany({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: { company: true, activations: true },
|
||||
}),
|
||||
db.asset.findMany({ orderBy: { renewalDate: 'asc' } }),
|
||||
])
|
||||
|
||||
const activeLicenses = licenses.filter((l) => l.status === 'active').length
|
||||
const suspended = licenses.filter((l) => l.status === 'suspended').length
|
||||
const expired = licenses.filter((l) => l.status === 'expired').length
|
||||
const totalActivations = licenses.reduce((s, l) => s + l.activations.length, 0)
|
||||
|
||||
const expiringSoon = assets.filter((a) => {
|
||||
const days = (a.renewalDate.getTime() - Date.now()) / 86400000
|
||||
return days <= 30 && a.status !== 'expired'
|
||||
})
|
||||
const expiredAssets = assets.filter((a) => a.status === 'expired')
|
||||
const monthlyRebill = assets
|
||||
.filter((a) => a.rebill)
|
||||
.reduce((s, a) => s + (a.costCentsPerMonth ?? 0), 0)
|
||||
|
||||
return NextResponse.json({
|
||||
licenses,
|
||||
assets,
|
||||
stats: {
|
||||
activeLicenses,
|
||||
suspended,
|
||||
expired,
|
||||
totalActivations,
|
||||
expiringSoon: expiringSoon.length,
|
||||
expiredAssets: expiredAssets.length,
|
||||
monthlyRebill,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/modules — module registry
|
||||
export async function GET() {
|
||||
const modules = await db.module.findMany({ orderBy: { id: 'asc' } })
|
||||
const settings = await db.setting.findMany()
|
||||
return NextResponse.json({
|
||||
modules,
|
||||
settings: Object.fromEntries(settings.map((s) => [s.key, s.value])),
|
||||
})
|
||||
}
|
||||
|
||||
// PATCH /api/aos/modules — toggle a module on/off
|
||||
export async function PATCH(req: Request) {
|
||||
const { id, enabled } = (await req.json()) as { id: string; enabled: boolean }
|
||||
const mod = await db.module.update({ where: { id }, data: { enabled } })
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: 'max@nordlicht.digital',
|
||||
action: 'module.' + (enabled ? 'enable' : 'disable'),
|
||||
entity: 'Module',
|
||||
entityId: id,
|
||||
after: JSON.stringify({ enabled }),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
return NextResponse.json({ module: mod })
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/projects — projects with tasks, time entries, company
|
||||
export async function GET() {
|
||||
const projects = await db.project.findMany({
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
include: {
|
||||
company: true,
|
||||
tasks: { orderBy: { createdAt: 'asc' } },
|
||||
timeEntries: { orderBy: { date: 'desc' } },
|
||||
},
|
||||
})
|
||||
|
||||
// weekly time totals (last 7 days)
|
||||
const since = new Date(Date.now() - 7 * 86400000)
|
||||
const recentTime = await db.timeEntry.findMany({
|
||||
where: { date: { gte: since } },
|
||||
})
|
||||
const minutesThisWeek = recentTime.reduce((s, t) => s + t.minutes, 0)
|
||||
|
||||
return NextResponse.json({ projects, minutesThisWeek })
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const maxDuration = 60
|
||||
|
||||
// POST /api/aos/seed — reseed the database (destructive, dev only)
|
||||
export async function POST() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return NextResponse.json({ error: 'not in production' }, { status: 403 })
|
||||
}
|
||||
try {
|
||||
// wipe via prisma (SQLite TRUNCATE not supported, use deleteMany per model in order)
|
||||
const { db } = await import('@/lib/db')
|
||||
await db.invoiceLine.deleteMany()
|
||||
await db.invoice.deleteMany()
|
||||
await db.asset.deleteMany()
|
||||
await db.licenseActivation.deleteMany()
|
||||
await db.license.deleteMany()
|
||||
await db.timeEntry.deleteMany()
|
||||
await db.task.deleteMany()
|
||||
await db.project.deleteMany()
|
||||
await db.lead.deleteMany()
|
||||
await db.deal.deleteMany()
|
||||
await db.person.deleteMany()
|
||||
await db.company.deleteMany()
|
||||
await db.auditLog.deleteMany()
|
||||
await db.setting.deleteMany()
|
||||
await db.module.deleteMany()
|
||||
await db.user.deleteMany()
|
||||
|
||||
await execAsync('bun run src/lib/seed.ts', { cwd: process.cwd() })
|
||||
return NextResponse.json({ ok: true })
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'seed failed'
|
||||
return NextResponse.json({ error: msg }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ message: "Hello, world!" });
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Toaster as Sonner } from "@/components/ui/sonner";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AOS — Agency Operating System",
|
||||
description:
|
||||
"Eine selbstgehostete Plattform, die eine Software-Agentur end-to-end betreibt: Leads, CRM, Projekte, Lizenzen, Finanzen — mit EU/DE-Compliance.",
|
||||
keywords: [
|
||||
"AOS",
|
||||
"Agency OS",
|
||||
"CRM",
|
||||
"Leads Finder",
|
||||
"Lizenzmanagement",
|
||||
"E-Rechnung",
|
||||
"DSGVO",
|
||||
"GoBD",
|
||||
],
|
||||
authors: [{ name: "Nordlicht Digital GmbH" }],
|
||||
icons: {
|
||||
icon: "https://z-cdn.chatglm.cn/z-ai/static/logo.svg",
|
||||
},
|
||||
openGraph: {
|
||||
title: "AOS — Agency Operating System",
|
||||
description:
|
||||
"Selbstgehostete Agentur-Plattform mit CRM, Leads Finder, Projekten, Lizenzen und Finanzen.",
|
||||
siteName: "AOS",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="de" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
|
||||
>
|
||||
<ThemeProvider attribute="class" defaultTheme="light" enableSystem={false}>
|
||||
{children}
|
||||
<Sonner position="bottom-right" richColors={false} closeButton />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { AppShell } from '@/components/aos/app-shell'
|
||||
|
||||
export default function Home() {
|
||||
return <AppShell />
|
||||
}
|
||||
Reference in new issue
Block a user