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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ coverage
.env
.env.local
.env.*.local
.infisical.json
.trigger
2 changes: 1 addition & 1 deletion apps/web/app/_components/search-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type SearchFormProps = {
export function SearchForm({
defaultValue = "",
action = "/",
placeholder = "Search ministers, portfolios, and departments",
placeholder = "Search ministers, offices, and departments",
size = "lg",
}: SearchFormProps) {
return (
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/_lib/presenters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export function humanizeSourceFamily(sourceFamily: string): string {

export function describeRelationship(entry: RelatedNode): string {
switch (entry.edgeType) {
case "HOLDS_PORTFOLIO":
return entry.direction === "outgoing" ? "Holds portfolio" : "Held by";
case "HOLDS_OFFICE":
return entry.direction === "outgoing" ? "Holds office" : "Held by";
case "MEMBER_OF_MINISTRY":
return entry.direction === "outgoing" ? "Member of" : "Includes";
case "SUPPORTED_BY_DEPARTMENT":
Expand Down
12 changes: 6 additions & 6 deletions apps/web/app/design/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default function DesignPage() {
<Badge tone="warning">Warning</Badge>
</div>
<div className="space-y-[var(--gg-space-3)]">
<Input placeholder="Search ministers, portfolios, and departments" />
<Input placeholder="Search ministers, offices, and departments" />
<Textarea placeholder="Notes, source caveats, or editorial guidance." />
</div>
</Card>
Expand Down Expand Up @@ -148,7 +148,7 @@ export default function DesignPage() {
<Badge tone="muted">Lead delivery agency</Badge>
</>
}
description="Supports the education portfolio, child policy, and statewide school governance."
description="Supports the education office grouping, child policy, and statewide school governance."
footer={
<Link
href="/"
Expand All @@ -175,7 +175,7 @@ export default function DesignPage() {
</>
}
description="Published by Victorian Public Sector Commission • effective 2025-06-05"
metadata="Portfolio mapping"
metadata="Office mapping"
title="Working with Ministers and Ministerial Officers"
/>
</div>
Expand All @@ -189,7 +189,7 @@ export default function DesignPage() {
/>
<Card className="space-y-[var(--gg-space-5)] p-[var(--gg-space-6)]">
<dl>
<KeyValue label="Portfolio group" value="Education" />
<KeyValue label="Office group" value="Education" />
<KeyValue label="Supporting department" value="Department of Education" />
<KeyValue label="Current ministry" value="Allan Ministry" />
</dl>
Expand Down Expand Up @@ -260,9 +260,9 @@ export default function DesignPage() {
<div className="grid gap-[var(--gg-space-4)]">
<TimelineEntry
date="5 December 2024"
description="Portfolio responsibilities were regrouped under a simplified education cluster."
description="Office responsibilities were regrouped under a simplified education cluster."
detail="Machinery of Government"
title="Education portfolio updated"
title="Education office grouping updated"
/>
<TimelineEntry
date="7 February 2025"
Expand Down
199 changes: 199 additions & 0 deletions apps/web/app/general-order/[officeSlug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import type { Metadata } from "next";
import type { Route } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";

import { Badge, Card, EmptyState } from "@govgraph/ui";

import { loadGeneralOrderOffice } from "../_lib/data";
import {
humanizeAdministrationMode,
humanizeAdministrationScope,
humanizeParseStatus,
humanizeRuleKind,
summarizeProvisionReferences,
} from "../_lib/presenters";

type GeneralOrderOfficePageProps = {
params: Promise<{ officeSlug: string }>;
};

export const dynamic = "force-dynamic";

export async function generateMetadata({
params,
}: GeneralOrderOfficePageProps): Promise<Metadata> {
const { officeSlug } = await params;
const state = await loadGeneralOrderOffice(officeSlug);

if (state.status !== "ready") {
return {
title: "General Order office | GovGraph",
};
}

return {
title: `${state.detail.officeName} | General Order | GovGraph`,
description: `Act allocations recorded for ${state.detail.officeName} in the Victorian General Order.`,
};
}

export default async function GeneralOrderOfficePage({
params,
}: GeneralOrderOfficePageProps) {
const { officeSlug } = await params;
const state = await loadGeneralOrderOffice(officeSlug);

if (state.status === "missing") {
notFound();
}

return (
<main className="mx-auto flex min-h-screen w-full max-w-6xl flex-col gap-8 px-6 py-8 sm:px-8 lg:px-10 lg:py-12">
<header className="grid gap-6 rounded-[2rem] border border-[var(--gg-color-border)] bg-white/75 p-6 shadow-[0_24px_80px_rgba(14,44,36,0.08)] backdrop-blur-sm lg:grid-cols-[1.2fr_0.8fr] lg:p-8">
<div className="space-y-5">
<div className="flex flex-wrap gap-4 text-sm font-medium uppercase tracking-[0.2em] text-[var(--gg-color-semantic-text-secondary)]">
<Link href="/" className="underline-offset-4 hover:underline">
Atlas
</Link>
<span>/</span>
<Link
href={"/general-order" as Route}
className="underline-offset-4 hover:underline"
>
General Order
</Link>
</div>

{state.status === "ready" ? (
<>
<div className="flex flex-wrap gap-3">
<Badge>{state.detail.actEntryCount} Acts</Badge>
{state.detail.partialRuleCount > 0 ? (
<Badge>{state.detail.partialRuleCount} partial rules</Badge>
) : null}
{state.detail.source.effectiveDate ? (
<Badge>effective {state.detail.source.effectiveDate}</Badge>
) : null}
</div>
<div className="space-y-3">
<h1 className="max-w-4xl text-4xl font-semibold tracking-tight text-[var(--gg-color-ink)] sm:text-5xl">
{state.detail.officeName}
</h1>
<p className="max-w-3xl text-base leading-8 text-[var(--gg-color-semantic-text-secondary)] sm:text-lg">
Browse the Acts, shared provisions, and carve-outs recorded
for this office in the latest imported General Order.
</p>
</div>
</>
) : null}
</div>

{state.status === "ready" ? (
<Card className="p-5">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-[var(--gg-color-semantic-text-secondary)]">
Source document
</p>
<h2 className="mt-4 text-xl font-semibold text-[var(--gg-color-ink)]">
{state.detail.source.title}
</h2>
<p className="mt-2 text-sm leading-6 text-[var(--gg-color-semantic-text-secondary)]">
Retrieved {state.detail.source.retrievedAt.slice(0, 10)}
</p>
<a
href={state.detail.source.sourceUrl}
className="mt-4 inline-flex text-sm font-medium text-[var(--gg-color-accent)] underline-offset-4 hover:underline"
>
Open official page
</a>
</Card>
) : null}
</header>

{state.status === "empty" ? (
<EmptyState
title="No General Order data loaded yet"
description="This office page needs the imported General Order rows in Postgres before it can show anything."
/>
) : null}

{state.status === "error" ? (
<EmptyState
title="Could not load this office"
description={`The app could not read the imported General Order data for this office. Error: ${state.message}`}
/>
) : null}

{state.status === "ready" ? (
<section className="space-y-4">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-[var(--gg-color-semantic-text-secondary)]">
Act allocations
</p>
<h2 className="mt-2 text-3xl font-semibold tracking-tight text-[var(--gg-color-ink)]">
Imported records for this office
</h2>
</div>
<div className="grid gap-4">
{state.detail.acts.map((act) => (
<Card key={`${act.officeName}-${act.headingText}`} className="p-5">
<div className="flex flex-wrap gap-3">
<Badge>{act.headingStyle}</Badge>
<Badge>{act.rules.length} rules</Badge>
</div>
<h3 className="mt-4 text-2xl font-semibold tracking-tight text-[var(--gg-color-ink)]">
{act.actName}
</h3>
<p className="mt-2 text-sm leading-6 text-[var(--gg-color-semantic-text-secondary)]">
{act.headingText}
</p>

<div className="mt-5 grid gap-4">
{act.rules.map((rule, index) => (
<div
key={`${act.headingText}-${rule.ruleKind}-${index}`}
className="rounded-[1.5rem] border border-[var(--gg-color-border)] bg-[var(--gg-color-paper)]/70 p-4"
>
<div className="flex flex-wrap gap-3">
<Badge>{humanizeRuleKind(rule.ruleKind)}</Badge>
<Badge>{humanizeAdministrationScope(rule.scope)}</Badge>
<Badge>
{humanizeAdministrationMode(rule.administrationMode)}
</Badge>
{rule.parseStatus !== "parsed" ? (
<Badge>{humanizeParseStatus(rule.parseStatus)}</Badge>
) : null}
{rule.kanonAssistance?.status === "enriched" ? (
<Badge>Kanon-assisted</Badge>
) : null}
</div>
<p className="mt-4 text-base leading-7 text-[var(--gg-color-ink)]">
{rule.rawText}
</p>
{rule.administeringOfficeNames.length > 1 ? (
<p className="mt-3 text-sm leading-6 text-[var(--gg-color-semantic-text-secondary)]">
Also involving: {rule.administeringOfficeNames.join(", ")}
</p>
) : null}
{rule.provisionReferences.length > 0 ? (
<p className="mt-2 text-sm leading-6 text-[var(--gg-color-semantic-text-secondary)]">
Provisions:{" "}
{summarizeProvisionReferences(rule.provisionReferences)}
</p>
) : null}
{rule.unparsedTail ? (
<p className="mt-2 text-sm leading-6 text-[var(--gg-color-semantic-text-secondary)]">
Unparsed tail preserved: {rule.unparsedTail}
</p>
) : null}
</div>
))}
</div>
</Card>
))}
</div>
</section>
) : null}
</main>
);
}
Loading
Loading