Initial commit: everythingbutwork MVP
Internes Project-Management-Tool für ToolCTRL — ergänzend zu Plane (Tickets) und Outline (Doku). Umfasst Puls-Übersicht, Tagesrhythmus (Lock-in + Reflexion), Bereichs-Detail-Views (Entwicklung/Vertrieb/Marketing), Pflicht-Minimum-Check und Blocker-Lifecycle gemäß Projektmanagement-Leitfaden v3.0. Stack: pnpm-Monorepo · Vite + React 18 + Tailwind · Fastify + tRPC v11 + Drizzle ORM · Postgres 16 · Authentik OIDC (Phase 2, Dev-Auto-Login aktiv). Planungs-Artefakte unter _bmad-output/planning-artifacts/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
4f33509a0f
131 files changed
+34339
No files matched your search
@@ -0,0 +1,67 @@
|
||||
import { desc, eq } from 'drizzle-orm';
|
||||
import { campaign, leadHandover, deal, area } from '@cockpit/db';
|
||||
import { CampaignSchemas } from '@cockpit/shared';
|
||||
import { router, protectedProcedure } from '../trpc.js';
|
||||
|
||||
const MARKETING_AREA_SLUG = 'marketing';
|
||||
|
||||
export const campaignRouter = router({
|
||||
list: protectedProcedure.query(async ({ ctx }) => {
|
||||
return ctx.db.select().from(campaign).orderBy(desc(campaign.createdAt));
|
||||
}),
|
||||
|
||||
create: protectedProcedure
|
||||
.input(CampaignSchemas.createCampaignInput)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const a = (
|
||||
await ctx.db.select().from(area).where(eq(area.slug, MARKETING_AREA_SLUG)).limit(1)
|
||||
)[0];
|
||||
if (!a) throw new Error('Marketing-Area fehlt — bitte seed laufen lassen.');
|
||||
const [created] = await ctx.db
|
||||
.insert(campaign)
|
||||
.values({
|
||||
areaId: a.id,
|
||||
name: input.name,
|
||||
status: input.status,
|
||||
reach: input.reach,
|
||||
ownerUserId: input.ownerUserId ?? null,
|
||||
})
|
||||
.returning();
|
||||
return created;
|
||||
}),
|
||||
|
||||
funnel: protectedProcedure.query(async ({ ctx }) => {
|
||||
// Lead-Funnel: simple Aggregation der bekannten Quellen.
|
||||
const campaigns = await ctx.db.select().from(campaign);
|
||||
const reichweite = campaigns.reduce((acc, c) => acc + c.reach, 0);
|
||||
const handovers = await ctx.db.select().from(leadHandover);
|
||||
const leads = handovers.length;
|
||||
const deals = await ctx.db.select().from(deal);
|
||||
const mql = deals.filter((d) => ['erstgespraech', 'demo', 'angebot'].includes(d.stage)).length;
|
||||
const uebergabe = handovers.length;
|
||||
// Frontend kann die Stage-Übergänge daraus rendern.
|
||||
return {
|
||||
reichweite,
|
||||
website: Math.round(reichweite * 0.2), // Placeholder bis ein Tracking-Feed existiert.
|
||||
leads,
|
||||
mql,
|
||||
uebergabe,
|
||||
};
|
||||
}),
|
||||
|
||||
crosslink: protectedProcedure.query(async ({ ctx }) => {
|
||||
const handovers = await ctx.db.select().from(leadHandover);
|
||||
const dealsFromHandovers = handovers.filter((h) => h.dealId !== null).length;
|
||||
// Won-Volumen aus Handover-bezogenen Deals dieses Quartals.
|
||||
const dealRows = await ctx.db.select().from(deal);
|
||||
const wonValue = dealRows
|
||||
.filter((d) => d.wonAt)
|
||||
.reduce((acc, d) => acc + (d.wonValueEur ?? 0), 0);
|
||||
return {
|
||||
uebergebenSeitTagen: 30,
|
||||
leadsUebergeben: handovers.length,
|
||||
dealsErzeugt: dealsFromHandovers,
|
||||
gewonneneEurQuartal: wonValue,
|
||||
};
|
||||
}),
|
||||
});
|
||||
Reference in new issue
Block a user