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