Features: - Auth mit JWT + Argon2 (Login, Register, Refresh) - Rollen-System (Chef/Disponent/Mitarbeiter) - User Management mit Berechtigungen - Aufträge mit Zuweisungen - Verfügbarkeitsplanung - Stundenzettel mit Foto-Upload Support - Modulares System mit Config - Entwickler-Panel Endpoints Tech: - Deno + Oak - PostgreSQL - CORS enabled
304 lines
8.9 KiB
TypeScript
304 lines
8.9 KiB
TypeScript
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<Order>(
|
|
`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<Order>(
|
|
`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<OrderAssignment & { user_name: string }>(
|
|
`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<Order>(
|
|
`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<Order>(
|
|
`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" };
|
|
});
|