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
bf07fca parent: eacbeb6 modified
src/i18n.rs +4 -0 | @@ -123,6 +123,7 @@ pub struct Tr { | ||
| 123 | 123 | // ─── stats ─── |
| 124 | 124 | pub title_stats: &'static str, |
| 125 | 125 | pub my_entries_arrow: &'static str, |
| 126 | + pub today: &'static str, | |
| 126 | 127 | pub this_week: &'static str, |
| 127 | 128 | pub this_month: &'static str, |
| 128 | 129 | pub this_year: &'static str, |
| @@ -394,6 +395,7 @@ pub static FR: Tr = Tr { | ||
| 394 | 395 | toast_timer_cancelled: "Minuteur annulé", |
| 395 | 396 | title_stats: "Statistiques", |
| 396 | 397 | my_entries_arrow: "Mes saisies →", |
| 398 | + today: "Aujourd'hui", | |
| 397 | 399 | this_week: "Cette semaine", |
| 398 | 400 | this_month: "Ce mois-ci", |
| 399 | 401 | this_year: "Cette année", |
| @@ -625,6 +627,7 @@ pub static EN: Tr = Tr { | ||
| 625 | 627 | toast_timer_cancelled: "Timer cancelled", |
| 626 | 628 | title_stats: "Statistics", |
| 627 | 629 | my_entries_arrow: "My entries →", |
| 630 | + today: "Today", | |
| 628 | 631 | this_week: "This week", |
| 629 | 632 | this_month: "This month", |
| 630 | 633 | this_year: "This year", |
| @@ -856,6 +859,7 @@ pub static DE: Tr = Tr { | ||
| 856 | 859 | toast_timer_cancelled: "Timer abgebrochen", |
| 857 | 860 | title_stats: "Statistik", |
| 858 | 861 | my_entries_arrow: "Meine Einträge →", |
| 862 | + today: "Heute", | |
| 859 | 863 | this_week: "Diese Woche", |
| 860 | 864 | this_month: "Dieser Monat", |
| 861 | 865 | 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 { | ||
| 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 | 38 | /// « Mercredi 6 août — aujourd'hui » / "Wednesday 6 August — today" / |
| 32 | 39 | /// „Mittwoch, 6. August — heute“ |
| 33 | 40 | 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); | |
| 37 | 42 | if date == paris_today() { |
| 38 | 43 | format!("{base}{}", lang.tr().today_suffix) |
| 39 | 44 | } else { |
| @@ -313,5 +318,9 @@ mod tests { | ||
| 313 | 318 | assert_eq!(day_month(Lang::En, d(2026, 8, 3)), "3 August"); |
| 314 | 319 | assert_eq!(day_month(Lang::De, d(2026, 8, 3)), "3. August"); |
| 315 | 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 | } |
| @@ -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 @@ | ||
| 1 | 1 | use std::collections::{BTreeMap, BTreeSet, HashSet}; |
| 2 | 2 | |
| 3 | 3 | use askama::Template; |
| 4 | +use chrono::Days; | |
| 4 | 5 | use axum::extract::{Query, State}; |
| 5 | 6 | use serde::Deserialize; |
| 6 | 7 | use uuid::Uuid; |
| @@ -13,7 +14,7 @@ use crate::db::stats::{DayStatRow, StatRow}; | ||
| 13 | 14 | use crate::db::users::UserRow; |
| 14 | 15 | use crate::db::{stats, categories, teams}; |
| 15 | 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 | 18 | use crate::render::{HtmlTemplate, Shell}; |
| 18 | 19 | use crate::routes::{shell, urlenc}; |
| 19 | 20 | use crate::state::AppState; |
| @@ -36,6 +37,7 @@ pub struct StatsPanel { | ||
| 36 | 37 | /// Nom du scope, affiché seulement s'il y a plusieurs panneaux. |
| 37 | 38 | pub title: Option<String>, |
| 38 | 39 | pub day_chart: DayChartView, |
| 40 | + /// Jour en cours, semaine, mois, année, tout l'historique. | |
| 39 | 41 | pub charts: Vec<ChartView>, |
| 40 | 42 | pub legend: Vec<LegendItem>, |
| 41 | 43 | } |
| @@ -85,6 +87,7 @@ pub async fn stats_page( | ||
| 85 | 87 | .and_then(Year::parse) |
| 86 | 88 | .unwrap_or_else(Year::current); |
| 87 | 89 | |
| 90 | + let today = paris_today(); | |
| 88 | 91 | let (week_from, week_to) = week.bounds(); |
| 89 | 92 | let (month_from, month_to) = month.bounds(); |
| 90 | 93 | let (year_from, year_to) = year.bounds(); |
| @@ -92,6 +95,8 @@ pub async fn stats_page( | ||
| 92 | 95 | let all_tasks = categories::list_by_creation(&state.pool, ctx.team_id).await?; |
| 93 | 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 | 100 | let week_rows = stats::totals(&state.pool, ctx.team_id, Some(week_from), Some(week_to)).await?; |
| 96 | 101 | let month_rows = |
| 97 | 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 | 150 | .unwrap_or(0); |
| 146 | 151 | |
| 147 | 152 | let tr = ctx.tr(); |
| 148 | - let today = paris_today(); | |
| 153 | + let today_label = day_full(ctx.lang, today); | |
| 149 | 154 | let mut pills = Vec::new(); |
| 150 | 155 | let mut panels = Vec::new(); |
| 151 | 156 | let mut sheets: Vec<Sheet> = Vec::new(); |
| @@ -179,6 +184,7 @@ pub async fn stats_page( | ||
| 179 | 184 | .cloned() |
| 180 | 185 | .collect() |
| 181 | 186 | }; |
| 187 | + let td = pick(&today_rows); | |
| 182 | 188 | let wk = pick(&week_rows); |
| 183 | 189 | let mo = pick(&month_rows); |
| 184 | 190 | let yr = pick(&year_rows); |
| @@ -213,6 +219,19 @@ pub async fn stats_page( | ||
| 213 | 219 | ); |
| 214 | 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 | 235 | let (week_chart, s1) = build_chart( |
| 217 | 236 | &key("sem"), |
| 218 | 237 | tr.this_week, |
| @@ -254,6 +273,7 @@ pub async fn stats_page( | ||
| 254 | 273 | ); |
| 255 | 274 | let (total_chart, s4) = |
| 256 | 275 | build_chart(&key("tot"), tr.all_time, tr.all_history, &all_users, &tot, &colors, None, tr); |
| 276 | + sheets.extend(s0); | |
| 257 | 277 | sheets.extend(s1); |
| 258 | 278 | sheets.extend(s2); |
| 259 | 279 | sheets.extend(s3); |
| @@ -269,7 +289,7 @@ pub async fn stats_page( | ||
| 269 | 289 | anchor, |
| 270 | 290 | title: many.then_some(scope_name), |
| 271 | 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 | 293 | legend: legend(&[&wk, &mo, &tot], &colors), |
| 274 | 294 | }); |
| 275 | 295 | } |
| @@ -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 | } |