1use askama::Template;
2
3use super::helpers::SortField;
4
5/// A project card on the root page — either healthy or failed.
6pub(super) enum ProjectCard {
7 Ok {
8 name: String,
9 open: usize,
10 in_progress: usize,
11 closed: usize,
12 total: usize,
13 },
14 Err {
15 name: String,
16 error: String,
17 },
18}
19
20/// Minimal view-model for a scored task in the "Next Up" table.
21pub(super) struct ScoredEntry {
22 pub(super) id: String,
23 pub(super) short_id: String,
24 pub(super) title: String,
25 pub(super) score: String,
26 pub(super) status: String,
27 pub(super) status_display: &'static str,
28}
29
30/// Minimal view-model for a task row in the project task table.
31pub(super) struct TaskRow {
32 pub(super) full_id: String,
33 pub(super) short_id: String,
34 pub(super) status: String,
35 pub(super) status_display: &'static str,
36 pub(super) priority: String,
37 pub(super) effort: String,
38 pub(super) title: String,
39 pub(super) created_at: String,
40 pub(super) created_at_display: String,
41}
42
43/// View-model for the task detail page.
44pub(super) struct TaskView {
45 pub(super) full_id: String,
46 pub(super) short_id: String,
47 pub(super) title: String,
48 pub(super) description: String,
49 pub(super) task_type: String,
50 pub(super) status: String,
51 pub(super) priority: String,
52 pub(super) effort: String,
53 pub(super) created_at: String,
54 pub(super) created_at_display: String,
55 pub(super) updated_at: String,
56 pub(super) updated_at_display: String,
57 pub(super) labels: Vec<String>,
58 pub(super) logs: Vec<LogView>,
59}
60
61pub(super) struct LogView {
62 pub(super) timestamp: String,
63 pub(super) timestamp_display: String,
64 pub(super) message: String,
65}
66
67/// A blocker reference for the task detail page.
68pub(super) struct BlockerRef {
69 pub(super) full_id: String,
70 pub(super) short_id: String,
71}
72
73/// Sort context passed to the task_table macro. When present, column headers
74/// become clickable links that set sort/order query params.
75pub(super) struct SortContext {
76 /// Base URL for sort links (e.g. `/projects/myproj`).
77 pub(super) base_href: String,
78 /// Current sort field.
79 pub(super) field: String,
80 /// Current sort order ("asc" or "desc").
81 pub(super) order: String,
82 /// Query string fragment for the current filters (without sort/order/page),
83 /// suitable for appending to hrefs.
84 pub(super) filter_qs: String,
85}
86
87impl SortContext {
88 /// Build the href for a column header link. Clicking the currently-active
89 /// column toggles direction; clicking a different column uses its default.
90 fn column_href(&self, col: &str) -> String {
91 let order = if col == self.field {
92 // Toggle current direction.
93 match self.order.as_str() {
94 "asc" => "desc",
95 _ => "asc",
96 }
97 } else {
98 // Use the column's sensible default.
99 SortField::parse(col)
100 .map(|f| f.default_order().as_str())
101 .unwrap_or("asc")
102 };
103 let mut qs = self.filter_qs.clone();
104 if !qs.is_empty() {
105 qs.push('&');
106 }
107 qs.push_str(&format!("sort={col}&order={order}"));
108 format!("{}?{qs}", self.base_href)
109 }
110
111 /// Return the arrow indicator for the active column, or empty string.
112 fn arrow(&self, col: &str) -> &str {
113 if col == self.field {
114 match self.order.as_str() {
115 "asc" => " ↑",
116 "desc" => " ↓",
117 _ => "",
118 }
119 } else {
120 ""
121 }
122 }
123}
124
125#[derive(Template)]
126#[template(path = "index.html")]
127pub(super) struct IndexTemplate {
128 pub(super) all_projects: Vec<String>,
129 pub(super) active_project: Option<String>,
130 pub(super) projects: Vec<ProjectCard>,
131}
132
133#[derive(Template)]
134#[template(path = "project.html")]
135pub(super) struct ProjectTemplate {
136 pub(super) all_projects: Vec<String>,
137 pub(super) active_project: Option<String>,
138 pub(super) project_name: String,
139 pub(super) stats_open: usize,
140 pub(super) stats_in_progress: usize,
141 pub(super) stats_closed: usize,
142 pub(super) next_up: Vec<ScoredEntry>,
143 pub(super) page_tasks: Vec<TaskRow>,
144 pub(super) all_labels: Vec<String>,
145 pub(super) filter_status: Option<String>,
146 pub(super) filter_priority: Option<String>,
147 pub(super) filter_effort: Option<String>,
148 pub(super) filter_label: Option<String>,
149 pub(super) filter_search: String,
150 pub(super) page: usize,
151 pub(super) total_pages: usize,
152 pub(super) pagination_pages: Vec<usize>,
153 pub(super) sort_ctx: SortContext,
154}
155
156impl ProjectTemplate {
157 /// Build a query-string fragment containing the current filters (no sort,
158 /// no page). Reused by both pagination and sort helpers.
159 fn filter_qs(&self) -> String {
160 let mut parts = Vec::new();
161 if let Some(ref s) = self.filter_status {
162 parts.push(format!("status={s}"));
163 }
164 if let Some(ref p) = self.filter_priority {
165 parts.push(format!("priority={p}"));
166 }
167 if let Some(ref e) = self.filter_effort {
168 parts.push(format!("effort={e}"));
169 }
170 if let Some(ref l) = self.filter_label {
171 parts.push(format!("label={l}"));
172 }
173 if !self.filter_search.is_empty() {
174 parts.push(format!("q={}", self.filter_search));
175 }
176 parts.join("&")
177 }
178
179 /// Build a pagination link preserving current filter and sort params.
180 fn pagination_href(&self, target_page: &usize) -> String {
181 let target_page = *target_page;
182 let mut qs = self.filter_qs();
183 if !qs.is_empty() {
184 qs.push('&');
185 }
186 qs.push_str(&format!(
187 "sort={}&order={}",
188 self.sort_ctx.field, self.sort_ctx.order
189 ));
190 qs.push_str(&format!("&page={target_page}"));
191 format!("/projects/{}?{qs}", self.project_name)
192 }
193}
194
195#[derive(Template)]
196#[template(path = "task.html")]
197pub(super) struct TaskTemplate {
198 pub(super) all_projects: Vec<String>,
199 pub(super) active_project: Option<String>,
200 pub(super) project_name: String,
201 pub(super) task: TaskView,
202 pub(super) blockers_open: Vec<BlockerRef>,
203 pub(super) blockers_resolved: Vec<BlockerRef>,
204 pub(super) subtasks: Vec<TaskRow>,
205}
206
207#[derive(Template)]
208#[template(path = "error.html")]
209pub(super) struct ErrorTemplate {
210 pub(super) all_projects: Vec<String>,
211 pub(super) active_project: Option<String>,
212 pub(super) status_code: u16,
213 pub(super) message: String,
214}