kasl/commands/export.rs
1//! Data export command for external analysis and backup.
2//!
3//! Provides comprehensive data export functionality supporting multiple output formats and data types for external analysis, backup, and integration.
4//!
5//! ## Features
6//!
7//! - **Export Formats**: CSV, JSON, Excel with formatting and multiple sheets
8//! - **Data Types**: Reports, tasks, summaries, and complete data export
9//! - **Flexible Output**: Custom file paths and automatic naming
10//! - **Date Filtering**: Export data for specific date ranges
11//!
12//! ## Usage
13//!
14//! ```bash
15//! # Export tasks to CSV
16//! kasl export tasks --format csv
17//!
18//! # Export today's report to Excel
19//! kasl export report --format xlsx
20//!
21//! # Export with custom filename
22//! kasl export tasks --format json --output my_tasks.json
23//! ```
24
25use crate::{
26 libs::{
27 config::Config,
28 export::{ExportData, ExportFormat, Exporter},
29 messages::Message,
30 },
31 msg_info,
32};
33use anyhow::Result;
34use chrono::{Local, NaiveDate};
35use clap::Args;
36use std::path::PathBuf;
37
38/// Command-line arguments for the export command.
39///
40/// The export command provides flexible options for data extraction,
41/// supporting different formats, data types, and output destinations.
42#[derive(Debug, Args)]
43pub struct ExportArgs {
44 /// Type of data to export
45 ///
46 /// Specifies which category of information to include in the export:
47 /// - **report**: Daily work report with intervals and productivity
48 /// - **tasks**: Task records with completion status and metadata
49 /// - **summary**: Monthly summary with aggregate statistics
50 /// - **all**: Complete data export including all available information
51 ///
52 /// Each data type provides different levels of detail and is suitable
53 /// for different analysis purposes.
54 #[arg(value_enum, default_value = "report")]
55 data: ExportData,
56
57 /// Output format for the exported data
58 ///
59 /// Controls the structure and format of the exported file:
60 /// - **csv**: Comma-separated values, compatible with Excel and other spreadsheet tools
61 /// - **json**: Structured JSON data, ideal for programmatic processing
62 /// - **excel**: Native Excel format with formatting, charts, and multiple worksheets
63 ///
64 /// Format selection affects both file structure and available features.
65 #[arg(short, long, value_enum, default_value = "csv")]
66 format: ExportFormat,
67
68 /// Custom output file path
69 ///
70 /// When specified, the export will be saved to this exact location.
71 /// If not provided, a default filename will be generated based on:
72 /// - Current timestamp for uniqueness
73 /// - Selected data type for clarity
74 /// - Chosen format for proper file extension
75 ///
76 /// Example default: `kasl_export_20250115_143022.csv`
77 #[arg(short, long)]
78 output: Option<PathBuf>,
79
80 /// Target date for data export
81 ///
82 /// Specifies which date's data to export. Accepts:
83 /// - `today`: Current date (default)
84 /// - `YYYY-MM-DD`: Specific date in ISO format
85 ///
86 /// For summary exports, this determines the month to summarize.
87 /// For daily reports and tasks, this specifies the exact date.
88 #[arg(short, long, default_value = "today")]
89 date: String,
90
91 /// Render the daily report as an hourly (SiServer-style) breakdown
92 ///
93 /// When enabled, the report is exported as a per-hour grid: each row
94 /// represents one hour of the workday with a description of the work
95 /// performed, and "Перерыв" is written for hours (or parts of hours) that
96 /// fall within a break or pause.
97 ///
98 /// This option only affects Excel report exports (`report --format excel`);
99 /// it is ignored for other data types and formats.
100 #[arg(long)]
101 hourly: bool,
102}
103
104/// Executes the data export command.
105///
106/// Orchestrates the complete export process including date parsing, exporter
107/// initialization, data processing, file generation, and user feedback.
108/// - Data format conversion errors
109/// - Output file write failures
110///
111/// # Arguments
112///
113/// * `args` - Parsed command-line arguments specifying export parameters
114///
115/// # Returns
116///
117/// Returns `Ok(())` on successful export completion, or an error if
118/// any step in the export process fails.
119///
120/// # Examples
121///
122/// ```bash
123/// # Export today's report as CSV
124/// kasl export report --format csv
125///
126/// # Export tasks from specific date as JSON
127/// kasl export tasks --format json --date 2025-01-15
128///
129/// # Export monthly summary to Excel with custom filename
130/// kasl export summary --format excel --output monthly_report.xlsx
131///
132/// # Export all data for backup purposes
133/// kasl export all --format json --output backup_2025_01.json
134/// ```
135///
136/// # Output Files
137///
138/// Generated files include:
139/// - **Metadata**: Export timestamp, data range, format version
140/// - **Data Records**: Requested information in chosen format
141/// - **Summary Statistics**: Totals, averages, and key metrics
142/// - **Format-Specific Features**: Charts (Excel), structured nesting (JSON)
143pub async fn cmd(args: ExportArgs) -> Result<()> {
144 let date = parse_date(&args.date)?;
145
146 msg_info!(Message::ExportingData(format!("{:?}", args.data), format!("{:?}", args.format)));
147
148 // Resolve the output path: an explicit --output always wins; otherwise, for
149 // report exports, fall back to the configured directory and file-name template.
150 let output = match args.output.clone() {
151 Some(path) => Some(path),
152 None => resolve_report_output(args.data, args.format, date)?,
153 };
154
155 // Initialize exporter with format and output configuration
156 let exporter = Exporter::new(args.format, output).hourly(args.hourly);
157
158 // Delegate to appropriate export handler based on data type
159 exporter.export(args.data, date).await?;
160
161 Ok(())
162}
163
164/// Resolves a default output path for report exports from configuration.
165///
166/// This is only applied to [`ExportData::Report`] exports when no explicit
167/// `--output` was provided and a report output directory is configured. The
168/// file name is built from the configured template (defaulting to
169/// `daily_report_{date}{seq}`), where `{date}` is the report date and `{seq}`
170/// is a per-day sequence suffix (empty for the first file of the day, then
171/// `_2`, `_3`, … for subsequent files). The chosen path is guaranteed not to
172/// overwrite an existing file.
173///
174/// Returns `Ok(None)` to defer to the exporter's built-in default naming when
175/// the export is not a report or no report directory is configured.
176fn resolve_report_output(data: ExportData, format: ExportFormat, date: NaiveDate) -> Result<Option<PathBuf>> {
177 if !matches!(data, ExportData::Report) {
178 return Ok(None);
179 }
180
181 let report_config = match Config::read()?.report {
182 Some(config) => config,
183 None => return Ok(None),
184 };
185
186 let output_dir = match report_config.output_dir {
187 Some(dir) if !dir.trim().is_empty() => PathBuf::from(dir),
188 _ => return Ok(None),
189 };
190
191 std::fs::create_dir_all(&output_dir)?;
192
193 let template = report_config
194 .filename_template
195 .filter(|t| !t.trim().is_empty())
196 .unwrap_or_else(|| "daily_report_{date}{seq}".to_string());
197
198 let extension = match format {
199 ExportFormat::Csv => "csv",
200 ExportFormat::Json => "json",
201 ExportFormat::Excel => "xlsx",
202 };
203 let date_str = date.format("%Y-%m-%d").to_string();
204
205 // Pick the first non-existing file for the day, appending _2, _3, … as needed.
206 for sequence in 1.. {
207 let seq_suffix = if sequence == 1 { String::new() } else { format!("_{}", sequence) };
208 let stem = template.replace("{date}", &date_str).replace("{seq}", &seq_suffix);
209 let candidate = output_dir.join(format!("{}.{}", stem, extension));
210 if !candidate.exists() {
211 return Ok(Some(candidate));
212 }
213 }
214
215 unreachable!("sequence iterator is unbounded")
216}
217
218/// Parses a date string supporting both 'today' and ISO format.
219///
220/// This utility function provides consistent date parsing across the export
221/// command, handling both user-friendly keywords and explicit date specifications.
222///
223/// ## Supported Formats
224///
225/// - **today** (case-insensitive): Returns current local date
226/// - **YYYY-MM-DD**: ISO 8601 date format (e.g., 2025-01-15)
227///
228/// ## Use Cases
229///
230/// Different date specifications serve different purposes:
231/// - `today`: Quick exports of current work data
232/// - Specific dates: Historical analysis, backup creation, data migration
233/// - Recent dates: Weekly or monthly review processes
234///
235/// # Arguments
236///
237/// * `date_str` - Date string to parse, either 'today' or 'YYYY-MM-DD'
238///
239/// # Returns
240///
241/// Returns the parsed `NaiveDate` on success, or an error if the date
242/// string is malformed or represents an invalid date.
243///
244/// # Error Scenarios
245///
246/// - Malformed date strings (e.g., `2025-13-45`, `invalid-date`)
247/// - Wrong date formats (e.g., `01/15/2025`, `15-01-2025`)
248/// - Non-existent dates (e.g., `2025-02-30`, `2025-04-31`)
249/// - Out-of-range values (e.g., month > 12, day > 31)
250///
251/// # Examples
252///
253/// ```text
254/// let today = parse_date("today")?; // Current date
255/// let christmas = parse_date("2025-12-25")?; // Specific holiday
256/// let start_year = parse_date("2025-01-01")?; // Year beginning
257/// ```
258fn parse_date(date_str: &str) -> Result<NaiveDate> {
259 if date_str.to_lowercase() == "today" {
260 Ok(Local::now().date_naive())
261 } else {
262 Ok(NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?)
263 }
264}