feat: Backend REST API Grundstruktur

🔐 Auth Routes:
- POST /register, /login, /refresh, /logout
- GET /me

👥 Contacts Routes:
- CRUD + /activities, /deals
- /import, /export (DSGVO Art. 20)

💰 Deals Routes:
- CRUD + /pipeline (Kanban View)
- /move, /won, /lost
- /forecast

📝 Activities Routes:
- CRUD + /upcoming
- /complete

📊 Pipelines Routes:
- CRUD + /stages

 Features:
- CORS Middleware
- Error Handler
- Request Logger
- API Documentation Endpoint
This commit is contained in:
2026-02-11 10:05:51 +00:00
parent 4b7297c199
commit cc74d66fad
6 changed files with 1010 additions and 19 deletions

View File

@@ -1,14 +1,28 @@
import { Application } from "@oak/oak"; import { Application } from "@oak/oak";
import "@std/dotenv/load"; import "@std/dotenv/load";
// Routes
import { authRouter } from "./routes/auth.ts";
import { contactsRouter } from "./routes/contacts.ts";
import { dealsRouter } from "./routes/deals.ts";
import { activitiesRouter } from "./routes/activities.ts";
import { pipelinesRouter } from "./routes/pipelines.ts";
const app = new Application(); const app = new Application();
const PORT = parseInt(Deno.env.get("PORT") || "8000"); const PORT = parseInt(Deno.env.get("PORT") || "8000");
// ============================================
// MIDDLEWARE
// ============================================
// CORS Middleware // CORS Middleware
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
ctx.response.headers.set("Access-Control-Allow-Origin", "*"); const origin = ctx.request.headers.get("origin") || "*";
ctx.response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); ctx.response.headers.set("Access-Control-Allow-Origin", origin);
ctx.response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH");
ctx.response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization"); ctx.response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
ctx.response.headers.set("Access-Control-Allow-Credentials", "true");
ctx.response.headers.set("Access-Control-Max-Age", "86400");
if (ctx.request.method === "OPTIONS") { if (ctx.request.method === "OPTIONS") {
ctx.response.status = 204; ctx.response.status = 204;
@@ -23,9 +37,34 @@ app.use(async (ctx, next) => {
const start = Date.now(); const start = Date.now();
await next(); await next();
const ms = Date.now() - start; const ms = Date.now() - start;
console.log(`${ctx.request.method} ${ctx.request.url.pathname} - ${ctx.response.status} (${ms}ms)`); const status = ctx.response.status;
const statusColor = status >= 500 ? "\x1b[31m" : status >= 400 ? "\x1b[33m" : "\x1b[32m";
console.log(`${statusColor}${status}\x1b[0m ${ctx.request.method} ${ctx.request.url.pathname} - ${ms}ms`);
}); });
// Error Handler
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
console.error("Error:", err);
ctx.response.status = 500;
ctx.response.body = {
success: false,
error: {
code: "INTERNAL_ERROR",
message: Deno.env.get("NODE_ENV") === "development"
? err.message
: "An internal error occurred",
},
};
}
});
// ============================================
// SYSTEM ROUTES
// ============================================
// Health Check // Health Check
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
if (ctx.request.url.pathname === "/health") { if (ctx.request.url.pathname === "/health") {
@@ -33,7 +72,7 @@ app.use(async (ctx, next) => {
status: "ok", status: "ok",
service: "pulse-crm-backend", service: "pulse-crm-backend",
version: "0.1.0", version: "0.1.0",
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}; };
return; return;
} }
@@ -46,36 +85,94 @@ app.use(async (ctx, next) => {
ctx.response.body = { ctx.response.body = {
name: "Pulse CRM API", name: "Pulse CRM API",
version: "1.0.0", version: "1.0.0",
docs: "/api/v1/docs", description: "Der Herzschlag deines Business",
endpoints: { endpoints: {
auth: "/api/v1/auth/*", auth: {
contacts: "/api/v1/contacts/*", "POST /api/v1/auth/register": "Register new user",
companies: "/api/v1/companies/*", "POST /api/v1/auth/login": "Login",
deals: "/api/v1/deals/*", "POST /api/v1/auth/refresh": "Refresh token",
pipelines: "/api/v1/pipelines/*", "POST /api/v1/auth/logout": "Logout",
activities: "/api/v1/activities/*", "GET /api/v1/auth/me": "Get current user",
users: "/api/v1/users/*" },
} contacts: {
"GET /api/v1/contacts": "List contacts",
"GET /api/v1/contacts/:id": "Get contact",
"POST /api/v1/contacts": "Create contact",
"PUT /api/v1/contacts/:id": "Update contact",
"DELETE /api/v1/contacts/:id": "Delete contact",
},
deals: {
"GET /api/v1/deals": "List deals",
"GET /api/v1/deals/pipeline": "Get pipeline view",
"GET /api/v1/deals/:id": "Get deal",
"POST /api/v1/deals": "Create deal",
"PUT /api/v1/deals/:id": "Update deal",
"POST /api/v1/deals/:id/move": "Move to stage",
"POST /api/v1/deals/:id/won": "Mark as won",
"POST /api/v1/deals/:id/lost": "Mark as lost",
},
activities: {
"GET /api/v1/activities": "List activities",
"GET /api/v1/activities/upcoming": "Upcoming tasks",
"POST /api/v1/activities": "Create activity",
"POST /api/v1/activities/:id/complete": "Complete task",
},
pipelines: {
"GET /api/v1/pipelines": "List pipelines",
"POST /api/v1/pipelines": "Create pipeline",
"PUT /api/v1/pipelines/:id/stages": "Update stages",
},
},
}; };
return; return;
} }
await next(); await next();
}); });
// 404 Handler // ============================================
// API ROUTES
// ============================================
app.use(authRouter.routes());
app.use(authRouter.allowedMethods());
app.use(contactsRouter.routes());
app.use(contactsRouter.allowedMethods());
app.use(dealsRouter.routes());
app.use(dealsRouter.allowedMethods());
app.use(activitiesRouter.routes());
app.use(activitiesRouter.allowedMethods());
app.use(pipelinesRouter.routes());
app.use(pipelinesRouter.allowedMethods());
// ============================================
// 404 HANDLER
// ============================================
app.use((ctx) => { app.use((ctx) => {
ctx.response.status = 404; ctx.response.status = 404;
ctx.response.body = { ctx.response.body = {
success: false, success: false,
error: { error: {
code: "NOT_FOUND", code: "NOT_FOUND",
message: "Endpoint not found" message: `Endpoint ${ctx.request.method} ${ctx.request.url.pathname} not found`,
} },
}; };
}); });
console.log(`🚀 Pulse CRM Backend starting on port ${PORT}...`); // ============================================
console.log(`📚 API Docs: http://localhost:${PORT}/api/v1`); // START SERVER
console.log(`❤️ Health: http://localhost:${PORT}/health`); // ============================================
console.log("");
console.log(" 🫀 Pulse CRM Backend");
console.log(" ====================");
console.log(` 📡 Server: http://localhost:${PORT}`);
console.log(` 📚 API: http://localhost:${PORT}/api/v1`);
console.log(` ❤️ Health: http://localhost:${PORT}/health`);
console.log("");
await app.listen({ port: PORT }); await app.listen({ port: PORT });

153
src/routes/activities.ts Normal file
View File

@@ -0,0 +1,153 @@
import { Router } from "@oak/oak";
const router = new Router({ prefix: "/api/v1/activities" });
// GET /api/v1/activities - List activities
router.get("/", async (ctx) => {
const query = ctx.request.url.searchParams;
const page = parseInt(query.get("page") || "1");
const limit = parseInt(query.get("limit") || "20");
const type = query.get("type"); // note, call, email, meeting, task
const contactId = query.get("contactId");
const dealId = query.get("dealId");
const userId = query.get("userId");
const completed = query.get("completed");
ctx.response.body = {
success: true,
data: [
{
id: "act-1",
type: "call",
subject: "Erstgespräch mit TechStart",
description: "Sehr interessiert an CRM Lösung",
contact: { id: "c-1", name: "Sarah Müller" },
deal: { id: "d-1", title: "TechStart CRM" },
user: { id: "u-1", name: "Max Mustermann" },
isCompleted: true,
completedAt: "2026-02-01T15:00:00Z",
createdAt: "2026-02-01T14:00:00Z",
},
{
id: "act-2",
type: "task",
subject: "Angebot erstellen",
dueDate: "2026-02-12T17:00:00Z",
contact: { id: "c-2", name: "Michael Fischer" },
deal: { id: "d-3", title: "ScaleUp Deal" },
user: { id: "u-1", name: "Max Mustermann" },
isCompleted: false,
createdAt: "2026-02-10T09:00:00Z",
},
],
meta: { page, limit, total: 25 },
};
});
// GET /api/v1/activities/upcoming - Upcoming tasks & meetings
router.get("/upcoming", async (ctx) => {
ctx.response.body = {
success: true,
data: {
today: [
{
id: "act-2",
type: "task",
subject: "Angebot erstellen",
dueDate: "2026-02-11T17:00:00Z",
},
],
thisWeek: [
{
id: "act-3",
type: "meeting",
subject: "Demo Präsentation",
dueDate: "2026-02-15T14:00:00Z",
},
],
overdue: [],
},
};
});
// GET /api/v1/activities/:id
router.get("/:id", async (ctx) => {
const { id } = ctx.params;
ctx.response.body = {
success: true,
data: {
id,
type: "call",
subject: "Erstgespräch",
description: "Details...",
durationMinutes: 30,
callOutcome: "successful",
contact: { id: "c-1", firstName: "Sarah", lastName: "Müller" },
deal: { id: "d-1", title: "TechStart CRM" },
user: { id: "u-1", firstName: "Max", lastName: "Mustermann" },
isCompleted: true,
completedAt: "2026-02-01T15:00:00Z",
createdAt: "2026-02-01T14:00:00Z",
},
};
});
// POST /api/v1/activities - Create activity
router.post("/", async (ctx) => {
const body = await ctx.request.body.json();
ctx.response.status = 201;
ctx.response.body = {
success: true,
message: "Activity created",
data: {
id: "new-act-uuid",
...body,
createdAt: new Date().toISOString(),
},
};
});
// PUT /api/v1/activities/:id - Update activity
router.put("/:id", async (ctx) => {
const { id } = ctx.params;
const body = await ctx.request.body.json();
ctx.response.body = {
success: true,
message: "Activity updated",
data: {
id,
...body,
updatedAt: new Date().toISOString(),
},
};
});
// POST /api/v1/activities/:id/complete - Mark as completed
router.post("/:id/complete", async (ctx) => {
const { id } = ctx.params;
ctx.response.body = {
success: true,
message: "Activity completed",
data: {
id,
isCompleted: true,
completedAt: new Date().toISOString(),
},
};
});
// DELETE /api/v1/activities/:id
router.delete("/:id", async (ctx) => {
const { id } = ctx.params;
ctx.response.body = {
success: true,
message: "Activity deleted",
};
});
export { router as activitiesRouter };

155
src/routes/auth.ts Normal file
View File

@@ -0,0 +1,155 @@
import { Router } from "@oak/oak";
const router = new Router({ prefix: "/api/v1/auth" });
// POST /api/v1/auth/register
router.post("/register", async (ctx) => {
const body = await ctx.request.body.json();
const { email, password, firstName, lastName, orgName } = body;
// TODO: Implement registration
// 1. Validate input (Zod)
// 2. Check if email exists
// 3. Hash password (Argon2)
// 4. Create organization
// 5. Create user
// 6. Send verification email
// 7. Return tokens
ctx.response.status = 201;
ctx.response.body = {
success: true,
message: "Registration successful",
data: {
user: {
id: "uuid",
email,
firstName,
lastName,
},
organization: {
id: "uuid",
name: orgName,
},
tokens: {
accessToken: "jwt_access_token",
refreshToken: "jwt_refresh_token",
},
},
};
});
// POST /api/v1/auth/login
router.post("/login", async (ctx) => {
const body = await ctx.request.body.json();
const { email, password } = body;
// TODO: Implement login
// 1. Find user by email
// 2. Verify password (Argon2)
// 3. Generate tokens
// 4. Log login (audit)
// 5. Return user + tokens
ctx.response.body = {
success: true,
message: "Login successful",
data: {
user: {
id: "uuid",
email,
firstName: "Max",
lastName: "Mustermann",
role: "admin",
orgId: "org_uuid",
},
tokens: {
accessToken: "jwt_access_token",
refreshToken: "jwt_refresh_token",
expiresIn: 900, // 15 minutes
},
},
};
});
// POST /api/v1/auth/refresh
router.post("/refresh", async (ctx) => {
const body = await ctx.request.body.json();
const { refreshToken } = body;
// TODO: Implement token refresh
// 1. Validate refresh token
// 2. Check if revoked
// 3. Generate new access token
// 4. Optionally rotate refresh token
ctx.response.body = {
success: true,
data: {
accessToken: "new_jwt_access_token",
expiresIn: 900,
},
};
});
// POST /api/v1/auth/logout
router.post("/logout", async (ctx) => {
const body = await ctx.request.body.json();
const { refreshToken } = body;
// TODO: Revoke refresh token
ctx.response.body = {
success: true,
message: "Logged out successfully",
};
});
// POST /api/v1/auth/forgot-password
router.post("/forgot-password", async (ctx) => {
const body = await ctx.request.body.json();
const { email } = body;
// TODO: Send password reset email
ctx.response.body = {
success: true,
message: "If the email exists, a reset link has been sent",
};
});
// POST /api/v1/auth/reset-password
router.post("/reset-password", async (ctx) => {
const body = await ctx.request.body.json();
const { token, password } = body;
// TODO: Reset password
ctx.response.body = {
success: true,
message: "Password reset successful",
};
});
// GET /api/v1/auth/me
router.get("/me", async (ctx) => {
// TODO: Get current user from JWT
ctx.response.body = {
success: true,
data: {
id: "uuid",
email: "user@example.com",
firstName: "Max",
lastName: "Mustermann",
role: "admin",
organization: {
id: "org_uuid",
name: "Demo Company",
plan: "pro",
},
},
};
});
export { router as authRouter };

214
src/routes/contacts.ts Normal file
View File

@@ -0,0 +1,214 @@
import { Router } from "@oak/oak";
const router = new Router({ prefix: "/api/v1/contacts" });
// GET /api/v1/contacts - List contacts
router.get("/", async (ctx) => {
const query = ctx.request.url.searchParams;
const page = parseInt(query.get("page") || "1");
const limit = parseInt(query.get("limit") || "20");
const search = query.get("search");
const status = query.get("status");
const companyId = query.get("companyId");
const ownerId = query.get("ownerId");
const tags = query.get("tags")?.split(",");
// TODO: Implement with database
// 1. Get org_id from JWT
// 2. Build query with filters
// 3. Paginate results
ctx.response.body = {
success: true,
data: [
{
id: "uuid-1",
firstName: "Sarah",
lastName: "Müller",
email: "sarah@example.com",
phone: "+49 30 123456",
company: { id: "comp-1", name: "TechStart GmbH" },
status: "active",
tags: ["VIP", "Entscheider"],
createdAt: "2026-01-15T10:00:00Z",
},
],
meta: {
page,
limit,
total: 150,
totalPages: 8,
},
};
});
// GET /api/v1/contacts/:id - Get single contact
router.get("/:id", async (ctx) => {
const { id } = ctx.params;
// TODO: Fetch from database
ctx.response.body = {
success: true,
data: {
id,
firstName: "Sarah",
lastName: "Müller",
email: "sarah@example.com",
phone: "+49 30 123456",
mobile: "+49 171 123456",
jobTitle: "CEO",
company: {
id: "comp-1",
name: "TechStart GmbH",
},
address: {
line1: "Hauptstraße 1",
city: "Berlin",
postalCode: "10115",
country: "Deutschland",
},
status: "active",
leadSource: "Website",
leadScore: 85,
tags: ["VIP", "Entscheider"],
customFields: {},
gdprConsent: true,
gdprConsentDate: "2026-01-01T00:00:00Z",
owner: {
id: "user-1",
name: "Max Mustermann",
},
createdAt: "2026-01-15T10:00:00Z",
updatedAt: "2026-02-01T14:30:00Z",
},
};
});
// POST /api/v1/contacts - Create contact
router.post("/", async (ctx) => {
const body = await ctx.request.body.json();
// TODO: Implement
// 1. Validate input (Zod)
// 2. Check for duplicates
// 3. Create contact
// 4. Log audit
ctx.response.status = 201;
ctx.response.body = {
success: true,
message: "Contact created",
data: {
id: "new-uuid",
...body,
createdAt: new Date().toISOString(),
},
};
});
// PUT /api/v1/contacts/:id - Update contact
router.put("/:id", async (ctx) => {
const { id } = ctx.params;
const body = await ctx.request.body.json();
// TODO: Implement
// 1. Validate input
// 2. Check ownership
// 3. Update contact
// 4. Log audit (before/after)
ctx.response.body = {
success: true,
message: "Contact updated",
data: {
id,
...body,
updatedAt: new Date().toISOString(),
},
};
});
// DELETE /api/v1/contacts/:id - Delete contact (soft delete)
router.delete("/:id", async (ctx) => {
const { id } = ctx.params;
// TODO: Implement soft delete
// 1. Check ownership
// 2. Set deleted_at
// 3. Log audit
ctx.response.body = {
success: true,
message: "Contact deleted",
};
});
// GET /api/v1/contacts/:id/activities - Get contact activities
router.get("/:id/activities", async (ctx) => {
const { id } = ctx.params;
ctx.response.body = {
success: true,
data: [
{
id: "act-1",
type: "call",
subject: "Erstgespräch",
description: "Sehr interessiert",
createdAt: "2026-02-01T14:00:00Z",
user: { id: "user-1", name: "Max Mustermann" },
},
],
};
});
// GET /api/v1/contacts/:id/deals - Get contact deals
router.get("/:id/deals", async (ctx) => {
const { id } = ctx.params;
ctx.response.body = {
success: true,
data: [
{
id: "deal-1",
title: "CRM Implementation",
value: 25000,
stage: "proposal",
status: "open",
},
],
};
});
// POST /api/v1/contacts/import - Import contacts (CSV)
router.post("/import", async (ctx) => {
// TODO: Handle CSV upload
ctx.response.body = {
success: true,
message: "Import started",
data: {
importId: "import-uuid",
status: "processing",
},
};
});
// GET /api/v1/contacts/export - Export contacts (DSGVO Art. 20)
router.get("/export", async (ctx) => {
const format = ctx.request.url.searchParams.get("format") || "json";
// TODO: Generate export
ctx.response.body = {
success: true,
message: "Export ready",
data: {
downloadUrl: "/api/v1/contacts/export/download/export-uuid",
expiresAt: new Date(Date.now() + 3600000).toISOString(),
},
};
});
export { router as contactsRouter };

263
src/routes/deals.ts Normal file
View File

@@ -0,0 +1,263 @@
import { Router } from "@oak/oak";
const router = new Router({ prefix: "/api/v1/deals" });
// GET /api/v1/deals - List deals
router.get("/", async (ctx) => {
const query = ctx.request.url.searchParams;
const page = parseInt(query.get("page") || "1");
const limit = parseInt(query.get("limit") || "20");
const pipelineId = query.get("pipelineId");
const stageId = query.get("stageId");
const status = query.get("status"); // open, won, lost
const ownerId = query.get("ownerId");
ctx.response.body = {
success: true,
data: [
{
id: "deal-1",
title: "TechStart CRM Implementation",
value: 25000,
currency: "EUR",
stage: { id: "proposal", name: "Angebot" },
status: "open",
probability: 50,
expectedCloseDate: "2026-03-15",
contact: { id: "contact-1", name: "Sarah Müller" },
company: { id: "comp-1", name: "TechStart GmbH" },
owner: { id: "user-1", name: "Max Mustermann" },
createdAt: "2026-01-20T10:00:00Z",
},
],
meta: { page, limit, total: 45, totalPages: 3 },
};
});
// GET /api/v1/deals/pipeline - Get deals grouped by stage (Kanban)
router.get("/pipeline", async (ctx) => {
const pipelineId = ctx.request.url.searchParams.get("pipelineId");
ctx.response.body = {
success: true,
data: {
pipeline: {
id: "pipeline-1",
name: "Sales Pipeline",
},
stages: [
{
id: "lead",
name: "Lead",
deals: [{ id: "deal-2", title: "New Lead", value: 10000 }],
totalValue: 10000,
count: 1,
},
{
id: "qualified",
name: "Qualifiziert",
deals: [{ id: "deal-3", title: "DataFlow", value: 15000 }],
totalValue: 15000,
count: 1,
},
{
id: "proposal",
name: "Angebot",
deals: [{ id: "deal-1", title: "TechStart", value: 25000 }],
totalValue: 25000,
count: 1,
},
{
id: "negotiation",
name: "Verhandlung",
deals: [{ id: "deal-4", title: "ScaleUp", value: 50000 }],
totalValue: 50000,
count: 1,
},
],
summary: {
totalValue: 100000,
weightedValue: 47500, // Based on probability
totalDeals: 4,
},
},
};
});
// GET /api/v1/deals/:id - Get single deal
router.get("/:id", async (ctx) => {
const { id } = ctx.params;
ctx.response.body = {
success: true,
data: {
id,
title: "TechStart CRM Implementation",
value: 25000,
currency: "EUR",
pipeline: { id: "pipeline-1", name: "Sales Pipeline" },
stage: { id: "proposal", name: "Angebot", probability: 50 },
status: "open",
expectedCloseDate: "2026-03-15",
contact: {
id: "contact-1",
firstName: "Sarah",
lastName: "Müller",
email: "sarah@techstart.de",
},
company: {
id: "comp-1",
name: "TechStart GmbH",
},
owner: {
id: "user-1",
firstName: "Max",
lastName: "Mustermann",
},
tags: ["Enterprise"],
customFields: {},
createdAt: "2026-01-20T10:00:00Z",
updatedAt: "2026-02-05T09:00:00Z",
},
};
});
// POST /api/v1/deals - Create deal
router.post("/", async (ctx) => {
const body = await ctx.request.body.json();
ctx.response.status = 201;
ctx.response.body = {
success: true,
message: "Deal created",
data: {
id: "new-deal-uuid",
...body,
createdAt: new Date().toISOString(),
},
};
});
// PUT /api/v1/deals/:id - Update deal
router.put("/:id", async (ctx) => {
const { id } = ctx.params;
const body = await ctx.request.body.json();
ctx.response.body = {
success: true,
message: "Deal updated",
data: {
id,
...body,
updatedAt: new Date().toISOString(),
},
};
});
// POST /api/v1/deals/:id/move - Move deal to different stage
router.post("/:id/move", async (ctx) => {
const { id } = ctx.params;
const body = await ctx.request.body.json();
const { stageId } = body;
// TODO: Implement stage move
// 1. Validate stage exists in pipeline
// 2. Update deal
// 3. Log activity
// 4. Trigger webhooks
ctx.response.body = {
success: true,
message: "Deal moved",
data: {
id,
stageId,
updatedAt: new Date().toISOString(),
},
};
});
// POST /api/v1/deals/:id/won - Mark deal as won
router.post("/:id/won", async (ctx) => {
const { id } = ctx.params;
ctx.response.body = {
success: true,
message: "Deal marked as won",
data: {
id,
status: "won",
actualCloseDate: new Date().toISOString(),
},
};
});
// POST /api/v1/deals/:id/lost - Mark deal as lost
router.post("/:id/lost", async (ctx) => {
const { id } = ctx.params;
const body = await ctx.request.body.json();
const { reason } = body;
ctx.response.body = {
success: true,
message: "Deal marked as lost",
data: {
id,
status: "lost",
lostReason: reason,
actualCloseDate: new Date().toISOString(),
},
};
});
// DELETE /api/v1/deals/:id - Delete deal
router.delete("/:id", async (ctx) => {
const { id } = ctx.params;
ctx.response.body = {
success: true,
message: "Deal deleted",
};
});
// GET /api/v1/deals/:id/activities - Get deal activities
router.get("/:id/activities", async (ctx) => {
const { id } = ctx.params;
ctx.response.body = {
success: true,
data: [
{
id: "act-1",
type: "note",
subject: "Anforderungen besprochen",
createdAt: "2026-02-01T10:00:00Z",
},
],
};
});
// GET /api/v1/deals/forecast - Sales forecast
router.get("/forecast", async (ctx) => {
ctx.response.body = {
success: true,
data: {
currentMonth: {
expected: 75000,
weighted: 35000,
won: 15000,
},
nextMonth: {
expected: 50000,
weighted: 20000,
},
quarter: {
expected: 200000,
weighted: 95000,
won: 45000,
},
},
};
});
export { router as dealsRouter };

109
src/routes/pipelines.ts Normal file
View File

@@ -0,0 +1,109 @@
import { Router } from "@oak/oak";
const router = new Router({ prefix: "/api/v1/pipelines" });
// GET /api/v1/pipelines - List pipelines
router.get("/", async (ctx) => {
ctx.response.body = {
success: true,
data: [
{
id: "pipeline-1",
name: "Sales Pipeline",
isDefault: true,
stages: [
{ id: "lead", name: "Lead", order: 1, probability: 10 },
{ id: "qualified", name: "Qualifiziert", order: 2, probability: 25 },
{ id: "proposal", name: "Angebot", order: 3, probability: 50 },
{ id: "negotiation", name: "Verhandlung", order: 4, probability: 75 },
{ id: "closed_won", name: "Gewonnen", order: 5, probability: 100 },
{ id: "closed_lost", name: "Verloren", order: 6, probability: 0 },
],
dealsCount: 15,
totalValue: 250000,
},
],
};
});
// GET /api/v1/pipelines/:id
router.get("/:id", async (ctx) => {
const { id } = ctx.params;
ctx.response.body = {
success: true,
data: {
id,
name: "Sales Pipeline",
isDefault: true,
stages: [
{ id: "lead", name: "Lead", order: 1, probability: 10 },
{ id: "qualified", name: "Qualifiziert", order: 2, probability: 25 },
{ id: "proposal", name: "Angebot", order: 3, probability: 50 },
{ id: "negotiation", name: "Verhandlung", order: 4, probability: 75 },
{ id: "closed_won", name: "Gewonnen", order: 5, probability: 100 },
{ id: "closed_lost", name: "Verloren", order: 6, probability: 0 },
],
},
};
});
// POST /api/v1/pipelines - Create pipeline
router.post("/", async (ctx) => {
const body = await ctx.request.body.json();
ctx.response.status = 201;
ctx.response.body = {
success: true,
message: "Pipeline created",
data: {
id: "new-pipeline-uuid",
...body,
},
};
});
// PUT /api/v1/pipelines/:id - Update pipeline
router.put("/:id", async (ctx) => {
const { id } = ctx.params;
const body = await ctx.request.body.json();
ctx.response.body = {
success: true,
message: "Pipeline updated",
data: {
id,
...body,
},
};
});
// PUT /api/v1/pipelines/:id/stages - Update stages (reorder, add, remove)
router.put("/:id/stages", async (ctx) => {
const { id } = ctx.params;
const body = await ctx.request.body.json();
const { stages } = body;
ctx.response.body = {
success: true,
message: "Stages updated",
data: {
id,
stages,
},
};
});
// DELETE /api/v1/pipelines/:id
router.delete("/:id", async (ctx) => {
const { id } = ctx.params;
// TODO: Check if pipeline has deals
ctx.response.body = {
success: true,
message: "Pipeline deleted",
};
});
export { router as pipelinesRouter };