Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 54 additions & 20 deletions src/app/(dashboard)/reports/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { auth } from "@/auth"
import { prisma } from "@/lib/prisma"
import { getReportData } from "@/lib/reports"
import { parseReportFilters, filtersToQuery } from "@/lib/reports/filters"
import { PRESET_DATA_TYPES } from "@/lib/dataTypes"
import { ReportToolbar } from "@/components/reports/ReportToolbar"
import { ReportFilterBar } from "@/components/reports/ReportFilterBar"
import { ReportCanvas, type ReportSectionEntry } from "@/components/reports/ReportCanvas"
import { KeyFindingsSection } from "@/components/reports/KeyFindingsSection"
import { ExposureSection } from "@/components/reports/ExposureSection"
import { DataTypeSection } from "@/components/reports/DataTypeSection"
Expand All @@ -10,44 +15,73 @@ import { EmployeeSection } from "@/components/reports/EmployeeSection"
import { ComplianceSection } from "@/components/reports/ComplianceSection"

function formatGeneratedAt(iso: string): string {
return new Date(iso).toLocaleString("en-US", {
dateStyle: "long",
timeStyle: "short",
})
return new Date(iso).toLocaleString("en-US", { dateStyle: "long", timeStyle: "short" })
}

export default async function ReportsPage() {
type SearchParams = Record<string, string | string[] | undefined>

export default async function ReportsPage({ searchParams }: { searchParams: Promise<SearchParams> }) {
const session = await auth()
const data = await getReportData(session!.user.companyId)
const companyId = session!.user.companyId

const sp = await searchParams
const params = new URLSearchParams()
for (const [k, v] of Object.entries(sp)) {
if (typeof v === "string") params.set(k, v)
}
const filters = parseReportFilters(params)
const filterQuery = filtersToQuery(filters)

const [data, deptGroups] = await Promise.all([
getReportData(companyId, filters),
prisma.employee.groupBy({ by: ["department"], where: { companyId } }),
])

const departments = [
...deptGroups.map((d) => d.department).filter((d): d is string => d !== null).sort(),
...(deptGroups.some((d) => d.department === null) ? ["Unknown"] : []),
]
const companyName = session!.user.name ?? ""

const sections: ReportSectionEntry[] = [
{ id: "findings", title: "Key Findings", defaultSpan: 12, content: <KeyFindingsSection findings={data.findings} /> },
{ id: "exposure", title: "Exposure", defaultSpan: 12, content: <ExposureSection data={data.exposure} /> },
{ id: "datatypes", title: "Data Types", defaultSpan: 6, content: <DataTypeSection rows={data.dataTypes} /> },
{ id: "departments", title: "Departments", defaultSpan: 6, content: <DepartmentSection rows={data.departments} /> },
{ id: "trends", title: "Trends", defaultSpan: 12, content: <TrendsSection data={data.trends} /> },
{ id: "employees", title: "Employees", defaultSpan: 12, content: <EmployeeSection rows={data.employees} /> },
{ id: "compliance", title: "Compliance", defaultSpan: 12, content: <ComplianceSection data={data.compliance} /> },
]

return (
<div id="report-root" className="h-full overflow-y-auto p-6">
<div id="report-root" className="flex h-full flex-col overflow-y-auto p-6">
<div className="mb-6 hidden print:block">
<h1 className="text-xl font-semibold text-foreground">DataShield Security Report</h1>
<p className="text-sm text-muted-foreground">
{companyName}, generated {formatGeneratedAt(data.generatedAt)}
</p>
</div>

<div className="mb-6 flex items-start justify-between gap-4 print:hidden">
<div className="no-print mb-4 flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-semibold text-foreground">Reports</h2>
<p className="text-sm text-muted-foreground">
Exposure, employees, trends and compliance overview
</p>
<p className="text-sm text-muted-foreground">Exposure, employees, trends and compliance overview</p>
</div>
<ReportToolbar generatedAt={data.generatedAt} />
<ReportToolbar generatedAt={data.generatedAt} filterQuery={filterQuery} />
</div>

<div className="no-print mb-4">
<ReportFilterBar filters={filters} departments={departments} dataTypes={PRESET_DATA_TYPES.map((t) => ({ key: t.key, label: t.label }))} />
</div>

<div className="space-y-6">
<KeyFindingsSection findings={data.findings} />
<ExposureSection data={data.exposure} />
<DataTypeSection rows={data.dataTypes} />
<DepartmentSection rows={data.departments} />
<TrendsSection data={data.trends} />
<EmployeeSection rows={data.employees} />
<ComplianceSection data={data.compliance} />
{/* Interactive grid (screen) */}
<ReportCanvas sections={sections} />

{/* Print-only stacked layout */}
<div className="hidden space-y-6 print:block">
{sections.map((s) => (
<div key={s.id}>{s.content}</div>
))}
</div>
</div>
)
Expand Down
7 changes: 5 additions & 2 deletions src/app/api/reports/export/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { requireAuth } from "@/lib/apiAuth"
import { getReportData } from "@/lib/reports"
import { parseReportFilters } from "@/lib/reports/filters"
import { reportCsv, type CsvSection } from "@/lib/reports/csv"

const SECTIONS: CsvSection[] = [
Expand All @@ -20,10 +21,12 @@ export async function GET(request: Request): Promise<Response> {
const { session, error } = await requireAuth()
if (error) return error

const section = new URL(request.url).searchParams.get("section") ?? "all"
const sp = new URL(request.url).searchParams
const section = sp.get("section") ?? "all"
if (!isSection(section)) return new Response("Invalid section", { status: 400 })

const data = await getReportData(session.user.companyId)
const filters = parseReportFilters(sp)
const data = await getReportData(session.user.companyId, filters)
const csv = reportCsv(section, data)

return new Response(csv, {
Expand Down
Loading