R/projects.R

Defines functions project_add project_edit task_folder task_getfile task_setfile task_open_analysis_file outlook_content

Documented in project_add project_edit task_folder task_getfile task_open_analysis_file task_setfile

# ==================================================================== #
# TITLE                                                                #
# Tools for Data Analysis at Certe                                     #
#                                                                      #
# AUTHORS                                                              #
# Berends MS (m.berends@certe.nl)                                      #
# Meijer BC (b.meijer@certe.nl)                                        #
# Hassing EEA (e.hassing@certe.nl)                                     #
#                                                                      #
# COPYRIGHT                                                            #
# (c) 2019 Certe Medische diagnostiek & advies - https://www.certe.nl  #
#                                                                      #
# LICENCE                                                              #
# This R package is free software; you can redistribute it and/or      #
# modify it under the terms of the GNU General Public License          #
# version 2.0, as published by the Free Software Foundation.           #
# This R package is distributed in the hope that it will be useful,    #
# but WITHOUT ANY WARRANTY; without even the implied warranty of       #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the         #
# GNU General Public License for more details.                         #
# ==================================================================== #

#' Start een nieuw project
#' 
#' Deze functie opent een nieuw scherm die verbindt met Trello.
#' @export
project_add <- function() {
  
  if (Sys.getenv("R_PROJECTS") == "") {
    stop('Environmental variable `R_PROJECTS` for user not set.', call. = FALSE)
  }
  
  library(certedata, warn.conflicts = FALSE, quietly = TRUE)
  
  library(shiny)
  library(shinyWidgets)
  
  outlook <- outlook_content()
  
  ui <- fluidPage(
    shinyjs::useShinyjs(),
    
    # keys: 112-123 = F1-F12, datum ('new Date()') nodig om steeds opnieuw te kunnen triggeren:
    # https://stackoverflow.com/a/44500961/4575331
    # F4 = accept, F8 = cancel
    tags$script('$(document).on("keydown", function (e) {
                if (e.which == 115) {
                Shiny.onInputChange("create", new Date());
                } else if (e.which == 119) {
                Shiny.onInputChange("cancel", new Date());
                }});
                $(document).ready(function() {
                var textbox = document.getElementById("title");
                textbox.focus();
                });'),
    tags$style(paste0(".container-fluid { margin-top: 15px; }
                      .well { background-color: ", colourpicker("certeblauw3"), "; }
                      .well label { color: ", colourpicker("certeblauw"), "; }
                      .form-group { margin-bottom: 5px; }
                      .well .form-group { margin-bottom: 14px; }
                      .h2, h2 { color: ", colourpicker("certeblauw"), "; }
                      certeblauw, .certeblauw { color: ", colourpicker("certeblauw"), "; }
                      certeroze { color: ", colourpicker("certeroze"), "; }
                      .results_count { margin-left: 10px; }
                      label[for=title] { font-size: 16px; }
                      #create { background-color: ", colourpicker("certegroen"), "; border-color: ", colourpicker("certegroen"), "; }
                      #cancel { background-color: ", colourpicker("certeroze"), "; border-color: ", colourpicker("certeroze"), ";}
                      .multi .selectize-input .item, .selectize-dropdown .active { background-color: ", colourpicker("certeblauw3"), " !important; }
                      .selectize-input .item.active { color: white; background-color: ", colourpicker("certeblauw"), " !important; }")),
    
    sidebarLayout(
      sidebarPanel(
        textInput("title", "Titel", placeholder = ""),
        textAreaInput("description", "Omschrijving", cols = 1, rows = 2, resize = "vertical"),
        textAreaInput("checklist", "Taken", cols = 1, rows = 3, resize = "vertical", placeholder = "(1 taak per regel)"),
        uiOutput("requested_by"),
        selectInput("priority", "Prioriteit", c("Laag", "Normaal", "Hoog"), "Normaal"),
        tags$label("Deadline"),
        checkboxInput("has_deadline", "Deadline instellen", TRUE),
        uiOutput("deadline"),
        br(),
        br(),
        actionButton("create", "Aanmaken (F4)", width = "49%", icon = icon("check"), class = "btn-success"),
        actionButton("cancel", "Annuleren (F8)", width = "49%", icon = icon("ban"), class = "btn-danger"),
        uiOutput("outlook_import_btn"),
        width = 6),
      
      mainPanel(
        img(src = img_trello(), height = "40px", style = "margin-top: 10px"),
        checkboxInput("trello_upload", "Uploaden naar Trello.com", TRUE),
        uiOutput("trello_boards"),
        uiOutput("trello_settings"),
        uiOutput("trello_search_select"),
        hr(),
        img(src = img_rstudio(), height = "45px"),
        br(),
        br(),
        radioButtons("filetype",
                     label =  "Bestandstype",
                     choices =  c("R Markdown", "R"),
                     selected = "R Markdown",
                     inline = TRUE,
                     width = "100%"),
        checkboxInput("rstudio_projectfile",
                      label = "RStudio-projectbestand aanmaken en openen",
                      value = FALSE,
                      width = "100%"),
        
        hr(),
        width = 6
      )
    )
  )
  
  server <- function(input, output, session) {
    
    is_active_project <- !is.null(rstudioapi::getActiveProject())
    
    output$requested_by <- renderUI({
      # gebruikers ophalen uit csv; waarden zijn eigenlijk Certe-ID's, maar je ziet namen met functies
      users <- users_list()
      if (!is.null(users)) {
        tagList(
          selectizeInput("requested_by",
                         label = "Aanvrager(s)",
                         choices = users,
                         multiple = TRUE,
                         options = list(
                           # niet-bestaande aanvragers ondersteunen:
                           create = TRUE,
                           # maakt nieuw item als je veld verlaat zonder op 'Add ...' te drukken:
                           createOnBlur = TRUE,
                           # na kiezen dropdown sluiten:
                           closeAfterSelect = TRUE))
        )
      } else {
        tagList(
          textInput("requested_by", "Aanvrager(s)")
        )
      }
    })
    
    output$deadline <- renderUI({
      if (input$has_deadline == TRUE) {
        tagList(
          dateInput("deadline", NULL,
                    value = Sys.Date() + 14,
                    min = Sys.Date(),
                    format = "DD d MM yyyy",
                    language = "nl")
        )
      }
    })
    
    output$outlook_import_btn <- renderUI({
      if (!is.null(outlook)) {
        tagList(
          br(),
          actionButton('outlook_import',
                       label = HTML(' <strong>Titel</strong> en <strong>aanvrager</strong> uit Outlook importeren'),
                       width = "100%",
                       icon = icon("envelope"),
                       class = 'btn-primary')
        )
      }
    })
    observeEvent(input$outlook_import, {
      
      shinyjs::disable("outlook_import") # shinyjs::useShinyjs() staat in UI
      
      sent_by <- as.character(outlook[1])
      subject <- as.character(outlook[2])
      
      # titel updaten
      updateTextInput(session, inputId = 'title', value = subject)
      
      # aanvrager updaten
      users <- get_certe_users()
      sent_by_id <- ''
      if (!is.null(users)) {
        sent_by_id <- get_certe_users() %>%
          mutate(name = gsub(' - ', '-', name)) %>%
          filter(name == sent_by) %>%
          pull(id)
      }
      if (length(sent_by_id) > 0) {
        # Certe-inlognummer gevonden in `users`
        updateSelectizeInput(session,
                             inputId = 'requested_by',
                             selected = sent_by_id)
      } else {
        # niet gevonden; toevoegen aan lijst en selecteren
        users_id <- users$id
        names(users_id) <- users$name
        names(sent_by) <- sent_by
        updateSelectizeInput(session,
                             inputId = 'requested_by',
                             choices = c(sent_by, users_id),
                             selected = sent_by)
      }
    })
    
    output$trello_boards <- renderUI({
      if (input$trello_upload == TRUE) {
        boards_settings <- trello_credentials("board", item = NULL) %>% strsplit(",") %>% unlist()
        boards_df <- trello_getboards() %>%
          filter(closed == FALSE & shortLink %in% boards_settings) %>%
          select(id, name, shortLink)
        boards_shortLink <- boards_df %>% pull(shortLink)
        names(boards_shortLink) <- boards_df %>% pull(name)
        
        slct <- selectInput("trello_boards",
                            label = "Bord",
                            choices = boards_shortLink,
                            selected = trello_credentials("board_default"),
                            multiple = FALSE,
                            width = "100%")
        
        # als er maar 1 bord is, deze select verbergen
        if (length(boards_shortLink) == 1) {
          slct <- shinyjs::hidden(slct)
        }
        
        tagList(slct)
      }
    })
    
    output$trello_settings <- renderUI({
      if (input$trello_upload == TRUE) {
        
        board_selected <- input$trello_boards
        
        if (!is.null(board_selected)) {
          
          if (length(board_selected) > 1) {
            board_selected <- board_selected[1]
          }
          
          lists <- trello_getlists(board = board_selected)$id
          names(lists) <- trello_getlists(board = board_selected)$name
          
          members <- trello_getmembers(board = board_selected)$fullName
          login <- Sys.info()['login']
          if (login == "5595") {
            active <- "Erwin Hassing"
          } else if (login %in% c("5580", "Matthijs")) {
            active <- "Matthijs Berends"
          } else {
            active <- NULL
          }
          
          tagList(
            selectInput("trello_list",
                        label = "Lijst",
                        choices = lists,
                        selected = lists[2],
                        multiple = FALSE,
                        width = "100%"),
            selectizeInput("trello_members",
                           label = "Leden",
                           choices = members,
                           selected = active,
                           multiple = TRUE,
                           width = "100%",
                           options = list(
                             # niet-bestaande leden niet ondersteunen:
                             create = FALSE,
                             createOnBlur = FALSE,
                             # na kiezen dropdown sluiten:
                             closeAfterSelect = TRUE)),
            textAreaInput("trello_comments",
                          label = "Opmerkingen",
                          width = "395px",
                          cols = 1,
                          rows = 2,
                          resize = "vertical",
                          placeholder = ""),
            searchInput("trello_search",
                        label = "Gerelateerde kaart(en)",
                        value = "",
                        placeholder = "Zoeken in titel/beschrijving/taken...",
                        btnSearch = icon("search"),
                        btnReset = icon("remove"),
                        width = "100%")
          )
        }
      }
    })
    
    
    output$trello_search_select <- renderUI({
      if (input$trello_upload == TRUE) {
        board_selected <- input$trello_boards
        searchterm <- input$trello_search
        if (!is.null(searchterm)) { # niet bij opstarten, dan is het NULL. Wordt hierna "".
          if (!is.null(board_selected)) {
            if (length(board_selected) > 1) {
              board_selected <- board_selected[1]
            }
            shinyjs::disable("trello_cards")
            found_cards <- trello_searchcard(searchterm, board = board_selected)
            shinyjs::enable("trello_cards")
            if (length(found_cards) > 0) {
              tagList(
                selectizeInput("trello_cards",
                               label = NULL,
                               choices = found_cards,
                               multiple = TRUE,
                               width = "100%",
                               options = list(
                                 # niet-bestaande kaarten niet ondersteunen:
                                 create = FALSE,
                                 createOnBlur = FALSE,
                                 # na kiezen dropdown sluiten:
                                 closeAfterSelect = TRUE)),
                p(paste(length(found_cards),
                        if_else(length(found_cards) == 1,
                                "resultaat",
                                "resultaten.")),
                  class = "certeblauw results_count")
              )
            }
          }
        }
      }
    })
    
    # OPSLAAN ----
    observeEvent(input$create, {
      shinyjs::disable("create")
      shinyjs::disable("cancel")
      
      # Titel controleren
      if (trimws(input$title) == "") {
        # melding staat in UI in JavaScript
        return(invisible())
      }
      
      description <- input$description
      if (all(is.null(description)) | length(description) == 0) {
        description <- ""
      }
      requested_by <- input$requested_by
      if (all(is.null(requested_by)) | length(requested_by) == 0) {
        requested_by <- ""
      }
      checklist <- input$checklist
      if (all(is.null(checklist)) | length(checklist) == 0) {
        checklist <- ""
      }
      trello_cards <- input$trello_cards
      if (all(is.null(trello_cards)) | length(trello_cards) == 0) {
        trello_cards <- ""
      }
      
      withProgress(message = "Aanmaken...", value = 0, {
        progress_items <- input$rstudio_projectfile + input$trello_upload + 1 # (+ 1 voor mappen aanmaken)
        # Trello ----
        trello_card_id <- NULL
        if (input$trello_upload == TRUE) {
          incProgress(1 / progress_items, detail = "Uploaden naar Trello")
          if (is.null(input$deadline) | input$has_deadline == FALSE) {
            deadline <- ""
          } else {
            deadline <- input$deadline
          }
          trello_card_id <- trello_upload(board = input$trello_boards,
                                          title = input$title,
                                          member = input$trello_members,
                                          requested_by = requested_by,
                                          project_path = "",
                                          list = input$trello_list,
                                          prio = input$priority,
                                          duedate = deadline,
                                          attachments = trello_cards,
                                          checklist = checklist %>% strsplit("\n") %>% unlist(),
                                          desc = description,
                                          comments = input$trello_comments)
        }
        
        fullpath <- paste0(Sys.getenv("R_PROJECTS"), "/",
                           gsub("(\\|/|:|\\*|\\?|\"|\\|)", "", input$title),
                           ifelse(is.null(trello_card_id), "", paste0(" - #", trello_card_id)))
        fullpath <- gsub("//", "/", fullpath, fixed = TRUE)
        
        header_text <- c(paste0(        '# Titel:            ', input$title),
                         if_else(!is.null(trello_card_id),
                                 paste0('# Trello-kaart:     #', trello_card_id),
                                 NA_character_),
                         if_else(requested_by != "",
                                 paste0('# Aangevraagd door: ', get_certe_user(requested_by)),
                                 NA_character_),
                         paste0(        '# Aangemaakt op:    ', format2(Sys.time(), "d mmmm yyyy H:MM")))
        
        if (input$filetype == "R Markdown") {
          extension <- 'Rmd'
          filecontent <- c(
            '---',
            paste0('title: "', input$title, '"'),
            'subtitle:  ""',
            'author: "`r Sys.getenv(\'R_USERNAME\')`"',
            'date: "`r sub(\'  \', \' \', format(Sys.Date(), \'%e %B %Y\'))`"',
            'output:',
            '  word_document:',
            '    toc: true',
            '    toc_depth: 2',
            '    fig_width: 6.5',
            '    fig_height: 5',
            '    fig_caption: true',
            paste0('    reference_docx: "', templatedoc(type = "refdoc"), '"'),
            '---',
            '',
            '```{r Setup, include=FALSE}',
            header_text,
            '',
            'knitr::opts_chunk$set(echo = FALSE, message = FALSE, warning = FALSE,',
            "                      results = 'asis', comment = NA, dpi = 600)",
            'library(certedata)',
            paste0('data_', trello_card_id, ' <- certedb_getmmb(dates = c(start, stop),'),
            '                           where = where(db$))',
            paste0('# data_', trello_card_id, ' <- certedb_query("SELECT * FROM...")'),
            "# Evt. na downloaden exporteren/importeren:",
            paste0("# export.R(data_", trello_card_id, ", task_setfile(data_", trello_card_id, ".rds))"),
            paste0('# data_', trello_card_id, ' <- import.R(task_getfile(".*rds$"))'),
            '# Inleiding',
            '',
            '```{r}',
            '',
            '```',
            ''
          )
        } else if (input$filetype == "R") {
          extension <- 'R'
          filecontent <- c(header_text,
                           "",
                           "library(certedata)",
                           paste0('data_', trello_card_id, ' <- certedb_getmmb(dates = c(start, stop),'),
                           '                           where = where(db$))',
                           paste0('# data_', trello_card_id, ' <- certedb_query("SELECT * FROM...")'),
                           "# Evt. na downloaden exporteren/importeren:",
                           paste0("export.R(data_", trello_card_id, ", task_setfile(data_", trello_card_id, ".rds))"),
                           paste0('data_', trello_card_id, ' <- import.R(task_getfile(".*rds$"))'),
                           ""
          )
        }
        
        incProgress(1 / progress_items, detail = "Map aanmaken")
        # map maken
        dir.create(fullpath, recursive = TRUE, showWarnings = FALSE)
        # bestand maken
        filename <- paste0(fullpath, "/Analyse", 
                           ifelse(!is.null(trello_card_id),
                                  paste0(" #", trello_card_id, "."),
                                  "."),
                           extension)
        writeLines(text = paste(filecontent[!is.na(filecontent)], collapse = "\n"),
                   con = file.path(filename))
        incProgress(1 / progress_items, detail = ifelse(isTRUE(input$rstudio_projectfile), "R-bestanden maken", ""))
        Sys.sleep(0.1)
      })
      
      if (isTRUE(input$rstudio_projectfile)) {
        # project aanmaken en openen
        rstudioapi::initializeProject(fullpath)
        rstudioapi::openProject(fullpath, newSession = TRUE)
      } else {
        # bestand openen
        rstudioapi::navigateToFile(filename)
      }
      
      stopApp()
    })
    
    # ANNULEREN ----
    observeEvent(input$cancel, {
      stopApp()
    })
  }
  
  
  if (is.null(outlook)) {
    height = 705
  } else {
    height = 765
  }
  viewer <- dialogViewer(dialogName = "Nieuwe taak/project",
                         width = 850,
                         height = height)
  
  suppressMessages(
    runGadget(app = ui,
              server = server,
              viewer = viewer,
              stopOnCancel = FALSE))
}

#' Bewerk een bestaand project
#' 
#' Deze functie opent een nieuw scherm die verbindt met Trello.
#' @param card_number Het nummer van een kaart, wordt automatisch bepaald op basis van het huidige geopende bestand in RStudio.
#' @export
project_edit <- function(card_number = trello_getcardfromfile()) {
  if (is.null(card_number) | all(is.na(card_number))) {
    return(invisible())
  }
  
  library(certedata, warn.conflicts = FALSE, quietly = TRUE)
  
  library(shiny)
  library(shinyWidgets)

  card_info <- trello_getcards() %>% filter(idShort == card_number) %>% as.list()
  if (length(card_info$id) == 0) {
    stop(paste0("Card #", card_number, " not found on Trello"), call. = FALSE)
  }
  lists <- trello_getlists()
  card_status <- lists %>% filter(id == card_info$idList) %>% pull(name)
  card_comments <- trello_getcomments(card_info$id) 
  if (NROW(card_comments) > 0) {
    card_comments <- card_comments %>% 
      transmute(by = memberCreator.fullName,
                date = date,
                text = data.text)
  }
  card_checklist <- trello_getchecklists() %>% filter(idCard == card_info$id) %>% pull(checkItems) %>% .[[1]]

  ui <- fluidPage(
    shinyjs::useShinyjs(),
    
    # keys: 112-123 = F1-F12, datum ('new Date()') nodig om steeds opnieuw te kunnen triggeren:
    # https://stackoverflow.com/a/44500961/4575331
    # F4 = accept, F8 = cancel
    tags$script('$(document).on("keydown", function (e) {
                if (e.which == 115) {
                Shiny.onInputChange("save", new Date());
                } else if (e.which == 119) {
                Shiny.onInputChange("cancel", new Date());
                }});
                $(document).ready(function() {
                var textbox = document.getElementById("comment");
                textbox.focus();
                });'),
    tags$style(paste0(".container-fluid { margin-top: 15px; }
                      .form-group { margin-bottom: 5px; }
                      .well .form-group { margin-bottom: 14px; }
                      .h2, h2 { color: ", colourpicker("certeblauw"), "; }
                      h5 { font-weight: bold }
                      certeblauw, .certeblauw { color: ", colourpicker("certeblauw"), "; }
                      certeroze { color: ", colourpicker("certeroze"), "; }
                      .results_count { margin-left: 10px; }
                      label[for=title] { font-size: 16px; }
                      .task input:checked ~ span { text-decoration: line-through; }
                      .comment { font-style: italic; margin: 0 }
                      p { cursor: default; }
                      .comment_box { border: 1px solid #dddddd; width: fit-content; border-radius: 10px; padding: 5px; margin-bottom: 10px; background-color: ", colourpicker("certeblauw3"), " }
                      #save { background-color: ", colourpicker("certegroen"), "; border-color: ", colourpicker("certegroen"), "; }
                      #cancel { background-color: ", colourpicker("certeroze"), "; border-color: ", colourpicker("certeroze"), ";}
                      .multi .selectize-input .item, .selectize-dropdown .active { background-color: ", colourpicker("certeblauw3"), " !important; }
                      .selectize-input .item.active { color: white; background-color: ", colourpicker("certeblauw"), " !important; }")),
    sidebarLayout(
      sidebarPanel(
        uiOutput('style_tag'),
        tags$label("Titel", style = "font-size: 16px;"),
        HTML(paste0('<p class="last_update">', card_info$name, '</p>')),
        tags$label("Laatste update"),
        HTML(paste0('<p class="last_update">', 
                    format2(as.Date(card_info$dateLastActivity), "dddd d mmmm yyyy"), 
                    " (", as.integer(difftime(Sys.Date(), as.Date(card_info$dateLastActivity), units = "days")), " dgn ~=",
                    " ", as.integer(difftime(Sys.Date(), as.Date(card_info$dateLastActivity), units = "weeks")),
                    " wkn geleden)</p>")),
        selectInput("status", "Status", choices = lists$name, selected = card_status),
        tags$label("Deadline"),
        checkboxInput("has_deadline", "Deadline instellen", value = ifelse(is.na(card_info$due), FALSE, TRUE)),
        uiOutput("deadline"),
        textAreaInput("comment", "Nieuwe opmerking", cols = 1, rows = 3, resize = "vertical"),
        br(),
        actionButton("save", "Opslaan (F4)", width = "43%", icon = icon("check"), class = "btn-success"),
        actionButton("cancel", "Annuleren (F8)", width = "43%", icon = icon("ban"), class = "btn-danger"),
        actionButton('trello_open', NULL, width = "11%", icon = icon("trello"), class = 'btn-primary'),
        width = 7),
      
      mainPanel(
        uiOutput("tasks_and_comments"),
        width = 5)
    )
  )
  
  server <- function(input, output, session) {
    shinyjs::disable("title")
    
    output$deadline <- renderUI({
      val <- case_when(isTRUE(input$has_deadline) & !is.na(as.Date(card_info$due)) ~ as.Date(card_info$due),
                       isTRUE(input$has_deadline) ~  Sys.Date() + 14,
                       TRUE ~ as.Date(NA_character_))
      tagList(
        dateInput("deadline", NULL,
                  value = val,
                  format = "DD d MM yyyy",
                  language = "nl"),
        checkboxInput("deadline_finished", "Deadline voltooid", value = card_info$dueComplete & !is.na(card_info$due))
      )
    })
    
    output$tasks_and_comments <- renderUI({
      desc <- unlist(strsplit(card_info$desc,  "\n\n"))
      desc <- desc[!desc %like% "[*].*[*]"] # niet die beginnen en eindigen met *, zoals 'Aangevraagd door' en 'Maplocatie'
      desc <- paste(gsub("\n", "<br>", desc, fixed = TRUE), collapse = "\n")

      if (length(desc) > 0 & desc != "") {
        l <- tagList(h4("Omschrijving"),
                     HTML(paste0("<p>", desc, "</p>")))
      } else {
        l <- tagList()
      }
     
      if (NROW(card_checklist) > 0) {
        l <- tagList(l, h4("Taken"))
        for (i in 1:nrow(card_checklist)) {
          l <- tagList(l,
                       div(checkboxInput(inputId = card_checklist$id[i], 
                                         label = card_checklist$name[i], 
                                         value = isTRUE(card_checklist$state[i] == "complete"), 
                                         width = "100%"),
                           class = "task"))
        }
      } else {
        l <- tagList(l, h5("Geen taken."))
      }
      l <- tagList(l, textAreaInput("newtasks", 
                                    label = NULL,
                                    width = "100%",
                                    cols = 1,
                                    rows = 1,
                                    resize = "vertical",
                                    placeholder = "Nieuwe taak (1 per regel)"))
      
      l <- tagList(l, hr())
      
      if (NROW(card_comments) > 0) {
        l <- tagList(l, h4("Opmerkingen"))
        for (i in 1:nrow(card_comments)) {
          l <- tagList(l, 
                       div(HTML(paste0('<p style="font-weight: bold; font-size: 11px;">', card_comments$by[i], " op ", format2(as.Date(card_comments$date[i]), "ddd d mmm yyyy"), ":</p>")),
                           HTML(paste0('<p class="comment">', card_comments$text[i], "</p>")),
                           class = "comment_box"))
        }
      } else {
        l <- tagList(l, h5("Geen opmerkingen."))
      }
      
      div(l, style = "height: 580px; overflow: auto;")
    })

    output$style_tag <- renderUI({
      if (input$status == "Bezig") {
        col1 <- colourpicker("certeblauw")
        col2 <- colourpicker("certeblauw2")
        col3 <- colourpicker("certeblauw3")
      } else if (input$status == "Voltooid") {
        col1 <- colourpicker("certegroen")
        col2 <- colourpicker("certegroen2")
        col3 <- colourpicker("certegroen3")
      } else if (input$status == "Wachten op een ander") {
        col1 <- colourpicker("certeroze")
        col2 <- colourpicker("certeroze2")
        col3 <- colourpicker("certeroze3")
      } else {
        col1 <- colourpicker("certeblauw")
        col2 <- "#f5f5f5"
        col3 <- "#f5f5f5"
      }
      return(tags$head(tags$style(HTML(paste0(".well { background-color:", col3, "; }
                                               .well label { color: ", col1, "; }
                                               .last_update { margin: 0 10px 10px; font-size: 13px; color: ", col1, "; }")))))              
    })
    
    # OPSLAAN ----
    observeEvent(input$save, {
      shinyjs::disable("save")
      shinyjs::disable("cancel")
      
      if (input$status != card_status & trimws(input$comment) == "" & input$status %like% "(wachten|voltooid)") {
        rstudioapi::showDialog("Status gewijzigd", "Als de status gewijzigd is naar 'wachten op een ander' of 'voltooid', moet een opmerking ingevuld worden.")
      } else {
        
        # status
        if (input$status != card_status) {
          trello_movecard(card_id = card_info$id, 
                          list_id = lists %>% filter(name == input$status) %>% pull(id))
        }
        
        # deadline
        if (input$has_deadline == FALSE) {
          trello_setdeadline(card_id = card_info$id,
                             duedate = NULL,
                             duecomplete = TRUE)
        } else {
          trello_setdeadline(card_id = card_info$id,
                             duedate = input$deadline,
                             duecomplete = input$deadline_finished)
        }
        
        # comment
        if (trimws(input$comment) != "") {
          trello_setcomment(card_id = card_info$id,
                            comment = input$comment)
        }
        
        # checklist
        if (NROW(card_checklist) > 0) {
          for (i in 1:nrow(card_checklist)) {
            trello_settask_state(card_id = card_info$id, 
                                 checkitem_id = card_checklist$id[i],  
                                 new_value = input[[card_checklist$id[i]]])
          }
        } 
        newtasks <- input$newtasks
        if (all(is.null(newtasks)) | length(newtasks) == 0) {
          newtasks <- ""
        }
        if (newtasks != "") {
          trello_addtask(card_id = card_info$id, 
                         new_items_vector = newtasks %>% strsplit("\n") %>% unlist())
        }
        
        stopApp()
      }
    })
    
    # ANNULEREN ----
    observeEvent(input$cancel, {
      stopApp()
    })
    
    # TRELLO OPENEN ----
    observeEvent(input$trello_open, {
      browseURL(card_info$url)
    })
  
  }
  
  viewer <- dialogViewer(dialogName = paste0("Taak/project aanpassen, kaart #", card_number),
                         width = 800,
                         height = 620)
  
  suppressMessages(
    runGadget(app = ui,
              server = server,
              viewer = viewer,
              stopOnCancel = FALSE))
}

#' Retourneer map-/bestandslocatie van huidige taak
#' @param card_number Nummer van huidige taak/project.
#' @param filename Bestandsnaam, zie Details.
#' @rdname task_folder
#' @details Alleen mappen in de locatie \code{Sys.getenv("R_PROJECTS")} worden doorzocht.
#' 
#' \strong{De functie \code{task_setfile()}} zoekt de taakmap van het kaartnummer en plakt daar \code{filename} achter. Het maakt zelf geen bestanden, maar kan gebruikt worden als:
#' 
#' \preformatted{
#' df \%>\% 
#'   export_excel(task_setfile("controlebestand"))
#' }
#' 
#' Hierdoor wordt \code{df} opgeslagen als bijv. \code{'Z:/.../Data-analyse/Project - #123/controlebestand.xlsx'}.
#' 
#' \strong{De functie \code{task_getfile()}} zoekt in de taakmap van het kaartnummer naar \code{filename} als reguliere expressie.
#' 
#' \strong{De functie \code{task_open_analysis_file()}} opent het analysebestand in de taakmap; \code{'Analyse #123.R'} of \code{'Analyse #123.Rmd'}.
#' @export
#' @examples
#' \donttest{
#' # terwijl je in een bestand zoals 'Analyse #550.R' zit:
#' data <- task_getfile(".*.xlsx") %>% import.excel()
#' # dit dan leest het eerste Excel-bestand uit die map.
#' 
#' # om op te slaan:
#' data %>% export.excel(task_setfile("test"))
#' # dit slaat `data` op als 'test.xlsx' in de taakmap.
#' }
task_folder <- function(card_number = trello_getcardfromfile()) {
  if (Sys.getenv("R_PROJECTS") == "") {
    stop("Variable 'R_PROJECTS' not set")
  }
  folders <- list.dirs(Sys.getenv("R_PROJECTS"),
                       full.names = FALSE,
                       recursive = FALSE)
  folder <- folders[folders %like% paste0("#", card_number)]
  folder <- folder[1L]
  if (!is.na(folder)) {
    paste0(Sys.getenv("R_PROJECTS"), folder, "/")
  } else {
    warning("No folder found")
    NA_character_
  }
}

#' @rdname task_folder
#' @export
task_getfile <- function(filename, card_number = trello_getcardfromfile()) {
  folder <- task_folder(card_number = card_number)
  filename <- filename[1L]
  if (!is.na(folder)) {
    # regex, proberen bestand te vinden
    file_found <- list.files(path = folder, 
                             pattern = filename,
                             full.names = FALSE, 
                             recursive = FALSE,
                             all.files = FALSE, 
                             include.dirs = FALSE,
                             ignore.case = TRUE)
    if (length(file_found) > 0) {
      if (length(file_found) > 1) {
        message("Files found with regex '", filename, "':\n  - ", paste(file_found, collapse = "\n  - "), "\nSelecting first hit.")
      }
      filename <- file_found[1L]
    }
    filename <- paste0(folder, filename)
  } else {
    return(NA_character_)
  }
  if (file.exists(filename)) {
    filename
  } else {
    NA_character_
  }
}

#' @rdname task_folder
#' @export
task_setfile <- function(filename, card_number = trello_getcardfromfile()) {
  folder <- task_folder(card_number = card_number)
  filename <- filename[1L]
  if (!is.na(folder)) {
    paste0(folder, filename)
  } else {
    NA_character_
  }
}

#' @rdname task_folder
#' @export
task_open_analysis_file <- function(card_number = trello_getcardfromfile()) {
  rstudioapi::navigateToFile(task_getfile("Analyse[.]R", card_number = card_number)) 
}

outlook_content <- function() {
  if (Sys.info()['sysname'] != "Windows") {
    return(NULL)
  }
  if (is.null(readClipboard())) {
    NULL
  } else {
    pasted <- readClipboard()
    if (pasted[1] %like% '^Van\tOnderwerp\tOntvangen\tGrootte') {
      content <- pasted[2] %>% strsplit("\t") %>% unlist()
      c('user' = content[1] %>%
          gsub(' - ', '-', .),
        'subject' = content[2] %>%
          gsub('^(RE:|FWD:|Ant:|Antw:|Doorst:)', '', .) %>%
          trimws('both') %>%
          toproper())
    } else {
      NULL
    }
  }
}

users_list <- function() {
  users <- get_certe_users()
  if (!is.null(users)) {
    users_id <- users$id
    if (!all(is.na(users$job))) {
      users$job[is.na(users$job) | users$job == ''] <- 'functie onbekend'
      names(users_id) <- paste0(users$name, ' (', tolower(users$job), ')')
    } else {
      names(users_id) <- users$name
    }
    users <- users_id
  }
  users
}

img_rstudio <- function() {
  c("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIE",
    "lsbHVzdHJhdG9yIDE5LjIuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcn",
    "Npb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3",
    "cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHdpZHRoPSIzMDBweCIgaGVpZ2h0PSIxMDBweCIgdmlld0JveD0iMCAwID",
    "MwMCAxMDAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMwMCAxMDA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIH",
    "R5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojNzRBQURCO30KCS5zdDF7ZmlsbDojNEU0RTRFO30KCS5zdDJ7ZmlsbDojRkZGRkZGO30KPC",
    "9zdHlsZT4KPGc+Cgk8Y2lyY2xlIGNsYXNzPSJzdDAiIGN4PSI1MSIgY3k9IjQ5LjkiIHI9IjUwIi8+Cgk8Zz4KCQk8cGF0aCBjbGFzcz0ic3",
    "QxIiBkPSJNMTExLjcsNjQuOGMyLjYsMS43LDYuMywyLjksMTAuMywyLjljNS45LDAsOS40LTMuMSw5LjQtNy42YzAtNC4xLTIuNC02LjYtOC",
    "40LTguOAoJCQljLTcuMy0yLjctMTEuOC02LjUtMTEuOC0xMi44YzAtNyw1LjgtMTIuMiwxNC41LTEyLjJjNC41LDAsNy45LDEuMSw5LjgsMi",
    "4ybC0xLjYsNC43Yy0xLjQtMC45LTQuNC0yLjEtOC40LTIuMQoJCQljLTYuMSwwLTguNCwzLjctOC40LDYuN2MwLDQuMiwyLjcsNi4zLDguOS",
    "w4LjZjNy42LDIuOSwxMS40LDYuNiwxMS40LDEzLjJjMCw2LjktNS4xLDEzLTE1LjYsMTNjLTQuMywwLTktMS4zLTExLjQtMi45CgkJCUwxMT",
    "EuNyw2NC44eiIvPgoJCTxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0xNTEuOCwzMS45djcuN2g4LjR2NC41aC04LjR2MTcuNGMwLDQsMS4xLDYuMy",
    "w0LjQsNi4zYzEuNiwwLDIuNS0wLjEsMy40LTAuNGwwLjMsNC41CgkJCWMtMS4xLDAuNC0yLjksMC44LTUuMiwwLjhjLTIuNywwLTQuOS0wLj",
    "ktNi4zLTIuNWMtMS42LTEuOC0yLjMtNC43LTIuMy04LjRWNDQuMWgtNXYtNC41aDV2LTZMMTUxLjgsMzEuOXoiLz4KCQk8cGF0aCBjbGFzcz",
    "0ic3QxIiBkPSJNMTkzLjcsNjNjMCwzLjQsMC4xLDYuMywwLjMsOC44aC01LjJsLTAuMy01LjNoLTAuMWMtMS41LDIuNi00LjksNi0xMC42LD",
    "ZjLTUuMSwwLTExLjEtMi45LTExLjEtMTQuMVYzOS42CgkJCWg1Ljl2MTcuOGMwLDYuMSwxLjksMTAuMyw3LjIsMTAuM2MzLjksMCw2LjYtMi",
    "43LDcuNy01LjRjMC4zLTAuOCwwLjUtMS45LDAuNS0zVjM5LjZoNS45VjYzSDE5My43eiIvPgoJCTxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0yMz",
    "EuMSwyNC42djM4LjljMCwyLjksMC4xLDYuMSwwLjMsOC4zaC01LjJsLTAuMy01LjZoLTAuMmMtMS43LDMuNi01LjYsNi4zLTEwLjgsNi4zCg",
    "kJCWMtNy44LDAtMTMuOC02LjYtMTMuOC0xNi40Yy0wLjEtMTAuNyw2LjYtMTcuMiwxNC40LTE3LjJjNSwwLDguMiwyLjMsOS43LDQuOWgwLj",
    "FWMjQuNkgyMzEuMXogTTIyNS40LDUyLjdjMC0wLjctMC4xLTEuNy0wLjMtMi41CgkJCWMtMC45LTMuNy00LjEtNi43LTguNC02LjdjLTYuMS",
    "wwLTkuNiw1LjMtOS42LDEyLjRjMCw2LjUsMy4zLDExLjksOS41LDExLjljMy45LDAsNy41LTIuNyw4LjYtN2MwLjItMC44LDAuMy0xLjYsMC",
    "4zLTIuNXYtNS42CgkJCUMyMjUuNSw1Mi43LDIyNS40LDUyLjcsMjI1LjQsNTIuN3oiLz4KCQk8cGF0aCBjbGFzcz0ic3QxIiBkPSJNMjQ3Lj",
    "QsMzAuNmMwLDItMS40LDMuNi0zLjcsMy42Yy0yLjEsMC0zLjUtMS42LTMuNS0zLjZzMS41LTMuNywzLjctMy43QzI0NiwyNi45LDI0Ny40LD",
    "I4LjUsMjQ3LjQsMzAuNnoKCQkJIE0yNDAuOSw3MS44VjM5LjZoNS45djMyLjJIMjQwLjl6Ii8+CgkJPHBhdGggY2xhc3M9InN0MSIgZD0iTT",
    "I4NS42LDU1LjVjMCwxMS45LTguMywxNy4xLTE2LDE3LjFjLTguNiwwLTE1LjQtNi40LTE1LjQtMTYuNmMwLTEwLjcsNy4xLTE3LDE2LTE3Cg",
    "kJCUMyNzkuNCwzOSwyODUuNiw0NS43LDI4NS42LDU1LjV6IE0yNjAuMSw1NS44YzAsNyw0LDEyLjQsOS43LDEyLjRjNS42LDAsOS44LTUuMy",
    "w5LjgtMTIuNWMwLTUuNS0yLjctMTIuMy05LjYtMTIuMwoJCQlDMjYzLjEsNDMuNCwyNjAuMSw0OS43LDI2MC4xLDU1Ljh6Ii8+Cgk8L2c+Cg",
    "k8Zz4KCQk8cGF0aCBjbGFzcz0ic3QyIiBkPSJNNjguMSw2NS45aDUuNHY0LjJoLTguM0w1MS42LDQ5LjVoLTcuM3YxNi4zaDcuMlY3MGgtMT",
    "h2LTQuMmg2LjFWMjkuN2wtNi4yLTAuOHYtNGMyLjMsMC41LDQuNCwwLjksNi45LDAuOQoJCQljMy44LDAsNy44LTAuOSwxMS42LTAuOWM3Lj",
    "UsMCwxNC40LDMuNCwxNC40LDExLjdjMCw2LjQtMy44LDEwLjUtOS44LDEyLjJMNjguMSw2NS45eiBNNDQuMiw0NS41bDMuOSwwLjEKCQkJYz",
    "kuNiwwLjIsMTMuMy0zLjUsMTMuMy04LjRjMC01LjctNC4xLTgtOS40LThjLTIuNSwwLTUsMC4yLTcuOCwwLjVDNDQuMiwyOS43LDQ0LjIsND",
    "UuNSw0NC4yLDQ1LjV6Ii8+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==") %>%
    concat()
}
img_trello <- function() {
  c("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iODg1cHgiIGhlaW",
    "dodD0iMjcycHgiIHZpZXdCb3g9IjAgMCA4ODUgMjcyIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zy",
    "IgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCA0MSAoMzUzMj",
    "YpIC0gaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoIC0tPgogICAgPHRpdGxlPnRyZWxsby1sb2dvLWJsdWUtZmxhdDwvdG",
    "l0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPgogICAgICAgIDxsaW5lYXJHcmFkaWVudCB4MT",
    "0iNTAlIiB5MT0iMCUiIHgyPSI1MCUiIHkyPSIxMDAlIiBpZD0ibGluZWFyR3JhZGllbnQtMSI+CiAgICAgICAgICAgIDxzdG9wIHN0b3AtY2",
    "9sb3I9IiMwMDc5QkYiIG9mZnNldD0iMCUiPjwvc3RvcD4KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzAwNzlCRiIgb2Zmc2V0PS",
    "IxMDAlIj48L3N0b3A+CiAgICAgICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDwvZGVmcz4KICAgIDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm",
    "9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSJMb2dvcyIgdHJhbn",
    "Nmb3JtPSJ0cmFuc2xhdGUoLTUwOS4wMDAwMDAsIC00ODUuMDAwMDAwKSI+CiAgICAgICAgICAgIDxnIGlkPSJHcm91cCIgdHJhbnNmb3JtPS",
    "J0cmFuc2xhdGUoLTkuMDAwMDAwLCAxLjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPGcgaWQ9IlRyZWxsby1Mb2dvIiB0cmFuc2Zvcm09In",
    "RyYW5zbGF0ZSg0NjguMDAwMDAwLCAwLjAwMDAwMCkiPgogICAgICAgICAgICAgICAgICAgIDxnIGlkPSJUcmVsbG8tTG9nby0tLUJsdWUtLS",
    "1GbGF0IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLjAwMDAwMCwgNDIwLjAwMDAwMCkiPgogICAgICAgICAgICAgICAgICAgICAgICA8ZyBpZD",
    "0iTG9nbyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNTAuMDAwMDAwLCA2NC4wMDAwMDApIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgID",
    "xnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUwLjAwMDAwMCwgMS4wMDAwMDApIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cG",
    "F0aCBkPSJNNjczLjI5MzU0LDE3Ny41ODk1MjUgQzY2MC41MjI4NjgsMTgyLjc5MzE0NyA2NTAuNDQ1Mjc4LDIwMC43NzA4ODEgNjM1LjExOD",
    "QsMjEwLjk4ODggQzYzNC4xNjgsMjExLjYyMjQgNjMzLjIxNzYsMjExLjkzOTIgNjMyLjU4NCwyMTEuOTM5MiBDNjMxLjMxNjgsMjExLjkzOT",
    "IgNjI5LjczMjgsMjEwLjY3MiA2MjkuNzMyOCwyMDQuOTY5NiBDNjI5LjczMjgsMTg1LjMyOCA2MzYuMDY4OCwxNzUuODI0IDY0MS40NTQ0LD",
    "E2MS44ODQ4IEM2NjAuMTQ1NiwxMTMuNDE0NCA2OTIuMTQyNCw3MS45MTM2IDcyMC42NTQ0LDI2LjYxMTIgQzcyMi44NzIsMjMuMTI2NCA3Mj",
    "QuMTM5MiwxOS4zMjQ4IDcyNC4xMzkyLDE1LjIwNjQgQzcyNC4xMzkyLDExLjcyMTYgNzIyLjg3Miw4Ljg3MDQgNzIxLjI4OCw1LjM4NTYgQz",
    "cyMC4wMjA4LDIuNTM0NCA3MTQuMzE4NCwwIDcwNy45ODI0LDAgQzcwNC40OTc2LDAgNzAxLjMyOTYsMC4zMTY4IDY5Ny4yMTEyLDAuMzE2OC",
    "BDNjgyLjMyMTYsMC4zMTY4IDY3OS4xNTM2LDIyLjE3NiA2NzUuNjY4OCwyNy44Nzg0IEM2NTEuOTA4OCw2OC4xMTIgNjI0LjY2NCwxMTcuOD",
    "Q5NiA2MTAuNzI0OCwxNTMuMDE0NCBDNjA3LjY2ODEwMiwxNjAuODk3NDY0IDYwNC4zNjYzMDksMTY4Ljc4MDUyOCA2MDEuODU2NjMsMTc2Lj",
    "g3MTAzNCBDNTg4LjA4MTg4MywxODAuODU5ODI2IDU3Ny43NjI0NTQsMjAwLjIyNzY5NyA1NjEuNjIwOCwyMTAuOTg4OCBDNTYwLjY3MDQsMj",
    "ExLjYyMjQgNTU5LjcyLDIxMS45MzkyIDU1OS4wODY0LDIxMS45MzkyIEM1NTcuODE5MiwyMTEuOTM5MiA1NTYuMjM1MiwyMTAuNjcyIDU1Ni",
    "4yMzUyLDIwNC45Njk2IEM1NTYuMjM1MiwxODUuMzI4IDU2Mi41NzEyLDE3NS44MjQgNTY3Ljk1NjgsMTYxLjg4NDggQzU4Ni42NDgsMTEzLj",
    "QxNDQgNjE4LjY0NDgsNzEuOTEzNiA2NDcuMTU2OCwyNi42MTEyIEM2NDkuMzc0NCwyMy4xMjY0IDY1MC42NDE2LDE5LjMyNDggNjUwLjY0MT",
    "YsMTUuMjA2NCBDNjUwLjY0MTYsMTEuNzIxNiA2NDkuMzc0NCw4Ljg3MDQgNjQ3Ljc5MDQsNS4zODU2IEM2NDYuNTIzMiwyLjUzNDQgNjQwLj",
    "gyMDgsMCA2MzQuNDg0OCwwIEM2MzEsMCA2MjcuODMyLDAuMzE2OCA2MjMuNzEzNiwwLjMxNjggQzYwOC44MjQsMC4zMTY4IDYwNS42NTYsMj",
    "IuMTc2IDYwMi4xNzEyLDI3Ljg3ODQgQzU3OC40MTEyLDY4LjExMiA1NTEuMTY2NCwxMTcuODQ5NiA1MzcuMjI3MiwxNTMuMDE0NCBDNTM2Lj",
    "ExOTAzOCwxNTUuODcyMjkyIDUzNC45Nzg2NjIsMTU4LjczMDE4NSA1MzMuODU1NDk1LDE2MS41OTc5NjIgQzUzMy41MDg2NTksMTYxLjc4Mz",
    "IxOCA1MzMuMTU0NDc3LDE2MS45ODQxMTQgNTMyLjc5MiwxNjIuMjAxNiBDNTE2LjYzNTIsMTcxLjcwNTYgNTAzLjAxMjgsMTg4LjQ5NiA0OD",
    "MuMzcxMiwxOTkuOTAwOCBDNDc5LjU2OTYsMjAyLjExODQgNDY3Ljg0OCwyMTAuMzU1MiA0NTguMzQ0LDIxMC4zNTUyIEM0NTYuMTI2NCwyMT",
    "AuMzU1MiA0NTQuMjI1NiwyMDkuNzIxNiA0NTIuMzI0OCwyMDguNzcxMiBDNDQ5LjQ3MzYsMjA3LjUwNCA0NDYuNjIyNCwyMDEuNDg0OCA0ND",
    "YuNjIyNCwxOTkuNTg0IEM0NDYuNjIyNCwxOTggNDQ2LjkzOTIsMTk3LjY4MzIgNDUwLjEwNzIsMTk1Ljc4MjQgQzQ3Ny45ODU2LDE3OC45OT",
    "IgNTAwLjc5NTIsMTU0LjkxNTIgNTIwLjQzNjgsMTMwLjgzODQgQzUyNy43MjMyLDEyMS45NjggNTM3LjIyNzIsMTA0LjU0NCA1MzcuMjI3Mi",
    "w5MS44NzIgQzUzNy4yMjcyLDgzLjYzNTIgNTM0LjM3Niw3NC4xMzEyIDUyNC41NTUyLDcwLjY0NjQgQzUxNy41ODU2LDY4LjExMiA1MDkuNj",
    "Y1Niw2Ni44NDQ4IDUwMy4zMjk2LDY2Ljg0NDggQzQ4Ni41MzkyLDY2Ljg0NDggNDc0LjE4NCw3NC40NDggNDY3LjIxNDQsODEuNzM0NCBDND",
    "YwLjExODk2Miw4OS4zMDkyNTk2IDQ1My4yNTU2OSw5Ny4wODcyNjUgNDQ2Ljg3OTMxLDEwNS4xMzg2ODUgQzQ0MC4yMTcwMDQsOTkuMzg2OD",
    "Q5NiA0MzAuODE5ODk3LDk2Ljk0MDggNDIxLjkxMiw5Ni45NDA4IEM0MTEuNDU3Niw5Ni45NDA4IDM5MS44MTYsMTA5LjkyOTYgMzgxLjY3OD",
    "QsMTE3LjUzMjggQzM4MC4wOTQ0LDExOC44IDM3OS4xNDQsMTE5LjQzMzYgMzc4LjUxMDQsMTE5LjQzMzYgQzM3OC4xOTM2LDExOS40MzM2ID",
    "M3Ny44NzY4LDExOS4xMTY4IDM3Ny44NzY4LDExOC40ODMyIEMzNzcuODc2OCwxMTguMTY2NCAzNzguNTEwNCwxMTUuMzE1MiAzNzguNTEwNC",
    "wxMTAuMjQ2NCBDMzc4LjUxMDQsMTA1LjgxMTIgMzc3LjU2LDEwMC4xMDg4IDM3My40NDE2LDkzLjEzOTIgQzM3Mi40OTEyLDkxLjU1NTIgMz",
    "Y2Ljc4ODgsODguMDcwNCAzNTkuODE5Miw4OC4wNzA0IEMzNTEuMjY1Niw4OC4wNzA0IDM0My4zNDU2LDkyLjE4ODggMzQzLjM0NTYsOTYuNj",
    "I0IEMzNDMuMzQ1Niw5OS43OTIgMzQ2LjE5NjgsMTAxLjA1OTIgMzQ2LjE5NjgsMTAzLjkxMDQgQzM0Ni4xOTY4LDEwNS40OTQ0IDM0NC45Mj",
    "k2LDExMi43ODA4IDM0My4wMjg4LDExOS43NTA0IEMzMzcuNjQzMiwxNDAuMDI1NiAzMzAuOTkwNCwxNTkuOTg0IDMyNC4wMjA4LDE3OS45ND",
    "I0IEMzMjAuMjE5MiwxOTEuMDMwNCAzMDcuNTQ3MiwyMDAuODUxMiAzMDcuNTQ3MiwyMTIuODg5NiBDMzA3LjU0NzIsMjE2LjY5MTIgMzEwLj",
    "A4MTYsMjIxLjc2IDMxMy41NjY0LDIyNS41NjE2IEMzMTkuMjY4OCwyMzEuODk3NiAzMjIuNzUzNiwyMzQuMTE1MiAzMjcuODIyNCwyMzQuMT",
    "E1MiBDMzMwLjA0LDIzNC4xMTUyIDMzMi41NzQ0LDIzMy40ODE2IDMzNC43OTIsMjMxLjU4MDggQzMzOS41NDQsMjI3LjQ2MjQgMzQyLjA3OD",
    "QsMjIzLjM0NCAzNDMuMzQ1NiwyMTcuNjQxNiBDMzUxLjU4MjQsMTgwLjI1OTIgMzc1LjM0MjQsMTU0LjkxNTIgNDAxLjYzNjgsMTM4LjEyND",
    "ggQzQxMC41MDcyLDEzMi40MjI0IDQyMi41NDU2LDEyNi40MDMyIDQyNC4xMjk2LDEyNi40MDMyIEM0MjUuNzUzODM5LDEyNi40MDMyIDQyOC",
    "4yOTQxMDMsMTI3LjE1MjY3NSA0MzAuODUzNzkzLDEyNy44ODMxMTEgQzQyMy41MDM1MjIsMTM5Ljg3MTk2OSA0MTcuNTQ3OTM5LDE1Mi41MT",
    "UzODYgNDEzLjY3NTIsMTY2LjAwMzIgQzQxMi40MDgsMTcwLjQzODQgNDExLjc3NDQsMTc0LjU1NjggNDExLjc3NDQsMTc4Ljk5MiBDNDExLj",
    "c3NDQsMTg2LjI3ODQgNDEzLjM1ODQsMTkzLjg4MTYgNDE1Ljg5MjgsMjAxLjE2OCBDNDE5LjM3NzYsMjExLjMwNTYgNDI1LjA4LDIyMC4xNz",
    "YgNDMzLDIyNC4yOTQ0IEM0NDcuNTcyOCwyMzEuODk3NiA0NTguMDI3MiwyMzYuMDE2IDQ2Ny41MzEyLDIzNi4wMTYgQzQ3Mi45MTY4LDIzNi",
    "4wMTYgNDc3LjAzNTIsMjM1LjA2NTYgNDgxLjQ3MDQsMjMyLjUzMTIgQzUwMi4yOTI2NzMsMjIwLjc1MDE3NyA1MTUuMDU4ODg0LDIxMS44MT",
    "I0NyA1MjQuMjc3MTU3LDIwMy44NzM4NDYgQzUyNC41Mjk5MTgsMjExLjkyMjkyMyA1MjYuMTE2NTY2LDIxNi4zODIzMTQgNTMxLjg0MTYsMj",
    "IyLjM5MzYgQzUzNy41NDQsMjI4LjQxMjggNTQ2LjQxNDQsMjMzLjc5ODQgNTU4LjEzNiwyMzYuMzMyOCBDNTYwLjAzNjgsMjM2LjY0OTYgNT",
    "YxLjkzNzYsMjM2Ljk2NjQgNTYzLjgzODQsMjM2Ljk2NjQgQzU3Ny4yNDk0OTksMjM2Ljk2NjQgNTkwLjE1NTk0NywyMjYuMDY1OTQxIDYwMC",
    "4xMDgwNzUsMjE1LjI2MDA0OSBDNjAxLjI2MDM0MiwyMTcuNjE4MDg4IDYwMi45MzQ4LDIxOS44Njg5OCA2MDUuMzM5MiwyMjIuMzkzNiBDNj",
    "ExLjA0MTYsMjI4LjQxMjggNjE5LjkxMiwyMzMuNzk4NCA2MzEuNjMzNiwyMzYuMzMyOCBDNjMzLjUzNDQsMjM2LjY0OTYgNjM1LjQzNTIsMj",
    "M2Ljk2NjQgNjM3LjMzNiwyMzYuOTY2NCBDNjUzLjA0NTc2NywyMzYuOTY2NCA2NjguMDYzMDYyLDIyMi4wMDkwMiA2NzguNDUwMzYsMjA5Lj",
    "c2NzM0NSBDNjgxLjkxMjI4MiwyMTYuOTI3ODQ4IDY5MS41ODY1NDMsMjI0LjAwNzM1MSA3MDMuMjMwNCwyMjkuNjggQzcwNi4zOTg0LDIzMS",
    "4yNjQgNzEwLjIsMjMyLjIxNDQgNzEzLjY4NDgsMjMyLjIxNDQgQzcyNy4zMDcyLDIzMi4yMTQ0IDczNy4xMjgsMjIxLjEyNjQgNzQ0LjczMT",
    "IsMjEyLjg4OTYgQzc2NC4zNzI4LDE5MS4zNDcyIDc3NS4xNDQsMTY0LjczNiA3ODMuMzgwOCwxMzEuMTU1MiBDNzg0LjAxNDQsMTI4LjYyMD",
    "ggNzg0Ljk2NDgsMTI3LjY3MDQgNzg2LjIzMiwxMjcuNjcwNCBDNzg5LjQsMTI3LjY3MDQgNzkzLjUxODQsMTI3LjY3MDQgNzk3Ljk1MzYsMT",
    "I3LjAzNjggQzgwNi41MDcyLDEyNS43Njk2IDgxMy4xNiwxMjIuOTE4NCA4MjEuMDgsMTIxLjY1MTIgQzgyNS44MzIsMTIwLjcwMDggODI1Lj",
    "E5ODQsMTE5LjExNjggODI5LjYzMzYsMTE3LjUzMjggQzgzMi44MDE2LDExNi4yNjU2IDgzNS4zMzYsMTE0Ljk5ODQgODM1LjMzNiwxMTEuOD",
    "MwNCBDODM1LjMzNiwxMDYuNDQ0OCA4MjUuNTE1MiwxMDAuNzQyNCA4MTAuNjI1NiwxMDAuNzQyNCBDNzk4LjkwNCwxMDAuNzQyNCA3OTEuOT",
    "M0NCwxMDEuNjkyOCA3ODcuMTgyNCwxMDEuNjkyOCBDNzc5Ljg5NiwxMDEuNjkyOCA3NzcuOTk1Miw5OS40NzUyIDc3My4yNDMyLDg3LjEyIE",
    "M3NzMuMjQzMiw4Ny4xMiA3NzcuOTk1Miw5OS40NzUyIDc3My4yNDMyLDg3LjEyIEM3NzIuMjkyOCw4NC41ODU2IDc3MS4zNDI0LDgzLjMxOD",
    "QgNzY4LjQ5MTIsODAuNzg0IEM3NjEuODM4NCw3NS4wODE2IDc1My42MDE2LDczLjE4MDggNzQ2Ljk0ODgsNzMuMTgwOCBDNzMxLjEwODgsNz",
    "MuMTgwOCA3MTcuNDg2NCw4OS4zMzc2IDcwNy45ODI0LDEwMi45NiBDNzA1Ljc2NDgsMTA2LjEyOCA3MDIuNTk2OCwxMDguNjYyNCA3MDAuMz",
    "c5MiwxMTIuMTQ3MiBDNjg3LjMyOTE4NSwxMzEuNzIyMjIyIDY3NC44MzQ1MTMsMTUzLjc5NjI5IDY3My4yOTM1NCwxNzcuNTg5NTI1IFogTT",
    "I2OC4yNjQsNjkuNjk2IEMyNzYuMTg0LDY5LjY5NiAyODAuNjE5Miw2OS4wNjI0IDI4MS41Njk2LDY5LjA2MjQgQzI4Mi41Miw2OS4wNjI0ID",
    "I4My4xNTM2LDY5LjM3OTIgMjgzLjE1MzYsNzAuMzI5NiBDMjgzLjE1MzYsNzAuOTYzMiAyODIuODM2OCw3MS45MTM2IDI4MC42MTkyLDc2Lj",
    "Y2NTYgQzI2MS4yOTQ0LDExOC4xNjY0IDI0Ny45ODg4LDE2MC4zMDA4IDIzNi4yNjcyLDIwNi44NzA0IEMyMzUuOTUwNCwyMDguMTM3NiAyMz",
    "UsMjEyLjg4OTYgMjM1LDIxNy42NDE2IEMyMzUsMjIyLjA3NjggMjM2LjI2NzIsMjI2LjgyODggMjQwLjcwMjQsMjI5LjM2MzIgQzI0OS4yNT",
    "YsMjM0LjQzMiAyNTYuMjI1NiwyMzYuNjQ5NiAyNjAuOTc3NiwyMzYuNjQ5NiBDMjY3Ljk0NzIsMjM2LjY0OTYgMjcxLjQzMiwyMzIuMjE0NC",
    "AyNzEuNDMyLDIyNC45MjggQzI3MS40MzIsMjE4LjU5MiAyNzEuNzQ4OCwyMTEuOTM5MiAyNzIuNjk5MiwyMDYuODcwNCBDMjgxLjU2OTYsMT",
    "YwLjMwMDggMjk0Ljg3NTIsMTIzLjg2ODggMzE1LjQ2NzIsODUuODUyOCBDMzI1LjI4OCw2Ny43OTUyIDMyNi41NTUyLDY2Ljg0NDggMzI2Lj",
    "U1NTIsNjUuMjYwOCBDMzI2LjU1NTIsNjQuNjI3MiAzMjYuMjM4NCw2My42NzY4IDMyNS42MDQ4LDYyLjcyNjQgQzMzOC4yNzY4LDU5Ljg3NT",
    "IgMzUzLjQ4MzIsNTcuOTc0NCAzNjcuNDIyNCw1Ny45NzQ0IEMzNjkuMDA2NCw1Ny45NzQ0IDM3NS4zNDI0LDYwLjE5MiAzNzYuMjkyOCw2MS",
    "4xNDI0IEMzNzguNTEwNCw2My4zNiAzODAuNzI4LDY1LjU3NzYgMzg0LjUyOTYsNjUuNTc3NiBDMzg3LjY5NzYsNjUuNTc3NiAzOTMuMDgzMi",
    "w2My4zNiAzOTQuOTg0LDYyLjA5MjggQzM5OS40MTkyLDU4LjkyNDggNDAxLjYzNjgsNTUuNDQgNDAxLjYzNjgsNDkuNzM3NiBDNDAxLjYzNj",
    "gsNDQuMzUyIDM4MS42Nzg0LDMxLjA0NjQgMzY3LjczOTIsMzEuMDQ2NCBDMzUwLjYzMiwzMS4wNDY0IDMzNS4xMDg4LDMzLjI2NCAzMjAuNT",
    "M2LDM2LjExNTIgQzMxNC4yLDM3LjM4MjQgMjkxLjM5MDQsNDAuODY3MiAyNzQuOTE2OCw0MC44NjcyIEMyNTkuMDc2OCw0MC44NjcyIDI2MC",
    "4wMjcyLDM4LjY0OTYgMjUzLjM3NDQsMzguNjQ5NiBDMjQ5LjU3MjgsMzguNjQ5NiAyNDcuNjcyLDQwLjU1MDQgMjQ2LjA4OCw0Mi4xMzQ0IE",
    "MyNDQuNTA0LDQzLjcxODQgMjQzLjU1MzYsNTAuMDU0NCAyNDMuNTUzNiw1NS43NTY4IEMyNDMuNTUzNiw1OC42MDggMjQzLjU1MzYsNjAuOD",
    "I1NiAyNDUuMTM3Niw2Mi43MjY0IEMyNDkuODg5Niw2OC40Mjg4IDI1OS4wNzY4LDY5LjY5NiAyNjguMjY0LDY5LjY5NiBDMjY4LjI2NCw2OS",
    "42OTYgMjU5LjA3NjgsNjkuNjk2IDI2OC4yNjQsNjkuNjk2IFogTTUwMy4wMTI4LDk1LjM1NjggQzUwMy4wMTI4LDk4LjIwOCA1MDIuMDYyNC",
    "wxMDAuMTA4OCA0OTguMjYwOCwxMDYuMTI4IEM0OTQuNDU5MiwxMTIuMTQ3MiA0OTYuMDQzMiwxMTIuMTQ3MiA0OTEuMjkxMiwxMTguNDgzMi",
    "BDNDgyLjEwNCwxMzAuODM4NCA0NzAuNjk5MiwxNDMuMTkzNiA0NTUuODA5NiwxNTYuODE2IEM0NTEuNjkxMiwxNjAuNjE3NiA0NTEuMDU3Ni",
    "wxNjAuNjE3NiA0NTAuNDI0LDE2MC42MTc2IEM0NTAuMTA3MiwxNjAuNjE3NiA0NDkuNDczNiwxNjAuMzAwOCA0NDkuNDczNiwxNTkuNjY3Mi",
    "BDNDQ5LjQ3MzYsMTU4LjcxNjggNDQ5Ljc5MDQsMTU3LjQ0OTYgNDUyLjk1ODQsMTUwLjc5NjggQzQ2Mi43NzkyLDEzMC4yMDQ4IDQ3NC44MT",
    "c2LDExNi41ODI0IDQ4OC4xMjMyLDEwMy4yNzY4IEM0OTUuNDA5Niw5NS45OTA0IDQ5OS44NDQ4LDkzLjQ1NiA1MDEuNDI4OCw5My40NTYgQz",
    "UwMi4zNzkyLDkzLjQ1NiA1MDMuMDEyOCw5My43NzI4IDUwMy4wMTI4LDk1LjM1NjggQzUwMy4wMTI4LDk1LjM1NjggNTAzLjAxMjgsOTMuNz",
    "cyOCA1MDMuMDEyOCw5NS4zNTY4IFogTTc0Ni42MzIsMTAzLjU5MzYgQzc0Ni45NDg4LDEwMy41OTM2IDc0Ny4yNjU2LDEwMy45MTA0IDc0Ny",
    "41ODI0LDEwNC41NDQgQzc0Ny44OTkyLDEwNS40OTQ0IDc0OC41MzI4LDEwNi40NDQ4IDc1MC4xMTY4LDEwNy43MTIgQzc1MS4zODQsMTA4Lj",
    "Y2MjQgNzUxLjM4NCwxMTMuMDk3NiA3NTEuMzg0LDExNi41ODI0IEM3NTEuMzg0LDE0Ny4zMTIgNzMzLjk2LDE3Mi42NTYgNzE2Ljg1MjgsMT",
    "k4LjMxNjggQzcxMy4wNTEyLDIwNC4wMTkyIDcxMC44MzM2LDIwNC45Njk2IDcwOC45MzI4LDIwNC45Njk2IEM3MDcuMzQ4OCwyMDQuOTY5Ni",
    "A3MDQuODE0NCwyMDAuODUxMiA3MDMuODY0LDE5OC4zMTY4IEM3MDIuNTk2OCwxOTQuODMyIDcwMi4yOCwxOTAuNzEzNiA3MDIuMjgsMTg5Lj",
    "EyOTYgQzcwMi4yOCwxNjQuNDE5MiA3MjUuNzIzMiwxMjcuNjcwNCA3NDEuMjQ2NCwxMDcuMzk1MiBDNzQzLjc4MDgsMTAzLjkxMDQgNzQ1Lj",
    "Y4MTYsMTAzLjU5MzYgNzQ2LjYzMiwxMDMuNTkzNiBDNzQ2LjYzMiwxMDMuNTkzNiA3NDUuNjgxNiwxMDMuNTkzNiA3NDYuNjMyLDEwMy41OT",
    "M2IFoiIGlkPSJUeXBlIiBmaWxsPSIjMDA3OUJGIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGcgaWQ9Ik1hcm",
    "siIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuMDAwMDAwLCAzNS4wMDAwMDApIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC",
    "AgPHJlY3QgaWQ9IkJvYXJkIiBmaWxsPSJ1cmwoI2xpbmVhckdyYWRpZW50LTEpIiB4PSIwIiB5PSIwIiB3aWR0aD0iMjAwIiBoZWlnaHQ9Ij",
    "IwMCIgcng9IjI1Ij48L3JlY3Q+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxyZWN0IGlkPSJSaWdodC1MaXN0IiBmaW",
    "xsPSIjRkZGRkZGIiB4PSIxMTMiIHk9IjI2IiB3aWR0aD0iNjEiIGhlaWdodD0iODcuNSIgcng9IjEyIj48L3JlY3Q+CiAgICAgICAgICAgIC",
    "AgICAgICAgICAgICAgICAgICAgICAgIDxyZWN0IGlkPSJMZWZ0LUxpc3QiIGZpbGw9IiNGRkZGRkYiIHg9IjI2IiB5PSIyNiIgd2lkdGg9Ij",
    "YxIiBoZWlnaHQ9IjEzNy41IiByeD0iMTIiPjwvcmVjdD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgIC",
    "AgICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgICAgICA8L2c+CiAgIC",
    "AgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") %>%
    concat()
}
msberends/certedata documentation built on Nov. 26, 2019, 5:19 a.m.