diff --git a/apps/dash-salesforce-crm/.gitignore b/apps/dash-salesforce-crm/.gitignore index 41e8e8e6a..1f65cd953 100644 --- a/apps/dash-salesforce-crm/.gitignore +++ b/apps/dash-salesforce-crm/.gitignore @@ -1,7 +1,192 @@ -venv/ -.env +# .gitignore specifies the files that shouldn't be included +# in version control and therefore shouldn't be included when +# deploying an application to Dash Enterprise +# This is a very exhaustive list! +# This list was based off of https://github.com/github/gitignore + +# Ignore data that is generated during the runtime of an application +# This folder is used by the "Large Data" sample applications +runtime_data/ +data/ + +# Omit SQLite databases that may be produced by dash-snapshots in development +*.db + +# Byte-compiled / optimized / DLL files __pycache__/ -apps/__pycache__/ -*.DS_Store -.vscode -secrets.sh \ No newline at end of file +*.py[cod] +*$py.class + + +# Jupyter Notebook + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# IPython +profile_default/ +ipython_config.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +secrets.sh + +# Spyder project settings +.spyderproject +.spyproject + +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + + +# macOS General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# History files +.Rhistory +.Rapp.history + +# Session Data files +.RData + +# User-specific files +.Ruserdata + +# Example code in package build process +*-Ex.R + +# Output files from R CMD check +/*.Rcheck/ + +# RStudio files +.Rproj.user/ + +# produced vignettes +vignettes/*.html +vignettes/*.pdf + +# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 +.httr-oauth + +# knitr and R markdown default cache directories +*_cache/ +/cache/ + +# Temporary files created by R markdown +*.utf8.md +*.knit.md + +# R Environment Variables +.Renviron + +# Linux +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# SublineText +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings \ No newline at end of file diff --git a/apps/dash-salesforce-crm/Procfile b/apps/dash-salesforce-crm/Procfile index 626d012fa..38371ebbf 100644 --- a/apps/dash-salesforce-crm/Procfile +++ b/apps/dash-salesforce-crm/Procfile @@ -1 +1 @@ -web: gunicorn index:server +web: gunicorn app:server diff --git a/apps/dash-salesforce-crm/README.md b/apps/dash-salesforce-crm/README.md index cc4396e90..4ed09c357 100644 --- a/apps/dash-salesforce-crm/README.md +++ b/apps/dash-salesforce-crm/README.md @@ -55,13 +55,13 @@ This app uses Salesforce API in order to implement a custom CRM dashboard. The A The following are screenshots for the app in this repo: -![Screenshot1](screenshots/opportunities_screenshot.png) +![Screenshot1](assets/github/opportunities_screenshot.png) -![Screenshot1](screenshots/leads_screenshot.png) +![Screenshot1](assets/github/leads_screenshot.png) -![Screenshot1](screenshots/cases_screenshot.png) +![Screenshot1](assets/github/cases_screenshot.png) -![Animated](screenshots/dash-salesforce-demo.gif) +![Animated](assets/github/dash-salesforce-demo.gif) diff --git a/apps/dash-salesforce-crm/app.py b/apps/dash-salesforce-crm/app.py index 1e98313a2..6d9806732 100644 --- a/apps/dash-salesforce-crm/app.py +++ b/apps/dash-salesforce-crm/app.py @@ -1,50 +1,31 @@ -import math -import dash -import dash_html_components as html +from dash import Dash, html, dcc, Input, Output +import dash_bootstrap_components as dbc +from pages import opportunities, cases, leads -from sfManager import sf_Manager +from constants import salesforce_manager +from utils.components import Header -app = dash.Dash( - __name__, meta_tags=[{"name": "viewport", "content": "width=device-width"}] +app = Dash( + __name__, + external_stylesheets=[dbc.themes.QUARTZ], + title="CRM Salesforce" +) +server = app.server + +app.layout = dbc.Container([ + Header(app), + dbc.Tabs([ + dbc.Tab(opportunities.layout, label="Opportunities"), + dbc.Tab(leads.layout, label="Leads"), + dbc.Tab(cases.layout, label="Cases"), + ]), + + dcc.Store(id="opportunities_df", data=salesforce_manager.get_opportunities().to_json(orient="split")), + dcc.Store(id="leads_df", data=salesforce_manager.get_leads().to_json(orient="split")), + dcc.Store(id="cases_df", data=salesforce_manager.get_cases().to_json(orient="split")), + ], + fluid=True, ) -app.config.suppress_callback_exceptions = True - -sf_manager = sf_Manager() - -millnames = ["", " K", " M", " B", " T"] # used to convert numbers - - -# return html Table with dataframe values -def df_to_table(df): - return html.Table( - [html.Tr([html.Th(col) for col in df.columns])] - + [ - html.Tr([html.Td(df.iloc[i][col]) for col in df.columns]) - for i in range(len(df)) - ] - ) - - -# returns most significant part of a number -def millify(n): - n = float(n) - millidx = max( - 0, - min( - len(millnames) - 1, int(math.floor(0 if n == 0 else math.log10(abs(n)) / 3)) - ), - ) - - return "{:.0f}{}".format(n / 10 ** (3 * millidx), millnames[millidx]) - - -# returns top indicator div -def indicator(color, text, id_value): - return html.Div( - [ - html.P(id=id_value, className="indicator_value"), - html.P(text, className="twelve columns indicator_text"), - ], - className="four columns indicator pretty_container", - ) +if __name__ == "__main__": + app.run_server(debug=True) diff --git a/apps/dash-salesforce-crm/assets/css/app.css b/apps/dash-salesforce-crm/assets/css/app.css new file mode 100644 index 000000000..d2e87d9eb --- /dev/null +++ b/apps/dash-salesforce-crm/assets/css/app.css @@ -0,0 +1,83 @@ +.header { + padding: 5px 15px 5px 10px; +} + +.header-logos { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + gap: 5%; +} + +.row { + margin-top: 20px; +} + +.card { + margin-bottom: 20px; +} + +.tab-content { + padding: 10px 10rem 0 10rem; +} + +/* graph background color */ +.main-svg { + background-color: transparent !important; +} +.layer.bg * { + fill: none !important; +} +/* Graph legend */ +.infolayer .legend .bg { + fill: none !important; +} + +/* table */ +td { + background-color: transparent !important; +} +th span { + font-weight: bold; + text-align: center; +} +th { + background-color: rgba(202, 65, 123, 0.7) !important; +} + + +/* Demo button css */ +.demo-button { + font-family: Open Sans,sans-serif; + text-decoration: none; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-radius: 8px; + font-weight: 700; + height: 2.5rem; + font-size: 13px; + -webkit-padding-start: 1rem; + padding-inline-start: 1rem; + -webkit-padding-end: 1rem; + padding-inline-end: 1rem; + color: #ffffff; + letter-spacing: 2px; + border: solid 1.5px transparent; + box-shadow: 2px 1000px 1px #0c0c0c inset; + background-image: linear-gradient(135deg, #7A76FF, #7A76FF, #7FE4FF); + -webkit-background-size: 200% 100%; + background-size: 200% 100%; + -webkit-background-position: 99%; + background-position: 99%; + background-origin: border-box; + transition: all .4s ease-in-out; + padding-top: 15px; + padding-bottom: 15px; +} +.demo-button:hover { + color: #7A76FF; +} \ No newline at end of file diff --git a/apps/dash-salesforce-crm/assets/css/extra_dropdown_style.css b/apps/dash-salesforce-crm/assets/css/extra_dropdown_style.css new file mode 100644 index 000000000..c22edb190 --- /dev/null +++ b/apps/dash-salesforce-crm/assets/css/extra_dropdown_style.css @@ -0,0 +1,27 @@ +.Select-control { + border: none !important; + box-shadow: inset 1px 1px rgb(255 255 255 / 20%), inset -1px -1px rgb(255 255 255 / 10%), 1px 3px 24px -1px rgb(0 0 0 / 15%) !important; + background-color: transparent !important; + background-image: linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%) !important; + -webkit-backdrop-filter: blur(5px) !important; + backdrop-filter: blur(5px) !important; + --bs-secondary-rgb: transparent !important; +} +.Select.has-value.Select--single > .Select-control .Select-value .Select-value-label { color: #FFFF !important; } + +.Select-menu-outer { + border: none !important; + box-shadow: inset 1px 1px rgb(255 255 255 / 20%), inset -1px -1px rgb(255 255 255 / 10%), 1px 3px 24px -1px rgb(0 0 0 / 15%) !important; + background-color: transparent !important; + background-image: linear-gradient(125deg,rgba(255,255,255,.5),rgba(255,255,255,.5) 70%) !important; + -webkit-backdrop-filter: blur(5px) !important; + backdrop-filter: blur(5px) !important; + --bs-secondary-rgb: transparent !important; +} +.Select-control .Select-input:focus { + background: transparent !important; +} +.modal-body .Select-menu-outer { + background-color: #CA417B !important; +} +.CalendarDay__selected, .CalendarDay__default:hover { color:#CA417B !important; } \ No newline at end of file diff --git a/apps/dash-salesforce-crm/screenshots/cases_screenshot.png b/apps/dash-salesforce-crm/assets/github/cases_screenshot.png similarity index 100% rename from apps/dash-salesforce-crm/screenshots/cases_screenshot.png rename to apps/dash-salesforce-crm/assets/github/cases_screenshot.png diff --git a/apps/dash-salesforce-crm/screenshots/dash-salesforce-demo.gif b/apps/dash-salesforce-crm/assets/github/dash-salesforce-demo.gif similarity index 100% rename from apps/dash-salesforce-crm/screenshots/dash-salesforce-demo.gif rename to apps/dash-salesforce-crm/assets/github/dash-salesforce-demo.gif diff --git a/apps/dash-salesforce-crm/screenshots/leads_screenshot.png b/apps/dash-salesforce-crm/assets/github/leads_screenshot.png similarity index 100% rename from apps/dash-salesforce-crm/screenshots/leads_screenshot.png rename to apps/dash-salesforce-crm/assets/github/leads_screenshot.png diff --git a/apps/dash-salesforce-crm/screenshots/opportunities_screenshot.png b/apps/dash-salesforce-crm/assets/github/opportunities_screenshot.png similarity index 100% rename from apps/dash-salesforce-crm/screenshots/opportunities_screenshot.png rename to apps/dash-salesforce-crm/assets/github/opportunities_screenshot.png diff --git a/apps/dash-salesforce-crm/assets/images/plotly-logo.png b/apps/dash-salesforce-crm/assets/images/plotly-logo.png new file mode 100644 index 000000000..984dd57ab Binary files /dev/null and b/apps/dash-salesforce-crm/assets/images/plotly-logo.png differ diff --git a/apps/dash-salesforce-crm/assets/logo.png b/apps/dash-salesforce-crm/assets/logo.png deleted file mode 100644 index 3d446bf5e..000000000 Binary files a/apps/dash-salesforce-crm/assets/logo.png and /dev/null differ diff --git a/apps/dash-salesforce-crm/assets/s1.css b/apps/dash-salesforce-crm/assets/s1.css deleted file mode 100644 index 927297c0c..000000000 --- a/apps/dash-salesforce-crm/assets/s1.css +++ /dev/null @@ -1,573 +0,0 @@ -body { - font-size: 0.75rem; - margin: 0; - padding: 0; - background-color: #f9f9f9; - font-family: "Asap", sans-serif; - -webkit-user-select: none; - /* Chrome all / Safari all */ - -moz-user-select: none; - /* Firefox all */ - -ms-user-select: none; - /* IE 10+ */ - user-select: none; - /* Likely future */ -} - -.pretty_container { - border-radius: 5px; - background-color: white; - margin: 0.5rem; - padding: 1rem; - position: relative; - border: 1px solid #f1f1f1; -} - -input { - padding: 0.5rem 0; - border-radius: 5px; -} - -.button:hover { - box-shadow: 4px 4px 4px grey; - transition: box-shadow 0.5s; -} - -.row { - align-items: center; -} - -.modal { - display: block; - /*Hidden by default */ - position: fixed; - /* Stay in place */ - z-index: 1000; - /* Sit on top */ - left: 0; - top: 0; - width: 100%; - /* Full width */ - height: 100%; - /* Full height */ - overflow: auto; - /* Enable scroll if needed */ - background-color: rgb(0, 0, 0); - /* Fallback color */ - background-color: rgba(0, 0, 0, 0.4); - /* Black w/ opacity */ -} - -.modal-content { - background-color: white; - margin: 5% auto; - /* 15% from the top and centered */ - padding: 2rem; - width: 50%; - /* Could be more or less, depending on screen size */ - color: #506784; - border-radius: 10px; -} - -.button { - background-color: #3a7cef; - color: white; - display: block; - border-radius: 5px; - text-transform: uppercase; - border: none; - letter-spacing: 0.1rem; - box-shadow: 4px 4px 4px lightgrey; - transition: box-shadow 0.5s; - width: 50%; - padding: 0.75rem; - text-align: center; - margin-left: 25%; -} -.subtitle { - text-align: center; - color: #333333; - font-size: 20px; -} - -tr:nth-child(even) { - background-color: #f0f0f0; -} -tr:nth-child(odd) { - background-color: #fafafa; -} - -td, -th { - border: 0px solid #ddd; - padding: 8px; - /* float: left; */ -} - -.header { - margin: 0px; - background-color: white; - color: #333333; - padding-right: 3%; - display: flex; - flex-direction: row-reverse; - justify-content: space-between; -} - -.header img { - margin-left: auto; - height: 75px; - max-width: 33%; -} - -.app-title { - color: #333333; - padding-left: 5%; - font-size: 2.25rem; - letter-spacing: -0.1rem; - vertical-align: middle; - display: flex; - flex: 1; - flex-direction: row; -} - -#subtitle { - display: none; -} - -.indicator_value { - color: #333333; -} -.header button { - background-color: white; - box-shadow: none; - border-radius: 5px; - font-size: 1rem; - padding: 12px; - cursor: pointer; -} - -#learn_more { - display: none; -} - -#menu { - display: block; - border: none; - font-size: 2rem; -} - -#menu p { - margin-block-start: 0; - margin-block-end: 0; -} - -.tabs { - border-top: 1px solid lightgrey; - display: none; - flex-direction: column; - padding: 1rem 5%; - margin-bottom: 1rem; - background-color: white; -} -.dropdown-styles { - border-radius: 5px; - background-color: white; - margin: 0.5rem; - padding-top: 7px; - position: relative; - border: 1px solid #f1f1f1; -} -.dd-styles { - border-radius: 5px; - background-color: white; - margin: 0.5rem; - padding-top: 7px; - position: relative; - border: 1px solid #f1f1f1; -} -.tabs > a { - text-decoration: none; - color: #333333; - font-size: 1.25rem; - text-align: center; -} - -.control, -.Select-control { - border: none !important; -} - -table { - border: 1px; - font-size: 1rem; - width: 100%; - font-family: Ubuntu; -} - -.table { - overflow: scroll; -} - -.indicators { - display: flex; - align-items: stretch; -} - -.indicator { - flex: 1; - padding: 0.75rem 1.75rem; - text-align: center; -} - -.indicator_value { - font-size: 2rem; - margin: 1rem 0; -} - -.indicator_value p { - margin: 1rem 0; -} - -::-webkit-scrollbar { - width: 0px; /* Remove scrollbar space */ - background: transparent; /* Optional: just make scrollbar invisible */ -} - -.has-value.Select--single > .Select-control .Select-value .Select-value-label, -.has-value.is-pseudo-focused.Select--single - > .Select-control - .Select-value - .Select-value-label { - color: #848484; -} - -#cases_reasons { - height: 100%; -} - -/* Layouts for different screen sizes */ -@media (max-width: 350px) { - .app-title { - margin-left: 0 !important; - } -} - -@media (max-width: 450px) { - .dropdown-styles { - width: 28% !important; - display: inline-block !important; - } - .app-title { - font-size: 1.5rem !important; - padding-left: 4% !important; - margin-left: 5%; - } - .header img { - height: 35px !important; - padding-left: 3% !important; - } - .indicator_value { - font-size: 1.25rem !important; - } - .indicator { - padding: 0.5rem; - } - .button { - width: 25% !important; - } -} - -@media (max-width: 900px) { - .indicator { - align-content: center; - justify-items: center; - text-align: center; - } - - .indicator:first-child { - margin-right: 0; - } - - .indicator:last-child { - margin-left: 0; - } - - #opportunity_grid, - #lead_grid, - #cases_grid { - margin: 10px; - margin-bottom: 10%; - } - .app-title { - font-size: 2rem; - padding-left: 20%; - } - .header img { - height: 50px; - padding-left: 5%; - } - .button { - margin: 2% auto; - width: 20%; - } - .dropdown-styles { - width: 30%; - display: inline-block; - } - .dd-styles { - width: 44%; - display: inline-block; - } - .indicator_value { - font-size: 1.5rem; - } -} - -@media (min-width: 900px) { - .header { - flex-direction: row; - } - - #subtitle { - display: block; - } - #learn_more { - display: block; - } - - #menu { - display: none; - } - - .tabs { - display: flex; - flex-direction: row; - } - - .tabs > a { - width: 20%; - } - - .tabs > a:first-child { - border-right: 1px solid lightgrey; - } - - .tabs > a:last-child { - border-left: 1px solid lightgrey; - } - - #lead_grid { - display: -ms-grid; - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; - -ms-grid-columns: 1fr 1fr 1fr 1fr 1fr 1fr; - grid-template-rows: auto; - -ms-grid-rows: auto; - width: 80%; - margin-bottom: 10%; - margin-left: auto; - margin-right: auto; - } - - #new_lead { - -ms-grid-column: 6; - -ms-grid-column-span: 1; - grid-column: 6 / 7; - -ms-grid-row: 1; - -ms-grid-row-span: 1; - grid-row: 1 / 2; - align-self: end; - } - - #leads_per_state { - -ms-grid-column: 1; - -ms-grid-column-span: 2; - grid-column: 1 / 3; - -ms-grid-row: 2; - -ms-grid-row-span: 3; - grid-row: 2 / 5; - } - - #lead_grid > .indicators { - -ms-grid-row: 2; - -ms-grid-row-span: 1; - grid-row: 2 / 3; - -ms-grid-column: 3; - -ms-grid-column-span: 4; - grid-column: 3 / 7; - } - - #submit_new_lead { - padding: 10px; - } - - #leads_source_container { - -ms-grid-row: 3; - -ms-grid-row-span: 2; - grid-row: 3 / 5; - -ms-grid-column: 3; - -ms-grid-column-span: 2; - grid-column: 3 / 5; - } - - #converted_leads_container { - -ms-grid-row: 3; - -ms-grid-row-span: 2; - grid-row: 3 / 5; - -ms-grid-column: 5; - -ms-grid-column-span: 2; - grid-column: 5 / 7; - } - - #leads_table { - -ms-grid-row: 6; - -ms-grid-row-span: 4; - grid-row: 6 / 10; - -ms-grid-column: 1; - -ms-grid-column-span: 6; - grid-column: 1 / 7; - max-height: 500px; - } - - #opportunity_grid { - display: -ms-grid; - display: grid; - width: 80%; - margin-bottom: 10%; - margin-left: auto; - margin-right: auto; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; - -ms-grid-columns: 1fr 1fr 1fr 1fr 1fr 1fr; - grid-template-rows: auto; - -ms-grid-rows: auto; - } - - #new_opportunity { - -ms-grid-column: 6; - -ms-grid-column-span: 1; - grid-column: 6 / 7; - -ms-grid-row: 1; - -ms-grid-row-span: 1; - grid-row: 1 / 1; - align-self: end; - } - - #converted_count_container { - -ms-grid-column: 1; - -ms-grid-column-span: 2; - grid-column: 1 / 3; - -ms-grid-row: 2; - -ms-grid-row-span: 4; - grid-row: 2 / 6; - } - - #opportunity_indicators { - -ms-grid-column: 3; - -ms-grid-column-span: 4; - grid-column: 3 / 7; - -ms-grid-row: 2; - -ms-grid-row-span: 1; - grid-row: 2 / 3; - } - - #opportunity_heatmap { - -ms-grid-column: 3; - -ms-grid-column-span: 4; - grid-column: 3 / 7; - -ms-grid-row: 3; - -ms-grid-row-span: 3; - grid-row: 3 / 6; - } - - #top_open_container { - -ms-grid-column: 1; - -ms-grid-column-span: 3; - grid-column: 1 / 4; - -ms-grid-row: 6; - -ms-grid-row-span: 4; - grid-row: 6 / 7; - } - - #top_lost_container { - -ms-grid-column: 4; - -ms-grid-column-span: 3; - grid-column: 4 / 7; - -ms-grid-row: 6; - -ms-grid-row-span: 4; - grid-row: 6 / 7; - } - - #cases_grid { - display: -ms-grid; - display: grid; - width: 80%; - margin-bottom: 10%; - margin-left: auto; - margin-right: auto; - -ms-grid-columns: 1fr 1fr 1fr 1fr 1fr 1fr; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; - -ms-grid-rows: auto; - grid-template-rows: auto; - } - - #new_case { - -ms-grid-column: 6; - -ms-grid-column-span: 1; - grid-column: 6 / 7; - -ms-grid-row: 1; - -ms-grid-row-span: 1; - grid-row: 1 / 1; - align-self: end; - } - - #cases_types_container { - -ms-grid-column: 1; - -ms-grid-column-span: 2; - grid-column: 1 / 3; - -ms-grid-row: 2; - -ms-grid-row-span: 3; - grid-row: 2 / 5; - } - - #cases_indicators { - -ms-grid-column: 3; - -ms-grid-column-span: 4; - grid-column: 3 / 7; - -ms-grid-row: 2; - -ms-grid-row-span: 1; - grid-row: 2 / 3; - } - - #cases_reasons_container { - -ms-grid-column: 3; - -ms-grid-column-span: 2; - grid-column: 3 / 5; - -ms-grid-row: 3; - -ms-grid-row-span: 2; - grid-row: 3 / 5; - } - - #cases_by_period_container { - -ms-grid-column: 5; - -ms-grid-column-span: 2; - grid-column: 5 / 7; - -ms-grid-row: 3; - -ms-grid-row-span: 2; - grid-row: 3 / 5; - } - - #cases_by_account_container { - -ms-grid-column: 1; - -ms-grid-column-span: 6; - grid-column: 1 / 7; - -ms-grid-row: 5; - -ms-grid-row-span: 1; - grid-row: 5 / 6; - } - - #cases_by_account { - display: block; - height: 100%; - } -} diff --git a/apps/dash-salesforce-crm/constants.py b/apps/dash-salesforce-crm/constants.py new file mode 100644 index 000000000..6500af68b --- /dev/null +++ b/apps/dash-salesforce-crm/constants.py @@ -0,0 +1,61 @@ + +from utils.salesforce_manager import SalesforceManager + +salesforce_manager = SalesforceManager() + +## used to convert numbers +millnames = ["", " K", " M", " B", " T"] + +states = [ + "AL", + "AK", + "AZ", + "AR", + "CA", + "CO", + "CT", + "DC", + "DE", + "FL", + "GA", + "HI", + "ID", + "IL", + "IN", + "IA", + "KS", + "KY", + "LA", + "ME", + "MD", + "MA", + "MI", + "MN", + "MS", + "MO", + "MT", + "NE", + "NV", + "NH", + "NJ", + "NM", + "NY", + "NC", + "ND", + "OH", + "OK", + "OR", + "PA", + "RI", + "SC", + "SD", + "TN", + "TX", + "UT", + "VT", + "VA", + "WA", + "WV", + "WI", + "WY", +] \ No newline at end of file diff --git a/apps/dash-salesforce-crm/index.py b/apps/dash-salesforce-crm/index.py deleted file mode 100644 index 4cd6257a3..000000000 --- a/apps/dash-salesforce-crm/index.py +++ /dev/null @@ -1,134 +0,0 @@ -import dash -import dash_core_components as dcc -import dash_html_components as html -from dash.dependencies import Input, Output, State -from app import sf_manager, app -from panels import opportunities, cases, leads - - -server = app.server - -app.layout = html.Div( - [ - html.Div( - className="row header", - children=[ - html.Button(id="menu", children=dcc.Markdown("≡")), - html.Span( - className="app-title", - children=[ - dcc.Markdown("**CRM App**"), - html.Span( - id="subtitle", - children=dcc.Markdown("  using Salesforce API"), - style={"font-size": "1.8rem", "margin-top": "15px"}, - ), - ], - ), - html.Img(src=app.get_asset_url("logo.png")), - html.A( - id="learn_more", - children=html.Button("Learn More"), - href="https://plot.ly/dash/", - ), - ], - ), - html.Div( - id="tabs", - className="row tabs", - children=[ - dcc.Link("Opportunities", href="/"), - dcc.Link("Leads", href="/"), - dcc.Link("Cases", href="/"), - ], - ), - html.Div( - id="mobile_tabs", - className="row tabs", - style={"display": "none"}, - children=[ - dcc.Link("Opportunities", href="/"), - dcc.Link("Leads", href="/"), - dcc.Link("Cases", href="/"), - ], - ), - dcc.Store( # opportunities df - id="opportunities_df", - data=sf_manager.get_opportunities().to_json(orient="split"), - ), - dcc.Store( # leads df - id="leads_df", data=sf_manager.get_leads().to_json(orient="split") - ), - dcc.Store( - id="cases_df", data=sf_manager.get_cases().to_json(orient="split") - ), # cases df - dcc.Location(id="url", refresh=False), - html.Div(id="tab_content"), - html.Link( - href="https://use.fontawesome.com/releases/v5.2.0/css/all.css", - rel="stylesheet", - ), - html.Link( - href="https://fonts.googleapis.com/css?family=Dosis", rel="stylesheet" - ), - html.Link( - href="https://fonts.googleapis.com/css?family=Open+Sans", rel="stylesheet" - ), - html.Link( - href="https://fonts.googleapis.com/css?family=Ubuntu", rel="stylesheet" - ), - ], - className="row", - style={"margin": "0%"}, -) - -# Update the index - - -@app.callback( - [ - Output("tab_content", "children"), - Output("tabs", "children"), - Output("mobile_tabs", "children"), - ], - [Input("url", "pathname")], -) -def display_page(pathname): - tabs = [ - dcc.Link("Opportunities", href="/dash-salesforce-crm/opportunities"), - dcc.Link("Leads", href="/dash-salesforce-crm/leads"), - dcc.Link("Cases", href="/dash-salesforce-crm/cases"), - ] - if pathname == "/dash-salesforce-crm/opportunities": - tabs[0] = dcc.Link( - dcc.Markdown("**■ Opportunities**"), - href="/dash-salesforce-crm/opportunities", - ) - return opportunities.layout, tabs, tabs - elif pathname == "/dash-salesforce-crm/cases": - tabs[2] = dcc.Link( - dcc.Markdown("**■ Cases**"), href="/dash-salesforce-crm/cases" - ) - return cases.layout, tabs, tabs - tabs[1] = dcc.Link( - dcc.Markdown("**■ Leads**"), href="/dash-salesforce-crm/leads" - ) - return leads.layout, tabs, tabs - - -@app.callback( - Output("mobile_tabs", "style"), - [Input("menu", "n_clicks")], - [State("mobile_tabs", "style")], -) -def show_menu(n_clicks, tabs_style): - if n_clicks: - if tabs_style["display"] == "none": - tabs_style["display"] = "flex" - else: - tabs_style["display"] = "none" - return tabs_style - - -if __name__ == "__main__": - app.run_server(debug=True) diff --git a/apps/dash-salesforce-crm/panels/__init__.py b/apps/dash-salesforce-crm/pages/__init__.py similarity index 100% rename from apps/dash-salesforce-crm/panels/__init__.py rename to apps/dash-salesforce-crm/pages/__init__.py diff --git a/apps/dash-salesforce-crm/pages/cases.py b/apps/dash-salesforce-crm/pages/cases.py new file mode 100644 index 000000000..bdb8fe399 --- /dev/null +++ b/apps/dash-salesforce-crm/pages/cases.py @@ -0,0 +1,89 @@ +from dash import html, dcc, Input, Output, State, callback +import dash_bootstrap_components as dbc +import pandas as pd + +from constants import salesforce_manager +from utils.components import cases_modal, cases_controls, cases_data_cards, cases_graphs +import utils.figures as figs + +layout = html.Div([ + dbc.Row(cases_controls), + dbc.Row(cases_data_cards), + dbc.Row(cases_graphs), + cases_modal(salesforce_manager), +]) + +@callback( + Output("left_cases_indicator", "children"), + Output("middle_cases_indicator", "children"), + Output("right_cases_indicator", "children"), + Output("cases_by_account", "figure"), + Output("cases_reasons", "figure"), + Output("cases_types", "figure"), + Output("cases_by_period", "figure"), + Input("cases_df", "data"), + Input("cases_period_dropdown", "value"), + Input("origin_dropdown", "value"), + Input("priority_dropdown", "value"), +) +def update_graphs(df, period, origin, priority): + df = pd.read_json(df, orient="split") + + ## Data cards + left_cases_indicator = len(df[(df["Priority"] == "Low") & (df["Status"] == "New")]["Priority"].index) + middle_cases_indicator = len(df[(df["Priority"] == "Medium") & (df["Status"] == "New")]["Priority"].index) + right_cases_indicator = len(df[(df["Priority"] == "High") & (df["Status"] == "New")]["Priority"].index) + + ## Figures + fig_by_account = figs.cases_by_account(df, salesforce_manager) + fig_pie_chart = figs.cases_pie_chart(df, "Reason", priority, origin) + fig_pie_chart_h = figs.cases_pie_chart(df, "Type", priority, origin, h_orientation=True) + fig_by_period = figs.cases_by_period(df, period, priority) + + return left_cases_indicator, middle_cases_indicator, right_cases_indicator, \ + fig_by_account, fig_pie_chart, fig_pie_chart_h, fig_by_period + +@callback( + Output("cases_modal", "is_open"), + Input("new_case", "n_clicks"), + State("cases_modal", "is_open"), +) +def toggle_cases_modal(n1, is_open): + if n1: + return not is_open + return is_open + +@callback( + Output("cases_df", "data"), + Input("submit_new_case", "n_clicks"), + State("new_case_account", "value"), + State("new_case_origin", "value"), + State("new_case_reason", "value"), + State("new_case_subject", "value"), + State("new_case_contact", "value"), + State("new_case_type", "value"), + State("new_case_status", "value"), + State("new_case_description", "value"), + State("new_case_priority", "value"), + State("cases_df", "data"), + prevent_initial_call=True, +) +def add_new_case(n_clicks, account_id, origin, reason, subject, contact_id, case_type, status, description, priority, current_df): + if n_clicks > 0: + query = { + "AccountId": account_id, + "Origin": origin, + "Reason": reason, + "Subject": subject, + "ContactId": contact_id, + "Type": case_type, + "Status": status, + "Description": description, + "Priority": priority, + } + + salesforce_manager.add_case(query) + df = salesforce_manager.get_cases() + return df.to_json(orient="split") + + return current_df diff --git a/apps/dash-salesforce-crm/pages/leads.py b/apps/dash-salesforce-crm/pages/leads.py new file mode 100644 index 000000000..063aa77f0 --- /dev/null +++ b/apps/dash-salesforce-crm/pages/leads.py @@ -0,0 +1,98 @@ +from dash import html, Input, Output, State, callback +import dash_bootstrap_components as dbc +import pandas as pd + +from constants import salesforce_manager +from utils.components import leads_modal, leads_controls, leads_data_cards, leads_graphs +import utils.figures as figs + +layout = html.Div([ + dbc.Row(leads_controls), + dbc.Row(leads_data_cards), + dbc.Row(leads_graphs), + leads_modal(), +]) + +@callback( + Output("left_leads_indicator", "children"), + Output("middle_leads_indicator", "children"), + Output("right_leads_indicator", "children"), + Output("lead_source", "figure"), + Output("leads_map", "figure"), + Output("converted_leads", "figure"), + Output("leads_table", "data"), + Output("leads_table", "columns"), + Input("leads_df", "data"), + Input("lead_source_dropdown", "value"), + Input("converted_leads_dropdown", "value"), +) +def update_graphs(df, status, period): + df = pd.read_json(df, orient="split") + + ## Data Cards + left_leads_indicator = len(df[df["Status"] == "Closed - Converted"].index) + middle_leads_indicator = len( df[ (df["Status"] == "Open - Not Contacted") | (df["Status"] == "Working - Contacted") ].index) + + lost_leads = len(df[df["Status"] == "Closed - Not Converted"].index) + right_leads_indicator = left_leads_indicator / (left_leads_indicator + lost_leads) * 100 + + ## Figures + fig_lead_source = figs.lead_source(status, df) + fig_leads_choropleth_map = figs.leads_choropleth_map(status, df) + fig_converted_leads_count = figs.converted_leads_count(period, df) + + ## Table + if status == "open": + df = df[ + (df["Status"] == "Open - Not Contacted") + | (df["Status"] == "Working - Contacted") + ] + elif status == "converted": + df = df[df["Status"] == "Closed - Converted"] + elif status == "lost": + df = df[df["Status"] == "Closed - Not Converted"] + df = df[["CreatedDate", "Status", "Company", "State", "LeadSource"]] + table_data = df.to_dict('records') + table_cols = [{"name": i, "id": i} for i in df.columns] + + return left_leads_indicator, middle_leads_indicator, f"{right_leads_indicator:.2f}%", \ + fig_lead_source, fig_leads_choropleth_map, fig_converted_leads_count, \ + table_data, table_cols + +@callback( + Output("leads_modal", "is_open"), + Input("new_lead", "n_clicks"), + State("leads_modal", "is_open"), +) +def toggle_cases_modal(n1, is_open): + if n1: + return not is_open + return is_open + + +@callback( + Output("leads_df", "data"), + Input("submit_new_lead", "n_clicks"), + State("new_lead_status", "value"), + State("new_lead_state", "value"), + State("new_lead_company", "value"), + State("new_lead_source", "value"), + State("leads_df", "data"), + prevent_initial_call=True, +) +def add_new_lead(n_clicks, status, state, company, source, current_df): + if n_clicks > 0: + if company == "": + company = "Not named yet" + query = { + "LastName": company, + "Company": company, + "Status": status, + "State": state, + "LeadSource": source, + } + salesforce_manager.add_lead(query) + df = salesforce_manager.get_leads() + return df.to_json(orient="split") + + return current_df diff --git a/apps/dash-salesforce-crm/pages/opportunities.py b/apps/dash-salesforce-crm/pages/opportunities.py new file mode 100644 index 000000000..fa8f8a35c --- /dev/null +++ b/apps/dash-salesforce-crm/pages/opportunities.py @@ -0,0 +1,96 @@ +from dash import html, dcc, Input, Output, State, callback +import dash_bootstrap_components as dbc +import pandas as pd + +from constants import salesforce_manager +from utils.helper_functions import millify, top_open_opportunities, top_lost_opportunities +from utils.components import opportunities_modal, opportunities_controls, opportunities_data_cards, opportunities_graphs +import utils.figures as figs + +layout = html.Div([ + dbc.Row(opportunities_controls), + dbc.Row(opportunities_data_cards), + dbc.Row(opportunities_graphs), + opportunities_modal(), +]) + + +@callback( + Output("left_opportunities_indicator", "children"), + Output("middle_opportunities_indicator", "children"), + Output("right_opportunities_indicator", "children"), + Output("opportunities_heatmap", "figure"), + Output("converted_count", "figure"), + Output("top_open_opportunities", "data"), + Output("top_open_opportunities", "columns"), + Output("top_lost_opportunities", "data"), + Output("top_lost_opportunities", "columns"), + Input("opportunities_df", "data"), + Input("heatmap_dropdown", "value"), + Input("converted_opportunities_dropdown", "value"), + Input("source_dropdown", "value"), +) +def update_graphs(df, stage, period, source): + df = pd.read_json(df, orient="split") + + ## Data Cards + left_opportunities_indicator = millify(str(df[df["IsWon"] == 1]["Amount"].sum())) + middle_opportunities_indicator = millify(str(df[(df["IsClosed"] == 0)]["Amount"].sum())) + right_opportunities_indicator = millify(str(df[(df["IsWon"] == 0) & (df["IsClosed"] == 1)]["Amount"].sum())) + + ## Figures + fig_converted_opportunities = figs.converted_opportunities(period, source, df) + fig_heatmap = figs.opportunities_heat_map_fig(df, stage) + + ## Table + table_data_top_ten, table_cols_top_ten = top_open_opportunities(df) + table_data_top_lost, table_cols_top_lost = top_lost_opportunities(df) + + return left_opportunities_indicator, middle_opportunities_indicator, right_opportunities_indicator, \ + fig_heatmap, fig_converted_opportunities, \ + table_data_top_ten, table_cols_top_ten, table_data_top_lost, table_cols_top_lost + +@callback( + Output("opportunities_modal", "is_open"), + Input("new_opportunity", "n_clicks"), + State("opportunities_modal", "is_open"), +) +def toggle_opportunities_modal(n1, is_open): + if n1: + return not is_open + return is_open + + +@callback( + Output("opportunities_df", "data"), + Input("submit_new_opportunity", "n_clicks"), + State("new_opportunity_name", "value"), + State("new_opportunity_stage", "value"), + State("new_opportunity_amount", "value"), + State("new_opportunity_probability", "value"), + State("new_opportunity_date", "date"), + State("new_opportunity_type", "value"), + State("new_opportunity_source", "value"), + State("opportunities_df", "data"), + prevent_initial_call=True, +) +def add_new_opportunity( + n_clicks, name, stage, amount, probability, date, o_type, source, current_df +): + if n_clicks > 0: + if name == "": + name = "Not named yet" + query = { + "Name": name, + "StageName": stage, + "Amount": amount, + "Probability": probability, + "CloseDate": date, + "Type": o_type, + "LeadSource": source, + } + salesforce_manager.add_opportunity(query) + df = salesforce_manager.get_opportunities() + return df.to_json(orient="split") + + return current_df \ No newline at end of file diff --git a/apps/dash-salesforce-crm/panels/cases.py b/apps/dash-salesforce-crm/panels/cases.py deleted file mode 100644 index 4a57224b3..000000000 --- a/apps/dash-salesforce-crm/panels/cases.py +++ /dev/null @@ -1,684 +0,0 @@ -# -*- coding: utf-8 -*- -import pandas as pd -from dash.dependencies import Input, Output, State -import dash_core_components as dcc -import dash_html_components as html -from plotly import graph_objs as go - -from app import app, indicator, sf_manager - -colors = {"background": "#F3F6FA", "background_div": "white"} - -accounts = sf_manager.get_accounts() -contacts = sf_manager.get_contacts() -users = sf_manager.get_users() - -# returns pie chart based on filters values -# column makes the function reusable - - -def pie_chart(df, column, priority, origin): - df = df.dropna(subset=["Type", "Reason", "Origin"]) - nb_cases = len(df.index) - types = [] - values = [] - - # filter priority and origin - if priority == "all_p": - if origin == "all": - types = df[column].unique().tolist() - else: - types = df[df["Origin"] == origin][column].unique().tolist() - else: - if origin == "all": - types = df[df["Priority"] == priority][column].unique().tolist() - else: - types = ( - df[(df["Priority"] == priority) & (df["Origin"] == origin)][column] - .unique() - .tolist() - ) - - # if no results were found - if types == []: - layout = dict( - autosize=True, annotations=[dict(text="No results found", showarrow=False)] - ) - return {"data": [], "layout": layout} - - for case_type in types: - nb_type = df.loc[df[column] == case_type].shape[0] - values.append(nb_type / nb_cases * 100) - - layout = go.Layout( - autosize=True, - margin=dict(l=0, r=0, b=0, t=4, pad=8), - paper_bgcolor="white", - plot_bgcolor="white", - ) - - trace = go.Pie( - labels=types, - values=values, - marker={"colors": ["#264e86", "#0074e4", "#74dbef", "#eff0f4"]}, - ) - - return {"data": [trace], "layout": layout} - - -def cases_by_period(df, period, priority, origin): - df = df.dropna(subset=["Type", "Reason", "Origin"]) - stages = df["Type"].unique() - - # priority filtering - if priority != "all_p": - df = df[df["Priority"] == priority] - - # period filtering - df["CreatedDate"] = pd.to_datetime(df["CreatedDate"], format="%Y-%m-%d") - if period == "W-MON": - df["CreatedDate"] = pd.to_datetime(df["CreatedDate"]) - pd.to_timedelta( - 7, unit="d" - ) - df = df.groupby([pd.Grouper(key="CreatedDate", freq=period), "Type"]).count() - - dates = df.index.get_level_values("CreatedDate").unique() - dates = [str(i) for i in dates] - - co = { # colors for stages - "Electrical": "#264e86", - "Other": "#0074e4", - "Structural": "#74dbef", - "Mechanical": "#eff0f4", - "Electronic": "rgb(255, 127, 14)", - } - - data = [] - for stage in stages: - stage_rows = [] - for date in dates: - try: - row = df.loc[(date, stage)] - stage_rows.append(row["IsDeleted"]) - except Exception as e: - stage_rows.append(0) - - data_trace = go.Bar( - x=dates, y=stage_rows, name=stage, marker=dict(color=co[stage]) - ) - data.append(data_trace) - - layout = go.Layout( - autosize=True, - barmode="stack", - margin=dict(l=40, r=25, b=40, t=0, pad=4), - paper_bgcolor="white", - plot_bgcolor="white", - ) - - return {"data": data, "layout": layout} - - -def cases_by_account(cases): - cases = cases.dropna(subset=["AccountId"]) - cases = pd.merge(cases, accounts, left_on="AccountId", right_on="Id") - cases = cases.groupby(["AccountId", "Name"]).count() - cases = cases.sort_values("IsDeleted") - data = [ - go.Bar( - y=cases.index.get_level_values("Name"), - x=cases["IsDeleted"], - orientation="h", - marker=dict(color="#0073e4"), - ) - ] # x could be any column value since its a count - - layout = go.Layout( - autosize=True, - barmode="stack", - margin=dict(l=210, r=25, b=20, t=0, pad=4), - paper_bgcolor="white", - plot_bgcolor="white", - ) - - return {"data": data, "layout": layout} - - -# returns modal (hidden by default) -def modal(): - contacts["Name"] = ( - contacts["Salutation"] - + " " - + contacts["FirstName"] - + " " - + contacts["LastName"] - ) - return html.Div( - html.Div( - [ - html.Div( - [ - html.Div( - [ - html.Span( - "New Case", - style={ - "color": "#506784", - "fontWeight": "bold", - "fontSize": "20", - }, - ), - html.Span( - "×", - id="cases_modal_close", - n_clicks=0, - style={ - "float": "right", - "cursor": "pointer", - "marginTop": "0", - "marginBottom": "17", - }, - ), - ], - className="row", - style={"borderBottom": "1px solid #C8D4E3"}, - ), - html.Div( - [ - html.Div( - [ - html.P( - "Account name", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - html.Div( - dcc.Dropdown( - id="new_case_account", - options=[ - { - "label": row["Name"], - "value": row["Id"], - } - for index, row in accounts.iterrows() - ], - clearable=False, - value=accounts.iloc[0].Id, - ) - ), - html.P( - "Priority", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Dropdown( - id="new_case_priority", - options=[ - {"label": "High", "value": "High"}, - {"label": "Medium", "value": "Medium"}, - {"label": "Low", "value": "Low"}, - ], - value="Medium", - clearable=False, - ), - html.P( - "Origin", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Dropdown( - id="new_case_origin", - options=[ - {"label": "Phone", "value": "Phone"}, - {"label": "Web", "value": "Web"}, - {"label": "Email", "value": "Email"}, - ], - value="Phone", - clearable=False, - ), - html.P( - "Reason", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Dropdown( - id="new_case_reason", - options=[ - { - "label": "Installation", - "value": "Installation", - }, - { - "label": "Equipment Complexity", - "value": "Equipment Complexity", - }, - { - "label": "Performance", - "value": "Performance", - }, - { - "label": "Breakdown", - "value": "Breakdown", - }, - { - "label": "Equipment Design", - "value": "Equipment Design", - }, - { - "label": "Feedback", - "value": "Feedback", - }, - {"label": "Other", "value": "Other"}, - ], - value="Installation", - clearable=False, - ), - html.P( - "Subject", - style={ - "float": "left", - "marginTop": "4", - "marginBottom": "2", - }, - className="row", - ), - dcc.Input( - id="new_case_subject", - placeholder="The Subject of the case", - type="text", - value="", - style={"width": "100%"}, - ), - ], - className="six columns", - style={"paddingRight": "15"}, - ), - html.Div( - [ - html.P( - "Contact name", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - html.Div( - dcc.Dropdown( - id="new_case_contact", - options=[ - { - "label": row["Name"], - "value": row["Id"], - } - for index, row in contacts.iterrows() - ], - clearable=False, - value=contacts.iloc[0].Id, - ) - ), - html.P( - "Type", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Dropdown( - id="new_case_type", - options=[ - { - "label": "Electrical", - "value": "Electrical", - }, - { - "label": "Mechanical", - "value": "Mechanical", - }, - { - "label": "Electronic", - "value": "Electronic", - }, - { - "label": "Structural", - "value": "Structural", - }, - {"label": "Other", "value": "Other"}, - ], - value="Electrical", - ), - html.P( - "Status", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Dropdown( - id="new_case_status", - options=[ - {"label": "New", "value": "New"}, - { - "label": "Working", - "value": "Working", - }, - { - "label": "Escalated", - "value": "Escalated", - }, - {"label": "Closed", "value": "Closed"}, - ], - value="New", - ), - html.P( - "Supplied Email", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Input( - id="new_case_email", - placeholder="email", - type="email", - value="", - style={"width": "100%"}, - ), - html.P( - "Description", - style={ - "float": "left", - "marginTop": "4", - "marginBottom": "2", - }, - className="row", - ), - dcc.Textarea( - id="new_case_description", - placeholder="Description of the case", - value="", - style={"width": "100%"}, - ), - ], - className="six columns", - style={"paddingLeft": "15"}, - ), - ], - style={"marginTop": "10", "textAlign": "center"}, - className="row", - ), - html.Span( - "Submit", - id="submit_new_case", - n_clicks=0, - className="button button--primary add pretty_container", - ), - ], - className="modal-content", - style={"textAlign": "center", "border": "1px solid #C8D4E3"}, - ) - ], - className="modal", - ), - id="cases_modal", - style={"display": "none"}, - ) - - -layout = [ - html.Div( - id="cases_grid", - children=[ - html.Div( - className="control dropdown-styles", - children=dcc.Dropdown( - id="cases_period_dropdown", - options=[ - {"label": "By day", "value": "D"}, - {"label": "By week", "value": "W-MON"}, - {"label": "By month", "value": "M"}, - ], - value="D", - clearable=False, - ), - ), - html.Div( - className="control dropdown-styles", - children=dcc.Dropdown( - id="priority_dropdown", - options=[ - {"label": "All priority", "value": "all_p"}, - {"label": "High priority", "value": "High"}, - {"label": "Medium priority", "value": "Medium"}, - {"label": "Low priority", "value": "Low"}, - ], - value="all_p", - clearable=False, - ), - ), - html.Div( - className="control dropdown-styles", - children=dcc.Dropdown( - id="origin_dropdown", - options=[ - {"label": "All origins", "value": "all"}, - {"label": "Phone", "value": "Phone"}, - {"label": "Web", "value": "Web"}, - {"label": "Email", "value": "Email"}, - ], - value="all", - clearable=False, - ), - ), - html.Span( - "Add new", - id="new_case", - n_clicks=0, - className="button button--primary add pretty_container", - ), - html.Div( - id="cases_indicators", - className="row indicators", - children=[ - indicator("#00cc96", "Low priority cases", "left_cases_indicator"), - indicator( - "#119DFF", "Medium priority cases", "middle_cases_indicator" - ), - indicator( - "#EF553B", "High priority cases", "right_cases_indicator" - ), - ], - ), - html.Div( - id="cases_types_container", - className="pretty_container chart_div", - children=[ - html.P("Cases Type"), - dcc.Graph( - id="cases_types", - config=dict(displayModeBar=False), - style={"height": "89%", "width": "98%"}, - ), - ], - ), - html.Div( - id="cases_reasons_container", - className="chart_div pretty_container", - children=[ - html.P("Cases Reasons"), - dcc.Graph(id="cases_reasons", config=dict(displayModeBar=False)), - ], - ), - html.Div( - id="cases_by_period_container", - className="pretty_container chart_div", - children=[ - html.P("Cases over Time"), - dcc.Graph(id="cases_by_period", config=dict(displayModeBar=False)), - ], - ), - html.Div( - id="cases_by_account_container", - className="pretty_container chart_div", - children=[ - html.P("Cases by Company"), - dcc.Graph(id="cases_by_account", config=dict(displayModeBar=False)), - ], - ), - ], - ), - modal(), -] - - -@app.callback(Output("left_cases_indicator", "children"), [Input("cases_df", "data")]) -def left_cases_indicator_callback(df): - df = pd.read_json(df, orient="split") - low = len(df[(df["Priority"] == "Low") & (df["Status"] == "New")]["Priority"].index) - return dcc.Markdown("**{}**".format(low)) - - -@app.callback(Output("middle_cases_indicator", "children"), [Input("cases_df", "data")]) -def middle_cases_indicator_callback(df): - df = pd.read_json(df, orient="split") - medium = len( - df[(df["Priority"] == "Medium") & (df["Status"] == "New")]["Priority"].index - ) - return dcc.Markdown("**{}**".format(medium)) - - -@app.callback(Output("right_cases_indicator", "children"), [Input("cases_df", "data")]) -def right_cases_indicator_callback(df): - df = pd.read_json(df, orient="split") - high = len( - df[(df["Priority"] == "High") & (df["Status"] == "New")]["Priority"].index - ) - return dcc.Markdown("**{}**".format(high)) - - -@app.callback( - Output("cases_reasons", "figure"), - [ - Input("priority_dropdown", "value"), - Input("origin_dropdown", "value"), - Input("cases_df", "data"), - ], -) -def cases_reasons_callback(priority, origin, df): - df = pd.read_json(df, orient="split") - chart = pie_chart(df, "Reason", priority, origin) - return chart - - -@app.callback( - Output("cases_types", "figure"), - [ - Input("priority_dropdown", "value"), - Input("origin_dropdown", "value"), - Input("cases_df", "data"), - ], -) -def cases_types_callback(priority, origin, df): - df = pd.read_json(df, orient="split") - chart = pie_chart(df, "Type", priority, origin) - chart["layout"]["legend"]["orientation"] = "h" - return chart - - -@app.callback( - Output("cases_by_period", "figure"), - [ - Input("cases_period_dropdown", "value"), - Input("origin_dropdown", "value"), - Input("priority_dropdown", "value"), - Input("cases_df", "data"), - ], -) -def cases_period_callback(period, origin, priority, df): - df = pd.read_json(df, orient="split") - return cases_by_period(df, period, priority, origin) - - -@app.callback(Output("cases_by_account", "figure"), [Input("cases_df", "data")]) -def cases_account_callback(df): - df = pd.read_json(df, orient="split") - return cases_by_account(df) - - -@app.callback(Output("cases_modal", "style"), [Input("new_case", "n_clicks")]) -def display_cases_modal_callback(n): - if n > 0: - return {"display": "block"} - return {"display": "none"} - - -@app.callback( - Output("new_case", "n_clicks"), - [Input("cases_modal_close", "n_clicks"), Input("submit_new_case", "n_clicks")], -) -def close_modal_callback(n, n2): - return 0 - - -@app.callback( - Output("cases_df", "data"), - [Input("submit_new_case", "n_clicks")], - [ - State("new_case_account", "value"), - State("new_case_origin", "value"), - State("new_case_reason", "value"), - State("new_case_subject", "value"), - State("new_case_contact", "value"), - State("new_case_type", "value"), - State("new_case_status", "value"), - State("new_case_description", "value"), - State("new_case_priority", "value"), - State("cases_df", "data"), - ], -) -def add_case_callback( - n_clicks, - account_id, - origin, - reason, - subject, - contact_id, - case_type, - status, - description, - priority, - current_df, -): - if n_clicks > 0: - query = { - "AccountId": account_id, - "Origin": origin, - "Reason": reason, - "Subject": subject, - "ContactId": contact_id, - "Type": case_type, - "Status": status, - "Description": description, - "Priority": priority, - } - - sf_manager.add_case(query) - df = sf_manager.get_cases() - return df.to_json(orient="split") - - return current_df diff --git a/apps/dash-salesforce-crm/panels/leads.py b/apps/dash-salesforce-crm/panels/leads.py deleted file mode 100644 index 05a8f16d8..000000000 --- a/apps/dash-salesforce-crm/panels/leads.py +++ /dev/null @@ -1,531 +0,0 @@ -# -*- coding: utf-8 -*- -import pandas as pd -from dash.dependencies import Input, Output, State -import dash_core_components as dcc -import dash_html_components as html -from plotly import graph_objs as go - -from app import app, indicator, df_to_table, sf_manager - -states = [ - "AL", - "AK", - "AZ", - "AR", - "CA", - "CO", - "CT", - "DC", - "DE", - "FL", - "GA", - "HI", - "ID", - "IL", - "IN", - "IA", - "KS", - "KY", - "LA", - "ME", - "MD", - "MA", - "MI", - "MN", - "MS", - "MO", - "MT", - "NE", - "NV", - "NH", - "NJ", - "NM", - "NY", - "NC", - "ND", - "OH", - "OK", - "OR", - "PA", - "RI", - "SC", - "SD", - "TN", - "TX", - "UT", - "VT", - "VA", - "WA", - "WV", - "WI", - "WY", -] - - -# returns choropleth map figure based on status filter -def choropleth_map(status, df): - if status == "open": - df = df[ - (df["Status"] == "Open - Not Contacted") - | (df["Status"] == "Working - Contacted") - ] - - elif status == "converted": - df = df[df["Status"] == "Closed - Converted"] - - elif status == "lost": - df = df[df["Status"] == "Closed - Not Converted"] - - df = df.groupby("State").count() - - scl = [[0.0, "rgb(38, 78, 134)"], [1.0, "#0091D5"]] # colors scale - - data = [ - dict( - type="choropleth", - colorscale=scl, - locations=df.index, - z=df["Id"], - locationmode="USA-states", - marker=dict(line=dict(color="rgb(255,255,255)", width=2)), - colorbar=dict(len=0.8), - ) - ] - - layout = dict( - autosize=True, - geo=dict( - scope="usa", - projection=dict(type="albers usa"), - lakecolor="rgb(255, 255, 255)", - ), - margin=dict(l=10, r=10, t=0, b=0), - ) - return dict(data=data, layout=layout) - - -# returns pie chart that shows lead source repartition -def lead_source(status, df): - if status == "open": - df = df[ - (df["Status"] == "Open - Not Contacted") - | (df["Status"] == "Working - Contacted") - ] - - elif status == "converted": - df = df[df["Status"] == "Closed - Converted"] - - elif status == "lost": - df = df[df["Status"] == "Closed - Not Converted"] - - nb_leads = len(df.index) - types = df["LeadSource"].unique().tolist() - values = [] - - # compute % for each leadsource type - for case_type in types: - nb_type = df[df["LeadSource"] == case_type].shape[0] - values.append(nb_type / nb_leads * 100) - - trace = go.Pie( - labels=types, - values=values, - marker={"colors": ["#264e86", "#0074e4", "#74dbef", "#eff0f4"]}, - ) - - layout = dict(autosize=True, margin=dict(l=15, r=10, t=0, b=65)) - return dict(data=[trace], layout=layout) - - -def converted_leads_count(period, df): - df["CreatedDate"] = pd.to_datetime(df["CreatedDate"], format="%Y-%m-%d") - df = df[df["Status"] == "Closed - Converted"] - - df = ( - df.groupby([pd.Grouper(key="CreatedDate", freq=period)]) - .count() - .reset_index() - .sort_values("CreatedDate") - ) - - trace = go.Scatter( - x=df["CreatedDate"], - y=df["Id"], - name="converted leads", - fill="tozeroy", - fillcolor="#e6f2ff", - ) - - data = [trace] - - layout = go.Layout( - autosize=True, - xaxis=dict(showgrid=False), - margin=dict(l=33, r=25, b=37, t=5, pad=4), - paper_bgcolor="white", - plot_bgcolor="white", - ) - - return {"data": data, "layout": layout} - - -def modal(): - return html.Div( - html.Div( - [ - html.Div( - [ - html.Div( - [ - html.Span( - "New Lead", - style={ - "color": "#506784", - "fontWeight": "bold", - "fontSize": "20", - }, - ), - html.Span( - "×", - id="leads_modal_close", - n_clicks=0, - style={ - "float": "right", - "cursor": "pointer", - "marginTop": "0", - "marginBottom": "17", - }, - ), - ], - className="row", - style={"borderBottom": "1px solid #C8D4E3"}, - ), - html.Div( - [ - html.P( - ["Company Name"], - style={ - "float": "left", - "marginTop": "4", - "marginBottom": "2", - }, - className="row", - ), - dcc.Input( - id="new_lead_company", - placeholder="Enter company name", - type="text", - value="", - style={"width": "100%"}, - ), - html.P( - "Company State", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Dropdown( - id="new_lead_state", - options=[ - {"label": state, "value": state} - for state in states - ], - value="NY", - ), - html.P( - "Status", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Dropdown( - id="new_lead_status", - options=[ - { - "label": "Open - Not Contacted", - "value": "Open - Not Contacted", - }, - { - "label": "Working - Contacted", - "value": "Working - Contacted", - }, - { - "label": "Closed - Converted", - "value": "Closed - Converted", - }, - { - "label": "Closed - Not Converted", - "value": "Closed - Not Converted", - }, - ], - value="Open - Not Contacted", - ), - html.P( - "Source", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Dropdown( - id="new_lead_source", - options=[ - {"label": "Web", "value": "Web"}, - { - "label": "Phone Inquiry", - "value": "Phone Inquiry", - }, - { - "label": "Partner Referral", - "value": "Partner Referral", - }, - { - "label": "Purchased List", - "value": "Purchased List", - }, - {"label": "Other", "value": "Other"}, - ], - value="Web", - ), - ], - className="row", - style={"padding": "2% 8%"}, - ), - html.Span( - "Submit", - id="submit_new_lead", - n_clicks=0, - className="button button--primary add pretty_container", - ), - ], - className="modal-content", - style={"textAlign": "center"}, - ) - ], - className="modal", - ), - id="leads_modal", - style={"display": "none"}, - ) - - -layout = [ - html.Div( - id="lead_grid", - children=[ - html.Div( - className="two columns dd-styles", - children=dcc.Dropdown( - id="converted_leads_dropdown", - options=[ - {"label": "By day", "value": "D"}, - {"label": "By week", "value": "W-MON"}, - {"label": "By month", "value": "M"}, - ], - value="D", - clearable=False, - ), - ), - html.Div( - className="two columns dd-styles", - children=dcc.Dropdown( - id="lead_source_dropdown", - options=[ - {"label": "All status", "value": "all"}, - {"label": "Open leads", "value": "open"}, - {"label": "Converted leads", "value": "converted"}, - {"label": "Lost leads", "value": "lost"}, - ], - value="all", - clearable=False, - ), - ), - html.Span( - "Add new", - id="new_lead", - n_clicks=0, - className="button pretty_container", - ), - html.Div( - className="row indicators", - children=[ - indicator("#00cc96", "Converted Leads", "left_leads_indicator"), - indicator("#119DFF", "Open Leads", "middle_leads_indicator"), - indicator("#EF553B", "Conversion Rates", "right_leads_indicator"), - ], - ), - html.Div( - id="leads_per_state", - className="chart_div pretty_container", - children=[ - html.P("Leads count per state"), - dcc.Graph( - id="map", - style={"height": "90%", "width": "98%"}, - config=dict(displayModeBar=False), - ), - ], - ), - html.Div( - id="leads_source_container", - className="six columns chart_div pretty_container", - children=[ - html.P("Leads by source"), - dcc.Graph( - id="lead_source", - style={"height": "90%", "width": "98%"}, - config=dict(displayModeBar=False), - ), - ], - ), - html.Div( - id="converted_leads_container", - className="six columns chart_div pretty_container", - children=[ - html.P("Converted Leads count"), - dcc.Graph( - id="converted_leads", - style={"height": "90%", "width": "98%"}, - config=dict(displayModeBar=False), - ), - ], - ), - html.Div(id="leads_table", className="row pretty_container table"), - ], - ), - modal(), -] - - -# updates left indicator based on df updates -@app.callback(Output("left_leads_indicator", "children"), [Input("leads_df", "data")]) -def left_leads_indicator_callback(df): - df = pd.read_json(df, orient="split") - converted_leads = len(df[df["Status"] == "Closed - Converted"].index) - return dcc.Markdown("**{}**".format(converted_leads)) - - -# updates middle indicator based on df updates -@app.callback(Output("middle_leads_indicator", "children"), [Input("leads_df", "data")]) -def middle_leads_indicator_callback(df): - df = pd.read_json(df, orient="split") - open_leads = len( - df[ - (df["Status"] == "Open - Not Contacted") - | (df["Status"] == "Working - Contacted") - ].index - ) - return dcc.Markdown("**{}**".format(open_leads)) - - -# updates right indicator based on df updates -@app.callback(Output("right_leads_indicator", "children"), [Input("leads_df", "data")]) -def right_leads_indicator_callback(df): - df = pd.read_json(df, orient="split") - converted_leads = len(df[df["Status"] == "Closed - Converted"].index) - lost_leads = len(df[df["Status"] == "Closed - Not Converted"].index) - conversion_rates = converted_leads / (converted_leads + lost_leads) * 100 - conversion_rates = "%.2f" % conversion_rates + "%" - return dcc.Markdown("**{}**".format(conversion_rates)) - - -# update pie chart figure based on dropdown's value and df updates -@app.callback( - Output("lead_source", "figure"), - [Input("lead_source_dropdown", "value"), Input("leads_df", "data")], -) -def lead_source_callback(status, df): - df = pd.read_json(df, orient="split") - return lead_source(status, df) - - -# update heat map figure based on dropdown's value and df updates -@app.callback( - Output("map", "figure"), - [Input("lead_source_dropdown", "value"), Input("leads_df", "data")], -) -def map_callback(status, df): - df = pd.read_json(df, orient="split") - return choropleth_map(status, df) - - -# update table based on dropdown's value and df updates -@app.callback( - Output("leads_table", "children"), - [Input("lead_source_dropdown", "value"), Input("leads_df", "data")], -) -def leads_table_callback(status, df): - df = pd.read_json(df, orient="split") - if status == "open": - df = df[ - (df["Status"] == "Open - Not Contacted") - | (df["Status"] == "Working - Contacted") - ] - elif status == "converted": - df = df[df["Status"] == "Closed - Converted"] - elif status == "lost": - df = df[df["Status"] == "Closed - Not Converted"] - df = df[["CreatedDate", "Status", "Company", "State", "LeadSource"]] - return df_to_table(df) - - -# update pie chart figure based on dropdown's value and df updates -@app.callback( - Output("converted_leads", "figure"), - [Input("converted_leads_dropdown", "value"), Input("leads_df", "data")], -) -def converted_leads_callback(period, df): - df = pd.read_json(df, orient="split") - return converted_leads_count(period, df) - - -# hide/show modal -@app.callback(Output("leads_modal", "style"), [Input("new_lead", "n_clicks")]) -def display_leads_modal_callback(n): - if n > 0: - return {"display": "block"} - return {"display": "none"} - - -# reset to 0 add button n_clicks property -@app.callback( - Output("new_lead", "n_clicks"), - [Input("leads_modal_close", "n_clicks"), Input("submit_new_lead", "n_clicks")], -) -def close_modal_callback(n, n2): - return 0 - - -# add new lead to salesforce and stores new df in hidden div -@app.callback( - Output("leads_df", "data"), - [Input("submit_new_lead", "n_clicks")], - [ - State("new_lead_status", "value"), - State("new_lead_state", "value"), - State("new_lead_company", "value"), - State("new_lead_source", "value"), - State("leads_df", "data"), - ], -) -def add_lead_callback(n_clicks, status, state, company, source, current_df): - if n_clicks > 0: - if company == "": - company = "Not named yet" - query = { - "LastName": company, - "Company": company, - "Status": status, - "State": state, - "LeadSource": source, - } - sf_manager.add_lead(query) - df = sf_manager.get_leads() - return df.to_json(orient="split") - - return current_df diff --git a/apps/dash-salesforce-crm/panels/opportunities.py b/apps/dash-salesforce-crm/panels/opportunities.py deleted file mode 100644 index f190ded6a..000000000 --- a/apps/dash-salesforce-crm/panels/opportunities.py +++ /dev/null @@ -1,615 +0,0 @@ -# -*- coding: utf-8 -*- -from datetime import date -import pandas as pd -from dash.dependencies import Input, Output, State -import dash_core_components as dcc -import dash_html_components as html -from plotly import graph_objs as go - -from app import app, indicator, millify, df_to_table, sf_manager - - -def converted_opportunities(period, source, df): - df["CreatedDate"] = pd.to_datetime(df["CreatedDate"], format="%Y-%m-%d") - - # source filtering - if source == "all_s": - df = df[df["IsWon"] == 1] - else: - df = df[(df["LeadSource"] == source) & (df["IsWon"] == 1)] - - # period filtering - if period == "W-MON": - df["CreatedDate"] = pd.to_datetime(df["CreatedDate"]) - pd.to_timedelta( - 7, unit="d" - ) - df = ( - df.groupby([pd.Grouper(key="CreatedDate", freq=period)]) - .count() - .reset_index() - .sort_values("CreatedDate") - ) - - # if no results were found - if df.empty: - layout = dict( - autosize=True, annotations=[dict(text="No results found", showarrow=False)] - ) - return {"data": [], "layout": layout} - - trace = go.Scatter( - x=df["CreatedDate"], - y=df["IsWon"], - name="converted opportunities", - fill="tozeroy", - fillcolor="#e6f2ff", - ) - - data = [trace] - - layout = go.Layout( - autosize=True, - xaxis=dict(showgrid=False), - margin=dict(l=35, r=25, b=23, t=5, pad=4), - paper_bgcolor="white", - plot_bgcolor="white", - ) - - return {"data": data, "layout": layout} - - -# returns heat map figure -def heat_map_fig(df, x, y): - z = [] - for lead_type in y: - z_row = [] - for stage in x: - probability = df[(df["StageName"] == stage) & (df["Type"] == lead_type)][ - "Probability" - ].mean() - z_row.append(probability) - z.append(z_row) - - trace = dict( - type="heatmap", z=z, x=x, y=y, name="mean probability", colorscale="Blues" - ) - layout = dict( - autosize=True, - margin=dict(t=25, l=210, b=85, pad=4), - paper_bgcolor="white", - plot_bgcolor="white", - ) - - return go.Figure(data=[trace], layout=layout) - - -# returns top 5 open opportunities -def top_open_opportunities(df): - df = df.sort_values("Amount", ascending=True) - cols = ["CreatedDate", "Name", "Amount", "StageName"] - df = df[cols].iloc[:5] - # only display 21 characters - df["Name"] = df["Name"].apply(lambda x: x[:30]) - return df_to_table(df) - - -# returns top 5 lost opportunities -def top_lost_opportunities(df): - df = df[df["StageName"] == "Closed Lost"] - cols = ["CreatedDate", "Name", "Amount", "StageName"] - df = df[cols].sort_values("Amount", ascending=False).iloc[:5] - # only display 21 characters - df["Name"] = df["Name"].apply(lambda x: x[:30]) - return df_to_table(df) - - -# returns modal (hidden by default) -def modal(): - return html.Div( - html.Div( - [ - html.Div( - [ - html.Div( - [ - html.Span( - "New Opportunity", - style={ - "color": "#506784", - "fontWeight": "bold", - "fontSize": "20", - }, - ), - html.Span( - "×", - id="opportunities_modal_close", - n_clicks=0, - style={ - "float": "right", - "cursor": "pointer", - "marginTop": "0", - "marginBottom": "17", - }, - ), - ], - className="row", - style={"borderBottom": "1px solid #C8D4E3"}, - ), - html.Div( - [ - html.Div( - [ - html.P( - ["Name"], - style={ - "float": "left", - "marginTop": "4", - "marginBottom": "2", - }, - className="row", - ), - dcc.Input( - id="new_opportunity_name", - placeholder="Name of the opportunity", - type="text", - value="", - style={"width": "100%"}, - ), - html.P( - ["StageName"], - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Dropdown( - id="new_opportunity_stage", - options=[ - { - "label": "Prospecting", - "value": "Prospecting", - }, - { - "label": "Qualification", - "value": "Qualification", - }, - { - "label": "Needs Analysis", - "value": "Needs Analysis", - }, - { - "label": "Value Proposition", - "value": "Value Proposition", - }, - { - "label": "Id. Decision Makers", - "value": "Closed", - }, - { - "label": "Perception Analysis", - "value": "Perception Analysis", - }, - { - "label": "Proposal/Price Quote", - "value": "Proposal/Price Quote", - }, - { - "label": "Negotiation/Review", - "value": "Negotiation/Review", - }, - { - "label": "Closed/Won", - "value": "Closed Won", - }, - { - "label": "Closed/Lost", - "value": "Closed Lost", - }, - ], - clearable=False, - value="Prospecting", - ), - html.P( - "Source", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Dropdown( - id="new_opportunity_source", - options=[ - {"label": "Web", "value": "Web"}, - { - "label": "Phone Inquiry", - "value": "Phone Inquiry", - }, - { - "label": "Partner Referral", - "value": "Partner Referral", - }, - { - "label": "Purchased List", - "value": "Purchased List", - }, - {"label": "Other", "value": "Other"}, - ], - value="Web", - ), - html.P( - ["Close Date"], - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - html.Div( - dcc.DatePickerSingle( - id="new_opportunity_date", - min_date_allowed=date.today(), - # max_date_allowed=dt(2017, 9, 19), - initial_visible_month=date.today(), - date=date.today(), - ), - style={"textAlign": "left"}, - ), - ], - className="six columns", - style={"paddingRight": "15"}, - ), - html.Div( - [ - html.P( - "Type", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Dropdown( - id="new_opportunity_type", - options=[ - { - "label": "Existing Customer - Replacement", - "value": "Existing Customer - Replacement", - }, - { - "label": "New Customer", - "value": "New Customer", - }, - { - "label": "Existing Customer - Upgrade", - "value": "Existing Customer - Upgrade", - }, - { - "label": "Existing Customer - Downgrade", - "value": "Existing Customer - Downgrade", - }, - ], - value="New Customer", - ), - html.P( - "Amount", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Input( - id="new_opportunity_amount", - placeholder="0", - type="number", - value="", - style={"width": "100%"}, - ), - html.P( - "Probability", - style={ - "textAlign": "left", - "marginBottom": "2", - "marginTop": "4", - }, - ), - dcc.Input( - id="new_opportunity_probability", - placeholder="0", - type="number", - max=100, - step=1, - value="", - style={"width": "100%"}, - ), - ], - className="six columns", - style={"paddingLeft": "15"}, - ), - ], - className="row", - style={"paddingTop": "2%"}, - ), - html.Span( - "Submit", - id="submit_new_opportunity", - n_clicks=0, - className="button button--primary add pretty_container", - ), - ], - className="modal-content", - style={"textAlign": "center"}, - ) - ], - className="modal", - ), - id="opportunities_modal", - style={"display": "none"}, - ) - - -layout = [ - html.Div( - id="opportunity_grid", - children=[ - html.Div( - className="control dropdown-styles", - children=dcc.Dropdown( - id="converted_opportunities_dropdown", - options=[ - {"label": "By day", "value": "D"}, - {"label": "By week", "value": "W-MON"}, - {"label": "By month", "value": "M"}, - ], - value="D", - clearable=False, - ), - ), - html.Div( - className="control dropdown-styles", - children=dcc.Dropdown( - id="heatmap_dropdown", - options=[ - {"label": "All stages", "value": "all_s"}, - {"label": "Cold stages", "value": "cold"}, - {"label": "Warm stages", "value": "warm"}, - {"label": "Hot stages", "value": "hot"}, - ], - value="all_s", - clearable=False, - ), - ), - html.Div( - className="control dropdown-styles", - children=dcc.Dropdown( - id="source_dropdown", - options=[ - {"label": "All sources", "value": "all_s"}, - {"label": "Web", "value": "Web"}, - {"label": "Word of Mouth", "value": "Word of mouth"}, - {"label": "Phone Inquiry", "value": "Phone Inquiry"}, - {"label": "Partner Referral", "value": "Partner Referral"}, - {"label": "Purchased List", "value": "Purchased List"}, - {"label": "Other", "value": "Other"}, - ], - value="all_s", - clearable=False, - ), - ), - html.Span( - "Add new", - id="new_opportunity", - n_clicks=0, - className="button pretty_container", - ), - html.Div( - id="opportunity_indicators", - className="row indicators", - children=[ - indicator( - "#00cc96", "Won opportunities", "left_opportunities_indicator" - ), - indicator( - "#119DFF", - "Open opportunities", - "middle_opportunities_indicator", - ), - indicator( - "#EF553B", "Lost opportunities", "right_opportunities_indicator" - ), - ], - ), - html.Div( - id="converted_count_container", - className="chart_div pretty_container", - children=[ - html.P("Converted Opportunities count"), - dcc.Graph( - id="converted_count", - style={"height": "90%", "width": "98%"}, - config=dict(displayModeBar=False), - ), - ], - ), - html.Div( - id="opportunity_heatmap", - className="chart_div pretty_container", - children=[ - html.P("Probabilty heatmap per Stage and Type"), - dcc.Graph( - id="heatmap", - style={"height": "90%", "width": "98%"}, - config=dict(displayModeBar=False), - ), - ], - ), - html.Div( - id="top_open_container", - className="pretty_container", - children=[ - html.Div([html.P("Top Open opportunities")], className="subtitle"), - html.Div(id="top_open_opportunities", className="table"), - ], - ), - html.Div( - id="top_lost_container", - className="pretty_container", - children=[ - html.Div([html.P("Top Lost opportunities")], className="subtitle"), - html.Div(id="top_lost_opportunities", className="table"), - ], - ), - ], - ), - modal(), -] - - -# updates heatmap figure based on dropdowns values or df updates -@app.callback( - Output("heatmap", "figure"), - [Input("heatmap_dropdown", "value"), Input("opportunities_df", "data")], -) -def heat_map_callback(stage, df): - df = pd.read_json(df, orient="split") - df = df[pd.notnull(df["Type"])] - x = [] - y = df["Type"].unique() - if stage == "all_s": - x = df["StageName"].unique() - elif stage == "cold": - x = ["Needs Analysis", "Prospecting", "Qualification"] - elif stage == "warm": - x = ["Value Proposition", "Id. Decision Makers", "Perception Analysis"] - else: - x = ["Proposal/Price Quote", "Negotiation/Review", "Closed Won"] - return heat_map_fig(df, x, y) - - -# updates converted opportunity count graph based on dropdowns values or df updates -@app.callback( - Output("converted_count", "figure"), - [ - Input("converted_opportunities_dropdown", "value"), - Input("source_dropdown", "value"), - Input("opportunities_df", "data"), - ], -) -def converted_opportunity_callback(period, source, df): - df = pd.read_json(df, orient="split") - return converted_opportunities(period, source, df) - - -# updates left indicator value based on df updates -@app.callback( - Output("left_opportunities_indicator", "children"), - [Input("opportunities_df", "data")], -) -def left_opportunities_indicator_callback(df): - df = pd.read_json(df, orient="split") - won = millify(str(df[df["IsWon"] == 1]["Amount"].sum())) - return dcc.Markdown("**{}**".format(won)) - - -# updates middle indicator value based on df updates -@app.callback( - Output("middle_opportunities_indicator", "children"), - [Input("opportunities_df", "data")], -) -def middle_opportunities_indicator_callback(df): - df = pd.read_json(df, orient="split") - active = millify(str(df[(df["IsClosed"] == 0)]["Amount"].sum())) - return dcc.Markdown("**{}**".format(active)) - - -# updates right indicator value based on df updates -@app.callback( - Output("right_opportunities_indicator", "children"), - [Input("opportunities_df", "data")], -) -def right_opportunities_indicator_callback(df): - df = pd.read_json(df, orient="split") - lost = millify(str(df[(df["IsWon"] == 0) & (df["IsClosed"] == 1)]["Amount"].sum())) - return dcc.Markdown("**{}**".format(lost)) - - -# hide/show modal -@app.callback( - Output("opportunities_modal", "style"), [Input("new_opportunity", "n_clicks")] -) -def display_opportunities_modal_callback(n): - if n > 0: - return {"display": "block"} - return {"display": "none"} - - -# reset to 0 add button n_clicks property -@app.callback( - Output("new_opportunity", "n_clicks"), - [ - Input("opportunities_modal_close", "n_clicks"), - Input("submit_new_opportunity", "n_clicks"), - ], -) -def close_modal_callback(n, n2): - return 0 - - -# add new opportunity to salesforce and stores new df in hidden div -@app.callback( - Output("opportunities_df", "data"), - [Input("submit_new_opportunity", "n_clicks")], - [ - State("new_opportunity_name", "value"), - State("new_opportunity_stage", "value"), - State("new_opportunity_amount", "value"), - State("new_opportunity_probability", "value"), - State("new_opportunity_date", "date"), - State("new_opportunity_type", "value"), - State("new_opportunity_source", "value"), - State("opportunities_df", "data"), - ], -) -def add_opportunity_callback( - n_clicks, name, stage, amount, probability, date, o_type, source, current_df -): - if n_clicks > 0: - if name == "": - name = "Not named yet" - query = { - "Name": name, - "StageName": stage, - "Amount": amount, - "Probability": probability, - "CloseDate": date, - "Type": o_type, - "LeadSource": source, - } - - sf_manager.add_opportunity(query) - - df = sf_manager.get_opportunities() - - return df.to_json(orient="split") - - return current_df - - -# updates top open opportunities based on df updates -@app.callback( - Output("top_open_opportunities", "children"), [Input("opportunities_df", "data")] -) -def top_open_opportunities_callback(df): - df = pd.read_json(df, orient="split") - return top_open_opportunities(df) - - -# updates top lost opportunities based on df updates -@app.callback( - Output("top_lost_opportunities", "children"), [Input("opportunities_df", "data")] -) -def top_lost_opportunities_callback(df): - df = pd.read_json(df, orient="split") - return top_lost_opportunities(df) diff --git a/apps/dash-salesforce-crm/requirements.txt b/apps/dash-salesforce-crm/requirements.txt index 202c644fb..c915af0ad 100644 --- a/apps/dash-salesforce-crm/requirements.txt +++ b/apps/dash-salesforce-crm/requirements.txt @@ -1,5 +1,5 @@ -certifi==2019.6.16 -simple_salesforce==0.74.2 -pandas==0.24.2 -dash==1.0.0 -gunicorn==19.9.0 \ No newline at end of file +dash==2.4.1 +pandas==1.4.2 +gunicorn==20.1.0 +cryptography==2.8 +simple_salesforce==1.11 \ No newline at end of file diff --git a/apps/dash-salesforce-crm/utils/components.py b/apps/dash-salesforce-crm/utils/components.py new file mode 100644 index 000000000..d966e5bbd --- /dev/null +++ b/apps/dash-salesforce-crm/utils/components.py @@ -0,0 +1,587 @@ +from dash import html, dcc, dash_table +import dash_bootstrap_components as dbc +from constants import states +from datetime import date + + +def Header(app): + name = [ + html.Span("CRM App"), + html.Span(" using Salesforce API", style={"font-size": "1.8rem", "margin-top": "15px"}), + ] + title = html.H2(name, style={"margin-top": 5}) + logo = html.Img(src=app.get_asset_url("images/plotly-logo.png"), style={"float": "right", "height": 60}) + link = html.A(logo, href="https://plotly.com/dash/", target="_blank") + demo_link = html.A("ENTERPRISE DEMO", href="https://plotly.com/get-demo/", target="_blank", className="demo-button") + return dbc.Row([dbc.Col(title, md=8), dbc.Col([demo_link, link], md=4, className="header-logos")], className="header") + +def dbc_indicator(text, id_value, width): + return dbc.Col( + dbc.Card([ + html.H1(id=id_value, className="card-title"), + html.P(text), + ], className="align-items-center"), width = width + ) + +def dbc_card(header, child_id, width, table=None): + if table is None: + child = dcc.Graph(id=child_id, config=dict(displayModeBar=False)) + else: + child = dash_table.DataTable(id=child_id, style_table={'overflowX': 'auto'},) + return dbc.Col( + dbc.Card( + dbc.CardBody([ + html.H4(header, className="card-title"), + child + ]) + ), width = width + ) + +### Leads page components ### ### ### ### + + +leads_controls = [ + dbc.Col([ + dcc.Dropdown( + id="converted_leads_dropdown", + options=[ + {"label": "By day", "value": "D"}, + {"label": "By week", "value": "W-MON"}, + {"label": "By month", "value": "M"}, + ], + value="M", + clearable=False, + searchable=False, + ) + ], width=2), + dbc.Col([ + dcc.Dropdown( + id="lead_source_dropdown", + options=[ + {"label": "All status", "value": "all"}, + {"label": "Open leads", "value": "open"}, + {"label": "Converted leads", "value": "converted"}, + {"label": "Lost leads", "value": "lost"}, + ], + value="all", + clearable=False, + searchable=False, + ) + ], width=2), + dbc.Col(width=6), + dbc.Col([ + dbc.Button("Add New Lead", color="primary", size="lg", id="new_lead"), + ], width=2) + ] +leads_data_cards = [ + dbc_indicator("Converted Leads", "left_leads_indicator", 4), + dbc_indicator("Open Leads", "middle_leads_indicator", 4), + dbc_indicator("Conversion Rates", "right_leads_indicator", 4), +] + +leads_graphs = [ + dbc_card("Leads Count per State", "leads_map", width=4), + dbc_card("Leads by Source", "lead_source", width=4), + dbc_card("Converted Leads Count", "converted_leads", width=4), + dbc_card("Table of Leads", "leads_table", width=12, table=True), +] + + +def leads_modal(): + return dbc.Modal([ + dbc.ModalHeader(dbc.ModalTitle("New Lead")), + dbc.ModalBody(dbc.Row([ + dbc.Col([ + dbc.Label("Company Name"), + dcc.Input( + id="new_lead_company", + placeholder="Enter company name", + type="text", + value="", + style={"width": "100%"}, + ), + ], width=6), + dbc.Col([ + dbc.Label("Company State"), + dcc.Dropdown( + id="new_lead_state", + options=states, + value="NY", + searchable=False, + ), + ], width=6), + dbc.Col([ + dbc.Label("Status"), + dcc.Dropdown( + id="new_lead_status", + options=["Open - Not Contacted", "Working - Contacted", "Closed - Converted", "Closed - Not Converted"], + value="Open - Not Contacted", + searchable=False, + ), + ], width=6), + dbc.Col([ + dbc.Label("Source"), + dcc.Dropdown( + id="new_lead_source", + options=["Web", "Phone Inquiry", "Partner Referral", "Purchased List", "Other"], + value="Web", + searchable=False, + ), + ], width=6), + ])), + dbc.ModalFooter(dbc.Button("Submit", id="submit_new_lead", className="ms-auto")), + ], + id="leads_modal", + is_open=False, +) + + + +### Cases page components ### ### ### ### + +cases_controls = [ + dbc.Col([ + dcc.Dropdown( + id="cases_period_dropdown", + options=[ + {"label": "By day", "value": "D"}, + {"label": "By week", "value": "W-MON"}, + {"label": "By month", "value": "M"}, + ], + value="M", + clearable=False, + searchable=False, + ), + ], width=2), + dbc.Col([ + dcc.Dropdown( + id="priority_dropdown", + options=[ + {"label": "All priority", "value": "all_p"}, + {"label": "High priority", "value": "High"}, + {"label": "Medium priority", "value": "Medium"}, + {"label": "Low priority", "value": "Low"}, + ], + value="all_p", + clearable=False, + searchable=False, + ), + ], width=2), + dbc.Col([ + dcc.Dropdown( + id="origin_dropdown", + options=[ + {"label": "All origins", "value": "all"}, + {"label": "Phone", "value": "Phone"}, + {"label": "Web", "value": "Web"}, + {"label": "Email", "value": "Email"}, + ], + value="all", + clearable=False, + searchable=False, + ), + ], width=2), + dbc.Col(width=4), + dbc.Col([ + dbc.Button("Add New Case", color="primary", size="lg", id="new_case"), + ], width=2) + ] +cases_data_cards = [ + dbc_indicator("Low priority cases", "left_cases_indicator", 4), + dbc_indicator("Medium priority cases", "middle_cases_indicator", 4), + dbc_indicator("High priority cases", "right_cases_indicator", 4), +] + +cases_graphs = [ + dbc_card("Cases Type", "cases_types", width=6), + dbc_card("Cases Reasons", "cases_reasons", width=6), + dbc_card("Cases over Time", "cases_by_period", width=6), + dbc_card("Cases by Company", "cases_by_account", width=6), +] + +def cases_modal(salesforce_manager): + accounts = salesforce_manager.get_accounts() + contacts = salesforce_manager.get_contacts() + + contacts["Name"] = ( + contacts["Salutation"] + + " " + + contacts["FirstName"] + + " " + + contacts["LastName"] + ) + return dbc.Modal([ + dbc.ModalHeader(dbc.ModalTitle("New Case")), + dbc.ModalBody(dbc.Row([ + dbc.Col([ + dbc.Label("Account Name"), + dcc.Dropdown( + id="new_case_account", + options=[ + { + "label": row["Name"], + "value": row["Id"], + } + for index, row in accounts.iterrows() + ], + clearable=False, + searchable=False, + value=accounts.iloc[0].Id, + ) + ], width=6), + dbc.Col([ + dbc.Label("Contact Name"), + dcc.Dropdown( + id="new_case_contact", + options=[ + { + "label": row["Name"], + "value": row["Id"], + } + for index, row in contacts.iterrows() + ], + clearable=False, + searchable=False, + value=contacts.iloc[0].Id, + ) + ], width=6), + dbc.Col([ + dbc.Label("Priority"), + dcc.Dropdown( + id="new_case_priority", + options=[ + {"label": "High", "value": "High"}, + {"label": "Medium", "value": "Medium"}, + {"label": "Low", "value": "Low"}, + ], + value="Medium", + clearable=False, + searchable=False, + ), + ], width=6), + dbc.Col([ + dbc.Label("Type"), + dcc.Dropdown( + id="new_case_type", + options=[ + { + "label": "Electrical", + "value": "Electrical", + }, + { + "label": "Mechanical", + "value": "Mechanical", + }, + { + "label": "Electronic", + "value": "Electronic", + }, + { + "label": "Structural", + "value": "Structural", + }, + {"label": "Other", "value": "Other"}, + ], + value="Electrical", + searchable=False, + ), + ], width=6), + dbc.Col([ + dbc.Label("Origin"), + dcc.Dropdown( + id="new_case_origin", + options=[ + {"label": "Phone", "value": "Phone"}, + {"label": "Web", "value": "Web"}, + {"label": "Email", "value": "Email"}, + ], + value="Phone", + clearable=False, + searchable=False, + ), + ], width=6), + dbc.Col([ + dbc.Label("Status"), + dcc.Dropdown( + id="new_case_status", + options=[ + {"label": "New", "value": "New"}, + { + "label": "Working", + "value": "Working", + }, + { + "label": "Escalated", + "value": "Escalated", + }, + {"label": "Closed", "value": "Closed"}, + ], + value="New", + searchable=False, + ), + ], width=6), + dbc.Col([ + dbc.Label("Reason"), + dcc.Dropdown( + id="new_case_reason", + options=[ + { + "label": "Installation", + "value": "Installation", + }, + { + "label": "Equipment Complexity", + "value": "Equipment Complexity", + }, + { + "label": "Performance", + "value": "Performance", + }, + { + "label": "Breakdown", + "value": "Breakdown", + }, + { + "label": "Equipment Design", + "value": "Equipment Design", + }, + { + "label": "Feedback", + "value": "Feedback", + }, + {"label": "Other", "value": "Other"}, + ], + value="Installation", + clearable=False, + searchable=False, + ), + ], width=6), + dbc.Col([ + dbc.Label("Supplied Email"), + dcc.Input( + id="new_case_email", + placeholder="email", + type="email", + value="", + style={"width": "100%"}, + ), + ], width=6), + dbc.Col([ + dbc.Label("Subject"), + dcc.Input( + id="new_case_subject", + placeholder="The Subject of the case", + type="text", + value="", + style={"width": "100%"}, + ), + ], width=6), + dbc.Col([ + dbc.Label("Description"), + dcc.Textarea( + id="new_case_description", + placeholder="Description of the case", + value="", + style={"width": "100%"}, + ), + ], width=6), + ])), + dbc.ModalFooter(dbc.Button("Submit", id="submit_new_case", className="ms-auto")), + ], + id="cases_modal", + is_open=False, +) + + +### Opportunities page components ### ### ### ### + +opportunities_controls = [ + dbc.Col([ + dcc.Dropdown( + id="converted_opportunities_dropdown", + options=[ + {"label": "By day", "value": "D"}, + {"label": "By week", "value": "W-MON"}, + {"label": "By month", "value": "M"}, + ], + value="M", + clearable=False, + searchable=False, + ), + ], width=2), + dbc.Col([ + dcc.Dropdown( + id="heatmap_dropdown", + options=[ + {"label": "All stages", "value": "all_s"}, + {"label": "Cold stages", "value": "cold"}, + {"label": "Warm stages", "value": "warm"}, + {"label": "Hot stages", "value": "hot"}, + ], + value="all_s", + clearable=False, + searchable=False, + ), + ], width=2), + dbc.Col([ + dcc.Dropdown( + id="source_dropdown", + options=[ + {"label": "All sources", "value": "all_s"}, + {"label": "Web", "value": "Web"}, + {"label": "Word of Mouth", "value": "Word of mouth"}, + {"label": "Phone Inquiry", "value": "Phone Inquiry"}, + {"label": "Partner Referral", "value": "Partner Referral"}, + {"label": "Purchased List", "value": "Purchased List"}, + {"label": "Other", "value": "Other"}, + ], + value="all_s", + clearable=False, + searchable=False, + ), + ], width=2), + dbc.Col(width=4), + dbc.Col([ + dbc.Button("Add New Opportunity", color="primary", size="lg", id="new_opportunity"), + ], width=2) + ] +opportunities_data_cards = [ + dbc_indicator("Won opportunities", "left_opportunities_indicator", 4), + dbc_indicator("Open opportunities", "middle_opportunities_indicator", 4), + dbc_indicator("Lost opportunities", "right_opportunities_indicator", 4), +] + +opportunities_graphs = [ + dbc_card("Converted Opportunities count", "converted_count", width=4), + dbc_card("Probabilty heatmap per Stage and Type", "opportunities_heatmap", width=8), + dbc_card("Top Open opportunities", "top_open_opportunities", width=6, table=True), + dbc_card("Top Lost opportunities", "top_lost_opportunities", width=6, table=True), +] + + +def opportunities_modal(): + return dbc.Modal([ + dbc.ModalHeader(dbc.ModalTitle("New Opportunity")), + dbc.ModalBody(dbc.Row([ + dbc.Col([ + dbc.Label("Name"), + dcc.Input( + id="new_opportunity_name", + placeholder="Name of the opportunity", + type="text", + value="", + style={"width": "100%"}, + ), + ], width=6), + dbc.Col([ + dbc.Label("StageName"), + dcc.Dropdown( + id="new_opportunity_stage", + options=[ + { + "label": "Prospecting", + "value": "Prospecting", + }, + { + "label": "Qualification", + "value": "Qualification", + }, + { + "label": "Needs Analysis", + "value": "Needs Analysis", + }, + { + "label": "Value Proposition", + "value": "Value Proposition", + }, + { + "label": "Id. Decision Makers", + "value": "Closed", + }, + { + "label": "Perception Analysis", + "value": "Perception Analysis", + }, + { + "label": "Proposal/Price Quote", + "value": "Proposal/Price Quote", + }, + { + "label": "Negotiation/Review", + "value": "Negotiation/Review", + }, + { + "label": "Closed/Won", + "value": "Closed Won", + }, + { + "label": "Closed/Lost", + "value": "Closed Lost", + }, + ], + clearable=False, + value="Prospecting", + searchable=False, + ) + ], width=6), + dbc.Col([ + dbc.Label("Source"), + dcc.Dropdown( + id="new_opportunity_source", + options=["Web", "Phone Inquiry", "Partner Referral", "Purchased List", "Other"], + value="Web", + searchable=False, + ), + ], width=6), + dbc.Col([ + dbc.Label("Close Date"), + html.Div( + dcc.DatePickerSingle( + id="new_opportunity_date", + min_date_allowed=date.today(), + # max_date_allowed=dt(2017, 9, 19), + initial_visible_month=date.today(), + date=date.today(), + ), + style={"textAlign": "left"}, + ), + ], width=6), + dbc.Col([ + dbc.Label("Type"), + dcc.Dropdown( + id="new_opportunity_type", + options=["Existing Customer - Replacement", "New Customer", "Existing Customer - Upgrade", "Existing Customer - Downgrade"], + value="New Customer", + searchable=False, + ), + ], width=6), + dbc.Col([ + dbc.Label("Amount"), + dcc.Input( + id="new_opportunity_amount", + placeholder="0", + type="number", + value="", + style={"width": "100%"}, + ), + ], width=6), + dbc.Col([ + dbc.Label("Probability"), + dcc.Input( + id="new_opportunity_probability", + placeholder="0", + type="number", + max=100, + step=1, + value="", + style={"width": "100%"}, + ), + ], width=6), + ])), + dbc.ModalFooter(dbc.Button("Submit", id="submit_new_opportunity", className="ms-auto")), + ], + id="opportunities_modal", + is_open=False, +) + + \ No newline at end of file diff --git a/apps/dash-salesforce-crm/utils/figures.py b/apps/dash-salesforce-crm/utils/figures.py new file mode 100644 index 000000000..d01ddc945 --- /dev/null +++ b/apps/dash-salesforce-crm/utils/figures.py @@ -0,0 +1,350 @@ + +from plotly import graph_objs as go +import pandas as pd + + + + + +### Opportunity page graphs ### ### ### ### + + + + +def leads_choropleth_map(status, df): + if status == "open": + df = df[ + (df["Status"] == "Open - Not Contacted") + | (df["Status"] == "Working - Contacted") + ] + + elif status == "converted": + df = df[df["Status"] == "Closed - Converted"] + + elif status == "lost": + df = df[df["Status"] == "Closed - Not Converted"] + + df = df.groupby("State").count() + + scl = [[0.0, "rgb(38, 78, 134)"], [1.0, "#0091D5"]] # colors scale + + data = [ + dict( + type="choropleth", + colorscale=scl, + locations=df.index, + z=df["Id"], + locationmode="USA-states", + marker=dict(line=dict(color="rgb(255,255,255)", width=2)), + colorbar=dict(len=0.8), + ) + ] + + layout = dict( + autosize=True, + geo=dict( + scope="usa", + projection=dict(type="albers usa"), + lakecolor="rgb(255, 255, 255)", + ), + margin=dict(l=10, r=10, t=0, b=0), + ) + return dict(data=data, layout=layout) + + +def lead_source(status, df): + if status == "open": + df = df[ + (df["Status"] == "Open - Not Contacted") + | (df["Status"] == "Working - Contacted") + ] + + elif status == "converted": + df = df[df["Status"] == "Closed - Converted"] + + elif status == "lost": + df = df[df["Status"] == "Closed - Not Converted"] + + nb_leads = len(df.index) + types = df["LeadSource"].unique().tolist() + values = [] + + # compute % for each leadsource type + for case_type in types: + nb_type = df[df["LeadSource"] == case_type].shape[0] + values.append(nb_type / nb_leads * 100) + + trace = go.Pie( + labels=types, + values=values, + marker={"colors": ["#264e86", "#0074e4", "#74dbef", "#eff0f4"]}, + ) + + layout = dict(autosize=True, margin=dict(l=15, r=10, t=0, b=65)) + return dict(data=[trace], layout=layout) + + +def converted_leads_count(period, df): + df["CreatedDate"] = pd.to_datetime(df["CreatedDate"], format="%Y-%m-%d") + df = df[df["Status"] == "Closed - Converted"] + + df = ( + df.groupby([pd.Grouper(key="CreatedDate", freq=period)]) + .count() + .reset_index() + .sort_values("CreatedDate") + ) + + trace = go.Scatter( + x=df["CreatedDate"], + y=df["Id"], + name="converted leads", + fill="tozeroy", + fillcolor="#e6f2ff", + ) + + data = [trace] + + layout = go.Layout( + autosize=True, + xaxis=dict(showgrid=False), + margin=dict(l=33, r=25, b=37, t=5, pad=4), + paper_bgcolor="white", + plot_bgcolor="white", + ) + + return {"data": data, "layout": layout} + + + + + +### Opportunity page graphs ### ### ### ### + + + + +def converted_opportunities(period, source, df): + print(df) + + df["CreatedDate"] = pd.to_datetime(df["CreatedDate"], format="%Y-%m-%d") + + # source filtering + if source == "all_s": + df = df[df["IsWon"] == 1] + else: + df = df[(df["LeadSource"] == source) & (df["IsWon"] == 1)] + # period filtering + if period == "W-MON": + df["CreatedDate"] = pd.to_datetime(df["CreatedDate"]) - pd.to_timedelta( + 7, unit="d" + ) + df = ( + df.groupby([pd.Grouper(key="CreatedDate", freq=period)]) + .count() + .reset_index() + .sort_values("CreatedDate") + ) + + # if no results were found + if df.empty: + layout = dict( + autosize=True, annotations=[dict(text="No results found", showarrow=False)] + ) + return {"data": [], "layout": layout} + + trace = go.Scatter( + x=df["CreatedDate"], + y=df["IsWon"], + name="converted opportunities", + fill="tozeroy", + fillcolor="#e6f2ff", + ) + + data = [trace] + + layout = go.Layout( + autosize=True, + xaxis=dict(showgrid=False), + margin=dict(l=35, r=25, b=23, t=5, pad=4), + paper_bgcolor="white", + plot_bgcolor="white", + ) + + return {"data": data, "layout": layout} + + +def opportunities_heat_map_fig(df, stage): + df = df[pd.notnull(df["Type"])] + x, y, z = [], df["Type"].unique(), [] + + if stage == "all_s": + x = df["StageName"].unique() + elif stage == "cold": + x = ["Needs Analysis", "Prospecting", "Qualification"] + elif stage == "warm": + x = ["Value Proposition", "Id. Decision Makers", "Perception Analysis"] + else: + x = ["Proposal/Price Quote", "Negotiation/Review", "Closed Won"] + + for lead_type in y: + z_row = [] + for stage in x: + probability = df[(df["StageName"] == stage) & (df["Type"] == lead_type)][ + "Probability" + ].mean() + z_row.append(probability) + z.append(z_row) + + trace = dict( + type="heatmap", z=z, x=x, y=y, name="mean probability", colorscale="Blues" + ) + layout = dict( + autosize=True, + margin=dict(t=25, l=210, b=85, pad=4), + paper_bgcolor="white", + plot_bgcolor="white", + xaxis=dict(zeroline=False, showgrid=False), + yaxis=dict(zeroline=False, showgrid=False) + ) + + return go.Figure(data=[trace], layout=layout) + + + + + +### Cases page graphs ### ### ### ### + + + + +def cases_pie_chart(df, column, priority, origin, h_orientation=None): + df = df.dropna(subset=["Type", "Reason", "Origin"]) + nb_cases = len(df.index) + types = [] + values = [] + + # filter priority and origin + if priority == "all_p": + if origin == "all": + types = df[column].unique().tolist() + else: + types = df[df["Origin"] == origin][column].unique().tolist() + else: + if origin == "all": + types = df[df["Priority"] == priority][column].unique().tolist() + else: + types = ( + df[(df["Priority"] == priority) & (df["Origin"] == origin)][column] + .unique() + .tolist() + ) + + # if no results were found + if types == []: + layout = dict( + autosize=True, annotations=[dict(text="No results found", showarrow=False)] + ) + return {"data": [], "layout": layout} + + for case_type in types: + nb_type = df.loc[df[column] == case_type].shape[0] + values.append(nb_type / nb_cases * 100) + + layout = go.Layout( + autosize=True, + margin=dict(l=0, r=0, b=0, t=4, pad=8), + paper_bgcolor="white", + plot_bgcolor="white", + ) + + trace = go.Pie( + labels=types, + values=values, + marker={"colors": ["#264e86", "#0074e4", "#74dbef", "#eff0f4"]}, + ) + + if h_orientation: + layout["legend"]["orientation"] = "h" + + return {"data": [trace], "layout": layout} + + +def cases_by_period(df, period, priority): + df = df.dropna(subset=["Type", "Reason", "Origin"]) + stages = df["Type"].unique() + + # priority filtering + if priority != "all_p": + df = df[df["Priority"] == priority] + + # period filtering + df["CreatedDate"] = pd.to_datetime(df["CreatedDate"], format="%Y-%m-%d") + if period == "W-MON": + df["CreatedDate"] = pd.to_datetime(df["CreatedDate"]) - pd.to_timedelta( + 7, unit="d" + ) + df = df.groupby([pd.Grouper(key="CreatedDate", freq=period), "Type"]).count() + + dates = df.index.get_level_values("CreatedDate").unique() + dates = [str(i) for i in dates] + + co = { # colors for stages + "Electrical": "#264e86", + "Other": "#0074e4", + "Structural": "#74dbef", + "Mechanical": "#eff0f4", + "Electronic": "rgb(255, 127, 14)", + } + + data = [] + for stage in stages: + stage_rows = [] + for date in dates: + try: + row = df.loc[(date, stage)] + stage_rows.append(row["IsDeleted"]) + except Exception as e: + stage_rows.append(0) + + data_trace = go.Bar( + x=dates, y=stage_rows, name=stage, marker=dict(color=co[stage]) + ) + data.append(data_trace) + + layout = go.Layout( + autosize=True, + barmode="stack", + margin=dict(l=40, r=25, b=40, t=0, pad=4), + paper_bgcolor="white", + plot_bgcolor="white", + ) + + return {"data": data, "layout": layout} + + +def cases_by_account(cases, salesforce_manager): + accounts = salesforce_manager.get_accounts() + cases = cases.dropna(subset=["AccountId"]) + cases = pd.merge(cases, accounts, left_on="AccountId", right_on="Id") + cases = cases.groupby(["AccountId", "Name"]).count() + cases = cases.sort_values("IsDeleted") + data = [ + go.Bar( + y=cases.index.get_level_values("Name"), + x=cases["IsDeleted"], + orientation="h", + marker=dict(color="#0073e4"), + ) + ] # x could be any column value since its a count + + layout = go.Layout( + autosize=True, + barmode="stack", + margin=dict(l=210, r=25, b=20, t=0, pad=4), + paper_bgcolor="white", + plot_bgcolor="white", + ) + + return {"data": data, "layout": layout} diff --git a/apps/dash-salesforce-crm/utils/helper_functions.py b/apps/dash-salesforce-crm/utils/helper_functions.py new file mode 100644 index 000000000..d1286ce36 --- /dev/null +++ b/apps/dash-salesforce-crm/utils/helper_functions.py @@ -0,0 +1,36 @@ +from constants import millnames +import math + + +# returns most significant part of a number +def millify(n): + n = float(n) + millidx = max( + 0, + min( + len(millnames) - 1, int(math.floor(0 if n == 0 else math.log10(abs(n)) / 3)) + ), + ) + return "{:.0f}{}".format(n / 10 ** (3 * millidx), millnames[millidx]) + + + +# returns top 5 open opportunities +def top_open_opportunities(df): + df = df.sort_values("Amount", ascending=True) + cols = ["CreatedDate", "Name", "Amount", "StageName"] + df = df[cols].iloc[:5] + # only display 21 characters + df["Name"] = df["Name"].apply(lambda x: x[:30]) + return df.to_dict('records'), [{"name": i, "id": i} for i in df.columns] + + +# returns top 5 lost opportunities +def top_lost_opportunities(df): + df = df[df["StageName"] == "Closed Lost"] + cols = ["CreatedDate", "Name", "Amount", "StageName"] + df = df[cols].sort_values("Amount", ascending=False).iloc[:5] + # only display 21 characters + df["Name"] = df["Name"].apply(lambda x: x[:30]) + return df.to_dict('records'), [{"name": i, "id": i} for i in df.columns] + diff --git a/apps/dash-salesforce-crm/sfManager.py b/apps/dash-salesforce-crm/utils/salesforce_manager.py similarity index 99% rename from apps/dash-salesforce-crm/sfManager.py rename to apps/dash-salesforce-crm/utils/salesforce_manager.py index d9d916d31..bd6977d48 100644 --- a/apps/dash-salesforce-crm/sfManager.py +++ b/apps/dash-salesforce-crm/utils/salesforce_manager.py @@ -3,8 +3,7 @@ import pandas as pd import os - -class sf_Manager: +class SalesforceManager: def __init__(self): # Create a free SalesForce account: https://developer.salesforce.com/signup self.sf = Salesforce( diff --git a/apps/dash-svm/README.md b/apps/dash-svm/README.md index abd26f12d..57e73c28a 100644 --- a/apps/dash-svm/README.md +++ b/apps/dash-svm/README.md @@ -64,6 +64,7 @@ An SVM is a popular Machine Learning model used in many different fields. You ca * **Matthew Chan** - *Code Review* - [@matthewchan15](https://github.com/matthewchan15) * **Yunke Xiao** - *Redesign* - [@YunkeXiao](https://github.com/YunkeXiao) * **celinehuang** - *Code Review* - [@celinehuang](https://github.com/celinehuang) +* **Daniel Anton S** - *2022 complete refactor* - [@danton267](https://github.com/danton267) ## Acknowledgments