🚀 Backend komplett implementiert

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
This commit is contained in:
2026-02-20 15:12:06 +00:00
parent a07c2ad858
commit ee19e45171
16 changed files with 2079 additions and 2 deletions

303
src/routes/orders.ts Normal file
View File

@@ -0,0 +1,303 @@
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" };
});