library(pacman) p_load( tidyverse, plotly, bslib, bsicons, crosstalk, DT, tidytext, sentimentr, prophet, survival, ggfortify, arules, arulesViz, htmltools )

col_date <- “Date” # The date column col_class <- “Classification” # The category/class column col_priority <- “Priority” # The priority column (P1, P2, etc) col_res_days <- “Resolution_Days” # The number of days to resolve col_text <- “Text_Description” # The column containing the text/complaint col_agent <- “Agent” # The agent name column col_escalated <- “Escalated” # The escalation (TRUE/FALSE) column col_id <- “ticket_id” # The unique ID column

if (!file.exists(“tickets.csv”)) { stop(“The file ‘tickets.csv’ was not found in the working directory. Please ensure the file is uploaded.”) }

raw_data <- read_csv(“tickets.csv”, show_col_types = FALSE)

tickets <- raw_data |> rename( Date = any_of(col_date), Classification = any_of(col_class), Priority = any_of(col_priority), Resolution_Days = any_of(col_res_days), Text_Description = any_of(col_text), Agent = any_of(col_agent), Escalated = any_of(col_escalated), ticket_id = any_of(col_id) )

tickets <- tickets |> mutate( across(any_of(c(“Classification”, “Priority”, “Agent”)), as.factor), Resolution_Days = as.numeric(Resolution_Days), Escalated = as.logical(Escalated) ) |> filter(!is.na(Date)) |> arrange(Date)

if (!“ticket_id” %in% colnames(tickets)) { tickets <- tickets |> mutate(ticket_id = paste0(“TKT-”, row_number())) }

sd <- SharedData$new(tickets, key = ~ticket_id, group = “tickets_group”)

kpi_total <- format(nrow(tickets), big.mark = “,”) kpi_avg_days <- round(mean(tickets\(Resolution_Days, na.rm = TRUE), 1) kpi_esc_rate <- if("Escalated" %in% colnames(tickets)) { paste0(round(mean(tickets\)Escalated, na.rm = TRUE) * 100, 1), “%”) } else { “N/A” }

transparent_layout <- function(p, …) { p |> layout(autosize = TRUE, paper_bgcolor = “rgba(0,0,0,0)”, plot_bgcolor = “rgba(0,0,0,0)”, font = list(family = “inherit”, color = “#343a40”), …) }

tags\(div( style = "padding: 14px 12px;", tags\)div( style = “display:flex; align-items:center; gap:8px; margin-bottom:18px; border-bottom:1px solid rgba(246,139,30,0.5); padding-bottom:12px;”, bs_icon(“sliders”, style = “color:#F68B1E; font-size:1.1rem;”), tags$h6(“Dashboard Filters”, style = “color:#F68B1E; font-weight:800; margin:0; font-size:0.95rem;”) ),

# Only show filter if column exists if(“Classification” %in% colnames(tickets)) { tags\(div( tags\)p(“CLASSIFICATION”, style = “color:#adb5bd; font-size:0.72rem; margin-bottom:4px;”), filter_select(“f_class”, “All types”, sd, ~Classification) ) },

tags$div(style = “margin-top:16px;”),

if(“Priority” %in% colnames(tickets)) { tags\(div( tags\)p(“PRIORITY LEVEL”, style = “color:#adb5bd; font-size:0.72rem; margin-bottom:4px;”), filter_checkbox(“f_priority”, NULL, sd, ~Priority, inline = TRUE) ) },

tags$div(style = “margin-top:16px;”),

if(“Resolution_Days” %in% colnames(tickets)) { tags\(div( tags\)p(“RESOLUTION DAYS”, style = “color:#adb5bd; font-size:0.72rem; margin-bottom:4px;”), filter_slider(“f_days”, NULL, sd, ~Resolution_Days, step = 0.5, round = 1) ) } )

layout_columns( col_widths = c(4, 4, 4), value_box(title = “Total Tickets”, value = kpi_total, showcase = bs_icon(“ticket-perforated-fill”), theme = value_box_theme(bg = “#ffffff”, fg = “#0B1F3A”)), value_box(title = “Avg. Resolution Days”, value = paste(kpi_avg_days, “days”), showcase = bs_icon(“clock-history”), theme = value_box_theme(bg = “#ffffff”, fg = “#0B1F3A”)), value_box(title = “Escalation Rate”, value = kpi_esc_rate, showcase = bs_icon(“activity”), theme = value_box_theme(bg = “#ffffff”, fg = “#0B1F3A”)) )

ts_raw <- tickets |> count(Date, name = “Volume”) |> arrange(Date) |> mutate(MA7 = as.numeric(stats::filter(Volume, rep(1/7, 7), sides = 1)))

fig_ts <- plot_ly() |> add_trace(data = ts_raw, x = ~Date, y = ~Volume, type = “scatter”, mode = “none”, fill = “tozeroy”, fillcolor = “rgba(246,139,30,0.22)”, name = “Daily Volume”) |> add_lines(data = ts_raw, x = ~Date, y = ~MA7, line = list(color = “#0B1F3A”, width = 2.2), name = “7-Day MA”) |> transparent_layout(xaxis = list(title = ““), yaxis = list(title =”Ticket Count”), margin = list(t = 10, b = 50))

donut_data <- tickets |> count(Classification) fig_donut <- plot_ly(donut_data, labels = ~Classification, values = ~n, type = “pie”, hole = 0.54, marker = list(colors = c(“#F68B1E”, “#0B1F3A”, “#6c9cbf”)), textinfo = “label+percent”) |> transparent_layout(margin = list(t = 10, b = 50))

fig_scatter <- plot_ly(data = sd, x = ~Date, y = ~Resolution_Days, color = ~Priority, type = “scatter”, mode = “markers”, marker = list(size = 5, opacity = 0.65)) |> transparent_layout(xaxis = list(title = “Date”), yaxis = list(title = “Days”), margin = list(t = 10, b = 50))

dt_live <- datatable(sd, extensions = “Buttons”, rownames = FALSE, options = list(dom = “Bfrtip”, buttons = list(“excel”, “csv”), pageLength = 8))

accordion( id = “ops_acc”, open = “ops_lab”, accordion_panel(title = “📚 Theory”, value = “ops_theory”, tags$div(style=“padding:15px”, “Operations Analysis Section.”)), accordion_panel(title = “🔬 Analysis Lab”, value = “ops_lab”, layout_columns(col_widths = c(7, 5), card(card_header(“📈 Ticket Volume”), fig_ts), card(card_header(“🍩 Breakdown”), fig_donut)), card(card_header(“⚡ Resolution Scatter”), fig_scatter), card(card_header(“📋 Ticket Register”), dt_live) ) )

if(“Text_Description” %in% colnames(tickets)) { token_df <- tickets |> mutate(doc_id = row_number()) |> select(doc_id, Classification, Text_Description) |> unnest_tokens(word, Text_Description) |> anti_join(stop_words, by = “word”) |> filter(!str_detect(word, “1+$”), nchar(word) > 2)

tfidf_df <- token_df |> count(Classification, word) |> bind_tf_idf(word, Classification, n) |> group_by(Classification) |> slice_max(tf_

Footnotes

  1. 0-9↩︎