From 75220668d5cd0412691d7407ac93f294e0d3e90d Mon Sep 17 00:00:00 2001 From: Anselme Irumva Date: Tue, 7 Jul 2026 12:51:05 +0200 Subject: [PATCH] CORE-545 Add treatment group to user preferences --- .../src/analytics/AnalyticsService.types.ts | 1 + .../InstructorStudentsTable.stories.tsx | 3 ++ .../InstructorStudentsTable.tsx | 31 +++++++++++++++++ .../src/hooks/useInstructorStudents.ts | 1 + .../hooks/useInstructorStudentsTableState.ts | 33 +++++++++++++++++-- .../src/i18n/locales/en-GB/translation.json | 3 +- .../src/i18n/locales/en-US/translation.json | 3 +- .../src/i18n/locales/es-AR/translation.json | 3 +- .../src/i18n/locales/es-ES/translation.json | 3 +- .../src/i18n/locales/ny-ZM/translation.json | 3 +- .../src/i18n/locales/pt-MZ/translation.json | 3 +- .../src/i18n/locales/sw-KE/translation.json | 3 +- admin-frontend/src/types/index.ts | 1 + 13 files changed, 81 insertions(+), 10 deletions(-) diff --git a/admin-frontend/src/analytics/AnalyticsService.types.ts b/admin-frontend/src/analytics/AnalyticsService.types.ts index 9e3d60f5..6bdf13b6 100644 --- a/admin-frontend/src/analytics/AnalyticsService.types.ts +++ b/admin-frontend/src/analytics/AnalyticsService.types.ts @@ -34,6 +34,7 @@ export interface StudentApiItem { career_explorer_messages_sent: number | null; last_login: string | null; last_active_module: string | null; + treatment_group: string | null; } export interface PaginatedResponse { diff --git a/admin-frontend/src/components/InstructorStudentsTable/InstructorStudentsTable.stories.tsx b/admin-frontend/src/components/InstructorStudentsTable/InstructorStudentsTable.stories.tsx index 75aaa2d5..c66c32ef 100644 --- a/admin-frontend/src/components/InstructorStudentsTable/InstructorStudentsTable.stories.tsx +++ b/admin-frontend/src/components/InstructorStudentsTable/InstructorStudentsTable.stories.tsx @@ -25,6 +25,7 @@ const storyInstructorStudentsFixture: InstructorStudentRow[] = [ careerExplorerMessagesSent: 5, lastLogin: "Today", lastActiveModuleId: "career-explorer", + treatmentGroup: "T1", }, { id: "student-2", @@ -39,6 +40,7 @@ const storyInstructorStudentsFixture: InstructorStudentRow[] = [ careerExplorerMessagesSent: 1, lastLogin: "2 days ago", lastActiveModuleId: "skills-discovery", + treatmentGroup: "T2", }, { id: "student-3", @@ -53,6 +55,7 @@ const storyInstructorStudentsFixture: InstructorStudentRow[] = [ careerExplorerMessagesSent: null, lastLogin: "1 week ago", lastActiveModuleId: "workplace-readiness", + treatmentGroup: null, }, ]; diff --git a/admin-frontend/src/components/InstructorStudentsTable/InstructorStudentsTable.tsx b/admin-frontend/src/components/InstructorStudentsTable/InstructorStudentsTable.tsx index 59cf6813..5383a801 100644 --- a/admin-frontend/src/components/InstructorStudentsTable/InstructorStudentsTable.tsx +++ b/admin-frontend/src/components/InstructorStudentsTable/InstructorStudentsTable.tsx @@ -44,9 +44,12 @@ const InstructorStudentsTable: React.FC = ({ setLastLoginFilter, lastModuleFilter, setLastModuleFilter, + treatmentGroupFilter, + setTreatmentGroupFilter, programmes, years, modules, + treatmentGroups, filteredRows, pagedRows, pageSize, @@ -131,6 +134,34 @@ const InstructorStudentsTable: React.FC = ({ }, render: (val, row) => {row.year}, }, + // Only surface the treatment group column/filter when at least one student is assigned to a group. + ...(treatmentGroups.length > 0 + ? [ + { + key: "treatmentGroup" as const, + label: t("instructorDashboard.studentsTable.headers.treatmentGroup").toUpperCase(), + sortable: true, + sortType: "text" as const, + align: "center" as const, + filter: { + options: [ + { value: "all", label: allLabel }, + ...treatmentGroups.map((group) => ({ value: group, label: group })), + ], + value: treatmentGroupFilter, + onChange: setTreatmentGroupFilter, + }, + render: (_val: unknown, row: InstructorStudentRow) => { + const label = row.treatmentGroup ?? PLACEHOLDER_SYMBOL; + return ( + + {label} + + ); + }, + }, + ] + : []), { key: "lastLogin", label: t("instructorDashboard.studentsTable.headers.lastLogin").toUpperCase(), diff --git a/admin-frontend/src/hooks/useInstructorStudents.ts b/admin-frontend/src/hooks/useInstructorStudents.ts index 60d27a24..4cf924e2 100644 --- a/admin-frontend/src/hooks/useInstructorStudents.ts +++ b/admin-frontend/src/hooks/useInstructorStudents.ts @@ -73,6 +73,7 @@ const toInstructorStudentRow = ( careerExplorerMessagesSent: item.career_explorer_messages_sent ?? null, lastLogin: formatLastLogin(item.last_login), lastActiveModuleId, + treatmentGroup: toText(item.treatment_group), }; }; diff --git a/admin-frontend/src/hooks/useInstructorStudentsTableState.ts b/admin-frontend/src/hooks/useInstructorStudentsTableState.ts index 1cfd95da..723af7f0 100644 --- a/admin-frontend/src/hooks/useInstructorStudentsTableState.ts +++ b/admin-frontend/src/hooks/useInstructorStudentsTableState.ts @@ -13,7 +13,8 @@ export type StudentsSortKey = | "careerReadinessStarted" | "careerReadinessCompleted" | "skillsDiscoveryStatus" - | "careerExplorerMessagesSent"; + | "careerExplorerMessagesSent" + | "treatmentGroup"; type SortDir = "asc" | "desc"; type UseInstructorStudentsTableStateOptions = { @@ -88,6 +89,7 @@ export function useInstructorStudentsTableState( const [yearFilter, setYearFilter] = useState("all"); const [lastLoginFilter, setLastLoginFilter] = useState("all"); const [lastModuleFilter, setLastModuleFilter] = useState("all"); + const [treatmentGroupFilter, setTreatmentGroupFilter] = useState("all"); const [pageIndex, setPageIndex] = useState(0); const debouncedNameSearch = useDebouncedValue(nameSearch, 250); @@ -101,6 +103,17 @@ export function useInstructorStudentsTableState( ); return ["all", ...Array.from(new Set([...knownModuleIds, ...dynamic]))]; }, [rows]); + const treatmentGroups = useMemo( + () => + Array.from( + new Set( + rows + .map((r) => (typeof r.treatmentGroup === "string" ? r.treatmentGroup.trim() : "")) + .filter((value) => value.length > 0) + ) + ).sort(), + [rows] + ); const filteredRows = useMemo(() => { const query = debouncedNameSearch.trim().toLowerCase(); @@ -109,11 +122,12 @@ export function useInstructorStudentsTableState( if (programme !== "all" && row.programme !== programme) return false; if (yearFilter !== "all" && row.year !== yearFilter) return false; if (lastModuleFilter !== "all" && row.lastActiveModuleId !== lastModuleFilter) return false; + if (treatmentGroupFilter !== "all" && (row.treatmentGroup ?? "") !== treatmentGroupFilter) return false; if (lastLoginFilter === "all") return true; return lastLoginDisplayMatchesFilter(row.lastLogin, lastLoginFilter); }); - }, [debouncedNameSearch, lastLoginFilter, lastModuleFilter, programme, rows, yearFilter]); + }, [debouncedNameSearch, lastLoginFilter, lastModuleFilter, programme, rows, treatmentGroupFilter, yearFilter]); const sortedRows = useMemo(() => { if (!sortKey) { @@ -135,6 +149,7 @@ export function useInstructorStudentsTableState( const order: Record = { not_started: 0, in_progress: 1, completed: 2 }; return order[row.skillsDiscoveryStatus] ?? 0; } + if (key === "treatmentGroup") return normalizeText(row.treatmentGroup ?? ""); return row.careerExplorerMessagesSent; }; @@ -158,7 +173,16 @@ export function useInstructorStudentsTableState( useEffect(() => { setPageIndex(0); - }, [debouncedNameSearch, programme, yearFilter, lastLoginFilter, lastModuleFilter, sortKey, sortDir]); + }, [ + debouncedNameSearch, + programme, + yearFilter, + lastLoginFilter, + lastModuleFilter, + treatmentGroupFilter, + sortKey, + sortDir, + ]); const totalPages = Math.max(1, Math.ceil(sortedRows.length / pageSize)); const safePageIndex = Math.min(pageIndex, totalPages - 1); @@ -206,9 +230,12 @@ export function useInstructorStudentsTableState( setLastLoginFilter, lastModuleFilter, setLastModuleFilter, + treatmentGroupFilter, + setTreatmentGroupFilter, programmes, years, modules, + treatmentGroups, filteredRows, pagedRows, pageSize, diff --git a/admin-frontend/src/i18n/locales/en-GB/translation.json b/admin-frontend/src/i18n/locales/en-GB/translation.json index efd8f0cd..9c3508a6 100644 --- a/admin-frontend/src/i18n/locales/en-GB/translation.json +++ b/admin-frontend/src/i18n/locales/en-GB/translation.json @@ -367,7 +367,8 @@ "careerExplorer": "Career Explorer", "prioritySectorsExplored": "Priority Sectors Explored", "lastLogin": "Last Login", - "lastActiveModule": "Last Active Module" + "lastActiveModule": "Last Active Module", + "treatmentGroup": "Treatment Group" }, "filters": { "all": "All", diff --git a/admin-frontend/src/i18n/locales/en-US/translation.json b/admin-frontend/src/i18n/locales/en-US/translation.json index 0807db4a..8ce98d7f 100644 --- a/admin-frontend/src/i18n/locales/en-US/translation.json +++ b/admin-frontend/src/i18n/locales/en-US/translation.json @@ -366,7 +366,8 @@ "careerExplorer": "Career Explorer", "prioritySectorsExplored": "Priority Sectors Explored", "lastLogin": "Last Login", - "lastActiveModule": "Last Active Module" + "lastActiveModule": "Last Active Module", + "treatmentGroup": "Treatment Group" }, "filters": { "all": "All", diff --git a/admin-frontend/src/i18n/locales/es-AR/translation.json b/admin-frontend/src/i18n/locales/es-AR/translation.json index 422f36d4..9e92405b 100644 --- a/admin-frontend/src/i18n/locales/es-AR/translation.json +++ b/admin-frontend/src/i18n/locales/es-AR/translation.json @@ -374,7 +374,8 @@ "careerExplorer": "Explorador de carrera", "prioritySectorsExplored": "Sectores prioritarios explorados", "lastLogin": "Último inicio de sesión", - "lastActiveModule": "Último módulo activo" + "lastActiveModule": "Último módulo activo", + "treatmentGroup": "Grupo de tratamiento" }, "filters": { "all": "Todo", diff --git a/admin-frontend/src/i18n/locales/es-ES/translation.json b/admin-frontend/src/i18n/locales/es-ES/translation.json index de3c8912..c3b08611 100644 --- a/admin-frontend/src/i18n/locales/es-ES/translation.json +++ b/admin-frontend/src/i18n/locales/es-ES/translation.json @@ -374,7 +374,8 @@ "careerExplorer": "Explorador de carrera", "prioritySectorsExplored": "Sectores prioritarios explorados", "lastLogin": "Último inicio de sesión", - "lastActiveModule": "Último módulo activo" + "lastActiveModule": "Último módulo activo", + "treatmentGroup": "Grupo de tratamiento" }, "filters": { "all": "Todo", diff --git a/admin-frontend/src/i18n/locales/ny-ZM/translation.json b/admin-frontend/src/i18n/locales/ny-ZM/translation.json index 5d615251..519e30b8 100644 --- a/admin-frontend/src/i18n/locales/ny-ZM/translation.json +++ b/admin-frontend/src/i18n/locales/ny-ZM/translation.json @@ -374,7 +374,8 @@ "careerExplorer": "Kufunafuna Ntchito", "prioritySectorsExplored": "Magawo Ofunika Kwambiri Afufuzidwa", "lastLogin": "Kulowa Komaliza", - "lastActiveModule": "Mojiyu Wogwira Pomaliza" + "lastActiveModule": "Mojiyu Wogwira Pomaliza", + "treatmentGroup": "Gulu la Chithandizo" }, "filters": { "all": "Zonse", diff --git a/admin-frontend/src/i18n/locales/pt-MZ/translation.json b/admin-frontend/src/i18n/locales/pt-MZ/translation.json index a811b622..4784d4d3 100644 --- a/admin-frontend/src/i18n/locales/pt-MZ/translation.json +++ b/admin-frontend/src/i18n/locales/pt-MZ/translation.json @@ -365,7 +365,8 @@ "careerExplorer": "Explorador de Carreiras", "prioritySectorsExplored": "Setores Prioritários Explorados", "lastLogin": "Último Acesso", - "lastActiveModule": "Último Módulo Ativo" + "lastActiveModule": "Último Módulo Ativo", + "treatmentGroup": "Grupo de Tratamento" }, "filters": { "all": "Todos", diff --git a/admin-frontend/src/i18n/locales/sw-KE/translation.json b/admin-frontend/src/i18n/locales/sw-KE/translation.json index b8e81b6a..050508f9 100644 --- a/admin-frontend/src/i18n/locales/sw-KE/translation.json +++ b/admin-frontend/src/i18n/locales/sw-KE/translation.json @@ -374,7 +374,8 @@ "careerExplorer": "Mtafiti wa Kazi", "prioritySectorsExplored": "Sekta za Kipaumbele Zilizogunduliwa", "lastLogin": "Mwisho wa Kuingia", - "lastActiveModule": "Moduli Amilifu ya Mwisho" + "lastActiveModule": "Moduli Amilifu ya Mwisho", + "treatmentGroup": "Kikundi cha Matibabu" }, "filters": { "all": "Wote", diff --git a/admin-frontend/src/types/index.ts b/admin-frontend/src/types/index.ts index 14482b9d..e6a8ccb3 100644 --- a/admin-frontend/src/types/index.ts +++ b/admin-frontend/src/types/index.ts @@ -41,6 +41,7 @@ export interface InstructorStudentRow { careerExplorerMessagesSent: number | null; lastLogin: string; lastActiveModuleId: string; + treatmentGroup: string | null; } export interface ModuleSummaryRow {