Skip to content

Commit 92d4557

Browse files
authored
Merge pull request #528 from CivicDataLab/analytics-map-view
feat(analytics): Enhance module configuration and view management
2 parents c7738fb + 51d4274 commit 92d4557

20 files changed

Lines changed: 363 additions & 246 deletions

app/sitemap.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { MetadataRoute } from 'next';
22

33
import { features, locales, siteUrl, states } from '@/config/site';
4-
import { getActiveModules } from '@/lib/analytics/module-config';
4+
import { getActiveModules, isModuleViewEnabled } from '@/lib/analytics/module-config';
55
import { fetchDatasets } from '@/lib/api';
66
import { ANALYTICS_VIEWS, routes } from '@/lib/routes';
77
import { type JsonScalar } from '@/lib/types';
@@ -30,6 +30,11 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
3030
const modules = getActiveModules(state.slug);
3131
for (const analyticsModule of modules) {
3232
for (const view of ANALYTICS_VIEWS) {
33+
if (
34+
!isModuleViewEnabled(state.slug, analyticsModule.slug, view)
35+
) {
36+
continue;
37+
}
3338
entries.push({
3439
url: `${siteUrl}/${locale}${routes.analytics(state.slug, analyticsModule.slug, { view })}`,
3540
lastModified,

components/analytics/analytics-layout.tsx

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@ import {
2323
type IndicatorCategory,
2424
type State,
2525
} from '@/config/graphql/analaytics-queries';
26-
import { features } from '@/config/site';
2726
import { getFactorRole } from '@/lib/analytics/factor-role';
2827
import { getRootIndicatorSlug } from '@/lib/analytics/root-indicator';
2928
import { getFactorNameBySlug, getLatestDate } from '@/lib/analytics/utils';
29+
import {
30+
hasSubDistrictSupport,
31+
isModuleViewEnabled,
32+
} from '@/lib/analytics/module-config';
3033
import { GraphQL } from '@/lib/api';
31-
import { hasSubDistrictSupport } from '@/lib/state-map-config';
34+
import { ANALYTICS_VIEWS } from '@/lib/routes';
3235
import { type JsonScalar } from '@/lib/types';
3336
import { cn } from '@/lib/utils';
3437
import Icons from '@/components/icons';
@@ -194,6 +197,22 @@ export function AnalyticsMainLayout() {
194197
return new Set(cumulative);
195198
}, [indicatorsByCategoryData?.data?.indicatorsByCategory, rootIndicatorSlug]);
196199

200+
const isChartViewEnabled = isModuleViewEnabled(
201+
stateSlug,
202+
analyticsModule,
203+
'chart'
204+
);
205+
206+
useEffect(() => {
207+
if (!view || view === 'map') return;
208+
if (
209+
isModuleViewEnabled(stateSlug, analyticsModule, view as 'chart' | 'table')
210+
) {
211+
return;
212+
}
213+
setView('map', { shallow: true });
214+
}, [view, stateSlug, analyticsModule, setView]);
215+
197216
// Keep govt-response subindicator compatible with selected view:
198217
// - map/table view => cumulative (*-fy-cumsum)
199218
// - chart view => monthly (without -fy-cumsum)
@@ -575,8 +594,7 @@ export function AnalyticsMainLayout() {
575594
useEffect(() => {
576595
if (!isMapView) return;
577596
const hasDistrictOrSubDistrict =
578-
Boolean(districtCode) ||
579-
(withSubDistrictSupport && Boolean(revenueCode));
597+
Boolean(districtCode) || (withSubDistrictSupport && Boolean(revenueCode));
580598
if (hasDistrictOrSubDistrict) {
581599
setIsOutputPaneOpen(true);
582600
}
@@ -594,27 +612,26 @@ export function AnalyticsMainLayout() {
594612
indicator
595613
);
596614

597-
const viewTabs = [
598-
{
599-
value: 'map',
600-
icon: Icons.IconMap,
601-
label: t('views.long.map'),
602-
},
603-
...(features.chart
604-
? [
605-
{
606-
value: 'chart',
607-
icon: Icons.IconChartBar,
608-
label: t('views.long.chart'),
609-
},
610-
]
611-
: []),
612-
{
613-
value: 'table',
614-
icon: Icons.IconTableAlias,
615-
label: t('views.long.table'),
616-
},
617-
];
615+
const viewTabs = ANALYTICS_VIEWS.filter((tabValue) =>
616+
isModuleViewEnabled(stateSlug, analyticsModule, tabValue)
617+
).map((tabValue) => {
618+
const icons = {
619+
map: Icons.IconMap,
620+
chart: Icons.IconChartBar,
621+
table: Icons.IconTableAlias,
622+
} as const;
623+
const labels = {
624+
map: t('views.long.map'),
625+
chart: t('views.long.chart'),
626+
table: t('views.long.table'),
627+
} as const;
628+
629+
return {
630+
value: tabValue,
631+
icon: icons[tabValue],
632+
label: labels[tabValue],
633+
};
634+
});
618635

619636
if (!currentSelectedState) {
620637
return (
@@ -811,7 +828,7 @@ export function AnalyticsMainLayout() {
811828
)}
812829
</div>
813830
</TabPanel>
814-
{features.chart && (
831+
{isChartViewEnabled && (
815832
<TabPanel value="chart">
816833
{/* <div className=" mt-2 h-[calc(100dvh_-_140px)]"> */}
817834
<div className="mt-2 h-full overflow-hidden">

components/analytics/analytics-mobile-layout.tsx

Lines changed: 53 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ import {
1616
type State,
1717
} from '@/config/graphql/analaytics-queries';
1818
import { features } from '@/config/site';
19+
import {
20+
hasSubDistrictSupport,
21+
isModuleReportDownloadable,
22+
isModuleViewEnabled,
23+
} from '@/lib/analytics/module-config';
1924
import { getLatestDate } from '@/lib/analytics/utils';
20-
import { routes } from '@/lib/routes';
21-
import { hasSubDistrictSupport } from '@/lib/state-map-config';
25+
import { ANALYTICS_VIEWS, routes } from '@/lib/routes';
2226
import { type JsonScalar } from '@/lib/types';
2327
import { cn, downloadStateReport } from '@/lib/utils';
2428
import Icons from '@/components/icons';
@@ -74,6 +78,10 @@ export function AnalyticsMobileLayout({
7478
currentSelectedState?.slug,
7579
analyticsModule
7680
);
81+
const downloadReportEnabled = isModuleReportDownloadable(
82+
currentSelectedState.slug,
83+
analyticsModule
84+
);
7785
const copyURL = useCopyURL();
7886
//Remove default page scroll to make only the content scrollable
7987
useLockBody();
@@ -88,41 +96,45 @@ export function AnalyticsMobileLayout({
8896
parseAsString.withDefault(timePeriod)
8997
);
9098

99+
const viewButtonConfig = {
100+
map: { icon: Icons.IconMap, title: t('views.map') },
101+
chart: { icon: Icons.IconChartBar, title: t('views.chart') },
102+
table: { icon: Icons.IconTableAlias, title: t('views.table') },
103+
} as const;
104+
91105
const buttons = [
92-
{
93-
icon: Icons.IconMap,
94-
title: t('views.map'),
95-
value: 'map',
96-
disabled: false,
97-
},
98-
...(features.chart
99-
? [
100-
{
101-
icon: Icons.IconChartBar,
102-
title: t('views.chart'),
103-
value: 'chart',
104-
disabled: false,
105-
},
106-
]
107-
: []),
108-
{
109-
icon: Icons.IconTableAlias,
110-
title: t('views.table'),
111-
value: 'table',
112-
disabled: false,
113-
},
106+
...ANALYTICS_VIEWS.filter((tabValue) =>
107+
isModuleViewEnabled(currentSelectedState.slug, analyticsModule, tabValue)
108+
).map((tabValue) => ({
109+
...viewButtonConfig[tabValue],
110+
value: tabValue,
111+
})),
114112
{
115113
icon: Icons.IconDots,
116114
title: t('views.more'),
117115
value: 'more',
118-
disabled: false,
119116
},
120117
];
121118

122119
const [view, setView] = useQueryState(
123120
'view',
124121
parseAsString.withDefault('map')
125122
);
123+
124+
React.useEffect(() => {
125+
if (!view || view === 'map' || view === 'more') return;
126+
if (
127+
isModuleViewEnabled(
128+
currentSelectedState.slug,
129+
analyticsModule,
130+
view as 'chart' | 'table'
131+
)
132+
) {
133+
return;
134+
}
135+
setView('map', { shallow: true });
136+
}, [view, currentSelectedState.slug, analyticsModule, setView]);
137+
126138
const searchParams = useSearchParams();
127139
const isMapView = !view || view === 'map';
128140
const districtCode = searchParams.get('district-code');
@@ -244,6 +256,15 @@ export function AnalyticsMobileLayout({
244256
);
245257

246258
case 'chart':
259+
if (
260+
!isModuleViewEnabled(
261+
currentSelectedState.slug,
262+
analyticsModule,
263+
'chart'
264+
)
265+
) {
266+
return null;
267+
}
247268
return (
248269
<div className="pt-[84px]">
249270
<ChartView
@@ -353,28 +374,21 @@ export function AnalyticsMobileLayout({
353374
trigger={
354375
<Button
355376
size="slim"
356-
className={cn(
357-
'basis-1/3 border-t-1 py-4',
358-
button.disabled && 'cursor-not-allowed opacity-50'
359-
)}
377+
className="basis-1/3 border-t-1 py-4"
360378
kind="tertiary"
361-
disabled={button.disabled}
362379
>
363380
<div className="flex flex-col items-center justify-center gap-1 bg-baseIndigoSolid1 ">
364381
<Icon
365382
source={button.icon}
366383
size={24}
367384
stroke={activeButton === button.value ? 3 : 2}
368-
className={button.disabled ? 'opacity-50' : ''}
369385
/>
370386
<Text
371387
variant="headingMd"
372388
fontWeight={
373389
activeButton === button.value ? 'bold' : 'medium'
374390
}
375-
className={
376-
button.disabled ? 'opacity-50' : 'text-textSubdued'
377-
}
391+
className="text-textSubdued"
378392
>
379393
{button.title}
380394
</Text>
@@ -393,6 +407,7 @@ export function AnalyticsMobileLayout({
393407
{
394408
content: t('actions.download.label'),
395409
icon: Icons.download,
410+
disabled: !downloadReportEnabled,
396411
onAction: () => {
397412
const confirmation = window.confirm(
398413
t('actions.download.confirm', {
@@ -422,32 +437,23 @@ export function AnalyticsMobileLayout({
422437
<Button
423438
key={index}
424439
size="slim"
425-
className={cn(
426-
'basis-1/3 border-t-1 py-4',
427-
button.disabled && 'cursor-not-allowed opacity-50'
428-
)}
440+
className="basis-1/3 border-t-1 py-4"
429441
kind="tertiary"
430442
onClick={() => {
431-
if (!button.disabled) {
432-
setActiveButton(button.value);
433-
setView(button.value, { shallow: false });
434-
}
443+
setActiveButton(button.value);
444+
setView(button.value, { shallow: false });
435445
}}
436-
disabled={button.disabled}
437446
>
438447
<div className="flex flex-col items-center justify-center gap-1 bg-baseIndigoSolid1 ">
439448
<Icon
440449
source={button.icon}
441450
size={24}
442451
stroke={activeButton === button.value ? 3 : 2}
443-
className={button.disabled ? 'opacity-50' : ''}
444452
/>
445453
<Text
446454
variant="headingMd"
447455
fontWeight={activeButton === button.value ? 'bold' : 'medium'}
448-
className={
449-
button.disabled ? 'opacity-50' : 'text-textSubdued'
450-
}
456+
className="text-textSubdued"
451457
>
452458
{button.title}
453459
</Text>

components/analytics/factor-list.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,19 @@ import {
1616
type State,
1717
} from '@/config/graphql/analaytics-queries';
1818
import { features } from '@/config/site';
19+
import { getFactorIcon } from '@/lib/analytics/factor-icon';
1920
import {
2021
areAllIndicatorLeaves,
2122
collectBranchSlugs,
2223
groupIndicatorsByCategory,
2324
shouldShowIndicatorCategories,
2425
} from '@/lib/analytics/indicator-tree';
26+
import { isModuleReportDownloadable } from '@/lib/analytics/module-config';
27+
import { isRootRiskIndicator } from '@/lib/analytics/root-indicator';
2528
import { getLatestDate } from '@/lib/analytics/utils';
2629
import { GraphQL } from '@/lib/api';
2730
import { routes } from '@/lib/routes';
2831
import { cn, downloadStateReport } from '@/lib/utils';
29-
import { getFactorIcon } from '@/lib/analytics/factor-icon';
30-
import { isRootRiskIndicator } from '@/lib/analytics/root-indicator';
3132
import Icons from '@/components/icons';
3233
import { MediaRendering } from '@/components/media-rendering';
3334
import RadioButton from './RadioButton';
@@ -53,7 +54,6 @@ function isDescendantSelected(
5354
);
5455
}
5556

56-
5757
export function FactorList({ currentState }: { currentState: State }) {
5858
const t = useTranslations('analytics');
5959
const tCommon = useTranslations('common');
@@ -163,6 +163,11 @@ export function FactorList({ currentState }: { currentState: State }) {
163163
return options;
164164
};
165165

166+
const downloadButtonDisabled = !isModuleReportDownloadable(
167+
currentState.slug,
168+
analyticsModule
169+
);
170+
166171
return (
167172
<>
168173
<MediaRendering minWidth={null} maxWidth="1023">
@@ -263,6 +268,7 @@ export function FactorList({ currentState }: { currentState: State }) {
263268
<Button
264269
className="self-start"
265270
onClick={async () => {
271+
if (downloadButtonDisabled) return;
266272
const confirmation = window.confirm(
267273
t('actions.download.confirm', {
268274
name: stateName(currentState.slug, currentState.name),
@@ -304,13 +310,24 @@ export function FactorList({ currentState }: { currentState: State }) {
304310
}}
305311
monochrome={true}
306312
kind="tertiary"
307-
308-
// disabled={downloadReportLoading}
313+
disabled={downloadButtonDisabled}
309314
>
310-
<div className="flex items-center gap-2">
311-
<Icon source={Icons.download} />
312-
313-
<Text variant="bodyMd">{t('actions.download.label')}</Text>
315+
<div className="relative flex w-full items-center gap-2">
316+
<Icon
317+
source={Icons.download}
318+
color={downloadButtonDisabled ? 'disabled' : 'default'}
319+
/>
320+
<Text
321+
variant="bodyMd"
322+
color={downloadButtonDisabled ? 'disabled' : 'default'}
323+
>
324+
{t('actions.download.label')}
325+
</Text>
326+
{downloadButtonDisabled && (
327+
<span className="font-medium py-0.2 absolute -right-24 top-0 inline-flex items-center gap-x-1.5 rounded-full bg-surfaceDefault px-1 text-[12px] text-textDefault">
328+
{t('views.comingSoon')}
329+
</span>
330+
)}
314331
</div>
315332
</Button>
316333
))}

0 commit comments

Comments
 (0)