1#![cfg(not(tarpaulin_include))]
5mod arguments;
6mod fetch_projects;
7mod print_table;
8
9use arguments::Command;
10use chrono::NaiveDate;
11use clap::Parser;
12use gitlab_time_report::charts::{BurndownType, ChartSettingError};
13use gitlab_time_report::dashboard::create_html;
14use gitlab_time_report::model::{Label, Milestone, TimeLog, TrackableItemKind};
15use gitlab_time_report::validation::{TimeLogValidator, ValidationProblem};
16use gitlab_time_report::{TimeDeltaExt, charts, create_csv, filters};
17use std::collections::{BTreeMap, HashSet};
18
19fn main() -> Result<(), String> {
20 let cli = arguments::Arguments::parse();
21
22 let project =
23 fetch_projects::fetch_projects(cli.url, cli.token.as_ref()).map_err(|e| e.to_string())?;
24
25 let start_date_for_validation: Option<NaiveDate> = match &cli.command {
26 Some(Command::Charts { chart_options } | Command::Dashboard { chart_options, .. }) => {
27 chart_options.start_date
28 }
29 _ => None,
30 };
31
32 validate_time_logs(
33 &project.time_logs,
34 cli.validation_details,
35 start_date_for_validation,
36 cli.validation_max_hours,
37 );
38
39 match &cli.command {
40 Some(Command::Export { output }) => {
41 create_csv(&project.time_logs, output.clone()).map_err(|e| e.to_string())?;
42 println!("CSV created at {}", output.display());
43 }
44 Some(Command::Charts { chart_options }) => {
45 let (selected_labels, other_label) = create_label_options(cli.labels);
46 create_charts(
47 &project.time_logs,
48 chart_options,
49 selected_labels.as_ref(),
50 other_label.as_ref(),
51 &project.name,
52 )
53 .map_err(|e| e.to_string())?;
54 }
55 Some(Command::Dashboard { chart_options }) => {
56 let (selected_labels, other_label) = create_label_options(cli.labels);
57 create_charts(
58 &project.time_logs,
59 chart_options,
60 selected_labels.as_ref(),
61 other_label.as_ref(),
62 &project.name,
63 )
64 .map_err(|e| e.to_string())?;
65
66 let path = create_html(
67 &project.time_logs,
68 &chart_options.output,
69 selected_labels.as_ref(),
70 other_label.as_ref(),
71 &project.name,
72 )
73 .map_err(|e| e.to_string())?;
74 println!("Dashboard '{}' successfully created.", path.display());
75 }
76 None => {
77 print_table::print_timelogs_in_timeframes_by_user(&project.time_logs);
78
79 let (selected_labels, other_label) = create_label_options(cli.labels);
80 print_table::print_total_time_by_label(
81 &project.time_logs,
82 selected_labels.as_ref(),
83 other_label.as_ref(),
84 );
85
86 print_table::print_total_time_by_milestone(&project.time_logs);
87 print_table::print_todays_timelogs(&project.time_logs);
88 }
89 }
90 Ok(())
91}
92
93pub(crate) fn validate_time_logs(
95 time_logs: &[TimeLog],
96 show_validation_details: bool,
97 start_date: Option<chrono::NaiveDate>,
98 max_hours: u16,
99) {
100 let mut validator = TimeLogValidator::new()
101 .with_validator(gitlab_time_report::validation::ExcessiveHoursValidator::new(max_hours))
102 .with_validator(gitlab_time_report::validation::HasSummaryValidator)
103 .with_validator(gitlab_time_report::validation::NoFutureDateValidator)
104 .with_validator(gitlab_time_report::validation::DuplicatesValidator::new());
105
106 if let Some(start_date) = start_date {
107 validator = validator.with_validator(
108 gitlab_time_report::validation::BeforeStartDateValidator::new(start_date),
109 );
110 }
111
112 let results = validator.validate(time_logs);
113 let number_of_problems = results.iter().filter(|r| !r.is_valid()).count();
114
115 if !show_validation_details {
117 match number_of_problems {
118 0 => println!("\nNo problems found in the time logs of the project."),
119 _ => println!(
120 "\n{number_of_problems} problems found in the time logs of the project. To see the problems, run with --validation-details",
121 ),
122 }
123 return;
124 }
125
126 println!(
127 "\
128=============================================
129 Time Logs with validation problems
130============================================="
131 );
132
133 for result in results {
134 if result.is_valid() {
135 continue;
136 }
137
138 let time_log = result.time_log;
139 let trackable_item_text = match &time_log.trackable_item.kind {
140 TrackableItemKind::Issue(_) => "Issue #",
141 TrackableItemKind::MergeRequest(_) => "Merge Request !",
142 };
143
144 println!(
145 "{trackable_item_text}{}: {}",
146 time_log.trackable_item.common.id, time_log.trackable_item.common.title,
147 );
148 println!(
149 "{}, ({}), {}: {}",
150 time_log.spent_at.date_naive(),
151 time_log.time_spent.to_hm_string(),
152 time_log.user.name,
153 time_log.summary.as_deref().unwrap_or_default()
154 );
155
156 for problem in &result.problems {
157 match problem {
158 ValidationProblem::ExcessiveHours { max_hours } => {
159 println!("Time spent exceeds maximum of {max_hours} hours");
160 }
161 ValidationProblem::MissingSummary => println!("No summary was entered"),
162 ValidationProblem::FutureDate => println!("Date is in the future"),
163 ValidationProblem::DuplicateEntry => println!("Duplicate entry"),
164 ValidationProblem::BeforeStartDate { start_date } => println!(
165 "Date is before project start date: {}",
166 start_date.format("%Y-%m-%d")
167 ),
168 }
169 println!();
170 }
171 }
172 println!(
173 "\
174=============================================
175Total Problems found: {number_of_problems}
176=============================================\n",
177 );
178}
179
180pub(crate) fn create_charts(
182 time_logs: &[TimeLog],
183 options: &arguments::ChartOptionsArgs,
184 selected_labels: Option<&HashSet<String>>,
185 other_label: Option<&Label>,
186 repository_name: &str,
187) -> Result<(), ChartSettingError> {
188 let burndown_options = charts::BurndownOptions::new(
189 time_logs,
190 options.weeks_per_sprint,
191 options.sprints,
192 options.hours_per_person,
193 options.start_date,
194 )?;
195 let mut render_options = charts::RenderOptions::new(
196 options.width,
197 options.height,
198 options.theme_json.as_deref(),
199 &options.output,
200 repository_name,
201 )?;
202
203 println!("Creating Bar Chart for Hours spent by Users...");
204 let by_user: BTreeMap<_, _> = filters::group_by_user(time_logs).collect();
205 charts::create_bar_chart(
206 by_user.clone(),
207 "Hours spent by Users",
208 "Users",
209 &mut render_options,
210 )?;
211
212 println!("Creating Burndown Charts...");
213 charts::create_burndown_chart(
214 time_logs,
215 &BurndownType::PerPerson,
216 &burndown_options,
217 &mut render_options,
218 )?;
219 charts::create_burndown_chart(
220 time_logs,
221 &BurndownType::Total,
222 &burndown_options,
223 &mut render_options,
224 )?;
225
226 println!("Creating Bar Chart for Hours spent by Milestones...");
227 let by_milestone = filters::group_by_milestone(time_logs).collect();
228 charts::create_bar_chart::<Milestone>(
229 by_milestone,
230 "Hours spent by Milestones",
231 "Milestones",
232 &mut render_options,
233 )?;
234
235 println!("Creating Pie Chart for Hours spent by Labels...");
236 let by_label: BTreeMap<_, _> =
237 filters::group_by_label(time_logs, selected_labels, other_label).collect();
238 charts::create_pie_chart(
239 by_label.clone(),
240 "Hours spent by Labels",
241 &mut render_options,
242 )?;
243
244 println!("Creating Bar Chart for Hours spent by Label and User...");
245 let by_label_and_user = by_label
246 .clone()
247 .into_iter()
248 .map(|(label, logs)| {
249 let time_by_label = filters::group_by_user(logs).collect();
250 (label, time_by_label)
251 })
252 .collect::<BTreeMap<Option<_>, BTreeMap<_, Vec<_>>>>();
253 charts::create_grouped_bar_chart(
254 by_label_and_user,
255 "Hours spent by Label and User",
256 50.0,
257 &mut render_options,
258 )?;
259
260 println!("Creating Bar Chart of Estimates and actual time by Labels...");
261 charts::create_estimate_chart::<Label>(
262 by_label,
263 "Estimates and actual time per Label",
264 &mut render_options,
265 )?;
266
267 println!("Creating Pie Chart for Hours spent by Issue or Merge Request...");
268 let by_type: BTreeMap<_, _> = filters::group_by_type(time_logs).collect();
269 let by_type_refs: BTreeMap<_, _> = by_type.iter().map(|(k, v)| (k, v.clone())).collect();
270 charts::create_pie_chart(
271 by_type_refs.clone(),
272 "Hours spent by Issue or Merge Request",
273 &mut render_options,
274 )?;
275
276 println!("Creating Bar Chart for Hours spent by Issue or Merge Request and User...");
277 let by_type_and_user = by_type_refs
278 .iter()
279 .map(|(kind, logs)| {
280 let time_by_label = filters::group_by_user(logs.clone()).collect();
281 (kind, time_by_label)
282 })
283 .collect::<BTreeMap<_, BTreeMap<_, Vec<_>>>>();
284 charts::create_grouped_bar_chart(
285 by_type_and_user,
286 "Hours spent by Issue or Merge Request and User",
287 0.0,
288 &mut render_options,
289 )?;
290
291 println!(
292 "Charts successfully created in directory '{}'.",
293 options.output.display()
294 );
295
296 Ok(())
297}
298
299pub(crate) fn create_label_options(
303 labels: Vec<String>,
304) -> (Option<HashSet<String>>, Option<Label>) {
305 let selected_labels = match labels.is_empty() {
306 true => None,
307 false => Some(HashSet::from_iter(labels)),
308 };
309
310 let other_label = selected_labels.as_ref().map(|_| Label {
312 title: "Others".to_string(),
313 });
314
315 (selected_labels, other_label)
316}