## ----include = FALSE---------------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ## ----------------------------------------------------------------------------- # install.packages(c("shiny", "bslib", "DT")) # # brightspaceR must be installed and authenticated: # # bs_auth() ## ----------------------------------------------------------------------------- # library(shiny) # library(bslib) # library(DT) # library(dplyr) # library(ggplot2) # library(lubridate) # library(brightspaceR) # # # ── Data loading ────────────────────────────────────────────────────────────── # # Load once at startup. In production, wrap in a reactive timer to refresh # # periodically. # message("Loading Brightspace data...") # users <- bs_get_dataset("Users") # enrollments <- bs_get_dataset("User Enrollments") # org_units <- bs_get_dataset("Org Units") # roles <- bs_get_dataset("Role Details") # grades <- bs_get_dataset("Grade Results") # grade_objects <- bs_get_dataset("Grade Objects") # # # Pre-join common combinations # enrollment_detail <- enrollments |> # bs_join_enrollments_roles(roles) |> # bs_join_enrollments_orgunits(org_units) # # grade_detail <- grades |> # bs_join_grades_objects(grade_objects) # # message("Data loaded.") # # # ── UI ──────────────────────────────────────────────────────────────────────── # ui <- page_sidebar( # title = "brightspaceR LMS Explorer", # theme = bs_theme( # preset = "shiny", # primary = "#f59e0b", # "navbar-bg" = "#1a1a2e" # ), # # sidebar = sidebar( # width = 280, # title = "Filters", # selectInput("role_filter", "Role", # choices = c("All", sort(unique(enrollment_detail$role_name))), # selected = "All" # ), # selectInput("course_filter", "Course", # choices = c("All", sort(unique( # org_units$name[org_units$type == "Course Offering"] # ))), # selected = "All" # ), # dateRangeInput("date_range", "Enrollment Date", # start = Sys.Date() - 365, # end = Sys.Date() # ), # hr(), # actionButton("refresh", "Refresh Data", class = "btn-outline-primary btn-sm") # ), # # # KPI cards # layout_columns( # col_widths = c(3, 3, 3, 3), # value_box( # title = "Total Users", value = textOutput("kpi_users"), # showcase = icon("users"), theme = "primary" # ), # value_box( # title = "Enrollments", value = textOutput("kpi_enrollments"), # showcase = icon("graduation-cap"), theme = "info" # ), # value_box( # title = "Courses", value = textOutput("kpi_courses"), # showcase = icon("book"), theme = "success" # ), # value_box( # title = "Avg Grade", value = textOutput("kpi_grade"), # showcase = icon("chart-line"), theme = "warning" # ) # ), # # # Charts row # layout_columns( # col_widths = c(6, 6), # card( # card_header("Enrollments by Role"), # plotOutput("role_chart", height = "300px") # ), # card( # card_header("Monthly Enrollment Trend"), # plotOutput("trend_chart", height = "300px") # ) # ), # # # Second charts row # layout_columns( # col_widths = c(6, 6), # card( # card_header("Grade Distribution"), # plotOutput("grade_chart", height = "300px") # ), # card( # card_header("Top 10 Courses"), # plotOutput("course_chart", height = "300px") # ) # ), # # # Data table # card( # card_header("Enrollment Detail"), # DTOutput("enrollment_table") # ) # ) # # # ── Server ──────────────────────────────────────────────────────────────────── # server <- function(input, output, session) { # # # Filtered enrollment data # filtered_enrollments <- reactive({ # df <- enrollment_detail # # if (input$role_filter != "All") { # df <- df |> filter(role_name == input$role_filter) # } # if (input$course_filter != "All") { # df <- df |> filter(name == input$course_filter) # } # if (!is.null(input$date_range)) { # df <- df |> filter( # as.Date(enrollment_date) >= input$date_range[1], # as.Date(enrollment_date) <= input$date_range[2] # ) # } # df # }) # # # Filtered grades # filtered_grades <- reactive({ # df <- grade_detail |> # filter(!is.na(points_numerator), points_numerator >= 0) # # if (input$course_filter != "All") { # course_ids <- org_units |> # filter(name == input$course_filter) |> # pull(org_unit_id) # df <- df |> filter(org_unit_id %in% course_ids) # } # df # }) # # # ── KPIs ── # output$kpi_users <- renderText({ # format(nrow(users), big.mark = ",") # }) # # output$kpi_enrollments <- renderText({ # format(nrow(filtered_enrollments()), big.mark = ",") # }) # # output$kpi_courses <- renderText({ # n <- filtered_enrollments() |> # filter(type == "Course Offering") |> # distinct(org_unit_id) |> # nrow() # format(n, big.mark = ",") # }) # # output$kpi_grade <- renderText({ # g <- filtered_grades() # if (nrow(g) == 0) return("--") # paste0(round(mean(g$points_numerator, na.rm = TRUE), 1), "%") # }) # # # ── Charts ── # chart_theme <- theme_minimal(base_size = 13) + # theme( # plot.background = element_rect(fill = "white", colour = NA), # panel.grid.minor = element_blank() # ) # # output$role_chart <- renderPlot({ # filtered_enrollments() |> # count(role_name, sort = TRUE) |> # head(8) |> # ggplot(aes(x = reorder(role_name, n), y = n, fill = role_name)) + # geom_col(show.legend = FALSE) + # coord_flip() + # scale_fill_brewer(palette = "Set2") + # labs(x = NULL, y = "Count") + # chart_theme # }) # # output$trend_chart <- renderPlot({ # filtered_enrollments() |> # mutate(month = floor_date(as.Date(enrollment_date), "month")) |> # count(month) |> # ggplot(aes(x = month, y = n)) + # geom_line(colour = "#818cf8", linewidth = 1) + # geom_point(colour = "#818cf8", size = 2) + # scale_x_date(date_labels = "%b %Y") + # labs(x = NULL, y = "New Enrollments") + # chart_theme # }) # # output$grade_chart <- renderPlot({ # filtered_grades() |> # ggplot(aes(x = points_numerator)) + # geom_histogram(binwidth = 5, fill = "#38bdf8", colour = "white") + # labs(x = "Grade (%)", y = "Count") + # chart_theme # }) # # output$course_chart <- renderPlot({ # filtered_enrollments() |> # filter(type == "Course Offering") |> # count(name, sort = TRUE) |> # head(10) |> # ggplot(aes(x = reorder(name, n), y = n)) + # geom_col(fill = "#f59e0b") + # coord_flip() + # labs(x = NULL, y = "Enrollments") + # chart_theme # }) # # # ── Data table ── # output$enrollment_table <- renderDT({ # filtered_enrollments() |> # select(any_of(c( # "user_id", "role_name", "name", "type", # "enrollment_date" # ))) |> # head(500) # }, options = list(pageLength = 15, scrollX = TRUE)) # # # ── Refresh button ── # observeEvent(input$refresh, { # showNotification("Refreshing data...", type = "message") # # In production, re-fetch from Brightspace here # }) # } # # shinyApp(ui, server) ## ----------------------------------------------------------------------------- # # From the directory containing app.R: # shiny::runApp() # # # Or run from anywhere: # shiny::runApp("/path/to/app.R") ## ----------------------------------------------------------------------------- # # In UI: # plotly::plotlyOutput("role_chart", height = "300px") # # # In server: # output$role_chart <- plotly::renderPlotly({ # p <- ggplot(...) + geom_col(...) # plotly::ggplotly(p) # }) ## ----------------------------------------------------------------------------- # # In server: # bs_data <- reactive({ # # Each user needs their own token # bs_auth_token(session$userData$token) # list( # users = bs_get_dataset("Users"), # enrollments = bs_get_dataset("User Enrollments") # ) # }) ## ----------------------------------------------------------------------------- # # In UI, inside the enrollment_table card: # downloadButton("download_csv", "Export CSV") # # # In server: # output$download_csv <- downloadHandler( # filename = function() { # paste0("enrollments_", Sys.Date(), ".csv") # }, # content = function(file) { # readr::write_csv(filtered_enrollments(), file) # } # ) ## ----------------------------------------------------------------------------- # # Re-fetch every 30 minutes # auto_refresh <- reactiveTimer(30 * 60 * 1000) # # live_enrollments <- reactive({ # auto_refresh() # bs_get_dataset("User Enrollments") # }) ## ----------------------------------------------------------------------------- # # Write once: # board <- pins::board_connect() # pins::pin_write(board, bs_get_dataset("Users"), "brightspace_users") # # # Read in app: # users <- pins::pin_read(board, "brightspace_users")