judu/ex-egopublic Fork
⑂ bf07fca
Commits
⬇ Clone ▾
git clone https://git.rickub.com/judu/ex-ego.git
git clone ssh://git@rickub.com/judu/ex-ego.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat(stats): graphique du jour en cours avant celui de la semaineUnverified

Colonnes = personnes, segments = catégories, comme les autres graphiques,
et scopé par groupe. Pas de navigation (comme « Tout l'historique ») : la
promenade dans les jours passés se fait par « Activité de la semaine ».

day_label() est scindé — day_full() rend le même libellé sans le suffixe
« — aujourd'hui », qui aurait fait doublon avec le titre.

Claude-Session: https://claude.ai/code/session_01D114WAMbcTKTZShiyksS1z
Julien Durillon committed 2026-08-11T17:20:42+02:00 Browse files
bf07fca parent: eacbeb6
modified src/i18n.rs +4 -0
@@ -123,6 +123,7 @@ pub struct Tr {
123123 // ─── stats ───
124124 pub title_stats: &'static str,
125125 pub my_entries_arrow: &'static str,
126+ pub today: &'static str,
126127 pub this_week: &'static str,
127128 pub this_month: &'static str,
128129 pub this_year: &'static str,
@@ -394,6 +395,7 @@ pub static FR: Tr = Tr {
394395 toast_timer_cancelled: "Minuteur annulé",
395396 title_stats: "Statistiques",
396397 my_entries_arrow: "Mes saisies →",
398+ today: "Aujourd'hui",
397399 this_week: "Cette semaine",
398400 this_month: "Ce mois-ci",
399401 this_year: "Cette année",
@@ -625,6 +627,7 @@ pub static EN: Tr = Tr {
625627 toast_timer_cancelled: "Timer cancelled",
626628 title_stats: "Statistics",
627629 my_entries_arrow: "My entries →",
630+ today: "Today",
628631 this_week: "This week",
629632 this_month: "This month",
630633 this_year: "This year",
@@ -856,6 +859,7 @@ pub static DE: Tr = Tr {
856859 toast_timer_cancelled: "Timer abgebrochen",
857860 title_stats: "Statistik",
858861 my_entries_arrow: "Meine Einträge →",
862+ today: "Heute",
859863 this_week: "Diese Woche",
860864 this_month: "Dieser Monat",
861865 this_year: "Dieses Jahr",
@@ -123,6 +123,7 @@ pub struct Tr {
123 // ─── stats ───123 // ─── stats ───
124 pub title_stats: &'static str,124 pub title_stats: &'static str,
125 pub my_entries_arrow: &'static str,125 pub my_entries_arrow: &'static str,
126+ pub today: &'static str,
126 pub this_week: &'static str,127 pub this_week: &'static str,
127 pub this_month: &'static str,128 pub this_month: &'static str,
128 pub this_year: &'static str,129 pub this_year: &'static str,
@@ -394,6 +395,7 @@ pub static FR: Tr = Tr {
394 toast_timer_cancelled: "Minuteur annulé",395 toast_timer_cancelled: "Minuteur annulé",
395 title_stats: "Statistiques",396 title_stats: "Statistiques",
396 my_entries_arrow: "Mes saisies →",397 my_entries_arrow: "Mes saisies →",
398+ today: "Aujourd'hui",
397 this_week: "Cette semaine",399 this_week: "Cette semaine",
398 this_month: "Ce mois-ci",400 this_month: "Ce mois-ci",
399 this_year: "Cette année",401 this_year: "Cette année",
@@ -625,6 +627,7 @@ pub static EN: Tr = Tr {
625 toast_timer_cancelled: "Timer cancelled",627 toast_timer_cancelled: "Timer cancelled",
626 title_stats: "Statistics",628 title_stats: "Statistics",
627 my_entries_arrow: "My entries →",629 my_entries_arrow: "My entries →",
630+ today: "Today",
628 this_week: "This week",631 this_week: "This week",
629 this_month: "This month",632 this_month: "This month",
630 this_year: "This year",633 this_year: "This year",
@@ -856,6 +859,7 @@ pub static DE: Tr = Tr {
856 toast_timer_cancelled: "Timer abgebrochen",859 toast_timer_cancelled: "Timer abgebrochen",
857 title_stats: "Statistik",860 title_stats: "Statistik",
858 my_entries_arrow: "Meine Einträge →",861 my_entries_arrow: "Meine Einträge →",
862+ today: "Heute",
859 this_week: "Diese Woche",863 this_week: "Diese Woche",
860 this_month: "Dieser Monat",864 this_month: "Dieser Monat",
861 this_year: "Dieses Jahr",865 this_year: "Dieses Jahr",
modified src/period.rs +12 -3
@@ -28,12 +28,17 @@ pub fn day_month(lang: Lang, date: NaiveDate) -> String {
2828 }
2929 }
3030
31+/// « Mercredi 6 août » / "Wednesday 6 August" / „Mittwoch, 6. August“
32+pub fn day_full(lang: Lang, date: NaiveDate) -> String {
33+ let weekday = lang.tr().weekdays[date.weekday().num_days_from_monday() as usize];
34+ let sep = if lang == Lang::De { ", " } else { " " };
35+ capitalize(&format!("{weekday}{sep}{}", day_month(lang, date)))
36+}
37+
3138 /// « Mercredi 6 août — aujourd'hui » / "Wednesday 6 August — today" /
3239 /// „Mittwoch, 6. August — heute“
3340 pub fn day_label(lang: Lang, date: NaiveDate) -> String {
34- let weekday = lang.tr().weekdays[date.weekday().num_days_from_monday() as usize];
35- let sep = if lang == Lang::De { ", " } else { " " };
36- let base = capitalize(&format!("{weekday}{sep}{}", day_month(lang, date)));
41+ let base = day_full(lang, date);
3742 if date == paris_today() {
3843 format!("{base}{}", lang.tr().today_suffix)
3944 } else {
@@ -313,5 +318,9 @@ mod tests {
313318 assert_eq!(day_month(Lang::En, d(2026, 8, 3)), "3 August");
314319 assert_eq!(day_month(Lang::De, d(2026, 8, 3)), "3. August");
315320 assert_eq!(day_label(Lang::De, d(2026, 3, 5)), "Donnerstag, 5. März");
321+ // day_full = day_label sans le suffixe « aujourd'hui ».
322+ assert_eq!(day_full(Lang::Fr, d(2026, 3, 5)), "Jeudi 5 mars");
323+ assert_eq!(day_full(Lang::En, d(2026, 3, 5)), "Thursday 5 March");
324+ assert_eq!(day_full(Lang::Fr, paris_today()), day_label(Lang::Fr, paris_today()).replace(Lang::Fr.tr().today_suffix, ""));
316325 }
317326 }
@@ -28,12 +28,17 @@ pub fn day_month(lang: Lang, date: NaiveDate) -> String {
28 }28 }
29 }29 }
30 30
31+/// « Mercredi 6 août » / "Wednesday 6 August" / „Mittwoch, 6. August“
32+pub fn day_full(lang: Lang, date: NaiveDate) -> String {
33+ let weekday = lang.tr().weekdays[date.weekday().num_days_from_monday() as usize];
34+ let sep = if lang == Lang::De { ", " } else { " " };
35+ capitalize(&format!("{weekday}{sep}{}", day_month(lang, date)))
36+}
37+
31 /// « Mercredi 6 août — aujourd'hui » / "Wednesday 6 August — today" /38 /// « Mercredi 6 août — aujourd'hui » / "Wednesday 6 August — today" /
32 /// „Mittwoch, 6. August — heute“39 /// „Mittwoch, 6. August — heute“
33 pub fn day_label(lang: Lang, date: NaiveDate) -> String {40 pub fn day_label(lang: Lang, date: NaiveDate) -> String {
34- let weekday = lang.tr().weekdays[date.weekday().num_days_from_monday() as usize];41+ let base = day_full(lang, date);
35- let sep = if lang == Lang::De { ", " } else { " " };
36- let base = capitalize(&format!("{weekday}{sep}{}", day_month(lang, date)));
37 if date == paris_today() {42 if date == paris_today() {
38 format!("{base}{}", lang.tr().today_suffix)43 format!("{base}{}", lang.tr().today_suffix)
39 } else {44 } else {
@@ -313,5 +318,9 @@ mod tests {
313 assert_eq!(day_month(Lang::En, d(2026, 8, 3)), "3 August");318 assert_eq!(day_month(Lang::En, d(2026, 8, 3)), "3 August");
314 assert_eq!(day_month(Lang::De, d(2026, 8, 3)), "3. August");319 assert_eq!(day_month(Lang::De, d(2026, 8, 3)), "3. August");
315 assert_eq!(day_label(Lang::De, d(2026, 3, 5)), "Donnerstag, 5. März");320 assert_eq!(day_label(Lang::De, d(2026, 3, 5)), "Donnerstag, 5. März");
321+ // day_full = day_label sans le suffixe « aujourd'hui ».
322+ assert_eq!(day_full(Lang::Fr, d(2026, 3, 5)), "Jeudi 5 mars");
323+ assert_eq!(day_full(Lang::En, d(2026, 3, 5)), "Thursday 5 March");
324+ assert_eq!(day_full(Lang::Fr, paris_today()), day_label(Lang::Fr, paris_today()).replace(Lang::Fr.tr().today_suffix, ""));
316 }325 }
317 }326 }
modified src/routes/stats.rs +23 -3
@@ -1,6 +1,7 @@
11 use std::collections::{BTreeMap, BTreeSet, HashSet};
22
33 use askama::Template;
4+use chrono::Days;
45 use axum::extract::{Query, State};
56 use serde::Deserialize;
67 use uuid::Uuid;
@@ -13,7 +14,7 @@ use crate::db::stats::{DayStatRow, StatRow};
1314 use crate::db::users::UserRow;
1415 use crate::db::{stats, categories, teams};
1516 use crate::error::AppError;
16-use crate::period::{Month, Week, Year, paris_today};
17+use crate::period::{Month, Week, Year, day_full, paris_today};
1718 use crate::render::{HtmlTemplate, Shell};
1819 use crate::routes::{shell, urlenc};
1920 use crate::state::AppState;
@@ -36,6 +37,7 @@ pub struct StatsPanel {
3637 /// Nom du scope, affiché seulement s'il y a plusieurs panneaux.
3738 pub title: Option<String>,
3839 pub day_chart: DayChartView,
40+ /// Jour en cours, semaine, mois, année, tout l'historique.
3941 pub charts: Vec<ChartView>,
4042 pub legend: Vec<LegendItem>,
4143 }
@@ -85,6 +87,7 @@ pub async fn stats_page(
8587 .and_then(Year::parse)
8688 .unwrap_or_else(Year::current);
8789
90+ let today = paris_today();
8891 let (week_from, week_to) = week.bounds();
8992 let (month_from, month_to) = month.bounds();
9093 let (year_from, year_to) = year.bounds();
@@ -92,6 +95,8 @@ pub async fn stats_page(
9295 let all_tasks = categories::list_by_creation(&state.pool, ctx.team_id).await?;
9396 let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>(), ctx.hc);
9497
98+ let today_rows =
99+ stats::totals(&state.pool, ctx.team_id, Some(today), Some(today + Days::new(1))).await?;
95100 let week_rows = stats::totals(&state.pool, ctx.team_id, Some(week_from), Some(week_to)).await?;
96101 let month_rows =
97102 stats::totals(&state.pool, ctx.team_id, Some(month_from), Some(month_to)).await?;
@@ -145,7 +150,7 @@ pub async fn stats_page(
145150 .unwrap_or(0);
146151
147152 let tr = ctx.tr();
148- let today = paris_today();
153+ let today_label = day_full(ctx.lang, today);
149154 let mut pills = Vec::new();
150155 let mut panels = Vec::new();
151156 let mut sheets: Vec<Sheet> = Vec::new();
@@ -179,6 +184,7 @@ pub async fn stats_page(
179184 .cloned()
180185 .collect()
181186 };
187+ let td = pick(&today_rows);
182188 let wk = pick(&week_rows);
183189 let mo = pick(&month_rows);
184190 let yr = pick(&year_rows);
@@ -213,6 +219,19 @@ pub async fn stats_page(
213219 );
214220 day_sheets.extend(ds);
215221
222+ // Jour en cours : pas de navigation (comme « Tout l'historique »),
223+ // la promenade dans les jours passés se fait par le graphique
224+ // « Activité de la semaine » juste au-dessus.
225+ let (today_chart, s0) = build_chart(
226+ &key("auj"),
227+ tr.today,
228+ &today_label,
229+ &all_users,
230+ &td,
231+ &colors,
232+ None,
233+ tr,
234+ );
216235 let (week_chart, s1) = build_chart(
217236 &key("sem"),
218237 tr.this_week,
@@ -254,6 +273,7 @@ pub async fn stats_page(
254273 );
255274 let (total_chart, s4) =
256275 build_chart(&key("tot"), tr.all_time, tr.all_history, &all_users, &tot, &colors, None, tr);
276+ sheets.extend(s0);
257277 sheets.extend(s1);
258278 sheets.extend(s2);
259279 sheets.extend(s3);
@@ -269,7 +289,7 @@ pub async fn stats_page(
269289 anchor,
270290 title: many.then_some(scope_name),
271291 day_chart,
272- charts: vec![week_chart, month_chart, year_chart, total_chart],
292+ charts: vec![today_chart, week_chart, month_chart, year_chart, total_chart],
273293 legend: legend(&[&wk, &mo, &tot], &colors),
274294 });
275295 }
@@ -1,6 +1,7 @@
1 use std::collections::{BTreeMap, BTreeSet, HashSet};1 use std::collections::{BTreeMap, BTreeSet, HashSet};
2 2
3 use askama::Template;3 use askama::Template;
4+use chrono::Days;
4 use axum::extract::{Query, State};5 use axum::extract::{Query, State};
5 use serde::Deserialize;6 use serde::Deserialize;
6 use uuid::Uuid;7 use uuid::Uuid;
@@ -13,7 +14,7 @@ use crate::db::stats::{DayStatRow, StatRow};
13 use crate::db::users::UserRow;14 use crate::db::users::UserRow;
14 use crate::db::{stats, categories, teams};15 use crate::db::{stats, categories, teams};
15 use crate::error::AppError;16 use crate::error::AppError;
16-use crate::period::{Month, Week, Year, paris_today};17+use crate::period::{Month, Week, Year, day_full, paris_today};
17 use crate::render::{HtmlTemplate, Shell};18 use crate::render::{HtmlTemplate, Shell};
18 use crate::routes::{shell, urlenc};19 use crate::routes::{shell, urlenc};
19 use crate::state::AppState;20 use crate::state::AppState;
@@ -36,6 +37,7 @@ pub struct StatsPanel {
36 /// Nom du scope, affiché seulement s'il y a plusieurs panneaux.37 /// Nom du scope, affiché seulement s'il y a plusieurs panneaux.
37 pub title: Option<String>,38 pub title: Option<String>,
38 pub day_chart: DayChartView,39 pub day_chart: DayChartView,
40+ /// Jour en cours, semaine, mois, année, tout l'historique.
39 pub charts: Vec<ChartView>,41 pub charts: Vec<ChartView>,
40 pub legend: Vec<LegendItem>,42 pub legend: Vec<LegendItem>,
41 }43 }
@@ -85,6 +87,7 @@ pub async fn stats_page(
85 .and_then(Year::parse)87 .and_then(Year::parse)
86 .unwrap_or_else(Year::current);88 .unwrap_or_else(Year::current);
87 89
90+ let today = paris_today();
88 let (week_from, week_to) = week.bounds();91 let (week_from, week_to) = week.bounds();
89 let (month_from, month_to) = month.bounds();92 let (month_from, month_to) = month.bounds();
90 let (year_from, year_to) = year.bounds();93 let (year_from, year_to) = year.bounds();
@@ -92,6 +95,8 @@ pub async fn stats_page(
92 let all_tasks = categories::list_by_creation(&state.pool, ctx.team_id).await?;95 let all_tasks = categories::list_by_creation(&state.pool, ctx.team_id).await?;
93 let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>(), ctx.hc);96 let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>(), ctx.hc);
94 97
98+ let today_rows =
99+ stats::totals(&state.pool, ctx.team_id, Some(today), Some(today + Days::new(1))).await?;
95 let week_rows = stats::totals(&state.pool, ctx.team_id, Some(week_from), Some(week_to)).await?;100 let week_rows = stats::totals(&state.pool, ctx.team_id, Some(week_from), Some(week_to)).await?;
96 let month_rows =101 let month_rows =
97 stats::totals(&state.pool, ctx.team_id, Some(month_from), Some(month_to)).await?;102 stats::totals(&state.pool, ctx.team_id, Some(month_from), Some(month_to)).await?;
@@ -145,7 +150,7 @@ pub async fn stats_page(
145 .unwrap_or(0);150 .unwrap_or(0);
146 151
147 let tr = ctx.tr();152 let tr = ctx.tr();
148- let today = paris_today();153+ let today_label = day_full(ctx.lang, today);
149 let mut pills = Vec::new();154 let mut pills = Vec::new();
150 let mut panels = Vec::new();155 let mut panels = Vec::new();
151 let mut sheets: Vec<Sheet> = Vec::new();156 let mut sheets: Vec<Sheet> = Vec::new();
@@ -179,6 +184,7 @@ pub async fn stats_page(
179 .cloned()184 .cloned()
180 .collect()185 .collect()
181 };186 };
187+ let td = pick(&today_rows);
182 let wk = pick(&week_rows);188 let wk = pick(&week_rows);
183 let mo = pick(&month_rows);189 let mo = pick(&month_rows);
184 let yr = pick(&year_rows);190 let yr = pick(&year_rows);
@@ -213,6 +219,19 @@ pub async fn stats_page(
213 );219 );
214 day_sheets.extend(ds);220 day_sheets.extend(ds);
215 221
222+ // Jour en cours : pas de navigation (comme « Tout l'historique »),
223+ // la promenade dans les jours passés se fait par le graphique
224+ // « Activité de la semaine » juste au-dessus.
225+ let (today_chart, s0) = build_chart(
226+ &key("auj"),
227+ tr.today,
228+ &today_label,
229+ &all_users,
230+ &td,
231+ &colors,
232+ None,
233+ tr,
234+ );
216 let (week_chart, s1) = build_chart(235 let (week_chart, s1) = build_chart(
217 &key("sem"),236 &key("sem"),
218 tr.this_week,237 tr.this_week,
@@ -254,6 +273,7 @@ pub async fn stats_page(
254 );273 );
255 let (total_chart, s4) =274 let (total_chart, s4) =
256 build_chart(&key("tot"), tr.all_time, tr.all_history, &all_users, &tot, &colors, None, tr);275 build_chart(&key("tot"), tr.all_time, tr.all_history, &all_users, &tot, &colors, None, tr);
276+ sheets.extend(s0);
257 sheets.extend(s1);277 sheets.extend(s1);
258 sheets.extend(s2);278 sheets.extend(s2);
259 sheets.extend(s3);279 sheets.extend(s3);
@@ -269,7 +289,7 @@ pub async fn stats_page(
269 anchor,289 anchor,
270 title: many.then_some(scope_name),290 title: many.then_some(scope_name),
271 day_chart,291 day_chart,
272- charts: vec![week_chart, month_chart, year_chart, total_chart],292+ charts: vec![today_chart, week_chart, month_chart, year_chart, total_chart],
273 legend: legend(&[&wk, &mo, &tot], &colors),293 legend: legend(&[&wk, &mo, &tot], &colors),
274 });294 });
275 }295 }