Skip to content

Commit ff2c1c2

Browse files
committed
Merge branch 'feat/history-unit-count'
2 parents ea721b8 + 1483719 commit ff2c1c2

File tree

18 files changed

+486
-230
lines changed

18 files changed

+486
-230
lines changed

app/src/app/api/reports/history-changes/route.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

app/src/app/import/jobs/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,9 @@ export default function ImportJobsPage() {
565565
{showProgress && (
566566
<div className="flex items-center space-x-2">
567567
<Progress value={import_completed_pct ?? 0} className="h-1.5 flex-grow" />
568-
<span className="text-xs text-gray-500 font-mono">{Math.round(import_completed_pct ?? 0)}%</span>
568+
<span className="text-xs text-gray-500 font-mono">
569+
{(import_completed_pct ?? 0).toFixed(2).replace(/\.?0+$/, "")}%
570+
</span>
569571
</div>
570572
)}
571573
<div className="text-xs font-mono whitespace-nowrap underline">

app/src/app/reports/ReportsPageClient.tsx renamed to app/src/app/reports/drilldown/ReportsPageClient.tsx

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
"use client";
22

3-
import { useDrillDownData } from "@/app/reports/use-drill-down-data";
3+
import { useDrillDownData } from "@/app/reports/drilldown/use-drill-down-data";
44
import { useMemo, useState } from "react";
55
import { useGuardedEffect } from "@/hooks/use-guarded-effect";
6-
import { BreadCrumb } from "@/app/reports/bread-crumb";
7-
import { DrillDownChart } from "@/app/reports/drill-down-chart";
8-
import { SearchLink } from "@/app/reports/search-link";
6+
import { BreadCrumb } from "@/app/reports/drilldown/bread-crumb";
7+
import { DrillDownChart } from "@/app/reports/drilldown/drill-down-chart";
8+
import { SearchLink } from "@/app/reports/drilldown/search-link";
99
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
1010
import { useBaseData } from "@/atoms/base-data";
1111
import { Tables } from "@/lib/database.types";
1212
import { Skeleton } from "@/components/ui/skeleton";
13+
import { getUnitTypeLabel, UnitTypeTabs } from "@/app/reports/unit-type-tabs";
14+
1315

1416
export default function ReportsPageClient({
1517
}) {
@@ -18,6 +20,8 @@ export default function ReportsPageClient({
1820
const {
1921
drillDown,
2022
isLoading,
23+
selectedUnitType,
24+
setSelectedUnitType,
2125
region,
2226
setRegion,
2327
activityCategory,
@@ -43,16 +47,22 @@ export default function ReportsPageClient({
4347
});
4448
}, [], 'ReportsPageClient:importHighchartsModules');
4549

50+
const unitTypeLabel = getUnitTypeLabel(selectedUnitType) ?? "Units";
51+
4652
const statisticalVariables = useMemo(() => {
4753
return [
48-
{ value: "count", label: "Count", title: "Number of enterprises" },
54+
{
55+
value: "count",
56+
label: "Count",
57+
title: `Number of ${unitTypeLabel}`,
58+
},
4959
...(statDefinitions.map(({ code, name }: Tables<'stat_definition_enabled'>) => ({
5060
value: code!,
5161
label: name!,
5262
title: name!,
5363
})) ?? []),
5464
];
55-
}, [statDefinitions]);
65+
}, [statDefinitions, unitTypeLabel]);
5666

5767
// Calculate max values only for unfiltered top-level data
5868
useGuardedEffect(() => {
@@ -96,21 +106,27 @@ export default function ReportsPageClient({
96106
}, [drillDown, region, activityCategory, statisticalVariables], 'ReportsPageClient:calculateMaxValues');
97107

98108
return (
99-
<main className="mx-auto flex w-full max-w-5xl flex-col px-2 py-8 md:py-12">
109+
<div className="mx-auto flex w-full max-w-5xl flex-col px-2">
100110
<h1 className="mb-3 text-center text-2xl">Statbus Data Drilldown</h1>
101-
<p className="mb-12 text-center">
111+
<p className="mb-4 text-center text-gray-600">
102112
Gain data insights by drilling through the bar charts below
103113
</p>
104-
<div className="w-full space-y-8">
105-
<Tabs defaultValue="count">
106-
<TabsList className="mx-auto">
107-
{statisticalVariables.map((statisticalVariable) => (
114+
<div className="w-full space-y-1">
115+
<div className="flex flex-col p-2">
116+
<UnitTypeTabs
117+
value={selectedUnitType}
118+
onValueChange={setSelectedUnitType}
119+
/>
120+
</div>
121+
<Tabs defaultValue="count" className="p-2">
122+
<TabsList className="flex gap-1 w-fit rounded-full">
123+
{statisticalVariables.map((option) => (
108124
<TabsTrigger
109-
key={statisticalVariable.value}
110-
value={statisticalVariable.value}
111-
className="capitalize"
125+
key={option.value}
126+
value={option.value}
127+
className="capitalize rounded-full data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-50 data-[state=active]:shadow-sm hover:text-zinc-800"
112128
>
113-
{statisticalVariable.label}
129+
{option.label}
114130
</TabsTrigger>
115131
))}
116132
</TabsList>
@@ -135,7 +151,10 @@ export default function ReportsPageClient({
135151
onSelect={setRegion}
136152
variable={statisticalVariable.value}
137153
title={statisticalVariable.title}
138-
maxTopLevelValue={maxStatValuesNoFiltering[statisticalVariable.value]?.region}
154+
maxTopLevelValue={
155+
maxStatValuesNoFiltering[statisticalVariable.value]
156+
?.region
157+
}
139158
/>
140159
) : (
141160
<Skeleton className="w-full h-[200px]" />
@@ -163,7 +182,10 @@ export default function ReportsPageClient({
163182
onSelect={setActivityCategory}
164183
variable={statisticalVariable.value}
165184
title={statisticalVariable.title}
166-
maxTopLevelValue={maxStatValuesNoFiltering[statisticalVariable.value]?.activity}
185+
maxTopLevelValue={
186+
maxStatValuesNoFiltering[statisticalVariable.value]
187+
?.activity
188+
}
167189
/>
168190
) : (
169191
<Skeleton className="w-full h-[200px]" />
@@ -178,11 +200,15 @@ export default function ReportsPageClient({
178200
</div>
179201
</TabsContent>
180202
))}
203+
<div className="flex justify-end space-y-6 bg-gray-100 p-6 mt-4">
204+
<SearchLink
205+
region={region}
206+
activityCategory={activityCategory}
207+
unitType={selectedUnitType}
208+
/>
209+
</div>
181210
</Tabs>
182-
<div className="flex justify-end space-y-6 bg-gray-100 p-6">
183-
<SearchLink region={region} activityCategory={activityCategory} />
184-
</div>
185211
</div>
186-
</main>
212+
</div>
187213
);
188214
}
File renamed without changes.
File renamed without changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Metadata } from "next";
2+
import { Suspense } from "react";
3+
import ReportsPageClient from "./ReportsPageClient";
4+
5+
export const metadata: Metadata = {
6+
title: "Reports Drilldown",
7+
};
8+
9+
export default function ReportsDrilldownPage() {
10+
return (
11+
<Suspense fallback={<div>Loading...</div>}>
12+
<ReportsPageClient />
13+
</Suspense>
14+
);
15+
}

app/src/app/reports/search-link.tsx renamed to app/src/app/reports/drilldown/search-link.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import {
1010
export const SearchLink = ({
1111
region,
1212
activityCategory,
13+
unitType,
1314
}: {
1415
readonly region: DrillDownPoint | null;
1516
readonly activityCategory: DrillDownPoint | null;
17+
readonly unitType: UnitType;
1618
}) => {
1719
const searchParams = new URLSearchParams();
1820

@@ -24,7 +26,7 @@ export const SearchLink = ({
2426
searchParams.set(ACTIVITY_CATEGORY_PATH, activityCategory.path);
2527
}
2628

27-
searchParams.set(UNIT_TYPE, "enterprise");
29+
searchParams.set(UNIT_TYPE, unitType);
2830

2931
return (
3032
<Button asChild>

app/src/app/reports/use-drill-down-data.tsx renamed to app/src/app/reports/drilldown/use-drill-down-data.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import { useState } from "react";
44
import { useTimeContext } from '@/atoms/app-derived';
55
import { fetchWithAuthRefresh } from "@/context/RestClientStore";
66
import { useSWRWithAuthRefresh, JwtExpiredError } from "@/hooks/use-swr-with-auth-refresh";
7+
import { useGuardedEffect } from "@/hooks/use-guarded-effect";
78

89
export const useDrillDownData = () => {
910
const { selectedTimeContext } = useTimeContext();
11+
const [selectedUnitType, setSelectedUnitType] =
12+
useState<UnitType>("enterprise");
1013
const [region, setRegion] = useState<DrillDownPoint | null>(null);
1114
const [activityCategory, setActivityCategory] =
1215
useState<DrillDownPoint | null>(null);
@@ -25,6 +28,17 @@ export const useDrillDownData = () => {
2528
urlSearchParams.set("valid_on", selectedTimeContext.valid_on);
2629
}
2730

31+
urlSearchParams.set("unit_type", selectedUnitType);
32+
33+
useGuardedEffect(
34+
() => {
35+
setRegion(null);
36+
setActivityCategory(null);
37+
},
38+
[selectedUnitType],
39+
"useDrillDownData:resetSelectionsOnUnitType"
40+
);
41+
2842
// Don't fetch until time context is ready — avoids a wasted ~5s request
2943
// with no valid_on that gets thrown away when selectedTimeContext arrives.
3044
const swrKey = selectedTimeContext?.valid_on
@@ -65,6 +79,8 @@ export const useDrillDownData = () => {
6579
return {
6680
drillDown,
6781
isLoading,
82+
selectedUnitType,
83+
setSelectedUnitType,
6884
region,
6985
setRegion,
7086
activityCategory,
Lines changed: 20 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,28 @@
11
"use client";
22

3-
import { useMemo, useState } from "react";
4-
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
5-
import { useStatisticalHistoryChanges } from "./use-statistical-history-changes";
3+
import { HistoryReports } from "../history-reports";
64
import { HistoryChangesChart } from "./history-changes-chart";
7-
import { Skeleton } from "@/components/ui/skeleton";
8-
import { useTimeContext } from "@/atoms/app-derived";
9-
import { Enums } from "@/lib/database.types";
10-
import {
11-
Select,
12-
SelectContent,
13-
SelectItem,
14-
SelectTrigger,
15-
SelectValue,
16-
} from "@/components/ui/select";
17-
18-
const unitTypes: { value: UnitType; label: string }[] = [
19-
{ value: "enterprise", label: "Enterprises" },
20-
{ value: "legal_unit", label: "Legal Units" },
21-
{ value: "establishment", label: "Establishments" },
22-
];
235

246
export default function HistoryChangesPage() {
25-
const { timeContexts } = useTimeContext();
26-
const [selectedUnitType, setSelectedUnitType] =
27-
useState<UnitType>("enterprise");
28-
const [selectedYear, setSelectedYear] = useState<string>("all");
29-
const resolution: Enums<"history_resolution"> =
30-
selectedYear === "all" ? "year" : "year-month";
31-
32-
const availableYears = useMemo(() => {
33-
const years = timeContexts
34-
.map((tc) =>
35-
tc.valid_from ? new Date(tc.valid_from).getFullYear() : null
36-
)
37-
.filter((year): year is number => year !== null);
38-
39-
return Array.from(new Set(years)).sort((a, b) => b - a);
40-
}, [timeContexts]);
41-
42-
const { history, isLoading } = useStatisticalHistoryChanges(
43-
selectedUnitType,
44-
resolution,
45-
[
46-
"births",
47-
"deaths",
48-
"name_change_count",
49-
"primary_activity_category_change_count",
50-
"physical_region_change_count",
51-
],
52-
selectedYear != "all" ? parseInt(selectedYear, 10) : undefined
53-
);
54-
55-
const handleYearSelect = (year: number) => {
56-
setSelectedYear(year.toString());
57-
};
58-
59-
607
return (
61-
<main className="mx-auto flex w-full max-w-5xl flex-col px-2 py-8 md:py-12">
62-
<h1 className="mb-3 text-center text-2xl">Changes over time</h1>
63-
<p className="mb-12 text-center">
64-
Annual overview of births, deaths, name changes, and other changes
65-
</p>
66-
<div className="w-full space-y-8">
67-
<div className="flex justify-between">
68-
<Tabs
69-
value={selectedUnitType}
70-
onValueChange={(value) => setSelectedUnitType(value as UnitType)}
71-
>
72-
<TabsList>
73-
{unitTypes.map((unitType) => (
74-
<TabsTrigger key={unitType.value} value={unitType.value}>
75-
{unitType.label}
76-
</TabsTrigger>
77-
))}
78-
</TabsList>
79-
</Tabs>
80-
<Select value={selectedYear} onValueChange={setSelectedYear}>
81-
<SelectTrigger className="w-[180px]">
82-
<SelectValue placeholder="Select Year" />
83-
</SelectTrigger>
84-
<SelectContent>
85-
<SelectItem value="all">All Years</SelectItem>
86-
{availableYears.map((year) => (
87-
<SelectItem key={year} value={year.toString()}>
88-
{year}
89-
</SelectItem>
90-
))}
91-
</SelectContent>
92-
</Select>
93-
</div>
94-
<div>
95-
{!isLoading && history ? (
96-
<HistoryChangesChart
97-
history={history}
98-
isYearlyView={selectedYear === "all"}
99-
onYearSelect={handleYearSelect}
100-
/>
101-
) : (
102-
<Skeleton className="w-full h-[400px]" />
103-
)}
104-
</div>
105-
</div>
106-
</main>
8+
<HistoryReports
9+
title="Changes over time"
10+
subtitle="Annual overview of births, deaths, name changes, and other changes"
11+
seriesCodes={[
12+
"births",
13+
"deaths",
14+
"name_change_count",
15+
"primary_activity_category_change_count",
16+
"physical_region_change_count",
17+
]}
18+
>
19+
{({ history, isYearlyView, onYearSelect }) => (
20+
<HistoryChangesChart
21+
history={history}
22+
isYearlyView={isYearlyView}
23+
onYearSelect={onYearSelect}
24+
/>
25+
)}
26+
</HistoryReports>
10727
);
10828
}

0 commit comments

Comments
 (0)