From de9dd7a49a1f0d6d3c14b384cb7d507384350c50 Mon Sep 17 00:00:00 2001 From: Jelson Rodrigues Date: Wed, 5 Nov 2025 08:15:49 -0300 Subject: [PATCH] feat: group repports in a week --- src/groupped_repport.rs | 155 ++++++++++++++++++++++++-------------- src/send_mail_util.rs | 46 +++++++++++ src/zip_directory_util.rs | 70 +++++++++++++++++ 3 files changed, 215 insertions(+), 56 deletions(-) create mode 100644 src/send_mail_util.rs create mode 100644 src/zip_directory_util.rs diff --git a/src/groupped_repport.rs b/src/groupped_repport.rs index 2308c99..54072fa 100644 --- a/src/groupped_repport.rs +++ b/src/groupped_repport.rs @@ -1,17 +1,16 @@ use std::fmt::Debug; -use chrono::Datelike; use itertools::Itertools; -use polars::prelude::buffer::validate_utf8; use polars::prelude::*; use reqwest; -use serde::Deserialize; use std::env; use std::time::Duration; -use walkdir; use csv; +pub mod send_mail_util; +pub mod zip_directory_util; + #[derive(Debug, serde::Deserialize)] struct CsvHeader { CATEGORIA: String, @@ -32,14 +31,6 @@ struct CsvEvaluation { ID_TALK: String, } -// impl TryFrom::> for CsvEvaluation { -// type Error = &'static str; - -// fn try_from(value: csv::DeserializeRecordsIter<&[u8], CsvHeader>) -> Result { -// todo!() -// } -// } - fn main() { match dotenv::dotenv().ok() { Some(_) => println!("Environment variables loaded from .env file"), @@ -54,6 +45,9 @@ fn main() { .unwrap_or(11432); let OLLAMA_AI_MODEL_DATA_SANITIZATION = env::var("OLLAMA_AI_MODEL_DATA_SANITIZATION") .expect("Missing environment variable OLLAMA_AI_MODEL_DATA_SANITIZATION"); + let BOT_EMAIL = env::var("BOT_EMAIL").expect("BOT_EMAIL has not been set!"); + let BOT_EMAIL_PASSWORD = + env::var("BOT_EMAIL_PASSWORD").expect("BOT_EMAIL_PASSWORD has not been set!"); let ip_address = ipaddress::IPAddress::parse(OLLAMA_URL.to_string()); let OLLAMA_SANITIZED_IP = match ip_address { @@ -71,6 +65,21 @@ fn main() { let current_date = chrono::Local::now(); let formatted_date = current_date.format("%Y-%m-%d").to_string(); + let current_date = chrono::Local::now(); + let first_day_of_current_week = current_date + .date_naive() + .week(chrono::Weekday::Sun) + .first_day(); + let current_date_minus_one_week = first_day_of_current_week + .checked_sub_days(chrono::Days::new(1)) + .expect("Failed to subtract one day"); + let first_day_of_last_week = current_date_minus_one_week + .week(chrono::Weekday::Sun) + .first_day(); + let last_day_of_last_week = current_date_minus_one_week + .week(chrono::Weekday::Sun) + .last_day(); + let previous_week_folder_names = std::fs::read_dir(std::path::Path::new("./evaluations")) .expect("Failed to read directory ./evaluations") .filter_map_ok(|entry| { @@ -98,17 +107,6 @@ fn main() { }; }) .filter_map_ok(|(week, directory_string)| { - let current_date = chrono::Local::now(); - let first_day_of_current_week = current_date - .date_naive() - .week(chrono::Weekday::Sun) - .first_day(); - let current_date_minus_one_week = first_day_of_current_week - .checked_sub_days(chrono::Days::new(1)) - .expect("Failed to subtract one day"); - let first_day_of_last_week = current_date_minus_one_week - .week(chrono::Weekday::Sun) - .first_day(); let first_day_of_week_in_folder_name = week.first_day(); if first_day_of_last_week == first_day_of_week_in_folder_name { @@ -132,14 +130,13 @@ fn main() { .expect("Failed to read PROMPT_DATA_SANITIZATION.txt"); let client = reqwest::blocking::Client::new(); - let a = previous_week_folder_names + let groupped_values = previous_week_folder_names .iter() .map(|folder_name| { let folder_base_path = std::path::Path::new("./evaluations"); let folder_date_path = folder_base_path.join(folder_name); std::fs::read_dir(folder_date_path) }) - .take(1) .filter_map_ok(|files_inside_folder_on_date| { let groupped_by_user_on_day = files_inside_folder_on_date .filter_ok(|entry| { @@ -157,7 +154,6 @@ fn main() { } None }) - // .take(1) .map(|file_name_csv| { println!("{:?}", file_name_csv.path()); let file_contents = std::fs::read_to_string(file_name_csv.path()) @@ -271,9 +267,6 @@ fn main() { let df = polars::frame::DataFrame::new(columns) .expect("Failed to concatenate into a dataframe"); - // println!("{:?}", df); - // Create a dataframe with the evaluation columns plus the talk id - // return a tuple with the dataframe and the user name, so it can be correctly merged after return Some((user_name.as_str().to_owned(), df)); }) @@ -293,45 +286,95 @@ fn main() { .expect("Failed to concatenate dataframes"); (name, groupped_df) }) - .collect_vec(); + .into_group_map(); + dbg!(&groupped_by_user_on_day); return Some(groupped_by_user_on_day); }) .filter_map(|res| { - if res.is_ok() { - return Some(res.unwrap()); - } - return None; - }) - .into_group_map() - .into_iter() - .map(|(name, eval_dataframe_vec)| { - let groupped_df = eval_dataframe_vec + if res.is_ok() { + return Some(res.unwrap()); + } + return None; + }) + .reduce(|mut acc, mut e| { + e.iter_mut().for_each(|(key, val)| { + if acc.contains_key(key) { + acc.get_mut(key) + .expect("Failed to obtain key that should already be present") + .append(val); + } else { + acc.insert(key.to_owned(), val.to_owned()); + } + }); + acc + }) + .and_then(|groupped_hashmap_df| { + let result = groupped_hashmap_df + .iter() + .map(|(key, val)| { + let dfs = val .iter() .cloned() .reduce(|acc, e| acc.vstack(&e).unwrap()) .expect("Failed to concatenate dataframes"); - (name, groupped_df) + (key.clone(), dfs) }) - // .collect_vec(); - - .collect_vec(); + .collect_vec(); + return Some(result); + }); - println!("{:?}", a); + // Setup groupped folder + if !std::fs::exists(format!("./groupped/")).unwrap() { + std::fs::create_dir(format!("./groupped")).expect("Failed to create directory") + } - // Read CSV files inside folder + // Setup previous week folder + if !std::fs::exists(format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}" + )) + .unwrap() + { + std::fs::create_dir(format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}" + )) + .expect("Failed to create directory") + } - // Use AI to sanitize the data + match groupped_values { + Some(mut val) => { + val.iter_mut().for_each(|(agent, groupped_evaluations)| { + let mut save_file_csv = std::fs::File::create(format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}/{agent}" + )) + .expect("Could not create csv file for saving"); + CsvWriter::new(&mut save_file_csv) + .include_header(true) + .with_separator(b';') + .finish(groupped_evaluations) + .expect("Failed to save Groupped DataFrame to CSV File"); + }); + } + None => {} + } - // Save into a hashmap, with the user name as key, the date, evaluation + zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file( + std::path::Path::new(&format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}" + )), + std::path::Path::new(&format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}.zip" + )), + ); - // Final file should look like - /* - Header: Att1, att2, att3, ... - categoria1 - categoria2 - categoria3 - ... - - */ + let recipients = "Wilson da Conceição Oliveira , Isadora G. Moura de Moura "; + send_mail_util::send_mail_util::send_email( + &format!( + "Relatório agrupado dos atendimentos semana {first_day_of_last_week} - {last_day_of_last_week}" + ), + &BOT_EMAIL, + &BOT_EMAIL_PASSWORD, + recipients, + &format!("./groupped/{first_day_of_last_week} - {last_day_of_last_week}.zip"), + ); } diff --git a/src/send_mail_util.rs b/src/send_mail_util.rs new file mode 100644 index 0000000..e6dbb28 --- /dev/null +++ b/src/send_mail_util.rs @@ -0,0 +1,46 @@ +pub mod send_mail_util { + use lettre::{ + Message, SmtpTransport, Transport, + message::{self, Attachment, Mailboxes, MultiPart, SinglePart, header::ContentType}, + }; + + pub fn send_email( + subject_of_email: &str, + bot_email: &str, + bot_email_password: &str, + to: &str, + zip_file_name: &str, + ) { + let filebody = std::fs::read(zip_file_name).unwrap(); + let content_type = ContentType::parse("application/zip").unwrap(); + let attachment = Attachment::new(zip_file_name.to_string()).body(filebody, content_type); + let mailboxes: Mailboxes = to.parse().unwrap(); + let to_header: message::header::To = mailboxes.into(); + + let email = Message::builder() + .from(format!("PipeRUN bot <{bot_email}>").parse().unwrap()) + .reply_to(format!("PipeRUN bot <{bot_email}>").parse().unwrap()) + .mailbox(to_header) + .subject(format!("{subject_of_email}")) + .multipart( + MultiPart::mixed() + .singlepart( + SinglePart::builder() + .header(ContentType::TEXT_PLAIN) + .body(String::from("Avaliacao dos atendimentos")), + ) + .singlepart(attachment), + ) + .unwrap(); + + // Create the SMTPS transport + let sender = SmtpTransport::from_url(&format!( + "smtps://{bot_email}:{bot_email_password}@mail.nova.net.br" + )) + .unwrap() + .build(); + + // Send the email via remote relay + sender.send(&email).unwrap(); + } +} diff --git a/src/zip_directory_util.rs b/src/zip_directory_util.rs new file mode 100644 index 0000000..d5fdc51 --- /dev/null +++ b/src/zip_directory_util.rs @@ -0,0 +1,70 @@ +pub mod zip_directory_util { + +use std::io::prelude::*; +use zip::write::SimpleFileOptions; + +use std::fs::File; +use std::path::Path; +use walkdir::{DirEntry, WalkDir}; + +fn zip_dir( + it: &mut dyn Iterator, + prefix: &Path, + writer: T, + method: zip::CompressionMethod, +) where + T: Write + Seek, +{ + let mut zip = zip::ZipWriter::new(writer); + let options = SimpleFileOptions::default() + .compression_method(method) + .unix_permissions(0o755); + + let prefix = Path::new(prefix); + let mut buffer = Vec::new(); + for entry in it { + let path = entry.path(); + let name = path.strip_prefix(prefix).unwrap(); + let path_as_string = name + .to_str() + .map(str::to_owned) + .expect("Failed to parse path"); + + // Write file or directory explicitly + // Some unzip tools unzip files with directory paths correctly, some do not! + if path.is_file() { + println!("adding file {path:?} as {name:?} ..."); + zip.start_file(path_as_string, options) + .expect("Failed to add file"); + let mut f = File::open(path).unwrap(); + + f.read_to_end(&mut buffer).expect("Failed to read file"); + zip.write_all(&buffer).expect("Failed to write file"); + buffer.clear(); + } else if !name.as_os_str().is_empty() { + // Only if not root! Avoids path spec / warning + // and mapname conversion failed error on unzip + println!("adding dir {path_as_string:?} as {name:?} ..."); + zip.add_directory(path_as_string, options) + .expect("Failed to add directory"); + } + } + zip.finish().expect("Failed to ZIP"); +} + +pub fn zip_source_dir_to_dst_file(src_dir: &Path, dst_file: &Path) { + if !Path::new(src_dir).is_dir() { + panic!("src_dir must be a directory"); + } + + let method = zip::CompressionMethod::Stored; + let path = Path::new(dst_file); + let file = File::create(path).unwrap(); + + let walkdir = WalkDir::new(src_dir); + let it = walkdir.into_iter(); + + zip_dir(&mut it.filter_map(|e| e.ok()), src_dir, file, method); +} + +} \ No newline at end of file