kasl/libs/view.rs
1//! Console display and table formatting system.
2//!
3//! Provides interface for rendering application data in well-formatted console tables.
4//! Handles presentation layer for work reports, task lists, summaries, templates, and tags.
5//!
6//! ## Features
7//!
8//! - **Structured Data Display**: Converts complex data structures into readable tables
9//! - **Consistent Formatting**: Maintains uniform appearance across all table types
10//! - **Report Visualization**: Displays pre-calculated productivity and work metrics
11//! - **Duration Formatting**: Handles time duration display in human-readable formats
12//!
13//! ## Usage
14//!
15//! ```rust,no_run
16//! # fn f() -> anyhow::Result<()> {
17//! use kasl::libs::view::View;
18//! use kasl::libs::task::Task;
19//! use kasl::db::workdays::Workday;
20//! use kasl::libs::report::WorkInterval;
21//! use chrono::{Local, TimeDelta};
22//! use std::collections::HashMap;
23//!
24//! let tasks: Vec<Task> = vec![];
25//! let workday = Workday {
26//! id: 1,
27//! date: Local::now().date_naive(),
28//! start: Local::now().naive_local(),
29//! end: None,
30//! };
31//! let intervals: Vec<WorkInterval> = vec![];
32//! let filtered_duration = TimeDelta::zero();
33//! let productivity = 0.0_f64;
34//! let summary_data = (HashMap::new(), String::new(), String::new());
35//!
36//! View::tasks(&tasks)?;
37//! View::report(&workday, &intervals, &filtered_duration, &productivity, &tasks)?;
38//! View::sum(&summary_data)?;
39//! # Ok(())
40//! # }
41//! ```
42
43use super::task::Task;
44use crate::db::templates::TaskTemplate;
45use crate::db::workdays::Workday;
46use crate::libs::formatter::{format_duration, terminal_cols, truncate_to_width};
47use crate::libs::messages::Message;
48use crate::libs::pause::Pause;
49use crate::libs::report;
50use crate::msg_print;
51use anyhow::Result;
52use chrono::{Duration, NaiveDate, TimeDelta};
53use prettytable::{Cell, Row, Table, format, row};
54use std::collections::HashMap;
55use unicode_width::UnicodeWidthStr;
56
57/// A utility struct for rendering application data to the console.
58///
59/// Serves as a namespace for various table rendering functions. All methods are static,
60/// making it easy to call formatting functions without needing to instantiate the struct.
61pub struct View {}
62
63impl View {
64 /// Displays a formatted table of tasks with comprehensive metadata.
65 ///
66 /// Renders a detailed table showing task information including identification numbers,
67 /// names, completion status, comments, and associated tags.
68 ///
69 /// # Arguments
70 ///
71 /// * `tasks` - A slice of `Task` structs to display in the table
72 ///
73 /// # Returns
74 ///
75 /// Returns `Ok(())` on successful table rendering, or an error if
76 /// the table cannot be displayed due to terminal or formatting issues.
77 pub fn tasks(tasks: &[Task]) -> Result<()> {
78 let show_task_id = tasks.iter().any(|t| t.task_id.is_some_and(|id| id != 0));
79 let show_comment = tasks.iter().any(|t| !t.comment.trim().is_empty());
80 let show_tags = tasks.iter().any(|t| !t.tags.is_empty());
81
82 let idx_width = tasks.len().to_string().width().max("#".width());
83 let id_width = tasks.iter().map(|t| t.id.unwrap_or(0).to_string().width()).max().unwrap_or(1).max("ID".width());
84 let task_id_width = if show_task_id {
85 tasks
86 .iter()
87 .map(|t| t.task_id.unwrap_or(0).to_string().width())
88 .max()
89 .unwrap_or(1)
90 .max("TASK ID".width())
91 } else {
92 0
93 };
94 let done_width = "DONE".width().max("100%".width());
95
96 // prettytable cell format: `| content |` → 3 chars overhead per column + 1 outer border
97 let mut num_cols = 4; // #, ID, NAME, DONE
98 if show_task_id {
99 num_cols += 1;
100 }
101 if show_comment {
102 num_cols += 1;
103 }
104 if show_tags {
105 num_cols += 1;
106 }
107
108 let mut fixed_content = idx_width + id_width + done_width;
109 if show_task_id {
110 fixed_content += task_id_width;
111 }
112
113 let frame_overhead = 3 * num_cols + 1;
114 let mut flexible = terminal_cols().saturating_sub(frame_overhead + fixed_content);
115
116 // Reserve a modest slice for optional text columns; NAME gets the rest.
117 let tags_width = if show_tags {
118 let width = (flexible / 5).clamp(8, 20);
119 flexible = flexible.saturating_sub(width);
120 width
121 } else {
122 0
123 };
124 let comment_width = if show_comment {
125 let width = (flexible / 3).clamp(12, 40);
126 flexible = flexible.saturating_sub(width);
127 width
128 } else {
129 0
130 };
131 let name_width = flexible.max(12);
132
133 let mut table = Table::new();
134 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
135
136 let mut titles = vec![Cell::new("#"), Cell::new("ID")];
137 if show_task_id {
138 titles.push(Cell::new("TASK ID"));
139 }
140 titles.push(Cell::new("NAME"));
141 if show_comment {
142 titles.push(Cell::new("COMMENT"));
143 }
144 titles.push(Cell::new("DONE"));
145 if show_tags {
146 titles.push(Cell::new("TAGS"));
147 }
148 table.set_titles(Row::new(titles));
149
150 for (index, task) in tasks.iter().enumerate() {
151 let mut cells = vec![Cell::new(&(index + 1).to_string()), Cell::new(&task.id.unwrap_or(0).to_string())];
152 if show_task_id {
153 cells.push(Cell::new(&task.task_id.unwrap_or(0).to_string()));
154 }
155 cells.push(Cell::new(&truncate_to_width(&task.name, name_width)));
156 if show_comment {
157 cells.push(Cell::new(&truncate_to_width(task.comment.trim(), comment_width)));
158 }
159 cells.push(Cell::new(&format!("{}%", task.completeness.unwrap_or(100))));
160 if show_tags {
161 let tags_str = task.tags.iter().map(|t| t.name.as_str()).collect::<Vec<_>>().join(", ");
162 cells.push(Cell::new(&truncate_to_width(&tags_str, tags_width)));
163 }
164 table.add_row(Row::new(cells));
165 }
166
167 table.printstd();
168 Ok(())
169 }
170
171 /// Displays a formatted daily work report using pre-calculated intervals.
172 ///
173 /// This method displays the core report data in a structured table format,
174 /// including work intervals, total time, and productivity metrics calculated
175 /// using the centralized Productivity module.
176 ///
177 /// ## Display Components
178 ///
179 /// 1. **Work Intervals**: Detailed breakdown of focused work periods
180 /// 2. **Total Duration**: Sum of all work intervals (may be filtered)
181 /// 3. **Productivity Percentage**: Calculated using comprehensive Productivity logic
182 /// 4. **Associated Tasks**: Tasks completed during the workday for context
183 ///
184 /// The productivity value displayed here is calculated using the same centralized
185 /// logic used throughout the application for consistency.
186 ///
187 /// # Arguments
188 ///
189 /// * `workday` - The workday record containing start/end times
190 /// * `intervals` - Pre-calculated and optionally filtered work intervals for display
191 /// * `filtered_duration` - Sum of displayed interval durations
192 /// * `productivity` - Productivity percentage from centralized Productivity calculation
193 /// * `tasks` - Tasks completed during the workday for context
194 pub fn report(workday: &Workday, intervals: &[report::WorkInterval], filtered_duration: &TimeDelta, productivity: &f64, tasks: &[Task]) -> Result<()> {
195 // Display formatted report header with readable date
196 msg_print!(Message::ReportHeader(workday.date.format("%B %-d, %Y").to_string()), true);
197
198 // Create and populate the work intervals table
199 let mut table = Table::new();
200 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
201 table.set_titles(row!["ID", "START", "END", "DURATION"]);
202
203 // Add each work interval as a table row with formatted times
204 for (index, interval) in intervals.iter().enumerate() {
205 table.add_row(row![
206 index + 1, // Sequential numbering for easy reference
207 interval.start.format("%H:%M"), // Start time in HH:MM format
208 interval.end.format("%H:%M"), // End time in HH:MM format
209 format_duration(&interval.duration) // Human-readable duration
210 ]);
211 }
212
213 // Add summary rows with total time and productivity metrics
214 table.add_empty_row(); // Visual separator before summary
215 table.add_row(row!["TOTAL", "", "", format_duration(filtered_duration)]);
216 table.add_row(row!["PRODUCTIVITY", "", "", format!("{:.1}%", productivity)]);
217
218 // Render the intervals table to console
219 table.printstd();
220
221 // Display associated tasks if any were completed during the day
222 if !tasks.is_empty() {
223 msg_print!(Message::TasksHeader, true);
224 Self::tasks(tasks)?;
225 }
226
227 Ok(())
228 }
229
230 /// Displays a monthly summary of working hours with daily breakdowns.
231 ///
232 /// This method renders a comprehensive monthly view that shows daily work
233 /// patterns, totals, and averages. It provides both detailed daily data
234 /// and aggregate statistics to help users understand their work patterns
235 /// over the entire month.
236 ///
237 /// ## Summary Structure
238 ///
239 /// The monthly summary includes:
240 /// - **Daily Breakdown**: Each day with date, hours worked, and workday status
241 /// - **Total Hours**: Cumulative time worked across all days in the month
242 /// - **Average Hours**: Mean daily working time for better pattern analysis
243 /// - **Work Days**: Count of days with recorded work activity
244 ///
245 /// ## Data Interpretation
246 ///
247 /// - **Workday Hours**: Actual time recorded for productive work days
248 /// - **Rest Day Hours**: Default hours applied to weekends and holidays
249 /// - **Missing Days**: Days without any recorded activity (shown as 0:00)
250 ///
251 /// # Arguments
252 ///
253 /// * `summary_data` - A tuple containing:
254 /// - `HashMap<NaiveDate, (String, String)>`: Daily durations and productivity data
255 /// - `String`: Total duration for the entire month
256 /// - `String`: Average daily duration across all days
257 ///
258 /// # Returns
259 ///
260 /// Returns `Ok(())` on successful summary display, or an error if
261 /// table formatting or rendering fails.
262 ///
263 /// # Examples
264 ///
265 /// ```rust,no_run
266 /// # fn f() -> anyhow::Result<()> {
267 /// use kasl::libs::view::View;
268 /// use std::collections::HashMap;
269 ///
270 /// let daily_map = HashMap::new();
271 /// let total_hours = String::new();
272 /// let average_hours = String::new();
273 ///
274 /// let summary_data = (daily_map, total_hours, average_hours);
275 /// View::sum(&summary_data)?;
276 /// # Ok(())
277 /// # }
278 /// ```
279 pub fn sum((daily_durations, total_duration, average_duration): &(HashMap<NaiveDate, (String, String)>, String, String)) -> Result<()> {
280 // Initialize table with appropriate formatting for summary data
281 let mut table: Table = Table::new();
282 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
283 table.set_titles(row!["DATE", "HOURS", "PRODUCTIVITY"]);
284
285 // Sort dates chronologically for logical display order
286 let mut sorted_dates: Vec<&NaiveDate> = daily_durations.keys().collect();
287 sorted_dates.sort();
288
289 // Add each day's data as a table row
290 for date in sorted_dates {
291 if let Some((duration, productivity)) = daily_durations.get(date) {
292 table.add_row(row![
293 date.format("%Y-%m-%d"), // ISO date format for consistency
294 duration, // Formatted duration string
295 productivity // Productivity percentage or status
296 ]);
297 }
298 }
299
300 // Add summary statistics with visual separation
301 table.add_empty_row(); // Visual separator before totals
302 table.add_row(row!["TOTAL", total_duration, ""]);
303 table.add_row(row!["AVERAGE", average_duration, ""]);
304
305 // Render the summary table to console
306 table.printstd();
307 Ok(())
308 }
309
310 /// Displays a table of pauses for a given day with total pause time.
311 ///
312 /// # Arguments
313 /// * `pauses` - A slice of `Pause` records to display.
314 /// * `total_pause_time` - The total duration of all pauses.
315 ///
316 /// # Returns
317 /// A `Result` indicating success.
318 pub fn pauses(pauses: &[Pause], total_pause_time: Duration) -> Result<()> {
319 let mut table = Table::new();
320 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
321 table.set_titles(row!["ID", "START", "END", "DURATION"]);
322
323 for (i, b) in pauses.iter().enumerate() {
324 table.add_row(row![
325 i + 1,
326 b.start.format("%H:%M"),
327 b.end.map(|t| t.format("%H:%M").to_string()).unwrap_or_else(|| "-".to_string()),
328 b.duration
329 .map(|duration: TimeDelta| format_duration(&duration))
330 .unwrap_or_else(|| "--:--".to_string())
331 ]);
332 }
333
334 // Add total row
335 if !pauses.is_empty() {
336 table.add_empty_row();
337 table.add_row(row!["TOTAL", "", "", format_duration(&total_pause_time)]);
338 }
339
340 table.printstd();
341 Ok(())
342 }
343
344 /// Displays a formatted table of task templates for reusable task creation.
345 ///
346 /// This method renders a comprehensive view of all available task templates,
347 /// showing their configuration and usage information. Templates provide a
348 /// convenient way to create commonly used tasks with pre-filled parameters.
349 ///
350 /// ## Template Information
351 ///
352 /// The table displays essential template metadata:
353 /// - **Template Name**: Unique identifier for template selection
354 /// - **Task Name**: Default task title that will be used
355 /// - **Comment**: Pre-configured task description or notes
356 /// - **Completeness**: Default completion percentage for new tasks
357 ///
358 /// ## Usage Context
359 ///
360 /// Templates are particularly useful for:
361 /// - Recurring tasks with standard parameters
362 /// - Team workflows with consistent task structures
363 /// - Quick task creation with minimal input required
364 /// - Standardized task naming and completion patterns
365 ///
366 /// # Arguments
367 ///
368 /// * `templates` - A slice of `TaskTemplate` structs to display
369 ///
370 /// # Returns
371 ///
372 /// Returns `Ok(())` on successful table rendering, or an error if
373 /// display operations fail.
374 ///
375 /// # Examples
376 ///
377 /// ```rust,no_run
378 /// # fn f() -> anyhow::Result<()> {
379 /// use kasl::libs::view::View;
380 /// use kasl::db::templates::TaskTemplate;
381 ///
382 /// let templates: Vec<TaskTemplate> = vec![/* template instances */];
383 /// View::templates(&templates)?;
384 /// # Ok(())
385 /// # }
386 /// ```
387 pub fn templates(templates: &[TaskTemplate]) -> Result<()> {
388 // Initialize table with clean formatting for template data
389 let mut table = Table::new();
390 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
391 table.set_titles(row!["TEMPLATE NAME", "TASK NAME", "COMMENT", "COMPLETENESS"]);
392
393 // Populate table with template information
394 for template in templates {
395 table.add_row(row![
396 template.name, // Unique template identifier
397 template.task_name, // Default task title
398 template.comment, // Pre-configured description
399 format!("{}%", template.completeness) // Default completion with % symbol
400 ]);
401 }
402
403 // Render the templates table to console
404 table.printstd();
405 Ok(())
406 }
407
408 /// Displays a formatted table of tags for task categorization and organization.
409 ///
410 /// This method provides a comprehensive view of all available tags that can
411 /// be applied to tasks for organization and filtering purposes. The table
412 /// shows both the functional and visual aspects of each tag.
413 ///
414 /// ## Tag Information
415 ///
416 /// The table displays key tag metadata:
417 /// - **ID**: Unique database identifier for programmatic reference
418 /// - **NAME**: Human-readable tag name used for categorization
419 /// - **COLOR**: Optional color coding for visual organization (if supported)
420 ///
421 /// ## Organizational Benefits
422 ///
423 /// Tags provide several organizational advantages:
424 /// - **Categorization**: Group related tasks by project, priority, or type
425 /// - **Filtering**: Quickly find tasks based on specific criteria
426 /// - **Visual Organization**: Color coding for rapid visual identification
427 /// - **Reporting**: Generate reports filtered by specific tag categories
428 ///
429 /// ## Color Display
430 ///
431 /// Colors are displayed as text values (hex codes, names, etc.) since
432 /// terminal color support varies. A dash (-) indicates no color assigned.
433 ///
434 /// # Arguments
435 ///
436 /// * `tags` - A slice of `Tag` structs to display in the table
437 ///
438 /// # Returns
439 ///
440 /// Returns `Ok(())` on successful table rendering, or an error if
441 /// display operations fail.
442 ///
443 /// # Examples
444 ///
445 /// ```rust,no_run
446 /// # fn f() -> anyhow::Result<()> {
447 /// use kasl::libs::view::View;
448 /// use kasl::db::tags::Tag;
449 ///
450 /// let tags: Vec<Tag> = vec![/* tag instances */];
451 /// View::tags(&tags)?;
452 /// # Ok(())
453 /// # }
454 /// ```
455 pub fn tags(tags: &[crate::db::tags::Tag]) -> Result<()> {
456 // Initialize table with appropriate formatting for tag data
457 let mut table = Table::new();
458 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
459 table.set_titles(row!["ID", "NAME", "COLOR"]);
460
461 // Populate table with tag information
462 for tag in tags {
463 table.add_row(row![
464 tag.id.unwrap_or(0), // Database ID, showing 0 for new tags
465 tag.name, // Human-readable tag name
466 tag.color.as_deref().unwrap_or("-") // Color value or dash if none
467 ]);
468 }
469
470 // Render the tags table to console
471 table.printstd();
472 Ok(())
473 }
474
475 /// Displays active Jira inbox items (pinned, score, then priority).
476 ///
477 /// SUMMARY is truncated to fit the terminal width (same approach as [`View::tasks`]).
478 pub fn jira_inbox(items: &[crate::db::jira_inbox::JiraInboxItem]) -> Result<()> {
479 let pin_width = "★".width().max(1);
480 let score_width = items
481 .iter()
482 .map(|i| i.sort_value.map(|v| format!("{}", v).width()).unwrap_or_else(|| "—".width()))
483 .max()
484 .unwrap_or(1)
485 .max("SCORE".width());
486 let priority_width = items
487 .iter()
488 .map(|i| i.priority.as_deref().unwrap_or("—").width())
489 .max()
490 .unwrap_or(1)
491 .max("PRIORITY".width());
492 let key_width = items.iter().map(|i| i.issue_key.width()).max().unwrap_or(1).max("KEY".width());
493 let status_width = items
494 .iter()
495 .map(|i| {
496 if i.status_name.is_empty() {
497 i.status_id.as_deref().unwrap_or("—").width()
498 } else {
499 i.status_name.width()
500 }
501 })
502 .max()
503 .unwrap_or(1)
504 .max("STATUS".width())
505 .min(18);
506
507 // "", SCORE, PRIORITY, KEY, STATUS, SUMMARY
508 let num_cols = 6;
509 let frame_overhead = 3 * num_cols + 1;
510 let fixed = pin_width + score_width + priority_width + key_width + status_width;
511 let summary_width = terminal_cols().saturating_sub(frame_overhead + fixed).max(12);
512
513 let mut table = Table::new();
514 table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
515 table.set_titles(row!["", "SCORE", "PRIORITY", "KEY", "STATUS", "SUMMARY"]);
516
517 for item in items {
518 let pin = if item.pinned { "★" } else { "" };
519 let score = item.sort_value.map(|v| format!("{}", v)).unwrap_or_else(|| "—".to_string());
520 let status_raw = if item.status_name.is_empty() {
521 item.status_id.as_deref().unwrap_or("—")
522 } else {
523 item.status_name.as_str()
524 };
525 table.add_row(row![
526 pin,
527 score,
528 item.priority.as_deref().unwrap_or("—"),
529 item.issue_key,
530 truncate_to_width(status_raw, status_width),
531 truncate_to_width(&item.summary, summary_width),
532 ]);
533 }
534
535 table.printstd();
536 Ok(())
537 }
538}