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}