Skip to content

Commit 8bbaacd

Browse files
committed
fix: lab dashboard page UX UI updates
1 parent 5bbb834 commit 8bbaacd

9 files changed

Lines changed: 195 additions & 113 deletions

File tree

src/components/react/LivingLabKPIsView.tsx

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,27 @@ import type { IKpi, IIKpiResultBeforeAfter } from "../../types";
22
import { Badge, ExpansionPanel } from "./ui";
33
import type { ICategory } from "../../types/Category";
44
import { KpiCard, KpiMultiple } from "./KpiCards";
5+
import ModalSplitChart, { type SplitItem } from "./KpiCards/ModalSplitChart";
56

67
interface IKpiResultsByCategory extends ICategory {
78
kpiResults: IIKpiResultBeforeAfter[];
89
}
910
type Props = {
1011
kpis?: IKpi[];
11-
categories: IKpiResultsByCategory[];
12+
categories?: IKpiResultsByCategory[];
13+
14+
modalSplitKpis?: {
15+
kpiName: string;
16+
before: { label: string; data: SplitItem[] };
17+
after: { label: string; data: SplitItem[] };
18+
}[];
1219
};
1320

14-
export function LivingLabKPIsView({ categories = [], kpis }: Props) {
21+
export function LivingLabKPIsView({
22+
categories = [],
23+
kpis,
24+
modalSplitKpis,
25+
}: Props) {
1526
const getKpiSection = (
1627
parentKpi: IKpi,
1728
resultKpis: IIKpiResultBeforeAfter[] = []
@@ -65,6 +76,7 @@ export function LivingLabKPIsView({ categories = [], kpis }: Props) {
6576
.map(([key, parentKpi]) => (
6677
// <div className="break-inside-avoid">
6778
<div
79+
key={key}
6880
className={`break-inside-avoid ${
6981
(kpiResultsMap.get(key) ?? [])?.length > 1
7082
? "md:col-span-2"
@@ -78,6 +90,41 @@ export function LivingLabKPIsView({ categories = [], kpis }: Props) {
7890
);
7991
};
8092

93+
if (modalSplitKpis && modalSplitKpis?.length > 0) {
94+
return (
95+
<ExpansionPanel
96+
header={
97+
<div className="flex flex-row justify-center items-center gap-2 rounded-2xl border-info bg-info px-1 py-1">
98+
<h5 className="text-center">Modal Split</h5>
99+
<Badge
100+
color="light"
101+
size="sm"
102+
tooltip="Number of KPIs in this category"
103+
displayTooltipIcon={false}
104+
>
105+
{modalSplitKpis?.length || 0}
106+
</Badge>
107+
</div>
108+
}
109+
content={
110+
<>
111+
{modalSplitKpis?.length > 0 &&
112+
modalSplitKpis.map(({ kpiName, before, after }) => (
113+
<div className="flex flex-col gap-4">
114+
<h5 className="text-center mt-4">{kpiName}</h5>
115+
<div className="bg-white p-6 rounded-lg shadow-md">
116+
<ModalSplitChart data={[before, after]} />
117+
</div>
118+
</div>
119+
))}
120+
</>
121+
}
122+
arrow
123+
open={true}
124+
/>
125+
);
126+
}
127+
81128
return (
82129
<div className="flex flex-col gap-4 mx-auto w-full">
83130
{categories.map(

src/components/react/LivingLabMeasures.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type LivingLabMeasuresProps = {
1010
measures?: IProject[];
1111
implementedMeasures?: LivingLabProjectsImplementationInput[];
1212
className?: string;
13+
measuresGrids?: number;
1314
isEditable?: boolean;
1415
};
1516

@@ -19,6 +20,7 @@ export function LivingLabMeasures({
1920
implementedMeasures = [],
2021
title = "Measures",
2122
className = "",
23+
measuresGrids = 2,
2224
isEditable = false,
2325
}: LivingLabMeasuresProps) {
2426
return (
@@ -27,7 +29,7 @@ export function LivingLabMeasures({
2729
>
2830
<div className="flex-1 grid grid-cols-1 gap-4">
2931
<h4 className="text-center">{title}</h4>
30-
<div className="grid grid-cols-1 lg:grid-cols-2 mx-1 lg:mx-4 gap-4">
32+
<div className={`grid grid-cols-${measuresGrids} mx-1 lg:mx-4 gap-4`}>
3133
{measures.map((m) => (
3234
<LivingLabMeasureForm
3335
livingLabId={livingLabId}

src/components/react/form/LivingLabMeasureForm.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,14 @@ export function LivingLabMeasureForm({
230230
</RButton>
231231
</div>
232232
</form>
233-
) : checked ? (
233+
) : checked && (startAt || description) ? (
234234
<div className="flex flex-col gap-1 text-left p-5 text-primary justify-center items-start">
235-
<div className="flex flex-row">
236-
<CalendarIcon className="h-5 w-5" />
237-
<small>
238-
{startAt ? formatDateToMonthYear(startAt) : "Unknown"}
239-
</small>
240-
</div>
235+
{startAt ? (
236+
<div className="flex flex-row justify-center items-center gap-1">
237+
<CalendarIcon className="h-5 w-5" />
238+
<small>{formatDateToMonthYear(startAt)}</small>
239+
</div>
240+
) : null}
241241
<small style={{ whiteSpace: "pre-line" }}>{description}</small>
242242
</div>
243243
) : null}

src/components/react/ui/SidebarMenu.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,12 @@ const DEFAULT_USER_MENU_ITEMS = [
121121

122122
export function SidebarMenu({ children, userInfo, currentLivingLab }: Props) {
123123
const labItem = {
124-
label: currentLivingLab?.name ?? "My Living Lab",
124+
label: "Manage " + (currentLivingLab?.name ?? "My Living Lab"),
125125
icon: <MapPinIcon />,
126126
};
127127
const labMenu: MenuItem[] = [
128128
{
129-
label: "Edit",
129+
label: "Edit Lab details",
130130
icon: <Cog8ToothIcon />,
131131
href: getUrl("/lab-admin/edit"),
132132
},
@@ -140,7 +140,7 @@ export function SidebarMenu({ children, userInfo, currentLivingLab }: Props) {
140140

141141
const navbarItems: MenuItem[] = [
142142
{
143-
label: "Home",
143+
label: userInfo ? "Go to ODP website" : "Home",
144144
icon: <HomeIcon />,
145145
href: getUrl("/"),
146146
className: "max-md:hidden",
@@ -155,11 +155,11 @@ export function SidebarMenu({ children, userInfo, currentLivingLab }: Props) {
155155
];
156156
if (currentLivingLab) {
157157
if (
158-
currentLivingLab?.authorizedLabs?.length &&
158+
currentLivingLab?.authorizedLabs &&
159159
currentLivingLab?.authorizedLabs?.length > 1
160160
) {
161161
navbarItems.push({
162-
label: "Labs",
162+
label: "My Labs",
163163
icon: <GlobeEuropeAfricaIcon />,
164164
subItems: currentLivingLab?.authorizedLabs?.map((item) => ({
165165
label: item.name,
@@ -177,7 +177,7 @@ export function SidebarMenu({ children, userInfo, currentLivingLab }: Props) {
177177

178178
if (userInfo)
179179
navbarItems.push({
180-
label: userInfo?.name,
180+
label: "My account",
181181
icon: <UserCircleIcon />,
182182
subItems: DEFAULT_USER_MENU_ITEMS,
183183
className: "max-md:hidden",

src/components/react/ui/SiteNavBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function SiteNavBar({
5353
let navbarItems: MenuItem[] = menuItems ?? [];
5454
if (userInfo) {
5555
const userMenu: MenuItem = {
56-
label: userInfo?.name,
56+
label: "Manage Labs",
5757
icon: <UserCircleIcon />,
5858
subItems: [
5959
{

src/lib/helpers/living-lab.ts

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
ITransportMode,
44
IProject,
55
IKpi,
6+
IIKpiResultBeforeAfter,
67
} from "../../types";
78
import type { SplitItem } from "../../components/react/KpiCards/ModalSplitChart";
89
import type { MarkerData } from "../../components/react/MapViewer";
@@ -43,49 +44,66 @@ export function separateMeasures(measures: IProject[]): {
4344
* Prepare modal split chart data from KPI results
4445
* KPI 15.a represents modal split per transport mode
4546
*/
46-
export function prepareModalSplitData(
47-
livingLab: ILivingLabPopulated,
47+
export function getModalSplitKpiResults(
48+
kpiDefinitions: IKpi[],
49+
allTransportModes: ITransportMode[],
50+
kpiResults: IIKpiResultBeforeAfter[]
51+
): {
52+
kpiName: string;
53+
before: { label: string; data: SplitItem[] };
54+
after: { label: string; data: SplitItem[] };
55+
}[] {
56+
if (!kpiDefinitions || !allTransportModes || !kpiResults.length) {
57+
return [];
58+
}
59+
return (
60+
kpiDefinitions
61+
?.filter((kpi) => ["15.a", "15.b", "15.c"].includes(kpi.kpi_number))
62+
.map((kpi) => {
63+
const modalSplitKpiResults = kpiResults.filter(
64+
(result) => result.kpidefinition_id === kpi.id
65+
);
66+
67+
return prepareModalSplitData(
68+
modalSplitKpiResults,
69+
allTransportModes,
70+
kpi
71+
);
72+
}) || []
73+
);
74+
}
75+
76+
function prepareModalSplitData(
77+
kpiResults: IIKpiResultBeforeAfter[],
4878
allTransportModes: ITransportMode[],
49-
kpidefinitions: IKpi[]
79+
parentKpiDefinition?: IKpi
5080
): {
81+
kpiName: string;
5182
before: { label: string; data: SplitItem[] };
5283
after: { label: string; data: SplitItem[] };
5384
} {
5485
if (
55-
!livingLab.kpi_results ||
86+
kpiResults?.length === 0 ||
5687
!allTransportModes ||
5788
allTransportModes.length === 0
5889
) {
5990
return {
60-
before: { label: "Before", data: [] },
61-
after: { label: "After", data: [] },
62-
};
63-
}
64-
65-
// Find KPI 15.a results (modal split)
66-
const modalSplitKpis = livingLab.kpi_results?.filter(
67-
(kpi) =>
68-
kpidefinitions?.find((def) => def.id === kpi.kpidefinition_id)
69-
?.kpi_number === "15.a"
70-
);
71-
72-
if (modalSplitKpis.length === 0) {
73-
return {
91+
kpiName: "Modal Split",
7492
before: { label: "Before", data: [] },
7593
after: { label: "After", data: [] },
7694
};
7795
}
7896

7997
const beforeData: SplitItem[] = [];
8098
const afterData: SplitItem[] = [];
81-
const beforeLabelWithMinYear = modalSplitKpis[0].result_before?.date
82-
? `Before (${new Date(modalSplitKpis[0].result_before.date).getFullYear()})`
99+
const beforeLabelWithMinYear = kpiResults[0].result_before?.date
100+
? `Before (${new Date(kpiResults[0].result_before.date).getFullYear()})`
83101
: "Before";
84-
const afterLabelWithMinYear = modalSplitKpis[0].result_after?.date
85-
? `After (${new Date(modalSplitKpis[0].result_after.date).getFullYear()})`
102+
const afterLabelWithMinYear = kpiResults[0].result_after?.date
103+
? `After (${new Date(kpiResults[0].result_after.date).getFullYear()})`
86104
: "After";
87105

88-
modalSplitKpis.forEach((kpi) => {
106+
kpiResults.forEach((kpi) => {
89107
// Find the transport mode for this KPI result
90108
const transportMode = allTransportModes.find(
91109
(tm) => tm.id === kpi.result_before?.transport_mode_id
@@ -111,6 +129,7 @@ export function prepareModalSplitData(
111129
});
112130

113131
return {
132+
kpiName: parentKpiDefinition?.name || "Modal Split",
114133
before: { label: beforeLabelWithMinYear, data: beforeData },
115134
after: { label: afterLabelWithMinYear, data: afterData },
116135
};

src/pages/index.astro

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,45 @@ const api = new ApiClient(Astro.request);
1818
1919
const livingLabsData = (await api.getLivingLabs()) as ILivingLabPopulated[];
2020
const measures = await api.getMeasures();
21-
const kpis = (await api.getKPIs())?.filter((kpi) => !kpi.parent_kpi_id);
21+
const allKPIs = await api.getKPIs();
22+
const parentKpis = allKPIs?.filter((kpi) => !kpi.parent_kpi_id);
2223
const transportModes = (await api.getTransportModes())?.filter(
2324
(tm) => tm.type === "NSM"
2425
);
2526
26-
const labs = livingLabsData?.map((lab) => ({
27-
id: lab.id,
28-
name: lab.name,
29-
coordinates: { lat: lab.lat, lng: lab.lng },
30-
radius: lab.radius ?? 50,
31-
// status: lab.status,
32-
totalMeasures: lab.projects?.length,
33-
kpisBefore: lab.kpi_results?.filter((kpi) => kpi.result_before).length,
34-
kpisAfter: lab.kpi_results?.filter((kpi) => kpi.result_after).length,
35-
transportModes: lab.transport_modes?.filter((tm) => tm.type === "NSM").length,
36-
sustainablePercentage: 0.4,
37-
}));
27+
const labs = livingLabsData?.map((lab) => {
28+
const kpisBeforeIds = lab.kpi_results
29+
?.filter((kpi) => kpi.result_before)
30+
?.map((kpiresult) => {
31+
const kpiDefinition = allKPIs?.find(
32+
(pk) => pk.id === kpiresult.kpidefinition_id
33+
);
34+
return kpiDefinition?.parent_kpi_id ?? kpiDefinition?.id;
35+
});
36+
37+
const kpisAfterIds = lab.kpi_results
38+
?.filter((kpi) => kpi.result_after)
39+
?.map((kpiresult) => {
40+
const kpiDefinition = allKPIs?.find(
41+
(pk) => pk.id === kpiresult.kpidefinition_id
42+
);
43+
return kpiDefinition?.parent_kpi_id ?? kpiDefinition?.id;
44+
});
45+
46+
return {
47+
id: lab.id,
48+
name: lab.name,
49+
coordinates: { lat: lab.lat, lng: lab.lng },
50+
radius: lab.radius ?? 50,
51+
// status: lab.status,
52+
totalMeasures: lab.projects?.length,
53+
kpisBefore: [...new Set(kpisBeforeIds)]?.length,
54+
kpisAfter: [...new Set(kpisAfterIds)]?.length,
55+
transportModes: lab.transport_modes?.filter((tm) => tm.type === "NSM")
56+
.length,
57+
sustainablePercentage: 0.4,
58+
};
59+
});
3860
---
3961

4062
<Layout role="visitor">
@@ -80,7 +102,7 @@ const labs = livingLabsData?.map((lab) => ({
80102
{
81103
id: 4,
82104
name: "Key Performance Indicators (KPIs) monitored",
83-
value: kpis?.length.toString() ?? "0",
105+
value: parentKpis?.length.toString() ?? "0",
84106
},
85107
]}
86108
/>

src/pages/lab-admin/measures.astro

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const livinglab = await api.getLivingLab(labId, ["projects"]);
1616
<Layout role="editor">
1717
<div class="mx-auto px-4 py-8 flex flex-col gap-8">
1818
<h2>What measures is your living lab implementing ?</h2>
19-
<p>Review the measures for Seamless Urban Mobility and New Shared Modes</p>
2019

2120
<InfoAlert title="What is a ‘measure’ ?" icon="question" variant="info">
2221
<small
@@ -37,7 +36,6 @@ const livinglab = await api.getLivingLab(labId, ["projects"]);
3736
title="What are PUSH measures ?"
3837
icon="question"
3938
variant="success"
40-
actionText="Access documentation"
4139
>
4240
<small
4341
>Measures that discourage private-car use through restrictive policies
@@ -58,7 +56,6 @@ const livinglab = await api.getLivingLab(labId, ["projects"]);
5856
title="What are PULL measures ?"
5957
icon="question"
6058
variant="success"
61-
actionText="Access documentation"
6259
>
6360
<small>
6461
Measures that encourage users to switch to sustainable transport

0 commit comments

Comments
 (0)