From 667e511bd950e0b0f5191778ee24a2a68a5dcd37 Mon Sep 17 00:00:00 2001 From: madeliri Date: Tue, 7 Apr 2026 11:56:24 +0300 Subject: [PATCH] 0.15.0 (2026-04-07) --- CHANGELOG.md | 10 +- README.md | 9 +- app.R | 1122 +++++++++++++++++------------ configs/schemas/main.xlsx | Bin 11692 -> 0 bytes configs/schemas/schema.xlsx | Bin 0 -> 13975 bytes configs/schemas/test_inline3.xlsx | Bin 11567 -> 0 bytes modules/data_validation.R | 12 +- modules/db.R | 116 ++- modules/utils.R | 94 ++- renv.lock | 78 +- 10 files changed, 875 insertions(+), 566 deletions(-) delete mode 100644 configs/schemas/main.xlsx create mode 100644 configs/schemas/schema.xlsx delete mode 100644 configs/schemas/test_inline3.xlsx diff --git a/CHANGELOG.md b/CHANGELOG.md index fd217da..3ddf0e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ -### 0.??.? +### 0.15.0 ##### features +- added `description_header` form type; - added checkboxes input form; - added button to reset data in forms; - added option to export input data to `.docx` format (installed pandoc is required), using `reference.docx` template; @@ -11,9 +12,8 @@ - in other cases - show warnings; ##### fixes - - fixed not erasing inputs while loading empty values (with checkboxes, radiobuttons); -- +number input validation +- number input validation - fix validation errors (2025-03-18); - fixes to db work: properly closing connection (2025-03-18); @@ -22,8 +22,8 @@ - some code refactoring; - replacing NumberImput to TextInput due to correct implement validation; - added options to enable/disable auth module (disabled on default) (2025-03-17); - - +- (breaking) replacing inner_tables (via `rhandsometable`) with nested_forms (rendering by shiny modal dialog feature) +- (breaking) renaming `main.xlsx` to `schema.xlsx`, schemas for nested forms now in seperate sheets in this file; ### 0.14.1 2024-10-14 diff --git a/README.md b/README.md index 4e1e790..407faa3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Данный проект представляет собой shiny-приложение (написанное на языке программирования R), для заполнения каких-то данных и возможностью последующего экспорта данных в `.xlsx`. -Структура полей для заполнения (соответственно и базы) описывается файлом `main.xlsx`, что позволяет быстро и читаемо сформировать необходимую для себя структуру. +Структура полей для заполнения (соответственно и базы) описывается файлом `schema.xlsx`, что позволяет быстро и читаемо сформировать необходимую для себя структуру. Заполненные данные хранятся локально с использованием `SQLite`. Так же возможно использование других баз данных (например `PostgreSQL`), однако это требует некоторой модификации кода. @@ -13,7 +13,7 @@ ... -# Cтруктура `main.xlsx` +# Cтруктура `schema.xlsx` Файл, формирующий структуру всей формы, представляет собой таблицу в формате `.xlsx`, состоящий из следующих столбцов: @@ -21,6 +21,7 @@ - `subgroup` - группировка второго уровня (колонки); - `form_id` - id; - `form_label` - Название формы; +- `form_description` - Описание формы; - `form_type` - тип формы, в настоящее время доступные следующие варианты: - `text` - простой текст; - `number` - число; @@ -30,9 +31,11 @@ - `radio` - выбор одного варианта (radio buttons); - `checkboxes` - выбор нескольких вариантов (checkboxes); - `description` - описание (отображение текста, без формы выбора/ввода); - - `inline_table` - вложенная таблица (rhandsometables); + - `description_header` - для отображение заголовка; + - `nested_form` - вложенная форма; - `choices` - варианты выбора (если предполагаются типом формы ввода); - `condition` - условие, при котором форма ввода будет отображаться; +- `required` - проверка заполненности поля: пустое значение - нет проверки, 1 - есть проверка # Как пользоваться diff --git a/app.R b/app.R index 457a139..e091c94 100644 --- a/app.R +++ b/app.R @@ -1,13 +1,13 @@ suppressPackageStartupMessages({ library(DBI) - library(RSQLite) + # library(RSQLite) library(tidyr) library(dplyr) library(purrr) library(magrittr) library(shiny) library(bslib) - library(rhandsontable) + # library(rhandsontable) library(shinymanager) }) @@ -20,7 +20,7 @@ source("helpers/functions.R") config <- config::get(file = "configs/config.yml") folder_with_schemas <- fs::path("configs/schemas") -FILE_SCHEME <- fs::path(folder_with_schemas, "main.xlsx") +FILE_SCHEME <- fs::path(folder_with_schemas, "schema.xlsx") # dbfile <- fs::path("data.sqlite") # options(box.path = getwd()) @@ -32,6 +32,8 @@ box::use( modules/data_validation ) +global_options$set_global_options() + # SETTINGS ================================ AUTH_ENABLED <- config$auth_module @@ -43,11 +45,11 @@ rmarkdown::find_pandoc(dir = "/opt/homebrew/bin/") if (!rmarkdown::pandoc_available()) warning("Can't find pandoc!") load_scheme_from_xlsx <- function( - filename, + sheet_name, colnames = c("part", "subgroup", "form_id", "form_label", "form_type") ) { - readxl::read_xlsx(filename) |> + readxl::read_xlsx(FILE_SCHEME, sheet = sheet_name) |> # fill NA down tidyr::fill(all_of(colnames), .direction = "down") |> dplyr::group_by(form_id) |> @@ -56,51 +58,74 @@ load_scheme_from_xlsx <- function( } -extract_forms_id_and_types_from_scheme <- function(scheme) { - scheme |> - dplyr::filter(!form_type %in% c("inline_table", "inline_table2","description", "description_header")) |> +extract_forms_id_and_types_from_scheme <- function(scheme, key = c("main_key", "nested_key")) { + + key <- match.arg(key) + + form_id_and_types_list <- scheme |> + dplyr::filter(!form_type %in% c("inline_table", "nested_forms","description", "description_header")) |> dplyr::distinct(form_id, form_type) |> tibble::deframe() + + if(!key %in% names(form_id_and_types_list)) cli::cli_abort("в схеме должно быть поле с ключем (key)") + form_id_and_types_list[names(form_id_and_types_list) != key] + } # SCHEME_MAIN UNPACK ========================== # load scheme -SCHEME_MAIN <- load_scheme_from_xlsx(FILE_SCHEME) +SCHEME_MAIN <- load_scheme_from_xlsx("main") # get list of simple inputs -inputs_simple_list <- extract_forms_id_and_types_from_scheme(SCHEME_MAIN) +main_id_and_types_list <- extract_forms_id_and_types_from_scheme(SCHEME_MAIN) -# get list of inputs with inline tables -inputs_tables_list <- SCHEME_MAIN |> - dplyr::filter(form_type == "inline_table") |> - dplyr::distinct(form_id) |> - tibble::deframe() +# # get list of inputs with inline tables +# inputs_tables_list <- SCHEME_MAIN |> +# dplyr::filter(form_type == "inline_table") |> +# dplyr::distinct(form_id) |> +# tibble::deframe() -inputs_table_df <- SCHEME_MAIN |> - dplyr::filter(form_type == "inline_table2") |> +# +nested_forms_df <- SCHEME_MAIN |> + dplyr::filter(form_type == "nested_forms") |> dplyr::distinct(form_id, .keep_all = TRUE) +# лист со схемами для всех вложенных формы +nested_forms_schemas_list <- purrr::map( + + .x = purrr::set_names(unique(nested_forms_df$form_id)), + .f = \(nested_form_id) { + + nested_form_scheme_sheet_name <- nested_forms_df |> + dplyr::filter(form_id == {nested_form_id}) |> + dplyr::pull(choices) + + # загрузка схемы для данной вложенной формы + load_scheme_from_xlsx(nested_form_scheme_sheet_name, colnames = c("subgroup","form_id", "form_label", "form_type")) + + } +) + # establish connection con <- db$make_db_connection() # init DB (write dummy data to "main" table) -db$check_if_table_is_exist_and_init_if_not("main", inputs_simple_list) +db$check_if_table_is_exist_and_init_if_not("main", main_id_and_types_list) purrr::walk( - .x = unique(inputs_table_df$form_id), + .x = unique(nested_forms_df$form_id), .f = \(table_name) { - this_inline_table2_info <- inputs_table_df |> + this_inline_table2_info <- nested_forms_df |> dplyr::filter(form_id == {table_name}) # получение имя файла с таблицой - inline_table2_file_name <- this_inline_table2_info$choices + nested_form_scheme_sheet_name <- this_inline_table2_info$choices # загрузка схемы для данной вложенной формы - this_inline_table2_scheme <- fs::path(folder_with_schemas, inline_table2_file_name) |> - load_scheme_from_xlsx(colnames = c("subgroup","form_id", "form_label", "form_type")) + this_nested_form_scheme <- load_scheme_from_xlsx(nested_form_scheme_sheet_name, colnames = c("subgroup","form_id", "form_label", "form_type")) - this_table_id_and_types_list <- extract_forms_id_and_types_from_scheme(this_inline_table2_scheme) + this_table_id_and_types_list <- extract_forms_id_and_types_from_scheme(this_nested_form_scheme, "nested_key") db$check_if_table_is_exist_and_init_if_not( table_name, @@ -114,38 +139,37 @@ purrr::walk( # close connection to prevent data loss db$close_db_connection(con) +# # INLINE TABLES ===================== +# # создаем для каждой таблицы объект +# inline_tables <- purrr::map( +# .x = purrr::set_names(inputs_tables_list), +# .f = \(x_inline_table_name) { -# INLINE TABLES ===================== -# создаем для каждой таблицы объект -inline_tables <- purrr::map( - .x = purrr::set_names(inputs_tables_list), - .f = \(x_inline_table_name) { +# # получить имя файла со схемой +# file_name <- SCHEME_MAIN |> +# dplyr::filter(form_id == x_inline_table_name) |> +# dplyr::pull(choices) - # получить имя файла со схемой - file_name <- SCHEME_MAIN |> - dplyr::filter(form_id == x_inline_table_name) |> - dplyr::pull(choices) +# # load scheme +# schemaaa <- readxl::read_xlsx(fs::path(folder_with_schemas, file_name)) |> +# tidyr::fill(dplyr::everything(), .direction = "down") - # load scheme - schemaaa <- readxl::read_xlsx(fs::path(folder_with_schemas, file_name)) |> - tidyr::fill(dplyr::everything(), .direction = "down") +# # список форм в схеме +# inline_forms <- schemaaa |> +# dplyr::distinct(form_id) |> +# dplyr::pull() - # список форм в схеме - inline_forms <- schemaaa |> - dplyr::distinct(form_id) |> - dplyr::pull() +# # макет таблицы (пустой) +# DF_gen <- as.list(setNames(rep(as.character(NA), length(inline_forms)), inline_forms)) |> +# as.data.frame() - # макет таблицы (пустой) - DF_gen <- as.list(setNames(rep(as.character(NA), length(inline_forms)), inline_forms)) |> - as.data.frame() +# # make 12 more empty rows +# DF_gen <- rbind(DF_gen, DF_gen[rep(1, 12), ]) +# rownames(DF_gen) <- NULL - # make 12 more empty rows - DF_gen <- rbind(DF_gen, DF_gen[rep(1, 12), ]) - rownames(DF_gen) <- NULL - - list(schema = schemaaa, df_empty = DF_gen) - } -) +# list(schema = schemaaa, df_empty = DF_gen) +# } +# ) # generate nav panels for each page nav_panels_list <- purrr::map( @@ -154,6 +178,7 @@ nav_panels_list <- purrr::map( # отделить схему для каждой страницы this_page_panels_scheme <- SCHEME_MAIN |> + dplyr::filter(!form_id %in% c("main_key", "nested_key")) |> dplyr::filter(part == {{page_name}}) this_page_panels <- utils$make_panels(this_page_panels_scheme) @@ -171,6 +196,7 @@ ui <- page_sidebar( title = config$header, theme = bs_theme(version = 5, preset = "bootstrap"), sidebar = sidebar( + actionButton("add_new_main_key_button", "ДОБАВИТЬ ЧТО_ТО", icon("floppy-disk", lib = "font-awesome")), actionButton("save_data_button", "Сохранить данные", icon("floppy-disk", lib = "font-awesome")), actionButton("clean_data_button", "Очистить данные", icon("user-plus", lib = "font-awesome")), textOutput("status_message"), @@ -183,6 +209,7 @@ ui <- page_sidebar( ), # list of rendered panels navset_card_underline( + id = "main", !!!nav_panels_list, header = NULL ) @@ -200,34 +227,12 @@ modal_clean_all <- modalDialog( easyClose = TRUE ) -# окно для подвтерждения удаления -modal_overwrite <- modalDialog( - "Запись с данным id уже существует в базе. Это действие перезапишет сохраненные ранее данные.", - title = "Перезаписать данные?", - footer = tagList( - actionButton("cancel_button", "Отмена"), - actionButton("data_save", "Перезаписать", class = "btn btn-danger") - ), - easyClose = TRUE -) - -# окно для подвтерждения удаления -modal_load_patients <- modalDialog( - "Загрузить данные", - uiOutput("load_menu"), - title = "Загрузить имеющиеся данные", - footer = tagList( - actionButton("cancel_button", "Отмена", class = "btn btn-danger"), - actionButton("read_data", "Загрузить данные"), - ), - easyClose = TRUE -) # init auth ======================= if (AUTH_ENABLED) ui <- shinymanager::secure_app(ui, enable_admin = TRUE) # SERVER LOGIC ============================= -server <- function(input, output) { +server <- function(input, output, session) { # AUTH SETUP ======================================== if (AUTH_ENABLED) { # check_credentials directly on sqlite db @@ -246,234 +251,92 @@ server <- function(input, output) { # REACTIVE VALUES ================================= # Create a reactive values object to store the input data - values <- reactiveValues(data = NULL) + values <- reactiveValues( + data = NULL, + main_key = NULL, + nested_key = NULL, + nested_form_id = NULL, + nested_id_and_types = NULL + ) rhand_tables <- reactiveValues() - # inline tables 2 ======================== - purrr::walk( - .x = inputs_table_df$form_id, - .f = \(table_name) { + # ========================================== + # ОБЩИЕ ФУНКЦИИ ============================ + # ========================================== - observeEvent(input[[table_name]], { + ## перенос данных из датафрейма в форму ----------------------- + load_data_to_form <- function(df, id_and_types_list, ns) { - ns <- NS(table_name) + input_types <- unname(id_and_types_list) + input_ids <- names(id_and_types_list) + if (missing(ns)) ns <- NULL - # данные для данной вложенной формы - this_inline_table2_info <- inputs_table_df |> - dplyr::filter(form_id == {table_name}) + # transform df to list + loaded_df_for_id <- as.list(df) + loaded_df_for_id <- df[input_ids] - # получение имя файла с таблицой - inline_table2_file_name <- this_inline_table2_info$choices - - # загрузка схемы для данной вложенной формы - this_inline_table2_scheme <- fs::path(folder_with_schemas, inline_table2_file_name) |> - load_scheme_from_xlsx(colnames = c("subgroup","form_id", "form_label", "form_type")) - - # # формирование карточек для данной формы - # yay_its_fun <- purrr::pmap( - # .l = dplyr::distinct(this_inline_table2_scheme, form_id, form_label, form_type), - # .f = utils$render_forms, - # main_scheme = this_inline_table2_scheme, - # ns = ns - # ) - yay_its_fun <- purrr::map( - .x = unique(this_inline_table2_scheme$subgroup), - .f = \(subgroup) { - - subroup_scheme <- this_inline_table2_scheme |> - filter(subgroup == {{subgroup}}) - - bslib::nav_panel( - title = subgroup, - purrr::pmap( - .l = dplyr::distinct(subroup_scheme, form_id, form_label, form_type), - .f = utils$render_forms, - main_scheme = subroup_scheme, - ns = ns - ) - ) - } - ) - - # ui для всплывающего окна - ui_for_inline_table <- card( - navset_card_underline( - sidebar = sidebar( - width = 300, - selectizeInput( - inputId = "aboba", - label = "key", - choices = c("a", "b") - ) - ), - !!!yay_its_fun - ) - ) - - # проверка данных для внутренних таблиц - iv_inner <- data_validation$init_val(this_inline_table2_scheme, ns) - iv_inner$enable() - - showModal(modalDialog( - ui_for_inline_table, - footer = modalButton("Dismiss"), - size = "l" - )) - - }) - } - ) - - # VALIDATIONS ============================ - # create new validator - iv_main <- data_validation$init_val(SCHEME_MAIN) - iv_main$enable() - - # STATUSES =============================== - # вывести отображение что что-то не так - output$status_message <- renderText({ - shiny::validate( - need(input$id, "⚠️ Необходимо указать id пациента!") - ) - paste0("ID: ", input$id) - }) - - output$status_message2 <- renderText({ - iv_main$is_valid() - }) - - # CREATE RHANDSOME TABLES ===================== - # записать массив пустых табличек в rhands_tables - purrr::walk( - .x = purrr::set_names(inputs_tables_list), - .f = \(x_inline_table) { - rhand_tables[[x_inline_table]] <- inline_tables[[x_inline_table]]$df_empty - } - ) - - # render tables - observe({ - # MESSAGE - purrr::walk( - .x = inputs_tables_list, - .f = \(x) { - # вытаскиваем схемы из заготовленного ранее списка - schema <- inline_tables[[x]]$schema - - # убрать дубликаты - schema_comp <- schema |> - dplyr::distinct(form_id, form_label, form_type) - - # заголовки - headers <- dplyr::pull(schema_comp, form_label) - - # fixes empty rows error - rownames(rhand_tables[[x]]) <- NULL - - # создать объект рандсонтебл - rh_tabel <- rhandsontable::rhandsontable( - rhand_tables[[x]], - colHeaders = headers, - rowHeaders = NULL, - height = 400, - ) |> - rhandsontable::hot_cols( - colWidths = 120, - manualColumnResize = TRUE, - columnSorting = TRUE - ) - - # циклом итерируемся по индексу; - for (i in seq(1, length(schema_comp$form_id))) { - # получаем информацию о типе столбца - type <- dplyr::filter(schema_comp, form_id == schema_comp$form_id[i]) |> - dplyr::pull(form_type) - - # информация о воможных вариантнах выбора - choices <- dplyr::filter(schema, form_id == schema_comp$form_id[i]) |> - dplyr::pull(choices) - - ## проверки - # текстовое поле - if (type == "text") { - rh_tabel <- rh_tabel |> - hot_col(col = headers[i], type = "autocomplete") - } - - # выбор из списка - if (type == "select_one") { - rh_tabel <- rh_tabel |> - hot_col(col = headers[i], type = "dropdown", source = choices) - } - - # дата - if (type == "date") { - rh_tabel <- rh_tabel |> - hot_col(col = headers[i], type = "date", dateFormat = "DD.MM.YYYY", language = "ru-RU") - } - } - - # передаем в оутпут полученный объект - output[[x]] <- renderRHandsontable({ - rh_tabel - }) - } - ) - }) - - - # BUTTONS LOGIC ====================== - ## clear all inputs ================== - # show modal on click of button - observeEvent(input$clean_data_button, { - showModal(modal_clean_all) - }) - - # when action confirm - perform action - observeEvent(input$clean_all_action, { - # rewrite all inputs with empty data + # rewrite input forms purrr::walk2( - .x = inputs_simple_list, - .y = names(inputs_simple_list), + .x = input_types, + .y = input_ids, .f = \(x_type, x_id) { - # using function to update forms + + # updating forms with loaded data utils$update_forms_with_data( - id = x_id, - type = x_type, - value = utils$get_empty_data(x_type) + form_id = x_id, + form_type = x_type, + value = df[[x_id]], + ns = ns ) } ) # inline tables - purrr::walk( - .x = inputs_tables_list, - .f = \(x_table_name) { - rhand_tables[[x_table_name]] <- inline_tables[[x_table_name]]$df_empty - } - ) + # purrr::walk( + # .x = inputs_tables_list, + # .f = \(x_table_name) { + # loaded_df_for_id <- read_df_from_db_by_id(x_table_name, con) - removeModal() - showNotification("Данные очищены!", type = "warning") - }) + # # если табличечки не пустые загружаем их + # if (!is.null(loaded_df_for_id) && nrow(loaded_df_for_id) != 0) { + # rhand_tables[[x_table_name]] <- subset(loaded_df_for_id, select = c(-key)) + # } else { + # rhand_tables[[x_table_name]] <- inline_tables[[x_table_name]]$df_empty + # } + # } + # ) - ## saving inputs to db ======================== - # сохранить простые данные; - observeEvent(input$save_data_button, { - req(input$id) - con <- db$make_db_connection("save_data_button") - on.exit(db$close_db_connection(con, "save_data_button"), add = TRUE) + # showNotification("Данные загружены!", type = "message") + # cli::cli_alert_success("данные для '{main_key}' из таблицы {table_name} успешно загружены") - ## MAIN + # log_action_to_db("load", main_key, con = con) + } + + ## сохранение данных из форм в базу данных -------- + save_inputs_to_db <- function( + table_name, + id_and_types_list, + ns, + con + ) { + + input_types <- unname(id_and_types_list) + input_ids <- names(id_and_types_list) + + if (missing(ns)) ns <- NULL + # собрать все значения по введенным данным; - result_df <- purrr::map( - .x = names(inputs_simple_list), - .f = \(x) { - input_d <- input[[x]] + exported_values <- purrr::map2( + .x = input_ids, + .y = input_types, + .f = \(x_id, x_type) { + + if (!is.null(ns)) x_id <- ns(x_id) + input_d <- input[[x_id]] # return empty if 0 element if (length(input_d) == 0) { - return(utils$get_empty_data(inputs_simple_list[[x]])) + return(utils$get_empty_data(x_type)) } # return element if there one if (length(input_d) == 1) { @@ -484,51 +347,448 @@ server <- function(input, output) { } ) - # make dataframe from that; - values$data <- setNames(result_df, names(inputs_simple_list)) %>% - as_tibble() + exported_df <- setNames(exported_values, input_ids) |> + as_tibble() - if (length(DBI::dbListTables(con)) == 0) { - # если база пустая, то просто записываем данные - write_all_to_db() - } else if ("main" %in% DBI::dbListTables(con)) { - # если главная таблица существует, то проверяем существование id + # пайплайн для главной таблицы + if (table_name == "main") { + exported_df <- exported_df |> + mutate( + main_key = values$main_key, + .before = 1 + ) + } - # GET DATA files - query <- glue::glue_sql(" - SELECT DISTINCT id - FROM main - WHERE id = {input$id} - ", .con = con) + # для всех остальных таблицы (вложенные) + if (table_name != "main") { + exported_df <- exported_df |> + mutate( + main_key = values$main_key, + nested_key = values$nested_key, + .before = 1 + ) + } - # получаем список записей с данным id - exist_main_df <- DBI::dbGetQuery(con, query) + # если данных нет - просто записать данные + log_action_to_db("save", values$main_key, con) - # проверка по наличию записей с данным ID в базе; - if (nrow(exist_main_df) == 0) { - # если данных нет - просто записать данные - log_action_to_db("save", input$id, con) - write_all_to_db() - } else { - # если есть выдать окно с подтверждением перезаписи - showModal(modal_overwrite) + db$write_df_to_db( + df = exported_df, + table_name = table_name, + main_key = values$main_key, + nested_key = values$nested_key, + con = con + ) + } + + # ==================================== + # NESTED FORMS ======================= + # ==================================== + ## кнопки для каждой вложенной таблицы ------------------------------- + purrr::walk( + .x = nested_forms_df$form_id, + .f = \(nested_form_id) { + + observeEvent(input[[nested_form_id]], { + req(values$main_key) + + con <- db$make_db_connection("nested_tables") + on.exit(db$close_db_connection(con, "nested_tables"), add = TRUE) + + values$nested_form_id <- nested_form_id + values$nested_key <- NULL # для нормальной работы реактивных значений + + show_modal_for_nested_form(con) + + }) + } + ) + + ## функция отображения вложенной формы для выбранной таблицы -------- + show_modal_for_nested_form <- function(con) { + + ns <- NS(values$nested_form_id) + + # загрузка схемы для данной вложенной формы + this_nested_form_scheme <- nested_forms_schemas_list[[values$nested_form_id]] + + values$nested_id_and_types <- extract_forms_id_and_types_from_scheme(this_nested_form_scheme, "nested_key") + + # выбираем все ключи из баз данных + kyes_for_this_table <- db$get_nested_keys_from_table(values$nested_form_id, values$main_key, con) + kyes_for_this_table <- unique(c(values$nested_key, kyes_for_this_table)) + values$nested_key <- kyes_for_this_table[[1]] + + # nested ui + yay_its_fun <- purrr::map( + .x = unique(this_nested_form_scheme$subgroup), + .f = \(subgroup) { + + subroup_scheme <- this_nested_form_scheme |> + dplyr::filter(subgroup == {{subgroup}}) |> + dplyr::filter(!form_id %in% c("main_key", "nested_key")) + + bslib::nav_panel( + title = subgroup, + purrr::pmap( + .l = dplyr::distinct(subroup_scheme, form_id, form_label, form_type), + .f = utils$render_forms, + main_scheme = subroup_scheme, + ns = ns + ) + ) } + ) + + # ui для всплывающего окна + ui_for_inline_table <- navset_card_underline( + sidebar = sidebar( + width = 300, + selectizeInput( + inputId = "nested_key_selector", + label = "nested_key label", + choices = kyes_for_this_table, + selected = values$nested_key, + # options = list(placeholder = "действие комиссии", create = FALSE, onInitialize = I('function() { this.setValue(""); }')) + ), + actionButton("add_new_nested_key_button", "add"), + actionButton("nested_form_save_button", "save") + ), + !!!yay_its_fun + ) + + + # проверка данных для внутренних таблиц + iv_inner <- data_validation$init_val(this_nested_form_scheme, ns) + iv_inner$enable() + + showModal(modalDialog( + ui_for_inline_table, + footer = actionButton("nested_form_close_button", "Закрыть"), + size = "l" + )) + } + + observeEvent(input$nested_form_close_button, { + removeModal() + }) + + ## сохранение данных из вложенной формы --------------- + observeEvent(input$nested_form_save_button, { + req(values$nested_form_id) + + con <- db$make_db_connection("nested_form_save_button") + on.exit(db$close_db_connection(con, "nested_form_save_button"), add = TRUE) + + # сохраняем данные основной формы!!! + save_inputs_to_db( + table_name = "main", + id_and_types_list = main_id_and_types_list, + con = con + ) + + # сохраняем данные текущей вложенной таблицы + save_inputs_to_db( + table_name = values$nested_form_id, + id_and_types_list = values$nested_id_and_types, + ns = NS(values$nested_form_id), + con = con + ) + + showNotification( + "Данные успешно сохраннены", + type = "message" + ) + }) + + ## обновление данных при переключении ключей ------------ + observeEvent(input$nested_key_selector, { + req(input$nested_key_selector) + req(values$main_key) + + # выбранный ключ в форме - перемещаем в RV + values$nested_key <- input$nested_key_selector + + }) + + observeEvent(values$nested_key, { + + con <- db$make_db_connection("nested_tables") + on.exit(db$close_db_connection(con, "nested_tables"), add = TRUE) + + kyes_for_this_table <- db$get_nested_keys_from_table(values$nested_form_id, values$main_key, con) + + if (values$nested_key %in% kyes_for_this_table) { + + # выгрузка датафрейма по общим и вложенным ключам + df <- db$read_df_from_db_by_id( + table_name = values$nested_form_id, + main_key = values$main_key, + nested_key = values$nested_key, + con = con + ) + + # загрузка данных в формы + load_data_to_form( + df = df, + id_and_types_list = values$nested_id_and_types, + ns = NS(values$nested_form_id) + ) } }) - ## get list of id's from db ===================== + ## добавление нового вложенного ключа ------------------- + observeEvent(input$add_new_nested_key_button, { + + removeModal() + + # та самая форма для ключа + scheme_for_key_input <- nested_forms_schemas_list[[values$nested_form_id]] |> + dplyr::filter(form_id %in% c("nested_key")) + + ui1 <- rlang::exec( + .fn = utils$render_forms, + !!!distinct(scheme_for_key_input, form_id, form_label, form_type), + main_scheme = scheme_for_key_input + ) + + showModal(modalDialog( + title = "Создать новую запись", + ui1, + footer = tagList( + actionButton("confirm_create_new_nested_key", "Создать") + ), + easyClose = TRUE + )) + + }) + + # действие при подтверждении создания новой записи + observeEvent(input$confirm_create_new_nested_key, { + req(input$nested_key) + + con <- db$make_db_connection("confirm_create_new_key") + on.exit(db$close_db_connection(con, "confirm_create_new_key"), add = TRUE) + + existed_key <- db$get_nested_keys_from_table( + table_name = values$nested_form_id, + main_key = values$main_key, + con + ) + + if (input$nested_key %in% existed_key) { + showNotification( + sprintf("В базе уже запись с данным ключем."), + type = "error" + ) + return() + } + + values$nested_key <- input$nested_key + utils$clean_forms(values$nested_id_and_types, NS(values$nested_form_id)) + removeModal() + show_modal_for_nested_form(con) + + }) + + # VALIDATIONS ============================ + # create new validator + iv_main <- data_validation$init_val(SCHEME_MAIN) + iv_main$enable() + + # STATUSES =============================== + # вывести отображение что что-то не так + output$status_message <- renderText({ + shiny::validate( + need(values$main_key, "⚠️ Необходимо указать id пациента!") + ) + paste0("ID: ", values$main_key) + }) + + output$status_message2 <- renderText({ + iv_main$is_valid() + }) + + # CREATE RHANDSOME TABLES ===================== + # записать массив пустых табличек в rhands_tables + # purrr::walk( + # .x = purrr::set_names(inputs_tables_list), + # .f = \(x_inline_table) { + # rhand_tables[[x_inline_table]] <- inline_tables[[x_inline_table]]$df_empty + # } + # ) + + # # render tables + # observe({ + # # MESSAGE + # purrr::walk( + # .x = inputs_tables_list, + # .f = \(x) { + # # вытаскиваем схемы из заготовленного ранее списка + # schema <- inline_tables[[x]]$schema + + # # убрать дубликаты + # schema_comp <- schema |> + # dplyr::distinct(form_id, form_label, form_type) + + # # заголовки + # headers <- dplyr::pull(schema_comp, form_label) + + # # fixes empty rows error + # rownames(rhand_tables[[x]]) <- NULL + + # # создать объект рандсонтебл + # rh_tabel <- rhandsontable::rhandsontable( + # rhand_tables[[x]], + # colHeaders = headers, + # rowHeaders = NULL, + # height = 400, + # ) |> + # rhandsontable::hot_cols( + # colWidths = 120, + # manualColumnResize = TRUE, + # columnSorting = TRUE + # ) + + # # циклом итерируемся по индексу; + # for (i in seq(1, length(schema_comp$form_id))) { + # # получаем информацию о типе столбца + # type <- dplyr::filter(schema_comp, form_id == schema_comp$form_id[i]) |> + # dplyr::pull(form_type) + + # # информация о воможных вариантнах выбора + # choices <- dplyr::filter(schema, form_id == schema_comp$form_id[i]) |> + # dplyr::pull(choices) + + # ## проверки + # # текстовое поле + # if (type == "text") { + # rh_tabel <- rh_tabel |> + # hot_col(col = headers[i], type = "autocomplete") + # } + + # # выбор из списка + # if (type == "select_one") { + # rh_tabel <- rh_tabel |> + # hot_col(col = headers[i], type = "dropdown", source = choices) + # } + + # # дата + # if (type == "date") { + # rh_tabel <- rh_tabel |> + # hot_col(col = headers[i], type = "date", dateFormat = "DD.MM.YYYY", language = "ru-RU") + # } + # } + + # # передаем в оутпут полученный объект + # output[[x]] <- renderRHandsontable({ + # rh_tabel + # }) + # } + # ) + # }) + + # ========================================= + # MAIN BUTTONS LOGIC ====================== + # ========================================= + ## добавить новый главный ключ ------------------------ + observeEvent(input$add_new_main_key_button, { + + # данные для главного ключа + scheme_for_key_input <- SCHEME_MAIN |> + dplyr::filter(form_id == "main_key") + + # создать форму для выбора ключа + ui1 <- rlang::exec( + .fn = utils$render_forms, + !!!distinct(scheme_for_key_input, form_id, form_label, form_type), + main_scheme = scheme_for_key_input + ) + + # даилог создания нового ключа + showModal(modalDialog( + title = "Создать новую запись", + ui1, + footer = tagList( + actionButton("confirm_create_new_main_key", "Создать") + ), + easyClose = TRUE + )) + + }) + + ## действие при подтверждении (проверка нового создаваемого ключа) ------- + observeEvent(input$confirm_create_new_main_key, { + req(input$main_key) + + con <- db$make_db_connection("confirm_create_new_main_key") + on.exit(db$close_db_connection(con, "confirm_create_new_key"), add = TRUE) + + existed_key <- db$get_keys_from_table("main", con) + + # если введенный ключ уже есть в базе + if (input$main_key %in% existed_key) { + showNotification( + sprintf("В базе уже запись с данным ключем."), + type = "error" + ) + return() + } + + values$main_key <- input$main_key + utils$clean_forms(main_id_and_types_list) + + removeModal() + }) + + ## очистка всех полей ----------------------- + # show modal on click of button + observeEvent(input$clean_data_button, { + showModal(modal_clean_all) + }) + + # when action confirm - perform action + observeEvent(input$clean_all_action, { + + # rewrite all inputs with empty data + utils$clean_forms(main_id_and_types_list) + + removeModal() + showNotification("Данные очищены!", type = "warning") + }) + + ## сохранение даннных ------------------------------- + observeEvent(input$save_data_button, { + req(values$main_key) + + con <- db$make_db_connection("save_data_button") + on.exit(db$close_db_connection(con, "save_data_button"), add = TRUE) + + save_inputs_to_db( + table_name = "main", + id_and_types_list = main_id_and_types_list, + con = con + ) + + showNotification( + "Данные успешно сохранены", + type = "message" + ) + }) + + ## список ключей для загрузки данных ------------------- observeEvent(input$load_data_button, { con <- db$make_db_connection("load_data_button") on.exit(db$close_db_connection(con, "load_data_button")) if (length(dbListTables(con)) != 0 && "main" %in% DBI::dbListTables(con)) { - # GET DATA files - ids <- DBI::dbGetQuery(con, "SELECT DISTINCT id FROM main") %>% - pull() - output$load_menu <- renderUI({ + # GET DATA files + ids <- db$get_keys_from_table("main", con) + + ui_load_menu <- renderUI({ selectizeInput( - inputId = "read_id_selector", + inputId = "load_data_key_selector", label = NULL, choices = ids, selected = NULL, @@ -539,92 +799,87 @@ server <- function(input, output) { ) }) } else { - output$load_menu <- renderUI({ + ui_load_menu <- renderUI({ h5("База данных не содержит записей") }) } - shiny::showModal(modal_load_patients) + shiny::showModal( + modalDialog( + "Загрузить данные", + ui_load_menu, + title = "Загрузить имеющиеся данные", + footer = tagList( + actionButton("cancel_button", "Отмена", class = "btn btn-danger"), + actionButton("load_data", "Загрузить данные"), + ), + easyClose = TRUE + ) + ) }) - ## load data to input forms ================================== - observeEvent(input$read_data, { - con <- db$make_db_connection("read_data") - on.exit(db$close_db_connection(con, "read_data"), add = TRUE) + ## загрузка данных по главному ключу ------------------ + observeEvent(input$load_data, { - # main df read - test_read_df <- read_df_from_db_by_id("main", con) + con <- db$make_db_connection("load_data") + on.exit(db$close_db_connection(con, "load_data"), add = TRUE) - # transform df to list - test_read_df <- as.list(test_read_df) - - # rewrite input forms - purrr::walk2( - .x = inputs_simple_list, - .y = names(inputs_simple_list), - .f = \(x_type, x_id) { - if (getOption("APP.DEBUG")) { - values_load <- test_read_df[[x_id]] - print(paste(x_type, x_id, values_load, sep = " || ")) - print(is.na(values_load)) - } - - # updating forms with loaded data - utils$update_forms_with_data( - id = x_id, - type = x_type, - value = test_read_df[[x_id]] - ) - } + df <- db$read_df_from_db_by_id( + table_name = "main", + main_key = input$load_data_key_selector, + con = con ) - # inline tables - purrr::walk( - .x = inputs_tables_list, - .f = \(x_table_name) { - test_read_df <- read_df_from_db_by_id(x_table_name, con) - - # если табличечки не пустые загружаем их - if (!is.null(test_read_df) && nrow(test_read_df) != 0) { - rhand_tables[[x_table_name]] <- subset(test_read_df, select = c(-id)) - } else { - rhand_tables[[x_table_name]] <- inline_tables[[x_table_name]]$df_empty - } - } + load_data_to_form( + df = df, + id_and_types_list = main_id_and_types_list ) + + values$main_key <- input$load_data_key_selector + removeModal() - showNotification("Данные загружены!", type = "warning") - message("load data") - log_action_to_db("load", input$read_id_selector, con = con) + }) ## export to .xlsx ==== output$downloadData <- downloadHandler( - filename = paste0("d2tra_", format(Sys.time(), "%Y%m%d_%H%M%S"), ".xlsx"), + filename = paste0("test_", format(Sys.time(), "%Y%m%d_%H%M%S"), ".xlsx"), content = function(file) { con <- db$make_db_connection("downloadData") on.exit(db$close_db_connection(con, "downloadData"), add = TRUE) # get all data list_of_df <- purrr::map( - .x = purrr::set_names(c("main", inputs_tables_list)), + .x = purrr::set_names(c("main", unique(nested_forms_df$form_id))), .f = \(x) { + df <- read_df_from_db_all(x, con) %>% tibble::as_tibble() # handle with data - if (nrow(df) >= 1 && x == "main") { - df <- df %>% - dplyr::mutate(dplyr::across(dplyr::contains("date"), as.Date)) %>% - print() - } + scheme <- if (x == "main") SCHEME_MAIN else nested_forms_schemas_list[[x]] + + data_columns <- subset(scheme, form_type == "date", form_id, drop = TRUE) + number_columns <- subset(scheme, form_type == "number", form_id, drop = TRUE) + + df <- df |> + dplyr::mutate( + # даты - к единому формату + dplyr::across(tidyselect::all_of({{data_columns}}), as.Date), + # числа - к единому формату десятичных значений + dplyr::across(tidyselect::all_of({{number_columns}}), ~ gsub("\\.", "," , .x)), + ) + df } ) + # set date params options("openxlsx2.dateFormat" = "dd.mm.yyyy") - print("DATA EXPORTED") + cli::cli_alert_success("DATA EXPORTED") + showNotification("База успешно экспортирована", type = "message") + log_action_to_db("export db", con = con) # pass tables to export @@ -641,7 +896,7 @@ server <- function(input, output) { ## export to .docx ==== output$downloadDocx <- downloadHandler( filename = function() { - paste0(input$id, "_", format(Sys.time(), "%Y%m%d_%H%M%S"), ".docx") + paste0(values$main_key, "_", format(Sys.time(), "%Y%m%d_%H%M%S"), ".docx") }, content = function(file) { # prepare YAML sections @@ -732,104 +987,79 @@ server <- function(input, output) { ) ## trigger saving function ============= - observeEvent(input$data_save, { - con <- db$make_db_connection("saving data (from modal conf)") - on.exit(db$close_db_connection(con, "saving data (from modal conf)"), add = TRUE) + # observeEvent(input$overwrite_data_confirm, { - # убираем плашку - removeModal() + # con <- db$make_db_connection("saving data (from modal conf)") + # on.exit(db$close_db_connection(con, "saving data (from modal conf)"), add = TRUE) - # записываем данные - write_all_to_db() - log_action_to_db("overwrite", input$id, con = con) - }) + # # убираем плашку + # removeModal() + + # # записываем данные + # db$write_df_to_db( + # df = exported_df, + # table_name = "main", + # main_key = values$main_key, + # con = con + # ) + + # log_action_to_db("overwrite", values$main_key, con = con) + # }) ## cancel ========================== observeEvent(input$cancel_button, { - # убираем плашку removeModal() }) # FUNCTIONS ============================== ## write all inputs to db ================ - write_all_to_db <- function() { - con <- db$make_db_connection("fn call `write_all_to_db()`") - # on.exit(close_db_connection("fn call `write_all_to_db()`"), add = TRUE) + # write_all_to_db <- function() { - # write main - write_df_to_db(values$data, "main", con) + # con <- db$make_db_connection("fn call `write_all_to_db()`") + # # on.exit(close_db_connection("fn call `write_all_to_db()`"), add = TRUE) - # write inline tables - for (i in inputs_tables_list) { - df <- tryCatch( - # проверка выражения - expr = { - hot_to_r(input[[i]]) - }, - # действия в случае ошибки - error = function(e) { - message(e$message) - showNotification( - glue::glue("Невозможно сохранить таблицу `{i}`! Данная ошибка может возникать в случае, если в таблице находятся пустые строки. Попробуйте удалить пустые строки и повторить сохранение."), # nolint - duration = NULL, - closeButton = FALSE, - id = paste0(i, "error_inline_tables"), - type = "error" - ) - tibble() - } - ) + # # write main + # write_df_to_db(exported_df, "main", con) - df <- df %>% - dplyr::as_tibble() %>% - janitor::remove_empty(which = c("rows")) %>% - # adding id to dbs - dplyr::mutate(id = input$id, .before = 1) + # # write inline tables + # for (i in inputs_tables_list) { + # df <- tryCatch( + # # проверка выражения + # expr = { + # hot_to_r(input[[i]]) + # }, + # # действия в случае ошибки + # error = function(e) { + # message(e$message) + # showNotification( + # glue::glue("Невозможно сохранить таблицу `{i}`! Данная ошибка может возникать в случае, если в таблице находятся пустые строки. Попробуйте удалить пустые строки и повторить сохранение."), # nolint + # duration = NULL, + # closeButton = FALSE, + # id = paste0(i, "error_inline_tables"), + # type = "error" + # ) + # tibble() + # } + # ) - # если таблица содержит хоть одну строку - сохранить таблицу в базу данных - if (nrow(df) != 0) { - write_df_to_db(df, i, con) - removeNotification(paste0(i, "error_inline_tables")) - } - } + # df <- df %>% + # dplyr::as_tibble() %>% + # janitor::remove_empty(which = c("rows")) %>% + # # adding id to dbs + # dplyr::mutate(key = input$main_key, .before = 1) - showNotification( - glue::glue("Данные пациента {input$id} сохранены!"), - type = "warning" - ) - } + # # если таблица содержит хоть одну строку - сохранить таблицу в базу данных + # if (nrow(df) != 0) { + # write_df_to_db(df, i, con) + # removeNotification(paste0(i, "error_inline_tables")) + # } + # } - ## helper function writing dbs ======== - write_df_to_db <- function(df, table_name, con) { - # disconnecting on parent function - - # delete exists data for this id - if (table_name %in% dbListTables(con)) { - del_query <- glue::glue("DELETE FROM {table_name} WHERE id = '{input$id}'") - DBI::dbExecute(con, del_query) - } - # записать данные - DBI::dbWriteTable(con, table_name, df, append = TRUE) - } - - ## reading tables from db by name and id ======== - read_df_from_db_by_id <- function(table_name, con) { - # DBI::dbConnect(RSQLite::SQLite(), dbfile) - # on.exit(DBI::dbDisconnect(con), add = TRUE) - - # check if this table exist - if (table_name %in% dbListTables(con)) { - - # prepare query - query <- glue::glue(" - SELECT * FROM {table_name} - WHERE id = '{input$read_id_selector}' - ") - - # get table as df - DBI::dbGetQuery(con, query) - } - } + # showNotification( + # glue::glue("Данные пациента {input$main_key} сохранены!"), + # type = "warning" + # ) + # } ## reading tables from db all ======== read_df_from_db_all <- function(table_name, con) { diff --git a/configs/schemas/main.xlsx b/configs/schemas/main.xlsx deleted file mode 100644 index 611089907a4c8ac2bb0906c22c3884e51b522edc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11692 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 diff --git a/configs/schemas/schema.xlsx b/configs/schemas/schema.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..5ad96e1ce623196f709d6220f2a14d2893d40211 GIT binary patch literal 13975 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 C+(eK7 literal 0 HcmV?d00001 diff --git a/configs/schemas/test_inline3.xlsx b/configs/schemas/test_inline3.xlsx deleted file mode 100644 index 0ccd690934bc65fc42bcff4376b49e5ae49c2b36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11567 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 Z7pW`<^SZkL0OIS%|5Ybb>Hhfk{{Wf4rGWqd diff --git a/modules/data_validation.R b/modules/data_validation.R index 2ba8adf..89ca464 100644 --- a/modules/data_validation.R +++ b/modules/data_validation.R @@ -15,7 +15,7 @@ init_val <- function(scheme, ns) { # формируем список id - тип inputs_simple_list <- scheme |> - dplyr::filter(!form_type %in% c("inline_table", "inline_table2","description", "description_header")) |> + dplyr::filter(!form_type %in% c("nested_forms","description", "description_header")) |> dplyr::distinct(form_id, form_type) |> tibble::deframe() @@ -42,6 +42,8 @@ init_val <- function(scheme, ns) { if (check_for_empty_data(x)) { return(NULL) } + # хак для пропуска значений + if (x == "NA") return(NULL) # check for numeric # if (grepl("^[-]?(\\d*\\,\\d+|\\d+\\,\\d*|\\d+)$", x)) NULL else "Значение должно быть числом." if (grepl("^[+-]?\\d*[\\.|\\,]?\\d+$", x)) NULL else "Значение должно быть числом." @@ -60,14 +62,16 @@ init_val <- function(scheme, ns) { x_input_id, function(x) { - # замена разделителя десятичных цифр - x <- stringr::str_replace(x, ",", ".") - # exit if empty if (check_for_empty_data(x)) { return(NULL) } + if (x == "NA") return(NULL) + + # замена разделителя десятичных цифр + x <- stringr::str_replace(x, ",", ".") + # check for currect value if (dplyr::between(as.double(x), ranges[1], ranges[2])) { NULL diff --git a/modules/db.R b/modules/db.R index 5d95084..d62826a 100644 --- a/modules/db.R +++ b/modules/db.R @@ -38,11 +38,25 @@ check_if_table_is_exist_and_init_if_not <- function( } else { - dummy_df <- dplyr::mutate(get_dummy_df(forms_id_type_list), id = "dummy") + if (table_name == "main") { + dummy_df <- dplyr::mutate( + get_dummy_df(forms_id_type_list), + main_key = "dummy", + .before = 1 + ) + } + if (table_name != "main") { + dummy_df <- get_dummy_df(forms_id_type_list) |> + dplyr::mutate( + main_key = "dummy", + nested_key = "dummy", + .before = 1 + ) + } # write dummy df into base, then delete dummy row DBI::dbWriteTable(con, table_name, dummy_df, append = TRUE) - DBI::dbExecute(con, "DELETE FROM main WHERE id = 'dummy'") + DBI::dbExecute(con, glue::glue("DELETE FROM {table_name} WHERE main_key = 'dummy'")) cli::cli_alert_success("таблица '{table_name}' успешно создана") } @@ -86,34 +100,43 @@ compare_existing_table_with_schema <- function( con = rlang::env_get(rlang::caller_env(), nm = "con") ) { + forms_id_type_list_names <- names(forms_id_type_list) + + if (table_name == "main") { + forms_id_type_list_names <- c("main_key", forms_id_type_list_names) + } else { + forms_id_type_list_names <- c("main_key", "nested_key", forms_id_type_list_names) + } + options(box.path = here::here()) box::use(modules/utils) # checking if db structure in form compatible with alrady writed data (in case on changig form) - if (identical(colnames(DBI::dbReadTable(con, table_name)), names(forms_id_type_list))) { + if (identical(colnames(DBI::dbReadTable(con, table_name)), forms_id_type_list_names)) { # ... } else { df_to_rewrite <- DBI::dbReadTable(con, table_name) - form_base_difference <- setdiff(names(forms_id_type_list), colnames(df_to_rewrite)) - base_form_difference <- setdiff(colnames(df_to_rewrite), names(forms_id_type_list)) + form_base_difference <- setdiff(forms_id_type_list_names, colnames(df_to_rewrite)) + base_form_difference <- setdiff(colnames(df_to_rewrite), forms_id_type_list_names) # if lengths are equal - if (length(names(forms_id_type_list)) == length(colnames(df_to_rewrite)) && + if (length(forms_id_type_list_names) == length(colnames(df_to_rewrite)) && length(form_base_difference) == 0 && length(base_form_difference) == 0) { - warning("changes in scheme file detected: assuming order changed only") + cli::cli_warn("changes in scheme file detected: assuming order changed only") + print(forms_id_type_list_names) } - if (length(names(forms_id_type_list)) == length(colnames(df_to_rewrite)) && + if (length(forms_id_type_list_names) == length(colnames(df_to_rewrite)) && length(form_base_difference) != 0 && length(base_form_difference) != 0) { - stop("changes in scheme file detected: structure has been changed") + cli::cli_abort("changes in scheme file detected: structure has been changed") } - if (length(names(forms_id_type_list)) > length(colnames(df_to_rewrite)) && length(form_base_difference) != 0) { - warning("changes in scheme file detected: new inputs form was added") - warning("trying to adapt database") + if (length(forms_id_type_list_names) > length(colnames(df_to_rewrite)) && length(form_base_difference) != 0) { + cli::cli_warn("changes in scheme file detected: new inputs form was added") + cli::cli_warn("trying to adapt database") # add empty data for each new input form for (i in form_base_difference) { @@ -123,15 +146,76 @@ compare_existing_table_with_schema <- function( # reorder due to scheme df_to_rewrite <- df_to_rewrite |> - dplyr::select(dplyr::all_of(names(forms_id_type_list))) + dplyr::select(dplyr::all_of(forms_id_type_list_names)) DBI::dbWriteTable(con, table_name, df_to_rewrite, overwrite = TRUE) - DBI::dbExecute(con, "DELETE FROM main WHERE id = 'dummy'") + DBI::dbExecute(con, glue::glue("DELETE FROM {table_name} WHERE main_key = 'dummy'")) } - if (length(names(forms_id_type_list)) < length(colnames(df_to_rewrite))) { - stop("changes in scheme file detected: some of inputs form was deleted! it may cause data loss!") + if (length(forms_id_type_list_names) < length(colnames(df_to_rewrite))) { + cli::cli_abort("changes in scheme file detected: some of inputs form was deleted! it may cause data loss!") } } +} + +#' @export +write_df_to_db <- function(df, table_name, main_key, nested_key, con) { + + # if(!missing(nested_key)) del_query <- glue::glue("DELETE FROM {table_name} WHERE key = '{key}'") + if (table_name == "main") { + del_query <- glue::glue("DELETE FROM main WHERE main_key = '{main_key}'") + DBI::dbExecute(con, del_query) + } + + if (table_name != "main") { + del_query <- glue::glue("DELETE FROM '{table_name}' WHERE main_key = '{main_key}' AND nested_key = '{nested_key}'") + DBI::dbExecute(con, del_query) + } + + # записать данные + DBI::dbWriteTable(con, table_name, df, append = TRUE) + + # report + cli::cli_alert_success("данные для '{main_key}' в таблице '{table_name}' успешно обновлены") + +} + +#' @export +#' reading tables from db by name and id ======== +read_df_from_db_by_id <- function(table_name, main_key, nested_key, con) { + + # check if this table exist + if (table_name == "main") { + query <- glue::glue(" + SELECT * + FROM main + WHERE main_key = '{main_key}' + ") + } + + if (table_name != "main") { + query <- glue::glue(" + SELECT * + FROM {table_name} + WHERE main_key = '{main_key}' AND nested_key = '{nested_key}' + ") + } + DBI::dbGetQuery(con, query) +} + +#' @export +get_keys_from_table <- function(table_name, con) { + + DBI::dbGetQuery(con, glue::glue("SELECT DISTINCT main_key FROM {table_name}")) |> + dplyr::pull() + +} + +#' @export +get_nested_keys_from_table <- function(table_name, main_key, con) { + + DBI::dbGetQuery(con, glue::glue("SELECT DISTINCT nested_key FROM {table_name} WHERE main_key == '{main_key}'")) |> + dplyr::pull() + } \ No newline at end of file diff --git a/modules/utils.R b/modules/utils.R index 66ebebe..de65346 100644 --- a/modules/utils.R +++ b/modules/utils.R @@ -49,7 +49,8 @@ render_forms <- function( form <- NULL # параметры только для этой формы - filterd_line <- dplyr::filter(main_scheme, form_id == {{form_id}}) + filterd_line <- main_scheme |> + dplyr::filter(form_id == {{form_id}}) # если передана ns() функция то подмеяем id для каждой формы в соответствии с пространством имен if (!missing(ns)) { @@ -178,12 +179,8 @@ render_forms <- function( ) } - # вложенная таблица - if (form_type == "inline_table") { - form <- rhandsontable::rHandsontableOutput(outputId = form_id) - } - - if (form_type == "inline_table2") { + # вложенная форма + if (form_type == "nested_forms") { form <- shiny::actionButton(inputId = form_id, label = label) } @@ -235,75 +232,104 @@ get_empty_data <- function(type) { #' @param value - value to update; #' @param local_delimeter - delimeter to split file update_forms_with_data <- function( - id, - type, + form_id, + form_type, value, - local_delimeter = getOption("SYMBOL_DELIM") + local_delimeter = getOption("SYMBOL_DELIM"), + ns ) { - if (type == "text") { - shiny::updateTextAreaInput(inputId = id, value = value) + # если передана ns() функция то подмеяем id для каждой формы в соответствии с пространством имен + if (!missing(ns) & !is.null(ns)) { + form_id <- ns(form_id) } - if (type == "number") { - shiny::updateTextAreaInput(inputId = id, value = value) + if (form_type == "text") { + shiny::updateTextAreaInput(inputId = form_id, value = value) + } + + if (form_type == "number") { + shiny::updateTextAreaInput(inputId = form_id, value = value) } # supress warnings when applying NA or NULL to date input form - if (type == "date") { + if (form_type == "date") { suppressWarnings( - shiny::updateDateInput(inputId = id, value = value) + shiny::updateDateInput(inputId = form_id, value = value) ) } # select_one - if (type == "select_one") { + if (form_type == "select_one") { # update choices - # old_choices <- subset(scheme, form_id == id, choices) |> dplyr::pull() + # old_choices <- subset(scheme, form_id == form_id, choices) |> dplyr::pull() # new_choices <- unique(c(old_choices, value)) # new_choices <- new_choices[!is.na(new_choices)] - # shiny::updateSelectizeInput(inputId = id, selected = value, choices = new_choices) - shiny::updateSelectizeInput(inputId = id, selected = value) + # shiny::updateSelectizeInput(inputId = form_id, selected = value, choices = new_choices) + shiny::updateSelectizeInput(inputId = form_id, selected = value) } # select_multiple # check if value is not NA and split by delimetr - if (type == "select_multiple" && !is.na(value)) { + if (form_type == "select_multiple" && !is.na(value)) { vars <- stringr::str_split_1(value, local_delimeter) # update choices - # old_choices <- subset(scheme, form_id == id, choices) |> dplyr::pull() + # old_choices <- subset(scheme, form_id == form_id, choices) |> dplyr::pull() # new_choices <- unique(c(old_choices, vars)) # new_choices <- new_choices[!is.na(new_choices)] - # shiny::updateSelectizeInput(inputId = id, selected = vars, choices = new_choices) - shiny::updateSelectizeInput(inputId = id, selected = vars) + # shiny::updateSelectizeInput(inputId = form_id, selected = vars, choices = new_choices) + shiny::updateSelectizeInput(inputId = form_id, selected = vars) } # in other case fill with `character(0)` to proper reseting form - if (type == "select_multiple" && is.na(value)) { - shiny::updateSelectizeInput(inputId = id, selected = character(0)) + if (form_type == "select_multiple" && is.na(value)) { + shiny::updateSelectizeInput(inputId = form_id, selected = character(0)) } # radio buttons - if (type == "radio" && !is.na(value)) { - shiny::updateRadioButtons(inputId = id, selected = value) + if (form_type == "radio" && !is.na(value)) { + shiny::updateRadioButtons(inputId = form_id, selected = value) } - if (type == "radio" && is.na(value)) { - shiny::updateRadioButtons(inputId = id, selected = character(0)) + if (form_type == "radio" && is.na(value)) { + shiny::updateRadioButtons(inputId = form_id, selected = character(0)) } # checkboxes - if (type == "checkbox" && !is.na(value)) { + if (form_type == "checkbox" && !is.na(value)) { vars <- stringr::str_split_1(value, local_delimeter) - shiny::updateCheckboxGroupInput(inputId = id, selected = vars) + shiny::updateCheckboxGroupInput(inputId = form_id, selected = vars) } - if (type == "checkbox" && is.na(value)) { - shiny::updateCheckboxGroupInput(inputId = id, selected = character(0)) + if (form_type == "checkbox" && is.na(value)) { + shiny::updateCheckboxGroupInput(inputId = form_id, selected = character(0)) } # if (type == "inline_table") { # message("EMPTY") # } } + +#' @export +clean_forms <- function(id_and_types_list, ns) { + + # если передана ns() функция то подмеяем id для каждой формы в соответствии с пространством имен + if (missing(ns)) ns <- NULL + + purrr::walk2( + .x = id_and_types_list, + .y = names(id_and_types_list), + .f = \(x_type, x_id) { + + # using function to update forms + update_forms_with_data( + form_id = x_id, + form_type = x_type, + value = get_empty_data(x_type), + ns = ns + ) + } + ) + +} \ No newline at end of file diff --git a/renv.lock b/renv.lock index 1724df8..38f11a7 100644 --- a/renv.lock +++ b/renv.lock @@ -530,6 +530,16 @@ ], "Hash": "b29cf3031f49b04ab9c852c912547eef" }, + "here": { + "Package": "here", + "Version": "1.0.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "rprojroot" + ], + "Hash": "24b224366f9c2e7534d2344d10d59211" + }, "highr": { "Package": "highr", "Version": "0.11", @@ -612,28 +622,6 @@ ], "Hash": "0080607b4a1a7b28979aecef976d8bc2" }, - "janitor": { - "Package": "janitor", - "Version": "2.2.0", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R", - "dplyr", - "hms", - "lifecycle", - "lubridate", - "magrittr", - "purrr", - "rlang", - "snakecase", - "stringi", - "stringr", - "tidyr", - "tidyselect" - ], - "Hash": "5baae149f1082f466df9d1442ba7aa65" - }, "jquerylib": { "Package": "jquerylib", "Version": "0.1.4", @@ -730,19 +718,6 @@ ], "Hash": "b8552d117e1b808b09a832f589b79035" }, - "lubridate": { - "Package": "lubridate", - "Version": "1.9.2", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R", - "generics", - "methods", - "timechange" - ], - "Hash": "e25f18436e3efd42c7c590a1c4c15390" - }, "magrittr": { "Package": "magrittr", "Version": "2.0.3", @@ -1019,6 +994,16 @@ ], "Hash": "3854c37590717c08c32ec8542a2e0a35" }, + "rprojroot": { + "Package": "rprojroot", + "Version": "2.0.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "4c8415e0ec1e29f3f4f6fc108bef0144" + }, "sass": { "Package": "sass", "Version": "0.4.9", @@ -1128,18 +1113,6 @@ ], "Hash": "fe6e75a1c1722b2d23cb4d4dbe1006df" }, - "snakecase": { - "Package": "snakecase", - "Version": "0.11.1", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R", - "stringi", - "stringr" - ], - "Hash": "58767e44739b76965332e8a4fe3f91f1" - }, "sourcetools": { "Package": "sourcetools", "Version": "0.1.7-1", @@ -1245,17 +1218,6 @@ ], "Hash": "79540e5fcd9e0435af547d885f184fd5" }, - "timechange": { - "Package": "timechange", - "Version": "0.2.0", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R", - "cpp11" - ], - "Hash": "8548b44f79a35ba1791308b61e6012d7" - }, "tinytex": { "Package": "tinytex", "Version": "0.46",