use askama::Template; use chrono::{NaiveDateTime, Utc}; use csv::ReaderBuilder; use std::collections::HashMap; use std::env; use std::fs; use std::path::Path; mod invoice_generator; use invoice_generator::{group_by_customer, read_csv_file, Customer}; fn fmt(v: f64) -> String { format!("{:.2}", v) } fn clean_csv_file( input_path: &Path, output_path: &Path, ) -> Result> { let file = fs::File::open(input_path)?; let mut rdr = ReaderBuilder::new() .delimiter(b'\t') .has_headers(true) .flexible(true) .from_reader(file); let output = fs::File::create(output_path)?; let mut writer = csv::WriterBuilder::new() .delimiter(b'\t') .from_writer(output); let mut batch_number = String::new(); for result in rdr.records() { let record = result?; let date_str = record.get(0).unwrap_or(""); let batch = record.get(1).unwrap_or("").to_string(); if batch_number.is_empty() { batch_number = batch.clone(); } let date = NaiveDateTime::parse_from_str(date_str, "%m/%d/%Y %I:%M:%S %p").unwrap_or_else(|_| { NaiveDateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S").unwrap_or_default() }); let row = vec![ date.format("%Y-%m-%d %H:%M:%S").to_string(), batch, record.get(2).unwrap_or("").to_string(), record.get(3).unwrap_or("").to_string(), record.get(4).unwrap_or("").to_string(), record.get(5).unwrap_or("").to_string(), record.get(6).unwrap_or("").to_string(), record.get(7).unwrap_or("").to_string(), record.get(8).unwrap_or("").to_string(), record.get(9).unwrap_or("").to_string(), record.get(10).unwrap_or("").to_string(), record.get(11).unwrap_or("").to_string(), record.get(12).unwrap_or("").to_string(), record.get(13).unwrap_or("").to_string(), record.get(14).unwrap_or("").to_string(), record.get(15).unwrap_or("").to_string(), ]; writer.write_record(&row)?; } writer.flush()?; Ok(batch_number) } #[derive(Clone)] struct ProductSummary { name: String, volume: String, amount: String, avg_price: String, } #[derive(Clone)] struct Summary { total_volume: String, grand_total: String, products: Vec, } #[derive(Clone)] struct CardData { card_number: String, transactions: Vec, total_amount: String, total_volume: String, } #[derive(Clone)] struct FormattedTransaction { date: String, quality_name: String, price: String, volume: String, amount: String, receipt: String, } #[derive(Clone)] struct PreparedCustomer { customer_number: String, cards: Vec, summary: Summary, } impl PreparedCustomer { fn from_customer(customer: Customer) -> Self { let cards: Vec = customer .cards .into_iter() .map(|(card_number, transactions)| { let formatted_txs: Vec = transactions .into_iter() .map(|t| FormattedTransaction { date: t.date.format("%Y-%m-%d %H:%M").to_string(), quality_name: t.quality_name, price: fmt(t.price * 0.8), volume: fmt(t.volume), amount: fmt(t.amount * 0.8), receipt: t.receipt, }) .collect(); let total_amount: f64 = formatted_txs .iter() .map(|t| t.amount.parse::().unwrap()) .sum(); let total_volume: f64 = formatted_txs .iter() .map(|t| t.volume.parse::().unwrap()) .sum(); CardData { card_number, transactions: formatted_txs, total_amount: fmt(total_amount), total_volume: fmt(total_volume), } }) .collect(); let grand_total: f64 = cards .iter() .map(|c| c.total_amount.parse::().unwrap()) .sum(); let mut product_totals: HashMap = HashMap::new(); for card in &cards { for tx in &card.transactions { let volume: f64 = tx.volume.parse().unwrap(); let amount: f64 = tx.amount.parse::().unwrap(); let entry = product_totals .entry(tx.quality_name.clone()) .or_insert((0.0, 0.0)); entry.0 += volume; entry.1 += amount; } } let mut products: Vec = product_totals .into_iter() .map(|(name, (volume, amount))| { let avg_price = if volume > 0.0 { amount / volume } else { 0.0 }; ProductSummary { name, volume: fmt(volume), amount: fmt(amount), avg_price: fmt(avg_price), } }) .collect(); products.sort_by(|a, b| a.name.cmp(&b.name)); let total_volume: f64 = products .iter() .map(|p| p.volume.parse::().unwrap()) .sum(); let summary = Summary { total_volume: fmt(total_volume), grand_total: fmt(grand_total), products, }; PreparedCustomer { customer_number: customer.customer_number, cards, summary, } } } #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate { customers: Vec<(String, usize)>, period: String, } #[derive(Template)] #[template(path = "customer.html")] struct CustomerTemplate { customer: PreparedCustomer, period: String, generated_date: String, } fn main() -> Result<(), Box> { let args: Vec = env::args().collect(); if args.len() != 3 { eprintln!("Användning: {} ", args[0]); std::process::exit(1); } let input_path = Path::new(&args[1]); let base_output_dir = Path::new(&args[2]); if !input_path.exists() { eprintln!("Fel: Filen hittades inte: {:?}", input_path); std::process::exit(1); } let filename = input_path .file_name() .and_then(|n| n.to_str()) .unwrap_or("unknown") .to_string(); println!("Konverterar {} till rensat format...", filename); let temp_cleaned_path = base_output_dir.join(format!("{}.temp.csv", filename.trim_end_matches(".txt"))); let batch_number = clean_csv_file(input_path, &temp_cleaned_path)?; let output_dir = base_output_dir.join(&batch_number); fs::create_dir_all(&output_dir)?; fs::copy(input_path, output_dir.join(format!("{}.txt", batch_number)))?; fs::rename( &temp_cleaned_path, output_dir.join(format!("{}.csv", batch_number)), )?; println!( "Konverterade {} transaktioner", fs::read_to_string(output_dir.join(format!("{}.csv", batch_number)))? .lines() .count() - 1 ); let batch = read_csv_file(&output_dir.join(format!("{}.csv", batch_number)))?; println!("Laddade {} transaktioner", batch.transactions.len()); let first_date = batch.transactions.first().map(|t| t.date).unwrap(); let last_date = batch.transactions.last().map(|t| t.date).unwrap(); let period = format!( "{} - {}", first_date.format("%Y-%m-%d"), last_date.format("%Y-%m-%d") ); let customers = group_by_customer(&[batch]); let index_customers: Vec<(String, usize)> = customers .iter() .map(|(num, c)| (num.clone(), c.cards.len())) .collect(); let html = IndexTemplate { customers: index_customers.clone(), period: period.clone(), } .render() .unwrap(); fs::write(output_dir.join("index.html"), html)?; let generated_date = Utc::now().format("%Y-%m-%d %H:%M").to_string(); let customer_count = customers.len(); for (customer_num, customer) in customers { let prepared = PreparedCustomer::from_customer(customer); let customer_html = CustomerTemplate { customer: prepared, period: period.clone(), generated_date: generated_date.clone(), } .render() .unwrap(); let filename = format!("customer_{}.html", customer_num); fs::write(output_dir.join(&filename), customer_html)?; println!("Genererade {}", filename); } println!( "\nGenererade {} kundfakturor i {:?}", customer_count, output_dir ); Ok(()) }