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

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

Download ZIP

feat(taches): priorité basse/normale/haute des tâchesUnverified

Enum en table (0014), radios-pills Normale pré-cochée, tri des blocs
actifs par priorité puis date d'activation (À venir reste chronologique),
liseré rouge brique + symbole ‼ pour les hautes (blanc en HC).
Julien Durillon committed 2026-08-25T00:35:53+02:00 Browse files
e722076 parent: bb2bb6b
modified assets/style.css +14 -0
@@ -49,6 +49,10 @@
4949 --shadow-sm: 0 1px 2px color-mix(in srgb, #2e2b25 14%, transparent);
5050 --shadow-md: 0 3px 10px color-mix(in srgb, #2e2b25 16%, transparent);
5151 --shadow-lg: 0 12px 32px color-mix(in srgb, #2e2b25 22%, transparent);
52+
53+ /* Priorité haute des tâches : rouge brique dans la famille terreuse de la
54+ palette (4.98:1 sur la surface — liseré ET symbole passent l'AA). */
55+ --color-prio: #a03b28;
5256 }
5357
5458 /* ═══ Contraste élevé (users.theme = 'hc', data-theme sur <html>) ═══
@@ -86,6 +90,9 @@
8690 --shadow-sm: 0 0 0 1px rgba(255, 255, 255, .28);
8791 --shadow-md: 0 0 0 1px rgba(255, 255, 255, .34);
8892 --shadow-lg: 0 0 0 1px rgba(255, 255, 255, .42);
93+
94+ /* Pas de rouge en HC : liseré épais neutre, le symbole ‼ porte l'info. */
95+ --color-prio: #ffffff;
8996 }
9097 /* Le toast s'inverse : son fond neutral-900 devient noir en HC. */
9198 :root[data-theme="hc"] .toast { background: #ffffff; color: #000000; }
@@ -698,6 +705,13 @@ h4.add_step_title { margin-top: var(--space-3); margin-bottom: var(--space-3); d
698705 .todo-card { background: var(--color-surface); border-radius: var(--radius-lg); padding: var(--space-4); }
699706 .todo-card.other { background: var(--color-neutral-200); }
700707 .todo-card.forme { box-shadow: inset 0 0 0 1.5px var(--color-accent-2); }
708+/* Priorité haute : liseré + ombre teintée (après .forme : il prime si les
709+ deux s'appliquent). Le symbole ‼ (aria-label) double la couleur. */
710+.todo-card.prio-haute {
711+ box-shadow: inset 0 0 0 2px var(--color-prio),
712+ 0 3px 10px color-mix(in srgb, var(--color-prio) 30%, transparent);
713+}
714+.prio-flag { color: var(--color-prio); font-weight: 700; }
701715 .todo-main { display: flex; align-items: center; gap: var(--space-3); }
702716 .todo-name { font-size: 16px; }
703717 .todo-meta { font-size: 13px; opacity: .7; }
@@ -49,6 +49,10 @@
49 --shadow-sm: 0 1px 2px color-mix(in srgb, #2e2b25 14%, transparent);49 --shadow-sm: 0 1px 2px color-mix(in srgb, #2e2b25 14%, transparent);
50 --shadow-md: 0 3px 10px color-mix(in srgb, #2e2b25 16%, transparent);50 --shadow-md: 0 3px 10px color-mix(in srgb, #2e2b25 16%, transparent);
51 --shadow-lg: 0 12px 32px color-mix(in srgb, #2e2b25 22%, transparent);51 --shadow-lg: 0 12px 32px color-mix(in srgb, #2e2b25 22%, transparent);
52+
53+ /* Priorité haute des tâches : rouge brique dans la famille terreuse de la
54+ palette (4.98:1 sur la surface — liseré ET symbole passent l'AA). */
55+ --color-prio: #a03b28;
52 }56 }
53 57
54 /* ═══ Contraste élevé (users.theme = 'hc', data-theme sur <html>) ═══58 /* ═══ Contraste élevé (users.theme = 'hc', data-theme sur <html>) ═══
@@ -86,6 +90,9 @@
86 --shadow-sm: 0 0 0 1px rgba(255, 255, 255, .28);90 --shadow-sm: 0 0 0 1px rgba(255, 255, 255, .28);
87 --shadow-md: 0 0 0 1px rgba(255, 255, 255, .34);91 --shadow-md: 0 0 0 1px rgba(255, 255, 255, .34);
88 --shadow-lg: 0 0 0 1px rgba(255, 255, 255, .42);92 --shadow-lg: 0 0 0 1px rgba(255, 255, 255, .42);
93+
94+ /* Pas de rouge en HC : liseré épais neutre, le symbole ‼ porte l'info. */
95+ --color-prio: #ffffff;
89 }96 }
90 /* Le toast s'inverse : son fond neutral-900 devient noir en HC. */97 /* Le toast s'inverse : son fond neutral-900 devient noir en HC. */
91 :root[data-theme="hc"] .toast { background: #ffffff; color: #000000; }98 :root[data-theme="hc"] .toast { background: #ffffff; color: #000000; }
@@ -698,6 +705,13 @@ h4.add_step_title { margin-top: var(--space-3); margin-bottom: var(--space-3); d
698 .todo-card { background: var(--color-surface); border-radius: var(--radius-lg); padding: var(--space-4); }705 .todo-card { background: var(--color-surface); border-radius: var(--radius-lg); padding: var(--space-4); }
699 .todo-card.other { background: var(--color-neutral-200); }706 .todo-card.other { background: var(--color-neutral-200); }
700 .todo-card.forme { box-shadow: inset 0 0 0 1.5px var(--color-accent-2); }707 .todo-card.forme { box-shadow: inset 0 0 0 1.5px var(--color-accent-2); }
708+/* Priorité haute : liseré + ombre teintée (après .forme : il prime si les
709+ deux s'appliquent). Le symbole ‼ (aria-label) double la couleur. */
710+.todo-card.prio-haute {
711+ box-shadow: inset 0 0 0 2px var(--color-prio),
712+ 0 3px 10px color-mix(in srgb, var(--color-prio) 30%, transparent);
713+}
714+.prio-flag { color: var(--color-prio); font-weight: 700; }
701 .todo-main { display: flex; align-items: center; gap: var(--space-3); }715 .todo-main { display: flex; align-items: center; gap: var(--space-3); }
702 .todo-name { font-size: 16px; }716 .todo-name { font-size: 16px; }
703 .todo-meta { font-size: 13px; opacity: .7; }717 .todo-meta { font-size: 13px; opacity: .7; }
added migrations/0014_todo_priority.sql +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+-- Priorité des tâches : Basse / Normale (défaut) / Haute. Attribut durable
2+-- de la ligne (conservé à la reprogrammation), modifiable dans l'édition.
3+-- Domaine fermé → table enum (cf. 0013), pas de CHECK.
4+CREATE TABLE exego_enums.enum_todo_priority (
5+ code TEXT PRIMARY KEY,
6+ sort_order SMALLINT NOT NULL,
7+ description TEXT NOT NULL
8+);
9+INSERT INTO exego_enums.enum_todo_priority (code, sort_order, description) VALUES
10+ ('basse', 1, 'en bas de liste, aucun marqueur'),
11+ ('normale', 2, 'priorité par défaut'),
12+ ('haute', 3, 'en haut de liste, liseré rouge');
13+
14+ALTER TABLE todos ADD COLUMN priority TEXT NOT NULL DEFAULT 'normale'
15+ REFERENCES exego_enums.enum_todo_priority(code);
new file mode 100644
@@ -0,0 +1,15 @@
1+-- Priorité des tâches : Basse / Normale (défaut) / Haute. Attribut durable
2+-- de la ligne (conservé à la reprogrammation), modifiable dans l'édition.
3+-- Domaine fermé → table enum (cf. 0013), pas de CHECK.
4+CREATE TABLE exego_enums.enum_todo_priority (
5+ code TEXT PRIMARY KEY,
6+ sort_order SMALLINT NOT NULL,
7+ description TEXT NOT NULL
8+);
9+INSERT INTO exego_enums.enum_todo_priority (code, sort_order, description) VALUES
10+ ('basse', 1, 'en bas de liste, aucun marqueur'),
11+ ('normale', 2, 'priorité par défaut'),
12+ ('haute', 3, 'en haut de liste, liseré rouge');
13+
14+ALTER TABLE todos ADD COLUMN priority TEXT NOT NULL DEFAULT 'normale'
15+ REFERENCES exego_enums.enum_todo_priority(code);
modified src/db/todos.rs +26 -7
@@ -8,6 +8,20 @@ pub const STATE_IN_PROGRESS: i16 = 1;
88 pub const STATE_TODO: i16 = 2;
99 pub const STATE_DONE: i16 = 3;
1010
11+/// Priorités — mêmes codes que `exego_enums.enum_todo_priority`.
12+pub const PRIO_LOW: &str = "basse";
13+pub const PRIO_NORMAL: &str = "normale";
14+pub const PRIO_HIGH: &str = "haute";
15+
16+/// Rang de tri (haute d'abord) — un code inconnu vaut normale.
17+pub fn prio_rank(p: &str) -> u8 {
18+ match p {
19+ PRIO_HIGH => 0,
20+ PRIO_LOW => 2,
21+ _ => 1,
22+ }
23+}
24+
1125 #[derive(sqlx::FromRow)]
1226 pub struct TodoRow {
1327 pub id: Uuid,
@@ -15,6 +29,7 @@ pub struct TodoRow {
1529 pub category_id: Uuid,
1630 pub activation_date: NaiveDate,
1731 pub state_id: i16,
32+ pub priority: String,
1833 pub created_at: DateTime<Utc>,
1934 /// Preneur si « en cours » (dernier passage à in_progress).
2035 pub taker_id: Option<Uuid>,
@@ -41,7 +56,7 @@ const GUARD: &str = "t.id = $1
4156 /// Toutes les todos non faites de l'équipe (les trois blocs se filtrent en Rust).
4257 pub async fn list_open(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec<TodoRow>> {
4358 sqlx::query_as(
44- "SELECT t.id, t.name, t.category_id, t.activation_date, t.state_id, t.created_at,
59+ "SELECT t.id, t.name, t.category_id, t.activation_date, t.state_id, t.priority, t.created_at,
4560 tk.user_id AS taker_id, tk.username AS taker_name, tk.changed_at AS taken_at
4661 FROM todos t
4762 JOIN categories c ON c.id = t.category_id
@@ -98,13 +113,14 @@ pub async fn create(
98113 category_id: Uuid,
99114 name: &str,
100115 activation: NaiveDate,
116+ priority: &str,
101117 assignees: &[Uuid],
102118 ) -> sqlx::Result<u64> {
103119 let mut tx = pool.begin().await?;
104120 let id = Uuid::now_v7();
105121 let created = sqlx::query(
106- "INSERT INTO todos (id, category_id, name, activation_date, state_id)
107- SELECT $1, $2, $3, $4, 2
122+ "INSERT INTO todos (id, category_id, name, activation_date, state_id, priority)
123+ SELECT $1, $2, $3, $4, 2, $6
108124 WHERE EXISTS (SELECT 1 FROM categories WHERE id = $2 AND team_id = $5)",
109125 )
110126 .bind(id)
@@ -112,6 +128,7 @@ pub async fn create(
112128 .bind(name)
113129 .bind(activation)
114130 .bind(team_id)
131+ .bind(priority)
115132 .execute(&mut *tx)
116133 .await?
117134 .rows_affected();
@@ -132,7 +149,7 @@ pub async fn get_editable(
132149 id: Uuid,
133150 ) -> sqlx::Result<Option<TodoRow>> {
134151 sqlx::query_as(&format!(
135- "SELECT t.id, t.name, t.category_id, t.activation_date, t.state_id, t.created_at,
152+ "SELECT t.id, t.name, t.category_id, t.activation_date, t.state_id, t.priority, t.created_at,
136153 NULL::uuid AS taker_id, NULL::text AS taker_name, NULL::timestamptz AS taken_at
137154 FROM todos t WHERE {GUARD} AND t.state_id <> 3"
138155 ))
@@ -151,7 +168,7 @@ pub async fn assignees_of(pool: &PgPool, id: Uuid) -> sqlx::Result<Vec<Uuid>> {
151168 .await
152169 }
153170
154-/// Édition (nom, catégorie de la même équipe, date, assignés).
171+/// Édition (nom, catégorie de la même équipe, date, priorité, assignés).
155172 pub async fn update(
156173 pool: &PgPool,
157174 team_id: Uuid,
@@ -160,11 +177,12 @@ pub async fn update(
160177 category_id: Uuid,
161178 name: &str,
162179 activation: NaiveDate,
180+ priority: &str,
163181 assignees: &[Uuid],
164182 ) -> sqlx::Result<u64> {
165183 let mut tx = pool.begin().await?;
166184 let n = sqlx::query(&format!(
167- "UPDATE todos t SET category_id = $4, name = $5, activation_date = $6
185+ "UPDATE todos t SET category_id = $4, name = $5, activation_date = $6, priority = $7
168186 WHERE {GUARD} AND t.state_id <> 3
169187 AND EXISTS (SELECT 1 FROM categories WHERE id = $4 AND team_id = $2)"
170188 ))
@@ -174,6 +192,7 @@ pub async fn update(
174192 .bind(category_id)
175193 .bind(name)
176194 .bind(activation)
195+ .bind(priority)
177196 .execute(&mut *tx)
178197 .await?
179198 .rows_affected();
@@ -250,7 +269,7 @@ pub async fn get_finishable(
250269 id: Uuid,
251270 ) -> sqlx::Result<Option<TodoRow>> {
252271 sqlx::query_as(&format!(
253- "SELECT t.id, t.name, t.category_id, t.activation_date, t.state_id, t.created_at,
272+ "SELECT t.id, t.name, t.category_id, t.activation_date, t.state_id, t.priority, t.created_at,
254273 NULL::uuid AS taker_id, NULL::text AS taker_name, NULL::timestamptz AS taken_at
255274 FROM todos t WHERE {GUARD} AND t.state_id IN (1, 2)"
256275 ))
@@ -8,6 +8,20 @@ pub const STATE_IN_PROGRESS: i16 = 1;
8 pub const STATE_TODO: i16 = 2;8 pub const STATE_TODO: i16 = 2;
9 pub const STATE_DONE: i16 = 3;9 pub const STATE_DONE: i16 = 3;
10 10
11+/// Priorités — mêmes codes que `exego_enums.enum_todo_priority`.
12+pub const PRIO_LOW: &str = "basse";
13+pub const PRIO_NORMAL: &str = "normale";
14+pub const PRIO_HIGH: &str = "haute";
15+
16+/// Rang de tri (haute d'abord) — un code inconnu vaut normale.
17+pub fn prio_rank(p: &str) -> u8 {
18+ match p {
19+ PRIO_HIGH => 0,
20+ PRIO_LOW => 2,
21+ _ => 1,
22+ }
23+}
24+
11 #[derive(sqlx::FromRow)]25 #[derive(sqlx::FromRow)]
12 pub struct TodoRow {26 pub struct TodoRow {
13 pub id: Uuid,27 pub id: Uuid,
@@ -15,6 +29,7 @@ pub struct TodoRow {
15 pub category_id: Uuid,29 pub category_id: Uuid,
16 pub activation_date: NaiveDate,30 pub activation_date: NaiveDate,
17 pub state_id: i16,31 pub state_id: i16,
32+ pub priority: String,
18 pub created_at: DateTime<Utc>,33 pub created_at: DateTime<Utc>,
19 /// Preneur si « en cours » (dernier passage à in_progress).34 /// Preneur si « en cours » (dernier passage à in_progress).
20 pub taker_id: Option<Uuid>,35 pub taker_id: Option<Uuid>,
@@ -41,7 +56,7 @@ const GUARD: &str = "t.id = $1
41 /// Toutes les todos non faites de l'équipe (les trois blocs se filtrent en Rust).56 /// Toutes les todos non faites de l'équipe (les trois blocs se filtrent en Rust).
42 pub async fn list_open(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec<TodoRow>> {57 pub async fn list_open(pool: &PgPool, team_id: Uuid) -> sqlx::Result<Vec<TodoRow>> {
43 sqlx::query_as(58 sqlx::query_as(
44- "SELECT t.id, t.name, t.category_id, t.activation_date, t.state_id, t.created_at,59+ "SELECT t.id, t.name, t.category_id, t.activation_date, t.state_id, t.priority, t.created_at,
45 tk.user_id AS taker_id, tk.username AS taker_name, tk.changed_at AS taken_at60 tk.user_id AS taker_id, tk.username AS taker_name, tk.changed_at AS taken_at
46 FROM todos t61 FROM todos t
47 JOIN categories c ON c.id = t.category_id62 JOIN categories c ON c.id = t.category_id
@@ -98,13 +113,14 @@ pub async fn create(
98 category_id: Uuid,113 category_id: Uuid,
99 name: &str,114 name: &str,
100 activation: NaiveDate,115 activation: NaiveDate,
116+ priority: &str,
101 assignees: &[Uuid],117 assignees: &[Uuid],
102 ) -> sqlx::Result<u64> {118 ) -> sqlx::Result<u64> {
103 let mut tx = pool.begin().await?;119 let mut tx = pool.begin().await?;
104 let id = Uuid::now_v7();120 let id = Uuid::now_v7();
105 let created = sqlx::query(121 let created = sqlx::query(
106- "INSERT INTO todos (id, category_id, name, activation_date, state_id)122+ "INSERT INTO todos (id, category_id, name, activation_date, state_id, priority)
107- SELECT $1, $2, $3, $4, 2123+ SELECT $1, $2, $3, $4, 2, $6
108 WHERE EXISTS (SELECT 1 FROM categories WHERE id = $2 AND team_id = $5)",124 WHERE EXISTS (SELECT 1 FROM categories WHERE id = $2 AND team_id = $5)",
109 )125 )
110 .bind(id)126 .bind(id)
@@ -112,6 +128,7 @@ pub async fn create(
112 .bind(name)128 .bind(name)
113 .bind(activation)129 .bind(activation)
114 .bind(team_id)130 .bind(team_id)
131+ .bind(priority)
115 .execute(&mut *tx)132 .execute(&mut *tx)
116 .await?133 .await?
117 .rows_affected();134 .rows_affected();
@@ -132,7 +149,7 @@ pub async fn get_editable(
132 id: Uuid,149 id: Uuid,
133 ) -> sqlx::Result<Option<TodoRow>> {150 ) -> sqlx::Result<Option<TodoRow>> {
134 sqlx::query_as(&format!(151 sqlx::query_as(&format!(
135- "SELECT t.id, t.name, t.category_id, t.activation_date, t.state_id, t.created_at,152+ "SELECT t.id, t.name, t.category_id, t.activation_date, t.state_id, t.priority, t.created_at,
136 NULL::uuid AS taker_id, NULL::text AS taker_name, NULL::timestamptz AS taken_at153 NULL::uuid AS taker_id, NULL::text AS taker_name, NULL::timestamptz AS taken_at
137 FROM todos t WHERE {GUARD} AND t.state_id <> 3"154 FROM todos t WHERE {GUARD} AND t.state_id <> 3"
138 ))155 ))
@@ -151,7 +168,7 @@ pub async fn assignees_of(pool: &PgPool, id: Uuid) -> sqlx::Result<Vec<Uuid>> {
151 .await168 .await
152 }169 }
153 170
154-/// Édition (nom, catégorie de la même équipe, date, assignés).171+/// Édition (nom, catégorie de la même équipe, date, priorité, assignés).
155 pub async fn update(172 pub async fn update(
156 pool: &PgPool,173 pool: &PgPool,
157 team_id: Uuid,174 team_id: Uuid,
@@ -160,11 +177,12 @@ pub async fn update(
160 category_id: Uuid,177 category_id: Uuid,
161 name: &str,178 name: &str,
162 activation: NaiveDate,179 activation: NaiveDate,
180+ priority: &str,
163 assignees: &[Uuid],181 assignees: &[Uuid],
164 ) -> sqlx::Result<u64> {182 ) -> sqlx::Result<u64> {
165 let mut tx = pool.begin().await?;183 let mut tx = pool.begin().await?;
166 let n = sqlx::query(&format!(184 let n = sqlx::query(&format!(
167- "UPDATE todos t SET category_id = $4, name = $5, activation_date = $6185+ "UPDATE todos t SET category_id = $4, name = $5, activation_date = $6, priority = $7
168 WHERE {GUARD} AND t.state_id <> 3186 WHERE {GUARD} AND t.state_id <> 3
169 AND EXISTS (SELECT 1 FROM categories WHERE id = $4 AND team_id = $2)"187 AND EXISTS (SELECT 1 FROM categories WHERE id = $4 AND team_id = $2)"
170 ))188 ))
@@ -174,6 +192,7 @@ pub async fn update(
174 .bind(category_id)192 .bind(category_id)
175 .bind(name)193 .bind(name)
176 .bind(activation)194 .bind(activation)
195+ .bind(priority)
177 .execute(&mut *tx)196 .execute(&mut *tx)
178 .await?197 .await?
179 .rows_affected();198 .rows_affected();
@@ -250,7 +269,7 @@ pub async fn get_finishable(
250 id: Uuid,269 id: Uuid,
251 ) -> sqlx::Result<Option<TodoRow>> {270 ) -> sqlx::Result<Option<TodoRow>> {
252 sqlx::query_as(&format!(271 sqlx::query_as(&format!(
253- "SELECT t.id, t.name, t.category_id, t.activation_date, t.state_id, t.created_at,272+ "SELECT t.id, t.name, t.category_id, t.activation_date, t.state_id, t.priority, t.created_at,
254 NULL::uuid AS taker_id, NULL::text AS taker_name, NULL::timestamptz AS taken_at273 NULL::uuid AS taker_id, NULL::text AS taker_name, NULL::timestamptz AS taken_at
255 FROM todos t WHERE {GUARD} AND t.state_id IN (1, 2)"274 FROM todos t WHERE {GUARD} AND t.state_id IN (1, 2)"
256 ))275 ))
modified src/i18n.rs +43 -0
@@ -252,6 +252,11 @@ pub struct Tr {
252252 pub change_password: &'static str,
253253 pub sign_out: &'static str,
254254 pub signout_note: &'static str,
255+ /// Carte Compte : accroche + lien vers la création d'une issue GitLab.
256+ pub feedback_title: &'static str,
257+ pub feedback_link: &'static str,
258+ /// Description préremplie de l'issue — p2 : version puis commit.
259+ pub issue_body_pat: &'static str,
255260 pub toast_lang_updated: &'static str,
256261 pub toast_password_updated: &'static str,
257262 pub toast_invite_sent: &'static str,
@@ -342,6 +347,12 @@ pub struct Tr {
342347 pub toast_todo_released: &'static str,
343348 pub toast_todo_done: &'static str,
344349 pub toast_todo_done_reprog: &'static str,
350+ /// Priorité (Basse / Normale / Haute) — radios du formulaire + marqueur.
351+ pub prio_label: &'static str,
352+ pub prio_low: &'static str,
353+ pub prio_normal: &'static str,
354+ pub prio_high: &'static str,
355+ pub prio_high_aria: &'static str,
345356 // ─── erreurs ───
346357 pub err_404: &'static str,
347358 pub err_csrf: &'static str,
@@ -551,6 +562,9 @@ pub static FR: Tr = Tr {
551562 change_password: "Changer le mot de passe",
552563 sign_out: "Se déconnecter",
553564 signout_note: "Le biscuit de session sera révoqué.",
565+ feedback_title: "Un problème ? Une idée ?",
566+ feedback_link: "Viens m'en parler ici",
567+ issue_body_pat: "<!-- Décris ton problème ou ton idée -->\n\n---\nVersion : {} — commit {}",
554568 toast_lang_updated: "Langue mise à jour",
555569 toast_password_updated: "Mot de passe mis à jour",
556570 toast_invite_sent: "Invitation envoyée",
@@ -636,6 +650,11 @@ pub static FR: Tr = Tr {
636650 toast_todo_released: "Tâche relâchée",
637651 toast_todo_done: "Tâche faite ✓",
638652 toast_todo_done_reprog: "Faite et reprogrammée ✓",
653+ prio_label: "Priorité",
654+ prio_low: "Basse",
655+ prio_normal: "Normale",
656+ prio_high: "Haute",
657+ prio_high_aria: "Priorité haute",
639658 err_404: "Page introuvable.",
640659 err_csrf: "Formulaire expiré, veuillez réessayer.",
641660 err_500: "Erreur interne.",
@@ -816,6 +835,9 @@ pub static EN: Tr = Tr {
816835 change_password: "Change password",
817836 sign_out: "Sign out",
818837 signout_note: "The session biscuit will be revoked.",
838+ feedback_title: "A problem? An idea?",
839+ feedback_link: "Come tell me about it here",
840+ issue_body_pat: "<!-- Describe your problem or your idea -->\n\n---\nVersion: {} — commit {}",
819841 toast_lang_updated: "Language updated",
820842 toast_password_updated: "Password updated",
821843 toast_invite_sent: "Invitation sent",
@@ -901,6 +923,11 @@ pub static EN: Tr = Tr {
901923 toast_todo_released: "Task released",
902924 toast_todo_done: "Task done ✓",
903925 toast_todo_done_reprog: "Done and rescheduled ✓",
926+ prio_label: "Priority",
927+ prio_low: "Low",
928+ prio_normal: "Normal",
929+ prio_high: "High",
930+ prio_high_aria: "High priority",
904931 err_404: "Page not found.",
905932 err_csrf: "The form expired, please try again.",
906933 err_500: "Internal error.",
@@ -1081,6 +1108,9 @@ pub static DE: Tr = Tr {
10811108 change_password: "Passwort ändern",
10821109 sign_out: "Abmelden",
10831110 signout_note: "Das Sitzungs-Biscuit wird widerrufen.",
1111+ feedback_title: "Ein Problem? Eine Idee?",
1112+ feedback_link: "Erzähl mir hier davon",
1113+ issue_body_pat: "<!-- Beschreib dein Problem oder deine Idee -->\n\n---\nVersion: {} — Commit {}",
10841114 toast_lang_updated: "Sprache aktualisiert",
10851115 toast_password_updated: "Passwort aktualisiert",
10861116 toast_invite_sent: "Einladung gesendet",
@@ -1166,6 +1196,11 @@ pub static DE: Tr = Tr {
11661196 toast_todo_released: "Aufgabe freigegeben",
11671197 toast_todo_done: "Aufgabe erledigt ✓",
11681198 toast_todo_done_reprog: "Erledigt und neu geplant ✓",
1199+ prio_label: "Priorität",
1200+ prio_low: "Niedrig",
1201+ prio_normal: "Normal",
1202+ prio_high: "Hoch",
1203+ prio_high_aria: "Hohe Priorität",
11691204 err_404: "Seite nicht gefunden.",
11701205 err_csrf: "Das Formular ist abgelaufen, bitte erneut versuchen.",
11711206 err_500: "Interner Fehler.",
@@ -1346,6 +1381,9 @@ pub static PT_BR: Tr = Tr {
13461381 change_password: "Alterar a senha",
13471382 sign_out: "Sair da conta",
13481383 signout_note: "O biscuit da sessão será revogado.",
1384+ feedback_title: "Um problema? Uma ideia?",
1385+ feedback_link: "Vem me contar aqui",
1386+ issue_body_pat: "<!-- Descreva seu problema ou sua ideia -->\n\n---\nVersão: {} — commit {}",
13491387 toast_lang_updated: "Idioma atualizado",
13501388 toast_password_updated: "Senha atualizada",
13511389 toast_invite_sent: "Convite enviado",
@@ -1431,6 +1469,11 @@ pub static PT_BR: Tr = Tr {
14311469 toast_todo_released: "Tarefa solta",
14321470 toast_todo_done: "Tarefa feita ✓",
14331471 toast_todo_done_reprog: "Feita e reprogramada ✓",
1472+ prio_label: "Prioridade",
1473+ prio_low: "Baixa",
1474+ prio_normal: "Normal",
1475+ prio_high: "Alta",
1476+ prio_high_aria: "Prioridade alta",
14341477 err_404: "Página não encontrada.",
14351478 err_csrf: "O formulário expirou, tente de novo.",
14361479 err_500: "Erro interno.",
@@ -252,6 +252,11 @@ pub struct Tr {
252 pub change_password: &'static str,252 pub change_password: &'static str,
253 pub sign_out: &'static str,253 pub sign_out: &'static str,
254 pub signout_note: &'static str,254 pub signout_note: &'static str,
255+ /// Carte Compte : accroche + lien vers la création d'une issue GitLab.
256+ pub feedback_title: &'static str,
257+ pub feedback_link: &'static str,
258+ /// Description préremplie de l'issue — p2 : version puis commit.
259+ pub issue_body_pat: &'static str,
255 pub toast_lang_updated: &'static str,260 pub toast_lang_updated: &'static str,
256 pub toast_password_updated: &'static str,261 pub toast_password_updated: &'static str,
257 pub toast_invite_sent: &'static str,262 pub toast_invite_sent: &'static str,
@@ -342,6 +347,12 @@ pub struct Tr {
342 pub toast_todo_released: &'static str,347 pub toast_todo_released: &'static str,
343 pub toast_todo_done: &'static str,348 pub toast_todo_done: &'static str,
344 pub toast_todo_done_reprog: &'static str,349 pub toast_todo_done_reprog: &'static str,
350+ /// Priorité (Basse / Normale / Haute) — radios du formulaire + marqueur.
351+ pub prio_label: &'static str,
352+ pub prio_low: &'static str,
353+ pub prio_normal: &'static str,
354+ pub prio_high: &'static str,
355+ pub prio_high_aria: &'static str,
345 // ─── erreurs ───356 // ─── erreurs ───
346 pub err_404: &'static str,357 pub err_404: &'static str,
347 pub err_csrf: &'static str,358 pub err_csrf: &'static str,
@@ -551,6 +562,9 @@ pub static FR: Tr = Tr {
551 change_password: "Changer le mot de passe",562 change_password: "Changer le mot de passe",
552 sign_out: "Se déconnecter",563 sign_out: "Se déconnecter",
553 signout_note: "Le biscuit de session sera révoqué.",564 signout_note: "Le biscuit de session sera révoqué.",
565+ feedback_title: "Un problème ? Une idée ?",
566+ feedback_link: "Viens m'en parler ici",
567+ issue_body_pat: "<!-- Décris ton problème ou ton idée -->\n\n---\nVersion : {} — commit {}",
554 toast_lang_updated: "Langue mise à jour",568 toast_lang_updated: "Langue mise à jour",
555 toast_password_updated: "Mot de passe mis à jour",569 toast_password_updated: "Mot de passe mis à jour",
556 toast_invite_sent: "Invitation envoyée",570 toast_invite_sent: "Invitation envoyée",
@@ -636,6 +650,11 @@ pub static FR: Tr = Tr {
636 toast_todo_released: "Tâche relâchée",650 toast_todo_released: "Tâche relâchée",
637 toast_todo_done: "Tâche faite ✓",651 toast_todo_done: "Tâche faite ✓",
638 toast_todo_done_reprog: "Faite et reprogrammée ✓",652 toast_todo_done_reprog: "Faite et reprogrammée ✓",
653+ prio_label: "Priorité",
654+ prio_low: "Basse",
655+ prio_normal: "Normale",
656+ prio_high: "Haute",
657+ prio_high_aria: "Priorité haute",
639 err_404: "Page introuvable.",658 err_404: "Page introuvable.",
640 err_csrf: "Formulaire expiré, veuillez réessayer.",659 err_csrf: "Formulaire expiré, veuillez réessayer.",
641 err_500: "Erreur interne.",660 err_500: "Erreur interne.",
@@ -816,6 +835,9 @@ pub static EN: Tr = Tr {
816 change_password: "Change password",835 change_password: "Change password",
817 sign_out: "Sign out",836 sign_out: "Sign out",
818 signout_note: "The session biscuit will be revoked.",837 signout_note: "The session biscuit will be revoked.",
838+ feedback_title: "A problem? An idea?",
839+ feedback_link: "Come tell me about it here",
840+ issue_body_pat: "<!-- Describe your problem or your idea -->\n\n---\nVersion: {} — commit {}",
819 toast_lang_updated: "Language updated",841 toast_lang_updated: "Language updated",
820 toast_password_updated: "Password updated",842 toast_password_updated: "Password updated",
821 toast_invite_sent: "Invitation sent",843 toast_invite_sent: "Invitation sent",
@@ -901,6 +923,11 @@ pub static EN: Tr = Tr {
901 toast_todo_released: "Task released",923 toast_todo_released: "Task released",
902 toast_todo_done: "Task done ✓",924 toast_todo_done: "Task done ✓",
903 toast_todo_done_reprog: "Done and rescheduled ✓",925 toast_todo_done_reprog: "Done and rescheduled ✓",
926+ prio_label: "Priority",
927+ prio_low: "Low",
928+ prio_normal: "Normal",
929+ prio_high: "High",
930+ prio_high_aria: "High priority",
904 err_404: "Page not found.",931 err_404: "Page not found.",
905 err_csrf: "The form expired, please try again.",932 err_csrf: "The form expired, please try again.",
906 err_500: "Internal error.",933 err_500: "Internal error.",
@@ -1081,6 +1108,9 @@ pub static DE: Tr = Tr {
1081 change_password: "Passwort ändern",1108 change_password: "Passwort ändern",
1082 sign_out: "Abmelden",1109 sign_out: "Abmelden",
1083 signout_note: "Das Sitzungs-Biscuit wird widerrufen.",1110 signout_note: "Das Sitzungs-Biscuit wird widerrufen.",
1111+ feedback_title: "Ein Problem? Eine Idee?",
1112+ feedback_link: "Erzähl mir hier davon",
1113+ issue_body_pat: "<!-- Beschreib dein Problem oder deine Idee -->\n\n---\nVersion: {} — Commit {}",
1084 toast_lang_updated: "Sprache aktualisiert",1114 toast_lang_updated: "Sprache aktualisiert",
1085 toast_password_updated: "Passwort aktualisiert",1115 toast_password_updated: "Passwort aktualisiert",
1086 toast_invite_sent: "Einladung gesendet",1116 toast_invite_sent: "Einladung gesendet",
@@ -1166,6 +1196,11 @@ pub static DE: Tr = Tr {
1166 toast_todo_released: "Aufgabe freigegeben",1196 toast_todo_released: "Aufgabe freigegeben",
1167 toast_todo_done: "Aufgabe erledigt ✓",1197 toast_todo_done: "Aufgabe erledigt ✓",
1168 toast_todo_done_reprog: "Erledigt und neu geplant ✓",1198 toast_todo_done_reprog: "Erledigt und neu geplant ✓",
1199+ prio_label: "Priorität",
1200+ prio_low: "Niedrig",
1201+ prio_normal: "Normal",
1202+ prio_high: "Hoch",
1203+ prio_high_aria: "Hohe Priorität",
1169 err_404: "Seite nicht gefunden.",1204 err_404: "Seite nicht gefunden.",
1170 err_csrf: "Das Formular ist abgelaufen, bitte erneut versuchen.",1205 err_csrf: "Das Formular ist abgelaufen, bitte erneut versuchen.",
1171 err_500: "Interner Fehler.",1206 err_500: "Interner Fehler.",
@@ -1346,6 +1381,9 @@ pub static PT_BR: Tr = Tr {
1346 change_password: "Alterar a senha",1381 change_password: "Alterar a senha",
1347 sign_out: "Sair da conta",1382 sign_out: "Sair da conta",
1348 signout_note: "O biscuit da sessão será revogado.",1383 signout_note: "O biscuit da sessão será revogado.",
1384+ feedback_title: "Um problema? Uma ideia?",
1385+ feedback_link: "Vem me contar aqui",
1386+ issue_body_pat: "<!-- Descreva seu problema ou sua ideia -->\n\n---\nVersão: {} — commit {}",
1349 toast_lang_updated: "Idioma atualizado",1387 toast_lang_updated: "Idioma atualizado",
1350 toast_password_updated: "Senha atualizada",1388 toast_password_updated: "Senha atualizada",
1351 toast_invite_sent: "Convite enviado",1389 toast_invite_sent: "Convite enviado",
@@ -1431,6 +1469,11 @@ pub static PT_BR: Tr = Tr {
1431 toast_todo_released: "Tarefa solta",1469 toast_todo_released: "Tarefa solta",
1432 toast_todo_done: "Tarefa feita ✓",1470 toast_todo_done: "Tarefa feita ✓",
1433 toast_todo_done_reprog: "Feita e reprogramada ✓",1471 toast_todo_done_reprog: "Feita e reprogramada ✓",
1472+ prio_label: "Prioridade",
1473+ prio_low: "Baixa",
1474+ prio_normal: "Normal",
1475+ prio_high: "Alta",
1476+ prio_high_aria: "Prioridade alta",
1434 err_404: "Página não encontrada.",1477 err_404: "Página não encontrada.",
1435 err_csrf: "O formulário expirou, tente de novo.",1478 err_csrf: "O formulário expirou, tente de novo.",
1436 err_500: "Erro interno.",1479 err_500: "Erro interno.",
modified src/routes/todos.rs +38 -3
@@ -12,7 +12,7 @@ use uuid::Uuid;
1212 use crate::charts::{color_map, fmt_minutes};
1313 use crate::db::categories;
1414 use crate::db::teams;
15-use crate::db::todos::{self, STATE_IN_PROGRESS, STATE_TODO, TodoRow};
15+use crate::db::todos::{self, PRIO_HIGH, PRIO_LOW, PRIO_NORMAL, STATE_IN_PROGRESS, STATE_TODO, TodoRow, prio_rank};
1616 use crate::error::AppError;
1717 use crate::i18n::{Tr, p1, p2};
1818 use crate::period::{DateVocab, day_label, day_month, paris_today};
@@ -49,6 +49,9 @@ pub struct TodoCard {
4949 pub assignees: Vec<AsgChip>,
5050 /// Je suis assigné → liseré sur la carte.
5151 pub for_me: bool,
52+ /// Priorité haute → liseré rouge + symbole (blocs actifs seulement,
53+ /// le bloc « À venir » reste neutre).
54+ pub high: bool,
5255 }
5356
5457 #[derive(Template)]
@@ -157,6 +160,7 @@ pub async fn todos_page(
157160 })
158161 .collect(),
159162 for_me,
163+ high: t.priority == PRIO_HIGH,
160164 }
161165 };
162166
@@ -183,6 +187,12 @@ pub async fn todos_page(
183187 // Les miennes en premier, puis les sans-assigné (stable).
184188 sel.sort_by_key(|t| !assigned_to_me(t));
185189 }
190+ if active {
191+ // Blocs actifs : la priorité prime sur tout (tri stable — dans un
192+ // même rang, l'ordre par date puis « à moi » est conservé).
193+ // « À venir » (active = false) reste chronologique pur.
194+ sel.sort_by_key(|t| prio_rank(&t.priority));
195+ }
186196 sel.into_iter().map(|t| card(t)).collect()
187197 };
188198
@@ -227,6 +237,13 @@ pub struct MemberChip {
227237 pub selected: bool,
228238 }
229239
240+/// Radio-pill de priorité (Normale pré-cochée : zéro clic pour le cas courant).
241+pub struct PrioChip {
242+ pub code: &'static str,
243+ pub label: &'static str,
244+ pub selected: bool,
245+}
246+
230247 #[derive(Template)]
231248 #[template(path = "todo_form.html")]
232249 pub struct TodoFormPage {
@@ -241,6 +258,7 @@ pub struct TodoFormPage {
241258 /// Motif + noms de jours/mois : le JS des steppers ± 1 jour recalcule
242259 /// `date_label` sans recharger, sans inventer de texte.
243260 pub vocab: DateVocab,
261+ pub prios: Vec<PrioChip>,
244262 pub members: Vec<MemberChip>,
245263 pub error: Option<&'static str>,
246264 }
@@ -302,6 +320,13 @@ async fn form_page(
302320 date,
303321 date_label: day_label(ctx.lang, date),
304322 vocab: DateVocab::of(ctx.lang),
323+ prios: {
324+ let current = todo.as_ref().map(|t| t.priority.as_str()).unwrap_or(PRIO_NORMAL);
325+ [(PRIO_LOW, tr.prio_low), (PRIO_NORMAL, tr.prio_normal), (PRIO_HIGH, tr.prio_high)]
326+ .into_iter()
327+ .map(|(code, label)| PrioChip { code, label, selected: code == current })
328+ .collect()
329+ },
305330 members: teams::members(&state.pool, ctx.team_id)
306331 .await?
307332 .into_iter()
@@ -323,6 +348,7 @@ struct TodoForm {
323348 name: String,
324349 category: Option<Uuid>,
325350 date: Option<NaiveDate>,
351+ priority: &'static str,
326352 assignees: Vec<Uuid>,
327353 }
328354
@@ -332,6 +358,7 @@ fn parse_form(pairs: &[(String, String)]) -> TodoForm {
332358 name: String::new(),
333359 category: None,
334360 date: None,
361+ priority: PRIO_NORMAL,
335362 assignees: Vec::new(),
336363 };
337364 for (k, v) in pairs {
@@ -340,6 +367,14 @@ fn parse_form(pairs: &[(String, String)]) -> TodoForm {
340367 "nom" => f.name = v.trim().to_string(),
341368 "categorie" => f.category = v.parse().ok(),
342369 "date" => f.date = v.parse().ok(),
370+ // Valeur inconnue → normale (la FK refuserait de toute façon).
371+ "priorite" => {
372+ f.priority = match v.as_str() {
373+ "basse" => PRIO_LOW,
374+ "haute" => PRIO_HIGH,
375+ _ => PRIO_NORMAL,
376+ }
377+ }
343378 "assigne" => {
344379 if let Ok(id) = v.parse() {
345380 f.assignees.push(id);
@@ -366,7 +401,7 @@ pub async fn todo_create(
366401 if f.name.is_empty() {
367402 return Ok(Redirect::to("/taches/nouvelle?err=incomplet"));
368403 }
369- match todos::create(&state.pool, ctx.team_id, ctx.user.id, cat, &f.name, date, &f.assignees).await? {
404+ match todos::create(&state.pool, ctx.team_id, ctx.user.id, cat, &f.name, date, f.priority, &f.assignees).await? {
370405 0 => Err(AppError::BadRequest("bad category".into())),
371406 _ => Ok(Redirect::to("/taches?ok=creee")),
372407 }
@@ -388,7 +423,7 @@ pub async fn todo_update(
388423 if f.name.is_empty() {
389424 return Ok(Redirect::to(&format!("/taches/{id}/modifier?err=incomplet")));
390425 }
391- match todos::update(&state.pool, ctx.team_id, ctx.user.id, id, cat, &f.name, date, &f.assignees).await? {
426+ match todos::update(&state.pool, ctx.team_id, ctx.user.id, id, cat, &f.name, date, f.priority, &f.assignees).await? {
392427 0 => Err(AppError::NotFound),
393428 _ => Ok(Redirect::to("/taches?ok=modifiee")),
394429 }
@@ -12,7 +12,7 @@ use uuid::Uuid;
12 use crate::charts::{color_map, fmt_minutes};12 use crate::charts::{color_map, fmt_minutes};
13 use crate::db::categories;13 use crate::db::categories;
14 use crate::db::teams;14 use crate::db::teams;
15-use crate::db::todos::{self, STATE_IN_PROGRESS, STATE_TODO, TodoRow};15+use crate::db::todos::{self, PRIO_HIGH, PRIO_LOW, PRIO_NORMAL, STATE_IN_PROGRESS, STATE_TODO, TodoRow, prio_rank};
16 use crate::error::AppError;16 use crate::error::AppError;
17 use crate::i18n::{Tr, p1, p2};17 use crate::i18n::{Tr, p1, p2};
18 use crate::period::{DateVocab, day_label, day_month, paris_today};18 use crate::period::{DateVocab, day_label, day_month, paris_today};
@@ -49,6 +49,9 @@ pub struct TodoCard {
49 pub assignees: Vec<AsgChip>,49 pub assignees: Vec<AsgChip>,
50 /// Je suis assigné → liseré sur la carte.50 /// Je suis assigné → liseré sur la carte.
51 pub for_me: bool,51 pub for_me: bool,
52+ /// Priorité haute → liseré rouge + symbole (blocs actifs seulement,
53+ /// le bloc « À venir » reste neutre).
54+ pub high: bool,
52 }55 }
53 56
54 #[derive(Template)]57 #[derive(Template)]
@@ -157,6 +160,7 @@ pub async fn todos_page(
157 })160 })
158 .collect(),161 .collect(),
159 for_me,162 for_me,
163+ high: t.priority == PRIO_HIGH,
160 }164 }
161 };165 };
162 166
@@ -183,6 +187,12 @@ pub async fn todos_page(
183 // Les miennes en premier, puis les sans-assigné (stable).187 // Les miennes en premier, puis les sans-assigné (stable).
184 sel.sort_by_key(|t| !assigned_to_me(t));188 sel.sort_by_key(|t| !assigned_to_me(t));
185 }189 }
190+ if active {
191+ // Blocs actifs : la priorité prime sur tout (tri stable — dans un
192+ // même rang, l'ordre par date puis « à moi » est conservé).
193+ // « À venir » (active = false) reste chronologique pur.
194+ sel.sort_by_key(|t| prio_rank(&t.priority));
195+ }
186 sel.into_iter().map(|t| card(t)).collect()196 sel.into_iter().map(|t| card(t)).collect()
187 };197 };
188 198
@@ -227,6 +237,13 @@ pub struct MemberChip {
227 pub selected: bool,237 pub selected: bool,
228 }238 }
229 239
240+/// Radio-pill de priorité (Normale pré-cochée : zéro clic pour le cas courant).
241+pub struct PrioChip {
242+ pub code: &'static str,
243+ pub label: &'static str,
244+ pub selected: bool,
245+}
246+
230 #[derive(Template)]247 #[derive(Template)]
231 #[template(path = "todo_form.html")]248 #[template(path = "todo_form.html")]
232 pub struct TodoFormPage {249 pub struct TodoFormPage {
@@ -241,6 +258,7 @@ pub struct TodoFormPage {
241 /// Motif + noms de jours/mois : le JS des steppers ± 1 jour recalcule258 /// Motif + noms de jours/mois : le JS des steppers ± 1 jour recalcule
242 /// `date_label` sans recharger, sans inventer de texte.259 /// `date_label` sans recharger, sans inventer de texte.
243 pub vocab: DateVocab,260 pub vocab: DateVocab,
261+ pub prios: Vec<PrioChip>,
244 pub members: Vec<MemberChip>,262 pub members: Vec<MemberChip>,
245 pub error: Option<&'static str>,263 pub error: Option<&'static str>,
246 }264 }
@@ -302,6 +320,13 @@ async fn form_page(
302 date,320 date,
303 date_label: day_label(ctx.lang, date),321 date_label: day_label(ctx.lang, date),
304 vocab: DateVocab::of(ctx.lang),322 vocab: DateVocab::of(ctx.lang),
323+ prios: {
324+ let current = todo.as_ref().map(|t| t.priority.as_str()).unwrap_or(PRIO_NORMAL);
325+ [(PRIO_LOW, tr.prio_low), (PRIO_NORMAL, tr.prio_normal), (PRIO_HIGH, tr.prio_high)]
326+ .into_iter()
327+ .map(|(code, label)| PrioChip { code, label, selected: code == current })
328+ .collect()
329+ },
305 members: teams::members(&state.pool, ctx.team_id)330 members: teams::members(&state.pool, ctx.team_id)
306 .await?331 .await?
307 .into_iter()332 .into_iter()
@@ -323,6 +348,7 @@ struct TodoForm {
323 name: String,348 name: String,
324 category: Option<Uuid>,349 category: Option<Uuid>,
325 date: Option<NaiveDate>,350 date: Option<NaiveDate>,
351+ priority: &'static str,
326 assignees: Vec<Uuid>,352 assignees: Vec<Uuid>,
327 }353 }
328 354
@@ -332,6 +358,7 @@ fn parse_form(pairs: &[(String, String)]) -> TodoForm {
332 name: String::new(),358 name: String::new(),
333 category: None,359 category: None,
334 date: None,360 date: None,
361+ priority: PRIO_NORMAL,
335 assignees: Vec::new(),362 assignees: Vec::new(),
336 };363 };
337 for (k, v) in pairs {364 for (k, v) in pairs {
@@ -340,6 +367,14 @@ fn parse_form(pairs: &[(String, String)]) -> TodoForm {
340 "nom" => f.name = v.trim().to_string(),367 "nom" => f.name = v.trim().to_string(),
341 "categorie" => f.category = v.parse().ok(),368 "categorie" => f.category = v.parse().ok(),
342 "date" => f.date = v.parse().ok(),369 "date" => f.date = v.parse().ok(),
370+ // Valeur inconnue → normale (la FK refuserait de toute façon).
371+ "priorite" => {
372+ f.priority = match v.as_str() {
373+ "basse" => PRIO_LOW,
374+ "haute" => PRIO_HIGH,
375+ _ => PRIO_NORMAL,
376+ }
377+ }
343 "assigne" => {378 "assigne" => {
344 if let Ok(id) = v.parse() {379 if let Ok(id) = v.parse() {
345 f.assignees.push(id);380 f.assignees.push(id);
@@ -366,7 +401,7 @@ pub async fn todo_create(
366 if f.name.is_empty() {401 if f.name.is_empty() {
367 return Ok(Redirect::to("/taches/nouvelle?err=incomplet"));402 return Ok(Redirect::to("/taches/nouvelle?err=incomplet"));
368 }403 }
369- match todos::create(&state.pool, ctx.team_id, ctx.user.id, cat, &f.name, date, &f.assignees).await? {404+ match todos::create(&state.pool, ctx.team_id, ctx.user.id, cat, &f.name, date, f.priority, &f.assignees).await? {
370 0 => Err(AppError::BadRequest("bad category".into())),405 0 => Err(AppError::BadRequest("bad category".into())),
371 _ => Ok(Redirect::to("/taches?ok=creee")),406 _ => Ok(Redirect::to("/taches?ok=creee")),
372 }407 }
@@ -388,7 +423,7 @@ pub async fn todo_update(
388 if f.name.is_empty() {423 if f.name.is_empty() {
389 return Ok(Redirect::to(&format!("/taches/{id}/modifier?err=incomplet")));424 return Ok(Redirect::to(&format!("/taches/{id}/modifier?err=incomplet")));
390 }425 }
391- match todos::update(&state.pool, ctx.team_id, ctx.user.id, id, cat, &f.name, date, &f.assignees).await? {426+ match todos::update(&state.pool, ctx.team_id, ctx.user.id, id, cat, &f.name, date, f.priority, &f.assignees).await? {
392 0 => Err(AppError::NotFound),427 0 => Err(AppError::NotFound),
393 _ => Ok(Redirect::to("/taches?ok=modifiee")),428 _ => Ok(Redirect::to("/taches?ok=modifiee")),
394 }429 }
modified templates/todo_form.html +12 -0
@@ -44,6 +44,18 @@
4444 data-months="{{ vocab.months }}" data-today="{{ vocab.today }}"
4545 data-today-suffix="{{ vocab.today_suffix }}">{{ date_label }}</p>
4646
47+ <div class="field" style="margin-bottom:var(--space-2)">
48+ <label id="todo-prio-label">{{ shell.tr.prio_label }}</label>
49+ </div>
50+ <div class="chips" role="radiogroup" aria-labelledby="todo-prio-label" style="margin-bottom:var(--space-6)">
51+ {% for p in prios %}
52+ <label class="chip">
53+ <input type="radio" name="priorite" value="{{ p.code }}" required {% if p.selected %}checked{% endif %}>
54+ {{ p.label }}
55+ </label>
56+ {% endfor %}
57+ </div>
58+
4759 <div class="field" style="margin-bottom:var(--space-2)">
4860 <label id="todo-asg-label">{{ shell.tr.assign_to }}</label>
4961 </div>
@@ -44,6 +44,18 @@
44 data-months="{{ vocab.months }}" data-today="{{ vocab.today }}"44 data-months="{{ vocab.months }}" data-today="{{ vocab.today }}"
45 data-today-suffix="{{ vocab.today_suffix }}">{{ date_label }}</p>45 data-today-suffix="{{ vocab.today_suffix }}">{{ date_label }}</p>
46 46
47+ <div class="field" style="margin-bottom:var(--space-2)">
48+ <label id="todo-prio-label">{{ shell.tr.prio_label }}</label>
49+ </div>
50+ <div class="chips" role="radiogroup" aria-labelledby="todo-prio-label" style="margin-bottom:var(--space-6)">
51+ {% for p in prios %}
52+ <label class="chip">
53+ <input type="radio" name="priorite" value="{{ p.code }}" required {% if p.selected %}checked{% endif %}>
54+ {{ p.label }}
55+ </label>
56+ {% endfor %}
57+ </div>
58+
47 <div class="field" style="margin-bottom:var(--space-2)">59 <div class="field" style="margin-bottom:var(--space-2)">
48 <label id="todo-asg-label">{{ shell.tr.assign_to }}</label>60 <label id="todo-asg-label">{{ shell.tr.assign_to }}</label>
49 </div>61 </div>
modified templates/todos.html +4 -4
@@ -24,11 +24,11 @@
2424 {% else %}
2525 <div class="todo-grid">
2626 {% for t in doing %}
27- <div class="todo-card{% if t.other %} other{% endif %}{% if t.for_me %} forme{% endif %}">
27+ <div class="todo-card{% if t.other %} other{% endif %}{% if t.for_me %} forme{% endif %}{% if t.high %} prio-haute{% endif %}">
2828 <div class="todo-main">
2929 <span class="team-dot" style="background:{{ t.color }}"></span>
3030 <div style="flex:1;min-width:0">
31- <div class="todo-name">{{ t.name }}</div>
31+ <div class="todo-name">{{ t.name }}{% if t.high %} <span class="prio-flag" role="img" aria-label="{{ shell.tr.prio_high_aria }}">&#8252;&#xFE0E;</span>{% endif %}</div>
3232 <div class="todo-meta">{{ t.meta }}</div>
3333 {% if !t.assignees.is_empty() %}
3434 <div class="asg-chips">
@@ -70,11 +70,11 @@
7070 {% else %}
7171 <div class="todo-grid">
7272 {% for t in open %}
73- <div class="todo-card{% if t.for_me %} forme{% endif %}">
73+ <div class="todo-card{% if t.for_me %} forme{% endif %}{% if t.high %} prio-haute{% endif %}">
7474 <div class="todo-main">
7575 <span class="team-dot" style="background:{{ t.color }}"></span>
7676 <div style="flex:1;min-width:0">
77- <div class="todo-name">{{ t.name }}</div>
77+ <div class="todo-name">{{ t.name }}{% if t.high %} <span class="prio-flag" role="img" aria-label="{{ shell.tr.prio_high_aria }}">&#8252;&#xFE0E;</span>{% endif %}</div>
7878 <div class="todo-meta{% if t.late %} late{% endif %}">{{ t.meta }}</div>
7979 {% if !t.assignees.is_empty() %}
8080 <div class="asg-chips">
@@ -24,11 +24,11 @@
24 {% else %}24 {% else %}
25 <div class="todo-grid">25 <div class="todo-grid">
26 {% for t in doing %}26 {% for t in doing %}
27- <div class="todo-card{% if t.other %} other{% endif %}{% if t.for_me %} forme{% endif %}">27+ <div class="todo-card{% if t.other %} other{% endif %}{% if t.for_me %} forme{% endif %}{% if t.high %} prio-haute{% endif %}">
28 <div class="todo-main">28 <div class="todo-main">
29 <span class="team-dot" style="background:{{ t.color }}"></span>29 <span class="team-dot" style="background:{{ t.color }}"></span>
30 <div style="flex:1;min-width:0">30 <div style="flex:1;min-width:0">
31- <div class="todo-name">{{ t.name }}</div>31+ <div class="todo-name">{{ t.name }}{% if t.high %} <span class="prio-flag" role="img" aria-label="{{ shell.tr.prio_high_aria }}">&#8252;&#xFE0E;</span>{% endif %}</div>
32 <div class="todo-meta">{{ t.meta }}</div>32 <div class="todo-meta">{{ t.meta }}</div>
33 {% if !t.assignees.is_empty() %}33 {% if !t.assignees.is_empty() %}
34 <div class="asg-chips">34 <div class="asg-chips">
@@ -70,11 +70,11 @@
70 {% else %}70 {% else %}
71 <div class="todo-grid">71 <div class="todo-grid">
72 {% for t in open %}72 {% for t in open %}
73- <div class="todo-card{% if t.for_me %} forme{% endif %}">73+ <div class="todo-card{% if t.for_me %} forme{% endif %}{% if t.high %} prio-haute{% endif %}">
74 <div class="todo-main">74 <div class="todo-main">
75 <span class="team-dot" style="background:{{ t.color }}"></span>75 <span class="team-dot" style="background:{{ t.color }}"></span>
76 <div style="flex:1;min-width:0">76 <div style="flex:1;min-width:0">
77- <div class="todo-name">{{ t.name }}</div>77+ <div class="todo-name">{{ t.name }}{% if t.high %} <span class="prio-flag" role="img" aria-label="{{ shell.tr.prio_high_aria }}">&#8252;&#xFE0E;</span>{% endif %}</div>
78 <div class="todo-meta{% if t.late %} late{% endif %}">{{ t.meta }}</div>78 <div class="todo-meta{% if t.late %} late{% endif %}">{{ t.meta }}</div>
79 {% if !t.assignees.is_empty() %}79 {% if !t.assignees.is_empty() %}
80 <div class="asg-chips">80 <div class="asg-chips">