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:
135
src/main.ts
135
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 });
|
||||
|
||||
153
src/routes/activities.ts
Normal file
153
src/routes/activities.ts
Normal 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
155
src/routes/auth.ts
Normal 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
214
src/routes/contacts.ts
Normal 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
263
src/routes/deals.ts
Normal 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
109
src/routes/pipelines.ts
Normal 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 };
|
||||
Reference in New Issue
Block a user