diff --git a/src/main.ts b/src/main.ts index d69c62e..fd6ba7b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,28 @@ import { Application } from "@oak/oak"; 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 PORT = parseInt(Deno.env.get("PORT") || "8000"); +// ============================================ +// MIDDLEWARE +// ============================================ + // CORS Middleware app.use(async (ctx, next) => { - ctx.response.headers.set("Access-Control-Allow-Origin", "*"); - ctx.response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + const origin = ctx.request.headers.get("origin") || "*"; + 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-Credentials", "true"); + ctx.response.headers.set("Access-Control-Max-Age", "86400"); if (ctx.request.method === "OPTIONS") { ctx.response.status = 204; @@ -23,9 +37,34 @@ app.use(async (ctx, next) => { const start = Date.now(); await next(); 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 app.use(async (ctx, next) => { if (ctx.request.url.pathname === "/health") { @@ -33,7 +72,7 @@ app.use(async (ctx, next) => { status: "ok", service: "pulse-crm-backend", version: "0.1.0", - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), }; return; } @@ -46,36 +85,94 @@ app.use(async (ctx, next) => { ctx.response.body = { name: "Pulse CRM API", version: "1.0.0", - docs: "/api/v1/docs", + description: "Der Herzschlag deines Business", endpoints: { - auth: "/api/v1/auth/*", - contacts: "/api/v1/contacts/*", - companies: "/api/v1/companies/*", - deals: "/api/v1/deals/*", - pipelines: "/api/v1/pipelines/*", - activities: "/api/v1/activities/*", - users: "/api/v1/users/*" - } + auth: { + "POST /api/v1/auth/register": "Register new user", + "POST /api/v1/auth/login": "Login", + "POST /api/v1/auth/refresh": "Refresh token", + "POST /api/v1/auth/logout": "Logout", + "GET /api/v1/auth/me": "Get current user", + }, + 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; } 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) => { ctx.response.status = 404; ctx.response.body = { success: false, error: { 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`); -console.log(`❤️ Health: http://localhost:${PORT}/health`); +// ============================================ +// START SERVER +// ============================================ + +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 }); diff --git a/src/routes/activities.ts b/src/routes/activities.ts new file mode 100644 index 0000000..1412439 --- /dev/null +++ b/src/routes/activities.ts @@ -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 }; diff --git a/src/routes/auth.ts b/src/routes/auth.ts new file mode 100644 index 0000000..c5506d8 --- /dev/null +++ b/src/routes/auth.ts @@ -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 }; diff --git a/src/routes/contacts.ts b/src/routes/contacts.ts new file mode 100644 index 0000000..af9b07d --- /dev/null +++ b/src/routes/contacts.ts @@ -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 }; diff --git a/src/routes/deals.ts b/src/routes/deals.ts new file mode 100644 index 0000000..546e958 --- /dev/null +++ b/src/routes/deals.ts @@ -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 }; diff --git a/src/routes/pipelines.ts b/src/routes/pipelines.ts new file mode 100644 index 0000000..58e180a --- /dev/null +++ b/src/routes/pipelines.ts @@ -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 };