diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b6a065 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.Rproj.user +.Rhistory +.RData +.Ruserdata diff --git a/Pre_runner.Rmd b/Pre_runner.Rmd new file mode 100644 index 0000000..49bc3a0 --- /dev/null +++ b/Pre_runner.Rmd @@ -0,0 +1,212 @@ +--- +title: "Pre" +author: "Scary Scarecrow" +date: '2022-06-27' +output: html_document +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set(echo = TRUE) +library(dplyr) +library(echarts4r) +library(lubridate) +library(shinymanager) +``` + +## Credentials + +```{r} +credentials <- data.frame( + user = c("shiny", "asitav", "rigo", "aldo"), + password = c("lanubia@2021", "lanubia@2021","lanubia@2021","lanubia@2021"), + admin = c(FALSE, TRUE, FALSE, FALSE), + email = c("hello@asitavsen.com","asitav.sen@lanubia.com","rigo.selassa@lanubia.com","aldo.silvano@lanubia.com"), + stringsAsFactors = FALSE +) + +create_db( + credentials_data = credentials, + sqlite_path = "./cred.sqlite", # will be created + passphrase = "kJuyhG657Hj&^%gshj*762hjsknh&662" +) +``` + + +## DB + +```{r} +connec <- dbConnect( + RPostgres::Postgres(), + dbname = dsn_database, + host = dsn_hostname, + port = dsn_port, + user = dsn_uid, + password = dsn_pwd +) +``` + + + +```{r} +#dat<-read.csv("./data/initial.csv") +#lims<-read.csv("./data/dev.limits.csv") +dat.1<- + dat |> + mutate(devia=Actual-Plan) |> + mutate(devia.per=round(devia/Plan,2)) +#dbCreateTable(connec, "calculated", dat.1) +dbWriteTable(connec, "calculated", dat.1, append=TRUE) +dbWriteTable(connec, "limits", lims, append=TRUE) + +#dbRemoveTable(connec,"calculated") +dat<-dbGetQuery( + connec, + 'SELECT * FROM calculated' +) + +lims<-dbGetQuery( + connec, + 'SELECT * FROM limits' +) + +#dbReadTable(connec, "calculated") + +exp<- + dat |> + inner_join(lims, by=c("GL.account")) |> + mutate(act.req=ifelse(devia.per>Limit & devia > 500,T,F)) |> + filter(act.req) |> + select(1:3) |> + mutate(explanation=c("Cyberattack","Overwork","Interview","Maintenance","Quarterly Report","New Legislation")) + +dbWriteTable(connec, "explanations", exp, append=TRUE) +exp<-dbGetQuery( + connec, + 'SELECT * FROM explanations' +) + +approvals<- exp |> mutate(approved=F) + +dbWriteTable(connec, "approvals", approvals, overwrite=T) + +approvals<-dbGetQuery( + connec, + 'SELECT * FROM approvals' +) + + + dat |> + inner_join(lims, by=c("GL.account")) |> + mutate(act.req=ifelse(devia.per>Limit & devia > 500,T,F)) |> + filter(act.req) |> + left_join(exp, by=c("month"="month","Cost.center"="Cost.center","GL.account"="GL.account")) + + + emailsids<-dat |> + select(Cost.center, GL.account) |> + distinct() |> + mutate(email=c("asitav.sen@lanubia.com")) +dbWriteTable(connec, "emails", emailsids, overwrite=T) +emailsids<-dbGetQuery( + connec, + 'SELECT * FROM emails' +) + +sel.emails<- emailsids |> + filter(email=="asitav.sen@lanubia.com") + + dat |> + inner_join(lims, by=c("GL.account")) |> + mutate(act.req=ifelse(devia.per>Limit & devia > 500,T,F)) |> + filter(act.req) |> + left_join(sel.emails, by=c("Cost.center"="Cost.center","GL.account"="GL.account")) |> + left_join(exp, by=c("month"="month","Cost.center"="Cost.center","GL.account"="GL.account")) |> + filter(is.na(explanation) | explanation=="") |> + filter(email=="asitav.sen@lanubia.com") |> + select(-c(9,10)) + + exp +``` + + + +```{r} + +dat.2<- + dat |> + mutate(month=ym(month)) |> + group_by(month) |> + summarise(Plan=sum(Plan),Actual=sum(Actual)) |> + mutate(devia=Actual-Plan) |> + mutate(deviation.percent=round(devia*100/Plan,2)) + + e_chart(dat.2, x=month) |> + e_line(serie = deviation.percent, smooth=T, color="cyan") |> + e_area(serie = deviation.percent, smooth=T, color="gray") |> + e_axis_labels(x = "month", y="Deviation") |> + e_format_y_axis(suffix = " %") |> + e_title("Deviation", "Selected Cost Centers") |> + e_tooltip() |> + e_legend(right = 100) |> + e_datazoom(x_index = c(0, 1)) |> + e_toolbox_feature(feature = c("saveAsImage","dataView")) |> + e_theme("chalk") + + history<-dat.2 |> select(month,deviation.percent) |> rename(ds=month, y=deviation.percent) + model <- prophet::prophet(history) + future <- prophet::make_future_dataframe(model, periods = 2) + forecast <- predict(model, future) + +dat |> + filter(month==max(month)) |> + mutate(cost_gl=paste0(Cost.center,"_",GL.account)) |> + group_by(cost_gl) |> + summarise(Plan=sum(Plan),Actual=sum(Actual)) |> + mutate(devia=Actual-Plan) |> + mutate(deviation.percent=round(devia*100/Plan,2)) |> + arrange(desc(deviation.percent)) |> + e_charts(cost_gl) |> + e_bar(Plan, name = "Plan", color="gray") |> + e_step(Actual, name = "Actual", color="red") |> + e_axis_labels(x = "GL+Cost Center", y="Deviation") |> + e_title("Selected Cost Centers") |> + e_tooltip() |> + e_legend(right = 100) |> + e_datazoom(x_index = 0, type = "slider") |> + e_datazoom(y_index = 0, type = "slider") |> + e_toolbox_feature(feature = c("saveAsImage","dataView")) |> + e_theme("chalk") +``` +```{r} +unique(dat$Cost.center) |> saveRDS("./data/costcenters.RDS") +unique(dat$GL.account) |> saveRDS("./data/glaccounts.RDS") +unique(dat$month) |> saveRDS("./data/months.RDS") +lims +``` + + + + + +```{r} +dat.dev<-dat |> + filter(devia>0) |> + select(2,3,6) + +e_charts(dat.dev) |> + e_pie(devia) + +blastula::create_smtp_creds_file( + file = "email_creds", + user = "apikey", + host = "smtp.sendgrid.net", + port = 465, + use_ssl = TRUE +) + +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 +``` + + + + diff --git a/bUdgEtRack.Rproj b/bUdgEtRack.Rproj new file mode 100644 index 0000000..8e3c2eb --- /dev/null +++ b/bUdgEtRack.Rproj @@ -0,0 +1,13 @@ +Version: 1.0 + +RestoreWorkspace: Default +SaveWorkspace: Default +AlwaysSaveHistory: Default + +EnableCodeIndexing: Yes +UseSpacesForTab: Yes +NumSpacesForTab: 2 +Encoding: UTF-8 + +RnwWeave: Sweave +LaTeX: pdfLaTeX diff --git a/cred.sqlite b/cred.sqlite new file mode 100644 index 0000000..3bfbfd4 Binary files /dev/null and b/cred.sqlite differ diff --git a/data/Jul22.csv b/data/Jul22.csv new file mode 100644 index 0000000..7fe1861 --- /dev/null +++ b/data/Jul22.csv @@ -0,0 +1,23 @@ +month,Cost center,GL account,Plan,Actual +202205,IT,Salaries,30000,29000 +202205,IT,Business travel,5000,6000 +202205,IT,Consultancy,10000,10500 +202205,IT,Training costs,5000,6000 +202205,HR local support,Salaries,15000,13000 +202205,HR local support,Events,10000,9000 +202205,HR local support,Training costs,2000,2700 +202205,Finance,Salaries,13000,11000 +202205,Finance,Consultancy,6000,5000 +202205,Finance,Training costs,500,500 +202205,Purchasing,Salaries,40000,40000 +202205,Purchasing,Consultancy,2000,1500 +202205,Purchasing,Training costs,1000,1500 +202205,HR services,Salaries,35000,39000 +202205,HR services,Business travel,10000,9000 +202205,HR services,Training costs,2000,1000 +202205,Public Relations,Salaries,10000,12000 +202205,Public Relations,Events,18000,19200 +202205,Public Relations,Business travel,2000,1000 +202205,Public Relations,Training costs,600,700 +202205,Facility management,Office lease,50000,50000 +202205,Facility management,Salaries,5000,4000 \ No newline at end of file diff --git a/data/Jun22.csv b/data/Jun22.csv new file mode 100644 index 0000000..9cacbfc --- /dev/null +++ b/data/Jun22.csv @@ -0,0 +1,23 @@ +month,Cost center,GL account,Plan,Actual +202205,IT,Salaries,30000,33000 +202205,IT,Business travel,5000,4000 +202205,IT,Consultancy,10000,11000 +202205,IT,Training costs,5000,6000 +202205,HR local support,Salaries,15000,16000 +202205,HR local support,Events,10000,8000 +202205,HR local support,Training costs,2000,3000 +202205,Finance,Salaries,13000,15000 +202205,Finance,Consultancy,6000,5000 +202205,Finance,Training costs,500,500 +202205,Purchasing,Salaries,40000,42000 +202205,Purchasing,Consultancy,2000,3000 +202205,Purchasing,Training costs,1000,2000 +202205,HR services,Salaries,35000,39000 +202205,HR services,Business travel,10000,9000 +202205,HR services,Training costs,2000,1000 +202205,Public Relations,Salaries,10000,12000 +202205,Public Relations,Events,18000,19000 +202205,Public Relations,Business travel,2000,1000 +202205,Public Relations,Training costs,600,600 +202205,Facility management,Office lease,50000,50000 +202205,Facility management,Salaries,5000,4000 \ No newline at end of file diff --git a/data/apr22.csv b/data/apr22.csv new file mode 100644 index 0000000..3c596bf --- /dev/null +++ b/data/apr22.csv @@ -0,0 +1,23 @@ +month,Cost center,GL account,Plan,Actual +202204,IT,Salaries,30000,31000 +202204,IT,Business travel,5000,6000 +202204,IT,Consultancy,10000,10500 +202204,IT,Training costs,5000,5000 +202204,HR local support,Salaries,15000,13000 +202204,HR local support,Events,10000,9000 +202204,HR local support,Training costs,2000,2700 +202204,Finance,Salaries,13000,15000 +202204,Finance,Consultancy,6000,5000 +202204,Finance,Training costs,500,500 +202204,Purchasing,Salaries,40000,40000 +202204,Purchasing,Consultancy,2000,1500 +202204,Purchasing,Training costs,1000,1500 +202204,HR services,Salaries,35000,39000 +202204,HR services,Business travel,10000,9000 +202204,HR services,Training costs,2000,1000 +202204,Public Relations,Salaries,10000,10000 +202204,Public Relations,Events,18000,19200 +202204,Public Relations,Business travel,2000,1000 +202204,Public Relations,Training costs,600,600 +202204,Facility management,Office lease,50000,50000 +202204,Facility management,Salaries,5000,4000 \ No newline at end of file diff --git a/data/costcenters.RDS b/data/costcenters.RDS new file mode 100644 index 0000000..65929b7 Binary files /dev/null and b/data/costcenters.RDS differ diff --git a/data/dev.limits.csv b/data/dev.limits.csv new file mode 100644 index 0000000..dd7182d --- /dev/null +++ b/data/dev.limits.csv @@ -0,0 +1,7 @@ +GL account,Limit +Office lease,0.15 +Events,0.10 +Consultancy,0.10 +Business travel,0.10 +Salaries,0.10 +Training costs,0.15 \ No newline at end of file diff --git a/data/email_creds b/data/email_creds new file mode 100644 index 0000000..4f69aff --- /dev/null +++ b/data/email_creds @@ -0,0 +1 @@ +{"type":"list","attributes":{"names":{"type":"character","attributes":{},"value":["version","host","port","use_ssl","user","password"]}},"value":[{"type":"integer","attributes":{},"value":[1]},{"type":"character","attributes":{},"value":["smtp.sendgrid.net"]},{"type":"double","attributes":{},"value":[465]},{"type":"logical","attributes":{},"value":[true]},{"type":"character","attributes":{},"value":["apikey"]},{"type":"character","attributes":{},"value":["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"]}]} diff --git a/data/glaccounts.RDS b/data/glaccounts.RDS new file mode 100644 index 0000000..eda2965 Binary files /dev/null and b/data/glaccounts.RDS differ diff --git a/data/initial.csv b/data/initial.csv new file mode 100644 index 0000000..d896442 --- /dev/null +++ b/data/initial.csv @@ -0,0 +1,67 @@ +month,Cost center,GL account,Plan,Actual +202201,IT,Salaries,30000,29000 +202201,IT,Business travel,5000,6150 +202201,IT,Consultancy,10000,12000 +202201,IT,Training costs,5000,4800 +202201,HR local support,Salaries,15000,15500 +202201,HR local support,Events,5000,4900 +202201,HR local support,Training costs,2000,1500 +202201,Finance,Salaries,13000,13000 +202201,Finance,Consultancy,6000,6500 +202201,Finance,Training costs,4000,4500 +202201,Purchasing,Salaries,40000,41500 +202201,Purchasing,Consultancy,2000,2000 +202201,Purchasing,Training costs,1000,1000 +202201,HR services,Salaries,35000,36000 +202201,HR services,Business travel,10000,14000 +202201,HR services,Training costs,2000,1800 +202201,Public Relations,Salaries,10000,8000 +202201,Public Relations,Events,18000,19000 +202201,Public Relations,Business travel,2000,1950 +202201,Public Relations,Training costs,600,500 +202201,Facility management,Office lease,50000,48000 +202201,Facility management,Salaries,5000,4500 +202202,IT,Salaries,30000,29000 +202202,IT,Business travel,5000,5000 +202202,IT,Consultancy,10000,9000 +202202,IT,Training costs,5000,5000 +202202,HR local support,Salaries,15000,15500 +202202,HR local support,Events,0,0 +202202,HR local support,Training costs,2000,2200 +202202,Finance,Salaries,13000,13000 +202202,Finance,Consultancy,6000,5000 +202202,Finance,Training costs,0,0 +202202,Purchasing,Salaries,40000,41500 +202202,Purchasing,Consultancy,2000,2000 +202202,Purchasing,Training costs,1000,0 +202202,HR services,Salaries,35000,36000 +202202,HR services,Business travel,10000,7000 +202202,HR services,Training costs,2000,2000 +202202,Public Relations,Salaries,10000,8000 +202202,Public Relations,Events,18000,10000 +202202,Public Relations,Business travel,2000,2200 +202202,Public Relations,Training costs,600,600 +202202,Facility management,Office lease,50000,48000 +202202,Facility management,Salaries,5000,4500 +202203,IT,Salaries,30000,30000 +202203,IT,Business travel,5000,6000 +202203,IT,Consultancy,10000,10500 +202203,IT,Training costs,5000,5000 +202203,HR local support,Salaries,15000,15000 +202203,HR local support,Events,10000,5000 +202203,HR local support,Training costs,2000,1000 +202203,Finance,Salaries,13000,13000 +202203,Finance,Consultancy,6000,7000 +202203,Finance,Training costs,500,500 +202203,Purchasing,Salaries,40000,40000 +202203,Purchasing,Consultancy,2000,2000 +202203,Purchasing,Training costs,1000,1000 +202203,HR services,Salaries,35000,35000 +202203,HR services,Business travel,10000,10000 +202203,HR services,Training costs,2000,3000 +202203,Public Relations,Salaries,10000,10600 +202203,Public Relations,Events,18000,18000 +202203,Public Relations,Business travel,2000,1000 +202203,Public Relations,Training costs,600,1000 +202203,Facility management,Office lease,50000,50000 +202203,Facility management,Salaries,5000,5000 \ No newline at end of file diff --git a/data/may22.csv b/data/may22.csv new file mode 100644 index 0000000..f64169b --- /dev/null +++ b/data/may22.csv @@ -0,0 +1,23 @@ +month,Cost center,GL account,Plan,Actual +202205,IT,Salaries,30000,31000 +202205,IT,Business travel,5000,6000 +202205,IT,Consultancy,10000,10500 +202205,IT,Training costs,5000,5000 +202205,HR local support,Salaries,15000,13000 +202205,HR local support,Events,10000,9000 +202205,HR local support,Training costs,2000,2700 +202205,Finance,Salaries,13000,15000 +202205,Finance,Consultancy,6000,5000 +202205,Finance,Training costs,500,500 +202205,Purchasing,Salaries,40000,40000 +202205,Purchasing,Consultancy,2000,1500 +202205,Purchasing,Training costs,1000,1500 +202205,HR services,Salaries,35000,39000 +202205,HR services,Business travel,10000,9000 +202205,HR services,Training costs,2000,1000 +202205,Public Relations,Salaries,10000,10000 +202205,Public Relations,Events,18000,19200 +202205,Public Relations,Business travel,2000,1000 +202205,Public Relations,Training costs,600,600 +202205,Facility management,Office lease,50000,50000 +202205,Facility management,Salaries,5000,4000 \ No newline at end of file diff --git a/data/may22.xlsx b/data/may22.xlsx new file mode 100644 index 0000000..182957d Binary files /dev/null and b/data/may22.xlsx differ diff --git a/data/months.RDS b/data/months.RDS new file mode 100644 index 0000000..52d5937 Binary files /dev/null and b/data/months.RDS differ diff --git a/email_creds b/email_creds new file mode 100644 index 0000000..4f69aff --- /dev/null +++ b/email_creds @@ -0,0 +1 @@ +{"type":"list","attributes":{"names":{"type":"character","attributes":{},"value":["version","host","port","use_ssl","user","password"]}},"value":[{"type":"integer","attributes":{},"value":[1]},{"type":"character","attributes":{},"value":["smtp.sendgrid.net"]},{"type":"double","attributes":{},"value":[465]},{"type":"logical","attributes":{},"value":[true]},{"type":"character","attributes":{},"value":["apikey"]},{"type":"character","attributes":{},"value":["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"]}]} diff --git a/helper_server.R b/helper_server.R new file mode 100644 index 0000000..ce99722 --- /dev/null +++ b/helper_server.R @@ -0,0 +1,87 @@ + + + +# Database details + + +dsn_database <- "postgres" + +dsn_hostname <- "localhost" + +dsn_port <- "5432" + +dsn_uid <- "postgres" + +dsn_pwd <- "julley09" + + + + +# Read Cost Center etc. list from data (Changes, modification needs creating the file again and redeploying the app) + +cost.centers <- readRDS("./data/costcenters.RDS") +glaccounts <- readRDS("./data/glaccounts.RDS") +mont <- readRDS("./data/months.RDS") + +# Email settings + +# smtp <- server( +# host = "smtp.sendgrid.net", +# port = 465, +# username = "apikey", +# password = "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" +# ) + + +# Function to get data from DB + +get.db.data <- function(qry = 'SELECT * FROM calculated') { + dbGetQuery(connec, + qry) +} + +# Function to aggregate monthly deviation + +mon.dev <- function(dat) { + dat |> + mutate(month = ym(month)) |> + group_by(month) |> + summarise(Plan = sum(Plan), Actual = sum(Actual)) |> + mutate(devia = Actual - Plan) |> + mutate(deviation.percent = round(devia * 100 / Plan, 2)) +} + +# Function to aggregate last month by gl and cost + +last.mon <- function(dat) { + dat |> + filter(month == max(month)) |> + mutate(cost_gl = paste0(Cost.center, "_", GL.account)) |> + group_by(cost_gl) |> + summarise(Plan = sum(Plan), Actual = sum(Actual)) |> + mutate(devia = Actual - Plan) |> + mutate(deviation.percent = round(devia * 100 / Plan, 2)) |> + arrange(desc(deviation.percent)) +} + + +admin.menu <- sidebarMenu( + id = "m", + menuItem("Dashboard", tabName = "dashboard", icon = icon("chart-line")), + menuItem("Upload", icon = icon("upload"), tabName = "upload"), + menuItem("Approvals", icon = icon("check"), tabName = "approvals"), + menuItem("Explanations", icon = icon("file"), tabName = "explanations"), + menuItem("Admin", icon = icon("toolbox"), tabName = "admin"), + menuItem("Contact us", icon = icon("at"), href = "https://lanubia.com/contact/") +) + +other.menu <- sidebarMenu( + id = "m", + menuItem( + "Dashboard", + tabName = "dashboard", + icon = icon("dashboard") + ), + menuItem("Explanations", icon = icon("th"), tabName = "explanations"), + menuItem("Contact us", icon = icon("id-card"), href = "https://lanubia.com/contact/") +) \ No newline at end of file diff --git a/helper_ui.R b/helper_ui.R new file mode 100644 index 0000000..3d7f7e2 --- /dev/null +++ b/helper_ui.R @@ -0,0 +1,169 @@ + +dashboard<- tabItem( + tabName = "dashboard", + autoWaiter(), + fluidRow( + column( + width = 4, + "Filter Cost Center and GL Accounts" + ), + column( + width = 4, + pickerInput( + "glaccount", + choices = glaccounts, + selected = glaccounts, + options = list( + `actions-box` = TRUE), + multiple = TRUE + ) + ), + column( + width = 4, + pickerInput( + "costcenter", + choices = cost.centers, # Getting the list from reading data from local. See helper_server + selected = cost.centers, + options = list( + `actions-box` = TRUE), + multiple = TRUE + ) + ) + ), + fluidRow( + valueUI("variation"), + valueUI("variabs"), + valueUI("actexpe"), + ), + fluidRow( + shinydashboardPlus::box( + title = "Deviation % by Month", + collapsible = TRUE, + status = "navy", + solidHeader = TRUE, + echarts4rOutput("monthlypervar",height = "300px") + ), + shinydashboardPlus::box( + title = "Abs. Deviation by Month", + collapsible = TRUE, + status = "navy", + solidHeader = TRUE, + echarts4rOutput("monthlyabsvar",height = "300px") + ) + ), + fluidRow( + shinydashboardPlus::box( + title = "Plan vs Actual", + collapsible = TRUE, + status = "navy", + solidHeader = TRUE, + sidebar = boxSidebar( + startOpen = FALSE, + id = "monthselector", + pickerInput( + "mont", + choices = mont, + selected = max(mont) + ) + ), + echarts4rOutput("monthlyabs",height = "300px") + ) + ) +) + +upload<- tabItem( + tabName = "upload", + autoWaiter(), + fluidRow( + + shinydashboardPlus::box( + title = "Fresh Upload", + collapsible = TRUE, + width = 12, + status = "navy", + solidHeader = TRUE, + fileInput( + "inpfile", + "Upload the file", + multiple = FALSE, + accept = ".csv", + width = NULL, + buttonLabel = "Browse...", + placeholder = "No file selected" + ), + datatableUI("freshdat"), + actionBttn("datsubmit","Submit") + ) + ) +) + +appndev <- tabItem( + tabName = "approvals", + autoWaiter(), + shinydashboardPlus::box( + title = "Deviations without explanation", + collapsible = TRUE, + width = 12, + status = "navy", + solidHeader = TRUE, + sidebar = boxSidebar( + startOpen = FALSE, + id = "limitselector", + sliderInput("limittoignore","Define limit", min=100, max=2000, step=100, value = 500) + ), + datatableUI("devnoexp"), + actionBttn("notify","Notify") + ), + shinydashboardPlus::box( + title = "Awaiting Approval", + collapsible = TRUE, + width = 12, + status = "navy", + solidHeader = TRUE, + excelOutput("expnoapp"), + actionBttn("approvalsubmit","Submit") + ) + +) + +explan <- tabItem( + tabName = "explanations", + autoWaiter(), + shinydashboardPlus::box( + title = "Explanations Pending", + collapsible = TRUE, + width = 12, + status = "navy", + solidHeader = TRUE, + excelOutput("explanationtofill"), + actionBttn("explanationsubmit","Submit") + ) + + +) + +admin<-tabItem( + tabName = "admin", + autoWaiter(), + shinydashboardPlus::box( + title = "Email IDs", + width = 8, + collapsible = TRUE, + status = "navy", + solidHeader = TRUE, + excelOutput("emailtable"), + actionBttn("emailsubmit","Submit") + ), + shinydashboardPlus::box( + title = "Deviation Rules", + width = 4, + collapsible = TRUE, + status = "navy", + solidHeader = TRUE, + excelOutput("limitable"), + actionBttn("limitsubmit","Submit") + ) + +) + + diff --git a/mod_datatable.R b/mod_datatable.R new file mode 100644 index 0000000..11fd227 --- /dev/null +++ b/mod_datatable.R @@ -0,0 +1,29 @@ +datatableUI<- function(id){ + ns<-NS(id) + DT::dataTableOutput(ns("dtable")) +} + +datatableServer<-function(id, dat){ + moduleServer( + id, + function(input,output,session){ + output$dtable<- renderDataTable({ + validate(need(nrow(dat())>0, "No data")) + datatable( + dat(), + extensions = "Buttons", + options = list( + paging = TRUE, + scrollX = TRUE, + searching = TRUE, + ordering = TRUE, + dom = 'Bfrtip', + buttons = c('copy', 'csv', 'excel', 'pdf'), + pageLength = 10, + lengthMenu = c(3, 5, 10) + ) + ) + }) + } + ) +} \ No newline at end of file diff --git a/mod_elinearea.R b/mod_elinearea.R new file mode 100644 index 0000000..c8baa45 --- /dev/null +++ b/mod_elinearea.R @@ -0,0 +1,32 @@ +elineareaUI<- function(id){ + ns<-NS(id) + echarts4rOutput("elaplot") +} + +elineareaServer<-function(id, dat){ + moduleServer( + id, + function(input,output,session){ + output$elaplot<- renderEcharts4r({ + dat |> + e_chart(x=month) |> + e_line(serie = deviation.percent, smooth=T, color="cyan") |> + e_area(serie = deviation.percent, smooth=T, color="cyan") |> + e_axis_labels(x = "month", y="Deviation") |> + e_format_y_axis(suffix = " %") |> + e_title("Deviation", "Selected Cost Centers") |> + e_tooltip(formatter = htmlwidgets::JS(" + function(params){ + return('Month: ' + params.value[0] + '
Deviation: ' + params.value[1] + '%') + } + ") + ) |> + e_legend(right = 100) |> + e_datazoom(x_index = c(0, 1)) |> + e_toolbox_feature(feature = c("saveAsImage","dataView")) |> + e_theme("forest") + + }) + } + ) +} \ No newline at end of file diff --git a/mod_step.R b/mod_step.R new file mode 100644 index 0000000..c30b92e --- /dev/null +++ b/mod_step.R @@ -0,0 +1,27 @@ +stepUI<- function(id){ + ns<-NS(id) + echarts4rOutput("elaplot") +} + +stepServer<-function(id, dat){ + moduleServer( + id, + function(input,output,session){ + output$elaplot<- renderEcharts4r({ + dat |> + e_charts(cost_gl) |> + e_bar(Plan, name = "Plan", color="gray") |> + e_step(Actual, name = "Actual", color="red") |> + e_axis_labels(x = "GL+Cost Center", y="Deviation") |> + e_title("Selected Cost Centers") |> + e_tooltip() |> + e_legend(right = 100) |> + e_datazoom(x_index = 0, type = "slider") |> + e_datazoom(y_index = 0, type = "slider") |> + e_toolbox_feature(feature = c("saveAsImage","dataView")) |> + e_theme("chalk") + + }) + } + ) +} \ No newline at end of file diff --git a/mod_valuebox.R b/mod_valuebox.R new file mode 100644 index 0000000..06a832e --- /dev/null +++ b/mod_valuebox.R @@ -0,0 +1,24 @@ + +valueUI<- function(id){ + ns<-NS(id) + valueBoxOutput(ns("vbox")) +} + +valueServer<-function(id, ttl="title",n,icn="credit-card",clr="red",symbl){ + moduleServer( + id, + function(input,output,session){ + #print(n()) + output$vbox<- renderValueBox({ + valueBox( + paste0(n," ", symbl), + ttl, + icon=icon(icn), + color = clr + ) + }) + + + } + ) +} \ No newline at end of file diff --git a/server.R b/server.R new file mode 100644 index 0000000..d223826 --- /dev/null +++ b/server.R @@ -0,0 +1,568 @@ +# Server logic +# Budget variation tracking of Aramco +# ::::::Asitav Sen:::::: +# ::LaNubia Consulting:: + +library(shiny) + +# Define server logic +shinyServer(function(input, output, session) { + res_auth <- secure_server( + check_credentials = check_credentials("cred.sqlite", + passphrase = "kJuyhG657Hj&^%gshj*762hjsknh&662"), + keep_token=TRUE + ) + + output$menu <- renderMenu({ + if (res_auth$admin == FALSE) { + other.menu + } else + admin.menu + }) + + # Connect to DB + connec <- dbConnect( + RPostgres::Postgres(), + dbname = dsn_database, + host = dsn_hostname, + port = dsn_port, + user = dsn_uid, + password = dsn_pwd + ) + + # On stop disconnect from DB + onStop(function() { + dbDisconnect(connec) + }) + + # Getting the data from DB + dat <- reactive({ + dbGetQuery(connec, + 'SELECT * FROM calculated') + #get.db.data('SELECT * FROM calculated') + }) + + # Getting deviation limits from DB + lims <- reactive({ + dbGetQuery(connec, + 'SELECT * FROM limits') + #get.db.data('SELECT * FROM limits') + }) + + # Getting the explanations from DB + + exp <- reactive({ + dbGetQuery(connec, + 'SELECT * FROM explanations') + #get.db.data('SELECT * FROM explanations') + }) + + # Getting approval Table from DB + approvals <- reactive({ + dbGetQuery(connec, + 'SELECT * FROM approvals') + #get.db.data('SELECT * FROM approvals') + }) + + # Getting email ids + + emailids <- reactive({ + dbGetQuery(connec, + 'SELECT * FROM emails') + #get.db.data('SELECT * FROM emails') + }) + + + dat.need.exp <- reactive({ + req(dat()) + dat() |> + inner_join(lims(), by = c("GL.account")) |> + mutate(act.req = ifelse(devia.per > Limit & + devia > input$limittoignore, T, F)) |> + filter(act.req) |> + left_join( + exp(), + by = c( + "month" = "month", + "Cost.center" = "Cost.center", + "GL.account" = "GL.account" + ) + ) |> + filter(is.na(explanation) | explanation == "") + }) + + # Update the gl account selector + observeEvent(input$costcenter, { + req(dat()) + choi <- dat() |> + filter(Cost.center %in% input$costcenter) |> + select(GL.account) |> distinct() |> pull() + updatePickerInput( + session = session, + inputId = "glaccount", + choices = choi, + selected = choi + ) + + }) + + + observeEvent(c(input$costcenter, input$glaccount), { + # Creating data that will be filtered using the GL and cost center selection + dat.filtered <- dat() |> + filter(Cost.center %in% input$costcenter) |> + filter(GL.account %in% input$glaccount) |> + mon.dev() |> + filter(month == max(month)) + last.var.per <- dat.filtered |> + pull(deviation.percent) #Last variation percent + + last.var.abs <- dat.filtered |> + pull(devia) #Last variation absolute val + + last.exp.act <- dat.filtered |> + pull(Actual) #Last expense + + valueServer( + "variation", + ttl = "Latest Variation %", + n = last.var.per, + icn = "percent", + clr = "red", + symbl = "%" + ) + + valueServer( + "variabs", + ttl = "Last Variation Amount", + n = last.var.abs, + icn = "dollar-sign", + clr = "green", + symbl = "$" + ) + + valueServer( + "actexpe", + ttl = "Last Expense", + n = last.exp.act, + icn = "coins", + clr = "blue", + symbl = "$" + ) + + }) + + # Data selected after aplying the filters of cost center and gl account + dat.selected <- reactive({ + req(dat()) + req(input$costcenter) + req(input$glaccount) + + dat() |> + filter(Cost.center %in% input$costcenter) |> + filter(GL.account %in% input$glaccount) + }) + + # Monthly aggregate + dat.monthly.per <- reactive({ + req(dat.selected()) + dat.selected() |> + mutate(month = ym(month)) |> + group_by(month) |> + summarise(Plan = sum(Plan), Actual = sum(Actual)) |> + mutate(devia = Actual - Plan) |> + mutate(deviation.percent = round(devia * 100 / Plan, 2)) + }) + + + + # Monthly percentage deviation + output$monthlypervar <- renderEcharts4r({ + req(dat.monthly.per()) + + + e_chart(dat.monthly.per(), x = month) |> + e_line(serie = deviation.percent, + smooth = T, + color = "cyan", opacity=0.8) |> + e_area(serie = deviation.percent, + smooth = T, + color = "gray", opacity=0.6) |> + e_axis_labels(x = "month", y = "Deviation") |> + e_format_y_axis(suffix = " %") |> + e_tooltip() |> + e_legend(right = 100) |> + e_datazoom(x_index = c(0, 1)) |> + e_image_g( + right = 50, + top = 20, + z = -999, + style = list( + image = "logo.png", + width = 75, + height = 75, + opacity = .6 + ) + ) |> + e_toolbox_feature(feature = c("saveAsImage", "dataView")) |> + e_theme("roma") + }) + + # Monthly Absolute Deviation + dat.monthly.abs <- reactive({ + dat.selected() |> + filter(month == input$mont) |> + mutate(cost_gl = paste0(Cost.center, "_", GL.account)) |> + group_by(cost_gl) |> + summarise(Plan = sum(Plan), Actual = sum(Actual)) |> + mutate(devia = Actual - Plan) |> + mutate(deviation.percent = round(devia * 100 / Plan, 2)) |> + arrange(desc(deviation.percent)) + }) + + # Monthly absolute by cost center + gl account + output$monthlyabs <- renderEcharts4r({ + req(dat.monthly.abs()) + dat.monthly.abs() |> + e_charts(cost_gl) |> + e_bar(Plan, name = "Plan", color = "gray", opacity=0.6) |> + e_step(Actual, name = "Actual", color = "red") |> + e_axis_labels(x = "GL+Cost Center", y = "Deviation") |> + #e_title("Plan Vs. Actual") |> + e_tooltip() |> + e_legend(right = 100) |> + e_datazoom(x_index = 0, type = "slider") |> + e_datazoom(y_index = 0, type = "slider") |> + e_image_g( + right = 50, + top = 20, + z = -999, + style = list( + image = "logo.png", + width = 75, + height = 75, + opacity = .6 + ) + ) |> + e_toolbox_feature(feature = c("saveAsImage", "dataView")) |> + e_theme("roma") + }) + + output$monthlyabsvar <- renderEcharts4r({ + req(dat.monthly.per()) + + e_chart(dat.monthly.per(), x = month) |> + e_bar(serie = devia) |> + e_axis_labels(x = "month", y = "Deviation") |> + e_format_y_axis(suffix = "€") |> + #e_title("Deviation Percentage by month") |> + e_tooltip() |> + e_legend(right = 100) |> + e_datazoom(x_index = c(0, 1)) |> + e_image_g( + right = 50, + top = 20, + z = -999, + style = list( + image = "logo.png", + width = 75, + height = 75, + opacity = .6 + ) + ) |> + e_visual_map(type = "piecewise", + pieces = list(list(gt = 0, + color = "red", opacity=0.5), + list(lte = 0, + color = "green", opacity=0.5))) |> + e_toolbox_feature(feature = c("saveAsImage", "dataView")) |> + e_theme("roma") + + }) + + # Table of deviations without explanation + + + datatableServer("devnoexp", dat.need.exp) + + # Notify + + observeEvent(input$notify, { + req(dat.need.exp()) + emaildf<-dat.need.exp() |> + inner_join(emailids(), by=c("Cost.center"="Cost.center","GL.account"="GL.account")) + print(emaildf) + if(is.null(emaildf) | nrow(emaildf)==0){ + showModal(modalDialog("No Data")) + } else { + emails<- + emaildf |> + select(email) |> pull() |> unique() + + for(i in 1:length(emails)){ + tbl <- emaildf |> + filter(email==emails[i]) |> kbl() + date_time <- add_readable_time() + email <- + compose_email(body = md(c( + glue::glue( + "Hello, + + Explanation required for expense deviation. Please visit xyz. Details are as follows. + + " + ), + tbl + )), + footer = md(glue::glue("Email sent on {date_time}."))) + + email |> + smtp_send( + to = emails[i], + from = "asitav.sen@lanubia.com", + subject = "Testing", + credentials = creds_file("email_creds") + ) + } + + + + showModal(modalDialog(title = "Done")) + } + + }) + + # Approvals + output$expnoapp <- renderExcel({ + req(approvals()) + appro <- approvals() + row.names(appro) <- NULL + appro <- appro[appro$approved == FALSE, ] + columns = data.frame( + title = colnames(appro), + type = c('numeric', 'text', 'text', 'text', 'checkbox') + ) + excelTable(data = appro, columns = columns, autoFill = TRUE) + + }) + + # Update approvals + observeEvent(input$approvalsubmit, { + approved.now <- excel_to_R(input$expnoapp) + if (is.null(input$expnoapp) || nrow(approved.now) == 0) { + showModal(modalDialog(title = "Nothing to Upload")) + } else{ + showModal(modalDialog(title = "Upload in Database?", + actionButton("finalapprovalsubmit", "Yes"))) + } + + }) + + + observeEvent(input$finalapprovalsubmit, { + appro <- approvals() + appro <- appro[appro$approved == TRUE, ] + approved.now <- excel_to_R(input$expnoapp) + appro <- rbind(appro, approved.now) + if (nrow(approved.now) > 0) { + if (dbWriteTable(connec, "approvals", appro, overwrite = TRUE)) { + removeModal() + session$reload() + } + } + + + }) + + # New Data + fresh.dat <- reactive({ + req(input$inpfile) + file <- input$inpfile + ext <- tools::file_ext(file$datapath) + req(file) + validate(need(ext == "csv", "Please upload a csv file")) + new.dat <- read.csv(file$datapath, header = T) + new.dat |> + mutate(devia = Actual - Plan) |> + mutate(devia.per = round(devia / Plan, 2)) + }, label = "fresh") + + # Show uploaded data + datatableServer("freshdat", fresh.dat) + + # Update DB + observeEvent(input$datsubmit, { + req(nrow(fresh.dat()) > 0) + fresh.dt <- fresh.dat() + if (is.null(fresh.dt) || nrow(fresh.dt) == 0) { + showModal(modalDialog(title = "Nothing to Upload")) + } else{ + showModal(modalDialog(title = "Upload in Database?", + actionButton("finaldatsubmit", "Yes"))) + } + + }) + + + observeEvent(input$finaldatsubmit, { + fresh.dt <- fresh.dat() + if (nrow(fresh.dt) > 0) { + if (dbWriteTable(connec, "calculated", fresh.dt, append = TRUE)) { + unique(fresh.dt$month) |> saveRDS("./data/months.RDS") + removeModal() + session$reload() + } + } + }) + + + + # Email id edit and show + output$emailtable <- renderExcel({ + req(emailids()) + email.table <- emailids() + columns = data.frame(title = colnames(email.table), + type = c('text', 'text', 'text')) + excelTable(data = email.table, columns = columns, autoFill = TRUE) + + }) + + # Email id table update + + observeEvent(input$emailsubmit, { + email.table <- excel_to_R(input$emailtable) + if (is.null(input$emailtable) || nrow(email.table) == 0) { + showModal(modalDialog(title = "Nothing to Upload")) + } else{ + showModal(modalDialog(title = "Upload in Database?", + actionButton("finalemailsubmit", "Yes"))) + } + + }) + + + observeEvent(input$finalemailsubmit, { + email.table <- excel_to_R(input$emailtable) + if (nrow(email.table) > 0) { + if (dbWriteTable(connec, "emails", email.table, overwrite = TRUE)) { + removeModal() + session$reload() + } + } + }) + + # Limits (deviation rules) id edit and show + output$limitable <- renderExcel({ + req(lims()) + limit.table <- lims() + columns = data.frame(title = colnames(limit.table), + type = c('text', 'numeric')) + excelTable(data = limit.table, columns = columns, autoFill = TRUE) + + }) + + # Email id table update + + observeEvent(input$limitsubmit, { + limit.table <- excel_to_R(input$limitable) + if (is.null(input$limitable) || nrow(limit.table) == 0) { + showModal(modalDialog(title = "Nothing to Upload")) + } else{ + showModal(modalDialog(title = "Upload in Database?", + actionButton("finallimitsubmit", "Yes"))) + } + + }) + + + observeEvent(input$finallimitsubmit, { + limit.table <- excel_to_R(input$limitable) + if (nrow(limit.table) > 0) { + if (dbWriteTable(connec, "limits", limit.table, overwrite = TRUE)) { + removeModal() + session$reload() + } + } + }) + + # User specific explanation filter + user.accounts <- reactive({ + req(dat()) + req(emailids()) + req(exp()) + req(res_auth$email) + dat() |> + inner_join(lims(), by = c("GL.account")) |> + mutate(act.req = ifelse(devia.per > Limit & + devia > input$limittoignore, T, F)) |> #change 500 to input + filter(act.req) |> + left_join(emailids(), + by = c("Cost.center" = "Cost.center", "GL.account" = "GL.account")) |> + left_join( + exp(), + by = c( + "month" = "month", + "Cost.center" = "Cost.center", + "GL.account" = "GL.account" + ) + ) |> + filter(is.na(explanation) | explanation == "") |> + filter(email == res_auth$email) |> + select(-c(9, 10)) + }) + + output$explanationtofill <- renderExcel({ + req(user.accounts()) + explanation.table <- user.accounts() + columns = data.frame( + title = colnames(explanation.table), + type = c( + 'numeric', + 'text', + 'text', + 'numeric', + 'numeric', + 'numeric', + 'numeric', + 'numeric', + 'text' + ) + ) + excelTable(data = explanation.table, columns = columns, autoFill = TRUE) + + }) + + + # Explanations table update + + observeEvent(input$explanationsubmit, { + explanation.table <- excel_to_R(input$explanationtofill) + if (is.null(input$explanationtofill) || + nrow(explanation.table) == 0) { + showModal(modalDialog(title = "Nothing to Upload")) + } else{ + showModal(modalDialog(title = "Upload in Database?", + actionButton("finalexpsubmit", "Yes"))) + } + + }) + + + observeEvent(input$finalexpsubmit, { + explanation.table <- excel_to_R(input$explanationtofill) + explanation.table <- explanation.table |> + select(c(month, Cost.center, GL.account, explanation)) + if (nrow(explanation.table) > 0) { + if (dbWriteTable(connec, "explanations", explanation.table, append = TRUE)) { + appr <- explanation.table |> mutate(approved = F) + if (dbWriteTable(connec, "approvals", appr, append = TRUE)) { + removeModal() + session$reload() + } + + } + } + }) + + +}) diff --git a/ui.R b/ui.R new file mode 100644 index 0000000..df15649 --- /dev/null +++ b/ui.R @@ -0,0 +1,125 @@ +# +# Custom App developed for Finance Team of Aramco +# ::Asitav Sen:: +# ::LaNubia Consulting:: +# ::asitav.sen@lanubia.com:: +# + +library(shiny) +library(shinydashboard) +library(countup) +library(shinyWidgets) +library(shinydashboardPlus) +library(DBI) +library(RPostgres) +library(dplyr) +library(echarts4r) +library(lubridate) +library(DT) +library(excelR) +library(blastula) +library(shinymanager) +library(glue) +library(shinythemes) +library(kableExtra) +library(waiter) + + + +source("helper_server.R") +source("mod_valuebox.R") +source("mod_elinearea.R") +source("mod_step.R") +source("mod_datatable.R") +source("helper_ui.R") + +# Define UI for application + + shinyUI( + secure_app( + shinydashboardPlus::dashboardPage( + title = "Budgetrack", + #skin = "blue-light", + #skin = "midnight", + header = shinydashboardPlus::dashboardHeader(title = "Budgetrack"), + sidebar = shinydashboardPlus::dashboardSidebar( + sidebarMenuOutput("menu") + ), + body = dashboardBody( + + tags$head(tags$style(HTML( + + ' + /* logo */ + .skin-blue .main-header .logo { + background-color: #0477ci; + } + + /* logo when hovered */ + .skin-blue .main-header .logo:hover { + background-color: #009adc; + } + + /* navbar (rest of the header) */ + .skin-blue .main-header .navbar { + background-color: #0033a0; + } + + /* main sidebar */ + .skin-blue .main-sidebar { + background-color: #ffffff; + } + + /* active selected tab in the sidebarmenu */ + .skin-blue .main-sidebar .sidebar .sidebar-menu .active a{ + background-color: #01a54b; + } + + /* other links in the sidebarmenu */ + .skin-blue .main-sidebar .sidebar .sidebar-menu a{ + background-color: #ffffff; + color: #000000; + } + + /* other links in the sidebarmenu when hovered */ + .skin-blue .main-sidebar .sidebar .sidebar-menu a:hover{ + background-color: #009adc; + } + ' + + ))), + tabItems(dashboard, + upload, + appndev, + explan, + admin)) + ), + enable_admin = TRUE, + theme = shinythemes::shinytheme("united"), + tags_top = + tags$div( + tags$h4("Created exclusively for ", style ="align:center"), + br(), + tags$img( + src = "https://www.aramco.com/images/affiliateLogo-2x.png", width = 100 + ), + br(), + br(), + tags$h4("By", style ="align:center"), + tags$img(src="logo.png", width=100) + ), + tags_bottom = tags$p( + "For any question, please contact", + tags$a( + href ="mailto:asitav.sen@lanubia.com?Subject=Aramco%20aBugdet", + target="_top","Asitav Sen" + ) + ), + background = "linear-gradient(225deg,rgb(0,163,224), + rgb(0,51,160), + rgb(0,132,61), + rgb(132,189,0));" + ) + +) + diff --git a/www/loader.gif b/www/loader.gif new file mode 100644 index 0000000..738cd76 Binary files /dev/null and b/www/loader.gif differ diff --git a/www/logo.png b/www/logo.png new file mode 100644 index 0000000..632c677 Binary files /dev/null and b/www/logo.png differ