Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
21 changes: 21 additions & 0 deletions app/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Link from "next/link";

export default function AppLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className="min-h-screen bg-slate-50">
<header className="border-b border-slate-200 bg-white">
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-4 sm:px-6">
<Link href="/" className="text-lg font-semibold text-slate-900">
ScoutBase
</Link>
<span className="text-sm text-slate-500">Premises compliance</span>
</div>
</header>
<main className="mx-auto max-w-6xl px-4 py-8 sm:px-6">{children}</main>
</div>
);
}
46 changes: 46 additions & 0 deletions app/(app)/premises/[id]/actions/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Suspense } from "react";
import { notFound, redirect } from "next/navigation";
import { ActionTracker } from "@/components/actions/ActionTracker";
import { PremisesPageHeader } from "@/components/premises/PremisesPageHeader";
import { requireAuthContext } from "@/lib/auth";
import { getPremisesForOrganisation } from "@/lib/premises";

type PageProps = {
params: { id: string };
};

export default async function PremisesActionsPage({ params }: PageProps) {
let auth;
try {
auth = await requireAuthContext();
} catch {
redirect("/");
}

const premises = await getPremisesForOrganisation(
params.id,
auth.organisationId,
);

if (!premises) {
notFound();
}

return (
<div>
<PremisesPageHeader
premisesId={premises.id}
premisesName={premises.name}
address={premises.address}
eyebrow="Actions tracker"
description="View and manage follow-up actions raised during audits. Filter by status, priority, and due date."
/>

<div className="mt-8">
<Suspense fallback={<p className="text-sm text-slate-500">Loading actions…</p>}>
<ActionTracker premisesId={premises.id} />
</Suspense>
</div>
</div>
);
}
72 changes: 72 additions & 0 deletions app/(app)/premises/[id]/audit/[auditId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import { AuditWizard } from "@/components/audit/AuditWizard";
import { requireAuthContext } from "@/lib/auth";
import { buildAuditOverview, getAuditForOrganisation } from "@/lib/audit";

type PageProps = {
params: { id: string; auditId: string };
searchParams: { section?: string };
};

export default async function AuditWizardPage({
params,
searchParams,
}: PageProps) {
let auth;
try {
auth = await requireAuthContext();
} catch {
redirect("/");
}

const audit = await getAuditForOrganisation(params.auditId, auth.organisationId);

if (!audit || audit.premises.id !== params.id) {
notFound();
}

const overview = buildAuditOverview(audit);
const initialSectionId =
searchParams.section &&
overview.sections.some((section) => section.id === searchParams.section)
? searchParams.section
: overview.sections[0]?.id;

if (!initialSectionId) {
return (
<div className="rounded-xl border border-amber-200 bg-amber-50 p-6">
<h2 className="text-lg font-semibold text-amber-950">
This audit needs to be restarted
</h2>
<p className="mt-2 text-sm text-amber-900">
No audit sections apply to the current premises profile. Update your
profile or start a new audit.
</p>
<div className="mt-4 flex flex-wrap gap-3">
<Link
href={`/premises/${params.id}/profile`}
className="inline-flex rounded-lg bg-amber-800 px-4 py-2 text-sm font-medium text-white hover:bg-amber-900"
>
Review premises profile
</Link>
<Link
href={`/premises/${params.id}/audit`}
className="inline-flex rounded-lg border border-amber-300 px-4 py-2 text-sm font-medium text-amber-950 hover:bg-amber-100"
>
Back to audit
</Link>
</div>
</div>
);
}

return (
<AuditWizard
premisesId={params.id}
auditId={params.auditId}
initialOverview={overview}
initialSectionId={initialSectionId}
/>
);
}
54 changes: 54 additions & 0 deletions app/(app)/premises/[id]/audit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { notFound, redirect } from "next/navigation";
import { AuditStatus } from "@prisma/client";
import { AuditStartPanel } from "@/components/audit/AuditStartPanel";
import { PremisesPageHeader } from "@/components/premises/PremisesPageHeader";
import { requireAuthContext } from "@/lib/auth";
import { getPremisesForOrganisation } from "@/lib/premises";
import { prisma } from "@/lib/prisma";

type PageProps = {
params: { id: string };
};

export default async function PremisesAuditPage({ params }: PageProps) {
let auth;
try {
auth = await requireAuthContext();
} catch {
redirect("/");
}

const premises = await getPremisesForOrganisation(params.id, auth.organisationId);

if (!premises) {
notFound();
}

const draft = await prisma.audit.findFirst({
where: {
premisesId: premises.id,
status: AuditStatus.DRAFT,
},
select: { id: true },
});

return (
<div>
<PremisesPageHeader
premisesId={premises.id}
premisesName={premises.name}
address={premises.address}
eyebrow="Annual safety audit"
/>

<div className="mt-8">
<AuditStartPanel
premisesId={premises.id}
premisesName={premises.name}
hasProfile={Boolean(premises.profile)}
draftAuditId={draft?.id ?? null}
/>
</div>
</div>
);
}
87 changes: 87 additions & 0 deletions app/(app)/premises/[id]/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { notFound, redirect } from "next/navigation";
import Link from "next/link";
import { requireAuthContext } from "@/lib/auth";
import { getPremisesForOrganisation } from "@/lib/premises";
import { EMPTY_PROFILE_FORM } from "@/lib/profile-labels";
import { ProfileWizard } from "@/components/profile/ProfileWizard";
import { PremisesPageHeader } from "@/components/premises/PremisesPageHeader";
import type { ProfileFormData } from "@/lib/profile-labels";
import type { BuildingAgeBand, FloodRiskZone, OwnershipType } from "@prisma/client";

type PageProps = {
params: { id: string };
};

function toFormData(profile: {
ownershipType: OwnershipType;
buildingAgeBand: BuildingAgeBand;
hasGas: boolean;
floodRiskZone: FloodRiskZone | null;
hasCateringKitchen: boolean;
hasGrounds: boolean;
hasSleeping: boolean;
hasVehicles: boolean;
hasPlantMachinery: boolean;
hasThirdPartyUsers: boolean;
}): ProfileFormData {
return {
ownershipType: profile.ownershipType,
buildingAgeBand: profile.buildingAgeBand,
hasGas: profile.hasGas,
floodRiskZone: profile.floodRiskZone,
hasCateringKitchen: profile.hasCateringKitchen,
hasGrounds: profile.hasGrounds,
hasSleeping: profile.hasSleeping,
hasVehicles: profile.hasVehicles,
hasPlantMachinery: profile.hasPlantMachinery,
hasThirdPartyUsers: profile.hasThirdPartyUsers,
};
}

export default async function PremisesProfilePage({ params }: PageProps) {
let auth;
try {
auth = await requireAuthContext();
} catch {
redirect("/");
}

const premises = await getPremisesForOrganisation(params.id, auth.organisationId);

if (!premises) {
notFound();
}

const initialProfile = premises.profile
? toFormData(premises.profile)
: EMPTY_PROFILE_FORM;

const initialApplicableSections = premises.profile?.applicableSections ?? [];

return (
<div>
<PremisesPageHeader
premisesId={premises.id}
premisesName={premises.name}
address={premises.address}
eyebrow="Premises profile"
description="Answer a few questions about your premises. We'll tailor the annual safety audit to what applies to your building — usually about five minutes."
/>
{premises.profile ? (
<Link
href={`/premises/${premises.id}/audit`}
className="mt-4 inline-flex text-sm font-medium text-emerald-700 hover:text-emerald-800"
>
Go to annual audit →
</Link>
) : null}
<div className="mt-8">
<ProfileWizard
premisesId={premises.id}
initialProfile={initialProfile}
initialApplicableSections={initialApplicableSections}
/>
</div>
</div>
);
}
Loading