Files
pn-new-crm/src/app/api/v1/reports/operational/route.ts
2026-06-02 10:11:26 +02:00

109 lines
3.0 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { withAuth, withPermission } from '@/lib/api/helpers';
import { errorResponse } from '@/lib/errors';
import { parseOperationalFilters } from '@/lib/services/reports/operational-filters';
import {
getOperationalKpis,
getUtilisationHeatmap,
getStatusMixOverTime,
getTenancyChurn,
getTenureDistribution,
getSigningBoxPlot,
getOccupancyByArea,
getDocumentsInPipeline,
getTenanciesEndingSoon,
getVacantBerths,
getStuckSigning,
getHighestValueVacant,
getOperationalAreaOptions,
operationalHasData,
} from '@/lib/services/reports/operational.service';
const querySchema = z.object({
from: z.string().datetime().optional(),
to: z.string().datetime().optional(),
});
function resolveRange(from?: string, to?: string): { from: Date; to: Date } {
const now = new Date();
const defaultFrom = new Date(now);
defaultFrom.setDate(defaultFrom.getDate() - 30);
return {
from: from ? new Date(from) : defaultFrom,
to: to ? new Date(to) : now,
};
}
export const GET = withAuth(
withPermission('reports', 'view_dashboard', async (req: NextRequest, ctx) => {
try {
const params = req.nextUrl.searchParams;
const { from, to } = querySchema.parse({
from: params.get('from') ?? undefined,
to: params.get('to') ?? undefined,
});
const range = resolveRange(from, to);
const filters = parseOperationalFilters(params);
const [
kpis,
utilisationHeatmap,
statusMix,
tenancyChurn,
tenureDistribution,
signingBoxPlot,
occupancyByArea,
docsInPipeline,
endingSoon,
vacantBerths,
stuckSigning,
highestValueVacant,
areaOptions,
hasData,
] = await Promise.all([
getOperationalKpis(ctx.portId, range, filters),
getUtilisationHeatmap(ctx.portId, 24, filters),
getStatusMixOverTime(ctx.portId),
getTenancyChurn(ctx.portId),
getTenureDistribution(ctx.portId),
getSigningBoxPlot(ctx.portId),
getOccupancyByArea(ctx.portId, filters),
getDocumentsInPipeline(ctx.portId),
getTenanciesEndingSoon(ctx.portId),
getVacantBerths(ctx.portId, 60, filters),
getStuckSigning(ctx.portId),
getHighestValueVacant(ctx.portId, 10, filters),
getOperationalAreaOptions(ctx.portId),
operationalHasData(ctx.portId),
]);
return NextResponse.json({
data: {
kpis,
utilisationHeatmap,
statusMix,
tenancyChurn,
tenureDistribution,
signingBoxPlot,
occupancyByArea,
docsInPipeline,
endingSoon,
vacantBerths,
stuckSigning,
highestValueVacant,
areaOptions,
hasData,
range: {
from: range.from.toISOString(),
to: range.to.toISOString(),
},
},
});
} catch (error) {
return errorResponse(error);
}
}),
);