refactor(catégories): « tâche » devient « catégorie » partout — migration 0005 (tasks → categories, task_id → category_id), routes /parametres/categories, code et vocabulaire UI ; le mot « tâche » est libéré pour la future todo listUnverified
Claude-Session: https://claude.ai/code/session_01FPKHKGfqvnvXKY9tNhYmkh
bb28d5f parent: 638f8c8 modified
a11y/audit.mjs +6 -6 | @@ -5,7 +5,7 @@ | ||
| 5 | 5 | // les violations moindres sont listées en avertissement. |
| 6 | 6 | // |
| 7 | 7 | // Prérequis : un serveur time-time qui tourne (base dev !) avec un compte |
| 8 | -// membre d'au moins deux équipes, dont l'équipe active a ≥ 1 tâche et | |
| 8 | +// membre d'au moins deux équipes, dont l'équipe active a ≥ 1 catégorie et | |
| 9 | 9 | // ≥ 1 temps saisi. Exemple de mise en place : |
| 10 | 10 | // |
| 11 | 11 | // cargo run -- team add "Audit" ; cargo run -- team add "Audit 2" |
| @@ -57,12 +57,12 @@ if (page.url().includes('/login')) { | ||
| 57 | 57 | process.exit(2); |
| 58 | 58 | } |
| 59 | 59 | |
| 60 | -// Une tâche et une saisie existantes servent aux pages paramétrées. | |
| 60 | +// Une catégorie et une saisie existantes servent aux pages paramétrées. | |
| 61 | 61 | const ids = await page.evaluate(async () => { |
| 62 | 62 | const step1 = await fetch('/temps/nouveau').then((r) => r.text()); |
| 63 | 63 | const times = await fetch('/temps').then((r) => r.text()); |
| 64 | 64 | return { |
| 65 | - task: step1.match(/tache=([0-9a-f-]{36})/)?.[1] ?? null, | |
| 65 | + categorie: step1.match(/categorie=([0-9a-f-]{36})/)?.[1] ?? null, | |
| 66 | 66 | entry: times.match(/\/temps\/([0-9a-f-]{36})\/modifier/)?.[1] ?? null, |
| 67 | 67 | }; |
| 68 | 68 | }); |
| @@ -70,15 +70,15 @@ const ids = await page.evaluate(async () => { | ||
| 70 | 70 | const pages = [ |
| 71 | 71 | { name: 'login', url: '/login', anon: true }, |
| 72 | 72 | { name: 'add-step1', url: '/temps/nouveau' }, |
| 73 | - ids.task && { name: 'add-step2', url: `/temps/nouveau?tache=${ids.task}` }, | |
| 73 | + ids.categorie && { name: 'add-step2', url: `/temps/nouveau?categorie=${ids.categorie}` }, | |
| 74 | 74 | { name: 'stats', url: '/stats' }, |
| 75 | 75 | { name: 'times', url: '/temps' }, |
| 76 | 76 | ids.entry && { name: 'edit', url: `/temps/${ids.entry}/modifier` }, |
| 77 | 77 | { name: 'settings', url: '/parametres' }, |
| 78 | - { name: 'tasks', url: '/parametres/taches' }, | |
| 78 | + { name: 'categories', url: '/parametres/categories' }, | |
| 79 | 79 | { name: 'team-sheet', url: '/temps/nouveau#equipes' }, |
| 80 | 80 | ].filter(Boolean); |
| 81 | -if (pages.length < 9) console.warn('⚠ tâche ou saisie manquante : certains écrans sont sautés.'); | |
| 81 | +if (pages.length < 9) console.warn('⚠ catégorie ou saisie manquante : certains écrans sont sautés.'); | |
| 82 | 82 | |
| 83 | 83 | let blocking = 0; |
| 84 | 84 | for (const p of pages) { |
| @@ -5,7 +5,7 @@ | |||
| 5 | // les violations moindres sont listées en avertissement. | 5 | // les violations moindres sont listées en avertissement. |
| 6 | // | 6 | // |
| 7 | // Prérequis : un serveur time-time qui tourne (base dev !) avec un compte | 7 | // Prérequis : un serveur time-time qui tourne (base dev !) avec un compte |
| 8 | -// membre d'au moins deux équipes, dont l'équipe active a ≥ 1 tâche et | 8 | +// membre d'au moins deux équipes, dont l'équipe active a ≥ 1 catégorie et |
| 9 | // ≥ 1 temps saisi. Exemple de mise en place : | 9 | // ≥ 1 temps saisi. Exemple de mise en place : |
| 10 | // | 10 | // |
| 11 | // cargo run -- team add "Audit" ; cargo run -- team add "Audit 2" | 11 | // cargo run -- team add "Audit" ; cargo run -- team add "Audit 2" |
| @@ -57,12 +57,12 @@ if (page.url().includes('/login')) { | |||
| 57 | process.exit(2); | 57 | process.exit(2); |
| 58 | } | 58 | } |
| 59 | 59 | ||
| 60 | -// Une tâche et une saisie existantes servent aux pages paramétrées. | 60 | +// Une catégorie et une saisie existantes servent aux pages paramétrées. |
| 61 | const ids = await page.evaluate(async () => { | 61 | const ids = await page.evaluate(async () => { |
| 62 | const step1 = await fetch('/temps/nouveau').then((r) => r.text()); | 62 | const step1 = await fetch('/temps/nouveau').then((r) => r.text()); |
| 63 | const times = await fetch('/temps').then((r) => r.text()); | 63 | const times = await fetch('/temps').then((r) => r.text()); |
| 64 | return { | 64 | return { |
| 65 | - task: step1.match(/tache=([0-9a-f-]{36})/)?.[1] ?? null, | 65 | + categorie: step1.match(/categorie=([0-9a-f-]{36})/)?.[1] ?? null, |
| 66 | entry: times.match(/\/temps\/([0-9a-f-]{36})\/modifier/)?.[1] ?? null, | 66 | entry: times.match(/\/temps\/([0-9a-f-]{36})\/modifier/)?.[1] ?? null, |
| 67 | }; | 67 | }; |
| 68 | }); | 68 | }); |
| @@ -70,15 +70,15 @@ const ids = await page.evaluate(async () => { | |||
| 70 | const pages = [ | 70 | const pages = [ |
| 71 | { name: 'login', url: '/login', anon: true }, | 71 | { name: 'login', url: '/login', anon: true }, |
| 72 | { name: 'add-step1', url: '/temps/nouveau' }, | 72 | { name: 'add-step1', url: '/temps/nouveau' }, |
| 73 | - ids.task && { name: 'add-step2', url: `/temps/nouveau?tache=${ids.task}` }, | 73 | + ids.categorie && { name: 'add-step2', url: `/temps/nouveau?categorie=${ids.categorie}` }, |
| 74 | { name: 'stats', url: '/stats' }, | 74 | { name: 'stats', url: '/stats' }, |
| 75 | { name: 'times', url: '/temps' }, | 75 | { name: 'times', url: '/temps' }, |
| 76 | ids.entry && { name: 'edit', url: `/temps/${ids.entry}/modifier` }, | 76 | ids.entry && { name: 'edit', url: `/temps/${ids.entry}/modifier` }, |
| 77 | { name: 'settings', url: '/parametres' }, | 77 | { name: 'settings', url: '/parametres' }, |
| 78 | - { name: 'tasks', url: '/parametres/taches' }, | 78 | + { name: 'categories', url: '/parametres/categories' }, |
| 79 | { name: 'team-sheet', url: '/temps/nouveau#equipes' }, | 79 | { name: 'team-sheet', url: '/temps/nouveau#equipes' }, |
| 80 | ].filter(Boolean); | 80 | ].filter(Boolean); |
| 81 | -if (pages.length < 9) console.warn('⚠ tâche ou saisie manquante : certains écrans sont sautés.'); | 81 | +if (pages.length < 9) console.warn('⚠ catégorie ou saisie manquante : certains écrans sont sautés.'); |
| 82 | 82 | ||
| 83 | let blocking = 0; | 83 | let blocking = 0; |
| 84 | for (const p of pages) { | 84 | for (const p of pages) { |
modified
assets/style.css +15 -15 | @@ -317,18 +317,18 @@ a.team-pill:hover { filter: brightness(.96); } | ||
| 317 | 317 | .login .tagline { font-size: 15px; opacity: .7; margin: 0 0 var(--space-8); max-width: 15em; } |
| 318 | 318 | .form-error { font-size: 13px; color: var(--color-accent-700); margin: 0 0 var(--space-3); } |
| 319 | 319 | |
| 320 | -/* — ajout étape 1 : liste de tâches — */ | |
| 320 | +/* — ajout étape 1 : liste de catégories — */ | |
| 321 | 321 | .step-hint { font-size: 13px; opacity: .7; margin: 0 0 var(--space-4); } |
| 322 | -.task-pick { display: grid; gap: var(--space-2); } | |
| 323 | -.task-pick a { | |
| 322 | +.category-pick { display: grid; gap: var(--space-2); } | |
| 323 | +.category-pick a { | |
| 324 | 324 | display: flex; align-items: center; gap: var(--space-3); |
| 325 | 325 | padding: var(--space-3) var(--space-4); background: var(--color-surface); |
| 326 | 326 | border: 1px solid transparent; border-radius: 999px; |
| 327 | 327 | font-size: 16px; color: var(--color-text); text-decoration: none; |
| 328 | 328 | } |
| 329 | -.task-pick a:hover { border-color: var(--color-accent); } | |
| 330 | -.task-pick .tname { flex: 1; } | |
| 331 | -.task-pick .tweek { font-size: 12px; opacity: .7; } | |
| 329 | +.category-pick a:hover { border-color: var(--color-accent); } | |
| 330 | +.category-pick .tname { flex: 1; } | |
| 331 | +.category-pick .tweek { font-size: 12px; opacity: .7; } | |
| 332 | 332 | .dot { border-radius: 999px; flex: none; display: inline-block; } |
| 333 | 333 | .dot-14 { width: 14px; height: 14px; } |
| 334 | 334 | .dot-12 { width: 12px; height: 12px; } |
| @@ -420,24 +420,24 @@ a.team-pill:hover { filter: brightness(.96); } | ||
| 420 | 420 | border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent); |
| 421 | 421 | } |
| 422 | 422 | .times .tinfo { flex: 1; min-width: 0; } |
| 423 | -.times .ttask { font-size: 15px; } | |
| 423 | +.times .tcat { font-size: 15px; } | |
| 424 | 424 | .times .tdate { font-size: 12px; opacity: .7; } |
| 425 | 425 | .times .tdur { font-size: 16px; font-family: var(--font-heading); } |
| 426 | 426 | |
| 427 | -/* — tâches — */ | |
| 428 | -.task-row { | |
| 427 | +/* — catégories — */ | |
| 428 | +.category-row { | |
| 429 | 429 | display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) 0; |
| 430 | 430 | border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent); |
| 431 | 431 | } |
| 432 | -.task-row form.rename { flex: 1; } | |
| 433 | -.task-row .rename .input { | |
| 432 | +.category-row form.rename { flex: 1; } | |
| 433 | +.category-row .rename .input { | |
| 434 | 434 | border-color: transparent; background: transparent; padding-left: 0; |
| 435 | 435 | font-size: 15px; border-radius: 0; |
| 436 | 436 | } |
| 437 | -.task-row .rename .input:hover { border-color: var(--color-divider); } | |
| 438 | -.task-row.archived .rename .input { opacity: .45; } | |
| 439 | -.task-add-row { display: flex; gap: var(--space-2); margin-bottom: var(--space-6); } | |
| 440 | -.task-add-row .input { flex: 1; min-height: 44px; } | |
| 437 | +.category-row .rename .input:hover { border-color: var(--color-divider); } | |
| 438 | +.category-row.archived .rename .input { opacity: .45; } | |
| 439 | +.category-add-row { display: flex; gap: var(--space-2); margin-bottom: var(--space-6); } | |
| 440 | +.category-add-row .input { flex: 1; min-height: 44px; } | |
| 441 | 441 | .visually-hidden { |
| 442 | 442 | position: absolute; width: 1px; height: 1px; overflow: hidden; |
| 443 | 443 | clip: rect(0 0 0 0); white-space: nowrap; |
| @@ -317,18 +317,18 @@ a.team-pill:hover { filter: brightness(.96); } | |||
| 317 | .login .tagline { font-size: 15px; opacity: .7; margin: 0 0 var(--space-8); max-width: 15em; } | 317 | .login .tagline { font-size: 15px; opacity: .7; margin: 0 0 var(--space-8); max-width: 15em; } |
| 318 | .form-error { font-size: 13px; color: var(--color-accent-700); margin: 0 0 var(--space-3); } | 318 | .form-error { font-size: 13px; color: var(--color-accent-700); margin: 0 0 var(--space-3); } |
| 319 | 319 | ||
| 320 | -/* — ajout étape 1 : liste de tâches — */ | 320 | +/* — ajout étape 1 : liste de catégories — */ |
| 321 | .step-hint { font-size: 13px; opacity: .7; margin: 0 0 var(--space-4); } | 321 | .step-hint { font-size: 13px; opacity: .7; margin: 0 0 var(--space-4); } |
| 322 | -.task-pick { display: grid; gap: var(--space-2); } | 322 | +.category-pick { display: grid; gap: var(--space-2); } |
| 323 | -.task-pick a { | 323 | +.category-pick a { |
| 324 | display: flex; align-items: center; gap: var(--space-3); | 324 | display: flex; align-items: center; gap: var(--space-3); |
| 325 | padding: var(--space-3) var(--space-4); background: var(--color-surface); | 325 | padding: var(--space-3) var(--space-4); background: var(--color-surface); |
| 326 | border: 1px solid transparent; border-radius: 999px; | 326 | border: 1px solid transparent; border-radius: 999px; |
| 327 | font-size: 16px; color: var(--color-text); text-decoration: none; | 327 | font-size: 16px; color: var(--color-text); text-decoration: none; |
| 328 | } | 328 | } |
| 329 | -.task-pick a:hover { border-color: var(--color-accent); } | 329 | +.category-pick a:hover { border-color: var(--color-accent); } |
| 330 | -.task-pick .tname { flex: 1; } | 330 | +.category-pick .tname { flex: 1; } |
| 331 | -.task-pick .tweek { font-size: 12px; opacity: .7; } | 331 | +.category-pick .tweek { font-size: 12px; opacity: .7; } |
| 332 | .dot { border-radius: 999px; flex: none; display: inline-block; } | 332 | .dot { border-radius: 999px; flex: none; display: inline-block; } |
| 333 | .dot-14 { width: 14px; height: 14px; } | 333 | .dot-14 { width: 14px; height: 14px; } |
| 334 | .dot-12 { width: 12px; height: 12px; } | 334 | .dot-12 { width: 12px; height: 12px; } |
| @@ -420,24 +420,24 @@ a.team-pill:hover { filter: brightness(.96); } | |||
| 420 | border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent); | 420 | border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent); |
| 421 | } | 421 | } |
| 422 | .times .tinfo { flex: 1; min-width: 0; } | 422 | .times .tinfo { flex: 1; min-width: 0; } |
| 423 | -.times .ttask { font-size: 15px; } | 423 | +.times .tcat { font-size: 15px; } |
| 424 | .times .tdate { font-size: 12px; opacity: .7; } | 424 | .times .tdate { font-size: 12px; opacity: .7; } |
| 425 | .times .tdur { font-size: 16px; font-family: var(--font-heading); } | 425 | .times .tdur { font-size: 16px; font-family: var(--font-heading); } |
| 426 | 426 | ||
| 427 | -/* — tâches — */ | 427 | +/* — catégories — */ |
| 428 | -.task-row { | 428 | +.category-row { |
| 429 | display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) 0; | 429 | display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) 0; |
| 430 | border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent); | 430 | border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent); |
| 431 | } | 431 | } |
| 432 | -.task-row form.rename { flex: 1; } | 432 | +.category-row form.rename { flex: 1; } |
| 433 | -.task-row .rename .input { | 433 | +.category-row .rename .input { |
| 434 | border-color: transparent; background: transparent; padding-left: 0; | 434 | border-color: transparent; background: transparent; padding-left: 0; |
| 435 | font-size: 15px; border-radius: 0; | 435 | font-size: 15px; border-radius: 0; |
| 436 | } | 436 | } |
| 437 | -.task-row .rename .input:hover { border-color: var(--color-divider); } | 437 | +.category-row .rename .input:hover { border-color: var(--color-divider); } |
| 438 | -.task-row.archived .rename .input { opacity: .45; } | 438 | +.category-row.archived .rename .input { opacity: .45; } |
| 439 | -.task-add-row { display: flex; gap: var(--space-2); margin-bottom: var(--space-6); } | 439 | +.category-add-row { display: flex; gap: var(--space-2); margin-bottom: var(--space-6); } |
| 440 | -.task-add-row .input { flex: 1; min-height: 44px; } | 440 | +.category-add-row .input { flex: 1; min-height: 44px; } |
| 441 | .visually-hidden { | 441 | .visually-hidden { |
| 442 | position: absolute; width: 1px; height: 1px; overflow: hidden; | 442 | position: absolute; width: 1px; height: 1px; overflow: hidden; |
| 443 | clip: rect(0 0 0 0); white-space: nowrap; | 443 | clip: rect(0 0 0 0); white-space: nowrap; |
added
migrations/0005_categories.sql +11 -0 | new file mode 100644 | ||
| @@ -0,0 +1,11 @@ | ||
| 1 | +-- « Tâche » devient « catégorie » (le mot tâche est repris par la todo list). | |
| 2 | +-- Renommage pur : aucune donnée ne bouge. Pendant la cohabitation bleu-vert, | |
| 3 | +-- l'ancien binaire (qui requête `tasks`) échoue le temps du basculement (~10 s) : | |
| 4 | +-- assumé, le renommage d'un objet n'est pas compatible avec les deux versions. | |
| 5 | +ALTER TABLE tasks RENAME TO categories; | |
| 6 | +ALTER TABLE categories RENAME CONSTRAINT tasks_pkey TO categories_pkey; | |
| 7 | +ALTER TABLE categories RENAME CONSTRAINT tasks_team_name_key TO categories_team_name_key; | |
| 8 | +ALTER TABLE categories RENAME CONSTRAINT tasks_team_id_fkey TO categories_team_id_fkey; | |
| 9 | + | |
| 10 | +ALTER TABLE time_entries RENAME COLUMN task_id TO category_id; | |
| 11 | +ALTER TABLE time_entries RENAME CONSTRAINT time_entries_task_id_fkey TO time_entries_category_id_fkey; | |
| new file mode 100644 | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | +-- « Tâche » devient « catégorie » (le mot tâche est repris par la todo list). | ||
| 2 | +-- Renommage pur : aucune donnée ne bouge. Pendant la cohabitation bleu-vert, | ||
| 3 | +-- l'ancien binaire (qui requête `tasks`) échoue le temps du basculement (~10 s) : | ||
| 4 | +-- assumé, le renommage d'un objet n'est pas compatible avec les deux versions. | ||
| 5 | +ALTER TABLE tasks RENAME TO categories; | ||
| 6 | +ALTER TABLE categories RENAME CONSTRAINT tasks_pkey TO categories_pkey; | ||
| 7 | +ALTER TABLE categories RENAME CONSTRAINT tasks_team_name_key TO categories_team_name_key; | ||
| 8 | +ALTER TABLE categories RENAME CONSTRAINT tasks_team_id_fkey TO categories_team_id_fkey; | ||
| 9 | + | ||
| 10 | +ALTER TABLE time_entries RENAME COLUMN task_id TO category_id; | ||
| 11 | +ALTER TABLE time_entries RENAME CONSTRAINT time_entries_task_id_fkey TO time_entries_category_id_fkey; | ||
modified
src/charts.rs +10 -10 | @@ -15,7 +15,7 @@ pub const PALETTE: [&str; 15] = [ | ||
| 15 | 15 | "#c98d8d", |
| 16 | 16 | ]; |
| 17 | 17 | |
| 18 | -/// task_id → couleur, à partir de la liste des tâches triée par id (création). | |
| 18 | +/// category_id → couleur, à partir de la liste des tâches triée par id (création). | |
| 19 | 19 | pub fn color_map(task_ids_in_creation_order: &[Uuid]) -> HashMap<Uuid, &'static str> { |
| 20 | 20 | task_ids_in_creation_order |
| 21 | 21 | .iter() |
| @@ -71,7 +71,7 @@ pub struct ChartView { | ||
| 71 | 71 | pub struct Sheet { |
| 72 | 72 | pub id: String, |
| 73 | 73 | pub color: &'static str, |
| 74 | - pub task: String, | |
| 74 | + pub category: String, | |
| 75 | 75 | pub who: String, |
| 76 | 76 | pub duration: String, |
| 77 | 77 | pub share: String, |
| @@ -129,13 +129,13 @@ pub fn build_chart( | ||
| 129 | 129 | let mut segs = Vec::new(); |
| 130 | 130 | if let Some(rs) = by_user.get(&u.id) { |
| 131 | 131 | for (ti, r) in rs.iter().enumerate() { |
| 132 | - let color = colors.get(&r.task_id).copied().unwrap_or(PALETTE[0]); | |
| 132 | + let color = colors.get(&r.category_id).copied().unwrap_or(PALETTE[0]); | |
| 133 | 133 | let sheet_id = format!("d-{key}-{ui}-{ti}"); |
| 134 | 134 | let share = (r.total * 100) / total.max(1); |
| 135 | 135 | sheets.push(Sheet { |
| 136 | 136 | id: sheet_id.clone(), |
| 137 | 137 | color, |
| 138 | - task: r.task_name.clone(), | |
| 138 | + category: r.category_name.clone(), | |
| 139 | 139 | who: u.username.clone(), |
| 140 | 140 | duration: fmt_minutes(r.total), |
| 141 | 141 | share: format!("{share} %"), |
| @@ -149,7 +149,7 @@ pub fn build_chart( | ||
| 149 | 149 | segs.push(Seg { |
| 150 | 150 | flex: r.total, |
| 151 | 151 | color, |
| 152 | - aria: format!("{} — {}", r.task_name, fmt_minutes(r.total)), | |
| 152 | + aria: format!("{} — {}", r.category_name, fmt_minutes(r.total)), | |
| 153 | 153 | sheet_id, |
| 154 | 154 | }); |
| 155 | 155 | } |
| @@ -186,8 +186,8 @@ pub fn legend(row_sets: &[&[StatRow]], colors: &HashMap<Uuid, &'static str>) -> | ||
| 186 | 186 | let mut seen: BTreeMap<String, &'static str> = BTreeMap::new(); |
| 187 | 187 | for rows in row_sets { |
| 188 | 188 | for r in *rows { |
| 189 | - seen.entry(r.task_name.clone()) | |
| 190 | - .or_insert_with(|| colors.get(&r.task_id).copied().unwrap_or(PALETTE[0])); | |
| 189 | + seen.entry(r.category_name.clone()) | |
| 190 | + .or_insert_with(|| colors.get(&r.category_id).copied().unwrap_or(PALETTE[0])); | |
| 191 | 191 | } |
| 192 | 192 | } |
| 193 | 193 | seen.into_iter() |
| @@ -206,12 +206,12 @@ mod tests { | ||
| 206 | 206 | } |
| 207 | 207 | } |
| 208 | 208 | |
| 209 | - fn row(uid: u128, tid: u128, task: &str, total: i64, entries: i64) -> StatRow { | |
| 209 | + fn row(uid: u128, tid: u128, category: &str, total: i64, entries: i64) -> StatRow { | |
| 210 | 210 | StatRow { |
| 211 | 211 | user_id: Uuid::from_u128(uid), |
| 212 | 212 | username: String::new(), |
| 213 | - task_id: Uuid::from_u128(tid), | |
| 214 | - task_name: task.into(), | |
| 213 | + category_id: Uuid::from_u128(tid), | |
| 214 | + category_name: category.into(), | |
| 215 | 215 | total, |
| 216 | 216 | entries, |
| 217 | 217 | } |
| @@ -15,7 +15,7 @@ pub const PALETTE: [&str; 15] = [ | |||
| 15 | "#c98d8d", | 15 | "#c98d8d", |
| 16 | ]; | 16 | ]; |
| 17 | 17 | ||
| 18 | -/// task_id → couleur, à partir de la liste des tâches triée par id (création). | 18 | +/// category_id → couleur, à partir de la liste des tâches triée par id (création). |
| 19 | pub fn color_map(task_ids_in_creation_order: &[Uuid]) -> HashMap<Uuid, &'static str> { | 19 | pub fn color_map(task_ids_in_creation_order: &[Uuid]) -> HashMap<Uuid, &'static str> { |
| 20 | task_ids_in_creation_order | 20 | task_ids_in_creation_order |
| 21 | .iter() | 21 | .iter() |
| @@ -71,7 +71,7 @@ pub struct ChartView { | |||
| 71 | pub struct Sheet { | 71 | pub struct Sheet { |
| 72 | pub id: String, | 72 | pub id: String, |
| 73 | pub color: &'static str, | 73 | pub color: &'static str, |
| 74 | - pub task: String, | 74 | + pub category: String, |
| 75 | pub who: String, | 75 | pub who: String, |
| 76 | pub duration: String, | 76 | pub duration: String, |
| 77 | pub share: String, | 77 | pub share: String, |
| @@ -129,13 +129,13 @@ pub fn build_chart( | |||
| 129 | let mut segs = Vec::new(); | 129 | let mut segs = Vec::new(); |
| 130 | if let Some(rs) = by_user.get(&u.id) { | 130 | if let Some(rs) = by_user.get(&u.id) { |
| 131 | for (ti, r) in rs.iter().enumerate() { | 131 | for (ti, r) in rs.iter().enumerate() { |
| 132 | - let color = colors.get(&r.task_id).copied().unwrap_or(PALETTE[0]); | 132 | + let color = colors.get(&r.category_id).copied().unwrap_or(PALETTE[0]); |
| 133 | let sheet_id = format!("d-{key}-{ui}-{ti}"); | 133 | let sheet_id = format!("d-{key}-{ui}-{ti}"); |
| 134 | let share = (r.total * 100) / total.max(1); | 134 | let share = (r.total * 100) / total.max(1); |
| 135 | sheets.push(Sheet { | 135 | sheets.push(Sheet { |
| 136 | id: sheet_id.clone(), | 136 | id: sheet_id.clone(), |
| 137 | color, | 137 | color, |
| 138 | - task: r.task_name.clone(), | 138 | + category: r.category_name.clone(), |
| 139 | who: u.username.clone(), | 139 | who: u.username.clone(), |
| 140 | duration: fmt_minutes(r.total), | 140 | duration: fmt_minutes(r.total), |
| 141 | share: format!("{share} %"), | 141 | share: format!("{share} %"), |
| @@ -149,7 +149,7 @@ pub fn build_chart( | |||
| 149 | segs.push(Seg { | 149 | segs.push(Seg { |
| 150 | flex: r.total, | 150 | flex: r.total, |
| 151 | color, | 151 | color, |
| 152 | - aria: format!("{} — {}", r.task_name, fmt_minutes(r.total)), | 152 | + aria: format!("{} — {}", r.category_name, fmt_minutes(r.total)), |
| 153 | sheet_id, | 153 | sheet_id, |
| 154 | }); | 154 | }); |
| 155 | } | 155 | } |
| @@ -186,8 +186,8 @@ pub fn legend(row_sets: &[&[StatRow]], colors: &HashMap<Uuid, &'static str>) -> | |||
| 186 | let mut seen: BTreeMap<String, &'static str> = BTreeMap::new(); | 186 | let mut seen: BTreeMap<String, &'static str> = BTreeMap::new(); |
| 187 | for rows in row_sets { | 187 | for rows in row_sets { |
| 188 | for r in *rows { | 188 | for r in *rows { |
| 189 | - seen.entry(r.task_name.clone()) | 189 | + seen.entry(r.category_name.clone()) |
| 190 | - .or_insert_with(|| colors.get(&r.task_id).copied().unwrap_or(PALETTE[0])); | 190 | + .or_insert_with(|| colors.get(&r.category_id).copied().unwrap_or(PALETTE[0])); |
| 191 | } | 191 | } |
| 192 | } | 192 | } |
| 193 | seen.into_iter() | 193 | seen.into_iter() |
| @@ -206,12 +206,12 @@ mod tests { | |||
| 206 | } | 206 | } |
| 207 | } | 207 | } |
| 208 | 208 | ||
| 209 | - fn row(uid: u128, tid: u128, task: &str, total: i64, entries: i64) -> StatRow { | 209 | + fn row(uid: u128, tid: u128, category: &str, total: i64, entries: i64) -> StatRow { |
| 210 | StatRow { | 210 | StatRow { |
| 211 | user_id: Uuid::from_u128(uid), | 211 | user_id: Uuid::from_u128(uid), |
| 212 | username: String::new(), | 212 | username: String::new(), |
| 213 | - task_id: Uuid::from_u128(tid), | 213 | + category_id: Uuid::from_u128(tid), |
| 214 | - task_name: task.into(), | 214 | + category_name: category.into(), |
| 215 | total, | 215 | total, |
| 216 | entries, | 216 | entries, |
| 217 | } | 217 | } |
renamed
src/db/categories.rs +12 -12 | similarity index 54% | ||
| rename from src/db/tasks.rs | ||
| rename to src/db/categories.rs | ||
| @@ -2,24 +2,24 @@ use sqlx::PgPool; | ||
| 2 | 2 | use uuid::Uuid; |
| 3 | 3 | |
| 4 | 4 | #[derive(sqlx::FromRow)] |
| 5 | -pub struct TaskRow { | |
| 5 | +pub struct CategoryRow { | |
| 6 | 6 | pub id: Uuid, |
| 7 | 7 | pub name: String, |
| 8 | 8 | pub archived: bool, |
| 9 | 9 | } |
| 10 | 10 | |
| 11 | -/// Toutes les tâches de l'équipe (écran Tâches). | |
| 12 | -pub async fn list_all(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec<TaskRow>> { | |
| 13 | - sqlx::query_as("SELECT id, name, archived FROM tasks WHERE team_id = $1 ORDER BY archived, name") | |
| 11 | +/// Toutes les catégories de l'équipe (écran Catégories). | |
| 12 | +pub async fn list_all(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec<CategoryRow>> { | |
| 13 | + sqlx::query_as("SELECT id, name, archived FROM categories WHERE team_id = $1 ORDER BY archived, name") | |
| 14 | 14 | .bind(team_id) |
| 15 | 15 | .fetch_all(pool) |
| 16 | 16 | .await |
| 17 | 17 | } |
| 18 | 18 | |
| 19 | -/// Les tâches de l'équipe par ordre de création (UUIDv7 ≈ chronologique) — | |
| 20 | -/// c'est l'ordre qui fixe la couleur de chaque tâche, par équipe. | |
| 21 | -pub async fn list_by_creation(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec<TaskRow>> { | |
| 22 | - sqlx::query_as("SELECT id, name, archived FROM tasks WHERE team_id = $1 ORDER BY id") | |
| 19 | +/// Les catégories de l'équipe par ordre de création (UUIDv7 ≈ chronologique) — | |
| 20 | +/// c'est l'ordre qui fixe la couleur de chaque catégorie, par équipe. | |
| 21 | +pub async fn list_by_creation(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec<CategoryRow>> { | |
| 22 | + sqlx::query_as("SELECT id, name, archived FROM categories WHERE team_id = $1 ORDER BY id") | |
| 23 | 23 | .bind(team_id) |
| 24 | 24 | .fetch_all(pool) |
| 25 | 25 | .await |
| @@ -27,7 +27,7 @@ pub async fn list_by_creation(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec< | ||
| 27 | 27 | |
| 28 | 28 | pub async fn create(pool: &PgPool, team_id: Uuid, name: &str) -> sqlx::Result<Uuid> { |
| 29 | 29 | let id = Uuid::now_v7(); |
| 30 | - sqlx::query("INSERT INTO tasks (id, team_id, name) VALUES ($1, $2, $3)") | |
| 30 | + sqlx::query("INSERT INTO categories (id, team_id, name) VALUES ($1, $2, $3)") | |
| 31 | 31 | .bind(id) |
| 32 | 32 | .bind(team_id) |
| 33 | 33 | .bind(name) |
| @@ -36,9 +36,9 @@ pub async fn create(pool: &PgPool, team_id: Uuid, name: &str) -> sqlx::Result<Uu | ||
| 36 | 36 | Ok(id) |
| 37 | 37 | } |
| 38 | 38 | |
| 39 | -/// Scopé à l'équipe active : 0 ligne = tâche d'une autre équipe / inexistante. | |
| 39 | +/// Scopé à l'équipe active : 0 ligne = catégorie d'une autre équipe / inexistante. | |
| 40 | 40 | pub async fn rename(pool: &PgPool, id: Uuid, team_id: Uuid, name: &str) -> sqlx::Result<u64> { |
| 41 | - let res = sqlx::query("UPDATE tasks SET name = $1 WHERE id = $2 AND team_id = $3") | |
| 41 | + let res = sqlx::query("UPDATE categories SET name = $1 WHERE id = $2 AND team_id = $3") | |
| 42 | 42 | .bind(name) |
| 43 | 43 | .bind(id) |
| 44 | 44 | .bind(team_id) |
| @@ -48,7 +48,7 @@ pub async fn rename(pool: &PgPool, id: Uuid, team_id: Uuid, name: &str) -> sqlx: | ||
| 48 | 48 | } |
| 49 | 49 | |
| 50 | 50 | pub async fn toggle_archived(pool: &PgPool, id: Uuid, team_id: Uuid) -> sqlx::Result<u64> { |
| 51 | - let res = sqlx::query("UPDATE tasks SET archived = NOT archived WHERE id = $1 AND team_id = $2") | |
| 51 | + let res = sqlx::query("UPDATE categories SET archived = NOT archived WHERE id = $1 AND team_id = $2") | |
| 52 | 52 | .bind(id) |
| 53 | 53 | .bind(team_id) |
| 54 | 54 | .execute(pool) |
| similarity index 54% | |||
| rename from src/db/tasks.rs | |||
| rename to src/db/categories.rs | |||
| @@ -2,24 +2,24 @@ use sqlx::PgPool; | |||
| 2 | use uuid::Uuid; | 2 | use uuid::Uuid; |
| 3 | 3 | ||
| 4 | #[derive(sqlx::FromRow)] | 4 | #[derive(sqlx::FromRow)] |
| 5 | -pub struct TaskRow { | 5 | +pub struct CategoryRow { |
| 6 | pub id: Uuid, | 6 | pub id: Uuid, |
| 7 | pub name: String, | 7 | pub name: String, |
| 8 | pub archived: bool, | 8 | pub archived: bool, |
| 9 | } | 9 | } |
| 10 | 10 | ||
| 11 | -/// Toutes les tâches de l'équipe (écran Tâches). | 11 | +/// Toutes les catégories de l'équipe (écran Catégories). |
| 12 | -pub async fn list_all(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec<TaskRow>> { | 12 | +pub async fn list_all(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec<CategoryRow>> { |
| 13 | - sqlx::query_as("SELECT id, name, archived FROM tasks WHERE team_id = $1 ORDER BY archived, name") | 13 | + sqlx::query_as("SELECT id, name, archived FROM categories WHERE team_id = $1 ORDER BY archived, name") |
| 14 | .bind(team_id) | 14 | .bind(team_id) |
| 15 | .fetch_all(pool) | 15 | .fetch_all(pool) |
| 16 | .await | 16 | .await |
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | -/// Les tâches de l'équipe par ordre de création (UUIDv7 ≈ chronologique) — | 19 | +/// Les catégories de l'équipe par ordre de création (UUIDv7 ≈ chronologique) — |
| 20 | -/// c'est l'ordre qui fixe la couleur de chaque tâche, par équipe. | 20 | +/// c'est l'ordre qui fixe la couleur de chaque catégorie, par équipe. |
| 21 | -pub async fn list_by_creation(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec<TaskRow>> { | 21 | +pub async fn list_by_creation(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec<CategoryRow>> { |
| 22 | - sqlx::query_as("SELECT id, name, archived FROM tasks WHERE team_id = $1 ORDER BY id") | 22 | + sqlx::query_as("SELECT id, name, archived FROM categories WHERE team_id = $1 ORDER BY id") |
| 23 | .bind(team_id) | 23 | .bind(team_id) |
| 24 | .fetch_all(pool) | 24 | .fetch_all(pool) |
| 25 | .await | 25 | .await |
| @@ -27,7 +27,7 @@ pub async fn list_by_creation(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec< | |||
| 27 | 27 | ||
| 28 | pub async fn create(pool: &PgPool, team_id: Uuid, name: &str) -> sqlx::Result<Uuid> { | 28 | pub async fn create(pool: &PgPool, team_id: Uuid, name: &str) -> sqlx::Result<Uuid> { |
| 29 | let id = Uuid::now_v7(); | 29 | let id = Uuid::now_v7(); |
| 30 | - sqlx::query("INSERT INTO tasks (id, team_id, name) VALUES ($1, $2, $3)") | 30 | + sqlx::query("INSERT INTO categories (id, team_id, name) VALUES ($1, $2, $3)") |
| 31 | .bind(id) | 31 | .bind(id) |
| 32 | .bind(team_id) | 32 | .bind(team_id) |
| 33 | .bind(name) | 33 | .bind(name) |
| @@ -36,9 +36,9 @@ pub async fn create(pool: &PgPool, team_id: Uuid, name: &str) -> sqlx::Result<Uu | |||
| 36 | Ok(id) | 36 | Ok(id) |
| 37 | } | 37 | } |
| 38 | 38 | ||
| 39 | -/// Scopé à l'équipe active : 0 ligne = tâche d'une autre équipe / inexistante. | 39 | +/// Scopé à l'équipe active : 0 ligne = catégorie d'une autre équipe / inexistante. |
| 40 | pub async fn rename(pool: &PgPool, id: Uuid, team_id: Uuid, name: &str) -> sqlx::Result<u64> { | 40 | pub async fn rename(pool: &PgPool, id: Uuid, team_id: Uuid, name: &str) -> sqlx::Result<u64> { |
| 41 | - let res = sqlx::query("UPDATE tasks SET name = $1 WHERE id = $2 AND team_id = $3") | 41 | + let res = sqlx::query("UPDATE categories SET name = $1 WHERE id = $2 AND team_id = $3") |
| 42 | .bind(name) | 42 | .bind(name) |
| 43 | .bind(id) | 43 | .bind(id) |
| 44 | .bind(team_id) | 44 | .bind(team_id) |
| @@ -48,7 +48,7 @@ pub async fn rename(pool: &PgPool, id: Uuid, team_id: Uuid, name: &str) -> sqlx: | |||
| 48 | } | 48 | } |
| 49 | 49 | ||
| 50 | pub async fn toggle_archived(pool: &PgPool, id: Uuid, team_id: Uuid) -> sqlx::Result<u64> { | 50 | pub async fn toggle_archived(pool: &PgPool, id: Uuid, team_id: Uuid) -> sqlx::Result<u64> { |
| 51 | - let res = sqlx::query("UPDATE tasks SET archived = NOT archived WHERE id = $1 AND team_id = $2") | 51 | + let res = sqlx::query("UPDATE categories SET archived = NOT archived WHERE id = $1 AND team_id = $2") |
| 52 | .bind(id) | 52 | .bind(id) |
| 53 | .bind(team_id) | 53 | .bind(team_id) |
| 54 | .execute(pool) | 54 | .execute(pool) |
modified
src/db/entries.rs +23 -23 | @@ -5,30 +5,30 @@ use uuid::Uuid; | ||
| 5 | 5 | #[derive(sqlx::FromRow)] |
| 6 | 6 | pub struct EntryRow { |
| 7 | 7 | pub id: Uuid, |
| 8 | - pub task_id: Uuid, | |
| 9 | - pub task_name: String, | |
| 8 | + pub category_id: Uuid, | |
| 9 | + pub category_name: String, | |
| 10 | 10 | pub minutes: i32, |
| 11 | 11 | pub entry_date: NaiveDate, |
| 12 | 12 | } |
| 13 | 13 | |
| 14 | -/// L'insert n'aboutit que si la tâche appartient à l'équipe active : | |
| 15 | -/// 0 ligne = tâche d'une autre équipe (ou inexistante). | |
| 14 | +/// L'insert n'aboutit que si la catégorie appartient à l'équipe active : | |
| 15 | +/// 0 ligne = catégorie d'une autre équipe (ou inexistante). | |
| 16 | 16 | pub async fn insert( |
| 17 | 17 | pool: &PgPool, |
| 18 | 18 | user_id: Uuid, |
| 19 | 19 | team_id: Uuid, |
| 20 | - task_id: Uuid, | |
| 20 | + category_id: Uuid, | |
| 21 | 21 | minutes: i32, |
| 22 | 22 | entry_date: NaiveDate, |
| 23 | 23 | ) -> sqlx::Result<u64> { |
| 24 | 24 | let res = sqlx::query( |
| 25 | - "INSERT INTO time_entries (id, user_id, task_id, minutes, entry_date) | |
| 25 | + "INSERT INTO time_entries (id, user_id, category_id, minutes, entry_date) | |
| 26 | 26 | SELECT $1, $2, $3, $4, $5 |
| 27 | - WHERE EXISTS (SELECT 1 FROM tasks WHERE id = $3 AND team_id = $6)", | |
| 27 | + WHERE EXISTS (SELECT 1 FROM categories WHERE id = $3 AND team_id = $6)", | |
| 28 | 28 | ) |
| 29 | 29 | .bind(Uuid::now_v7()) |
| 30 | 30 | .bind(user_id) |
| 31 | - .bind(task_id) | |
| 31 | + .bind(category_id) | |
| 32 | 32 | .bind(minutes) |
| 33 | 33 | .bind(entry_date) |
| 34 | 34 | .bind(team_id) |
| @@ -47,8 +47,8 @@ pub async fn list_first( | ||
| 47 | 47 | limit: i64, |
| 48 | 48 | ) -> sqlx::Result<(Vec<EntryRow>, bool)> { |
| 49 | 49 | let mut rows: Vec<EntryRow> = sqlx::query_as( |
| 50 | - "SELECT e.id, e.task_id, t.name AS task_name, e.minutes, e.entry_date | |
| 51 | - FROM time_entries e JOIN tasks t ON t.id = e.task_id | |
| 50 | + "SELECT e.id, e.category_id, t.name AS category_name, e.minutes, e.entry_date | |
| 51 | + FROM time_entries e JOIN categories t ON t.id = e.category_id | |
| 52 | 52 | WHERE e.user_id = $1 AND t.team_id = $2 |
| 53 | 53 | ORDER BY e.entry_date DESC, e.id DESC |
| 54 | 54 | LIMIT $3", |
| @@ -63,7 +63,7 @@ pub async fn list_first( | ||
| 63 | 63 | Ok((rows, has_more)) |
| 64 | 64 | } |
| 65 | 65 | |
| 66 | -/// Mes minutes de la semaine en cours dans l'équipe, par tâche (étape 1). | |
| 66 | +/// Mes minutes de la semaine en cours dans l'équipe, par catégorie (étape 1). | |
| 67 | 67 | pub async fn week_totals_for_user( |
| 68 | 68 | pool: &PgPool, |
| 69 | 69 | user_id: Uuid, |
| @@ -72,10 +72,10 @@ pub async fn week_totals_for_user( | ||
| 72 | 72 | to: NaiveDate, |
| 73 | 73 | ) -> sqlx::Result<Vec<(Uuid, i64)>> { |
| 74 | 74 | sqlx::query_as( |
| 75 | - "SELECT e.task_id, SUM(e.minutes)::BIGINT | |
| 76 | - FROM time_entries e JOIN tasks t ON t.id = e.task_id | |
| 75 | + "SELECT e.category_id, SUM(e.minutes)::BIGINT | |
| 76 | + FROM time_entries e JOIN categories t ON t.id = e.category_id | |
| 77 | 77 | WHERE e.user_id = $1 AND t.team_id = $2 AND e.entry_date >= $3 AND e.entry_date < $4 |
| 78 | - GROUP BY e.task_id", | |
| 78 | + GROUP BY e.category_id", | |
| 79 | 79 | ) |
| 80 | 80 | .bind(user_id) |
| 81 | 81 | .bind(team_id) |
| @@ -93,8 +93,8 @@ pub async fn get_owned( | ||
| 93 | 93 | team_id: Uuid, |
| 94 | 94 | ) -> sqlx::Result<Option<EntryRow>> { |
| 95 | 95 | sqlx::query_as( |
| 96 | - "SELECT e.id, e.task_id, t.name AS task_name, e.minutes, e.entry_date | |
| 97 | - FROM time_entries e JOIN tasks t ON t.id = e.task_id | |
| 96 | + "SELECT e.id, e.category_id, t.name AS category_name, e.minutes, e.entry_date | |
| 97 | + FROM time_entries e JOIN categories t ON t.id = e.category_id | |
| 98 | 98 | WHERE e.id = $1 AND e.user_id = $2 AND t.team_id = $3", |
| 99 | 99 | ) |
| 100 | 100 | .bind(id) |
| @@ -105,23 +105,23 @@ pub async fn get_owned( | ||
| 105 | 105 | } |
| 106 | 106 | |
| 107 | 107 | /// Ownership + équipe vérifiés en SQL : l'entrée doit être à moi et dans |
| 108 | -/// l'équipe active, la nouvelle tâche aussi. 0 ligne touchée = refus. | |
| 108 | +/// l'équipe active, la nouvelle catégorie aussi. 0 ligne touchée = refus. | |
| 109 | 109 | pub async fn update_owned( |
| 110 | 110 | pool: &PgPool, |
| 111 | 111 | id: Uuid, |
| 112 | 112 | user_id: Uuid, |
| 113 | 113 | team_id: Uuid, |
| 114 | - task_id: Uuid, | |
| 114 | + category_id: Uuid, | |
| 115 | 115 | minutes: i32, |
| 116 | 116 | entry_date: NaiveDate, |
| 117 | 117 | ) -> sqlx::Result<u64> { |
| 118 | 118 | let res = sqlx::query( |
| 119 | - "UPDATE time_entries SET task_id = $1, minutes = $2, entry_date = $3 | |
| 119 | + "UPDATE time_entries SET category_id = $1, minutes = $2, entry_date = $3 | |
| 120 | 120 | WHERE id = $4 AND user_id = $5 |
| 121 | - AND task_id IN (SELECT id FROM tasks WHERE team_id = $6) | |
| 122 | - AND $1 IN (SELECT id FROM tasks WHERE team_id = $6)", | |
| 121 | + AND category_id IN (SELECT id FROM categories WHERE team_id = $6) | |
| 122 | + AND $1 IN (SELECT id FROM categories WHERE team_id = $6)", | |
| 123 | 123 | ) |
| 124 | - .bind(task_id) | |
| 124 | + .bind(category_id) | |
| 125 | 125 | .bind(minutes) |
| 126 | 126 | .bind(entry_date) |
| 127 | 127 | .bind(id) |
| @@ -141,7 +141,7 @@ pub async fn delete_owned( | ||
| 141 | 141 | let res = sqlx::query( |
| 142 | 142 | "DELETE FROM time_entries |
| 143 | 143 | WHERE id = $1 AND user_id = $2 |
| 144 | - AND task_id IN (SELECT id FROM tasks WHERE team_id = $3)", | |
| 144 | + AND category_id IN (SELECT id FROM categories WHERE team_id = $3)", | |
| 145 | 145 | ) |
| 146 | 146 | .bind(id) |
| 147 | 147 | .bind(user_id) |
| @@ -5,30 +5,30 @@ use uuid::Uuid; | |||
| 5 | #[derive(sqlx::FromRow)] | 5 | #[derive(sqlx::FromRow)] |
| 6 | pub struct EntryRow { | 6 | pub struct EntryRow { |
| 7 | pub id: Uuid, | 7 | pub id: Uuid, |
| 8 | - pub task_id: Uuid, | 8 | + pub category_id: Uuid, |
| 9 | - pub task_name: String, | 9 | + pub category_name: String, |
| 10 | pub minutes: i32, | 10 | pub minutes: i32, |
| 11 | pub entry_date: NaiveDate, | 11 | pub entry_date: NaiveDate, |
| 12 | } | 12 | } |
| 13 | 13 | ||
| 14 | -/// L'insert n'aboutit que si la tâche appartient à l'équipe active : | 14 | +/// L'insert n'aboutit que si la catégorie appartient à l'équipe active : |
| 15 | -/// 0 ligne = tâche d'une autre équipe (ou inexistante). | 15 | +/// 0 ligne = catégorie d'une autre équipe (ou inexistante). |
| 16 | pub async fn insert( | 16 | pub async fn insert( |
| 17 | pool: &PgPool, | 17 | pool: &PgPool, |
| 18 | user_id: Uuid, | 18 | user_id: Uuid, |
| 19 | team_id: Uuid, | 19 | team_id: Uuid, |
| 20 | - task_id: Uuid, | 20 | + category_id: Uuid, |
| 21 | minutes: i32, | 21 | minutes: i32, |
| 22 | entry_date: NaiveDate, | 22 | entry_date: NaiveDate, |
| 23 | ) -> sqlx::Result<u64> { | 23 | ) -> sqlx::Result<u64> { |
| 24 | let res = sqlx::query( | 24 | let res = sqlx::query( |
| 25 | - "INSERT INTO time_entries (id, user_id, task_id, minutes, entry_date) | 25 | + "INSERT INTO time_entries (id, user_id, category_id, minutes, entry_date) |
| 26 | SELECT $1, $2, $3, $4, $5 | 26 | SELECT $1, $2, $3, $4, $5 |
| 27 | - WHERE EXISTS (SELECT 1 FROM tasks WHERE id = $3 AND team_id = $6)", | 27 | + WHERE EXISTS (SELECT 1 FROM categories WHERE id = $3 AND team_id = $6)", |
| 28 | ) | 28 | ) |
| 29 | .bind(Uuid::now_v7()) | 29 | .bind(Uuid::now_v7()) |
| 30 | .bind(user_id) | 30 | .bind(user_id) |
| 31 | - .bind(task_id) | 31 | + .bind(category_id) |
| 32 | .bind(minutes) | 32 | .bind(minutes) |
| 33 | .bind(entry_date) | 33 | .bind(entry_date) |
| 34 | .bind(team_id) | 34 | .bind(team_id) |
| @@ -47,8 +47,8 @@ pub async fn list_first( | |||
| 47 | limit: i64, | 47 | limit: i64, |
| 48 | ) -> sqlx::Result<(Vec<EntryRow>, bool)> { | 48 | ) -> sqlx::Result<(Vec<EntryRow>, bool)> { |
| 49 | let mut rows: Vec<EntryRow> = sqlx::query_as( | 49 | let mut rows: Vec<EntryRow> = sqlx::query_as( |
| 50 | - "SELECT e.id, e.task_id, t.name AS task_name, e.minutes, e.entry_date | 50 | + "SELECT e.id, e.category_id, t.name AS category_name, e.minutes, e.entry_date |
| 51 | - FROM time_entries e JOIN tasks t ON t.id = e.task_id | 51 | + FROM time_entries e JOIN categories t ON t.id = e.category_id |
| 52 | WHERE e.user_id = $1 AND t.team_id = $2 | 52 | WHERE e.user_id = $1 AND t.team_id = $2 |
| 53 | ORDER BY e.entry_date DESC, e.id DESC | 53 | ORDER BY e.entry_date DESC, e.id DESC |
| 54 | LIMIT $3", | 54 | LIMIT $3", |
| @@ -63,7 +63,7 @@ pub async fn list_first( | |||
| 63 | Ok((rows, has_more)) | 63 | Ok((rows, has_more)) |
| 64 | } | 64 | } |
| 65 | 65 | ||
| 66 | -/// Mes minutes de la semaine en cours dans l'équipe, par tâche (étape 1). | 66 | +/// Mes minutes de la semaine en cours dans l'équipe, par catégorie (étape 1). |
| 67 | pub async fn week_totals_for_user( | 67 | pub async fn week_totals_for_user( |
| 68 | pool: &PgPool, | 68 | pool: &PgPool, |
| 69 | user_id: Uuid, | 69 | user_id: Uuid, |
| @@ -72,10 +72,10 @@ pub async fn week_totals_for_user( | |||
| 72 | to: NaiveDate, | 72 | to: NaiveDate, |
| 73 | ) -> sqlx::Result<Vec<(Uuid, i64)>> { | 73 | ) -> sqlx::Result<Vec<(Uuid, i64)>> { |
| 74 | sqlx::query_as( | 74 | sqlx::query_as( |
| 75 | - "SELECT e.task_id, SUM(e.minutes)::BIGINT | 75 | + "SELECT e.category_id, SUM(e.minutes)::BIGINT |
| 76 | - FROM time_entries e JOIN tasks t ON t.id = e.task_id | 76 | + FROM time_entries e JOIN categories t ON t.id = e.category_id |
| 77 | WHERE e.user_id = $1 AND t.team_id = $2 AND e.entry_date >= $3 AND e.entry_date < $4 | 77 | WHERE e.user_id = $1 AND t.team_id = $2 AND e.entry_date >= $3 AND e.entry_date < $4 |
| 78 | - GROUP BY e.task_id", | 78 | + GROUP BY e.category_id", |
| 79 | ) | 79 | ) |
| 80 | .bind(user_id) | 80 | .bind(user_id) |
| 81 | .bind(team_id) | 81 | .bind(team_id) |
| @@ -93,8 +93,8 @@ pub async fn get_owned( | |||
| 93 | team_id: Uuid, | 93 | team_id: Uuid, |
| 94 | ) -> sqlx::Result<Option<EntryRow>> { | 94 | ) -> sqlx::Result<Option<EntryRow>> { |
| 95 | sqlx::query_as( | 95 | sqlx::query_as( |
| 96 | - "SELECT e.id, e.task_id, t.name AS task_name, e.minutes, e.entry_date | 96 | + "SELECT e.id, e.category_id, t.name AS category_name, e.minutes, e.entry_date |
| 97 | - FROM time_entries e JOIN tasks t ON t.id = e.task_id | 97 | + FROM time_entries e JOIN categories t ON t.id = e.category_id |
| 98 | WHERE e.id = $1 AND e.user_id = $2 AND t.team_id = $3", | 98 | WHERE e.id = $1 AND e.user_id = $2 AND t.team_id = $3", |
| 99 | ) | 99 | ) |
| 100 | .bind(id) | 100 | .bind(id) |
| @@ -105,23 +105,23 @@ pub async fn get_owned( | |||
| 105 | } | 105 | } |
| 106 | 106 | ||
| 107 | /// Ownership + équipe vérifiés en SQL : l'entrée doit être à moi et dans | 107 | /// Ownership + équipe vérifiés en SQL : l'entrée doit être à moi et dans |
| 108 | -/// l'équipe active, la nouvelle tâche aussi. 0 ligne touchée = refus. | 108 | +/// l'équipe active, la nouvelle catégorie aussi. 0 ligne touchée = refus. |
| 109 | pub async fn update_owned( | 109 | pub async fn update_owned( |
| 110 | pool: &PgPool, | 110 | pool: &PgPool, |
| 111 | id: Uuid, | 111 | id: Uuid, |
| 112 | user_id: Uuid, | 112 | user_id: Uuid, |
| 113 | team_id: Uuid, | 113 | team_id: Uuid, |
| 114 | - task_id: Uuid, | 114 | + category_id: Uuid, |
| 115 | minutes: i32, | 115 | minutes: i32, |
| 116 | entry_date: NaiveDate, | 116 | entry_date: NaiveDate, |
| 117 | ) -> sqlx::Result<u64> { | 117 | ) -> sqlx::Result<u64> { |
| 118 | let res = sqlx::query( | 118 | let res = sqlx::query( |
| 119 | - "UPDATE time_entries SET task_id = $1, minutes = $2, entry_date = $3 | 119 | + "UPDATE time_entries SET category_id = $1, minutes = $2, entry_date = $3 |
| 120 | WHERE id = $4 AND user_id = $5 | 120 | WHERE id = $4 AND user_id = $5 |
| 121 | - AND task_id IN (SELECT id FROM tasks WHERE team_id = $6) | 121 | + AND category_id IN (SELECT id FROM categories WHERE team_id = $6) |
| 122 | - AND $1 IN (SELECT id FROM tasks WHERE team_id = $6)", | 122 | + AND $1 IN (SELECT id FROM categories WHERE team_id = $6)", |
| 123 | ) | 123 | ) |
| 124 | - .bind(task_id) | 124 | + .bind(category_id) |
| 125 | .bind(minutes) | 125 | .bind(minutes) |
| 126 | .bind(entry_date) | 126 | .bind(entry_date) |
| 127 | .bind(id) | 127 | .bind(id) |
| @@ -141,7 +141,7 @@ pub async fn delete_owned( | |||
| 141 | let res = sqlx::query( | 141 | let res = sqlx::query( |
| 142 | "DELETE FROM time_entries | 142 | "DELETE FROM time_entries |
| 143 | WHERE id = $1 AND user_id = $2 | 143 | WHERE id = $1 AND user_id = $2 |
| 144 | - AND task_id IN (SELECT id FROM tasks WHERE team_id = $3)", | 144 | + AND category_id IN (SELECT id FROM categories WHERE team_id = $3)", |
| 145 | ) | 145 | ) |
| 146 | .bind(id) | 146 | .bind(id) |
| 147 | .bind(user_id) | 147 | .bind(user_id) |
modified
src/db/mod.rs +1 -1 | @@ -1,7 +1,7 @@ | ||
| 1 | 1 | pub mod entries; |
| 2 | 2 | pub mod invitations; |
| 3 | 3 | pub mod stats; |
| 4 | -pub mod tasks; | |
| 4 | +pub mod categories; | |
| 5 | 5 | pub mod timers; |
| 6 | 6 | pub mod teams; |
| 7 | 7 | pub mod users; |
| @@ -1,7 +1,7 @@ | |||
| 1 | pub mod entries; | 1 | pub mod entries; |
| 2 | pub mod invitations; | 2 | pub mod invitations; |
| 3 | pub mod stats; | 3 | pub mod stats; |
| 4 | -pub mod tasks; | 4 | +pub mod categories; |
| 5 | pub mod timers; | 5 | pub mod timers; |
| 6 | pub mod teams; | 6 | pub mod teams; |
| 7 | pub mod users; | 7 | pub mod users; |
modified
src/db/stats.rs +8 -8 | @@ -7,16 +7,16 @@ pub struct StatRow { | ||
| 7 | 7 | pub user_id: Uuid, |
| 8 | 8 | /// Sert à faire apparaître les ex-membres qui ont des temps (historique intact). |
| 9 | 9 | pub username: String, |
| 10 | - pub task_id: Uuid, | |
| 11 | - pub task_name: String, | |
| 10 | + pub category_id: Uuid, | |
| 11 | + pub category_name: String, | |
| 12 | 12 | pub total: i64, |
| 13 | 13 | /// Nombre de saisies (pour la sheet de détail). |
| 14 | 14 | pub entries: i64, |
| 15 | 15 | } |
| 16 | 16 | |
| 17 | -/// Totaux par utilisateur × tâche de l'équipe sur [from, to) ; | |
| 18 | -/// bornes NULL = depuis le début. Les tâches archivées restent incluses, | |
| 19 | -/// et les temps des ex-membres aussi (l'équipe se déduit de la tâche). | |
| 17 | +/// Totaux par utilisateur × catégorie de l'équipe sur [from, to) ; | |
| 18 | +/// bornes NULL = depuis le début. Les catégories archivées restent incluses, | |
| 19 | +/// et les temps des ex-membres aussi (l'équipe se déduit de la catégorie). | |
| 20 | 20 | pub async fn totals( |
| 21 | 21 | pool: &PgPool, |
| 22 | 22 | team_id: Uuid, |
| @@ -24,15 +24,15 @@ pub async fn totals( | ||
| 24 | 24 | to: Option<NaiveDate>, |
| 25 | 25 | ) -> sqlx::Result<Vec<StatRow>> { |
| 26 | 26 | sqlx::query_as( |
| 27 | - "SELECT e.user_id, u.username, e.task_id, t.name AS task_name, | |
| 27 | + "SELECT e.user_id, u.username, e.category_id, t.name AS category_name, | |
| 28 | 28 | SUM(e.minutes)::BIGINT AS total, COUNT(*)::BIGINT AS entries |
| 29 | 29 | FROM time_entries e |
| 30 | - JOIN tasks t ON t.id = e.task_id | |
| 30 | + JOIN categories t ON t.id = e.category_id | |
| 31 | 31 | JOIN users u ON u.id = e.user_id |
| 32 | 32 | WHERE t.team_id = $1 |
| 33 | 33 | AND ($2::date IS NULL OR e.entry_date >= $2) |
| 34 | 34 | AND ($3::date IS NULL OR e.entry_date < $3) |
| 35 | - GROUP BY e.user_id, u.username, e.task_id, t.name", | |
| 35 | + GROUP BY e.user_id, u.username, e.category_id, t.name", | |
| 36 | 36 | ) |
| 37 | 37 | .bind(team_id) |
| 38 | 38 | .bind(from) |
| @@ -7,16 +7,16 @@ pub struct StatRow { | |||
| 7 | pub user_id: Uuid, | 7 | pub user_id: Uuid, |
| 8 | /// Sert à faire apparaître les ex-membres qui ont des temps (historique intact). | 8 | /// Sert à faire apparaître les ex-membres qui ont des temps (historique intact). |
| 9 | pub username: String, | 9 | pub username: String, |
| 10 | - pub task_id: Uuid, | 10 | + pub category_id: Uuid, |
| 11 | - pub task_name: String, | 11 | + pub category_name: String, |
| 12 | pub total: i64, | 12 | pub total: i64, |
| 13 | /// Nombre de saisies (pour la sheet de détail). | 13 | /// Nombre de saisies (pour la sheet de détail). |
| 14 | pub entries: i64, | 14 | pub entries: i64, |
| 15 | } | 15 | } |
| 16 | 16 | ||
| 17 | -/// Totaux par utilisateur × tâche de l'équipe sur [from, to) ; | 17 | +/// Totaux par utilisateur × catégorie de l'équipe sur [from, to) ; |
| 18 | -/// bornes NULL = depuis le début. Les tâches archivées restent incluses, | 18 | +/// bornes NULL = depuis le début. Les catégories archivées restent incluses, |
| 19 | -/// et les temps des ex-membres aussi (l'équipe se déduit de la tâche). | 19 | +/// et les temps des ex-membres aussi (l'équipe se déduit de la catégorie). |
| 20 | pub async fn totals( | 20 | pub async fn totals( |
| 21 | pool: &PgPool, | 21 | pool: &PgPool, |
| 22 | team_id: Uuid, | 22 | team_id: Uuid, |
| @@ -24,15 +24,15 @@ pub async fn totals( | |||
| 24 | to: Option<NaiveDate>, | 24 | to: Option<NaiveDate>, |
| 25 | ) -> sqlx::Result<Vec<StatRow>> { | 25 | ) -> sqlx::Result<Vec<StatRow>> { |
| 26 | sqlx::query_as( | 26 | sqlx::query_as( |
| 27 | - "SELECT e.user_id, u.username, e.task_id, t.name AS task_name, | 27 | + "SELECT e.user_id, u.username, e.category_id, t.name AS category_name, |
| 28 | SUM(e.minutes)::BIGINT AS total, COUNT(*)::BIGINT AS entries | 28 | SUM(e.minutes)::BIGINT AS total, COUNT(*)::BIGINT AS entries |
| 29 | FROM time_entries e | 29 | FROM time_entries e |
| 30 | - JOIN tasks t ON t.id = e.task_id | 30 | + JOIN categories t ON t.id = e.category_id |
| 31 | JOIN users u ON u.id = e.user_id | 31 | JOIN users u ON u.id = e.user_id |
| 32 | WHERE t.team_id = $1 | 32 | WHERE t.team_id = $1 |
| 33 | AND ($2::date IS NULL OR e.entry_date >= $2) | 33 | AND ($2::date IS NULL OR e.entry_date >= $2) |
| 34 | AND ($3::date IS NULL OR e.entry_date < $3) | 34 | AND ($3::date IS NULL OR e.entry_date < $3) |
| 35 | - GROUP BY e.user_id, u.username, e.task_id, t.name", | 35 | + GROUP BY e.user_id, u.username, e.category_id, t.name", |
| 36 | ) | 36 | ) |
| 37 | .bind(team_id) | 37 | .bind(team_id) |
| 38 | .bind(from) | 38 | .bind(from) |
modified
src/main.rs +1 -1 | @@ -17,7 +17,7 @@ use sqlx::postgres::PgPoolOptions; | ||
| 17 | 17 | use crate::state::AppState; |
| 18 | 18 | |
| 19 | 19 | #[derive(Parser)] |
| 20 | -#[command(name = "time-time", about = "Comptage de temps passé sur des tâches")] | |
| 20 | +#[command(name = "time-time", about = "Comptage de temps passé sur des catégories")] | |
| 21 | 21 | struct Cli { |
| 22 | 22 | #[command(subcommand)] |
| 23 | 23 | command: Option<Command>, |
| @@ -17,7 +17,7 @@ use sqlx::postgres::PgPoolOptions; | |||
| 17 | use crate::state::AppState; | 17 | use crate::state::AppState; |
| 18 | 18 | ||
| 19 | #[derive(Parser)] | 19 | #[derive(Parser)] |
| 20 | -#[command(name = "time-time", about = "Comptage de temps passé sur des tâches")] | 20 | +#[command(name = "time-time", about = "Comptage de temps passé sur des catégories")] |
| 21 | struct Cli { | 21 | struct Cli { |
| 22 | #[command(subcommand)] | 22 | #[command(subcommand)] |
| 23 | command: Option<Command>, | 23 | command: Option<Command>, |
modified
src/routes/entries.rs +35 -35 | @@ -10,7 +10,7 @@ use uuid::Uuid; | ||
| 10 | 10 | |
| 11 | 11 | use crate::charts::{color_map, fmt_minutes}; |
| 12 | 12 | use crate::db::entries::{self, EntryRow}; |
| 13 | -use crate::db::tasks::{self, TaskRow}; | |
| 13 | +use crate::db::categories::{self, CategoryRow}; | |
| 14 | 14 | use crate::db::timers; |
| 15 | 15 | use crate::error::AppError; |
| 16 | 16 | use crate::period::{Week, day_label, paris_today}; |
| @@ -21,9 +21,9 @@ use crate::team::ActiveTeam; | ||
| 21 | 21 | |
| 22 | 22 | const PAGE_SIZE: i64 = 20; |
| 23 | 23 | |
| 24 | -/// ─── Étape 1 : choisir la tâche ─── | |
| 24 | +/// ─── Étape 1 : choisir la catégorie ─── | |
| 25 | 25 | |
| 26 | -pub struct PickTask { | |
| 26 | +pub struct PickCategory { | |
| 27 | 27 | pub href: String, |
| 28 | 28 | pub color: &'static str, |
| 29 | 29 | pub name: String, |
| @@ -42,7 +42,7 @@ pub struct MeasuredView { | ||
| 42 | 42 | #[template(path = "add_step1.html")] |
| 43 | 43 | pub struct AddStep1Page { |
| 44 | 44 | pub shell: Shell, |
| 45 | - pub tasks: Vec<PickTask>, | |
| 45 | + pub categories: Vec<PickCategory>, | |
| 46 | 46 | pub measured: Option<MeasuredView>, |
| 47 | 47 | } |
| 48 | 48 | |
| @@ -73,7 +73,7 @@ pub struct Preset { | ||
| 73 | 73 | pub struct AddStep2Page { |
| 74 | 74 | pub shell: Shell, |
| 75 | 75 | pub csrf: String, |
| 76 | - pub tache: Uuid, | |
| 76 | + pub categorie: Uuid, | |
| 77 | 77 | pub picked_color: &'static str, |
| 78 | 78 | pub picked_name: String, |
| 79 | 79 | pub minutes: i32, |
| @@ -91,7 +91,7 @@ pub struct AddStep2Page { | ||
| 91 | 91 | |
| 92 | 92 | #[derive(Deserialize)] |
| 93 | 93 | pub struct NewQuery { |
| 94 | - tache: Option<Uuid>, | |
| 94 | + categorie: Option<Uuid>, | |
| 95 | 95 | minutes: Option<i32>, |
| 96 | 96 | date: Option<NaiveDate>, |
| 97 | 97 | /// minutes de la saisie qui vient d'aboutir (toast). |
| @@ -102,8 +102,8 @@ pub struct NewQuery { | ||
| 102 | 102 | abandonne: Option<u8>, |
| 103 | 103 | } |
| 104 | 104 | |
| 105 | -fn step2_href(tache: Uuid, minutes: i32, date: NaiveDate) -> String { | |
| 106 | - format!("/temps/nouveau?tache={tache}&minutes={minutes}&date={date}") | |
| 105 | +fn step2_href(categorie: Uuid, minutes: i32, date: NaiveDate) -> String { | |
| 106 | + format!("/temps/nouveau?categorie={categorie}&minutes={minutes}&date={date}") | |
| 107 | 107 | } |
| 108 | 108 | |
| 109 | 109 | pub async fn entry_new( |
| @@ -118,7 +118,7 @@ pub async fn entry_new( | ||
| 118 | 118 | } else { |
| 119 | 119 | q.ok.map(|m| format!("{} ajouté ✓", fmt_minutes(m))) |
| 120 | 120 | }; |
| 121 | - let all_tasks = tasks::list_by_creation(&state.pool, ctx.team_id).await?; | |
| 121 | + let all_tasks = categories::list_by_creation(&state.pool, ctx.team_id).await?; | |
| 122 | 122 | let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>()); |
| 123 | 123 | |
| 124 | 124 | let timer_row = timers::get(&state.pool, ctx.user.id).await?; |
| @@ -147,9 +147,9 @@ pub async fn entry_new( | ||
| 147 | 147 | // Mesure de timer figée dans l'équipe active : elle préremplit l'étape 2. |
| 148 | 148 | let pending = timer_row.filter(|t| t.team_id == ctx.team_id && t.stopped_at.is_some()); |
| 149 | 149 | |
| 150 | - // Étape 2 si une tâche (active) est choisie. | |
| 150 | + // Étape 2 si une catégorie (active) est choisie. | |
| 151 | 151 | if let Some(picked) = q |
| 152 | - .tache | |
| 152 | + .categorie | |
| 153 | 153 | .and_then(|id| all_tasks.iter().find(|t| t.id == id && !t.archived)) |
| 154 | 154 | { |
| 155 | 155 | let measured = pending.as_ref().and_then(|t| t.measured_minutes()); |
| @@ -183,7 +183,7 @@ pub async fn entry_new( | ||
| 183 | 183 | return Ok(HtmlTemplate(AddStep2Page { |
| 184 | 184 | csrf: ctx.user.csrf_token(&state), |
| 185 | 185 | shell: sh, |
| 186 | - tache: picked.id, | |
| 186 | + categorie: picked.id, | |
| 187 | 187 | picked_color: colors[&picked.id], |
| 188 | 188 | picked_name: picked.name.clone(), |
| 189 | 189 | minutes, |
| @@ -201,19 +201,19 @@ pub async fn entry_new( | ||
| 201 | 201 | .into_response()); |
| 202 | 202 | } |
| 203 | 203 | |
| 204 | - // Étape 1 : mes totaux de la semaine par tâche, puis liste des actives. | |
| 204 | + // Étape 1 : mes totaux de la semaine par catégorie, puis liste des actives. | |
| 205 | 205 | let (wfrom, wto) = Week::current().bounds(); |
| 206 | 206 | let week: HashMap<Uuid, i64> = |
| 207 | 207 | entries::week_totals_for_user(&state.pool, ctx.user.id, ctx.team_id, wfrom, wto) |
| 208 | 208 | .await? |
| 209 | 209 | .into_iter() |
| 210 | 210 | .collect(); |
| 211 | - let mut active: Vec<&TaskRow> = all_tasks.iter().filter(|t| !t.archived).collect(); | |
| 211 | + let mut active: Vec<&CategoryRow> = all_tasks.iter().filter(|t| !t.archived).collect(); | |
| 212 | 212 | active.sort_by(|a, b| a.name.cmp(&b.name)); |
| 213 | - let tasks = active | |
| 213 | + let categories = active | |
| 214 | 214 | .into_iter() |
| 215 | - .map(|t| PickTask { | |
| 216 | - href: format!("/temps/nouveau?tache={}", t.id), | |
| 215 | + .map(|t| PickCategory { | |
| 216 | + href: format!("/temps/nouveau?categorie={}", t.id), | |
| 217 | 217 | color: colors[&t.id], |
| 218 | 218 | name: t.name.clone(), |
| 219 | 219 | week_label: week |
| @@ -223,14 +223,14 @@ pub async fn entry_new( | ||
| 223 | 223 | }) |
| 224 | 224 | .collect(); |
| 225 | 225 | let measured = pending.and_then(|t| t.measured_minutes()).map(|m| MeasuredView { |
| 226 | - title: format!("{} mesurées — choisis la tâche", fmt_minutes(m)), | |
| 226 | + title: format!("{} mesurées — choisis la catégorie", fmt_minutes(m)), | |
| 227 | 227 | warning: m > 240, |
| 228 | 228 | }); |
| 229 | 229 | let mut sh = shell(&state, &ctx, "Ajouter du temps", "add", toast).await?; |
| 230 | 230 | sh.show_start = can_start; |
| 231 | 231 | Ok(HtmlTemplate(AddStep1Page { |
| 232 | 232 | shell: sh, |
| 233 | - tasks, | |
| 233 | + categories, | |
| 234 | 234 | measured, |
| 235 | 235 | }) |
| 236 | 236 | .into_response()) |
| @@ -239,7 +239,7 @@ pub async fn entry_new( | ||
| 239 | 239 | #[derive(Deserialize)] |
| 240 | 240 | pub struct EntryForm { |
| 241 | 241 | minutes: i32, |
| 242 | - tache: Uuid, | |
| 242 | + categorie: Uuid, | |
| 243 | 243 | date: NaiveDate, |
| 244 | 244 | csrf: String, |
| 245 | 245 | } |
| @@ -266,14 +266,14 @@ pub async fn entry_create( | ||
| 266 | 266 | &state.pool, |
| 267 | 267 | ctx.user.id, |
| 268 | 268 | ctx.team_id, |
| 269 | - form.tache, | |
| 269 | + form.categorie, | |
| 270 | 270 | form.minutes, |
| 271 | 271 | form.date, |
| 272 | 272 | ) |
| 273 | 273 | .await?; |
| 274 | 274 | if n == 0 { |
| 275 | 275 | return Err(AppError::BadRequest( |
| 276 | - "Cette tâche n'appartient pas à l'équipe active.".into(), | |
| 276 | + "Cette catégorie n'appartient pas à l'équipe active.".into(), | |
| 277 | 277 | )); |
| 278 | 278 | } |
| 279 | 279 | // Une mesure de timer en attente est consommée par l'enregistrement. |
| @@ -285,7 +285,7 @@ pub async fn entry_create( | ||
| 285 | 285 | |
| 286 | 286 | pub struct TimeView { |
| 287 | 287 | pub color: &'static str, |
| 288 | - pub task: String, | |
| 288 | + pub category: String, | |
| 289 | 289 | pub date: String, |
| 290 | 290 | pub duration: String, |
| 291 | 291 | pub edit_href: String, |
| @@ -319,14 +319,14 @@ pub async fn entries_list( | ||
| 319 | 319 | let (rows, has_more) = |
| 320 | 320 | entries::list_first(&state.pool, ctx.user.id, ctx.team_id, page * PAGE_SIZE).await?; |
| 321 | 321 | |
| 322 | - let all_tasks = tasks::list_by_creation(&state.pool, ctx.team_id).await?; | |
| 322 | + let all_tasks = categories::list_by_creation(&state.pool, ctx.team_id).await?; | |
| 323 | 323 | let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>()); |
| 324 | 324 | |
| 325 | 325 | let entries = rows |
| 326 | 326 | .into_iter() |
| 327 | 327 | .map(|e: EntryRow| TimeView { |
| 328 | - color: colors.get(&e.task_id).copied().unwrap_or("#a19786"), | |
| 329 | - task: e.task_name, | |
| 328 | + color: colors.get(&e.category_id).copied().unwrap_or("#a19786"), | |
| 329 | + category: e.category_name, | |
| 330 | 330 | date: day_label(e.entry_date), |
| 331 | 331 | duration: fmt_minutes(e.minutes as i64), |
| 332 | 332 | edit_href: format!("/temps/{}/modifier", e.id), |
| @@ -341,7 +341,7 @@ pub async fn entries_list( | ||
| 341 | 341 | |
| 342 | 342 | /// ─── Édition ─── |
| 343 | 343 | |
| 344 | -pub struct TaskOption { | |
| 344 | +pub struct CategoryOption { | |
| 345 | 345 | pub id: Uuid, |
| 346 | 346 | pub name: String, |
| 347 | 347 | pub selected: bool, |
| @@ -354,7 +354,7 @@ pub struct EditPage { | ||
| 354 | 354 | pub csrf: String, |
| 355 | 355 | pub action: String, |
| 356 | 356 | pub delete_action: String, |
| 357 | - pub tasks: Vec<TaskOption>, | |
| 357 | + pub categories: Vec<CategoryOption>, | |
| 358 | 358 | pub minutes: i32, |
| 359 | 359 | pub date: NaiveDate, |
| 360 | 360 | } |
| @@ -367,13 +367,13 @@ pub async fn entry_edit( | ||
| 367 | 367 | let entry = entries::get_owned(&state.pool, id, ctx.user.id, ctx.team_id) |
| 368 | 368 | .await? |
| 369 | 369 | .ok_or(AppError::NotFound)?; |
| 370 | - // La tâche de l'entrée reste proposée même archivée depuis. | |
| 371 | - let mut options: Vec<TaskOption> = tasks::list_all(&state.pool, ctx.team_id) | |
| 370 | + // La catégorie de l'entrée reste proposée même archivée depuis. | |
| 371 | + let mut options: Vec<CategoryOption> = categories::list_all(&state.pool, ctx.team_id) | |
| 372 | 372 | .await? |
| 373 | 373 | .into_iter() |
| 374 | - .filter(|t| !t.archived || t.id == entry.task_id) | |
| 375 | - .map(|t| TaskOption { | |
| 376 | - selected: t.id == entry.task_id, | |
| 374 | + .filter(|t| !t.archived || t.id == entry.category_id) | |
| 375 | + .map(|t| CategoryOption { | |
| 376 | + selected: t.id == entry.category_id, | |
| 377 | 377 | name: if t.archived { |
| 378 | 378 | format!("{} (archivée)", t.name) |
| 379 | 379 | } else { |
| @@ -388,7 +388,7 @@ pub async fn entry_edit( | ||
| 388 | 388 | shell: shell(&state, &ctx, "Modifier le temps", "times", None).await?, |
| 389 | 389 | action: format!("/temps/{id}"), |
| 390 | 390 | delete_action: format!("/temps/{id}/supprimer"), |
| 391 | - tasks: options, | |
| 391 | + categories: options, | |
| 392 | 392 | minutes: entry.minutes, |
| 393 | 393 | date: entry.entry_date, |
| 394 | 394 | })) |
| @@ -409,7 +409,7 @@ pub async fn entry_update( | ||
| 409 | 409 | id, |
| 410 | 410 | ctx.user.id, |
| 411 | 411 | ctx.team_id, |
| 412 | - form.tache, | |
| 412 | + form.categorie, | |
| 413 | 413 | form.minutes, |
| 414 | 414 | form.date, |
| 415 | 415 | ) |
| @@ -10,7 +10,7 @@ use uuid::Uuid; | |||
| 10 | 10 | ||
| 11 | use crate::charts::{color_map, fmt_minutes}; | 11 | use crate::charts::{color_map, fmt_minutes}; |
| 12 | use crate::db::entries::{self, EntryRow}; | 12 | use crate::db::entries::{self, EntryRow}; |
| 13 | -use crate::db::tasks::{self, TaskRow}; | 13 | +use crate::db::categories::{self, CategoryRow}; |
| 14 | use crate::db::timers; | 14 | use crate::db::timers; |
| 15 | use crate::error::AppError; | 15 | use crate::error::AppError; |
| 16 | use crate::period::{Week, day_label, paris_today}; | 16 | use crate::period::{Week, day_label, paris_today}; |
| @@ -21,9 +21,9 @@ use crate::team::ActiveTeam; | |||
| 21 | 21 | ||
| 22 | const PAGE_SIZE: i64 = 20; | 22 | const PAGE_SIZE: i64 = 20; |
| 23 | 23 | ||
| 24 | -/// ─── Étape 1 : choisir la tâche ─── | 24 | +/// ─── Étape 1 : choisir la catégorie ─── |
| 25 | 25 | ||
| 26 | -pub struct PickTask { | 26 | +pub struct PickCategory { |
| 27 | pub href: String, | 27 | pub href: String, |
| 28 | pub color: &'static str, | 28 | pub color: &'static str, |
| 29 | pub name: String, | 29 | pub name: String, |
| @@ -42,7 +42,7 @@ pub struct MeasuredView { | |||
| 42 | #[template(path = "add_step1.html")] | 42 | #[template(path = "add_step1.html")] |
| 43 | pub struct AddStep1Page { | 43 | pub struct AddStep1Page { |
| 44 | pub shell: Shell, | 44 | pub shell: Shell, |
| 45 | - pub tasks: Vec<PickTask>, | 45 | + pub categories: Vec<PickCategory>, |
| 46 | pub measured: Option<MeasuredView>, | 46 | pub measured: Option<MeasuredView>, |
| 47 | } | 47 | } |
| 48 | 48 | ||
| @@ -73,7 +73,7 @@ pub struct Preset { | |||
| 73 | pub struct AddStep2Page { | 73 | pub struct AddStep2Page { |
| 74 | pub shell: Shell, | 74 | pub shell: Shell, |
| 75 | pub csrf: String, | 75 | pub csrf: String, |
| 76 | - pub tache: Uuid, | 76 | + pub categorie: Uuid, |
| 77 | pub picked_color: &'static str, | 77 | pub picked_color: &'static str, |
| 78 | pub picked_name: String, | 78 | pub picked_name: String, |
| 79 | pub minutes: i32, | 79 | pub minutes: i32, |
| @@ -91,7 +91,7 @@ pub struct AddStep2Page { | |||
| 91 | 91 | ||
| 92 | #[derive(Deserialize)] | 92 | #[derive(Deserialize)] |
| 93 | pub struct NewQuery { | 93 | pub struct NewQuery { |
| 94 | - tache: Option<Uuid>, | 94 | + categorie: Option<Uuid>, |
| 95 | minutes: Option<i32>, | 95 | minutes: Option<i32>, |
| 96 | date: Option<NaiveDate>, | 96 | date: Option<NaiveDate>, |
| 97 | /// minutes de la saisie qui vient d'aboutir (toast). | 97 | /// minutes de la saisie qui vient d'aboutir (toast). |
| @@ -102,8 +102,8 @@ pub struct NewQuery { | |||
| 102 | abandonne: Option<u8>, | 102 | abandonne: Option<u8>, |
| 103 | } | 103 | } |
| 104 | 104 | ||
| 105 | -fn step2_href(tache: Uuid, minutes: i32, date: NaiveDate) -> String { | 105 | +fn step2_href(categorie: Uuid, minutes: i32, date: NaiveDate) -> String { |
| 106 | - format!("/temps/nouveau?tache={tache}&minutes={minutes}&date={date}") | 106 | + format!("/temps/nouveau?categorie={categorie}&minutes={minutes}&date={date}") |
| 107 | } | 107 | } |
| 108 | 108 | ||
| 109 | pub async fn entry_new( | 109 | pub async fn entry_new( |
| @@ -118,7 +118,7 @@ pub async fn entry_new( | |||
| 118 | } else { | 118 | } else { |
| 119 | q.ok.map(|m| format!("{} ajouté ✓", fmt_minutes(m))) | 119 | q.ok.map(|m| format!("{} ajouté ✓", fmt_minutes(m))) |
| 120 | }; | 120 | }; |
| 121 | - let all_tasks = tasks::list_by_creation(&state.pool, ctx.team_id).await?; | 121 | + let all_tasks = categories::list_by_creation(&state.pool, ctx.team_id).await?; |
| 122 | let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>()); | 122 | let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>()); |
| 123 | 123 | ||
| 124 | let timer_row = timers::get(&state.pool, ctx.user.id).await?; | 124 | let timer_row = timers::get(&state.pool, ctx.user.id).await?; |
| @@ -147,9 +147,9 @@ pub async fn entry_new( | |||
| 147 | // Mesure de timer figée dans l'équipe active : elle préremplit l'étape 2. | 147 | // Mesure de timer figée dans l'équipe active : elle préremplit l'étape 2. |
| 148 | let pending = timer_row.filter(|t| t.team_id == ctx.team_id && t.stopped_at.is_some()); | 148 | let pending = timer_row.filter(|t| t.team_id == ctx.team_id && t.stopped_at.is_some()); |
| 149 | 149 | ||
| 150 | - // Étape 2 si une tâche (active) est choisie. | 150 | + // Étape 2 si une catégorie (active) est choisie. |
| 151 | if let Some(picked) = q | 151 | if let Some(picked) = q |
| 152 | - .tache | 152 | + .categorie |
| 153 | .and_then(|id| all_tasks.iter().find(|t| t.id == id && !t.archived)) | 153 | .and_then(|id| all_tasks.iter().find(|t| t.id == id && !t.archived)) |
| 154 | { | 154 | { |
| 155 | let measured = pending.as_ref().and_then(|t| t.measured_minutes()); | 155 | let measured = pending.as_ref().and_then(|t| t.measured_minutes()); |
| @@ -183,7 +183,7 @@ pub async fn entry_new( | |||
| 183 | return Ok(HtmlTemplate(AddStep2Page { | 183 | return Ok(HtmlTemplate(AddStep2Page { |
| 184 | csrf: ctx.user.csrf_token(&state), | 184 | csrf: ctx.user.csrf_token(&state), |
| 185 | shell: sh, | 185 | shell: sh, |
| 186 | - tache: picked.id, | 186 | + categorie: picked.id, |
| 187 | picked_color: colors[&picked.id], | 187 | picked_color: colors[&picked.id], |
| 188 | picked_name: picked.name.clone(), | 188 | picked_name: picked.name.clone(), |
| 189 | minutes, | 189 | minutes, |
| @@ -201,19 +201,19 @@ pub async fn entry_new( | |||
| 201 | .into_response()); | 201 | .into_response()); |
| 202 | } | 202 | } |
| 203 | 203 | ||
| 204 | - // Étape 1 : mes totaux de la semaine par tâche, puis liste des actives. | 204 | + // Étape 1 : mes totaux de la semaine par catégorie, puis liste des actives. |
| 205 | let (wfrom, wto) = Week::current().bounds(); | 205 | let (wfrom, wto) = Week::current().bounds(); |
| 206 | let week: HashMap<Uuid, i64> = | 206 | let week: HashMap<Uuid, i64> = |
| 207 | entries::week_totals_for_user(&state.pool, ctx.user.id, ctx.team_id, wfrom, wto) | 207 | entries::week_totals_for_user(&state.pool, ctx.user.id, ctx.team_id, wfrom, wto) |
| 208 | .await? | 208 | .await? |
| 209 | .into_iter() | 209 | .into_iter() |
| 210 | .collect(); | 210 | .collect(); |
| 211 | - let mut active: Vec<&TaskRow> = all_tasks.iter().filter(|t| !t.archived).collect(); | 211 | + let mut active: Vec<&CategoryRow> = all_tasks.iter().filter(|t| !t.archived).collect(); |
| 212 | active.sort_by(|a, b| a.name.cmp(&b.name)); | 212 | active.sort_by(|a, b| a.name.cmp(&b.name)); |
| 213 | - let tasks = active | 213 | + let categories = active |
| 214 | .into_iter() | 214 | .into_iter() |
| 215 | - .map(|t| PickTask { | 215 | + .map(|t| PickCategory { |
| 216 | - href: format!("/temps/nouveau?tache={}", t.id), | 216 | + href: format!("/temps/nouveau?categorie={}", t.id), |
| 217 | color: colors[&t.id], | 217 | color: colors[&t.id], |
| 218 | name: t.name.clone(), | 218 | name: t.name.clone(), |
| 219 | week_label: week | 219 | week_label: week |
| @@ -223,14 +223,14 @@ pub async fn entry_new( | |||
| 223 | }) | 223 | }) |
| 224 | .collect(); | 224 | .collect(); |
| 225 | let measured = pending.and_then(|t| t.measured_minutes()).map(|m| MeasuredView { | 225 | let measured = pending.and_then(|t| t.measured_minutes()).map(|m| MeasuredView { |
| 226 | - title: format!("{} mesurées — choisis la tâche", fmt_minutes(m)), | 226 | + title: format!("{} mesurées — choisis la catégorie", fmt_minutes(m)), |
| 227 | warning: m > 240, | 227 | warning: m > 240, |
| 228 | }); | 228 | }); |
| 229 | let mut sh = shell(&state, &ctx, "Ajouter du temps", "add", toast).await?; | 229 | let mut sh = shell(&state, &ctx, "Ajouter du temps", "add", toast).await?; |
| 230 | sh.show_start = can_start; | 230 | sh.show_start = can_start; |
| 231 | Ok(HtmlTemplate(AddStep1Page { | 231 | Ok(HtmlTemplate(AddStep1Page { |
| 232 | shell: sh, | 232 | shell: sh, |
| 233 | - tasks, | 233 | + categories, |
| 234 | measured, | 234 | measured, |
| 235 | }) | 235 | }) |
| 236 | .into_response()) | 236 | .into_response()) |
| @@ -239,7 +239,7 @@ pub async fn entry_new( | |||
| 239 | #[derive(Deserialize)] | 239 | #[derive(Deserialize)] |
| 240 | pub struct EntryForm { | 240 | pub struct EntryForm { |
| 241 | minutes: i32, | 241 | minutes: i32, |
| 242 | - tache: Uuid, | 242 | + categorie: Uuid, |
| 243 | date: NaiveDate, | 243 | date: NaiveDate, |
| 244 | csrf: String, | 244 | csrf: String, |
| 245 | } | 245 | } |
| @@ -266,14 +266,14 @@ pub async fn entry_create( | |||
| 266 | &state.pool, | 266 | &state.pool, |
| 267 | ctx.user.id, | 267 | ctx.user.id, |
| 268 | ctx.team_id, | 268 | ctx.team_id, |
| 269 | - form.tache, | 269 | + form.categorie, |
| 270 | form.minutes, | 270 | form.minutes, |
| 271 | form.date, | 271 | form.date, |
| 272 | ) | 272 | ) |
| 273 | .await?; | 273 | .await?; |
| 274 | if n == 0 { | 274 | if n == 0 { |
| 275 | return Err(AppError::BadRequest( | 275 | return Err(AppError::BadRequest( |
| 276 | - "Cette tâche n'appartient pas à l'équipe active.".into(), | 276 | + "Cette catégorie n'appartient pas à l'équipe active.".into(), |
| 277 | )); | 277 | )); |
| 278 | } | 278 | } |
| 279 | // Une mesure de timer en attente est consommée par l'enregistrement. | 279 | // Une mesure de timer en attente est consommée par l'enregistrement. |
| @@ -285,7 +285,7 @@ pub async fn entry_create( | |||
| 285 | 285 | ||
| 286 | pub struct TimeView { | 286 | pub struct TimeView { |
| 287 | pub color: &'static str, | 287 | pub color: &'static str, |
| 288 | - pub task: String, | 288 | + pub category: String, |
| 289 | pub date: String, | 289 | pub date: String, |
| 290 | pub duration: String, | 290 | pub duration: String, |
| 291 | pub edit_href: String, | 291 | pub edit_href: String, |
| @@ -319,14 +319,14 @@ pub async fn entries_list( | |||
| 319 | let (rows, has_more) = | 319 | let (rows, has_more) = |
| 320 | entries::list_first(&state.pool, ctx.user.id, ctx.team_id, page * PAGE_SIZE).await?; | 320 | entries::list_first(&state.pool, ctx.user.id, ctx.team_id, page * PAGE_SIZE).await?; |
| 321 | 321 | ||
| 322 | - let all_tasks = tasks::list_by_creation(&state.pool, ctx.team_id).await?; | 322 | + let all_tasks = categories::list_by_creation(&state.pool, ctx.team_id).await?; |
| 323 | let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>()); | 323 | let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>()); |
| 324 | 324 | ||
| 325 | let entries = rows | 325 | let entries = rows |
| 326 | .into_iter() | 326 | .into_iter() |
| 327 | .map(|e: EntryRow| TimeView { | 327 | .map(|e: EntryRow| TimeView { |
| 328 | - color: colors.get(&e.task_id).copied().unwrap_or("#a19786"), | 328 | + color: colors.get(&e.category_id).copied().unwrap_or("#a19786"), |
| 329 | - task: e.task_name, | 329 | + category: e.category_name, |
| 330 | date: day_label(e.entry_date), | 330 | date: day_label(e.entry_date), |
| 331 | duration: fmt_minutes(e.minutes as i64), | 331 | duration: fmt_minutes(e.minutes as i64), |
| 332 | edit_href: format!("/temps/{}/modifier", e.id), | 332 | edit_href: format!("/temps/{}/modifier", e.id), |
| @@ -341,7 +341,7 @@ pub async fn entries_list( | |||
| 341 | 341 | ||
| 342 | /// ─── Édition ─── | 342 | /// ─── Édition ─── |
| 343 | 343 | ||
| 344 | -pub struct TaskOption { | 344 | +pub struct CategoryOption { |
| 345 | pub id: Uuid, | 345 | pub id: Uuid, |
| 346 | pub name: String, | 346 | pub name: String, |
| 347 | pub selected: bool, | 347 | pub selected: bool, |
| @@ -354,7 +354,7 @@ pub struct EditPage { | |||
| 354 | pub csrf: String, | 354 | pub csrf: String, |
| 355 | pub action: String, | 355 | pub action: String, |
| 356 | pub delete_action: String, | 356 | pub delete_action: String, |
| 357 | - pub tasks: Vec<TaskOption>, | 357 | + pub categories: Vec<CategoryOption>, |
| 358 | pub minutes: i32, | 358 | pub minutes: i32, |
| 359 | pub date: NaiveDate, | 359 | pub date: NaiveDate, |
| 360 | } | 360 | } |
| @@ -367,13 +367,13 @@ pub async fn entry_edit( | |||
| 367 | let entry = entries::get_owned(&state.pool, id, ctx.user.id, ctx.team_id) | 367 | let entry = entries::get_owned(&state.pool, id, ctx.user.id, ctx.team_id) |
| 368 | .await? | 368 | .await? |
| 369 | .ok_or(AppError::NotFound)?; | 369 | .ok_or(AppError::NotFound)?; |
| 370 | - // La tâche de l'entrée reste proposée même archivée depuis. | 370 | + // La catégorie de l'entrée reste proposée même archivée depuis. |
| 371 | - let mut options: Vec<TaskOption> = tasks::list_all(&state.pool, ctx.team_id) | 371 | + let mut options: Vec<CategoryOption> = categories::list_all(&state.pool, ctx.team_id) |
| 372 | .await? | 372 | .await? |
| 373 | .into_iter() | 373 | .into_iter() |
| 374 | - .filter(|t| !t.archived || t.id == entry.task_id) | 374 | + .filter(|t| !t.archived || t.id == entry.category_id) |
| 375 | - .map(|t| TaskOption { | 375 | + .map(|t| CategoryOption { |
| 376 | - selected: t.id == entry.task_id, | 376 | + selected: t.id == entry.category_id, |
| 377 | name: if t.archived { | 377 | name: if t.archived { |
| 378 | format!("{} (archivée)", t.name) | 378 | format!("{} (archivée)", t.name) |
| 379 | } else { | 379 | } else { |
| @@ -388,7 +388,7 @@ pub async fn entry_edit( | |||
| 388 | shell: shell(&state, &ctx, "Modifier le temps", "times", None).await?, | 388 | shell: shell(&state, &ctx, "Modifier le temps", "times", None).await?, |
| 389 | action: format!("/temps/{id}"), | 389 | action: format!("/temps/{id}"), |
| 390 | delete_action: format!("/temps/{id}/supprimer"), | 390 | delete_action: format!("/temps/{id}/supprimer"), |
| 391 | - tasks: options, | 391 | + categories: options, |
| 392 | minutes: entry.minutes, | 392 | minutes: entry.minutes, |
| 393 | date: entry.entry_date, | 393 | date: entry.entry_date, |
| 394 | })) | 394 | })) |
| @@ -409,7 +409,7 @@ pub async fn entry_update( | |||
| 409 | id, | 409 | id, |
| 410 | ctx.user.id, | 410 | ctx.user.id, |
| 411 | ctx.team_id, | 411 | ctx.team_id, |
| 412 | - form.tache, | 412 | + form.categorie, |
| 413 | form.minutes, | 413 | form.minutes, |
| 414 | form.date, | 414 | form.date, |
| 415 | ) | 415 | ) |
modified
src/routes/mod.rs +4 -4 | @@ -36,11 +36,11 @@ pub fn router(state: AppState) -> Router { | ||
| 36 | 36 | .route("/temps/{id}/supprimer", post(entries::entry_delete)) |
| 37 | 37 | .route("/stats", get(stats::stats_page)) |
| 38 | 38 | .route("/parametres", get(settings::settings_page)) |
| 39 | - .route("/parametres/taches", get(settings::tasks_page).post(settings::task_create)) | |
| 40 | - .route("/parametres/taches/{id}", post(settings::task_rename)) | |
| 39 | + .route("/parametres/categories", get(settings::categories_page).post(settings::category_create)) | |
| 40 | + .route("/parametres/categories/{id}", post(settings::category_rename)) | |
| 41 | 41 | .route( |
| 42 | - "/parametres/taches/{id}/archiver", | |
| 43 | - post(settings::task_toggle_archive), | |
| 42 | + "/parametres/categories/{id}/archiver", | |
| 43 | + post(settings::category_toggle_archive), | |
| 44 | 44 | ) |
| 45 | 45 | .route( |
| 46 | 46 | "/parametres/mot-de-passe", |
| @@ -36,11 +36,11 @@ pub fn router(state: AppState) -> Router { | |||
| 36 | .route("/temps/{id}/supprimer", post(entries::entry_delete)) | 36 | .route("/temps/{id}/supprimer", post(entries::entry_delete)) |
| 37 | .route("/stats", get(stats::stats_page)) | 37 | .route("/stats", get(stats::stats_page)) |
| 38 | .route("/parametres", get(settings::settings_page)) | 38 | .route("/parametres", get(settings::settings_page)) |
| 39 | - .route("/parametres/taches", get(settings::tasks_page).post(settings::task_create)) | 39 | + .route("/parametres/categories", get(settings::categories_page).post(settings::category_create)) |
| 40 | - .route("/parametres/taches/{id}", post(settings::task_rename)) | 40 | + .route("/parametres/categories/{id}", post(settings::category_rename)) |
| 41 | .route( | 41 | .route( |
| 42 | - "/parametres/taches/{id}/archiver", | 42 | + "/parametres/categories/{id}/archiver", |
| 43 | - post(settings::task_toggle_archive), | 43 | + post(settings::category_toggle_archive), |
| 44 | ) | 44 | ) |
| 45 | .route( | 45 | .route( |
| 46 | "/parametres/mot-de-passe", | 46 | "/parametres/mot-de-passe", |
modified
src/routes/settings.rs +39 -39 | @@ -7,7 +7,7 @@ use uuid::Uuid; | ||
| 7 | 7 | |
| 8 | 8 | use crate::auth::password; |
| 9 | 9 | use crate::charts::color_map; |
| 10 | -use crate::db::{invitations, tasks, teams, users}; | |
| 10 | +use crate::db::{invitations, categories, teams, users}; | |
| 11 | 11 | use crate::error::AppError; |
| 12 | 12 | use crate::render::{HtmlTemplate, Shell}; |
| 13 | 13 | use crate::routes::shell; |
| @@ -134,9 +134,9 @@ pub async fn settings_page( | ||
| 134 | 134 | })) |
| 135 | 135 | } |
| 136 | 136 | |
| 137 | -/// ─── Tâches ─── | |
| 137 | +/// ─── Catégories ─── | |
| 138 | 138 | |
| 139 | -pub struct TaskView { | |
| 139 | +pub struct CategoryView { | |
| 140 | 140 | pub color: &'static str, |
| 141 | 141 | pub name: String, |
| 142 | 142 | pub archived: bool, |
| @@ -145,52 +145,52 @@ pub struct TaskView { | ||
| 145 | 145 | } |
| 146 | 146 | |
| 147 | 147 | #[derive(Template)] |
| 148 | -#[template(path = "tasks.html")] | |
| 149 | -pub struct TasksPage { | |
| 148 | +#[template(path = "categories.html")] | |
| 149 | +pub struct CategoriesPage { | |
| 150 | 150 | pub shell: Shell, |
| 151 | 151 | pub csrf: String, |
| 152 | - pub tasks: Vec<TaskView>, | |
| 152 | + pub categories: Vec<CategoryView>, | |
| 153 | 153 | } |
| 154 | 154 | |
| 155 | -pub async fn tasks_page( | |
| 155 | +pub async fn categories_page( | |
| 156 | 156 | State(state): State<AppState>, |
| 157 | 157 | ctx: ActiveTeam, |
| 158 | 158 | Query(q): Query<SettingsQuery>, |
| 159 | -) -> Result<HtmlTemplate<TasksPage>, AppError> { | |
| 159 | +) -> Result<HtmlTemplate<CategoriesPage>, AppError> { | |
| 160 | 160 | let toast = match (q.ok.as_deref(), q.err.as_deref()) { |
| 161 | - (Some("ajoutee"), _) => Some("Tâche ajoutée".to_string()), | |
| 162 | - (Some("renommee"), _) => Some("Tâche renommée".to_string()), | |
| 161 | + (Some("ajoutee"), _) => Some("Catégorie ajoutée".to_string()), | |
| 162 | + (Some("renommee"), _) => Some("Catégorie renommée".to_string()), | |
| 163 | 163 | (Some("archivee"), _) => { |
| 164 | - Some("Tâche archivée — les temps restent dans les stats".to_string()) | |
| 164 | + Some("Catégorie archivée — les temps restent dans les stats".to_string()) | |
| 165 | 165 | } |
| 166 | - (Some("restauree"), _) => Some("Tâche restaurée".to_string()), | |
| 167 | - (_, Some("doublon")) => Some("Une tâche porte déjà ce nom".to_string()), | |
| 166 | + (Some("restauree"), _) => Some("Catégorie restaurée".to_string()), | |
| 167 | + (_, Some("doublon")) => Some("Une catégorie porte déjà ce nom".to_string()), | |
| 168 | 168 | (_, Some("vide")) => Some("Le nom ne peut pas être vide".to_string()), |
| 169 | 169 | _ => None, |
| 170 | 170 | }; |
| 171 | - let by_creation = tasks::list_by_creation(&state.pool, ctx.team_id).await?; | |
| 171 | + let by_creation = categories::list_by_creation(&state.pool, ctx.team_id).await?; | |
| 172 | 172 | let colors = color_map(&by_creation.iter().map(|t| t.id).collect::<Vec<_>>()); |
| 173 | 173 | let mut list = by_creation; |
| 174 | 174 | list.sort_by(|a, b| (a.archived, &a.name).cmp(&(b.archived, &b.name))); |
| 175 | - let tasks = list | |
| 175 | + let categories = list | |
| 176 | 176 | .into_iter() |
| 177 | - .map(|t| TaskView { | |
| 177 | + .map(|t| CategoryView { | |
| 178 | 178 | color: colors[&t.id], |
| 179 | - rename_action: format!("/parametres/taches/{}", t.id), | |
| 180 | - toggle_action: format!("/parametres/taches/{}/archiver", t.id), | |
| 179 | + rename_action: format!("/parametres/categories/{}", t.id), | |
| 180 | + toggle_action: format!("/parametres/categories/{}/archiver", t.id), | |
| 181 | 181 | name: t.name, |
| 182 | 182 | archived: t.archived, |
| 183 | 183 | }) |
| 184 | 184 | .collect(); |
| 185 | - Ok(HtmlTemplate(TasksPage { | |
| 185 | + Ok(HtmlTemplate(CategoriesPage { | |
| 186 | 186 | csrf: ctx.user.csrf_token(&state), |
| 187 | - shell: shell(&state, &ctx, "Tâches", "add", toast).await?, | |
| 188 | - tasks, | |
| 187 | + shell: shell(&state, &ctx, "Catégories", "add", toast).await?, | |
| 188 | + categories, | |
| 189 | 189 | })) |
| 190 | 190 | } |
| 191 | 191 | |
| 192 | 192 | #[derive(Deserialize)] |
| 193 | -pub struct TaskForm { | |
| 193 | +pub struct CategoryFormb { | |
| 194 | 194 | name: String, |
| 195 | 195 | csrf: String, |
| 196 | 196 | } |
| @@ -199,42 +199,42 @@ fn is_unique_violation(e: &sqlx::Error) -> bool { | ||
| 199 | 199 | matches!(e, sqlx::Error::Database(db) if db.is_unique_violation()) |
| 200 | 200 | } |
| 201 | 201 | |
| 202 | -pub async fn task_create( | |
| 202 | +pub async fn category_create( | |
| 203 | 203 | State(state): State<AppState>, |
| 204 | 204 | ctx: ActiveTeam, |
| 205 | - Form(form): Form<TaskForm>, | |
| 205 | + Form(form): Form<CategoryFormb>, | |
| 206 | 206 | ) -> Result<Redirect, AppError> { |
| 207 | 207 | if !ctx.user.check_csrf(&state, &form.csrf) { |
| 208 | 208 | return Err(AppError::Csrf); |
| 209 | 209 | } |
| 210 | 210 | let name = form.name.trim(); |
| 211 | 211 | if name.is_empty() { |
| 212 | - return Ok(Redirect::to("/parametres/taches?err=vide")); | |
| 212 | + return Ok(Redirect::to("/parametres/categories?err=vide")); | |
| 213 | 213 | } |
| 214 | - match tasks::create(&state.pool, ctx.team_id, name).await { | |
| 215 | - Ok(_) => Ok(Redirect::to("/parametres/taches?ok=ajoutee")), | |
| 216 | - Err(e) if is_unique_violation(&e) => Ok(Redirect::to("/parametres/taches?err=doublon")), | |
| 214 | + match categories::create(&state.pool, ctx.team_id, name).await { | |
| 215 | + Ok(_) => Ok(Redirect::to("/parametres/categories?ok=ajoutee")), | |
| 216 | + Err(e) if is_unique_violation(&e) => Ok(Redirect::to("/parametres/categories?err=doublon")), | |
| 217 | 217 | Err(e) => Err(e.into()), |
| 218 | 218 | } |
| 219 | 219 | } |
| 220 | 220 | |
| 221 | -pub async fn task_rename( | |
| 221 | +pub async fn category_rename( | |
| 222 | 222 | State(state): State<AppState>, |
| 223 | 223 | ctx: ActiveTeam, |
| 224 | 224 | Path(id): Path<Uuid>, |
| 225 | - Form(form): Form<TaskForm>, | |
| 225 | + Form(form): Form<CategoryFormb>, | |
| 226 | 226 | ) -> Result<Redirect, AppError> { |
| 227 | 227 | if !ctx.user.check_csrf(&state, &form.csrf) { |
| 228 | 228 | return Err(AppError::Csrf); |
| 229 | 229 | } |
| 230 | 230 | let name = form.name.trim(); |
| 231 | 231 | if name.is_empty() { |
| 232 | - return Ok(Redirect::to("/parametres/taches?err=vide")); | |
| 232 | + return Ok(Redirect::to("/parametres/categories?err=vide")); | |
| 233 | 233 | } |
| 234 | - match tasks::rename(&state.pool, id, ctx.team_id, name).await { | |
| 234 | + match categories::rename(&state.pool, id, ctx.team_id, name).await { | |
| 235 | 235 | Ok(0) => Err(AppError::NotFound), |
| 236 | - Ok(_) => Ok(Redirect::to("/parametres/taches?ok=renommee")), | |
| 237 | - Err(e) if is_unique_violation(&e) => Ok(Redirect::to("/parametres/taches?err=doublon")), | |
| 236 | + Ok(_) => Ok(Redirect::to("/parametres/categories?ok=renommee")), | |
| 237 | + Err(e) if is_unique_violation(&e) => Ok(Redirect::to("/parametres/categories?err=doublon")), | |
| 238 | 238 | Err(e) => Err(e.into()), |
| 239 | 239 | } |
| 240 | 240 | } |
| @@ -242,11 +242,11 @@ pub async fn task_rename( | ||
| 242 | 242 | #[derive(Deserialize)] |
| 243 | 243 | pub struct ToggleForm { |
| 244 | 244 | csrf: String, |
| 245 | - /// true si la tâche était archivée (pour le message du toast). | |
| 245 | + /// true si la catégorie était archivée (pour le message du toast). | |
| 246 | 246 | restaure: Option<u8>, |
| 247 | 247 | } |
| 248 | 248 | |
| 249 | -pub async fn task_toggle_archive( | |
| 249 | +pub async fn category_toggle_archive( | |
| 250 | 250 | State(state): State<AppState>, |
| 251 | 251 | ctx: ActiveTeam, |
| 252 | 252 | Path(id): Path<Uuid>, |
| @@ -255,13 +255,13 @@ pub async fn task_toggle_archive( | ||
| 255 | 255 | if !ctx.user.check_csrf(&state, &form.csrf) { |
| 256 | 256 | return Err(AppError::Csrf); |
| 257 | 257 | } |
| 258 | - if tasks::toggle_archived(&state.pool, id, ctx.team_id).await? == 0 { | |
| 258 | + if categories::toggle_archived(&state.pool, id, ctx.team_id).await? == 0 { | |
| 259 | 259 | return Err(AppError::NotFound); |
| 260 | 260 | } |
| 261 | 261 | Ok(Redirect::to(if form.restaure.is_some() { |
| 262 | - "/parametres/taches?ok=restauree" | |
| 262 | + "/parametres/categories?ok=restauree" | |
| 263 | 263 | } else { |
| 264 | - "/parametres/taches?ok=archivee" | |
| 264 | + "/parametres/categories?ok=archivee" | |
| 265 | 265 | })) |
| 266 | 266 | } |
| 267 | 267 | |
| @@ -7,7 +7,7 @@ use uuid::Uuid; | |||
| 7 | 7 | ||
| 8 | use crate::auth::password; | 8 | use crate::auth::password; |
| 9 | use crate::charts::color_map; | 9 | use crate::charts::color_map; |
| 10 | -use crate::db::{invitations, tasks, teams, users}; | 10 | +use crate::db::{invitations, categories, teams, users}; |
| 11 | use crate::error::AppError; | 11 | use crate::error::AppError; |
| 12 | use crate::render::{HtmlTemplate, Shell}; | 12 | use crate::render::{HtmlTemplate, Shell}; |
| 13 | use crate::routes::shell; | 13 | use crate::routes::shell; |
| @@ -134,9 +134,9 @@ pub async fn settings_page( | |||
| 134 | })) | 134 | })) |
| 135 | } | 135 | } |
| 136 | 136 | ||
| 137 | -/// ─── Tâches ─── | 137 | +/// ─── Catégories ─── |
| 138 | 138 | ||
| 139 | -pub struct TaskView { | 139 | +pub struct CategoryView { |
| 140 | pub color: &'static str, | 140 | pub color: &'static str, |
| 141 | pub name: String, | 141 | pub name: String, |
| 142 | pub archived: bool, | 142 | pub archived: bool, |
| @@ -145,52 +145,52 @@ pub struct TaskView { | |||
| 145 | } | 145 | } |
| 146 | 146 | ||
| 147 | #[derive(Template)] | 147 | #[derive(Template)] |
| 148 | -#[template(path = "tasks.html")] | 148 | +#[template(path = "categories.html")] |
| 149 | -pub struct TasksPage { | 149 | +pub struct CategoriesPage { |
| 150 | pub shell: Shell, | 150 | pub shell: Shell, |
| 151 | pub csrf: String, | 151 | pub csrf: String, |
| 152 | - pub tasks: Vec<TaskView>, | 152 | + pub categories: Vec<CategoryView>, |
| 153 | } | 153 | } |
| 154 | 154 | ||
| 155 | -pub async fn tasks_page( | 155 | +pub async fn categories_page( |
| 156 | State(state): State<AppState>, | 156 | State(state): State<AppState>, |
| 157 | ctx: ActiveTeam, | 157 | ctx: ActiveTeam, |
| 158 | Query(q): Query<SettingsQuery>, | 158 | Query(q): Query<SettingsQuery>, |
| 159 | -) -> Result<HtmlTemplate<TasksPage>, AppError> { | 159 | +) -> Result<HtmlTemplate<CategoriesPage>, AppError> { |
| 160 | let toast = match (q.ok.as_deref(), q.err.as_deref()) { | 160 | let toast = match (q.ok.as_deref(), q.err.as_deref()) { |
| 161 | - (Some("ajoutee"), _) => Some("Tâche ajoutée".to_string()), | 161 | + (Some("ajoutee"), _) => Some("Catégorie ajoutée".to_string()), |
| 162 | - (Some("renommee"), _) => Some("Tâche renommée".to_string()), | 162 | + (Some("renommee"), _) => Some("Catégorie renommée".to_string()), |
| 163 | (Some("archivee"), _) => { | 163 | (Some("archivee"), _) => { |
| 164 | - Some("Tâche archivée — les temps restent dans les stats".to_string()) | 164 | + Some("Catégorie archivée — les temps restent dans les stats".to_string()) |
| 165 | } | 165 | } |
| 166 | - (Some("restauree"), _) => Some("Tâche restaurée".to_string()), | 166 | + (Some("restauree"), _) => Some("Catégorie restaurée".to_string()), |
| 167 | - (_, Some("doublon")) => Some("Une tâche porte déjà ce nom".to_string()), | 167 | + (_, Some("doublon")) => Some("Une catégorie porte déjà ce nom".to_string()), |
| 168 | (_, Some("vide")) => Some("Le nom ne peut pas être vide".to_string()), | 168 | (_, Some("vide")) => Some("Le nom ne peut pas être vide".to_string()), |
| 169 | _ => None, | 169 | _ => None, |
| 170 | }; | 170 | }; |
| 171 | - let by_creation = tasks::list_by_creation(&state.pool, ctx.team_id).await?; | 171 | + let by_creation = categories::list_by_creation(&state.pool, ctx.team_id).await?; |
| 172 | let colors = color_map(&by_creation.iter().map(|t| t.id).collect::<Vec<_>>()); | 172 | let colors = color_map(&by_creation.iter().map(|t| t.id).collect::<Vec<_>>()); |
| 173 | let mut list = by_creation; | 173 | let mut list = by_creation; |
| 174 | list.sort_by(|a, b| (a.archived, &a.name).cmp(&(b.archived, &b.name))); | 174 | list.sort_by(|a, b| (a.archived, &a.name).cmp(&(b.archived, &b.name))); |
| 175 | - let tasks = list | 175 | + let categories = list |
| 176 | .into_iter() | 176 | .into_iter() |
| 177 | - .map(|t| TaskView { | 177 | + .map(|t| CategoryView { |
| 178 | color: colors[&t.id], | 178 | color: colors[&t.id], |
| 179 | - rename_action: format!("/parametres/taches/{}", t.id), | 179 | + rename_action: format!("/parametres/categories/{}", t.id), |
| 180 | - toggle_action: format!("/parametres/taches/{}/archiver", t.id), | 180 | + toggle_action: format!("/parametres/categories/{}/archiver", t.id), |
| 181 | name: t.name, | 181 | name: t.name, |
| 182 | archived: t.archived, | 182 | archived: t.archived, |
| 183 | }) | 183 | }) |
| 184 | .collect(); | 184 | .collect(); |
| 185 | - Ok(HtmlTemplate(TasksPage { | 185 | + Ok(HtmlTemplate(CategoriesPage { |
| 186 | csrf: ctx.user.csrf_token(&state), | 186 | csrf: ctx.user.csrf_token(&state), |
| 187 | - shell: shell(&state, &ctx, "Tâches", "add", toast).await?, | 187 | + shell: shell(&state, &ctx, "Catégories", "add", toast).await?, |
| 188 | - tasks, | 188 | + categories, |
| 189 | })) | 189 | })) |
| 190 | } | 190 | } |
| 191 | 191 | ||
| 192 | #[derive(Deserialize)] | 192 | #[derive(Deserialize)] |
| 193 | -pub struct TaskForm { | 193 | +pub struct CategoryFormb { |
| 194 | name: String, | 194 | name: String, |
| 195 | csrf: String, | 195 | csrf: String, |
| 196 | } | 196 | } |
| @@ -199,42 +199,42 @@ fn is_unique_violation(e: &sqlx::Error) -> bool { | |||
| 199 | matches!(e, sqlx::Error::Database(db) if db.is_unique_violation()) | 199 | matches!(e, sqlx::Error::Database(db) if db.is_unique_violation()) |
| 200 | } | 200 | } |
| 201 | 201 | ||
| 202 | -pub async fn task_create( | 202 | +pub async fn category_create( |
| 203 | State(state): State<AppState>, | 203 | State(state): State<AppState>, |
| 204 | ctx: ActiveTeam, | 204 | ctx: ActiveTeam, |
| 205 | - Form(form): Form<TaskForm>, | 205 | + Form(form): Form<CategoryFormb>, |
| 206 | ) -> Result<Redirect, AppError> { | 206 | ) -> Result<Redirect, AppError> { |
| 207 | if !ctx.user.check_csrf(&state, &form.csrf) { | 207 | if !ctx.user.check_csrf(&state, &form.csrf) { |
| 208 | return Err(AppError::Csrf); | 208 | return Err(AppError::Csrf); |
| 209 | } | 209 | } |
| 210 | let name = form.name.trim(); | 210 | let name = form.name.trim(); |
| 211 | if name.is_empty() { | 211 | if name.is_empty() { |
| 212 | - return Ok(Redirect::to("/parametres/taches?err=vide")); | 212 | + return Ok(Redirect::to("/parametres/categories?err=vide")); |
| 213 | } | 213 | } |
| 214 | - match tasks::create(&state.pool, ctx.team_id, name).await { | 214 | + match categories::create(&state.pool, ctx.team_id, name).await { |
| 215 | - Ok(_) => Ok(Redirect::to("/parametres/taches?ok=ajoutee")), | 215 | + Ok(_) => Ok(Redirect::to("/parametres/categories?ok=ajoutee")), |
| 216 | - Err(e) if is_unique_violation(&e) => Ok(Redirect::to("/parametres/taches?err=doublon")), | 216 | + Err(e) if is_unique_violation(&e) => Ok(Redirect::to("/parametres/categories?err=doublon")), |
| 217 | Err(e) => Err(e.into()), | 217 | Err(e) => Err(e.into()), |
| 218 | } | 218 | } |
| 219 | } | 219 | } |
| 220 | 220 | ||
| 221 | -pub async fn task_rename( | 221 | +pub async fn category_rename( |
| 222 | State(state): State<AppState>, | 222 | State(state): State<AppState>, |
| 223 | ctx: ActiveTeam, | 223 | ctx: ActiveTeam, |
| 224 | Path(id): Path<Uuid>, | 224 | Path(id): Path<Uuid>, |
| 225 | - Form(form): Form<TaskForm>, | 225 | + Form(form): Form<CategoryFormb>, |
| 226 | ) -> Result<Redirect, AppError> { | 226 | ) -> Result<Redirect, AppError> { |
| 227 | if !ctx.user.check_csrf(&state, &form.csrf) { | 227 | if !ctx.user.check_csrf(&state, &form.csrf) { |
| 228 | return Err(AppError::Csrf); | 228 | return Err(AppError::Csrf); |
| 229 | } | 229 | } |
| 230 | let name = form.name.trim(); | 230 | let name = form.name.trim(); |
| 231 | if name.is_empty() { | 231 | if name.is_empty() { |
| 232 | - return Ok(Redirect::to("/parametres/taches?err=vide")); | 232 | + return Ok(Redirect::to("/parametres/categories?err=vide")); |
| 233 | } | 233 | } |
| 234 | - match tasks::rename(&state.pool, id, ctx.team_id, name).await { | 234 | + match categories::rename(&state.pool, id, ctx.team_id, name).await { |
| 235 | Ok(0) => Err(AppError::NotFound), | 235 | Ok(0) => Err(AppError::NotFound), |
| 236 | - Ok(_) => Ok(Redirect::to("/parametres/taches?ok=renommee")), | 236 | + Ok(_) => Ok(Redirect::to("/parametres/categories?ok=renommee")), |
| 237 | - Err(e) if is_unique_violation(&e) => Ok(Redirect::to("/parametres/taches?err=doublon")), | 237 | + Err(e) if is_unique_violation(&e) => Ok(Redirect::to("/parametres/categories?err=doublon")), |
| 238 | Err(e) => Err(e.into()), | 238 | Err(e) => Err(e.into()), |
| 239 | } | 239 | } |
| 240 | } | 240 | } |
| @@ -242,11 +242,11 @@ pub async fn task_rename( | |||
| 242 | #[derive(Deserialize)] | 242 | #[derive(Deserialize)] |
| 243 | pub struct ToggleForm { | 243 | pub struct ToggleForm { |
| 244 | csrf: String, | 244 | csrf: String, |
| 245 | - /// true si la tâche était archivée (pour le message du toast). | 245 | + /// true si la catégorie était archivée (pour le message du toast). |
| 246 | restaure: Option<u8>, | 246 | restaure: Option<u8>, |
| 247 | } | 247 | } |
| 248 | 248 | ||
| 249 | -pub async fn task_toggle_archive( | 249 | +pub async fn category_toggle_archive( |
| 250 | State(state): State<AppState>, | 250 | State(state): State<AppState>, |
| 251 | ctx: ActiveTeam, | 251 | ctx: ActiveTeam, |
| 252 | Path(id): Path<Uuid>, | 252 | Path(id): Path<Uuid>, |
| @@ -255,13 +255,13 @@ pub async fn task_toggle_archive( | |||
| 255 | if !ctx.user.check_csrf(&state, &form.csrf) { | 255 | if !ctx.user.check_csrf(&state, &form.csrf) { |
| 256 | return Err(AppError::Csrf); | 256 | return Err(AppError::Csrf); |
| 257 | } | 257 | } |
| 258 | - if tasks::toggle_archived(&state.pool, id, ctx.team_id).await? == 0 { | 258 | + if categories::toggle_archived(&state.pool, id, ctx.team_id).await? == 0 { |
| 259 | return Err(AppError::NotFound); | 259 | return Err(AppError::NotFound); |
| 260 | } | 260 | } |
| 261 | Ok(Redirect::to(if form.restaure.is_some() { | 261 | Ok(Redirect::to(if form.restaure.is_some() { |
| 262 | - "/parametres/taches?ok=restauree" | 262 | + "/parametres/categories?ok=restauree" |
| 263 | } else { | 263 | } else { |
| 264 | - "/parametres/taches?ok=archivee" | 264 | + "/parametres/categories?ok=archivee" |
| 265 | })) | 265 | })) |
| 266 | } | 266 | } |
| 267 | 267 | ||
modified
src/routes/stats.rs +3 -3 | @@ -7,7 +7,7 @@ use uuid::Uuid; | ||
| 7 | 7 | |
| 8 | 8 | use crate::charts::{ChartNav, ChartView, LegendItem, Sheet, build_chart, color_map, legend}; |
| 9 | 9 | use crate::db::users::UserRow; |
| 10 | -use crate::db::{stats, tasks, teams}; | |
| 10 | +use crate::db::{stats, categories, teams}; | |
| 11 | 11 | use crate::error::AppError; |
| 12 | 12 | use crate::period::{Month, Week, Year}; |
| 13 | 13 | use crate::render::{HtmlTemplate, Shell}; |
| @@ -60,7 +60,7 @@ pub async fn stats_page( | ||
| 60 | 60 | let (month_from, month_to) = month.bounds(); |
| 61 | 61 | let (year_from, year_to) = year.bounds(); |
| 62 | 62 | |
| 63 | - let all_tasks = tasks::list_by_creation(&state.pool, ctx.team_id).await?; | |
| 63 | + let all_tasks = categories::list_by_creation(&state.pool, ctx.team_id).await?; | |
| 64 | 64 | let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>()); |
| 65 | 65 | |
| 66 | 66 | let week_rows = stats::totals(&state.pool, ctx.team_id, Some(week_from), Some(week_to)).await?; |
| @@ -70,7 +70,7 @@ pub async fn stats_page( | ||
| 70 | 70 | let total_rows = stats::totals(&state.pool, ctx.team_id, None, None).await?; |
| 71 | 71 | |
| 72 | 72 | // Barres = membres actuels (même à zéro) ∪ ex-membres qui ont des temps |
| 73 | - // sur les tâches de l'équipe (historique intact), triés par username. | |
| 73 | + // sur les catégories de l'équipe (historique intact), triés par username. | |
| 74 | 74 | let mut by_name: BTreeMap<String, Uuid> = teams::members(&state.pool, ctx.team_id) |
| 75 | 75 | .await? |
| 76 | 76 | .into_iter() |
| @@ -7,7 +7,7 @@ use uuid::Uuid; | |||
| 7 | 7 | ||
| 8 | use crate::charts::{ChartNav, ChartView, LegendItem, Sheet, build_chart, color_map, legend}; | 8 | use crate::charts::{ChartNav, ChartView, LegendItem, Sheet, build_chart, color_map, legend}; |
| 9 | use crate::db::users::UserRow; | 9 | use crate::db::users::UserRow; |
| 10 | -use crate::db::{stats, tasks, teams}; | 10 | +use crate::db::{stats, categories, teams}; |
| 11 | use crate::error::AppError; | 11 | use crate::error::AppError; |
| 12 | use crate::period::{Month, Week, Year}; | 12 | use crate::period::{Month, Week, Year}; |
| 13 | use crate::render::{HtmlTemplate, Shell}; | 13 | use crate::render::{HtmlTemplate, Shell}; |
| @@ -60,7 +60,7 @@ pub async fn stats_page( | |||
| 60 | let (month_from, month_to) = month.bounds(); | 60 | let (month_from, month_to) = month.bounds(); |
| 61 | let (year_from, year_to) = year.bounds(); | 61 | let (year_from, year_to) = year.bounds(); |
| 62 | 62 | ||
| 63 | - let all_tasks = tasks::list_by_creation(&state.pool, ctx.team_id).await?; | 63 | + let all_tasks = categories::list_by_creation(&state.pool, ctx.team_id).await?; |
| 64 | let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>()); | 64 | let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>()); |
| 65 | 65 | ||
| 66 | let week_rows = stats::totals(&state.pool, ctx.team_id, Some(week_from), Some(week_to)).await?; | 66 | let week_rows = stats::totals(&state.pool, ctx.team_id, Some(week_from), Some(week_to)).await?; |
| @@ -70,7 +70,7 @@ pub async fn stats_page( | |||
| 70 | let total_rows = stats::totals(&state.pool, ctx.team_id, None, None).await?; | 70 | let total_rows = stats::totals(&state.pool, ctx.team_id, None, None).await?; |
| 71 | 71 | ||
| 72 | // Barres = membres actuels (même à zéro) ∪ ex-membres qui ont des temps | 72 | // Barres = membres actuels (même à zéro) ∪ ex-membres qui ont des temps |
| 73 | - // sur les tâches de l'équipe (historique intact), triés par username. | 73 | + // sur les catégories de l'équipe (historique intact), triés par username. |
| 74 | let mut by_name: BTreeMap<String, Uuid> = teams::members(&state.pool, ctx.team_id) | 74 | let mut by_name: BTreeMap<String, Uuid> = teams::members(&state.pool, ctx.team_id) |
| 75 | .await? | 75 | .await? |
| 76 | .into_iter() | 76 | .into_iter() |
modified
src/routes/timer.rs +1 -1 | @@ -40,7 +40,7 @@ pub struct CsrfOnly { | ||
| 40 | 40 | csrf: String, |
| 41 | 41 | } |
| 42 | 42 | |
| 43 | -/// POST /timer/arreter — fige la mesure et ouvre le choix de la tâche. | |
| 43 | +/// POST /timer/arreter — fige la mesure et ouvre le choix de la catégorie. | |
| 44 | 44 | /// Si le timer (ou la mesure) vit dans une autre équipe que l'équipe active |
| 45 | 45 | /// de l'appareil, on bascule dessus au passage : l'enregistrement ne peut se |
| 46 | 46 | /// faire que là-bas. Timer inexistant : redirection douce. |
| @@ -40,7 +40,7 @@ pub struct CsrfOnly { | |||
| 40 | csrf: String, | 40 | csrf: String, |
| 41 | } | 41 | } |
| 42 | 42 | ||
| 43 | -/// POST /timer/arreter — fige la mesure et ouvre le choix de la tâche. | 43 | +/// POST /timer/arreter — fige la mesure et ouvre le choix de la catégorie. |
| 44 | /// Si le timer (ou la mesure) vit dans une autre équipe que l'équipe active | 44 | /// Si le timer (ou la mesure) vit dans une autre équipe que l'équipe active |
| 45 | /// de l'appareil, on bascule dessus au passage : l'enregistrement ne peut se | 45 | /// de l'appareil, on bascule dessus au passage : l'enregistrement ne peut se |
| 46 | /// faire que là-bas. Timer inexistant : redirection douce. | 46 | /// faire que là-bas. Timer inexistant : redirection douce. |
modified
templates/add_step1.html +6 -6 | @@ -10,12 +10,12 @@ | ||
| 10 | 10 | </form> |
| 11 | 11 | </div> |
| 12 | 12 | {% endif %} |
| 13 | -{% if tasks.is_empty() %} | |
| 14 | -<p class="step-hint">Aucune tâche pour l'instant — commence par en créer une.</p> | |
| 13 | +{% if categories.is_empty() %} | |
| 14 | +<p class="step-hint">Aucune catégorie pour l'instant — commence par en créer une.</p> | |
| 15 | 15 | {% else %} |
| 16 | -<p class="step-hint">Étape 1 sur 2 — choisis la tâche</p> | |
| 17 | -<div class="task-pick"> | |
| 18 | - {% for t in tasks %} | |
| 16 | +<p class="step-hint">Étape 1 sur 2 — choisis la catégorie</p> | |
| 17 | +<div class="category-pick"> | |
| 18 | + {% for t in categories %} | |
| 19 | 19 | <a href="{{ t.href }}"> |
| 20 | 20 | <span class="dot dot-14" style="background:{{ t.color }}"></span> |
| 21 | 21 | <span class="tname">{{ t.name }}</span> |
| @@ -24,5 +24,5 @@ | ||
| 24 | 24 | {% endfor %} |
| 25 | 25 | </div> |
| 26 | 26 | {% endif %} |
| 27 | -<a class="btn btn-ghost" style="margin-top:var(--space-4)" href="/parametres/taches">Gérer les tâches</a> | |
| 27 | +<a class="btn btn-ghost" style="margin-top:var(--space-4)" href="/parametres/categories">Gérer les catégories</a> | |
| 28 | 28 | {% endblock %} |
| @@ -10,12 +10,12 @@ | |||
| 10 | </form> | 10 | </form> |
| 11 | </div> | 11 | </div> |
| 12 | {% endif %} | 12 | {% endif %} |
| 13 | -{% if tasks.is_empty() %} | 13 | +{% if categories.is_empty() %} |
| 14 | -<p class="step-hint">Aucune tâche pour l'instant — commence par en créer une.</p> | 14 | +<p class="step-hint">Aucune catégorie pour l'instant — commence par en créer une.</p> |
| 15 | {% else %} | 15 | {% else %} |
| 16 | -<p class="step-hint">Étape 1 sur 2 — choisis la tâche</p> | 16 | +<p class="step-hint">Étape 1 sur 2 — choisis la catégorie</p> |
| 17 | -<div class="task-pick"> | 17 | +<div class="category-pick"> |
| 18 | - {% for t in tasks %} | 18 | + {% for t in categories %} |
| 19 | <a href="{{ t.href }}"> | 19 | <a href="{{ t.href }}"> |
| 20 | <span class="dot dot-14" style="background:{{ t.color }}"></span> | 20 | <span class="dot dot-14" style="background:{{ t.color }}"></span> |
| 21 | <span class="tname">{{ t.name }}</span> | 21 | <span class="tname">{{ t.name }}</span> |
| @@ -24,5 +24,5 @@ | |||
| 24 | {% endfor %} | 24 | {% endfor %} |
| 25 | </div> | 25 | </div> |
| 26 | {% endif %} | 26 | {% endif %} |
| 27 | -<a class="btn btn-ghost" style="margin-top:var(--space-4)" href="/parametres/taches">Gérer les tâches</a> | 27 | +<a class="btn btn-ghost" style="margin-top:var(--space-4)" href="/parametres/categories">Gérer les catégories</a> |
| 28 | {% endblock %} | 28 | {% endblock %} |
modified
templates/add_step2.html +2 -2 | @@ -1,6 +1,6 @@ | ||
| 1 | 1 | {% extends "app.html" %} |
| 2 | 2 | {% block content %} |
| 3 | -<a class="btn btn-ghost" style="margin:0 0 var(--space-3);padding-left:0" href="/temps/nouveau">← Changer de tâche</a> | |
| 3 | +<a class="btn btn-ghost" style="margin:0 0 var(--space-3);padding-left:0" href="/temps/nouveau">← Changer de catégorie</a> | |
| 4 | 4 | <div class="picked"> |
| 5 | 5 | <span class="dot dot-16" style="background:{{ picked_color }}"></span> |
| 6 | 6 | <span class="pname">{{ picked_name }}</span> |
| @@ -8,7 +8,7 @@ | ||
| 8 | 8 | |
| 9 | 9 | <form method="post" action="/temps"> |
| 10 | 10 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 11 | - <input type="hidden" name="tache" value="{{ tache }}"> | |
| 11 | + <input type="hidden" name="categorie" value="{{ categorie }}"> | |
| 12 | 12 | |
| 13 | 13 | <label class="mini-label" for="minutes">Durée</label> |
| 14 | 14 | <div class="duration-row"> |
| @@ -1,6 +1,6 @@ | |||
| 1 | {% extends "app.html" %} | 1 | {% extends "app.html" %} |
| 2 | {% block content %} | 2 | {% block content %} |
| 3 | -<a class="btn btn-ghost" style="margin:0 0 var(--space-3);padding-left:0" href="/temps/nouveau">← Changer de tâche</a> | 3 | +<a class="btn btn-ghost" style="margin:0 0 var(--space-3);padding-left:0" href="/temps/nouveau">← Changer de catégorie</a> |
| 4 | <div class="picked"> | 4 | <div class="picked"> |
| 5 | <span class="dot dot-16" style="background:{{ picked_color }}"></span> | 5 | <span class="dot dot-16" style="background:{{ picked_color }}"></span> |
| 6 | <span class="pname">{{ picked_name }}</span> | 6 | <span class="pname">{{ picked_name }}</span> |
| @@ -8,7 +8,7 @@ | |||
| 8 | 8 | ||
| 9 | <form method="post" action="/temps"> | 9 | <form method="post" action="/temps"> |
| 10 | <input type="hidden" name="csrf" value="{{ csrf }}"> | 10 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 11 | - <input type="hidden" name="tache" value="{{ tache }}"> | 11 | + <input type="hidden" name="categorie" value="{{ categorie }}"> |
| 12 | 12 | ||
| 13 | <label class="mini-label" for="minutes">Durée</label> | 13 | <label class="mini-label" for="minutes">Durée</label> |
| 14 | <div class="duration-row"> | 14 | <div class="duration-row"> |
renamed
templates/categories.html +6 -6 | similarity index 74% | ||
| rename from templates/tasks.html | ||
| rename to templates/categories.html | ||
| @@ -1,17 +1,17 @@ | ||
| 1 | 1 | {% extends "app.html" %} |
| 2 | 2 | {% block content %} |
| 3 | -<p class="step-hint">Les tâches appartiennent à l'équipe {{ shell.team_name }} et sont modifiables par tous ses membres. Archiver une tâche la retire des listes ; les temps déjà saisis restent dans les stats.</p> | |
| 4 | -<form method="post" action="/parametres/taches" class="task-add-row"> | |
| 3 | +<p class="step-hint">Les catégories appartiennent à l'équipe {{ shell.team_name }} et sont modifiables par tous ses membres. Archiver une catégorie la retire des listes ; les temps déjà saisis restent dans les stats.</p> | |
| 4 | +<form method="post" action="/parametres/categories" class="category-add-row"> | |
| 5 | 5 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 6 | - <input class="input" name="name" placeholder="Nouvelle tâche" required aria-label="Nouvelle tâche"> | |
| 6 | + <input class="input" name="name" placeholder="Nouvelle catégorie" required aria-label="Nouvelle catégorie"> | |
| 7 | 7 | <button type="submit" class="btn btn-primary">Ajouter</button> |
| 8 | 8 | </form> |
| 9 | -{% for t in tasks %} | |
| 10 | -<div class="task-row {% if t.archived %}archived{% endif %}"> | |
| 9 | +{% for t in categories %} | |
| 10 | +<div class="category-row {% if t.archived %}archived{% endif %}"> | |
| 11 | 11 | <span class="dot dot-12" style="background:{{ t.color }}"></span> |
| 12 | 12 | <form method="post" action="{{ t.rename_action }}" class="rename"> |
| 13 | 13 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 14 | - <input class="input" name="name" value="{{ t.name }}" required aria-label="Renommer la tâche (Entrée pour valider)"> | |
| 14 | + <input class="input" name="name" value="{{ t.name }}" required aria-label="Renommer la catégorie (Entrée pour valider)"> | |
| 15 | 15 | {# tabindex=-1 : la soumission clavier passe par Entrée dans l'input ; |
| 16 | 16 | sans ça, ce bouton invisible est un stop de tabulation sans focus visible. #} |
| 17 | 17 | <button type="submit" class="visually-hidden" tabindex="-1">Renommer</button> |
| similarity index 74% | |||
| rename from templates/tasks.html | |||
| rename to templates/categories.html | |||
| @@ -1,17 +1,17 @@ | |||
| 1 | {% extends "app.html" %} | 1 | {% extends "app.html" %} |
| 2 | {% block content %} | 2 | {% block content %} |
| 3 | -<p class="step-hint">Les tâches appartiennent à l'équipe {{ shell.team_name }} et sont modifiables par tous ses membres. Archiver une tâche la retire des listes ; les temps déjà saisis restent dans les stats.</p> | 3 | +<p class="step-hint">Les catégories appartiennent à l'équipe {{ shell.team_name }} et sont modifiables par tous ses membres. Archiver une catégorie la retire des listes ; les temps déjà saisis restent dans les stats.</p> |
| 4 | -<form method="post" action="/parametres/taches" class="task-add-row"> | 4 | +<form method="post" action="/parametres/categories" class="category-add-row"> |
| 5 | <input type="hidden" name="csrf" value="{{ csrf }}"> | 5 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 6 | - <input class="input" name="name" placeholder="Nouvelle tâche" required aria-label="Nouvelle tâche"> | 6 | + <input class="input" name="name" placeholder="Nouvelle catégorie" required aria-label="Nouvelle catégorie"> |
| 7 | <button type="submit" class="btn btn-primary">Ajouter</button> | 7 | <button type="submit" class="btn btn-primary">Ajouter</button> |
| 8 | </form> | 8 | </form> |
| 9 | -{% for t in tasks %} | 9 | +{% for t in categories %} |
| 10 | -<div class="task-row {% if t.archived %}archived{% endif %}"> | 10 | +<div class="category-row {% if t.archived %}archived{% endif %}"> |
| 11 | <span class="dot dot-12" style="background:{{ t.color }}"></span> | 11 | <span class="dot dot-12" style="background:{{ t.color }}"></span> |
| 12 | <form method="post" action="{{ t.rename_action }}" class="rename"> | 12 | <form method="post" action="{{ t.rename_action }}" class="rename"> |
| 13 | <input type="hidden" name="csrf" value="{{ csrf }}"> | 13 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 14 | - <input class="input" name="name" value="{{ t.name }}" required aria-label="Renommer la tâche (Entrée pour valider)"> | 14 | + <input class="input" name="name" value="{{ t.name }}" required aria-label="Renommer la catégorie (Entrée pour valider)"> |
| 15 | {# tabindex=-1 : la soumission clavier passe par Entrée dans l'input ; | 15 | {# tabindex=-1 : la soumission clavier passe par Entrée dans l'input ; |
| 16 | sans ça, ce bouton invisible est un stop de tabulation sans focus visible. #} | 16 | sans ça, ce bouton invisible est un stop de tabulation sans focus visible. #} |
| 17 | <button type="submit" class="visually-hidden" tabindex="-1">Renommer</button> | 17 | <button type="submit" class="visually-hidden" tabindex="-1">Renommer</button> |
modified
templates/edit.html +3 -3 | @@ -4,9 +4,9 @@ | ||
| 4 | 4 | <form method="post" action="{{ action }}"> |
| 5 | 5 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 6 | 6 | <div class="field" style="margin-bottom:var(--space-3)"> |
| 7 | - <label for="tache">Tâche</label> | |
| 8 | - <select class="input" id="tache" name="tache" required style="min-height:44px"> | |
| 9 | - {% for t in tasks %} | |
| 7 | + <label for="categorie">Catégorie</label> | |
| 8 | + <select class="input" id="categorie" name="categorie" required style="min-height:44px"> | |
| 9 | + {% for t in categories %} | |
| 10 | 10 | <option value="{{ t.id }}"{% if t.selected %} selected{% endif %}>{{ t.name }}</option> |
| 11 | 11 | {% endfor %} |
| 12 | 12 | </select> |
| @@ -4,9 +4,9 @@ | |||
| 4 | <form method="post" action="{{ action }}"> | 4 | <form method="post" action="{{ action }}"> |
| 5 | <input type="hidden" name="csrf" value="{{ csrf }}"> | 5 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 6 | <div class="field" style="margin-bottom:var(--space-3)"> | 6 | <div class="field" style="margin-bottom:var(--space-3)"> |
| 7 | - <label for="tache">Tâche</label> | 7 | + <label for="categorie">Catégorie</label> |
| 8 | - <select class="input" id="tache" name="tache" required style="min-height:44px"> | 8 | + <select class="input" id="categorie" name="categorie" required style="min-height:44px"> |
| 9 | - {% for t in tasks %} | 9 | + {% for t in categories %} |
| 10 | <option value="{{ t.id }}"{% if t.selected %} selected{% endif %}>{{ t.name }}</option> | 10 | <option value="{{ t.id }}"{% if t.selected %} selected{% endif %}>{{ t.name }}</option> |
| 11 | {% endfor %} | 11 | {% endfor %} |
| 12 | </select> | 12 | </select> |
modified
templates/login.html +1 -1 | @@ -6,7 +6,7 @@ | ||
| 6 | 6 | <svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="#f5ead8" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></svg> |
| 7 | 7 | </div> |
| 8 | 8 | <h1>time-time</h1> |
| 9 | - <p class="tagline">Le temps passé sur chaque tâche, partagé avec ton équipe.</p> | |
| 9 | + <p class="tagline">Le temps passé sur chaque catégorie, partagé avec ton équipe.</p> | |
| 10 | 10 | <form method="post" action="/login"> |
| 11 | 11 | <div class="field" style="margin-bottom:var(--space-3)"> |
| 12 | 12 | <label for="username">Identifiant</label> |
| @@ -6,7 +6,7 @@ | |||
| 6 | <svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="#f5ead8" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></svg> | 6 | <svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="#f5ead8" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></svg> |
| 7 | </div> | 7 | </div> |
| 8 | <h1>time-time</h1> | 8 | <h1>time-time</h1> |
| 9 | - <p class="tagline">Le temps passé sur chaque tâche, partagé avec ton équipe.</p> | 9 | + <p class="tagline">Le temps passé sur chaque catégorie, partagé avec ton équipe.</p> |
| 10 | <form method="post" action="/login"> | 10 | <form method="post" action="/login"> |
| 11 | <div class="field" style="margin-bottom:var(--space-3)"> | 11 | <div class="field" style="margin-bottom:var(--space-3)"> |
| 12 | <label for="username">Identifiant</label> | 12 | <label for="username">Identifiant</label> |
modified
templates/stats.html +2 -2 | @@ -53,12 +53,12 @@ | ||
| 53 | 53 | {% endfor %} |
| 54 | 54 | |
| 55 | 55 | {% for s in sheets %} |
| 56 | -<div class="sheet-wrap" id="{{ s.id }}" role="dialog" aria-label="Détail — {{ s.task }}"> | |
| 56 | +<div class="sheet-wrap" id="{{ s.id }}" role="dialog" aria-label="Détail — {{ s.category }}"> | |
| 57 | 57 | <a class="sheet-backdrop" href="#fermer" aria-label="Fermer"></a> |
| 58 | 58 | <div class="sheet"> |
| 59 | 59 | <div class="sheet-head"> |
| 60 | 60 | <span class="dot dot-14" style="background:{{ s.color }}"></span> |
| 61 | - <span class="pname">{{ s.task }}</span> | |
| 61 | + <span class="pname">{{ s.category }}</span> | |
| 62 | 62 | <a class="btn btn-ghost close" href="#fermer">Fermer</a> |
| 63 | 63 | </div> |
| 64 | 64 | <div class="sheet-figures"> |
| @@ -53,12 +53,12 @@ | |||
| 53 | {% endfor %} | 53 | {% endfor %} |
| 54 | 54 | ||
| 55 | {% for s in sheets %} | 55 | {% for s in sheets %} |
| 56 | -<div class="sheet-wrap" id="{{ s.id }}" role="dialog" aria-label="Détail — {{ s.task }}"> | 56 | +<div class="sheet-wrap" id="{{ s.id }}" role="dialog" aria-label="Détail — {{ s.category }}"> |
| 57 | <a class="sheet-backdrop" href="#fermer" aria-label="Fermer"></a> | 57 | <a class="sheet-backdrop" href="#fermer" aria-label="Fermer"></a> |
| 58 | <div class="sheet"> | 58 | <div class="sheet"> |
| 59 | <div class="sheet-head"> | 59 | <div class="sheet-head"> |
| 60 | <span class="dot dot-14" style="background:{{ s.color }}"></span> | 60 | <span class="dot dot-14" style="background:{{ s.color }}"></span> |
| 61 | - <span class="pname">{{ s.task }}</span> | 61 | + <span class="pname">{{ s.category }}</span> |
| 62 | <a class="btn btn-ghost close" href="#fermer">Fermer</a> | 62 | <a class="btn btn-ghost close" href="#fermer">Fermer</a> |
| 63 | </div> | 63 | </div> |
| 64 | <div class="sheet-figures"> | 64 | <div class="sheet-figures"> |
modified
templates/timer.html +1 -1 | @@ -5,7 +5,7 @@ | ||
| 5 | 5 | <p class="chrono-time" data-started="{{ started_ms }}">{{ live_label }}</p> |
| 6 | 6 | <p class="chrono-start">Démarré à {{ start_clock }}</p> |
| 7 | 7 | </div> |
| 8 | -<p class="chrono-hint">Termine le minuteur pour choisir la tâche et enregistrer ce temps.</p> | |
| 8 | +<p class="chrono-hint">Termine le minuteur pour choisir la catégorie et enregistrer ce temps.</p> | |
| 9 | 9 | <div style="display:flex;flex-direction:column;gap:var(--space-3)"> |
| 10 | 10 | <form method="post" action="/timer/arreter" style="margin:0;display:flex"> |
| 11 | 11 | <input type="hidden" name="csrf" value="{{ shell.csrf }}"> |
| @@ -5,7 +5,7 @@ | |||
| 5 | <p class="chrono-time" data-started="{{ started_ms }}">{{ live_label }}</p> | 5 | <p class="chrono-time" data-started="{{ started_ms }}">{{ live_label }}</p> |
| 6 | <p class="chrono-start">Démarré à {{ start_clock }}</p> | 6 | <p class="chrono-start">Démarré à {{ start_clock }}</p> |
| 7 | </div> | 7 | </div> |
| 8 | -<p class="chrono-hint">Termine le minuteur pour choisir la tâche et enregistrer ce temps.</p> | 8 | +<p class="chrono-hint">Termine le minuteur pour choisir la catégorie et enregistrer ce temps.</p> |
| 9 | <div style="display:flex;flex-direction:column;gap:var(--space-3)"> | 9 | <div style="display:flex;flex-direction:column;gap:var(--space-3)"> |
| 10 | <form method="post" action="/timer/arreter" style="margin:0;display:flex"> | 10 | <form method="post" action="/timer/arreter" style="margin:0;display:flex"> |
| 11 | <input type="hidden" name="csrf" value="{{ shell.csrf }}"> | 11 | <input type="hidden" name="csrf" value="{{ shell.csrf }}"> |
modified
templates/times.html +1 -1 | @@ -8,7 +8,7 @@ | ||
| 8 | 8 | <li> |
| 9 | 9 | <span class="dot dot-12" style="background:{{ e.color }}"></span> |
| 10 | 10 | <div class="tinfo"> |
| 11 | - <div class="ttask">{{ e.task }}</div> | |
| 11 | + <div class="tcat">{{ e.category }}</div> | |
| 12 | 12 | <div class="tdate">{{ e.date }}</div> |
| 13 | 13 | </div> |
| 14 | 14 | <span class="tdur">{{ e.duration }}</span> |
| @@ -8,7 +8,7 @@ | |||
| 8 | <li> | 8 | <li> |
| 9 | <span class="dot dot-12" style="background:{{ e.color }}"></span> | 9 | <span class="dot dot-12" style="background:{{ e.color }}"></span> |
| 10 | <div class="tinfo"> | 10 | <div class="tinfo"> |
| 11 | - <div class="ttask">{{ e.task }}</div> | 11 | + <div class="tcat">{{ e.category }}</div> |
| 12 | <div class="tdate">{{ e.date }}</div> | 12 | <div class="tdate">{{ e.date }}</div> |
| 13 | </div> | 13 | </div> |
| 14 | <span class="tdur">{{ e.duration }}</span> | 14 | <span class="tdur">{{ e.duration }}</span> |