1use super::task::Task;
37use crate::db::templates::TaskTemplate;
38use crate::db::workdays::Workday;
39use crate::libs::formatter::{format_duration, terminal_cols, truncate_to_width};
40use crate::libs::messages::Message;
41use crate::libs::pause::Pause;
42use crate::libs::report;
43use crate::msg_print;
44use anyhow::Result;
45use chrono::{Duration, NaiveDate, TimeDelta};
46use prettytable::{Cell, Row, Table, format, row};
47use std::collections::HashMap;
48use unicode_width::UnicodeWidthStr;
49
50pub struct View {}
55
56impl View {
57 pub fn tasks(tasks: &[Task]) -> Result<()> {
62 let show_task_id = tasks.iter().any(|t| t.task_id.is_some_and(|id| id != 0));
63 let show_comment = tasks.iter().any(|t| !t.comment.trim().is_empty());
64 let show_tags = tasks.iter().any(|t| !t.tags.is_empty());
65
66 let idx_width = tasks.len().to_string().width().max("#".width());
67 let id_width = tasks.iter().map(|t| t.id.unwrap_or(0).to_string().width()).max().unwrap_or(1).max("ID".width());
68 let task_id_width = if show_task_id {
69 tasks
70 .iter()
71 .map(|t| t.task_id.unwrap_or(0).to_string().width())
72 .max()
73 .unwrap_or(1)
74 .max("TASK ID".width())
75 } else {
76 0
77 };
78 let done_width = "DONE".width().max("100%".width());
79
80 let mut num_cols = 4; if show_task_id {
83 num_cols += 1;
84 }
85 if show_comment {
86 num_cols += 1;
87 }
88 if show_tags {
89 num_cols += 1;
90 }
91
92 let mut fixed_content = idx_width + id_width + done_width;
93 if show_task_id {
94 fixed_content += task_id_width;
95 }
96
97 let frame_overhead = 3 * num_cols + 1;
98 let mut flexible = terminal_cols().saturating_sub(frame_overhead + fixed_content);
99
100 let tags_width = if show_tags {
102 let width = (flexible / 5).clamp(8, 20);
103 flexible = flexible.saturating_sub(width);
104 width
105 } else {
106 0
107 };
108 let comment_width = if show_comment {
109 let width = (flexible / 3).clamp(12, 40);
110 flexible = flexible.saturating_sub(width);
111 width
112 } else {
113 0
114 };
115 let name_width = flexible.max(12);
116
117 let mut table = Table::new();
118 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
119
120 let mut titles = vec![Cell::new("#"), Cell::new("ID")];
121 if show_task_id {
122 titles.push(Cell::new("TASK ID"));
123 }
124 titles.push(Cell::new("NAME"));
125 if show_comment {
126 titles.push(Cell::new("COMMENT"));
127 }
128 titles.push(Cell::new("DONE"));
129 if show_tags {
130 titles.push(Cell::new("TAGS"));
131 }
132 table.set_titles(Row::new(titles));
133
134 for (index, task) in tasks.iter().enumerate() {
135 let mut cells = vec![Cell::new(&(index + 1).to_string()), Cell::new(&task.id.unwrap_or(0).to_string())];
136 if show_task_id {
137 cells.push(Cell::new(&task.task_id.unwrap_or(0).to_string()));
138 }
139 cells.push(Cell::new(&truncate_to_width(&task.name, name_width)));
140 if show_comment {
141 cells.push(Cell::new(&truncate_to_width(task.comment.trim(), comment_width)));
142 }
143 cells.push(Cell::new(&format!("{}%", task.completeness.unwrap_or(100))));
144 if show_tags {
145 let tags_str = task.tags.iter().map(|t| t.name.as_str()).collect::<Vec<_>>().join(", ");
146 cells.push(Cell::new(&truncate_to_width(&tags_str, tags_width)));
147 }
148 table.add_row(Row::new(cells));
149 }
150
151 table.printstd();
152 Ok(())
153 }
154
155 pub fn report(workday: &Workday, intervals: &[report::WorkInterval], filtered_duration: &TimeDelta, productivity: &f64, tasks: &[Task]) -> Result<()> {
157 msg_print!(Message::ReportHeader(workday.date.format("%B %-d, %Y").to_string()), true);
159
160 let mut table = Table::new();
162 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
163 table.set_titles(row!["ID", "START", "END", "DURATION"]);
164
165 for (index, interval) in intervals.iter().enumerate() {
167 table.add_row(row![
168 index + 1, interval.start.format("%H:%M"), interval.end.format("%H:%M"), format_duration(&interval.duration) ]);
173 }
174
175 table.add_empty_row(); table.add_row(row!["TOTAL", "", "", format_duration(filtered_duration)]);
178 table.add_row(row!["PRODUCTIVITY", "", "", format!("{:.1}%", productivity)]);
179
180 table.printstd();
182
183 if !tasks.is_empty() {
185 msg_print!(Message::TasksHeader, true);
186 Self::tasks(tasks)?;
187 }
188
189 Ok(())
190 }
191
192 pub fn sum((daily_durations, total_duration, average_duration): &(HashMap<NaiveDate, (String, String)>, String, String)) -> Result<()> {
211 let mut table: Table = Table::new();
213 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
214 table.set_titles(row!["DATE", "HOURS", "PRODUCTIVITY"]);
215
216 let mut sorted_dates: Vec<&NaiveDate> = daily_durations.keys().collect();
218 sorted_dates.sort();
219
220 for date in sorted_dates {
222 if let Some((duration, productivity)) = daily_durations.get(date) {
223 table.add_row(row![
224 date.format("%Y-%m-%d"), duration, productivity ]);
228 }
229 }
230
231 table.add_empty_row(); table.add_row(row!["TOTAL", total_duration, ""]);
234 table.add_row(row!["AVERAGE", average_duration, ""]);
235
236 table.printstd();
238 Ok(())
239 }
240
241 pub fn pauses(pauses: &[Pause], total_pause_time: Duration) -> Result<()> {
243 let mut table = Table::new();
244 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
245 table.set_titles(row!["ID", "START", "END", "DURATION"]);
246
247 for (i, b) in pauses.iter().enumerate() {
248 table.add_row(row![
249 i + 1,
250 b.start.format("%H:%M"),
251 b.end.map(|t| t.format("%H:%M").to_string()).unwrap_or_else(|| "-".to_string()),
252 b.duration
253 .map(|duration: TimeDelta| format_duration(&duration))
254 .unwrap_or_else(|| "--:--".to_string())
255 ]);
256 }
257
258 if !pauses.is_empty() {
260 table.add_empty_row();
261 table.add_row(row!["TOTAL", "", "", format_duration(&total_pause_time)]);
262 }
263
264 table.printstd();
265 Ok(())
266 }
267
268 pub fn templates(templates: &[TaskTemplate]) -> Result<()> {
283 let mut table = Table::new();
285 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
286 table.set_titles(row!["TEMPLATE NAME", "TASK NAME", "COMMENT", "COMPLETENESS"]);
287
288 for template in templates {
290 table.add_row(row![
291 template.name, template.task_name, template.comment, format!("{}%", template.completeness) ]);
296 }
297
298 table.printstd();
300 Ok(())
301 }
302
303 pub fn tags(tags: &[crate::db::tags::Tag]) -> Result<()> {
318 let mut table = Table::new();
320 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
321 table.set_titles(row!["ID", "NAME", "COLOR"]);
322
323 for tag in tags {
325 table.add_row(row![
326 tag.id.unwrap_or(0), tag.name, tag.color.as_deref().unwrap_or("-") ]);
330 }
331
332 table.printstd();
334 Ok(())
335 }
336
337 pub fn jira_inbox(items: &[crate::db::jira_inbox::JiraInboxItem]) -> Result<()> {
343 let now = chrono::Local::now().naive_local();
344 let badges: Vec<String> = items.iter().map(|i| i.badge(now).unwrap_or_default()).collect();
345 let pin_width = "★".width().max(1);
346 let change_width = badges.iter().map(|b| b.width()).max().unwrap_or(1).max("CHANGE".width()).min(24);
347 let score_width = items
348 .iter()
349 .map(|i| i.sort_value.map(|v| format!("{}", v).width()).unwrap_or_else(|| "—".width()))
350 .max()
351 .unwrap_or(1)
352 .max("SCORE".width());
353 let priority_width = items
354 .iter()
355 .map(|i| i.priority.as_deref().unwrap_or("—").width())
356 .max()
357 .unwrap_or(1)
358 .max("PRIORITY".width());
359 let key_width = items.iter().map(|i| i.issue_key.width()).max().unwrap_or(1).max("KEY".width());
360 let status_width = items
361 .iter()
362 .map(|i| {
363 if i.status_name.is_empty() {
364 i.status_id.as_deref().unwrap_or("—").width()
365 } else {
366 i.status_name.width()
367 }
368 })
369 .max()
370 .unwrap_or(1)
371 .max("STATUS".width())
372 .min(18);
373
374 let num_cols = 7;
376 let frame_overhead = 3 * num_cols + 1;
377 let fixed = pin_width + change_width + score_width + priority_width + key_width + status_width;
378 let summary_width = terminal_cols().saturating_sub(frame_overhead + fixed).max(12);
379
380 let mut table = Table::new();
381 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
382 table.set_titles(row!["", "CHANGE", "SCORE", "PRIORITY", "KEY", "STATUS", "SUMMARY"]);
383
384 for (item, badge) in items.iter().zip(&badges) {
385 let pin = if item.pinned { "★" } else { "" };
386 let score = item.sort_value.map(|v| format!("{}", v)).unwrap_or_else(|| "—".to_string());
387 let status_raw = if item.status_name.is_empty() {
388 item.status_id.as_deref().unwrap_or("—")
389 } else {
390 item.status_name.as_str()
391 };
392 table.add_row(row![
393 pin,
394 truncate_to_width(badge, change_width),
395 score,
396 item.priority.as_deref().unwrap_or("—"),
397 item.issue_key,
398 truncate_to_width(status_raw, status_width),
399 truncate_to_width(&item.summary, summary_width),
400 ]);
401 }
402
403 table.printstd();
404 Ok(())
405 }
406}