/* * This file is part of bzkf-rwdp-check * * Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ use std::collections::HashMap; use std::error::Error; use clap::Parser; use console::{style, Term}; use csv::WriterBuilder; use itertools::Itertools; use crate::cli::{Cli, SubCommand}; use crate::common::{Check, DiffRecord, Icd10GroupSize}; use crate::database::DatabaseSource; use crate::lkrexport::{to_database_id, LkrExportProtocolFile, Meldung}; mod cli; mod common; mod database; mod lkrexport; mod opal; mod resources; fn request_password_if_none(password: Option) -> String { if let Some(password) = password { password } else { let password = dialoguer::Password::new() .with_prompt("Password") .interact() .unwrap_or_default(); let _ = Term::stdout().clear_last_lines(1); password } } fn sanitize_year(year: String) -> String { if year.len() == 4 { year } else { format!("2{:0>3}", year) } } fn print_items(items: &[Icd10GroupSize]) { let term = Term::stdout(); let _ = term.write_line( &style("Anzahl der Conditions nach ICD-10-Gruppe") .yellow() .to_string(), ); items.iter().for_each(|item| { let _ = term.write_line(&format!( "{:<20} {:<6} ={:>6}", item.name, item.schema_version.as_ref().unwrap_or(&String::new()), item.size )); }); let sum: usize = items .iter() .filter(|item| item.name != "Other") .map(|item| item.size) .sum(); let _ = term.write_line(&style("─".repeat(35)).dim().to_string()); let _ = term.write_line( &style(format!( "{:<20} {:<6} ={:>6}", "Summe (C**.*/D**.*)", "", sum )) .dim() .to_string(), ); let sum: usize = items.iter().map(|item| item.size).sum(); let _ = term.write_line( &style(format!("{:<20} {:<6} ={:>6}", "Gesamtsumme", "", sum)) .dim() .to_string(), ); let _ = term.write_line(&style("─".repeat(35)).dim().to_string()); } fn print_extern_notice(include_extern: bool) { let _ = Term::stdout().write_line( format!( "{} Die Datenbankanfrage schließt Meldungen mit externer Diagnose {}.", style("Hinweis:").bold().underlined(), match include_extern { true => style("ein").yellow(), false => style("nicht ein (Standard)").green(), } ) .as_str(), ); } fn main() -> Result<(), Box> { let term = Term::stdout(); match Cli::parse().cmd { SubCommand::OpalFile { file } => { let items = opal::OpalCsvFile::check(file.as_path()).map_err(|_e| "Kann Datei nicht lesen")?; print_items(&items); } SubCommand::Database { database, host, password, port, user, year, ignore_exports_since, include_extern, include_histo_zyto, schema_versions, } => { let password = request_password_if_none(password); let year = sanitize_year(year); let _ = term.write_line( &style(format!("Warte auf Daten für das Diagnosejahr {}...", year)) .blue() .bright() .to_string(), ); let db = DatabaseSource::new(&database, &host, &password, port, &user); let items = db .check( &year, &ignore_exports_since.unwrap_or("9999-12-31".into()), include_extern, include_histo_zyto, schema_versions, ) .map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?; let _ = term.clear_last_lines(1); print_extern_notice(include_extern); print_items(&items); } SubCommand::Export { pat_id, database, host, password, port, user, output, year, ignore_exports_since, xls_csv, include_extern, include_histo_zyto, } => { let password = request_password_if_none(password); let year = sanitize_year(year); let _ = term.write_line( &style(format!("Warte auf Daten für das Diagnosejahr {}...", year)) .blue() .bright() .to_string(), ); let db = DatabaseSource::new(&database, &host, &password, port, &user); let items = db .export( &year, &ignore_exports_since.unwrap_or("9999-12-31".into()), pat_id, include_extern, include_histo_zyto, ) .map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?; let _ = term.clear_last_lines(1); let writer_builder = &mut WriterBuilder::new(); let mut writer_builder = writer_builder.has_headers(true); if xls_csv { writer_builder = writer_builder.delimiter(b';'); } let mut writer = writer_builder .from_path(output.as_path()) .expect("writeable file"); items .iter() .for_each(|item| writer.serialize(item).unwrap()); let _ = term.write_line( &style(format!( "{} Conditions für das Jahr {} in Datei '{}' exportiert", items.len(), year, output.to_str().unwrap_or_default() )) .green() .to_string(), ); print_extern_notice(include_extern); } SubCommand::Compare { pat_id, database, host, password, port, user, file, year, ignore_exports_since, include_extern, include_histo_zyto, } => { let password = request_password_if_none(password); let year = sanitize_year(year); let _ = term.write_line( &style(format!("Warte auf Daten für das Diagnosejahr {}...", year)) .blue() .bright() .to_string(), ); let db = DatabaseSource::new(&database, &host, &password, port, &user); let db_items = db .export( &year, &ignore_exports_since.unwrap_or("9999-12-31".into()), pat_id, include_extern, include_histo_zyto, ) .map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?; let _ = term.clear_last_lines(1); let csv_items = opal::OpalCsvFile::export(file.as_path()).map_err(|_e| "Kann Datei nicht lesen")?; let mut not_in_csv = db_items .iter() .filter(|db_item| { !csv_items .iter() .map(|csv_item| &csv_item.condition_id) .contains(&db_item.condition_id) }) .collect::>(); print_extern_notice(include_extern); let _ = term.write_line( &style(format!( "{} Conditions aus der Datenbank für das Jahr {} - aber nicht in Datei '{}'", not_in_csv.len(), year, file.to_str().unwrap_or_default() )) .green() .to_string(), ); let _ = term.write_line(&format!( "{:<64} {:<10} {:<5} {:<5} {}", "Condition-ID", "Datum", "ICD10", "", "PAT-ID" )); not_in_csv.sort_by_key(|item| item.condition_id.to_string()); not_in_csv .iter() .for_each(|item| match Check::is_relevant(&item.icd_10_code) { true => { let _ = term.write_line(&format!( "{:<64} {:<10} {:<5} {:<5} {}", item.condition_id, item.diagnosis_date, style(&item.icd_10_code).bold().red(), "", match &item.pat_id { Some(ref pat_id) => pat_id.to_string(), _ => "".to_string(), } )); } false => { let _ = term.write_line(&format!( "{:<64} {:<10} {:<5} {:<5} {}", item.condition_id, item.diagnosis_date, item.icd_10_code, "", match &item.pat_id { Some(ref pat_id) => pat_id.to_string(), _ => "".to_string(), } )); } }); let mut not_in_db = csv_items .iter() .filter(|csv_item| { !db_items .iter() .map(|db_item| &db_item.condition_id) .contains(&csv_item.condition_id) }) .collect::>(); let _ = term.write_line( &style(format!( "{} Conditions aus Datei '{}' - aber nicht in der Datenbank für das Jahr {}", not_in_db.len(), file.to_str().unwrap_or_default(), year )) .green() .to_string(), ); let _ = term.write_line(&format!( "{:<64} {:<10} {:<5}", "Condition-ID", "Datum", "ICD10" )); not_in_db.sort_by_key(|item| item.condition_id.to_string()); not_in_db .iter() .for_each(|item| match Check::is_relevant(&item.icd_10_code) { true => { let _ = term.write_line(&format!( "{:<64} {:<10} {:<5}", item.condition_id, item.diagnosis_date, style(&item.icd_10_code).bold().red() )); } false => { let _ = term.write_line(&format!( "{:<64} {:<10} {:<5}", item.condition_id, item.diagnosis_date, item.icd_10_code )); } }); let mut icd10diff = db_items .iter() .filter(|db_item| { csv_items .iter() .map(|db_item| &db_item.condition_id) .contains(&db_item.condition_id) }) .filter(|db_item| { !csv_items .iter() .map(|csv_item| { format!("{}-{}", csv_item.condition_id, csv_item.icd_10_code) }) .contains(&format!("{}-{}", db_item.condition_id, db_item.icd_10_code)) }) .map(|db_item| DiffRecord { pat_id: db_item.pat_id.as_ref().map(|pat_id| pat_id.to_string()), condition_id: db_item.condition_id.to_string(), diagnosis_date: db_item.diagnosis_date.to_string(), csv_icd10_code: db_item.icd_10_code.to_string(), db_icd10_code: csv_items .iter() .filter(|csv_item| csv_item.condition_id == db_item.condition_id) .collect_vec() .first() .unwrap() .icd_10_code .to_string(), }) .collect::>(); let _ = term.write_line( &style(format!( "{} Conditions mit Unterschied im ICD10-Code", icd10diff.len() )) .green() .to_string(), ); icd10diff.sort_by_key(|item| item.condition_id.to_string()); let _ = term.write_line(&format!( "{:<64} {:<10} {:<5} {:<5} {}", "Condition-ID", "Datum", "CSV", "DB", "PAT-ID" )); icd10diff.iter().for_each(|item| { let _ = term.write_line(&format!( "{:<64} {:<10} {:<5} {:<5} {}", item.condition_id, item.diagnosis_date, match Check::is_relevant(&item.csv_icd10_code) { true => style(format!("{:<5}", item.csv_icd10_code)).bold().red(), _ => style(format!("{:<5}", item.csv_icd10_code)), }, match Check::is_relevant(&item.db_icd10_code) { true => style(format!("{:<5}", item.db_icd10_code)).bold().red(), _ => style(format!("{:<5}", item.db_icd10_code)), }, match &item.pat_id { Some(ref pat_id) => pat_id.to_string(), _ => "".to_string(), } )); }); } SubCommand::CheckExport { database, host, password, port, user, file, package, } => { let password = request_password_if_none(password); let _ = term.write_line( &style(format!( "Warte auf Daten für den LKR-Export '{}'...", package )) .blue() .bright() .to_string(), ); let db = DatabaseSource::new(&database, &host, &password, port, &user); let db_entries = db .exported(package) .map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?; let db_meldungen = db_entries .iter() .map(|entry| LkrExportProtocolFile::parse(&entry.1)) .filter(|entry| entry.is_ok()) .flat_map(|entry| entry.unwrap().meldungen()) .filter(|meldung| meldung.id().is_some()) .map(|meldung| (meldung.id().unwrap(), meldung)) .collect::>(); let xml_meldungen = LkrExportProtocolFile::parse_file(file.as_path()) .map_err(|_e| "Fehler bei Zugriff auf die Protokolldatei")? .meldungen() .into_iter() .filter(|meldung| meldung.id().is_some()) .map(|meldung| (meldung.id().unwrap(), meldung)) .collect::>(); let missing_xml_ids = db_meldungen .keys() .filter(|&key| !xml_meldungen.contains_key(key)) .collect_vec(); let _ = term.clear_last_lines(1); let _ = term.write_line( &style(format!( "{} Datenbankeinträge mit {} Meldungen abgerufen", db_entries.len(), db_meldungen.len() )) .green() .to_string(), ); if db_meldungen.len() != xml_meldungen.len() { let _ = term.write_line( &style("\nNicht übereinstimmende Anzahl an Meldungen:") .yellow() .to_string(), ); let _ = term.write_line(&format!( "Datenbank: {:>10}\nProtokolldatei: {:>10}", db_meldungen.len(), xml_meldungen.len() )); let missing_db_ids = xml_meldungen .keys() .filter(|&key| !db_meldungen.contains_key(key)) .collect_vec(); if !missing_db_ids.is_empty() { let _ = term.write_line( &style("\nIn der Datenbank fehlende Meldungen:") .yellow() .to_string(), ); missing_db_ids.iter().sorted().for_each(|&item| { let _ = term.write_line(&format!( "{} ({})", item, to_database_id(item).unwrap_or("?".into()) )); }); } if !missing_xml_ids.is_empty() { let _ = term.write_line( &style("\nIn der Protokolldatei fehlende Meldungen:") .yellow() .to_string(), ); missing_xml_ids.iter().sorted().for_each(|&item| { let _ = term.write_line(&format!( "{} ({})", item, to_database_id(item).unwrap_or("?".into()) )); }); } } let multiple_meldung_entries = db_entries .iter() .map(|(lkr_meldung, meldung)| (lkr_meldung, LkrExportProtocolFile::parse(meldung))) .filter_map(|(lkr_meldung, meldung)| { if meldung.unwrap().meldungen().len() > 1 { Some(lkr_meldung) } else { None } }) .sorted() .collect_vec(); if !multiple_meldung_entries.is_empty() { let _ = term.write_line( &style("\nFolgende Einträge in `lkr_meldung_export` haben mehrere Meldungsinhalte in `xml_daten`:") .yellow() .to_string(), ); multiple_meldung_entries.iter().for_each(|item| { let _ = term.write_line(&item.to_string()); }); } let different_content = db_meldungen .iter() .filter(|(id, _)| !missing_xml_ids.contains(id)) .filter(|(id, meldung)| { xml_meldungen .get(&id.to_string()) .unwrap_or(&Meldung { raw_value: String::new(), }) .sanitized_xml_string() != meldung.sanitized_xml_string() }) .map(|(_, meldung)| meldung.id().unwrap_or("?".into())) .collect_vec(); if !different_content.is_empty() { let _ = term.write_line( &style(&format!( "\nFolgende {} Meldungen unterscheiden sich in der Datenbank und der Protokolldatei:", different_content.len() )) .yellow() .to_string(), ); let _ = term.write_line( "Dies kann auch aufgrund der verwendeten XML-Encodierung auftreten und bedeutet nicht immer eine inhaltliche Abweichung." ); different_content .iter() .sorted_by(|id1, id2| { to_database_id(id1) .unwrap_or_default() .cmp(&to_database_id(id2).unwrap_or_default()) }) .for_each(|id| { let _ = term.write_line(&format!( "{} ({})", id, to_database_id(id).unwrap_or("?".into()) )); }); } } } Ok(()) }