/* * advotracker - Hotline tackingtool for Advocats * * Copyright 2021 Ralf Zerres * SPDX-License-Identifier: (0BSD or MIT) */ // suppress creation of a new console window on window #![windows_subsystem = "windows"] use locales::t; use orbtk::{ prelude::*, theme_default::{THEME_DEFAULT, THEME_DEFAULT_COLORS_DARK, THEME_DEFAULT_FONTS}, theming::config::ThemeConfig, }; use cfg_if::cfg_if; use serde::Deserialize; use std::env; use std::time::SystemTime; use tracing::{Level, info, trace}; use advotracker::{ data::{constants::*, structures::Email}, widgets::global_state::GlobalState, //services::exports::send_ticketdata::sendticketdata, widgets::policycheck::*, widgets::policycheck_state::*, widgets::main_view, widgets::ticketdata::*, }; #[cfg(target_os = "windows")] use orbtk::theme_fluent::{THEME_FLUENT, THEME_FLUENT_COLORS_DARK, THEME_FLUENT_FONTS}; pub enum TicketdataAction { ClearForm(), SendForm(), UpdatePolicyCode(String) } // German localization file. static ADVOTRACKER_DE_DE: &str = include_str!("../assets/advotracker/advotracker_de_DE.ron"); /// Get the active language environment. fn get_lang() -> String { // get system environment let mut lang = env::var("LANG").unwrap_or_else(|_| "C".to_string()); lang = lang.substring(0,5).to_string(); // "de_DE.UTF-8" -> "de_DE" info!("GUI-Language: preset to {:?}", lang); // return the active language lang } cfg_if! { if #[cfg(windows)] { /// Extend and register theme assets. fn theme() -> Theme { register_default_fonts(Theme::from_config( ThemeConfig::from(DEFAULT_DARK_EXT) .extend(ThemeConfig::from(THEME_DEFAULT)) .extend(ThemeConfig::from(THEME_DEFAULT_COLORS_DARK)) .extend(ThemeConfig::from(THEME_DEFAULT_FONTS)), )) } fn theme_fluent() -> Theme { register_fluent_fonts(Theme::from_config( ThemeConfig::from(FLUENT_DARK_EXT) .extend(ThemeConfig::from(THEME_FLUENT)) .extend(ThemeConfig::from(THEME_FLUENT_COLORS_DARK)) .extend(ThemeConfig::from(THEME_FLUENT_FONTS)), )) // register_fluent_fonts(Theme::from_config( // ThemeConfig::from(FLUENT_LIGHT_EXT) // .extend(ThemeConfig::from(THEME_FLUENT)) // .extend(ThemeConfig::from(THEME_FLUENT_COLORS_DARK)) // .extend(ThemeConfig::from(THEME_FLUENT_FONTS)), } } else { /// Extend and register theme assets. fn theme() -> Theme { register_default_fonts(Theme::from_config( ThemeConfig::from(DEFAULT_DARK_EXT) .extend(ThemeConfig::from(THEME_DEFAULT)) .extend(ThemeConfig::from(THEME_DEFAULT_COLORS_DARK)) .extend(ThemeConfig::from(THEME_DEFAULT_FONTS)), )) } } } // Main fn main() -> Result<(), Box> { // initialize the tracing subsystem // a drop in replacement for classical logging // reference: https://tokio.rs/blog/2019-08-tracing/ let span = tracing::span!(Level::TRACE, "advotracker"); let _enter = span.enter(); let subscriber = fmt::Subscriber::builder() .with_env_filter(trace) //.with_max_level(tracing::Level::DEBUG) .finish(); tracing::subscriber::with_default(subscriber, || { // get system environment let lang = get_lang(); let mut state = t!("state.started", lang); let mut res = t!("parse.environment", lang); trace!(target: "advotracker", process = ?res, state = ?state); trace!(target: "advotracker", environment = "system", lang = ?lang); trace!(target: "advotracker", machine = ?&machine_kind); // how to handle unimplemented lang resources?? res = t!("parse.environment", lang); trace!(target: "advotracker", environment = "envy", lang = ?lang); state = t!("state.finished", lang); trace!(target: "advotracker", process = ?res, state = ?state); // main tasks res = t!("main.started", lang); state = t!("state.started", lang); trace!(target: "advotracker", process = ?res, state = ?state); // use this only if you want to run it as web application. orbtk::initialize(); // Preset localization and language from given environment. // if no dictionary is set for the chosen default language // the content of the text property will be drawn. //let language = lang.substring(0,5); // "de_DE.UTF-8" -> "de_DE" //info!("GUI-Language: preset to {:?}", language); let localization = RonLocalization::create() .language(&lang) .dictionary("de_DE", ADVOTRACKER_DE_DE) .build(); Application::from_name("nwx.advotracker") .localization(localization) .theme(theme()) .window(|ctx| { Window::new() .title("AdvoTracker - DirectCall") .position((500.0, 100.0)) .size(800.0, 600.0) //.min_width(460.0) //.min_height(380.0) .resizeable(true) .child(main_view::MainView::new().build(ctx)) .build(ctx) }) .run(); state = t!("state.finished", lang); res = t!("main.finished", lang); trace!(target: "advotracker", process = ?res, state = ?state); }); Ok(()) } // View widget!(MainView { policycheck_view: PolicyCheck }); impl Template for MainView { fn template(self, _id: Entity, ctx: &mut BuildContext<'_>) -> Self { let ticketdata_view = TicketdataView::new() .build(ctx); let policycheck_view = PolicycheckView::new() .target(ticketdata_view.0) .build(ctx); self.name("MainView") .child( TabWidget::new() .tab(ID_POLICY_CHECK_VIEW, policycheck_view) .tab(ID_TICKET_DATA_VIEW, ticketdata_view) .build(ctx), ) } } // Macro that initializes the widget structures/variables for the policy check view widget!( /// Form to enter data of a ticket record TicketdataView { // language used inside the widget lang: String, // title used in the header ticket_data_title: String, // entity id that will receive the messages target: u32 } ); /// The template implementation of the ticket view /// All GUI elements are styled using the "style" attribute referencing to a ron based css impl Template for TicketdataView { fn template(self, id: Entity, ctx: &mut BuildContext<'_>) -> Self { let tenent_logo = Container::new() .margin((16, 16, 0, 0)) .attach(Grid::column(0)) .v_align("center") .child( ImageWidget::new() .image("assets/advotracker/hiedemann_logo.png") .v_align("center") .build(ctx), ) .build(ctx); let ticket_data_bottom_bar = Container::new() .id(ID_TICKET_DATA_BOTTOM_BAR) .style(STYLE_BOTTOM_BAR) .attach(Grid::row(4)) .attach(Grid::column(1)) .attach(Grid::column_span(2)) .v_align("end") .child( Container::new() .attach(Grid::column(1)) .h_align("end") .v_align("end") .child( TextBlock::new() .margin((0, 9, 48, 0)) .text("©Networkx GmbH") .build(ctx) ) .build(ctx), ) .build(ctx); let ticket_data_button_menu = Button::new() .id(ID_TICKET_DATA_BUTTON_MENU) .attach(Grid::row(0)) .attach(Grid::column(2)) .h_align("end") .icon(material_icons_font::MD_MENU) //.style("button_single_content") .style(STYLE_BUTTON_MENU) .build(ctx); let ticket_data_form = Container::new() .id(ID_TICKET_DATA_FORM) .name(ID_TICKET_DATA_FORM) .attach(Grid::row(2)) .attach(Grid::column(1)) .style("container_form") .child( Grid::new() .id(ID_TICKET_DATA_GRID) .name(ID_TICKET_DATA_GRID) .columns( Columns::create() .push("auto") // Label .push(16) // Delimiter .push("*") // Data .push(32) // Delimiter (2x margin) ) .rows( Rows::create() .push("auto") // Row 0 .push(14) // Seperator .push("auto") // Row 2 .push(14) // Seperator .push("auto") // Row 4 .push(14) // Seperator .push("auto") // Row 6 .push(14) // Seperator .push("auto") // Row 8 .push(14) // Seperator .push("auto") // Row 10 .push(14) // Seperator .push("auto") // Row 12 .push(14) // Seperator ) .child( TextBlock::new() .id(ID_TICKET_DATA_LABEL_POLICY_CODE) .attach(Grid::row(0)) .attach(Grid::column(0)) .text("Policy code") .h_align("end") .v_align("center") .build(ctx), ) .child( TextBox::new() .id(ID_TICKET_DATA_POLICY_CODE) .attach(Grid::row(0)) .attach(Grid::column(2)) .text("") .v_align("center") .build(ctx), ) .child( TextBlock::new() .id(ID_TICKET_DATA_LABEL_POLICY_HOLDER) .attach(Grid::row(2)) .attach(Grid::column(0)) .text("Policy holder") .h_align("end") .v_align("center") .build(ctx), ) .child( TextBox::new() .id(ID_TICKET_DATA_POLICY_HOLDER) .attach(Grid::row(2)) .attach(Grid::column(2)) .text("") .water_mark("Name des Versicherungsnehmers") .build(ctx), ) .child( TextBlock::new() .id(ID_TICKET_DATA_LABEL_DEDUCTIBLE) .attach(Grid::row(4)) .attach(Grid::column(0)) .text("Deductible") .h_align("end") .v_align("center") .build(ctx), ) .child( TextBox::new() .id(ID_TICKET_DATA_DEDUCTIBLE) .attach(Grid::row(4)) .attach(Grid::column(2)) .text("") .water_mark("im Beratungsgespräch erfragen") .build(ctx), ) .child( TextBlock::new() .id(ID_TICKET_DATA_LABEL_CALLBACK_NUMBER) .attach(Grid::row(6)) .attach(Grid::column(0)) .text("Callback number") .h_align("end") .v_align("center") .build(ctx), ) .child( TextBox::new() .id(ID_TICKET_DATA_CALLBACK_NUMBER) .attach(Grid::row(6)) .attach(Grid::column(2)) .text("") .water_mark("wie zu erreichen") .build(ctx), ) .child( TextBlock::new() .id(ID_TICKET_DATA_LABEL_CALLBACK_DATE) .attach(Grid::row(8)) .attach(Grid::column(0)) .text("Callback date") .h_align("end") .v_align("center") .build(ctx), ) .child( TextBox::new() .id(ID_TICKET_DATA_CALLBACK_DATE) .attach(Grid::row(8)) .attach(Grid::column(2)) .text("") .water_mark("Rückruf gewünscht um") .build(ctx), ) .child( TextBlock::new() .id(ID_TICKET_DATA_LABEL_HARM_TYPE) .attach(Grid::row(10)) .attach(Grid::column(0)) .text("Harm type") .h_align("end") .v_align("center") .build(ctx), ) .child( TextBox::new() .id(ID_TICKET_DATA_HARM_TYPE) .name(ID_TICKET_DATA_HARM_TYPE) .attach(Grid::row(10)) .attach(Grid::column(2)) .text("") .water_mark("Schadenstyp") .build(ctx), ) .child( TextBlock::new() .id(ID_TICKET_DATA_LABEL_IVR_COMMENT) .attach(Grid::row(12)) .attach(Grid::column(0)) .text("IVR comment") .h_align("end") .v_align("center") .build(ctx), ) .child( TextBox::new() .id(ID_TICKET_DATA_IVR_COMMENT) .attach(Grid::row(12)) .attach(Grid::column(2)) .text("") .water_mark("Kommentar zur Haftung, bzw. zur Deckung") //.height(48.0) .build(ctx), ) .build(ctx), ) .build(ctx); let ticket_data_form_action = Container::new() .id(ID_TICKET_DATA_ACTION_GRID) .attach(Grid::row(3)) .attach(Grid::column(1)) //.style(STYLE_CONTAINER_ACTION) .padding(14) .h_align("center") .child( Stack::new() //.style(STYLE_STACK_ACTION) .orientation("horizontal") .spacing(50) .child( Button::new() .id(ID_TICKET_DATA_ACTION_BUTTON_CLEAR) .style(STYLE_BUTTON_ACTION) .text("Clear") .on_click(move |states, _| { states.send_message(TicketdataAction::ClearForm(), id); false }) .build(ctx), ) .child( Button::new() .id(ID_TICKET_DATA_ACTION_BUTTON_SEND) .style(STYLE_BUTTON_ACTION) .text("Send") //.visibility(Visibility::Collapsed) .on_click(move |states, _entity| { states.send_message(TicketdataAction::SendForm(), id); false }) .build(ctx), ) .build(ctx), ) .build(ctx); // vector with valid mail recipients addresses (mail_to) let items_mail_to = vec![ PROP_MAIL_TO_1, PROP_MAIL_TO_2, PROP_MAIL_TO_3, PROP_MAIL_TO_4, ]; let items_mail_to_count = items_mail_to.len(); // vector with valid carbon copy recipients addresses (mail_to) let items_mail_cc = vec![ PROP_MAIL_CC_1, PROP_MAIL_CC_2, ]; let items_mail_cc_count = items_mail_cc.len(); let ticket_data_form_mail = Container::new() .id(ID_TICKET_DATA_CONTAINER_MAIL) .name(ID_TICKET_DATA_CONTAINER_MAIL) .attach(Grid::row(1)) .attach(Grid::column(1)) .style(STYLE_CONTAINER_MAIL) .child( Grid::new() .columns( Columns::create() .push(16) // Delimiter .push("stretch") // Label .push(16) // Delimiter .push("auto") // MailAddress .push("32") // Delimiter (2x margin) ) .rows( Rows::create() .push("auto") // Row 0 .push(2) // Seperator .push("auto") // Row 2 .push(2) // Seperator ) .child( TextBlock::new() .id(ID_TICKET_DATA_MAIL_TO) .attach(Grid::row(0)) .attach(Grid::column(1)) .style(STYLE_MAIL_LABEL) .text("Recipient (To)") .build(ctx), ) .child( /// WIP: get selected item ComboBox::new() .id(ID_TICKET_DATA_MAIL_TO) .attach(Grid::row(0)) .attach(Grid::column(3)) .style(STYLE_MAIL_TO) .count(items_mail_to_count) .items_builder(move |ibc, index| { let text_mail_to = TicketdataView::items_mail_to_ref(&ibc.get_widget(id))[index].clone(); TextBox::new() .text(text_mail_to) .v_align("center") .build(ibc) }) .selected_index(0) .build(ctx), ) .child( TextBlock::new() .id(ID_TICKET_DATA_LABEL_MAIL_CC) .attach(Grid::row(2)) .attach(Grid::column(1)) .style(STYLE_MAIL_LABEL) .text("Copie (CC)") .build(ctx), ) .child( ComboBox::new() .id(ID_TICKET_DATA_MAIL_CC) .attach(Grid::row(2)) .attach(Grid::column(3)) .style(STYLE_MAIL_CC) .count(items_mail_cc_count) .items_builder(move |ibc, index| { let text_mail_cc = TicketdataView::items_mail_cc_ref(&ibc.get_widget(id))[index].clone(); TextBox::new() .text(text_mail_cc) .v_align("center") .build(ibc) }) .selected_index(0) .build(ctx), ) .build(ctx), ) .build(ctx); let ticket_data_header_text = TextBlock::new() .id(ID_TICKET_DATA_HEADER) .attach(Grid::column(0)) .style(STYLE_HEADER_TEXT) .text("Ticket Data") .build(ctx); let ticket_data_header_bar = Container::new() .id(ID_TICKET_DATA_HEADER_BAR) .attach(Grid::row(0)) .attach(Grid::column_span(3)) .style(STYLE_HEADER_BAR) .child(tenent_logo) .child(ticket_data_header_text) .child(ticket_data_button_menu) .build(ctx); // Widget: Ticket data view self.id(ID_TICKET_DATA_VIEW) .name(ID_TICKET_DATA_VIEW) .min_height(410.0) .child( Grid::new() .id(ID_TICKET_DATA_GRID) .columns( Columns::create() .push(50) // Left margin .push("*") // Content .push(50) // Right margin ) .rows( Rows::create() .push("auto") // Header_Bar .push("auto") // Mail_Form .push("*") // Input_Form .push("auto") // Action .push("auto") // Bottom_Bar ) .child(ticket_data_header_bar) // row 0 .child(ticket_data_form_mail) // row 1 .child(ticket_data_form) // row 2 .child(ticket_data_form_action) // row 3 .child(ticket_data_bottom_bar) // row 4 .build(ctx), ) } } // State impl GlobalState for TicketdataState {} /// Method definitions, that react on any given state change inside the `Ticketdata` widget. impl TicketdataState { /// Clear the text property of all children of the given form entity pub fn clear_form(entity: Entity, ctx: &mut Context<'_>) { if let Some(count) = ctx.get_widget(entity).children_count() { info!("Widget name: {:?}", ctx.get_widget(entity).get::("name")); info!("Widget id: {:?}", ctx.get_widget(entity).get::("id")); } // identify the form by its id if let form_entity = ctx.child(ID_TICKET_DATA_GRID).entity() { info!("Form id: {:?}", ctx.get_widget(form_entity).get::("id")); info!("Form node name: {:?}", ctx.get_widget(form_entity).get::("name")); // Loop through children if let Some(count) = ctx.get_widget(form_entity).children_count() { for c in 1..=count { info!("WIP clear entry of child {:?}", c); //info!("WIP clear entry of child {:?}: {:?}", c, ctx.get_child(form_entity).get::("name")); } } } } pub fn send_form(entity: Entity, ctx: &mut Context<'_>, lang: &str) { // type conversion (String -> u64) //let policy_code = ctx.child(ID_TICKET_DATA_POLICY_CODE).get::("text").unwrap().parse::().unwrap(); // WIP: get selected items ComboBox'es //let mail_cc_index = *TicketdataView::selected_index_ref(&ctx.widget()) as usize; //let mail_cc_selected = TicketdataView::mail_cc_ref(&ctx.widget())[mail_cc_index].clone(); // create Email structures let email = Email { // WIP: mail_to -> selected index auslesen //mail_to: ctx.child(ID_TICKET_DATA_MAIL_TO).get::("text").to_string(), // WIP: mail_cc -> selected index auslesen //mail_cc: ctx.child(ID_TICKET_DATA_MAIL_CC).get::("text").to_string(), mail_to: PROP_MAIL_TO_1.to_string(), mail_cc: PROP_MAIL_CC_1.to_string(), mail_bcc: PROP_MAIL_BCC_1.to_string(), mail_from: PROP_MAIL_FROM.to_string(), mail_reply: PROP_MAIL_REPLY.to_string(), subject: PROP_MAIL_SUBJECT.to_string(), policy_code: ctx.child(ID_TICKET_DATA_POLICY_CODE).get::("text").to_string(), policy_holder: ctx.child(ID_TICKET_DATA_POLICY_HOLDER).get::("text").to_string(), deductible: ctx.child(ID_TICKET_DATA_DEDUCTIBLE).get::("text").to_string(), callback_number: ctx.child(ID_TICKET_DATA_CALLBACK_NUMBER).get::("text").to_string(), callback_date: ctx.child(ID_TICKET_DATA_CALLBACK_DATE).get::("text").to_string(), harm_type: ctx.child(ID_TICKET_DATA_HARM_TYPE).get::("text").to_string(), ivr_comment: ctx.child(ID_TICKET_DATA_IVR_COMMENT).get::("text").to_string(), }; info!("WIP: Sending form to construct eMail to {:?}", email); // send email via service let _ = sendticketdata(&email, &lang); } } /// Supported methods handled inside the `TicketState` impl State for TicketdataState { /// Initialize the state of widgets inside `TicketState` fn init(&mut self, _: &mut Registry, ctx: &mut Context<'_>) { let time_start= SystemTime::now(); trace!(target: "advotracker", ticketdata_state = "init", status = "started"); // Initialize required entities self.button_menu = ctx .entity_of_child(ID_TICKET_DATA_BUTTON_MENU) .expect("TicketState.init: Can't find resource entity 'ID_TICKET_DATA_BUTTON_MENU'."); self.target = Entity::from(ctx.widget().try_clone::("target") .expect("TicketState.init: Can't find resource entity 'target'.")); // Get language from environment self.lang = TicketdataState::get_lang(); let time_end = SystemTime::now(); let duration = time_end.duration_since(time_start); trace!(target: "advotracker", ticketdata_state = "init", status = "finished", duration = ?duration); } fn messages( &mut self, mut messages: MessageReader, _registry: &mut Registry, ctx: &mut Context<'_>, ) { for message in messages.read::() { match message { TicketdataAction::ClearForm() => { info!("message: {:?} recieved", message); TicketdataState::clear_form(ctx.entity(), ctx); } TicketdataAction::SendForm() => { info!("message: {:?} recieved", message); TicketdataState::send_form(ctx.entity(), ctx, &self.lang); } _ => { println!("messages: action not implemented!"); } } } for message in messages.read::() { match message { PolicycheckAction::UpdatePolicyCode => { info!("Message received: 'PolicycheckAction::UpdatePolicyCode'"); TextBlock::text_set(&mut ctx.child(ID_TICKET_DATA_POLICY_CODE), policy_code); } _ => { println!("messages: action not implemented!"); } } } } fn update(&mut self, _: &mut Registry, ctx: &mut Context<'_>) { let actions: Vec = self.actions.drain(..).collect(); for action in actions { match action { TicketdataAction::ClearForm() => { info!("update: send_message {:?}", action); ctx.send_message(TicketdataAction::ClearForm(), self.target); } TicketdataAction::SendForm() => { //ctx.send_message(TicketdataAction::SendForm(), self.ID_TICKETDATA_FORM); info!("update: send_message {:?}", action); } _ => { println!("TicketdataAction: action not implemented!"); } } } } } /// send ticket data via eMail pub fn sendticketdata(email: &Email, lang: &str) -> Result<(), Box> { let mut res = t!("sendticketdata.export.started", lang); let mut state = t!("state.started", lang); trace!(target: "sendticketdata", process = ?res, state = ?state); let ascii_body = String::new() + &"Vers.-Schein/Schadennummer".to_string() + &(email.policy_code) + &"\n" + &"Versicherungsnehmer: ".to_string() + &(email.policy_holder) + &"\n" + &"Selbstbehalt: ".to_string() + &(email.deductible) + &"\n" + &"Rückrufnummer: ".to_string()+ &(email.callback_number) + &"\n" + &"Erreichbarkeit: ".to_string() + &(email.callback_date) + &"\n" + &"Rechtsproblem: ".to_string() + &(email.harm_type) + &"\n" + &"Rechtsrat: ".to_string() + &(email.ivr_comment) + &"\n"; info!("email body: {:?}", ascii_body); let message = Message::builder() //.reply_to("Kanzlei Hiedemann ".parse().unwrap()) .to("Kanzlei Hiedemann ".parse().unwrap()) .cc(" ".parse().unwrap()) .reply_to((email.mail_reply).parse().unwrap()) //.to((email.mail_to).parse().unwrap()) //.cc((email.mail_cc).parse().unwrap()) //.bcc((email.mail_bcc).parse().unwrap()) .from((email.mail_from).parse().unwrap()) .subject(String::new() + &email.subject.to_string() + &" (".to_string() + &email.policy_code.to_string() + &")".to_string() ) .multipart( MultiPart::alternative() // This is composed of two parts. .singlepart( SinglePart::builder() .header(header::ContentType( "text/plain; charset=utf8".parse().unwrap(), )) .body(String::from(ascii_body)), ) ) .expect("failed to build email"); info!("message: {:?}", message); trace!(target: "sendticketdata", email = ?email); state = t!("state.finished", lang); res = t!("sendticketdata.export.finished", lang); trace!(target: "sendticketdata", process = ?res, state = ?state); Ok(()) }