snooze vor expired, contracts, display snoozed contracts if an item is missing, un snooze implemented, fixed invoice upload bug
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as contractService from '../services/contract.service.js';
|
||||
import * as contractCockpitService from '../services/contractCockpit.service.js';
|
||||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function getContracts(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { customerId, type, status, search, page, limit, tree } = req.query;
|
||||
@@ -194,3 +197,48 @@ export async function getCockpit(req: AuthRequest, res: Response): Promise<void>
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SNOOZE (VERTRAG ZURÜCKSTELLEN) ====================
|
||||
|
||||
export async function snoozeContract(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const { nextReviewDate, months } = req.body;
|
||||
|
||||
let reviewDate: Date | null = null;
|
||||
|
||||
if (nextReviewDate) {
|
||||
// Explizites Datum angegeben
|
||||
reviewDate = new Date(nextReviewDate);
|
||||
} else if (months) {
|
||||
// Monate angegeben → berechne Datum
|
||||
reviewDate = new Date();
|
||||
reviewDate.setMonth(reviewDate.getMonth() + months);
|
||||
}
|
||||
// Wenn beides leer → nextReviewDate wird auf null gesetzt (Snooze aufheben)
|
||||
|
||||
const updated = await prisma.contract.update({
|
||||
where: { id },
|
||||
data: { nextReviewDate: reviewDate },
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
nextReviewDate: true,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: updated,
|
||||
message: reviewDate
|
||||
? `Vertrag zurückgestellt bis ${reviewDate.toLocaleDateString('de-DE')}`
|
||||
: 'Zurückstellung aufgehoben',
|
||||
} as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('Snooze error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Zurückstellen des Vertrags',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ router.delete('/:id', authenticate, requirePermission('contracts:delete'), contr
|
||||
// Follow-up contract
|
||||
router.post('/:id/follow-up', authenticate, requirePermission('contracts:create'), contractController.createFollowUp);
|
||||
|
||||
// Snooze (Vertrag zurückstellen)
|
||||
router.patch('/:id/snooze', authenticate, requirePermission('contracts:update'), contractController.snoozeContract);
|
||||
|
||||
// Get decrypted password
|
||||
router.get('/:id/password', authenticate, requirePermission('contracts:read'), contractController.getContractPassword);
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface CockpitSummary {
|
||||
missingInvoices: number;
|
||||
openTasks: number;
|
||||
pendingContracts: number;
|
||||
reviewDue: number; // Erneute Prüfung fällig (Snooze abgelaufen)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -229,17 +230,46 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
missingInvoices: 0,
|
||||
openTasks: 0,
|
||||
pendingContracts: 0,
|
||||
reviewDue: 0,
|
||||
},
|
||||
};
|
||||
|
||||
for (const contract of contracts) {
|
||||
const issues: CockpitIssue[] = [];
|
||||
|
||||
// SNOOZE-LOGIK: Prüfen ob Snooze aktiv ist (für Fristen-Unterdrückung)
|
||||
let snoozeActive = false;
|
||||
if (contract.nextReviewDate) {
|
||||
const reviewDate = new Date(contract.nextReviewDate);
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
reviewDate.setHours(0, 0, 0, 0);
|
||||
|
||||
if (reviewDate > now) {
|
||||
// Snooze aktiv → NUR Fristen-Warnungen unterdrücken, andere Prüfungen laufen weiter
|
||||
snoozeActive = true;
|
||||
} else {
|
||||
// Snooze abgelaufen → "Erneute Prüfung fällig" Warnung
|
||||
const daysSince = Math.floor((now.getTime() - reviewDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
issues.push({
|
||||
type: 'review_due',
|
||||
label: 'Erneute Prüfung fällig',
|
||||
urgency: daysSince > 30 ? 'critical' : 'warning',
|
||||
daysRemaining: -daysSince,
|
||||
details: daysSince === 0
|
||||
? 'Heute zur Prüfung fällig'
|
||||
: `Zur Prüfung seit ${daysSince} Tagen fällig`,
|
||||
});
|
||||
summary.byCategory.reviewDue++;
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfen ob aktiver Folgevertrag existiert - dann keine Kündigungswarnungen nötig
|
||||
const hasActiveFollowUp = contract.followUpContract?.status === 'ACTIVE';
|
||||
|
||||
// 1. KÜNDIGUNGSFRIST (nur wenn kein aktiver Folgevertrag)
|
||||
if (!hasActiveFollowUp) {
|
||||
// 1. KÜNDIGUNGSFRIST (nur wenn kein aktiver Folgevertrag UND Snooze nicht aktiv)
|
||||
// Snooze unterdrückt NUR Fristen-bezogene Warnungen!
|
||||
if (!hasActiveFollowUp && !snoozeActive) {
|
||||
const cancellationDeadline = calculateCancellationDeadline(
|
||||
contract.endDate,
|
||||
contract.cancellationPeriod?.code
|
||||
@@ -284,21 +314,24 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. VERTRAGSENDE
|
||||
const daysToEnd = daysUntil(contract.endDate);
|
||||
if (daysToEnd !== null && daysToEnd <= okDays) {
|
||||
const urgency = getUrgencyByDays(daysToEnd, criticalDays, warningDays, okDays);
|
||||
if (urgency !== 'none') {
|
||||
issues.push({
|
||||
type: 'contract_ending',
|
||||
label: 'Vertragsende',
|
||||
urgency,
|
||||
daysRemaining: daysToEnd,
|
||||
details: daysToEnd < 0
|
||||
? `Vertrag seit ${Math.abs(daysToEnd)} Tagen abgelaufen!`
|
||||
: `Noch ${daysToEnd} Tage bis Vertragsende`,
|
||||
});
|
||||
summary.byCategory.contractEnding++;
|
||||
// 2. VERTRAGSENDE (nur wenn Snooze nicht aktiv)
|
||||
// Snooze unterdrückt NUR Fristen-bezogene Warnungen!
|
||||
if (!snoozeActive) {
|
||||
const daysToEnd = daysUntil(contract.endDate);
|
||||
if (daysToEnd !== null && daysToEnd <= okDays) {
|
||||
const urgency = getUrgencyByDays(daysToEnd, criticalDays, warningDays, okDays);
|
||||
if (urgency !== 'none') {
|
||||
issues.push({
|
||||
type: 'contract_ending',
|
||||
label: 'Vertragsende',
|
||||
urgency,
|
||||
daysRemaining: daysToEnd,
|
||||
details: daysToEnd < 0
|
||||
? `Vertrag seit ${Math.abs(daysToEnd)} Tagen abgelaufen!`
|
||||
: `Noch ${daysToEnd} Tage bis Vertragsende`,
|
||||
});
|
||||
summary.byCategory.contractEnding++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,13 +39,15 @@ export async function getInvoice(energyContractDetailsId: number, invoiceId: num
|
||||
|
||||
/**
|
||||
* Neue Rechnung hinzufügen
|
||||
*
|
||||
* Hinweis: Die Validierung ob ein Dokument vorhanden ist, erfolgt NICHT hier,
|
||||
* da der typische Flow so aussieht:
|
||||
* 1. Invoice erstellen (ohne Dokument) → Invoice-ID zurückbekommen
|
||||
* 2. Dokument hochladen mit der Invoice-ID
|
||||
*
|
||||
* Die Validierung ob alle Rechnungen Dokumente haben, erfolgt im Cockpit.
|
||||
*/
|
||||
export async function addInvoice(energyContractDetailsId: number, data: CreateInvoiceData) {
|
||||
// Validierung: documentPath ist Pflicht, außer bei NOT_AVAILABLE
|
||||
if (data.invoiceType !== 'NOT_AVAILABLE' && !data.documentPath) {
|
||||
throw new Error('Dokument ist Pflicht (außer bei Typ "Nicht verfügbar")');
|
||||
}
|
||||
|
||||
// Prüfen ob EnergyContractDetails existiert
|
||||
const energyDetails = await prisma.energyContractDetails.findUnique({
|
||||
where: { id: energyContractDetailsId },
|
||||
|
||||
Reference in New Issue
Block a user