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