Skip to content

Commit 5916afc

Browse files
committed
Add member preferences statistics page
1 parent f92f7ff commit 5916afc

File tree

12 files changed

+308
-4
lines changed

12 files changed

+308
-4
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { useJson } from "Hooks/useJson";
2+
import { useTranslation } from "i18n/hooks";
3+
4+
interface OptionCount {
5+
option: string;
6+
count: number;
7+
}
8+
9+
interface QuestionTypeStats {
10+
question_type: string;
11+
total_respondents: number;
12+
options: OptionCount[];
13+
}
14+
15+
interface MemberPreferencesResponse {
16+
preference_statistics: Record<string, QuestionTypeStats>;
17+
}
18+
19+
// Human-readable names for question types
20+
const QUESTION_TYPE_NAMES: Record<string, string> = {
21+
ROOM_PREFERENCE: "Room Preferences",
22+
MACHINE_PREFERENCE: "Machine Preferences",
23+
SKILL_LEVEL: "Self-Reported Skill Level",
24+
};
25+
26+
export default function MemberPreferencesPage() {
27+
const { t } = useTranslation("task_statistics");
28+
29+
const { data, isLoading, error } = useJson<MemberPreferencesResponse>({
30+
url: `/tasks/statistics/member_preferences`,
31+
});
32+
33+
if (isLoading) {
34+
return (
35+
<div className="uk-width-1-1">
36+
<h2>{t("member_preferences.title")}</h2>
37+
<div>{t("loading")}</div>
38+
</div>
39+
);
40+
}
41+
42+
if (error) {
43+
return (
44+
<div className="uk-width-1-1">
45+
<h2>{t("member_preferences.title")}</h2>
46+
<div className="uk-alert-danger">{t("error_loading")}</div>
47+
</div>
48+
);
49+
}
50+
51+
if (!data || Object.keys(data.preference_statistics).length === 0) {
52+
return (
53+
<div className="uk-width-1-1">
54+
<h2>{t("member_preferences.title")}</h2>
55+
<p>{t("member_preferences.no_data")}</p>
56+
</div>
57+
);
58+
}
59+
60+
// Sort question types for consistent display order
61+
const sortedQuestionTypes = Object.keys(data.preference_statistics).sort();
62+
63+
return (
64+
<div className="uk-width-1-1">
65+
<h2>{t("member_preferences.title")}</h2>
66+
<p className="uk-text-muted">
67+
{t("member_preferences.description")}
68+
</p>
69+
70+
{sortedQuestionTypes.map((questionType) => {
71+
const stats = data.preference_statistics[questionType];
72+
if (!stats) return null;
73+
const displayName =
74+
QUESTION_TYPE_NAMES[questionType] || questionType;
75+
76+
return (
77+
<div key={questionType} className="uk-margin-large-bottom">
78+
<h3>{displayName}</h3>
79+
<p className="uk-text-meta">
80+
{t("member_preferences.total_respondents", {
81+
count: stats.total_respondents,
82+
})}
83+
</p>
84+
85+
{stats.options.length === 0 ? (
86+
<p className="uk-text-muted">
87+
{t("member_preferences.no_responses")}
88+
</p>
89+
) : (
90+
<table className="uk-table uk-table-small uk-table-striped uk-table-hover">
91+
<thead>
92+
<tr>
93+
<th>
94+
{t("member_preferences.option")}
95+
</th>
96+
<th style={{ width: "100px" }}>
97+
{t("member_preferences.count")}
98+
</th>
99+
<th style={{ width: "150px" }}>
100+
{t("member_preferences.percentage")}
101+
</th>
102+
</tr>
103+
</thead>
104+
<tbody>
105+
{stats.options.map((optionData) => {
106+
const percentage =
107+
stats.total_respondents > 0
108+
? (
109+
(optionData.count /
110+
stats.total_respondents) *
111+
100
112+
).toFixed(1)
113+
: "0";
114+
115+
return (
116+
<tr key={optionData.option}>
117+
<td>{optionData.option}</td>
118+
<td>{optionData.count}</td>
119+
<td>
120+
<div
121+
style={{
122+
display: "flex",
123+
alignItems:
124+
"center",
125+
gap: "8px",
126+
}}
127+
>
128+
<div
129+
style={{
130+
width: "80px",
131+
height: "16px",
132+
backgroundColor:
133+
"#e5e5e5",
134+
borderRadius:
135+
"4px",
136+
overflow:
137+
"hidden",
138+
}}
139+
>
140+
<div
141+
style={{
142+
width: `${percentage}%`,
143+
height: "100%",
144+
backgroundColor:
145+
"#1e87f0",
146+
transition:
147+
"width 0.3s ease",
148+
}}
149+
/>
150+
</div>
151+
<span>
152+
{percentage}%
153+
</span>
154+
</div>
155+
</td>
156+
</tr>
157+
);
158+
})}
159+
</tbody>
160+
</table>
161+
)}
162+
</div>
163+
);
164+
})}
165+
</div>
166+
);
167+
}

admin/src/Statistics/Routes.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { RouteObject } from "react-router-dom";
22
import { RedirectToSubpage } from "../Components/Routes";
33
import AccessLogPage from "./AccessLogPage";
44
import AllTasksPage from "./AllTasksPage";
5+
import MemberPreferencesPage from "./MemberPreferencesPage";
56
import MembershipPage from "./MembershipPage";
67
import MembersOfInterestPage from "./MembersOfInterestPage";
78
import QuizPage from "./QuizPage";
@@ -46,6 +47,10 @@ const routes: RouteObject[] = [
4647
path: "task_activity",
4748
element: <TaskActivityPage />,
4849
},
50+
{
51+
path: "member_preferences",
52+
element: <MemberPreferencesPage />,
53+
},
4954
];
5055

5156
export default routes;

admin/src/app.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ const nav = {
160160
text: "Task Activity",
161161
target: "/statistics/task_activity",
162162
},
163+
{
164+
text: "Member Preferences",
165+
target: "/statistics/member_preferences",
166+
},
163167
],
164168
},
165169
{

admin/src/i18n/generated_locales/en.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@
8282
},
8383
"error_loading": "Could not load task statistics",
8484
"loading": "Loading...",
85+
"member_preferences": {
86+
"count": "Count",
87+
"description": "Statistics about survey questions asked to members by the task system.",
88+
"no_data": "No preference data available yet.",
89+
"no_responses": "No responses yet.",
90+
"option": "Option",
91+
"percentage": "Percentage",
92+
"title": "Member Preferences",
93+
"total_respondents": "{{count}} members have answered this question"
94+
},
8595
"task_log": {
8696
"date": "Date",
8797
"member": "Member",

admin/src/i18n/generated_locales/sv.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@
8282
},
8383
"error_loading": "Kunde inte ladda uppgiftsstatistik",
8484
"loading": "Laddar...",
85+
"member_preferences": {
86+
"count": "Antal",
87+
"description": "Statistik om enkätfrågor som ställs till medlemmar av uppgiftssystemet.",
88+
"no_data": "Inga preferensdata tillgängliga ännu.",
89+
"no_responses": "Inga svar ännu.",
90+
"option": "Alternativ",
91+
"percentage": "Procent",
92+
"title": "Medlemspreferenser",
93+
"total_respondents": "{{count}} medlemmar har svarat på denna fråga"
94+
},
8595
"task_log": {
8696
"date": "Datum",
8797
"member": "Medlem",

admin/src/i18n/locales.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import sv from "./generated_locales/sv.json";
2525
"member_tasks":
2626
((key: "by_label.count" | "by_label.label" | "by_label.no_labels" | "by_label.title" | "error_loading" | "history.date" | "history.labels" | "history.no_tasks" | "history.status" | "history.task" | "history.title" | "loading" | "no_data" | "summary.not_specified" | "summary.preferred_rooms" | "summary.skill_level" | "summary.time_at_space" | "summary.title" | "summary.total_completed") => string)
2727
"task_statistics":
28-
((key: "cards.completed_count" | "cards.last_completed" | "cards.last_completer" | "cards.never" | "cards.no_cards" | "cards.overdue" | "cards.score" | "cards.task" | "cards.title" | "error_loading" | "loading" | "task_log.date" | "task_log.member" | "task_log.no_logs" | "task_log.room" | "task_log.show_only_completed" | "task_log.status" | "task_log.task" | "task_log.title" | "title") => string)
28+
((key: "cards.completed_count" | "cards.last_completed" | "cards.last_completer" | "cards.never" | "cards.no_cards" | "cards.overdue" | "cards.score" | "cards.task" | "cards.title" | "error_loading" | "loading" | "member_preferences.count" | "member_preferences.description" | "member_preferences.no_data" | "member_preferences.no_responses" | "member_preferences.option" | "member_preferences.percentage" | "member_preferences.title" | "task_log.date" | "task_log.member" | "task_log.no_logs" | "task_log.room" | "task_log.show_only_completed" | "task_log.status" | "task_log.task" | "task_log.title" | "title") => string)
29+
& ((key: "member_preferences.total_respondents", args: { "count": string | number}) => string)
2930
"tasks":
3031
((key: "actions.already_completed_by_someone_else" | "actions.assigned" | "actions.completed" | "actions.ignored" | "actions.not_done_confused" | "actions.not_done_did_something_else" | "actions.not_done_forgot" | "actions.not_done_no_time" | "actions.not_done_rerolled") => string)
3132
& ((key: "actions.not_done", args: { "count": string | number}) => string)
@@ -42,7 +43,8 @@ export type LOCALE_SCHEMA_GLOBAL = (
4243
& ((key: "common:todo") => string)
4344
& ((key: "member_quizzes:error_loading" | "member_quizzes:loading" | "member_quizzes:no_quizzes" | "member_quizzes:table.completion" | "member_quizzes:table.correctly_answered" | "member_quizzes:table.quiz") => string)
4445
& ((key: "member_tasks:by_label.count" | "member_tasks:by_label.label" | "member_tasks:by_label.no_labels" | "member_tasks:by_label.title" | "member_tasks:error_loading" | "member_tasks:history.date" | "member_tasks:history.labels" | "member_tasks:history.no_tasks" | "member_tasks:history.status" | "member_tasks:history.task" | "member_tasks:history.title" | "member_tasks:loading" | "member_tasks:no_data" | "member_tasks:summary.not_specified" | "member_tasks:summary.preferred_rooms" | "member_tasks:summary.skill_level" | "member_tasks:summary.time_at_space" | "member_tasks:summary.title" | "member_tasks:summary.total_completed") => string)
45-
& ((key: "task_statistics:cards.completed_count" | "task_statistics:cards.last_completed" | "task_statistics:cards.last_completer" | "task_statistics:cards.never" | "task_statistics:cards.no_cards" | "task_statistics:cards.overdue" | "task_statistics:cards.score" | "task_statistics:cards.task" | "task_statistics:cards.title" | "task_statistics:error_loading" | "task_statistics:loading" | "task_statistics:task_log.date" | "task_statistics:task_log.member" | "task_statistics:task_log.no_logs" | "task_statistics:task_log.room" | "task_statistics:task_log.show_only_completed" | "task_statistics:task_log.status" | "task_statistics:task_log.task" | "task_statistics:task_log.title" | "task_statistics:title") => string)
46+
& ((key: "task_statistics:cards.completed_count" | "task_statistics:cards.last_completed" | "task_statistics:cards.last_completer" | "task_statistics:cards.never" | "task_statistics:cards.no_cards" | "task_statistics:cards.overdue" | "task_statistics:cards.score" | "task_statistics:cards.task" | "task_statistics:cards.title" | "task_statistics:error_loading" | "task_statistics:loading" | "task_statistics:member_preferences.count" | "task_statistics:member_preferences.description" | "task_statistics:member_preferences.no_data" | "task_statistics:member_preferences.no_responses" | "task_statistics:member_preferences.option" | "task_statistics:member_preferences.percentage" | "task_statistics:member_preferences.title" | "task_statistics:task_log.date" | "task_statistics:task_log.member" | "task_statistics:task_log.no_logs" | "task_statistics:task_log.room" | "task_statistics:task_log.show_only_completed" | "task_statistics:task_log.status" | "task_statistics:task_log.task" | "task_statistics:task_log.title" | "task_statistics:title") => string)
47+
& ((key: "task_statistics:member_preferences.total_respondents", args: { "count": string | number}) => string)
4648
& ((key: "tasks:actions.already_completed_by_someone_else" | "tasks:actions.assigned" | "tasks:actions.completed" | "tasks:actions.ignored" | "tasks:actions.not_done_confused" | "tasks:actions.not_done_did_something_else" | "tasks:actions.not_done_forgot" | "tasks:actions.not_done_no_time" | "tasks:actions.not_done_rerolled") => string)
4749
& ((key: "tasks:actions.not_done", args: { "count": string | number}) => string)
4850
& ((key: "time:relative_generic.now") => string)

api/src/i18n/en.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@
5959
"task_statistics:cards.title": "All Tasks",
6060
"task_statistics:error_loading": "Could not load task statistics",
6161
"task_statistics:loading": "Loading...",
62+
"task_statistics:member_preferences.count": "Count",
63+
"task_statistics:member_preferences.description": "Statistics about survey questions asked to members by the task system.",
64+
"task_statistics:member_preferences.no_data": "No preference data available yet.",
65+
"task_statistics:member_preferences.no_responses": "No responses yet.",
66+
"task_statistics:member_preferences.option": "Option",
67+
"task_statistics:member_preferences.percentage": "Percentage",
68+
"task_statistics:member_preferences.title": "Member Preferences",
69+
"task_statistics:member_preferences.total_respondents": "{count} members have answered this question",
6270
"task_statistics:task_log.date": "Date",
6371
"task_statistics:task_log.member": "Member",
6472
"task_statistics:task_log.no_logs": "No task activity yet.",

0 commit comments

Comments
 (0)