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

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

Download ZIP

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
Julien Durillon committed 2026-08-08T22:25:35+02:00 Browse files
bb28d5f parent: 638f8c8
modified a11y/audit.mjs +6 -6
@@ -5,7 +5,7 @@
55 // les violations moindres sont listées en avertissement.
66 //
77 // 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
99 // ≥ 1 temps saisi. Exemple de mise en place :
1010 //
1111 // cargo run -- team add "Audit" ; cargo run -- team add "Audit 2"
@@ -57,12 +57,12 @@ if (page.url().includes('/login')) {
5757 process.exit(2);
5858 }
5959
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.
6161 const ids = await page.evaluate(async () => {
6262 const step1 = await fetch('/temps/nouveau').then((r) => r.text());
6363 const times = await fetch('/temps').then((r) => r.text());
6464 return {
65- task: step1.match(/tache=([0-9a-f-]{36})/)?.[1] ?? null,
65+ categorie: step1.match(/categorie=([0-9a-f-]{36})/)?.[1] ?? null,
6666 entry: times.match(/\/temps\/([0-9a-f-]{36})\/modifier/)?.[1] ?? null,
6767 };
6868 });
@@ -70,15 +70,15 @@ const ids = await page.evaluate(async () => {
7070 const pages = [
7171 { name: 'login', url: '/login', anon: true },
7272 { 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}` },
7474 { name: 'stats', url: '/stats' },
7575 { name: 'times', url: '/temps' },
7676 ids.entry && { name: 'edit', url: `/temps/${ids.entry}/modifier` },
7777 { name: 'settings', url: '/parametres' },
78- { name: 'tasks', url: '/parametres/taches' },
78+ { name: 'categories', url: '/parametres/categories' },
7979 { name: 'team-sheet', url: '/temps/nouveau#equipes' },
8080 ].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.');
8282
8383 let blocking = 0;
8484 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 compte7 // 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 et8+// 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); }
317317 .login .tagline { font-size: 15px; opacity: .7; margin: 0 0 var(--space-8); max-width: 15em; }
318318 .form-error { font-size: 13px; color: var(--color-accent-700); margin: 0 0 var(--space-3); }
319319
320-/* — ajout étape 1 : liste de tâches — */
320+/* — ajout étape 1 : liste de catégories — */
321321 .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 {
324324 display: flex; align-items: center; gap: var(--space-3);
325325 padding: var(--space-3) var(--space-4); background: var(--color-surface);
326326 border: 1px solid transparent; border-radius: 999px;
327327 font-size: 16px; color: var(--color-text); text-decoration: none;
328328 }
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; }
332332 .dot { border-radius: 999px; flex: none; display: inline-block; }
333333 .dot-14 { width: 14px; height: 14px; }
334334 .dot-12 { width: 12px; height: 12px; }
@@ -420,24 +420,24 @@ a.team-pill:hover { filter: brightness(.96); }
420420 border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent);
421421 }
422422 .times .tinfo { flex: 1; min-width: 0; }
423-.times .ttask { font-size: 15px; }
423+.times .tcat { font-size: 15px; }
424424 .times .tdate { font-size: 12px; opacity: .7; }
425425 .times .tdur { font-size: 16px; font-family: var(--font-heading); }
426426
427-/* — tâches — */
428-.task-row {
427+/* — catégories — */
428+.category-row {
429429 display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) 0;
430430 border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent);
431431 }
432-.task-row form.rename { flex: 1; }
433-.task-row .rename .input {
432+.category-row form.rename { flex: 1; }
433+.category-row .rename .input {
434434 border-color: transparent; background: transparent; padding-left: 0;
435435 font-size: 15px; border-radius: 0;
436436 }
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; }
441441 .visually-hidden {
442442 position: absolute; width: 1px; height: 1px; overflow: hidden;
443443 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] = [
1515 "#c98d8d",
1616 ];
1717
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).
1919 pub fn color_map(task_ids_in_creation_order: &[Uuid]) -> HashMap<Uuid, &'static str> {
2020 task_ids_in_creation_order
2121 .iter()
@@ -71,7 +71,7 @@ pub struct ChartView {
7171 pub struct Sheet {
7272 pub id: String,
7373 pub color: &'static str,
74- pub task: String,
74+ pub category: String,
7575 pub who: String,
7676 pub duration: String,
7777 pub share: String,
@@ -129,13 +129,13 @@ pub fn build_chart(
129129 let mut segs = Vec::new();
130130 if let Some(rs) = by_user.get(&u.id) {
131131 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]);
133133 let sheet_id = format!("d-{key}-{ui}-{ti}");
134134 let share = (r.total * 100) / total.max(1);
135135 sheets.push(Sheet {
136136 id: sheet_id.clone(),
137137 color,
138- task: r.task_name.clone(),
138+ category: r.category_name.clone(),
139139 who: u.username.clone(),
140140 duration: fmt_minutes(r.total),
141141 share: format!("{share} %"),
@@ -149,7 +149,7 @@ pub fn build_chart(
149149 segs.push(Seg {
150150 flex: r.total,
151151 color,
152- aria: format!("{} — {}", r.task_name, fmt_minutes(r.total)),
152+ aria: format!("{} — {}", r.category_name, fmt_minutes(r.total)),
153153 sheet_id,
154154 });
155155 }
@@ -186,8 +186,8 @@ pub fn legend(row_sets: &[&[StatRow]], colors: &HashMap<Uuid, &'static str>) ->
186186 let mut seen: BTreeMap<String, &'static str> = BTreeMap::new();
187187 for rows in row_sets {
188188 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]));
191191 }
192192 }
193193 seen.into_iter()
@@ -206,12 +206,12 @@ mod tests {
206206 }
207207 }
208208
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 {
210210 StatRow {
211211 user_id: Uuid::from_u128(uid),
212212 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(),
215215 total,
216216 entries,
217217 }
@@ -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_order20 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;
22 use uuid::Uuid;
33
44 #[derive(sqlx::FromRow)]
5-pub struct TaskRow {
5+pub struct CategoryRow {
66 pub id: Uuid,
77 pub name: String,
88 pub archived: bool,
99 }
1010
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")
1414 .bind(team_id)
1515 .fetch_all(pool)
1616 .await
1717 }
1818
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")
2323 .bind(team_id)
2424 .fetch_all(pool)
2525 .await
@@ -27,7 +27,7 @@ pub async fn list_by_creation(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec<
2727
2828 pub async fn create(pool: &PgPool, team_id: Uuid, name: &str) -> sqlx::Result<Uuid> {
2929 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)")
3131 .bind(id)
3232 .bind(team_id)
3333 .bind(name)
@@ -36,9 +36,9 @@ pub async fn create(pool: &PgPool, team_id: Uuid, name: &str) -> sqlx::Result<Uu
3636 Ok(id)
3737 }
3838
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.
4040 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")
4242 .bind(name)
4343 .bind(id)
4444 .bind(team_id)
@@ -48,7 +48,7 @@ pub async fn rename(pool: &PgPool, id: Uuid, team_id: Uuid, name: &str) -> sqlx:
4848 }
4949
5050 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")
5252 .bind(id)
5353 .bind(team_id)
5454 .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 .await16 .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 .await25 .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;
55 #[derive(sqlx::FromRow)]
66 pub struct EntryRow {
77 pub id: Uuid,
8- pub task_id: Uuid,
9- pub task_name: String,
8+ pub category_id: Uuid,
9+ pub category_name: String,
1010 pub minutes: i32,
1111 pub entry_date: NaiveDate,
1212 }
1313
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).
1616 pub async fn insert(
1717 pool: &PgPool,
1818 user_id: Uuid,
1919 team_id: Uuid,
20- task_id: Uuid,
20+ category_id: Uuid,
2121 minutes: i32,
2222 entry_date: NaiveDate,
2323 ) -> sqlx::Result<u64> {
2424 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)
2626 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)",
2828 )
2929 .bind(Uuid::now_v7())
3030 .bind(user_id)
31- .bind(task_id)
31+ .bind(category_id)
3232 .bind(minutes)
3333 .bind(entry_date)
3434 .bind(team_id)
@@ -47,8 +47,8 @@ pub async fn list_first(
4747 limit: i64,
4848 ) -> sqlx::Result<(Vec<EntryRow>, bool)> {
4949 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
5252 WHERE e.user_id = $1 AND t.team_id = $2
5353 ORDER BY e.entry_date DESC, e.id DESC
5454 LIMIT $3",
@@ -63,7 +63,7 @@ pub async fn list_first(
6363 Ok((rows, has_more))
6464 }
6565
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).
6767 pub async fn week_totals_for_user(
6868 pool: &PgPool,
6969 user_id: Uuid,
@@ -72,10 +72,10 @@ pub async fn week_totals_for_user(
7272 to: NaiveDate,
7373 ) -> sqlx::Result<Vec<(Uuid, i64)>> {
7474 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
7777 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",
7979 )
8080 .bind(user_id)
8181 .bind(team_id)
@@ -93,8 +93,8 @@ pub async fn get_owned(
9393 team_id: Uuid,
9494 ) -> sqlx::Result<Option<EntryRow>> {
9595 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
9898 WHERE e.id = $1 AND e.user_id = $2 AND t.team_id = $3",
9999 )
100100 .bind(id)
@@ -105,23 +105,23 @@ pub async fn get_owned(
105105 }
106106
107107 /// 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.
109109 pub async fn update_owned(
110110 pool: &PgPool,
111111 id: Uuid,
112112 user_id: Uuid,
113113 team_id: Uuid,
114- task_id: Uuid,
114+ category_id: Uuid,
115115 minutes: i32,
116116 entry_date: NaiveDate,
117117 ) -> sqlx::Result<u64> {
118118 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
120120 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)",
123123 )
124- .bind(task_id)
124+ .bind(category_id)
125125 .bind(minutes)
126126 .bind(entry_date)
127127 .bind(id)
@@ -141,7 +141,7 @@ pub async fn delete_owned(
141141 let res = sqlx::query(
142142 "DELETE FROM time_entries
143143 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)",
145145 )
146146 .bind(id)
147147 .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, $526 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_date50+ "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_id51+ FROM time_entries e JOIN categories t ON t.id = e.category_id
52 WHERE e.user_id = $1 AND t.team_id = $252 WHERE e.user_id = $1 AND t.team_id = $2
53 ORDER BY e.entry_date DESC, e.id DESC53 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)::BIGINT75+ "SELECT e.category_id, SUM(e.minutes)::BIGINT
76- FROM time_entries e JOIN tasks t ON t.id = e.task_id76+ 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 < $477 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_date96+ "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_id97+ 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 dans107 /// 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 = $3119+ "UPDATE time_entries SET category_id = $1, minutes = $2, entry_date = $3
120 WHERE id = $4 AND user_id = $5120 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_entries142 "DELETE FROM time_entries
143 WHERE id = $1 AND user_id = $2143 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 @@
11 pub mod entries;
22 pub mod invitations;
33 pub mod stats;
4-pub mod tasks;
4+pub mod categories;
55 pub mod timers;
66 pub mod teams;
77 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 {
77 pub user_id: Uuid,
88 /// Sert à faire apparaître les ex-membres qui ont des temps (historique intact).
99 pub username: String,
10- pub task_id: Uuid,
11- pub task_name: String,
10+ pub category_id: Uuid,
11+ pub category_name: String,
1212 pub total: i64,
1313 /// Nombre de saisies (pour la sheet de détail).
1414 pub entries: i64,
1515 }
1616
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).
2020 pub async fn totals(
2121 pool: &PgPool,
2222 team_id: Uuid,
@@ -24,15 +24,15 @@ pub async fn totals(
2424 to: Option<NaiveDate>,
2525 ) -> sqlx::Result<Vec<StatRow>> {
2626 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,
2828 SUM(e.minutes)::BIGINT AS total, COUNT(*)::BIGINT AS entries
2929 FROM time_entries e
30- JOIN tasks t ON t.id = e.task_id
30+ JOIN categories t ON t.id = e.category_id
3131 JOIN users u ON u.id = e.user_id
3232 WHERE t.team_id = $1
3333 AND ($2::date IS NULL OR e.entry_date >= $2)
3434 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",
3636 )
3737 .bind(team_id)
3838 .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 entries28 SUM(e.minutes)::BIGINT AS total, COUNT(*)::BIGINT AS entries
29 FROM time_entries e29 FROM time_entries e
30- JOIN tasks t ON t.id = e.task_id30+ JOIN categories t ON t.id = e.category_id
31 JOIN users u ON u.id = e.user_id31 JOIN users u ON u.id = e.user_id
32 WHERE t.team_id = $132 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;
1717 use crate::state::AppState;
1818
1919 #[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")]
2121 struct Cli {
2222 #[command(subcommand)]
2323 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;
1010
1111 use crate::charts::{color_map, fmt_minutes};
1212 use crate::db::entries::{self, EntryRow};
13-use crate::db::tasks::{self, TaskRow};
13+use crate::db::categories::{self, CategoryRow};
1414 use crate::db::timers;
1515 use crate::error::AppError;
1616 use crate::period::{Week, day_label, paris_today};
@@ -21,9 +21,9 @@ use crate::team::ActiveTeam;
2121
2222 const PAGE_SIZE: i64 = 20;
2323
24-/// ─── Étape 1 : choisir la tâche ───
24+/// ─── Étape 1 : choisir la catégorie ───
2525
26-pub struct PickTask {
26+pub struct PickCategory {
2727 pub href: String,
2828 pub color: &'static str,
2929 pub name: String,
@@ -42,7 +42,7 @@ pub struct MeasuredView {
4242 #[template(path = "add_step1.html")]
4343 pub struct AddStep1Page {
4444 pub shell: Shell,
45- pub tasks: Vec<PickTask>,
45+ pub categories: Vec<PickCategory>,
4646 pub measured: Option<MeasuredView>,
4747 }
4848
@@ -73,7 +73,7 @@ pub struct Preset {
7373 pub struct AddStep2Page {
7474 pub shell: Shell,
7575 pub csrf: String,
76- pub tache: Uuid,
76+ pub categorie: Uuid,
7777 pub picked_color: &'static str,
7878 pub picked_name: String,
7979 pub minutes: i32,
@@ -91,7 +91,7 @@ pub struct AddStep2Page {
9191
9292 #[derive(Deserialize)]
9393 pub struct NewQuery {
94- tache: Option<Uuid>,
94+ categorie: Option<Uuid>,
9595 minutes: Option<i32>,
9696 date: Option<NaiveDate>,
9797 /// minutes de la saisie qui vient d'aboutir (toast).
@@ -102,8 +102,8 @@ pub struct NewQuery {
102102 abandonne: Option<u8>,
103103 }
104104
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}")
107107 }
108108
109109 pub async fn entry_new(
@@ -118,7 +118,7 @@ pub async fn entry_new(
118118 } else {
119119 q.ok.map(|m| format!("{} ajouté ✓", fmt_minutes(m)))
120120 };
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?;
122122 let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>());
123123
124124 let timer_row = timers::get(&state.pool, ctx.user.id).await?;
@@ -147,9 +147,9 @@ pub async fn entry_new(
147147 // Mesure de timer figée dans l'équipe active : elle préremplit l'étape 2.
148148 let pending = timer_row.filter(|t| t.team_id == ctx.team_id && t.stopped_at.is_some());
149149
150- // Étape 2 si une tâche (active) est choisie.
150+ // Étape 2 si une catégorie (active) est choisie.
151151 if let Some(picked) = q
152- .tache
152+ .categorie
153153 .and_then(|id| all_tasks.iter().find(|t| t.id == id && !t.archived))
154154 {
155155 let measured = pending.as_ref().and_then(|t| t.measured_minutes());
@@ -183,7 +183,7 @@ pub async fn entry_new(
183183 return Ok(HtmlTemplate(AddStep2Page {
184184 csrf: ctx.user.csrf_token(&state),
185185 shell: sh,
186- tache: picked.id,
186+ categorie: picked.id,
187187 picked_color: colors[&picked.id],
188188 picked_name: picked.name.clone(),
189189 minutes,
@@ -201,19 +201,19 @@ pub async fn entry_new(
201201 .into_response());
202202 }
203203
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.
205205 let (wfrom, wto) = Week::current().bounds();
206206 let week: HashMap<Uuid, i64> =
207207 entries::week_totals_for_user(&state.pool, ctx.user.id, ctx.team_id, wfrom, wto)
208208 .await?
209209 .into_iter()
210210 .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();
212212 active.sort_by(|a, b| a.name.cmp(&b.name));
213- let tasks = active
213+ let categories = active
214214 .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),
217217 color: colors[&t.id],
218218 name: t.name.clone(),
219219 week_label: week
@@ -223,14 +223,14 @@ pub async fn entry_new(
223223 })
224224 .collect();
225225 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)),
227227 warning: m > 240,
228228 });
229229 let mut sh = shell(&state, &ctx, "Ajouter du temps", "add", toast).await?;
230230 sh.show_start = can_start;
231231 Ok(HtmlTemplate(AddStep1Page {
232232 shell: sh,
233- tasks,
233+ categories,
234234 measured,
235235 })
236236 .into_response())
@@ -239,7 +239,7 @@ pub async fn entry_new(
239239 #[derive(Deserialize)]
240240 pub struct EntryForm {
241241 minutes: i32,
242- tache: Uuid,
242+ categorie: Uuid,
243243 date: NaiveDate,
244244 csrf: String,
245245 }
@@ -266,14 +266,14 @@ pub async fn entry_create(
266266 &state.pool,
267267 ctx.user.id,
268268 ctx.team_id,
269- form.tache,
269+ form.categorie,
270270 form.minutes,
271271 form.date,
272272 )
273273 .await?;
274274 if n == 0 {
275275 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(),
277277 ));
278278 }
279279 // Une mesure de timer en attente est consommée par l'enregistrement.
@@ -285,7 +285,7 @@ pub async fn entry_create(
285285
286286 pub struct TimeView {
287287 pub color: &'static str,
288- pub task: String,
288+ pub category: String,
289289 pub date: String,
290290 pub duration: String,
291291 pub edit_href: String,
@@ -319,14 +319,14 @@ pub async fn entries_list(
319319 let (rows, has_more) =
320320 entries::list_first(&state.pool, ctx.user.id, ctx.team_id, page * PAGE_SIZE).await?;
321321
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?;
323323 let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>());
324324
325325 let entries = rows
326326 .into_iter()
327327 .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,
330330 date: day_label(e.entry_date),
331331 duration: fmt_minutes(e.minutes as i64),
332332 edit_href: format!("/temps/{}/modifier", e.id),
@@ -341,7 +341,7 @@ pub async fn entries_list(
341341
342342 /// ─── Édition ───
343343
344-pub struct TaskOption {
344+pub struct CategoryOption {
345345 pub id: Uuid,
346346 pub name: String,
347347 pub selected: bool,
@@ -354,7 +354,7 @@ pub struct EditPage {
354354 pub csrf: String,
355355 pub action: String,
356356 pub delete_action: String,
357- pub tasks: Vec<TaskOption>,
357+ pub categories: Vec<CategoryOption>,
358358 pub minutes: i32,
359359 pub date: NaiveDate,
360360 }
@@ -367,13 +367,13 @@ pub async fn entry_edit(
367367 let entry = entries::get_owned(&state.pool, id, ctx.user.id, ctx.team_id)
368368 .await?
369369 .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)
372372 .await?
373373 .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,
377377 name: if t.archived {
378378 format!("{} (archivée)", t.name)
379379 } else {
@@ -388,7 +388,7 @@ pub async fn entry_edit(
388388 shell: shell(&state, &ctx, "Modifier le temps", "times", None).await?,
389389 action: format!("/temps/{id}"),
390390 delete_action: format!("/temps/{id}/supprimer"),
391- tasks: options,
391+ categories: options,
392392 minutes: entry.minutes,
393393 date: entry.entry_date,
394394 }))
@@ -409,7 +409,7 @@ pub async fn entry_update(
409409 id,
410410 ctx.user.id,
411411 ctx.team_id,
412- form.tache,
412+ form.categorie,
413413 form.minutes,
414414 form.date,
415415 )
@@ -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) = q151 if let Some(picked) = q
152- .tache152+ .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 = active213+ 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: week219 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 = rows325 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 {
3636 .route("/temps/{id}/supprimer", post(entries::entry_delete))
3737 .route("/stats", get(stats::stats_page))
3838 .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))
4141 .route(
42- "/parametres/taches/{id}/archiver",
43- post(settings::task_toggle_archive),
42+ "/parametres/categories/{id}/archiver",
43+ post(settings::category_toggle_archive),
4444 )
4545 .route(
4646 "/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;
77
88 use crate::auth::password;
99 use crate::charts::color_map;
10-use crate::db::{invitations, tasks, teams, users};
10+use crate::db::{invitations, categories, teams, users};
1111 use crate::error::AppError;
1212 use crate::render::{HtmlTemplate, Shell};
1313 use crate::routes::shell;
@@ -134,9 +134,9 @@ pub async fn settings_page(
134134 }))
135135 }
136136
137-/// ─── Tâches ───
137+/// ─── Catégories ───
138138
139-pub struct TaskView {
139+pub struct CategoryView {
140140 pub color: &'static str,
141141 pub name: String,
142142 pub archived: bool,
@@ -145,52 +145,52 @@ pub struct TaskView {
145145 }
146146
147147 #[derive(Template)]
148-#[template(path = "tasks.html")]
149-pub struct TasksPage {
148+#[template(path = "categories.html")]
149+pub struct CategoriesPage {
150150 pub shell: Shell,
151151 pub csrf: String,
152- pub tasks: Vec<TaskView>,
152+ pub categories: Vec<CategoryView>,
153153 }
154154
155-pub async fn tasks_page(
155+pub async fn categories_page(
156156 State(state): State<AppState>,
157157 ctx: ActiveTeam,
158158 Query(q): Query<SettingsQuery>,
159-) -> Result<HtmlTemplate<TasksPage>, AppError> {
159+) -> Result<HtmlTemplate<CategoriesPage>, AppError> {
160160 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()),
163163 (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())
165165 }
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()),
168168 (_, Some("vide")) => Some("Le nom ne peut pas être vide".to_string()),
169169 _ => None,
170170 };
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?;
172172 let colors = color_map(&by_creation.iter().map(|t| t.id).collect::<Vec<_>>());
173173 let mut list = by_creation;
174174 list.sort_by(|a, b| (a.archived, &a.name).cmp(&(b.archived, &b.name)));
175- let tasks = list
175+ let categories = list
176176 .into_iter()
177- .map(|t| TaskView {
177+ .map(|t| CategoryView {
178178 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),
181181 name: t.name,
182182 archived: t.archived,
183183 })
184184 .collect();
185- Ok(HtmlTemplate(TasksPage {
185+ Ok(HtmlTemplate(CategoriesPage {
186186 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,
189189 }))
190190 }
191191
192192 #[derive(Deserialize)]
193-pub struct TaskForm {
193+pub struct CategoryFormb {
194194 name: String,
195195 csrf: String,
196196 }
@@ -199,42 +199,42 @@ fn is_unique_violation(e: &sqlx::Error) -> bool {
199199 matches!(e, sqlx::Error::Database(db) if db.is_unique_violation())
200200 }
201201
202-pub async fn task_create(
202+pub async fn category_create(
203203 State(state): State<AppState>,
204204 ctx: ActiveTeam,
205- Form(form): Form<TaskForm>,
205+ Form(form): Form<CategoryFormb>,
206206 ) -> Result<Redirect, AppError> {
207207 if !ctx.user.check_csrf(&state, &form.csrf) {
208208 return Err(AppError::Csrf);
209209 }
210210 let name = form.name.trim();
211211 if name.is_empty() {
212- return Ok(Redirect::to("/parametres/taches?err=vide"));
212+ return Ok(Redirect::to("/parametres/categories?err=vide"));
213213 }
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")),
217217 Err(e) => Err(e.into()),
218218 }
219219 }
220220
221-pub async fn task_rename(
221+pub async fn category_rename(
222222 State(state): State<AppState>,
223223 ctx: ActiveTeam,
224224 Path(id): Path<Uuid>,
225- Form(form): Form<TaskForm>,
225+ Form(form): Form<CategoryFormb>,
226226 ) -> Result<Redirect, AppError> {
227227 if !ctx.user.check_csrf(&state, &form.csrf) {
228228 return Err(AppError::Csrf);
229229 }
230230 let name = form.name.trim();
231231 if name.is_empty() {
232- return Ok(Redirect::to("/parametres/taches?err=vide"));
232+ return Ok(Redirect::to("/parametres/categories?err=vide"));
233233 }
234- match tasks::rename(&state.pool, id, ctx.team_id, name).await {
234+ match categories::rename(&state.pool, id, ctx.team_id, name).await {
235235 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")),
238238 Err(e) => Err(e.into()),
239239 }
240240 }
@@ -242,11 +242,11 @@ pub async fn task_rename(
242242 #[derive(Deserialize)]
243243 pub struct ToggleForm {
244244 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).
246246 restaure: Option<u8>,
247247 }
248248
249-pub async fn task_toggle_archive(
249+pub async fn category_toggle_archive(
250250 State(state): State<AppState>,
251251 ctx: ActiveTeam,
252252 Path(id): Path<Uuid>,
@@ -255,13 +255,13 @@ pub async fn task_toggle_archive(
255255 if !ctx.user.check_csrf(&state, &form.csrf) {
256256 return Err(AppError::Csrf);
257257 }
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 {
259259 return Err(AppError::NotFound);
260260 }
261261 Ok(Redirect::to(if form.restaure.is_some() {
262- "/parametres/taches?ok=restauree"
262+ "/parametres/categories?ok=restauree"
263263 } else {
264- "/parametres/taches?ok=archivee"
264+ "/parametres/categories?ok=archivee"
265265 }))
266266 }
267267
@@ -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 = list175+ 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;
77
88 use crate::charts::{ChartNav, ChartView, LegendItem, Sheet, build_chart, color_map, legend};
99 use crate::db::users::UserRow;
10-use crate::db::{stats, tasks, teams};
10+use crate::db::{stats, categories, teams};
1111 use crate::error::AppError;
1212 use crate::period::{Month, Week, Year};
1313 use crate::render::{HtmlTemplate, Shell};
@@ -60,7 +60,7 @@ pub async fn stats_page(
6060 let (month_from, month_to) = month.bounds();
6161 let (year_from, year_to) = year.bounds();
6262
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?;
6464 let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>());
6565
6666 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(
7070 let total_rows = stats::totals(&state.pool, ctx.team_id, None, None).await?;
7171
7272 // 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.
7474 let mut by_name: BTreeMap<String, Uuid> = teams::members(&state.pool, ctx.team_id)
7575 .await?
7676 .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 temps72 // 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 {
4040 csrf: String,
4141 }
4242
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.
4444 /// Si le timer (ou la mesure) vit dans une autre équipe que l'équipe active
4545 /// de l'appareil, on bascule dessus au passage : l'enregistrement ne peut se
4646 /// 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 active44 /// 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 se45 /// 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 @@
1010 </form>
1111 </div>
1212 {% 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>
1515 {% 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 %}
1919 <a href="{{ t.href }}">
2020 <span class="dot dot-14" style="background:{{ t.color }}"></span>
2121 <span class="tname">{{ t.name }}</span>
@@ -24,5 +24,5 @@
2424 {% endfor %}
2525 </div>
2626 {% 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>
2828 {% 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 @@
11 {% extends "app.html" %}
22 {% 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>
44 <div class="picked">
55 <span class="dot dot-16" style="background:{{ picked_color }}"></span>
66 <span class="pname">{{ picked_name }}</span>
@@ -8,7 +8,7 @@
88
99 <form method="post" action="/temps">
1010 <input type="hidden" name="csrf" value="{{ csrf }}">
11- <input type="hidden" name="tache" value="{{ tache }}">
11+ <input type="hidden" name="categorie" value="{{ categorie }}">
1212
1313 <label class="mini-label" for="minutes">Durée</label>
1414 <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 @@
11 {% extends "app.html" %}
22 {% 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">
55 <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">
77 <button type="submit" class="btn btn-primary">Ajouter</button>
88 </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 %}">
1111 <span class="dot dot-12" style="background:{{ t.color }}"></span>
1212 <form method="post" action="{{ t.rename_action }}" class="rename">
1313 <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)">
1515 {# tabindex=-1 : la soumission clavier passe par Entrée dans l'input ;
1616 sans ça, ce bouton invisible est un stop de tabulation sans focus visible. #}
1717 <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 @@
44 <form method="post" action="{{ action }}">
55 <input type="hidden" name="csrf" value="{{ csrf }}">
66 <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 %}
1010 <option value="{{ t.id }}"{% if t.selected %} selected{% endif %}>{{ t.name }}</option>
1111 {% endfor %}
1212 </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 @@
66 <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>
77 </div>
88 <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>
1010 <form method="post" action="/login">
1111 <div class="field" style="margin-bottom:var(--space-3)">
1212 <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 @@
5353 {% endfor %}
5454
5555 {% 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 }}">
5757 <a class="sheet-backdrop" href="#fermer" aria-label="Fermer"></a>
5858 <div class="sheet">
5959 <div class="sheet-head">
6060 <span class="dot dot-14" style="background:{{ s.color }}"></span>
61- <span class="pname">{{ s.task }}</span>
61+ <span class="pname">{{ s.category }}</span>
6262 <a class="btn btn-ghost close" href="#fermer">Fermer</a>
6363 </div>
6464 <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 @@
55 <p class="chrono-time" data-started="{{ started_ms }}">{{ live_label }}</p>
66 <p class="chrono-start">Démarré à {{ start_clock }}</p>
77 </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>
99 <div style="display:flex;flex-direction:column;gap:var(--space-3)">
1010 <form method="post" action="/timer/arreter" style="margin:0;display:flex">
1111 <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 @@
88 <li>
99 <span class="dot dot-12" style="background:{{ e.color }}"></span>
1010 <div class="tinfo">
11- <div class="ttask">{{ e.task }}</div>
11+ <div class="tcat">{{ e.category }}</div>
1212 <div class="tdate">{{ e.date }}</div>
1313 </div>
1414 <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>