diff --git a/.gitignore b/.gitignore
index 2073ab2..bf2c9c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,10 +58,13 @@ py/__pycache__
static_files/
../env1
logs.log
-
+# Google OAuth tokens
+.secrets/
+google-service-account*.json
<<<<<<< HEAD
venv/
=======
static_files/*
!static_files/labelling_file.xlsx
>>>>>>> f7f1675 (Add labelling file)
+.secrets
diff --git a/WWW/manuals/user_manual.pdf b/WWW/manuals/user_manual.pdf
new file mode 100644
index 0000000..154dfe1
Binary files /dev/null and b/WWW/manuals/user_manual.pdf differ
diff --git a/server.R b/server.R
index 7833ecd..bc46acd 100644
--- a/server.R
+++ b/server.R
@@ -17,6 +17,9 @@ function(input, output, session){
source("server/auth.R")
source("server/homepage_translation_labels.R", local = TRUE)
+ source("server/google_sheets_feedback.R", local = TRUE)
+ source("server/feedback_form_server.R", local = TRUE)
+
register_homepage_labels(output, session, get_rv_labels)
USER = user_auth(input, output, session)
@@ -68,6 +71,8 @@ function(input, output, session){
source("server/history_visualize_auto_actions.R", local = TRUE)
source("server/history_visualize_custom_actions.R", local = TRUE)
+ add_resources_translation(output, session)
+ feedback_form_server(input, output, session, USER)
api_base <- reactive({
val <- input$fastapi_base
if (is.null(val) || !nzchar(trimws(val))) {
diff --git a/server/feedback_form_server.R b/server/feedback_form_server.R
new file mode 100644
index 0000000..ffe4242
--- /dev/null
+++ b/server/feedback_form_server.R
@@ -0,0 +1,533 @@
+# server/feedback_form_server.R
+
+feedback_form_server <- function(input, output, session, USER = NULL) {
+
+ feedback_status <- reactiveVal(NULL)
+ submission_in_progress <- reactiveVal(FALSE)
+
+
+ # Retrieve translated labels from the platform translation system.
+ lbl <- function(key) {
+
+ value <- tryCatch(
+ get_rv_labels(key),
+ error = function(e) NULL
+ )
+
+ if (
+ is.null(value) ||
+ length(value) == 0 ||
+ is.na(value[1]) ||
+ !nzchar(trimws(as.character(value[1])))
+ ) {
+ return(key)
+ }
+
+ as.character(value[1])
+ }
+
+
+ # Safely convert input values to a single character value.
+ safe_character <- function(value) {
+
+ if (is.null(value) || length(value) == 0) {
+ return("")
+ }
+
+ value <- as.character(value[1])
+
+ if (is.na(value)) {
+ return("")
+ }
+
+ trimws(value)
+ }
+
+
+ output$feedback_status_ui <- renderUI({
+
+ status <- feedback_status()
+
+ if (is.null(status)) {
+ return(NULL)
+ }
+
+ if (identical(status$type, "success")) {
+
+ div(
+ style = "
+ padding: 14px 18px;
+ border-radius: 12px;
+ background: #e8f7ee;
+ color: #176b3a;
+ font-weight: 700;
+ border: 1px solid #bce5c8;
+ ",
+ icon("circle-check"),
+ status$message
+ )
+
+ } else if (identical(status$type, "warning")) {
+
+ div(
+ style = "
+ padding: 14px 18px;
+ border-radius: 12px;
+ background: #fff8e1;
+ color: #8a5a00;
+ font-weight: 700;
+ border: 1px solid #f0d98c;
+ ",
+ icon("triangle-exclamation"),
+ status$message
+ )
+
+ } else {
+
+ div(
+ style = "
+ padding: 14px 18px;
+ border-radius: 12px;
+ background: #fdecea;
+ color: #b42318;
+ font-weight: 700;
+ border: 1px solid #f5c2c0;
+ ",
+ icon("circle-exclamation"),
+ status$message
+ )
+ }
+ })
+
+
+ # Disable the submit button while a submission is being processed.
+ observe({
+
+ shinyjs::toggleState(
+ id = "submit_feedback",
+ condition = !submission_in_progress()
+ )
+ })
+
+
+ observeEvent(input$submit_feedback, {
+
+ if (submission_in_progress()) {
+ return()
+ }
+
+ feedback_status(NULL)
+
+
+ # Read and clean selected form values.
+ task <- safe_character(input$feedback_task)
+
+ contact_permission <- safe_character(
+ input$feedback_contact_permission
+ )
+
+ email_address <- safe_character(
+ input$feedback_email
+ )
+
+
+ # Validate required task description.
+ if (!nzchar(task)) {
+
+ feedback_status(list(
+ type = "error",
+ message = lbl("feedback_required_message")
+ ))
+
+ return()
+ }
+
+
+ # Validate email when the user asks to be contacted.
+ yes_label <- lbl("yes")
+
+ if (
+ identical(contact_permission, yes_label) &&
+ !nzchar(email_address)
+ ) {
+
+ feedback_status(list(
+ type = "error",
+ message = lbl("feedback_email_required_message")
+ ))
+
+ return()
+ }
+
+
+ # Validate email format when an email is entered.
+ if (
+ nzchar(email_address) &&
+ !grepl(
+ "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$",
+ email_address
+ )
+ ) {
+
+ feedback_status(list(
+ type = "error",
+ message = lbl("feedback_invalid_email_message")
+ ))
+
+ return()
+ }
+
+
+ submission_in_progress(TRUE)
+
+ on.exit(
+ submission_in_progress(FALSE),
+ add = TRUE
+ )
+
+
+ # Local feedback storage directory.
+ feedback_dir <- file.path("feedback")
+
+ if (!dir.exists(feedback_dir)) {
+
+ dir.create(
+ feedback_dir,
+ recursive = TRUE,
+ showWarnings = FALSE
+ )
+ }
+
+
+ feedback_file <- file.path(
+ feedback_dir,
+ "feedback_responses.csv"
+ )
+
+
+ # Logged-in username.
+ username <- tryCatch({
+
+ if (!is.null(USER) && !is.null(USER$username)) {
+ safe_character(USER$username)
+ } else {
+ ""
+ }
+
+ }, error = function(e) {
+ ""
+ })
+
+
+ # Current application language.
+ current_language <- tryCatch({
+
+ safe_character(rv_lang$selected_language)
+
+ }, error = function(e) {
+ ""
+ })
+
+
+ # Current active application tab.
+ active_tab <- tryCatch({
+
+ safe_character(input$tabs)
+
+ }, error = function(e) {
+ ""
+ })
+
+
+ # Multi-select issue responses.
+ issues <- input$feedback_issues
+
+ if (is.null(issues) || length(issues) == 0) {
+
+ issues <- ""
+
+ } else {
+
+ issues <- paste(
+ as.character(issues),
+ collapse = "; "
+ )
+ }
+
+
+ # Create one submission row.
+ feedback_row <- data.frame(
+
+ timestamp = format(
+ Sys.time(),
+ "%Y-%m-%d %H:%M:%S %Z"
+ ),
+
+ username = username,
+
+ language = current_language,
+
+ active_tab = active_tab,
+
+ role = safe_character(
+ input$feedback_role
+ ),
+
+ use_frequency = safe_character(
+ input$feedback_use_frequency
+ ),
+
+ module = safe_character(
+ input$feedback_module
+ ),
+
+ task = task,
+
+ ease_of_use = as.integer(
+ input$feedback_ease
+ ),
+
+ expected_results = safe_character(
+ input$feedback_expected_results
+ ),
+
+ speed_responsiveness = as.integer(
+ input$feedback_speed
+ ),
+
+ issues_experienced = issues,
+
+ issue_description = safe_character(
+ input$feedback_issue_description
+ ),
+
+ recommend_likelihood = as.integer(
+ input$feedback_recommend
+ ),
+
+ contact_permission = contact_permission,
+
+ email_address = email_address,
+
+ session_token = safe_character(
+ session$token
+ ),
+
+ stringsAsFactors = FALSE
+ )
+
+
+ # Save a local backup before sending to Google Sheets.
+ local_save_success <- FALSE
+
+ tryCatch({
+
+ if (file.exists(feedback_file)) {
+
+ existing <- read.csv(
+ feedback_file,
+ stringsAsFactors = FALSE,
+ check.names = FALSE,
+ fileEncoding = "UTF-8"
+ )
+
+ expected_columns <- names(feedback_row)
+
+ # If an older CSV has a different structure, preserve it as a backup
+ # and start a new file using the current structure.
+ if (!identical(names(existing), expected_columns)) {
+
+ backup_name <- file.path(
+ feedback_dir,
+ paste0(
+ "feedback_responses_old_",
+ format(Sys.time(), "%Y%m%d_%H%M%S"),
+ ".csv"
+ )
+ )
+
+ file.copy(
+ feedback_file,
+ backup_name,
+ overwrite = FALSE
+ )
+
+ existing <- data.frame()
+ }
+
+
+ combined <- if (nrow(existing) > 0) {
+ rbind(existing, feedback_row)
+ } else {
+ feedback_row
+ }
+
+
+ write.csv(
+ combined,
+ feedback_file,
+ row.names = FALSE,
+ na = "",
+ fileEncoding = "UTF-8"
+ )
+
+ } else {
+
+ write.csv(
+ feedback_row,
+ feedback_file,
+ row.names = FALSE,
+ na = "",
+ fileEncoding = "UTF-8"
+ )
+ }
+
+ local_save_success <- TRUE
+
+ }, error = function(e) {
+
+ warning(
+ paste(
+ lbl("feedback_local_save_warning"),
+ conditionMessage(e)
+ )
+ )
+ })
+
+
+ # Append the same submission to Google Sheets.
+ sheet_result <- tryCatch({
+
+ append_feedback_to_google_sheet(
+ feedback_row
+ )
+
+ list(
+ success = TRUE,
+ error = NULL
+ )
+
+ }, error = function(e) {
+
+ warning(
+ paste(
+ lbl("feedback_google_sheet_warning"),
+ conditionMessage(e)
+ )
+ )
+
+ list(
+ success = FALSE,
+ error = conditionMessage(e)
+ )
+ })
+
+
+ # Handle Google Sheets failure.
+ if (!sheet_result$success) {
+
+ if (local_save_success) {
+
+ feedback_status(list(
+ type = "warning",
+ message = paste(
+ lbl("feedback_sheet_sync_warning"),
+ sheet_result$error
+ )
+ ))
+
+ } else {
+
+ feedback_status(list(
+ type = "error",
+ message = paste(
+ lbl("feedback_error_message"),
+ sheet_result$error
+ )
+ ))
+ }
+
+ return()
+ }
+
+
+ # Reset form fields after successful submission.
+ updateSelectInput(
+ session,
+ "feedback_role",
+ selected = character(0)
+ )
+
+ updateSelectInput(
+ session,
+ "feedback_use_frequency",
+ selected = character(0)
+ )
+
+ updateSelectInput(
+ session,
+ "feedback_module",
+ selected = character(0)
+ )
+
+ updateTextAreaInput(
+ session,
+ "feedback_task",
+ value = ""
+ )
+
+ updateSliderInput(
+ session,
+ "feedback_ease",
+ value = 3
+ )
+
+ updateRadioButtons(
+ session,
+ "feedback_expected_results",
+ selected = character(0)
+ )
+
+ updateSliderInput(
+ session,
+ "feedback_speed",
+ value = 3
+ )
+
+ updateCheckboxGroupInput(
+ session,
+ "feedback_issues",
+ selected = character(0)
+ )
+
+ updateTextAreaInput(
+ session,
+ "feedback_issue_description",
+ value = ""
+ )
+
+ updateSliderInput(
+ session,
+ "feedback_recommend",
+ value = 3
+ )
+
+ updateRadioButtons(
+ session,
+ "feedback_contact_permission",
+ selected = character(0)
+ )
+
+ updateTextInput(
+ session,
+ "feedback_email",
+ value = ""
+ )
+
+
+ feedback_status(list(
+ type = "success",
+ message = lbl("feedback_success_message")
+ ))
+ })
+}
\ No newline at end of file
diff --git a/server/google_sheets_feedback.R b/server/google_sheets_feedback.R
new file mode 100644
index 0000000..01051f8
--- /dev/null
+++ b/server/google_sheets_feedback.R
@@ -0,0 +1,396 @@
+# server/google_sheets_feedback.R
+#
+# Handles Google Sheets configuration, authentication,
+# validation, and feedback submission.
+
+get_google_feedback_label <- function(key) {
+
+ value <- tryCatch(
+ get_rv_labels(key),
+ error = function(e) NULL
+ )
+
+ if (
+ is.null(value) ||
+ length(value) == 0 ||
+ is.na(value[1]) ||
+ !nzchar(trimws(as.character(value[1])))
+ ) {
+ return(key)
+ }
+
+ as.character(value[1])
+}
+
+
+get_feedback_sheet_config <- function() {
+
+ sheet_id <- trimws(
+ Sys.getenv(
+ "GOOGLE_FEEDBACK_SHEET_ID",
+ unset = ""
+ )
+ )
+
+ google_account <- trimws(
+ Sys.getenv(
+ "GOOGLE_FEEDBACK_ACCOUNT",
+ unset = ""
+ )
+ )
+
+ token_cache <- trimws(
+ Sys.getenv(
+ "GOOGLE_FEEDBACK_TOKEN_CACHE",
+ unset = ".secrets"
+ )
+ )
+
+ worksheet <- trimws(
+ Sys.getenv(
+ "GOOGLE_FEEDBACK_WORKSHEET",
+ unset = "1"
+ )
+ )
+
+
+ if (!nzchar(sheet_id)) {
+ stop(
+ get_google_feedback_label(
+ "feedback_sheet_id_not_configured"
+ ),
+ call. = FALSE
+ )
+ }
+
+
+ if (!nzchar(google_account)) {
+ stop(
+ get_google_feedback_label(
+ "feedback_google_account_not_configured"
+ ),
+ call. = FALSE
+ )
+ }
+
+
+ if (!nzchar(token_cache)) {
+ stop(
+ get_google_feedback_label(
+ "feedback_token_cache_not_configured"
+ ),
+ call. = FALSE
+ )
+ }
+
+
+ if (!nzchar(worksheet)) {
+ stop(
+ get_google_feedback_label(
+ "feedback_worksheet_not_configured"
+ ),
+ call. = FALSE
+ )
+ }
+
+
+ list(
+ sheet_id = sheet_id,
+ google_account = google_account,
+ token_cache = token_cache,
+ worksheet = worksheet
+ )
+}
+
+
+authenticate_feedback_sheet <- function() {
+
+ if (!requireNamespace("googlesheets4", quietly = TRUE)) {
+ stop(
+ get_google_feedback_label(
+ "feedback_googlesheets_package_missing"
+ ),
+ call. = FALSE
+ )
+ }
+
+
+ config <- get_feedback_sheet_config()
+
+
+ app_root <- normalizePath(
+ ".",
+ winslash = "/",
+ mustWork = TRUE
+ )
+
+
+ token_cache_path <- if (
+ grepl(
+ "^(?:[A-Za-z]:[/\\\\]|/)",
+ config$token_cache
+ )
+ ) {
+ config$token_cache
+ } else {
+ file.path(
+ app_root,
+ config$token_cache
+ )
+ }
+
+
+ if (!dir.exists(token_cache_path)) {
+
+ created <- dir.create(
+ token_cache_path,
+ recursive = TRUE,
+ showWarnings = FALSE
+ )
+
+ if (!created && !dir.exists(token_cache_path)) {
+ stop(
+ get_google_feedback_label(
+ "feedback_token_cache_creation_failed"
+ ),
+ call. = FALSE
+ )
+ }
+ }
+
+
+ authentication_result <- tryCatch({
+
+ googlesheets4::gs4_auth(
+ email = config$google_account,
+ cache = token_cache_path,
+ scopes = "https://www.googleapis.com/auth/spreadsheets"
+ )
+
+ TRUE
+
+ }, error = function(e) {
+
+ stop(
+ paste(
+ get_google_feedback_label(
+ "feedback_google_authentication_failed"
+ ),
+ conditionMessage(e)
+ ),
+ call. = FALSE
+ )
+ })
+
+
+ if (!isTRUE(authentication_result)) {
+ stop(
+ get_google_feedback_label(
+ "feedback_google_authentication_failed"
+ ),
+ call. = FALSE
+ )
+ }
+
+
+ authenticated_user <- tryCatch(
+ googlesheets4::gs4_user(),
+ error = function(e) NULL
+ )
+
+
+ if (is.null(authenticated_user)) {
+ stop(
+ get_google_feedback_label(
+ "feedback_google_user_not_authenticated"
+ ),
+ call. = FALSE
+ )
+ }
+
+
+ config$token_cache <- token_cache_path
+
+ invisible(config)
+}
+
+
+normalise_feedback_for_sheet <- function(feedback_row) {
+
+ if (!is.data.frame(feedback_row)) {
+ stop(
+ get_google_feedback_label(
+ "feedback_row_not_dataframe"
+ ),
+ call. = FALSE
+ )
+ }
+
+
+ if (nrow(feedback_row) != 1L) {
+ stop(
+ get_google_feedback_label(
+ "feedback_row_invalid_count"
+ ),
+ call. = FALSE
+ )
+ }
+
+
+ if (ncol(feedback_row) == 0L) {
+ stop(
+ get_google_feedback_label(
+ "feedback_row_has_no_columns"
+ ),
+ call. = FALSE
+ )
+ }
+
+
+ if (
+ is.null(names(feedback_row)) ||
+ any(!nzchar(names(feedback_row)))
+ ) {
+ stop(
+ get_google_feedback_label(
+ "feedback_row_missing_column_names"
+ ),
+ call. = FALSE
+ )
+ }
+
+
+ feedback_row[] <- lapply(
+ feedback_row,
+ function(column) {
+
+ if (
+ inherits(
+ column,
+ c(
+ "POSIXct",
+ "POSIXlt",
+ "Date"
+ )
+ )
+ ) {
+ return(as.character(column))
+ }
+
+
+ if (is.factor(column)) {
+ return(as.character(column))
+ }
+
+
+ if (is.list(column)) {
+ return(
+ vapply(
+ column,
+ function(value) {
+
+ if (
+ is.null(value) ||
+ length(value) == 0
+ ) {
+ return("")
+ }
+
+ paste(
+ as.character(value),
+ collapse = "; "
+ )
+ },
+ character(1)
+ )
+ )
+ }
+
+
+ column
+ }
+ )
+
+
+ feedback_row[] <- lapply(
+ feedback_row,
+ function(column) {
+
+ if (is.character(column)) {
+ column[is.na(column)] <- ""
+ }
+
+ if (is.numeric(column)) {
+ column[is.na(column)] <- NA_real_
+ }
+
+ if (is.logical(column)) {
+ column[is.na(column)] <- FALSE
+ }
+
+ column
+ }
+ )
+
+
+ feedback_row
+}
+
+
+append_feedback_to_google_sheet <- function(feedback_row) {
+
+ config <- authenticate_feedback_sheet()
+
+ feedback_row <- normalise_feedback_for_sheet(
+ feedback_row
+ )
+
+
+ worksheet <- if (
+ grepl(
+ "^[0-9]+$",
+ config$worksheet
+ )
+ ) {
+ as.integer(config$worksheet)
+ } else {
+ config$worksheet
+ }
+
+
+ append_result <- tryCatch({
+
+ googlesheets4::sheet_append(
+ ss = config$sheet_id,
+ data = feedback_row,
+ sheet = worksheet
+ )
+
+ TRUE
+
+ }, error = function(e) {
+
+ stop(
+ paste(
+ get_google_feedback_label(
+ "feedback_google_sheet_append_failed"
+ ),
+ conditionMessage(e)
+ ),
+ call. = FALSE
+ )
+ })
+
+
+ if (!isTRUE(append_result)) {
+ stop(
+ get_google_feedback_label(
+ "feedback_google_sheet_append_failed"
+ ),
+ call. = FALSE
+ )
+ }
+
+
+ invisible(TRUE)
+}
\ No newline at end of file
diff --git a/server/header_footer_configs.R b/server/header_footer_configs.R
index 5c6aee6..8dfc9ed 100644
--- a/server/header_footer_configs.R
+++ b/server/header_footer_configs.R
@@ -184,7 +184,41 @@ menu_translation = function(){
history_sidebar_command_item("save", get_rv_labels("save"), icon("floppy-disk", lib = "font-awesome"))
),
- menuItem(HTML(paste0("")), tabName = "addResources", icon = icon("book")),
+ menuItem(
+ text = HTML(
+ paste0(
+ ""
+ )
+ ),
+ icon = icon("book"),
+ startExpanded = FALSE,
+
+ menuSubItem(
+ text = HTML(
+ paste0(
+ ""
+ )
+ ),
+ tabName = "feedbackForm",
+ icon = icon("comment-dots")
+ ),
+
+ menuSubItem(
+ text = HTML(
+ paste0(
+ ""
+ )
+ ),
+ tabName = "userManual",
+ icon = icon("book-open")
+ )
+ ),
if (isTRUE(isolate(USER$username) %in% c("scygu@aphrc.org", "guest565@aphrc.org"))) {
menuItem(get_rv_labels("admin_menu_label"), tabName = "adminPanel", icon = icon("shield-halved"))
diff --git a/static_files/labelling_file.xlsx b/static_files/labelling_file.xlsx
index f1d3156..e2841dc 100644
Binary files a/static_files/labelling_file.xlsx and b/static_files/labelling_file.xlsx differ
diff --git a/ui/add_resources_ui.R b/ui/add_resources_ui.R
index 26ed7d3..3892e95 100644
--- a/ui/add_resources_ui.R
+++ b/ui/add_resources_ui.R
@@ -1,5 +1,512 @@
-add_resources_ui = function() {
- tabItem(tabName = "addResources",
- fluidRow()
- )
-}
+# ui/add_resources_ui.R
+
+
+feedback_form_ui <- function() {
+
+ tabItem(
+ tabName = "feedbackForm",
+
+ div(
+ style = "padding: 25px;",
+
+ div(
+ style = "
+ background: #ffffff;
+ border-radius: 20px;
+ padding: 28px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.08);
+ border: 1px solid #e8eef7;
+ width: 100%;
+ ",
+
+ uiOutput("feedback_form_title_ui"),
+ uiOutput("feedback_form_description_ui"),
+
+ tags$hr(),
+
+ fluidRow(
+ column(
+ width = 6,
+ uiOutput("feedback_role_ui")
+ ),
+ column(
+ width = 6,
+ uiOutput("feedback_use_frequency_ui")
+ )
+ ),
+
+ fluidRow(
+ column(
+ width = 6,
+ uiOutput("feedback_module_ui")
+ ),
+ column(
+ width = 6,
+ uiOutput("feedback_task_ui")
+ )
+ ),
+
+ fluidRow(
+ column(
+ width = 6,
+ uiOutput("feedback_ease_ui")
+ ),
+ column(
+ width = 6,
+ uiOutput("feedback_expected_results_ui")
+ )
+ ),
+
+ fluidRow(
+ column(
+ width = 6,
+ uiOutput("feedback_speed_ui")
+ ),
+ column(
+ width = 6,
+ uiOutput("feedback_recommend_ui")
+ )
+ ),
+
+ uiOutput("feedback_issues_ui"),
+
+ uiOutput("feedback_issue_description_ui"),
+ uiOutput("feedback_additional_comments_ui"),
+
+ fluidRow(
+ column(
+ width = 6,
+ uiOutput("feedback_contact_permission_ui")
+ ),
+ column(
+ width = 6,
+ uiOutput("feedback_email_ui")
+ )
+ ),
+
+ br(),
+
+ actionButton(
+ inputId = "submit_feedback",
+ label = NULL,
+ icon = icon("paper-plane"),
+ class = "btn-primary",
+ style = "
+ padding: 12px 22px;
+ border-radius: 12px;
+ font-weight: 700;
+ background: #2c7be5;
+ border: none;
+ "
+ ),
+
+ br(),
+ br(),
+
+ uiOutput("feedback_status_ui")
+ )
+ )
+ )
+}
+
+
+user_manual_ui <- function() {
+
+ tabItem(
+ tabName = "userManual",
+
+ div(
+ style = "padding: 25px;",
+
+ div(
+ style = "
+ background: #ffffff;
+ border-radius: 20px;
+ padding: 28px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.08);
+ border: 1px solid #e8eef7;
+ ",
+
+ uiOutput("user_manual_title_ui"),
+ uiOutput("user_manual_description_ui"),
+
+ tags$iframe(
+ src = "manuals/user_manual.pdf",
+ width = "100%",
+ height = "850px",
+ style = "
+ border: none;
+ border-radius: 16px;
+ ",
+ loading = "lazy"
+ ),
+
+ br(),
+
+ uiOutput("user_manual_button_ui")
+ )
+ )
+ )
+}
+
+
+add_resources_translation <- function(output, session) {
+
+ lbl <- function(key) {
+
+ value <- tryCatch(
+ get_rv_labels(key),
+ error = function(e) NULL
+ )
+
+ if (
+ is.null(value) ||
+ length(value) == 0 ||
+ is.na(value[1]) ||
+ !nzchar(trimws(as.character(value[1])))
+ ) {
+ return(key)
+ }
+
+ as.character(value[1])
+ }
+
+
+ output$feedback_form_title_ui <- renderUI({
+
+ h2(
+ icon("comment-dots"),
+ lbl("feedback_form_title"),
+ style = "
+ font-weight: 800;
+ color: #243447;
+ margin-top: 0;
+ "
+ )
+ })
+
+
+ output$feedback_form_description_ui <- renderUI({
+
+ p(
+ lbl("feedback_form_description"),
+ style = "
+ font-size: 16px;
+ color: #5f6b7a;
+ "
+ )
+ })
+
+
+ output$feedback_role_ui <- renderUI({
+
+ selectInput(
+ inputId = "feedback_role",
+ label = lbl("feedback_role"),
+ choices = c(
+ lbl("feedback_role_researcher"),
+ lbl("feedback_role_data_scientist"),
+ lbl("feedback_role_statistician"),
+ lbl("feedback_role_epidemiologist"),
+ lbl("feedback_role_public_health"),
+ lbl("feedback_role_monitoring_evaluation"),
+ lbl("feedback_role_student"),
+ lbl("feedback_role_policy_maker"),
+ lbl("feedback_other")
+ ),
+ selected = character(0),
+ width = "100%"
+ )
+ })
+
+
+ output$feedback_use_frequency_ui <- renderUI({
+
+ selectInput(
+ inputId = "feedback_use_frequency",
+ label = lbl("feedback_use_frequency"),
+ choices = c(
+ lbl("feedback_frequency_first_time"),
+ lbl("feedback_frequency_occasionally"),
+ lbl("feedback_frequency_monthly"),
+ lbl("feedback_frequency_weekly"),
+ lbl("feedback_frequency_daily")
+ ),
+ selected = character(0),
+ width = "100%"
+ )
+ })
+
+
+ output$feedback_module_ui <- renderUI({
+
+ checkboxGroupInput(
+ inputId = "feedback_module",
+ label = lbl("feedback_module"),
+ choices = c(
+ lbl("feedback_module_data_management"),
+ lbl("feedback_module_data_visualization"),
+ lbl("feedback_module_data_anonymization"),
+ lbl("feedback_module_omop"),
+ lbl("feedback_module_research_questions"),
+ lbl("feedback_module_machine_learning"),
+ lbl("feedback_module_deep_learning"),
+ lbl("feedback_module_overall"),
+ lbl("feedback_other")
+ ),
+ selected = character(0),
+ width = "100%"
+ )
+ })
+
+
+ output$feedback_task_ui <- renderUI({
+
+ textAreaInput(
+ inputId = "feedback_task",
+ label = lbl("feedback_task"),
+ placeholder = lbl("feedback_your_answer"),
+ width = "100%",
+ height = "120px"
+ )
+ })
+
+
+ output$feedback_ease_ui <- renderUI({
+
+ tagList(
+ tags$label(
+ lbl("feedback_ease"),
+ class = "control-label"
+ ),
+
+ div(
+ style = "
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 4px;
+ color: #5f6b7a;
+ font-size: 13px;
+ ",
+ span(lbl("feedback_ease_very_difficult")),
+ span(lbl("feedback_ease_very_easy"))
+ ),
+
+ sliderInput(
+ inputId = "feedback_ease",
+ label = NULL,
+ min = 1,
+ max = 5,
+ value = 3,
+ step = 1,
+ width = "100%"
+ )
+ )
+ })
+
+
+ output$feedback_expected_results_ui <- renderUI({
+
+ radioButtons(
+ inputId = "feedback_expected_results",
+ label = lbl("feedback_expected_results"),
+ choices = c(
+ lbl("feedback_expected_completely"),
+ lbl("feedback_expected_mostly"),
+ lbl("feedback_expected_partially"),
+ lbl("feedback_expected_not_at_all")
+ ),
+ selected = character(0)
+ )
+ })
+
+
+ output$feedback_speed_ui <- renderUI({
+
+ tagList(
+ tags$label(
+ lbl("feedback_speed"),
+ class = "control-label"
+ ),
+
+ div(
+ style = "
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 4px;
+ color: #5f6b7a;
+ font-size: 13px;
+ ",
+ span(lbl("feedback_speed_very_poor")),
+ span(lbl("feedback_speed_excellent"))
+ ),
+
+ sliderInput(
+ inputId = "feedback_speed",
+ label = NULL,
+ min = 1,
+ max = 5,
+ value = 3,
+ step = 1,
+ width = "100%"
+ )
+ )
+ })
+
+
+ output$feedback_issues_ui <- renderUI({
+
+ checkboxGroupInput(
+ inputId = "feedback_issues",
+ label = lbl("feedback_issues"),
+ choices = c(
+ lbl("feedback_issue_slow_loading"),
+ lbl("feedback_issue_errors"),
+ lbl("feedback_issue_incorrect_outputs"),
+ lbl("feedback_issue_visualization"),
+ lbl("feedback_issue_navigation"),
+ lbl("feedback_issue_missing_functionality"),
+ lbl("feedback_issue_none"),
+ lbl("feedback_other")
+ ),
+ selected = character(0),
+ width = "100%"
+ )
+ })
+
+
+ output$feedback_issue_description_ui <- renderUI({
+
+ textAreaInput(
+ inputId = "feedback_issue_description",
+ label = lbl("feedback_issue_description"),
+ placeholder = lbl("feedback_your_answer"),
+ width = "100%",
+ height = "130px"
+ )
+ })
+
+
+ output$feedback_recommend_ui <- renderUI({
+
+ tagList(
+ tags$label(
+ lbl("feedback_recommend"),
+ class = "control-label"
+ ),
+
+ div(
+ style = "
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 4px;
+ color: #5f6b7a;
+ font-size: 13px;
+ ",
+ span(lbl("feedback_recommend_not_likely")),
+ span(lbl("feedback_recommend_extremely_likely"))
+ ),
+
+ sliderInput(
+ inputId = "feedback_recommend",
+ label = NULL,
+ min = 1,
+ max = 5,
+ value = 3,
+ step = 1,
+ width = "100%"
+ )
+ )
+ })
+
+ output$feedback_additional_comments_ui <- renderUI({
+
+ textAreaInput(
+ inputId = "feedback_additional_comments",
+ label = lbl("feedback_additional_comments"),
+ placeholder = lbl("feedback_your_answer"),
+ width = "100%",
+ height = "130px"
+ )
+ })
+
+ output$feedback_contact_permission_ui <- renderUI({
+
+ radioButtons(
+ inputId = "feedback_contact_permission",
+ label = lbl("feedback_contact_permission"),
+ choices = c(
+ lbl("yes"),
+ lbl("no")
+ ),
+ selected = character(0)
+ )
+ })
+
+
+ output$feedback_email_ui <- renderUI({
+
+ textInput(
+ inputId = "feedback_email",
+ label = lbl("feedback_email"),
+ placeholder = lbl("feedback_your_answer"),
+ width = "100%"
+ )
+ })
+
+
+ observe({
+
+ updateActionButton(
+ session = session,
+ inputId = "submit_feedback",
+ label = lbl("submit_feedback")
+ )
+ })
+
+
+ output$user_manual_title_ui <- renderUI({
+
+ h2(
+ icon("book-open"),
+ lbl("user_manual_title"),
+ style = "
+ font-weight: 800;
+ color: #243447;
+ margin-top: 0;
+ "
+ )
+ })
+
+
+ output$user_manual_description_ui <- renderUI({
+
+ p(
+ lbl("user_manual_description"),
+ style = "
+ font-size: 16px;
+ color: #5f6b7a;
+ "
+ )
+ })
+
+
+ output$user_manual_button_ui <- renderUI({
+
+ tags$a(
+ lbl("open_user_manual"),
+ href = "manuals/user_manual.pdf",
+ target = "_blank",
+ style = "
+ display: inline-block;
+ margin-top: 18px;
+ padding: 12px 20px;
+ border-radius: 12px;
+ background: #2c7be5;
+ color: white;
+ text-decoration: none;
+ font-weight: 700;
+ "
+ )
+ })
+}
\ No newline at end of file
diff --git a/ui/dashboard_body.R b/ui/dashboard_body.R
index 2ebb4c4..77a542d 100644
--- a/ui/dashboard_body.R
+++ b/ui/dashboard_body.R
@@ -148,8 +148,9 @@ aphrcBody <- dashboardBody(
, deeplearning_ui()
## OMOP resources
- , add_resources_ui()
-
+ , feedback_form_ui()
+ , user_manual_ui()
+
## Admin dashboard (only rendered for admin user)
, admin_ui()
diff --git a/ui/load_r_packages.R b/ui/load_r_packages.R
index 5a7e35e..58813ed 100644
--- a/ui/load_r_packages.R
+++ b/ui/load_r_packages.R
@@ -96,7 +96,8 @@ libraries <- c(
"fs",
"rpart",
"RSNNS",
- "naivebayes"
+ "naivebayes",
+ "googlesheets4"
)
diff --git a/ui/tooltip.R b/ui/tooltip.R
index f9f4bd9..7396552 100644
--- a/ui/tooltip.R
+++ b/ui/tooltip.R
@@ -21,6 +21,7 @@ nocode_tooltip_keys <- function() {
omop_visualizations = "tooltip_omop_visualizations",
CohortConstructor = "tooltip_CohortConstructor",
FeatureExtraction = "tooltip_FeatureExtraction",
+ anonymization = "tooltip_anonymization",
anonymization_quant = "tooltip_anonymization_quant",
anonymization_qual = "tooltip_anonymization_qual",
researchQuestions = "tooltip_researchQuestions",