commit 7dcf291217f1592709686994fa70e7ceecffc0d7 Author: madeliri Date: Sun Mar 2 22:37:56 2025 +0300 0.14.1 go diff --git a/.Rprofile b/.Rprofile new file mode 100644 index 0000000..81b960f --- /dev/null +++ b/.Rprofile @@ -0,0 +1 @@ +source("renv/activate.R") diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3567fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/renv + +.Renviron +.DS_Store +.lintr + +*.sqlite +*.tar \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1610f37 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,68 @@ +### 0.??.? + +##### features +- added checkboxes input form; +- added button to reset data in forms; +- added option to export input data to `.docx` format (using `rmarkdown`), using `reference.docx` template; +- added new column in `main.xlsx` schema with `required` option: now you can set specifically which forms is required (1 - is required, NA - is not required) - this option now used in input validation (doesn't block saving data yet); +- checking on load if schema changed (comparing to existing db): + - if new input form added in schema - adding it also on database (with empty values); + - if input form deleted - stop app to prevent data loss; + - if input form was renamed - stop app to prevent data loss; + - in other cases - show warnings; + +##### fixes + +- fixed not erasing inputs while loading empty values (with checkboxes, radiobuttons); +- +number input validation + +##### changes + +- some code refactoring; +- replacing NumberImput to TextInput due to correct implement validation; + + + +### 0.14.1 2024-10-14 + +##### fixes + +- catching crash file due to bug in rhandsometable (fail to export tables with empty rows) + + + +### 0.14 2024-10-14 + +##### changes + +- code rafactoring +- add visual data validation + + + +### 0.13 2024-10-11 + +##### changes + +- moving script to init login db to separate file +- wider inline tables + + + +### 0.12 2024-09-29 + +##### fixes: + +- error while saving tables to db due to wrong data formats + +##### changes: + +- moving config.yml to configs folder +- moving schemas to configs folder + + +### 0.11 2024-09-23 + +##### fixes: +- error while loading table due to change PostrgreSQL driver +- error while export db as .xlsx due to misspelling in button name \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c653b3c --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +The MIT License (MIT) + +Copyright © 2025 @madeliri + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==================================== + +Данная лицензия разрешает лицам, получившим копию данного программного обеспечения и сопутствующей документации (далее — Программное обеспечение), безвозмездно использовать Программное обеспечение без ограничений, включая неограниченное право на использование, копирование, изменение, слияние, публикацию, распространение, сублицензирование и/или продажу копий Программного обеспечения, а также лицам, которым предоставляется данное Программное обеспечение, при соблюдении следующих условий: + +Указанное выше уведомление об авторском праве и данные условия должны быть включены во все копии или значимые части данного Программного обеспечения. + +ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ, НО НЕ ОГРАНИЧИВАЯСЬ ИМИ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО КАКИМ-ЛИБО ИСКАМ, ЗА УЩЕРБ ИЛИ ПО ИНЫМ ТРЕБОВАНИЯМ, В ТОМ ЧИСЛЕ, ПРИ ДЕЙСТВИИ КОНТРАКТА, ДЕЛИКТЕ ИЛИ ИНОЙ СИТУАЦИИ, ВОЗНИКШИМ ИЗ-ЗА ИСПОЛЬЗОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫХ ДЕЙСТВИЙ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..88613bf --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# not ready yet + +# О репозитории + +Данный проект представляет собой shiny-приложение (написанное на языке програмирования R), для заполнения каких-то данных и последующим экспортом в `.xlsx` формат. + +Структура формы (соответственно и базы) задается на осно + +Данные хранятся в базе данных `SQLite` (так же возможно использование `PostgreSQL`). + +# Зачем? + +... + + +# структура main.xlsx + +Файл, формирующий структуру всей форму, представляет собой таблицу в формате `.xlsx`, состоящий из следующих столбцов: + +- `part` - группировка первого уровня; +- `subgroup` - группировка второго уровня (наименование колонок); +- `form_id` - id; +- `form_label` - Название формы; +- `form_type` - тип формы, в настоящее время доступные следующие варианты: + - `text` - простой текст; + - `date` - дата; + - `select_one` - выбор одного варианта (выпадающий список); + - `select_multiple` - выбор нескольких вариантов (выпадающий список); + - `number` - число; + - `radio` - выбор одного варианта (radio buttons); + - `description` - описание (отображение текста, без формы выбора/ввода); + - `inline_table` - вложенная таблица (rhandsometables); +- `choices` - варианты выбора (если предполагаются типом формы ввода); +- `condition` - условие, при котором форма ввода будет отображаться; \ No newline at end of file diff --git a/app.R b/app.R new file mode 100644 index 0000000..6575419 --- /dev/null +++ b/app.R @@ -0,0 +1,1010 @@ +suppressPackageStartupMessages({ + library(DBI) + library(RSQLite) + library(tibble) + library(tidyr) + library(dplyr) + library(purrr) + library(magrittr) + library(shiny) + library(bslib) + library(rhandsontable) + library(shinymanager) +}) + +source("helpers/functions.R") + +# SOURCE FILES ============================ +config <- config::get(file = "configs/config.yml") + +folder_with_schemas <- fs::path("configs/schemas") +FILE_SCHEME <- fs::path(folder_with_schemas, "main.xlsx") +dbfile <- fs::path("data.sqlite") +DEBUG <- TRUE + +# TEMP ! NEED TO HANDLE +rmarkdown::find_pandoc(dir = "/opt/homebrew/bin/") + + +# SCHEME_MAIN UNPACK ========================== +# load scheme +SCHEME_MAIN <- readxl::read_xlsx(FILE_SCHEME) %>% + # fill NA down + fill(c(part, subgroup, form_id, form_label, form_type), .direction = "down") %>% + group_by(form_id) %>% + fill(condition, .direction = "down") %>% + ungroup() + +# get list of simple inputs +inputs_simple_list <- SCHEME_MAIN %>% + filter(!form_type %in% c("inline_table", "description")) %>% + distinct(form_id, form_type) %>% + deframe + +# get list of inputs with inline tables +inputs_tables_list <- SCHEME_MAIN %>% + filter(form_type == "inline_table") %>% + distinct(form_id) %>% + deframe + + +# SETUP DB ========================== +con <- DBI::dbConnect( + drv = RSQLite::SQLite(), + dbname = dbfile, +) + +# Init DB (write dummy data to "main" table) +if (!"main" %in% dbListTables(con)) { + + dummy_df <- get_dummy_df() %>% + mutate(id = "dummy") + + # write dummy df into base, then delete dummy row + DBI::dbWriteTable(con, "main", dummy_df, append = TRUE) + DBI::dbExecute(con, "DELETE FROM main WHERE id = 'dummy'") + + rm(dummy_df) +} + +# checking if db structure in form compatible with alrady writed data (in case on changig form) +if (identical(colnames(DBI::dbReadTable(con, "main")), names(inputs_simple_list))) { + print("identical") +} else { + df_to_rewrite <- DBI::dbReadTable(con, "main") + form_base_difference <- setdiff(names(inputs_simple_list), colnames(df_to_rewrite)) + base_form_difference <- setdiff(colnames(df_to_rewrite), names(inputs_simple_list)) + + # if lenght are equal + if (length(names(inputs_simple_list)) == length(colnames(df_to_rewrite)) && + length(form_base_difference) == 0 && + length(base_form_difference) == 0) { + warning("changes in scheme file detected: pressuming here simply changed order") + } + + if (length(names(inputs_simple_list)) == 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") + } + + if (length(names(inputs_simple_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") + + for (i in form_base_difference) { + df_to_rewrite <- df_to_rewrite %>% + mutate(!!sym(i) := get_empty_data(inputs_simple_list[i])) + } + df_to_rewrite <- df_to_rewrite %>% + select(all_of(names(inputs_simple_list))) + + DBI::dbWriteTable(con, "main", df_to_rewrite, overwrite = TRUE) + DBI::dbExecute(con, "DELETE FROM main WHERE id = 'dummy'") + } + + if (length(names(inputs_simple_list)) < length(colnames(df_to_rewrite))) { + stop("changes in scheme file detected: some of inputs form was deleted! it may cause data loss!") + } + + rm(df_to_rewrite, form_base_difference) + +} + + +# write dummies (for test purposes) +# purrr::map( +# .x = 1:300, +# .f = \(x) { +# dummy_df <- purrr::map2( +# .x = purrr::set_names(names(inputs_simple_list)), +# .y = inputs_simple_list, +# .f = \(x_id, y_type) { +# if (y_type %in% c("text", "select_one", "select_multiple")) return("dummy") +# if (y_type %in% c("radio")) return("dummy") +# if (y_type %in% c("date")) return(as.Date("1990-01-01")) +# if (y_type %in% c("number")) return(as.double(999)) +# } +# ) %>% +# as_tibble() %>% +# mutate(id = glue::glue("test{x}")) +# dbWriteTable(con, "main", dummy_df, append = TRUE) +# dbExecute(con, "DELETE FROM main WHERE id = 'dummy'") +# } +# ) + + +# INLINE TABLES ===================== +# создаем для каждой таблицы объект +inline_tables <- map( + .x = purrr::set_names(inputs_tables_list), + .f = \(x_inline_table_name) { + + # получить имя файла со схемой + file_name <- SCHEME_MAIN %>% + filter(form_id == x_inline_table_name) %>% + pull(choices) + + # load scheme + schemaaa <- readxl::read_xlsx(fs::path(folder_with_schemas, file_name)) %>% + fill(everything(), .direction = "down") + + # список форм в схеме + inline_forms <- schemaaa %>% + distinct(form_id) %>% + pull + + # макет таблицы (пустой) + 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 + + list(schema = schemaaa, df_empty = DF_gen) + } +) + + +# создание объектов для ввода +# функция +create_forms <- function(form_id, form_label, form_type) { + + # check if have condition + condition <- filter(SCHEME_MAIN, form_id == {{form_id}}) %>% distinct(condition) %>% pull + choices <- filter(SCHEME_MAIN, form_id == {{form_id}}) %>% pull(choices) + + # simple text input + if (form_type == "text") { + form <- shiny::textAreaInput( + inputId = form_id, + label = tags$span(style = "color: #444444; font-weight: 550;", form_label), + rows = 1 + ) + } + + # simple number input + if (form_type == "number") { + + form <- textAreaInput( + inputId = form_id, + label = tags$span(style = "color: #444444; font-weight: 550;", form_label), + rows = 1 + ) + } + + # simple date input + if (form_type == "date") { + # supress warning while trying keep data form empty by default + suppressWarnings({ + form <- dateInput( + inputId = form_id, + label = tags$span(style = "color: #444444; font-weight: 550;", form_label), + value = NA, # keep empty + format = "dd.mm.yyyy", + weekstart = 1, + language = "ru" + ) + }) + } + + # еденичный выбор + if (form_type == "select_one") { + form <- selectizeInput( + inputId = form_id, + label = tags$span(style = "color: #444444; font-weight: 550;", form_label), + choices = choices, + selected = NULL, + options = list( + # placeholder = "выберите из списка...", + create = FALSE, + onInitialize = I('function() { this.setValue(""); }') + ) + ) + } + + # множественный выбор + if (form_type == "select_multiple") { + form <- selectizeInput( + inputId = form_id, + label = tags$span(style = "color: #444444; font-weight: 550;", form_label), + choices = choices, + selected = NULL, + multiple = TRUE, + options = list( + # placeholder = "множественный выбор", + create = FALSE, + onInitialize = I('function() { this.setValue(""); }') + ) + ) + } + + # множественный выбор + if (form_type == "radio") { + form <- radioButtons( + inputId = form_id, + label = tags$span(style = "color: #444444; font-weight: 550;", form_label), + choices = choices, + selected = character(0) + ) + } + + if (form_type == "checkbox") { + form <- checkboxGroupInput( + inputId = form_id, + # label = tags$span(style = "color: #444444; font-weight: 550;", form_label), + label = h6(form_label), + choices = choices, + selected = character(0) + ) + } + + # вложенная таблица + if (form_type == "inline_table") { + form <- rHandsontableOutput(outputId = form_id) + } + + # вложенная таблица + if (form_type == "description") { + form <- div(HTML(form_label), style = "color:Gray;font-size: 90%;") + } + + # если есть условие создать кондитионал панель + if (!is.na(condition)) { + form <- conditionalPanel( + condition = condition, + form + ) + } + + form +} + + + +# GENERATE UI ================================== +# functions for making cards +make_cards_fn <- function(sub_group) { + + subgroups_inputs <- df_forms %>% + filter(subgroup == {{sub_group}}) %>% + distinct(form_id, form_label, form_type) + + card( + card_header(sub_group, container = htmltools::h5), + full_screen = TRUE, + width = "4000px", + card_body( + fill = TRUE, + # передаем все аргументы в функцию для создания елементов + purrr::pmap(subgroups_inputs, create_forms) + ) + ) +} + +# get pages list +pages_list <- unique(SCHEME_MAIN$part) + +# TODO: replace with unique(SCHEME_MAIN$part) + +# get all forms df +df_forms <- SCHEME_MAIN %>% + distinct(part, subgroup, form_id, form_label, form_type) + +# generate nav panels +nav_panels_list <- purrr::map( + .x = pages_list, + .f = \(x_page) { + + # get info about inputs for current page + page_forms <- SCHEME_MAIN %>% + filter(part == {{x_page}}) %>% + distinct(subgroup, form_id, form_label, form_type) + + # get list of columns + cols_list <- unique(page_forms$subgroup) + + # making cards + cards <- purrr::map( + .x = cols_list, + .f = make_cards_fn + ) + + # make page wrap + page_wrap <- layout_column_wrap( + width = "350px", height = NULL, #was 800 + fixed_width = TRUE, + !!!cards # unpack list of cards + ) + + # add panel wrap to nav_panel + nav_panel(x_page, page_wrap) + } +) + + + +# MODALS ======================== +# окно для подвтерждения очищения данных +modal_clean_all <- modalDialog( + "Данное действие очистит все заполненные данные. Убедитесь, что нужные данные сохранены.", + title = "Очистить форму?", + footer = tagList( + actionButton("cancel_button", "Отмена"), + actionButton("clean_all_action", "Очистить.", class = "btn btn-danger") + ), + 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 +) + + + + +# UI ======================= +ui <- page_sidebar( + title = config$header, + theme = bs_theme(version = 5, preset = "bootstrap"), + sidebar = sidebar( + actionButton("save_data_button", "Сохранить данные", icon("floppy-disk", lib = "font-awesome")), + actionButton("clean_data_button", "Очистить данные", icon("user-plus", lib = "font-awesome")), + textOutput("status_message"), + textOutput("status_message2"), + actionButton("load_data_button", "Загрузить данные", icon("pencil", lib = "font-awesome")), + downloadButton("downloadData", "Экспорт в .xlsx"), + downloadButton("downloadDocx", "get .docx (test only)") + ), + # list of rendered panels + navset_card_underline( + !!!nav_panels_list, + header = NULL + ) +) + +# init auth ======================= +ui <- shinymanager::secure_app(ui, enable_admin = TRUE) + + +# SERVER LOGIC ============================= +server <- function(input, output) { + + # AUTH SETUP ======================================== + # check_credentials directly on sqlite db + res_auth <- shinymanager::secure_server( + check_credentials = check_credentials( + db = "auth.sqlite", + passphrase = Sys.getenv("AUTH_DB_KEY") + ), + keep_token = TRUE + ) + + output$auth_output <- renderPrint({ + reactiveValuesToList(res_auth) + }) + + # REACTIVE VALUES ================================= + # Create a reactive values object to store the input data + values <- reactiveValues(data = NULL) + rhand_tables <- reactiveValues() + + # VALIDATIONS ============================ + # create new validataion + iv <- shinyvalidate::InputValidator$new() + + # add rules to all inputs + purrr::walk( + .x = names(inputs_simple_list), + .f = \(x_input_id) { + + form_type <- inputs_simple_list[[x_input_id]] + choices <- filter(SCHEME_MAIN, form_id == {{x_input_id}}) %>% pull(choices) + val_required <- filter(SCHEME_MAIN, form_id == {{x_input_id}}) %>% distinct(required) %>% pull(required) + + # for `number` type: if in `choices` column has values then parsing them to range validation + # value `0; 250` -> transform to rule validation value from 0 to 250 + if (form_type == "number") { + iv$add_rule(x_input_id, function(x) { + # exit if empty + if (check_for_empty_data(x)) return(NULL) + # check for numeric + if (grepl("^[-]?(\\d*\\,\\d+|\\d+\\,\\d*|\\d+)$", x)) NULL else "Значение должно быть числом." + }) + + if (!is.na(choices)) { + # разделить на числа + ranges <- as.integer(stringr::str_split_1(choices, "; ")) + + # проверка на кол-во значений + if (length(ranges) > 3) { + warning("Количество переданных элементов'", x_input_id, "' > 2") + } else { + iv$add_rule( + x_input_id, + function(x) { + # exit if empty + if (check_for_empty_data(x)) return(NULL) + # check for currect value + if (between(as.integer(x), ranges[1], ranges[2])) { + NULL + } else { + glue::glue("Значение должно быть между {ranges[1]} и {ranges[2]}.") + } + } + ) + } + } + } + + # if in `required` column value is `1` apply standart validation + if (!is.na(val_required) && val_required == 1) { + iv$add_rule(x_input_id, shinyvalidate::sv_required(message = "Необходимо заполнить.")) + } + } + ) + # enable validator + iv$enable() + + # STATUSES =============================== + # вывести отображение что что-то не так + output$status_message <- renderText({ + shiny::validate( + need(input$id, "⚠️ Необходимо указать id пациента!") + ) + paste0("ID: ", input$id) + }) + + output$status_message2 <- renderText({ + iv$is_valid() + # res_auth$admin + }) + + + # 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 %>% + distinct(form_id, form_label, form_type) + + # заголовки + headers <- pull(schema_comp, form_label) + + # fixes empty rows error + rownames(rhand_tables[[x]]) <- NULL + + # создать объект рандсонтебл + rh_tabel <- rhandsontable( + rhand_tables[[x]], + colHeaders = headers, + rowHeaders = NULL, + height = 400, + ) %>% + hot_cols(colWidths = 120, manualColumnResize = TRUE, columnSorting = TRUE) + + # циклом итерируемся по индексу; + for (i in seq(1, length(schema_comp$form_id))) { + + # получаем информацию о типе столбца + type <- filter(schema_comp, form_id == schema_comp$form_id[i]) %>% pull(form_type) + + # информация о воможных вариантнах выбора + choices <- filter(schema, form_id == schema_comp$form_id[i]) %>% 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 + purrr::walk2( + .x = inputs_simple_list, + .y = names(inputs_simple_list), + .f = \(x_type, x_id) { + + # using function to update forms + update_forms_with_data( + id = x_id, + type = x_type, + value = get_empty_data(x_type) + ) + } + ) + + # inline tables + purrr::walk( + .x = inputs_tables_list, + .f = \(x_table_name) { + rhand_tables[[x_table_name]] <- inline_tables[[x_table_name]]$df_empty + } + ) + + removeModal() + showNotification("Данные очищены!", type = "warning") + }) + + ## saving inputs to db ======================== + # сохранить простые данные; + observeEvent(input$save_data_button, { + req(input$id) + + ## MAIN + # собрать все значения по введенным данным; + result_df <- purrr::map( + .x = names(inputs_simple_list), + .f = \(x) { + type <- inputs_simple_list[[x]] + input_d <- input[[x]] + + # return empty if 0 element + if (length(input_d) == 0) return(get_empty_data(type)) + # return element if there one + if (length(input_d) == 1) return(input_d) + # если елементов больше одного - объединять через ";" + if (length(input_d) > 1) paste(input_d, collapse = "; ") + } + ) + + # make dataframe from that; + values$data <- setNames(result_df, names(inputs_simple_list)) %>% + as_tibble() + + if (length(dbListTables(con)) == 0) { + # если база пустая, то просто записываем данные + write_all_to_db() + } else if ("main" %in% dbListTables(con)) { + # если главная таблица существует, то проверяем существование id + + # GET DATA files + query <- glue::glue_sql(" + SELECT DISTINCT id + FROM main + WHERE id = {input$id} + ", .con = con) + + # получаем список записей с данным id + exist_main_df <- dbGetQuery(con, query) + + # проверка по наличию записей с данным ID в базе; + if (nrow(exist_main_df) == 0) { + # если данных нет - просто записать данные + log_action_to_db("save", input$id, con) + write_all_to_db() + } else { + # если есть выдать окно с подтверждением перезаписи + showModal(modal_overwrite) + } + } + }) + + + ## get list of id's from db ===================== + observeEvent(input$load_data_button, { + + if (length(dbListTables(con)) != 0 && "main" %in% dbListTables(con)) { + # GET DATA files + ids <- dbGetQuery(con, "SELECT DISTINCT id FROM main") %>% + pull + + output$load_menu <- renderUI({ + selectizeInput( + inputId = "read_id_selector", + label = NULL, + choices = ids, + selected = NULL, + options = list( + placeholder = "id пациента", + onInitialize = I('function() { this.setValue(""); }') + ) + ) + }) + } else { + output$load_menu <- renderUI({ + h5("База данных не содержит записей") + }) + } + + shiny::showModal(modal_load_patients) + }) + + ## load data to input forms ================================== + observeEvent(input$read_data, { + + # main df read + test_read_df <- read_df_from_db_by_id("main", con) + + # 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 (DEBUG) { + values_load <- test_read_df[[x_id]] + print(paste(x_type, x_id, values_load, sep = " || ")) + print(is.na(values_load)) + } + + # using function to update forms + update_forms_with_data( + id = x_id, + type = x_type, + value = test_read_df[[x_id]] + ) + } + ) + + # 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 + } + } + ) + 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"), + content = function(file) { + # get all data + list_of_df <- purrr::map( + .x = purrr::set_names(c("main", inputs_tables_list)), + .f = \(x) { + df <- read_df_from_db_all(x, con) %>% + tibble::as_tibble() + + # handle with data + if (nrow(df) >= 1 && x == "main") { + df <- df %>% + mutate(across(contains("date"), as.Date)) %>% + print() + } + df + } + ) + # set date params + options("openxlsx2.dateFormat" = "dd.mm.yyyy") + + print("DATA EXPORTED") + log_action_to_db("export db", con = con) + + # pass tables to export + openxlsx2::write_xlsx( + purrr::compact(list_of_df), + file, + na.strings = "", + as_table = TRUE, + col_widths = 15 + ) + } + ) + + ## export to .docx ==== + output$downloadDocx <- downloadHandler( + filename = function() { + paste0(input$id, "_", format(Sys.time(), "%Y%m%d_%H%M%S"), ".docx") + }, + content = function(file) { + + # prepare YAML sections + empty_vec <- c( + "---", + "output:", + " word_document:", + " reference_docx: reference.docx", + "---", + "\n" + ) + + # iterate by scheme parts + purrr::walk( + .x = unique(SCHEME_MAIN$part), + .f = \(x_iter1) { + + # write level 1 header + HEADER_1 <- paste("#", x_iter1, "\n") + empty_vec <<- c(empty_vec, HEADER_1) + + # iterate by level2 headers (subgroups) + purrr::walk( + .x = pull(unique(subset(SCHEME_MAIN, part == x_iter1, "subgroup"))), + .f = \(x_iter2) { + + # get header 2 name + HEADER_2 <- paste("##", x_iter2, "\n") + + # for some reason set litle scheme... + litle_scheme <- subset( + x = SCHEME_MAIN, + subset = part == x_iter1 & subgroup == x_iter2, + select = c("form_id", "form_label", "form_type") + ) |> + unique() + + # iterate by id in subgroups + VALUES <- purrr::map_chr( + .x = litle_scheme$form_id, + .f = \(x_id) { + + docx_type <- subset(litle_scheme, form_id == x_id, "form_type") + docx_label <- subset(litle_scheme, form_id == x_id, "form_label") + + # logic for render documemts + if (docx_type %in% c("text", "number", "date", "select_one", "select_multiple", "radio", "checkbox")) { + docx_value <- input[[x_id]] + + # if more than two objects: collapse + if (length(docx_value) > 1) docx_value <- paste(docx_value, collapse = ", ") + + # if non empty data - add string + if (!check_for_empty_data(docx_value)) paste0("**", docx_label, "**: ", docx_value, "\n") else NA + + } else if (docx_type == "description") { + # treat description label as citation text + paste0(">", docx_label, "\n") + } else { + paste0(docx_label, ": ", "NOT IMPLEMENTED YET", "\n") + } + } + ) + # append to vector parsed data + empty_vec <<- (c(empty_vec, HEADER_2, VALUES)) + } + ) + } + ) + + # set temp folder and names + temp_folder <- tempdir() + temp_report <- file.path(temp_folder, "rmarkdown_output.Rmd") + temp_template <- file.path(temp_folder, "reference.docx") + + # clean from NA strings + empty_vec <- empty_vec[!is.na(empty_vec)] + + # write vector to temp .Rmd file + writeLines(empty_vec, temp_report, sep = "\n") + # copy template .docx file + file.copy("references/reference.docx", temp_template, overwrite = TRUE) + + # render file via pandoc + rmarkdown::render( + temp_report, + output_file = file, + output_format = "word_document", + envir = new.env(parent = globalenv()) + ) + } + ) + + ## trigger saving function ============= + observeEvent(input$data_save, { + # убираем плашку + removeModal() + + # записываем данные + write_all_to_db() + log_action_to_db("overwrite", input$id, con = con) + }) + + ## cancel ========================== + observeEvent(input$cancel_button, { + # убираем плашку + removeModal() + }) + + # FUNCTIONS ============================== + ## write all inputs to db ================ + write_all_to_db <- function() { + + # write main + write_df_to_db(values$data, "main", con) + + # 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() + } + ) + + df <- df %>% + as_tibble() %>% + janitor::remove_empty(which = c("rows")) %>% + # adding id to dbs + mutate(id = input$id, .before = 1) + + # если таблица содержит хоть одну строку - сохранить таблицу в базу данных + if (nrow(df) != 0) { + write_df_to_db(df, i, con) + removeNotification(paste0(i, "error_inline_tables")) + } + } + + showNotification( + glue::glue("Данные пациента {input$id} сохранены!"), + type = "warning" + ) + } + + ## helper function writing dbs ======== + write_df_to_db <- function(df, table_name, con) { + + # 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) { + # 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) + } + } + + ## reading tables from db all ======== + read_df_from_db_all <- function(table_name, con) { + # check if this table exist + if (table_name %in% dbListTables(con)) { + # prepare query + query <- glue::glue(" + SELECT * FROM {table_name} + ") + + # get table as df + DBI::dbGetQuery(con, query) + } + } + + ## LOGGING ACTIONS + log_action_to_db <- function(action, pat_id = as.character(NA), con) { + action_row <- tibble( + user = res_auth$user, + action = action, + id = pat_id, + date = Sys.time() + ) + DBI::dbWriteTable(con, "log", action_row, append = TRUE) + } + +} + +options(shiny.port = config$shiny_port) +options(shiny.host = config$shiny_host) + +app <- shinyApp(ui = ui, server = server) + +runApp(app, launch.browser = TRUE) \ No newline at end of file diff --git a/configs/config.yml b/configs/config.yml new file mode 100644 index 0000000..f4e0414 --- /dev/null +++ b/configs/config.yml @@ -0,0 +1,6 @@ +default: + header: "TEST" + version: "0.14.1" + # shiny serve option + shiny_host: "127.0.0.1" + shiny_port: 1337 \ No newline at end of file diff --git a/configs/schemas/example_inline.xlsx b/configs/schemas/example_inline.xlsx new file mode 100644 index 0000000..18ec741 Binary files /dev/null and b/configs/schemas/example_inline.xlsx differ diff --git a/configs/schemas/main.xlsx b/configs/schemas/main.xlsx new file mode 100644 index 0000000..4c83c5b Binary files /dev/null and b/configs/schemas/main.xlsx differ diff --git a/helpers/functions.R b/helpers/functions.R new file mode 100644 index 0000000..617366f --- /dev/null +++ b/helpers/functions.R @@ -0,0 +1,106 @@ + +get_dummy_data <- function(type) { + if (type %in% c("text", "select_one", "select_multiple")) return("dummy") + if (type %in% c("radio", "checkbox")) return("dummy") + if (type %in% c("date")) return(as.Date("1990-01-01")) + if (type %in% c("number")) return(as.double(999)) +} + +get_empty_data <- function(type) { + if (type %in% c("text", "select_one", "select_multiple")) return(as.character(NA)) + if (type %in% c("radio", "checkbox")) return(as.character(NA)) + if (type %in% c("date")) return(as.Date(NA)) + if (type %in% c("number")) return(as.character(NA)) +} + +get_dummy_df <- function() { + purrr::map( + .x = inputs_simple_list, + .f = get_empty_data + ) %>% + as_tibble() +} + + +#' @description Function check if variable contains some sort of empty data +#' (NULL, NA, "", other 0-length data) and return `TRUE` (`FALSE` if data is +#' not 'empty'). +#' +#' Needed for proper data validation. +check_for_empty_data <- function(value_to_check) { + # for any 0-length + if (length(value_to_check) == 0) return(TRUE) + + # for NA + if (is.logical(value_to_check) && is.na(value_to_check)) return(TRUE) + + # for NULL + if (is.null(value_to_check)) return(TRUE) + + # for non-empty Date (RETURN FALSE) + if (inherits(value_to_check, "Date") && length(value_to_check) != 0) return(FALSE) + + # for empty strings (stands before checking non-empty data for avoid mistakes) + if (value_to_check == "") return(TRUE) + + FALSE +} + + +#' @description Function update input forms. +#' @param id - input form id; +#' @param type - type of form; +#' @param value - value to update; +update_forms_with_data <- function(id, type, value) { + if (type == "text") { + shiny::updateTextAreaInput(inputId = id, value = value) + } + + if (type == "number") { + shiny::updateTextAreaInput(inputId = id, value = value) + } + + # supress warnings when applying NA or NULL to date input form + if (type == "date") { + suppressWarnings( + shiny::updateDateInput(inputId = id, value = value) + ) + } + + # select_one + if (type == "select_one") { + shiny::updateSelectizeInput(inputId = id, selected = value) + } + + # select_multiple + # check if value is not NA and split by delimetr + if (type == "select_multiple" && !is.na(value)) { + vars <- stringr::str_split_1(value, "; ") + shiny::updateSelectizeInput(inputId = 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)) + } + + # radio buttons + if (type == "radio" && !is.na(value)) { + shiny::updateRadioButtons(inputId = id, selected = value) + } + if (type == "radio" && is.na(value)) { + shiny::updateRadioButtons(inputId = id, selected = character(0)) + } + + # checkboxes + if (type == "checkbox" && !is.na(value)) { + vars <- stringr::str_split_1(value, "; ") + shiny::updateCheckboxGroupInput(inputId = id, selected = vars) + } + if (type == "checkbox" && is.na(value)) { + shiny::updateCheckboxGroupInput(inputId = id, selected = character(0)) + } + + if (type == "inline_table") { + message("EMPTY") + } +} \ No newline at end of file diff --git a/helpers/init_login_db.r b/helpers/init_login_db.r new file mode 100644 index 0000000..3031bfa --- /dev/null +++ b/helpers/init_login_db.r @@ -0,0 +1,19 @@ +# script to setup authentification database (using shinymanager) + +# SETUP AUTH ============================= +# Init DB using credentials data +credentials <- data.frame( + user = c("admin", "user"), + password = c("admin", "user"), + # password will automatically be hashed + admin = c(TRUE, FALSE), + stringsAsFactors = FALSE +) + +# Init the database +shinymanager::create_db( + credentials_data = credentials, + sqlite_path = "auth.sqlite", # will be created + passphrase = Sys.getenv("AUTH_DB_KEY") + # passphrase = "passphrase_wihtout_keyring" +) diff --git a/references/reference.docx b/references/reference.docx new file mode 100644 index 0000000..394e6a2 Binary files /dev/null and b/references/reference.docx differ diff --git a/renv.lock b/renv.lock new file mode 100644 index 0000000..a2c8a65 --- /dev/null +++ b/renv.lock @@ -0,0 +1,1383 @@ +{ + "R": { + "Version": "4.3.1", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://packagemanager.posit.co/cran/latest" + } + ] + }, + "Packages": { + "DBI": { + "Package": "DBI", + "Version": "1.2.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "065ae649b05f1ff66bb0c793107508f5" + }, + "DT": { + "Package": "DT", + "Version": "0.33", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "crosstalk", + "htmltools", + "htmlwidgets", + "httpuv", + "jquerylib", + "jsonlite", + "magrittr", + "promises" + ], + "Hash": "64ff3427f559ce3f2597a4fe13255cb6" + }, + "MASS": { + "Package": "MASS", + "Version": "7.3-60", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "methods", + "stats", + "utils" + ], + "Hash": "a56a6365b3fa73293ea8d084be0d9bb0" + }, + "Matrix": { + "Package": "Matrix", + "Version": "1.5-4.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "graphics", + "grid", + "lattice", + "methods", + "stats", + "utils" + ], + "Hash": "38082d362d317745fb932e13956dccbb" + }, + "R.methodsS3": { + "Package": "R.methodsS3", + "Version": "1.8.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "utils" + ], + "Hash": "278c286fd6e9e75d0c2e8f731ea445c8" + }, + "R.oo": { + "Package": "R.oo", + "Version": "1.26.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R.methodsS3", + "methods", + "utils" + ], + "Hash": "4fed809e53ddb5407b3da3d0f572e591" + }, + "R.utils": { + "Package": "R.utils", + "Version": "2.12.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R.methodsS3", + "R.oo", + "methods", + "tools", + "utils" + ], + "Hash": "3dc2829b790254bfba21e60965787651" + }, + "R6": { + "Package": "R6", + "Version": "2.5.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "470851b6d5d0ac559e9d01bb352b4021" + }, + "RColorBrewer": { + "Package": "RColorBrewer", + "Version": "1.1-3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "45f0398006e83a5b10b72a90663d8d8c" + }, + "RPostgres": { + "Package": "RPostgres", + "Version": "1.4.7", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "DBI", + "R", + "bit64", + "blob", + "cpp11", + "hms", + "lubridate", + "methods", + "plogr", + "withr" + ], + "Hash": "beb7e18bf3f9e096f716a52a77ec793c" + }, + "RSQLite": { + "Package": "RSQLite", + "Version": "2.3.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "DBI", + "R", + "bit64", + "blob", + "cpp11", + "memoise", + "methods", + "pkgconfig", + "plogr", + "rlang" + ], + "Hash": "4bf2af2d714d0ee2b4fec0e6b8c150e6" + }, + "Rcpp": { + "Package": "Rcpp", + "Version": "1.0.11", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "methods", + "utils" + ], + "Hash": "ae6cbbe1492f4de79c45fce06f967ce8" + }, + "askpass": { + "Package": "askpass", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "sys" + ], + "Hash": "cad6cf7f1d5f6e906700b9d3e718c796" + }, + "base64enc": { + "Package": "base64enc", + "Version": "0.1-3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "543776ae6848fde2f48ff3816d0628bc" + }, + "billboarder": { + "Package": "billboarder", + "Version": "0.5.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "ggplot2", + "htmltools", + "htmlwidgets", + "jsonlite", + "magrittr", + "rlang", + "scales", + "shiny" + ], + "Hash": "8cfc73709bd06a57f4039fe15083b238" + }, + "bit": { + "Package": "bit", + "Version": "4.0.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "d242abec29412ce988848d0294b208fd" + }, + "bit64": { + "Package": "bit64", + "Version": "4.0.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "bit", + "methods", + "stats", + "utils" + ], + "Hash": "9fe98599ca456d6552421db0d6772d8f" + }, + "blob": { + "Package": "blob", + "Version": "1.2.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "methods", + "rlang", + "vctrs" + ], + "Hash": "40415719b5a479b87949f3aa0aee737c" + }, + "bslib": { + "Package": "bslib", + "Version": "0.8.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "base64enc", + "cachem", + "fastmap", + "grDevices", + "htmltools", + "jquerylib", + "jsonlite", + "lifecycle", + "memoise", + "mime", + "rlang", + "sass" + ], + "Hash": "b299c6741ca9746fb227debcb0f9fb6c" + }, + "cachem": { + "Package": "cachem", + "Version": "1.0.8", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "fastmap", + "rlang" + ], + "Hash": "c35768291560ce302c0a6589f92e837d" + }, + "cellranger": { + "Package": "cellranger", + "Version": "1.1.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "rematch", + "tibble" + ], + "Hash": "f61dbaec772ccd2e17705c1e872e9e7c" + }, + "cli": { + "Package": "cli", + "Version": "3.6.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "utils" + ], + "Hash": "89e6d8219950eac806ae0c489052048a" + }, + "colorspace": { + "Package": "colorspace", + "Version": "2.1-0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "grDevices", + "graphics", + "methods", + "stats" + ], + "Hash": "f20c47fd52fae58b4e377c37bb8c335b" + }, + "commonmark": { + "Package": "commonmark", + "Version": "1.9.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "d691c61bff84bd63c383874d2d0c3307" + }, + "config": { + "Package": "config", + "Version": "0.3.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "yaml" + ], + "Hash": "8b7222e9d9eb5178eea545c0c4d33fc2" + }, + "cpp11": { + "Package": "cpp11", + "Version": "0.4.7", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "5a295d7d963cc5035284dcdbaf334f4e" + }, + "crayon": { + "Package": "crayon", + "Version": "1.5.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "grDevices", + "methods", + "utils" + ], + "Hash": "e8a1e41acf02548751f45c718d55aa6a" + }, + "crosstalk": { + "Package": "crosstalk", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R6", + "htmltools", + "jsonlite", + "lazyeval" + ], + "Hash": "ab12c7b080a57475248a30f4db6298c0" + }, + "digest": { + "Package": "digest", + "Version": "0.6.33", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "b18a9cf3c003977b0cc49d5e76ebe48d" + }, + "dplyr": { + "Package": "dplyr", + "Version": "1.1.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "cli", + "generics", + "glue", + "lifecycle", + "magrittr", + "methods", + "pillar", + "rlang", + "tibble", + "tidyselect", + "utils", + "vctrs" + ], + "Hash": "e85ffbebaad5f70e1a2e2ef4302b4949" + }, + "ellipsis": { + "Package": "ellipsis", + "Version": "0.3.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "rlang" + ], + "Hash": "bb0eec2fe32e88d9e2836c2f73ea2077" + }, + "evaluate": { + "Package": "evaluate", + "Version": "0.21", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "d59f3b464e8da1aef82dc04b588b8dfb" + }, + "fansi": { + "Package": "fansi", + "Version": "1.0.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "utils" + ], + "Hash": "3e8583a60163b4bc1a80016e63b9959e" + }, + "farver": { + "Package": "farver", + "Version": "2.1.1", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "8106d78941f34855c440ddb946b8f7a5" + }, + "fastmap": { + "Package": "fastmap", + "Version": "1.1.1", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "f7736a18de97dea803bde0a2daaafb27" + }, + "fontawesome": { + "Package": "fontawesome", + "Version": "0.5.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "htmltools", + "rlang" + ], + "Hash": "c2efdd5f0bcd1ea861c2d4e2a883a67d" + }, + "fs": { + "Package": "fs", + "Version": "1.6.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "47b5f30c720c23999b913a1a635cf0bb" + }, + "generics": { + "Package": "generics", + "Version": "0.1.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "15e9634c0fcd294799e9b2e929ed1b86" + }, + "ggplot2": { + "Package": "ggplot2", + "Version": "3.4.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "MASS", + "R", + "cli", + "glue", + "grDevices", + "grid", + "gtable", + "isoband", + "lifecycle", + "mgcv", + "rlang", + "scales", + "stats", + "tibble", + "vctrs", + "withr" + ], + "Hash": "313d31eff2274ecf4c1d3581db7241f9" + }, + "glue": { + "Package": "glue", + "Version": "1.6.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "4f2596dfb05dac67b9dc558e5c6fba2e" + }, + "gtable": { + "Package": "gtable", + "Version": "0.3.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "grid", + "lifecycle", + "rlang" + ], + "Hash": "b29cf3031f49b04ab9c852c912547eef" + }, + "here": { + "Package": "here", + "Version": "1.0.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "rprojroot" + ], + "Hash": "24b224366f9c2e7534d2344d10d59211" + }, + "highr": { + "Package": "highr", + "Version": "0.10", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "xfun" + ], + "Hash": "06230136b2d2b9ba5805e1963fa6e890" + }, + "hms": { + "Package": "hms", + "Version": "1.1.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "lifecycle", + "methods", + "pkgconfig", + "rlang", + "vctrs" + ], + "Hash": "b59377caa7ed00fa41808342002138f9" + }, + "htmltools": { + "Package": "htmltools", + "Version": "0.5.8.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "base64enc", + "digest", + "fastmap", + "grDevices", + "rlang", + "utils" + ], + "Hash": "81d371a9cc60640e74e4ab6ac46dcedc" + }, + "htmlwidgets": { + "Package": "htmlwidgets", + "Version": "1.6.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "grDevices", + "htmltools", + "jsonlite", + "knitr", + "rmarkdown", + "yaml" + ], + "Hash": "04291cc45198225444a397606810ac37" + }, + "httpuv": { + "Package": "httpuv", + "Version": "1.6.12", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "Rcpp", + "later", + "promises", + "utils" + ], + "Hash": "c992f75861325961c29a188b45e549f7" + }, + "isoband": { + "Package": "isoband", + "Version": "0.2.7", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "grid", + "utils" + ], + "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", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "htmltools" + ], + "Hash": "5aab57a3bd297eee1c1d862735972182" + }, + "jsonlite": { + "Package": "jsonlite", + "Version": "1.8.7", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "methods" + ], + "Hash": "266a20443ca13c65688b2116d5220f76" + }, + "knitr": { + "Package": "knitr", + "Version": "1.44", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "evaluate", + "highr", + "methods", + "tools", + "xfun", + "yaml" + ], + "Hash": "60885b9f746c9dfaef110d070b5f7dc0" + }, + "labeling": { + "Package": "labeling", + "Version": "0.4.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "graphics", + "stats" + ], + "Hash": "b64ec208ac5bc1852b285f665d6368b3" + }, + "later": { + "Package": "later", + "Version": "1.3.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "Rcpp", + "rlang" + ], + "Hash": "40401c9cf2bc2259dfe83311c9384710" + }, + "lattice": { + "Package": "lattice", + "Version": "0.21-8", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "grid", + "stats", + "utils" + ], + "Hash": "0b8a6d63c8770f02a8b5635f3c431e6b" + }, + "lazyeval": { + "Package": "lazyeval", + "Version": "0.2.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "d908914ae53b04d4c0c0fd72ecc35370" + }, + "lifecycle": { + "Package": "lifecycle", + "Version": "1.0.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "rlang" + ], + "Hash": "001cecbeac1cff9301bdc3775ee46a86" + }, + "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", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "7ce2733a9826b3aeb1775d56fd305472" + }, + "memoise": { + "Package": "memoise", + "Version": "2.0.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "cachem", + "rlang" + ], + "Hash": "e2817ccf4a065c5d9d7f2cfbe7c1d78c" + }, + "mgcv": { + "Package": "mgcv", + "Version": "1.8-42", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "Matrix", + "R", + "graphics", + "methods", + "nlme", + "splines", + "stats", + "utils" + ], + "Hash": "3460beba7ccc8946249ba35327ba902a" + }, + "mime": { + "Package": "mime", + "Version": "0.12", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "tools" + ], + "Hash": "18e9c28c1d3ca1560ce30658b22ce104" + }, + "munsell": { + "Package": "munsell", + "Version": "0.5.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "colorspace", + "methods" + ], + "Hash": "6dfe8bf774944bd5595785e3229d8771" + }, + "nlme": { + "Package": "nlme", + "Version": "3.1-162", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "graphics", + "lattice", + "stats", + "utils" + ], + "Hash": "0984ce8da8da9ead8643c5cbbb60f83e" + }, + "openssl": { + "Package": "openssl", + "Version": "2.1.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "askpass" + ], + "Hash": "2a0dc8c6adfb6f032e4d4af82d258ab5" + }, + "openxlsx2": { + "Package": "openxlsx2", + "Version": "1.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "Rcpp", + "grDevices", + "magrittr", + "stringi", + "utils", + "zip" + ], + "Hash": "02d543303f348bff0663a6bcaa23196b" + }, + "pillar": { + "Package": "pillar", + "Version": "1.9.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "cli", + "fansi", + "glue", + "lifecycle", + "rlang", + "utf8", + "utils", + "vctrs" + ], + "Hash": "15da5a8412f317beeee6175fbc76f4bb" + }, + "pkgconfig": { + "Package": "pkgconfig", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "utils" + ], + "Hash": "01f28d4278f15c76cddbea05899c5d6f" + }, + "plogr": { + "Package": "plogr", + "Version": "0.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "09eb987710984fc2905c7129c7d85e65" + }, + "prettyunits": { + "Package": "prettyunits", + "Version": "1.1.1", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "95ef9167b75dde9d2ccc3c7528393e7e" + }, + "progress": { + "Package": "progress", + "Version": "1.2.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R6", + "crayon", + "hms", + "prettyunits" + ], + "Hash": "14dc9f7a3c91ebb14ec5bb9208a07061" + }, + "promises": { + "Package": "promises", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R6", + "Rcpp", + "fastmap", + "later", + "magrittr", + "rlang", + "stats" + ], + "Hash": "0d8a15c9d000970ada1ab21405387dee" + }, + "purrr": { + "Package": "purrr", + "Version": "1.0.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "lifecycle", + "magrittr", + "rlang", + "vctrs" + ], + "Hash": "1cba04a4e9414bdefc9dcaa99649a8dc" + }, + "rappdirs": { + "Package": "rappdirs", + "Version": "0.3.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "5e3c5dc0b071b21fa128676560dbe94d" + }, + "readxl": { + "Package": "readxl", + "Version": "1.4.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cellranger", + "cpp11", + "progress", + "tibble", + "utils" + ], + "Hash": "8cf9c239b96df1bbb133b74aef77ad0a" + }, + "rematch": { + "Package": "rematch", + "Version": "2.0.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "cbff1b666c6fa6d21202f07e2318d4f1" + }, + "renv": { + "Package": "renv", + "Version": "1.0.7", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "utils" + ], + "Hash": "397b7b2a265bc5a7a06852524dabae20" + }, + "rhandsontable": { + "Package": "rhandsontable", + "Version": "0.3.8", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "htmlwidgets", + "jsonlite", + "magrittr", + "methods", + "utils" + ], + "Hash": "cc9b9fd1376181e84c88621711454676" + }, + "rlang": { + "Package": "rlang", + "Version": "1.1.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "utils" + ], + "Hash": "a85c767b55f0bf9b7ad16c6d7baee5bb" + }, + "rmarkdown": { + "Package": "rmarkdown", + "Version": "2.24", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "bslib", + "evaluate", + "fontawesome", + "htmltools", + "jquerylib", + "jsonlite", + "knitr", + "methods", + "stringr", + "tinytex", + "tools", + "utils", + "xfun", + "yaml" + ], + "Hash": "3854c37590717c08c32ec8542a2e0a35" + }, + "rprojroot": { + "Package": "rprojroot", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "1de7ab598047a87bba48434ba35d497d" + }, + "sass": { + "Package": "sass", + "Version": "0.4.9", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R6", + "fs", + "htmltools", + "rappdirs", + "rlang" + ], + "Hash": "d53dbfddf695303ea4ad66f86e99b95d" + }, + "scales": { + "Package": "scales", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "RColorBrewer", + "farver", + "labeling", + "lifecycle", + "munsell", + "rlang", + "viridisLite" + ], + "Hash": "906cb23d2f1c5680b8ce439b44c6fa63" + }, + "scrypt": { + "Package": "scrypt", + "Version": "0.1.6", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "Rcpp" + ], + "Hash": "5669c376aea6546ff938528588101b7a" + }, + "shiny": { + "Package": "shiny", + "Version": "1.8.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "bslib", + "cachem", + "commonmark", + "crayon", + "ellipsis", + "fastmap", + "fontawesome", + "glue", + "grDevices", + "htmltools", + "httpuv", + "jsonlite", + "later", + "lifecycle", + "methods", + "mime", + "promises", + "rlang", + "sourcetools", + "tools", + "utils", + "withr", + "xtable" + ], + "Hash": "3a1f41807d648a908e3c7f0334bf85e6" + }, + "shinymanager": { + "Package": "shinymanager", + "Version": "1.0.410", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "DBI", + "DT", + "R.utils", + "R6", + "RSQLite", + "billboarder", + "htmltools", + "openssl", + "scrypt", + "shiny" + ], + "Hash": "7992f5ecc5e8d40200dc8ba4e97d33eb" + }, + "shinyvalidate": { + "Package": "shinyvalidate", + "Version": "0.1.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "glue", + "htmltools", + "rlang", + "shiny" + ], + "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", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "5f5a7629f956619d519205ec475fe647" + }, + "stringi": { + "Package": "stringi", + "Version": "1.7.12", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "stats", + "tools", + "utils" + ], + "Hash": "ca8bd84263c77310739d2cf64d84d7c9" + }, + "stringr": { + "Package": "stringr", + "Version": "1.5.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "magrittr", + "rlang", + "stringi", + "vctrs" + ], + "Hash": "671a4d384ae9d32fc47a14e98bfa3dc8" + }, + "sys": { + "Package": "sys", + "Version": "3.4.2", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "3a1be13d68d47a8cd0bfd74739ca1555" + }, + "tibble": { + "Package": "tibble", + "Version": "3.2.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "fansi", + "lifecycle", + "magrittr", + "methods", + "pillar", + "pkgconfig", + "rlang", + "utils", + "vctrs" + ], + "Hash": "a84e2cc86d07289b3b6f5069df7a004c" + }, + "tidyr": { + "Package": "tidyr", + "Version": "1.3.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "cpp11", + "dplyr", + "glue", + "lifecycle", + "magrittr", + "purrr", + "rlang", + "stringr", + "tibble", + "tidyselect", + "utils", + "vctrs" + ], + "Hash": "e47debdc7ce599b070c8e78e8ac0cfcf" + }, + "tidyselect": { + "Package": "tidyselect", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "rlang", + "vctrs", + "withr" + ], + "Hash": "79540e5fcd9e0435af547d885f184fd5" + }, + "timechange": { + "Package": "timechange", + "Version": "0.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cpp11" + ], + "Hash": "8548b44f79a35ba1791308b61e6012d7" + }, + "tinytex": { + "Package": "tinytex", + "Version": "0.46", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "xfun" + ], + "Hash": "0c41a73214d982f539c56a7773c7afa5" + }, + "utf8": { + "Package": "utf8", + "Version": "1.2.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "62b65c52671e6665f803ff02954446e9" + }, + "vctrs": { + "Package": "vctrs", + "Version": "0.6.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "rlang" + ], + "Hash": "266c1ca411266ba8f365fcc726444b87" + }, + "viridisLite": { + "Package": "viridisLite", + "Version": "0.4.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "c826c7c4241b6fc89ff55aaea3fa7491" + }, + "withr": { + "Package": "withr", + "Version": "2.5.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "stats" + ], + "Hash": "4b25e70111b7d644322e9513f403a272" + }, + "xfun": { + "Package": "xfun", + "Version": "0.40", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "stats", + "tools" + ], + "Hash": "be07d23211245fc7d4209f54c4e4ffc8" + }, + "xtable": { + "Package": "xtable", + "Version": "1.8-4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "stats", + "utils" + ], + "Hash": "b8acdf8af494d9ec19ccb2481a9b11c2" + }, + "yaml": { + "Package": "yaml", + "Version": "2.3.7", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "0d0056cc5383fbc240ccd0cb584bf436" + }, + "zip": { + "Package": "zip", + "Version": "2.3.0", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "d98c94dacb7e0efcf83b0a133a705504" + } + } +}