import { Router } from "@oak/oak"; import { query, queryOne, execute } from "../db/postgres.ts"; import { AppError } from "../middleware/error.ts"; import { authMiddleware, requireDisponentOrHigher } from "../middleware/auth.ts"; import type { Order, OrderAssignment } from "../types/index.ts"; export const ordersRouter = new Router({ prefix: "/api/orders" }); // Get all orders (filtered by role) ordersRouter.get("/", authMiddleware, async (ctx) => { const { id: userId, org_id: orgId, role } = ctx.state.auth.user; const status = ctx.request.url.searchParams.get("status"); let orders: Order[]; const params: unknown[] = [orgId]; let whereClause = "WHERE o.org_id = $1"; if (status) { whereClause += " AND o.status = $2"; params.push(status); } if (role === "mitarbeiter") { // Mitarbeiter only sees assigned orders const paramIndex = params.length + 1; whereClause += ` AND EXISTS ( SELECT 1 FROM order_assignments oa WHERE oa.order_id = o.id AND oa.user_id = $${paramIndex} )`; params.push(userId); } orders = await query( `SELECT o.*, u.first_name || ' ' || u.last_name as creator_name, (SELECT COUNT(*) FROM order_assignments WHERE order_id = o.id) as assigned_count FROM orders o LEFT JOIN users u ON o.created_by = u.id ${whereClause} ORDER BY o.start_time DESC NULLS LAST, o.created_at DESC`, params ); ctx.response.body = { orders }; }); // Get single order with assignments ordersRouter.get("/:id", authMiddleware, async (ctx) => { const { id: userId, org_id: orgId, role } = ctx.state.auth.user; const orderId = ctx.params.id; const order = await queryOne( `SELECT o.*, u.first_name || ' ' || u.last_name as creator_name FROM orders o LEFT JOIN users u ON o.created_by = u.id WHERE o.id = $1 AND o.org_id = $2`, [orderId, orgId] ); if (!order) { throw new AppError("Order not found", 404); } // Check if Mitarbeiter is assigned to this order if (role === "mitarbeiter") { const assignment = await queryOne<{ id: string }>( `SELECT id FROM order_assignments WHERE order_id = $1 AND user_id = $2`, [orderId, userId] ); if (!assignment) { throw new AppError("Access denied", 403); } } // Get assignments const assignments = await query( `SELECT oa.*, u.first_name || ' ' || u.last_name as user_name, u.phone as user_phone FROM order_assignments oa JOIN users u ON oa.user_id = u.id WHERE oa.order_id = $1 ORDER BY oa.created_at`, [orderId] ); ctx.response.body = { order, assignments }; }); // Create order ordersRouter.post("/", requireDisponentOrHigher, async (ctx) => { const { id: userId, org_id: orgId } = ctx.state.auth.user; const body = await ctx.request.body.json(); const { title, description, location, address, client_name, client_contact, status, start_time, end_time, required_staff, special_instructions } = body; if (!title) { throw new AppError("Title is required", 400); } const result = await queryOne<{ id: string; number: number }>( `INSERT INTO orders (org_id, title, description, location, address, client_name, client_contact, status, start_time, end_time, required_staff, special_instructions, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, number`, [orgId, title, description || null, location || null, address || null, client_name || null, client_contact || null, status || "draft", start_time || null, end_time || null, required_staff || 1, special_instructions || null, userId] ); ctx.response.status = 201; ctx.response.body = { message: "Order created", orderId: result?.id, number: result?.number }; }); // Update order ordersRouter.put("/:id", requireDisponentOrHigher, async (ctx) => { const { id: userId, org_id: orgId, role } = ctx.state.auth.user; const orderId = ctx.params.id; const body = await ctx.request.body.json(); // Get order const order = await queryOne( `SELECT * FROM orders WHERE id = $1 AND org_id = $2`, [orderId, orgId] ); if (!order) { throw new AppError("Order not found", 404); } // Disponent can only edit their own orders if (role === "disponent" && order.created_by !== userId) { throw new AppError("Access denied", 403); } // Build update const allowedFields = [ "title", "description", "location", "address", "client_name", "client_contact", "status", "start_time", "end_time", "required_staff", "special_instructions" ]; const updates: string[] = []; const values: unknown[] = []; let paramIndex = 1; for (const field of allowedFields) { if (body[field] !== undefined) { updates.push(`${field} = $${paramIndex}`); values.push(body[field] === "" ? null : body[field]); paramIndex++; } } if (updates.length === 0) { throw new AppError("No valid fields to update", 400); } values.push(orderId); await execute( `UPDATE orders SET ${updates.join(", ")} WHERE id = $${paramIndex}`, values ); ctx.response.body = { message: "Order updated" }; }); // Delete order ordersRouter.delete("/:id", requireDisponentOrHigher, async (ctx) => { const { id: userId, org_id: orgId, role } = ctx.state.auth.user; const orderId = ctx.params.id; const order = await queryOne( `SELECT * FROM orders WHERE id = $1 AND org_id = $2`, [orderId, orgId] ); if (!order) { throw new AppError("Order not found", 404); } // Disponent can only delete their own orders if (role === "disponent" && order.created_by !== userId) { throw new AppError("Access denied", 403); } await execute(`DELETE FROM orders WHERE id = $1`, [orderId]); ctx.response.body = { message: "Order deleted" }; }); // ============ ASSIGNMENTS ============ // Assign user to order ordersRouter.post("/:id/assign", requireDisponentOrHigher, async (ctx) => { const { org_id: orgId, role } = ctx.state.auth.user; const orderId = ctx.params.id; const body = await ctx.request.body.json(); const { user_id, note } = body; if (!user_id) { throw new AppError("User ID required", 400); } // Verify order exists const order = await queryOne<{ id: string }>( `SELECT id FROM orders WHERE id = $1 AND org_id = $2`, [orderId, orgId] ); if (!order) { throw new AppError("Order not found", 404); } // Verify user exists and is in same org const user = await queryOne<{ id: string; role: string }>( `SELECT id, role FROM users WHERE id = $1 AND org_id = $2 AND active = true`, [user_id, orgId] ); if (!user) { throw new AppError("User not found", 404); } // Check if already assigned const existing = await queryOne<{ id: string }>( `SELECT id FROM order_assignments WHERE order_id = $1 AND user_id = $2`, [orderId, user_id] ); if (existing) { throw new AppError("User already assigned", 409); } await execute( `INSERT INTO order_assignments (order_id, user_id, status, note) VALUES ($1, $2, 'pending', $3)`, [orderId, user_id, note || null] ); ctx.response.status = 201; ctx.response.body = { message: "User assigned" }; }); // Remove assignment ordersRouter.delete("/:id/assign/:userId", requireDisponentOrHigher, async (ctx) => { const { org_id: orgId } = ctx.state.auth.user; const orderId = ctx.params.id; const assignedUserId = ctx.params.userId; // Verify order exists const order = await queryOne<{ id: string }>( `SELECT id FROM orders WHERE id = $1 AND org_id = $2`, [orderId, orgId] ); if (!order) { throw new AppError("Order not found", 404); } await execute( `DELETE FROM order_assignments WHERE order_id = $1 AND user_id = $2`, [orderId, assignedUserId] ); ctx.response.body = { message: "Assignment removed" }; }); // Update assignment status (confirm/decline) - for assigned user ordersRouter.put("/:id/assignment", authMiddleware, async (ctx) => { const { id: userId } = ctx.state.auth.user; const orderId = ctx.params.id; const body = await ctx.request.body.json(); const { status, note } = body; if (!status || !["confirmed", "declined"].includes(status)) { throw new AppError("Valid status required (confirmed/declined)", 400); } const assignment = await queryOne<{ id: string }>( `SELECT id FROM order_assignments WHERE order_id = $1 AND user_id = $2`, [orderId, userId] ); if (!assignment) { throw new AppError("Assignment not found", 404); } const confirmedAt = status === "confirmed" ? "NOW()" : "NULL"; await execute( `UPDATE order_assignments SET status = $1, note = $2, confirmed_at = ${confirmedAt} WHERE order_id = $3 AND user_id = $4`, [status, note || null, orderId, userId] ); ctx.response.body = { message: "Assignment updated" }; });