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 }
88 }
89 Ok(())
90}
91
92pub(crate) fn validate_time_logs(
94 time_logs: &[TimeLog],
95 show_validation_details: bool,
96 start_date: Option<chrono::NaiveDate>,
97 max_hours: u16,
98) {
99 let mut validator = TimeLogValidator::new()
100 .with_validator(gitlab_time_report::validation::ExcessiveHoursValidator::new(max_hours))
101 .with_validator(gitlab_time_report::validation::HasSummaryValidator)
102 .with_validator(gitlab_time_report::validation::NoFutureDateValidator)
103 .with_validator(gitlab_time_report::validation::DuplicatesValidator::new());
104
105 if let Some(start_date) = start_date {
106 validator = validator.with_validator(
107 gitlab_time_report::validation::BeforeStartDateValidator::new(start_date),
108 );
109 }
110
111 let results = validator.validate(time_logs);
112 let number_of_problems = results.iter().filter(|r| !r.is_valid()).count();
113
114 if !show_validation_details {
116 match number_of_problems {
117 0 => println!("\nNo problems found in the time logs of the project."),
118 _ => println!(
119 "\n{number_of_problems} problems found in the time logs of the project. To see the problems, run with --validation-details",
120 ),
121 }
122 return;
123 }
124
125 println!(
126 "\
127=============================================
128 Time Logs with validation problems
129============================================="
130 );
131
132 for result in results {
133 if result.is_valid() {
134 continue;
135 }
136
137 let time_log = result.time_log;
138 let trackable_item_text = match &time_log.trackable_item.kind {
139 TrackableItemKind::Issue(_) => "Issue #",
140 TrackableItemKind::MergeRequest(_) => "Merge Request !",
141 };
142
143 println!(
144 "{trackable_item_text}{}: {}",
145 time_log.trackable_item.common.id, time_log.trackable_item.common.title,
146 );
147 println!(
148 "{}, ({}), {}: {}",
149 time_log.spent_at.date_naive(),
150 time_log.time_spent.to_hm_string(),
151 time_log.user.name,
152 time_log.summary.as_deref().unwrap_or_default()
153 );
154
155 for problem in &result.problems {
156 match problem {
157 ValidationProblem::ExcessiveHours { max_hours } => {
158 println!("Time spent exceeds maximum of {max_hours} hours");
159 }
160 ValidationProblem::MissingSummary => println!("No summary was entered"),
161 ValidationProblem::FutureDate => println!("Date is in the future"),
162 ValidationProblem::DuplicateEntry => println!("Duplicate entry"),
163 ValidationProblem::BeforeStartDate { start_date } => println!(
164 "Date is before project start date: {}",
165 start_date.format("%Y-%m-%d")
166 ),
167 }
168 println!();
169 }
170 }
171 println!(
172 "\
173=============================================
174Total Problems found: {number_of_problems}
175=============================================\n",
176 );
177}
178
179pub(crate) fn create_charts(
181 time_logs: &[TimeLog],
182 options: &arguments::ChartOptionsArgs,
183 selected_labels: Option<&HashSet<String>>,
184 other_label: Option<&Label>,
185 repository_name: &str,
186) -> Result<(), ChartSettingError> {
187 let burndown_options = charts::BurndownOptions::new(
188 time_logs,
189 options.weeks_per_sprint,
190 options.sprints,
191 options.hours_per_person,
192 options.start_date,
193 )?;
194 let mut render_options = charts::RenderOptions::new(
195 options.width,
196 options.height,
197 options.theme_json.as_deref(),
198 &options.output,
199 repository_name,
200 )?;
201
202 println!("Creating Bar Chart for Hours spent by Users...");
203 let by_user: BTreeMap<_, _> = filters::group_by_user(time_logs).collect();
204 charts::create_bar_chart(
205 by_user.clone(),
206 "Hours spent by Users",
207 "Users",
208 &mut render_options,
209 )?;
210
211 println!("Creating Burndown Charts...");
212 charts::create_burndown_chart(
213 time_logs,
214 &BurndownType::PerPerson,
215 &burndown_options,
216 &mut render_options,
217 )?;
218 charts::create_burndown_chart(
219 time_logs,
220 &BurndownType::Total,
221 &burndown_options,
222 &mut render_options,
223 )?;
224
225 println!("Creating Bar Chart for Hours spent by Milestones...");
226 let by_milestone = filters::group_by_milestone(time_logs).collect();
227 charts::create_bar_chart::<Milestone>(
228 by_milestone,
229 "Hours spent by Milestones",
230 "Milestones",
231 &mut render_options,
232 )?;
233
234 println!("Creating Pie Chart for Hours spent by Labels...");
235 let by_label: BTreeMap<_, _> =
236 filters::group_by_label(time_logs, selected_labels, other_label).collect();
237 charts::create_pie_chart(
238 by_label.clone(),
239 "Hours spent by Labels",
240 &mut render_options,
241 )?;
242
243 println!("Creating Bar Chart for Hours spent by Label and User...");
244 let by_label_and_user = by_label
245 .clone()
246 .into_iter()
247 .map(|(label, logs)| {
248 let time_by_label = filters::group_by_user(logs).collect();
249 (label, time_by_label)
250 })
251 .collect::<BTreeMap<Option<_>, BTreeMap<_, Vec<_>>>>();
252 charts::create_grouped_bar_chart(
253 by_label_and_user,
254 "Hours spent by Label and User",
255 50.0,
256 &mut render_options,
257 )?;
258
259 println!("Creating Bar Chart of Estimates and actual time by Labels...");
260 charts::create_estimate_chart::<Label>(
261 by_label,
262 "Estimates and actual time per Label",
263 &mut render_options,
264 )?;
265
266 println!("Creating Pie Chart for Hours spent by Issue or Merge Request...");
267 let by_type: BTreeMap<_, _> = filters::group_by_type(time_logs).collect();
268 let by_type_refs: BTreeMap<_, _> = by_type.iter().map(|(k, v)| (k, v.clone())).collect();
269 charts::create_pie_chart(
270 by_type_refs.clone(),
271 "Hours spent by Issue or Merge Request",
272 &mut render_options,
273 )?;
274
275 println!("Creating Bar Chart for Hours spent by Issue or Merge Request and User...");
276 let by_type_and_user = by_type_refs
277 .iter()
278 .map(|(kind, logs)| {
279 let time_by_label = filters::group_by_user(logs.clone()).collect();
280 (kind, time_by_label)
281 })
282 .collect::<BTreeMap<_, BTreeMap<_, Vec<_>>>>();
283 charts::create_grouped_bar_chart(
284 by_type_and_user,
285 "Hours spent by Issue or Merge Request and User",
286 0.0,
287 &mut render_options,
288 )?;
289
290 println!(
291 "Charts successfully created in directory '{}'.",
292 options.output.display()
293 );
294
295 Ok(())
296}
297
298pub(crate) fn create_label_options(
302 labels: Vec<String>,
303) -> (Option<HashSet<String>>, Option<Label>) {
304 let selected_labels = match labels.is_empty() {
305 true => None,
306 false => Some(HashSet::from_iter(labels)),
307 };
308
309 let other_label = selected_labels.as_ref().map(|_| Label {
311 title: "Others".to_string(),
312 });
313
314 (selected_labels, other_label)
315}