Skip to main content

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}