chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
import { handlers } from "~/server/auth.js";
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
@@ -0,0 +1,105 @@
|
||||
import { renderToBuffer } from "@react-pdf/renderer";
|
||||
import { createElement } from "react";
|
||||
import { NextResponse } from "next/server";
|
||||
import * as XLSX from "xlsx";
|
||||
import { buildSplitAllocationReadModel } from "@planarchy/application";
|
||||
import { prisma } from "@planarchy/db";
|
||||
import type { AllocationLike } from "@planarchy/shared";
|
||||
import { auth } from "~/server/auth.js";
|
||||
import { AllocationReport } from "~/components/reports/AllocationReport.js";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const startDate = searchParams.get("startDate") ? new Date(searchParams.get("startDate")!) : new Date();
|
||||
const endDate = searchParams.get("endDate") ? new Date(searchParams.get("endDate")!) : new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
|
||||
const format = searchParams.get("format") ?? "pdf";
|
||||
|
||||
const [demandRequirements, assignments] = await Promise.all([
|
||||
prisma.demandRequirement.findMany({
|
||||
where: {
|
||||
status: { not: "CANCELLED" },
|
||||
startDate: { lte: endDate },
|
||||
endDate: { gte: startDate },
|
||||
},
|
||||
include: {
|
||||
project: { select: { id: true, name: true, shortCode: true } },
|
||||
},
|
||||
orderBy: [{ project: { name: "asc" } }, { startDate: "asc" }],
|
||||
take: 1000,
|
||||
}),
|
||||
prisma.assignment.findMany({
|
||||
where: {
|
||||
status: { not: "CANCELLED" },
|
||||
startDate: { lte: endDate },
|
||||
endDate: { gte: startDate },
|
||||
},
|
||||
include: {
|
||||
resource: { select: { id: true, displayName: true, eid: true, lcrCents: true } },
|
||||
project: { select: { id: true, name: true, shortCode: true } },
|
||||
},
|
||||
orderBy: [{ project: { name: "asc" } }, { startDate: "asc" }],
|
||||
take: 1000,
|
||||
}),
|
||||
]);
|
||||
|
||||
const allocationView = buildSplitAllocationReadModel({
|
||||
demandRequirements,
|
||||
assignments,
|
||||
});
|
||||
const assignmentRows = allocationView.assignments.slice(0, 500);
|
||||
|
||||
const rows = assignmentRows.map((a: AllocationLike & {
|
||||
resource?: { displayName?: string | null } | null;
|
||||
project?: { shortCode: string; name: string } | null;
|
||||
}) => ({
|
||||
resourceName: a.resource?.displayName ?? "Unknown",
|
||||
projectName: a.project ? `${a.project.shortCode} — ${a.project.name}` : "Unknown project",
|
||||
role: a.role ?? "",
|
||||
startDate: new Date(a.startDate).toLocaleDateString("en-GB"),
|
||||
endDate: new Date(a.endDate).toLocaleDateString("en-GB"),
|
||||
hoursPerDay: a.hoursPerDay,
|
||||
dailyCostCents: a.dailyCostCents,
|
||||
}));
|
||||
|
||||
const ts = Date.now();
|
||||
|
||||
if (format === "xlsx") {
|
||||
const sheetData = rows.map((r: typeof rows[number]) => ({
|
||||
Resource: r.resourceName,
|
||||
Project: r.projectName,
|
||||
Role: r.role,
|
||||
"Start Date": r.startDate,
|
||||
"End Date": r.endDate,
|
||||
"Hours/Day": r.hoursPerDay,
|
||||
"Daily Cost (ct)": r.dailyCostCents,
|
||||
}));
|
||||
const ws = XLSX.utils.json_to_sheet(sheetData);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "Allocations");
|
||||
const buffer = XLSX.write(wb, { type: "buffer", bookType: "xlsx" }) as Buffer;
|
||||
return new NextResponse(buffer as unknown as BodyInit, {
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"Content-Disposition": `attachment; filename="allocations-${ts}.xlsx"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const title = `Allocation Report ${startDate.toLocaleDateString("en-GB")} – ${endDate.toLocaleDateString("en-GB")}`;
|
||||
const generatedAt = new Date().toLocaleString("en-GB");
|
||||
const doc = createElement(AllocationReport, { title, generatedAt, rows });
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const buffer = await renderToBuffer(doc as any);
|
||||
|
||||
return new NextResponse(buffer as unknown as BodyInit, {
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
"Content-Disposition": `attachment; filename="allocations-${ts}.pdf"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { eventBus } from "@planarchy/api/sse";
|
||||
import { SSE_EVENT_TYPES } from "@planarchy/shared";
|
||||
import { auth } from "~/server/auth.js";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
// Send initial connection confirmation
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({ type: SSE_EVENT_TYPES.PING, timestamp: new Date().toISOString() })}\n\n`),
|
||||
);
|
||||
|
||||
// Subscribe to event bus
|
||||
const unsubscribe = eventBus.subscribe((event) => {
|
||||
try {
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
|
||||
} catch {
|
||||
// Client disconnected
|
||||
}
|
||||
});
|
||||
|
||||
// Heartbeat every 30 seconds
|
||||
const heartbeat = setInterval(() => {
|
||||
try {
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({ type: SSE_EVENT_TYPES.PING, timestamp: new Date().toISOString() })}\n\n`),
|
||||
);
|
||||
} catch {
|
||||
clearInterval(heartbeat);
|
||||
unsubscribe();
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
// Cleanup on close
|
||||
return () => {
|
||||
clearInterval(heartbeat);
|
||||
unsubscribe();
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
Connection: "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createTRPCContext } from "@planarchy/api";
|
||||
import { appRouter } from "@planarchy/api/router";
|
||||
import { prisma } from "@planarchy/db";
|
||||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { auth } from "~/server/auth.js";
|
||||
|
||||
const handler = async (req: NextRequest) => {
|
||||
const session = await auth();
|
||||
|
||||
const dbUser = session?.user?.email
|
||||
? await prisma.user.findUnique({
|
||||
where: { email: session.user.email },
|
||||
select: { id: true, systemRole: true, permissionOverrides: true },
|
||||
})
|
||||
: null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const options: any = {
|
||||
endpoint: "/api/trpc",
|
||||
req,
|
||||
router: appRouter,
|
||||
createContext: () => createTRPCContext({ session, dbUser }),
|
||||
};
|
||||
|
||||
if (process.env["NODE_ENV"] === "development") {
|
||||
options.onError = ({ path, error }: { path?: string; error: { message: string } }) => {
|
||||
console.error(`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`);
|
||||
};
|
||||
}
|
||||
|
||||
return fetchRequestHandler(options);
|
||||
};
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
Reference in New Issue
Block a user