Aufgaben ohne Kunde/Vertrag anlegbar
ContractTask.contractId nullable (Migration). Neuer staff-only Endpoint POST /tasks fuer allgemeine Aufgaben ohne Vertrag/Kunde. Ohne Vertrag gibt es keinen Kunden -> visibleInPortal serverseitig immer false, Portal-Reply 403 bei contractloser Aufgabe, getAllTasks-Portal-Filter schliesst sie automatisch aus (kein contract-Match). Task-Modal (Mitarbeiter): Checkbox "Ohne Kunde (allgemeine Aufgabe)" blendet Kunden-/Vertragsauswahl UND "Im Kundenportal sichtbar" aus. Task-Liste zeigt solche Aufgaben als "Allgemeine Aufgabe (ohne Vertrag)" ohne Vertrags-Link/ Zum-Vertrag-Button. Verifiziert: contractlose Aufgabe -> contractId null, visibleInPortal erzwungen false (auch wenn true geschickt); mit Vertrag weiterhin waehlbar. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -118,6 +118,41 @@ export async function getTasks(req: AuthRequest, res: Response): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Allgemeine Aufgabe OHNE Vertrag/Kunde (nur Mitarbeiter/Admin). Ohne Vertrag
|
||||
// gibt es keinen Kunden → visibleInPortal ist hier bedeutungslos und immer
|
||||
// false (der Service erzwingt das ohnehin).
|
||||
export async function createGeneralTask(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
if (req.user?.isCustomerPortal) {
|
||||
res.status(403).json({ success: false, error: 'Kein Zugriff' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
const { title, description } = req.body ?? {};
|
||||
if (!title || !String(title).trim()) {
|
||||
res.status(400).json({ success: false, error: 'Titel ist erforderlich' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
const task = await contractTaskService.createTask({
|
||||
contractId: null,
|
||||
title: String(title).trim(),
|
||||
description,
|
||||
visibleInPortal: false,
|
||||
createdBy: req.user?.email,
|
||||
});
|
||||
await logChange({
|
||||
req, action: 'CREATE', resourceType: 'ContractTask',
|
||||
resourceId: task.id.toString(),
|
||||
label: `Allgemeine Aufgabe "${String(title).trim()}" erstellt (ohne Vertrag)`,
|
||||
});
|
||||
res.status(201).json({ success: true, data: task } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Aufgabe',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTask(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const contractId = parseInt(req.params.contractId);
|
||||
@@ -361,6 +396,12 @@ export async function createCustomerReply(req: AuthRequest, res: Response): Prom
|
||||
return;
|
||||
}
|
||||
|
||||
// Allgemeine Aufgaben (ohne Vertrag/Kunde) sind reine Mitarbeiter-Aufgaben
|
||||
// und für Portal-User nie zugänglich.
|
||||
if (task.contractId == null) {
|
||||
res.status(403).json({ success: false, error: 'Kein Zugriff' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
// Strikter Owner-Check über den Vertrag (mit Live-Vollmacht-Prüfung
|
||||
// via hasAuthorization, Pentest Runde 6 – HOCH-04). Damit kann ein
|
||||
// Portal-User keine fremde Task-ID mit visibleInPortal=true abgreifen.
|
||||
|
||||
@@ -22,6 +22,14 @@ router.get(
|
||||
contractTaskController.getTaskStats
|
||||
);
|
||||
|
||||
// Allgemeine Aufgabe OHNE Vertrag/Kunde anlegen (nur Mitarbeiter)
|
||||
router.post(
|
||||
'/tasks',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
contractTaskController.createGeneralTask
|
||||
);
|
||||
|
||||
// ==================== TASKS BY CONTRACT ====================
|
||||
|
||||
// Alle Aufgaben eines Vertrags abrufen (auch für Kundenportal, aber nur sichtbare)
|
||||
|
||||
@@ -50,18 +50,21 @@ export async function getTaskById(id: number) {
|
||||
}
|
||||
|
||||
export async function createTask(data: {
|
||||
contractId: number;
|
||||
contractId?: number | null;
|
||||
title: string;
|
||||
description?: string;
|
||||
visibleInPortal?: boolean;
|
||||
createdBy?: string;
|
||||
}) {
|
||||
// Ohne Vertrag (allgemeine Aufgabe) gibt es keinen Kunden → nie im Portal
|
||||
// sichtbar, egal was der Client schickt.
|
||||
const hasContract = data.contractId != null;
|
||||
return prisma.contractTask.create({
|
||||
data: {
|
||||
contractId: data.contractId,
|
||||
contractId: data.contractId ?? null,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
visibleInPortal: data.visibleInPortal ?? false,
|
||||
visibleInPortal: hasContract ? (data.visibleInPortal ?? false) : false,
|
||||
createdBy: data.createdBy,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user