added new files

This commit is contained in:
Jun-te Kim 2025-12-10 17:30:56 +00:00
parent d5d9bad73a
commit 4f992e51eb
5 changed files with 454 additions and 0 deletions

0
backend/__init__.py Normal file
View file

0
backend/src/__init__.py Normal file
View file

View file

@ -0,0 +1,27 @@
# app.py
from dash import Dash, html, dcc
import dash
import dash_bootstrap_components as dbc
app = Dash(
__name__,
use_pages=True,
external_stylesheets=[dbc.themes.BOOTSTRAP],
)
server = app.server
app.layout = dbc.Container([
html.H1("Welcome to DomnaInsights", className="text-center my-4"),
# Navigation bar
dbc.Nav([
dbc.NavLink("Planned vs Completed", href="/", active="exact"),
dbc.NavLink("Sales Forecast", href="/sales-forecast", active="exact"),
], pills=True, justified=True, className="mb-4"),
dash.page_container # <-- Page content loads here
], fluid=True)
if __name__ == "__main__":
app.run(debug=True)

View file

@ -0,0 +1,302 @@
# pages/planned_vs_completed.py
import dash
from dash import html, dcc, dash_table, Input, Output, State, ctx
import dash_bootstrap_components as dbc
import pandas as pd
from datetime import datetime, timedelta
import json
import os
# from backend.src.dashboard.services.file_manager import FileManager
# from backend.src.dashboard.services.json_reader import jsonReader
# from backend.src.dashboard.components.pivot_charts import (
# build_pivot_tables_and_charts,
# week_start_monday,
# )
from dashboard.services.file_manager import FileManager
from dashboard.services.json_reader import jsonReader
from dashboard.components.pivot_charts import (
build_pivot_tables_and_charts,
week_start_monday,
)
# -----------------------------------------------------
# Register Page
# -----------------------------------------------------
dash.register_page(__name__, path="/", name="Planned vs Completed")
SAFE_DELIM = "\\\\"
# -----------------------------------------------------
# Helper: Current Monday
# -----------------------------------------------------
def current_week_start():
today = datetime.today()
monday = today - timedelta(days=today.weekday())
return monday.strftime("%Y-%m-%d")
# -----------------------------------------------------
# Load & Build Master DF
# -----------------------------------------------------
def build_master_df(local=False):
if local is False:
s3 = FileManager()
key, path, data = s3.download_and_read_latest()
else:
file_path = os.path.join(os.path.dirname(__file__), "data.json")
with open(file_path, "r") as f:
data = json.load(f)
hubspot_data = jsonReader(data)
frames = []
for p in hubspot_data.line_item_names:
df = hubspot_data.generate_df_via_product_type(p)
if df is None or df.empty:
continue
df["product_type"] = p
df["price"] = pd.to_numeric(df["price"], errors="coerce").fillna(0)
df["Planned Week"] = df["expected_commencement_date"].apply(week_start_monday)
df["raw_completed_week"] = df.get("submission_date", None)
df["raw_completed_week"] = df["raw_completed_week"].apply(week_start_monday)
# corrected completed week logic
def corrected(row):
planned = row["Planned Week"]
submitted = row["raw_completed_week"]
if not submitted:
return None
if not planned:
return submitted
return planned if submitted > planned else submitted
df["Completed Week"] = df.apply(corrected, axis=1)
df.drop(columns=["raw_completed_week"], inplace=True)
frames.append(df)
return pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
# Load data once (refresh button can rebuild)
df = build_master_df()
# -----------------------------------------------------
# Page Layout
# -----------------------------------------------------
layout = html.Div([
html.H1("Planned vs Completed — Pivot Tables + Charts",
style={"textAlign": "center"}),
# ---------------- FILTERS ----------------
dcc.Dropdown(
id="date-filter",
options=[{"label": "All Dates", "value": "All Dates"}] +
[{"label": d, "value": d}
for d in sorted(df["Planned Week"].dropna().unique())],
value=current_week_start()
if current_week_start() in df["Planned Week"].unique()
else "All Dates",
clearable=False,
style={"width": "300px", "margin": "20px auto"},
),
html.Button("Refresh Data", id="refresh-btn", n_clicks=0),
html.Hr(),
# ---------------- JOBS TABLE ----------------
html.H2("Jobs Pivot Table", style={"textAlign": "center"}),
dash_table.DataTable(
id="jobs-table",
page_size=40,
sort_action="native",
cell_selectable=True,
style_table={"overflowX": "scroll", "maxWidth": "98%", "margin": "0 auto"},
style_cell={"textAlign": "center", "minWidth": "80px", "padding": "6px"},
style_cell_conditional=[
{"if": {"column_id": "Product Type"},
"textAlign": "left",
"fontWeight": "bold",
"minWidth": "150px"},
],
style_header={"fontWeight": "bold", "backgroundColor": "#f5f5f5"},
style_data_conditional=[
{
"if": {"filter_query": "{Product Type} = 'TOTAL'"},
"fontWeight": "bold",
"backgroundColor": "#f0f0f0",
}
],
),
html.Hr(),
# ---------------- REVENUE TABLE ----------------
html.H2("Revenue (£) Pivot Table", style={"textAlign": "center"}),
dash_table.DataTable(
id="revenue-table",
page_size=40,
sort_action="native",
cell_selectable=True,
style_table={"overflowX": "scroll", "maxWidth": "98%", "margin": "0 auto"},
style_cell={"textAlign": "center", "minWidth": "80px", "padding": "6px"},
style_cell_conditional=[
{"if": {"column_id": "Product Type"},
"textAlign": "left",
"fontWeight": "bold",
"minWidth": "150px"},
],
style_header={"fontWeight": "bold", "backgroundColor": "#f5f5f5"},
style_data_conditional=[
{
"if": {"filter_query": "{Product Type} = 'TOTAL'"},
"fontWeight": "bold",
"backgroundColor": "#f0f0f0",
}
],
),
html.Hr(),
# ---------------- CHARTS ----------------
html.H2("Jobs Line Chart", style={"textAlign": "center"}),
dcc.Graph(id="jobs-graph", style={"height": "400px"}),
html.Hr(),
html.H2("Revenue Line Chart (£)", style={"textAlign": "center"}),
dcc.Graph(id="revenue-graph", style={"height": "400px"}),
html.Hr(),
# ---------------- MODAL ----------------
dbc.Modal(
[
dbc.ModalHeader("HubSpot IDs"),
dbc.ModalBody(id="modal-body"),
dbc.ModalFooter(
dbc.Button("Close", id="close-modal", className="ms-auto")
),
],
id="hubspot-modal",
size="lg",
is_open=False,
),
])
# -----------------------------------------------------
# Callback: Update tables + charts
# -----------------------------------------------------
@dash.callback(
Output("jobs-table", "data"),
Output("jobs-table", "columns"),
Output("revenue-table", "data"),
Output("revenue-table", "columns"),
Output("jobs-graph", "figure"),
Output("revenue-graph", "figure"),
Input("date-filter", "value"),
Input("refresh-btn", "n_clicks"),
)
def update_outputs(selected_week, n_clicks):
global df
if n_clicks > 0:
df = build_master_df()
return build_pivot_tables_and_charts(df, selected_week)
# -----------------------------------------------------
# Modal: Display HubSpot IDs when clicking a cell
# -----------------------------------------------------
def id_to_link(deal_id):
url = f"https://app.hubspot.com/contacts/145275138/record/0-3/{deal_id}"
match = df.loc[df["hubspot_id"].astype(str) == str(deal_id)]
return html.Li(html.A(match.iloc[0].get("deal_name"), href=url, target="_blank"))
@dash.callback(
Output("hubspot-modal", "is_open"),
Output("modal-body", "children"),
Output("jobs-table", "active_cell"),
Output("revenue-table", "active_cell"),
Input("jobs-table", "active_cell"),
Input("revenue-table", "active_cell"),
Input("close-modal", "n_clicks"),
State("jobs-table", "data"),
State("revenue-table", "data"),
State("hubspot-modal", "is_open"),
)
def open_modal(jobs_cell, revenue_cell, close_click, jobs_data, revenue_data, is_open):
triggered = ctx.triggered_id
# -------------------------
# CLOSE THE MODAL
# -------------------------
if triggered == "close-modal":
return False, "", None, None
# -------------------------
# Helper: renderer for modal content
# -------------------------
def build_modal(row, col_id):
if col_id == "Product Type":
return html.P("This column has no IDs.")
parts = col_id.split(" ")
# Jobs table style → 2025-02-05_Planned
if "_" in parts[0]:
week = parts[0].split("_")[0]
else:
# Revenue table style → 2025-02-05 Planned £
week = parts[0]
label = col_id.lower()
is_planned = "planned" in label
id_key = f"{week}_planned_ids" if is_planned else f"{week}_actual_ids"
raw_ids = row.get(id_key, "")
if not raw_ids:
return html.P("No IDs recorded for this cell.")
ids = raw_ids.split(SAFE_DELIM)
seen = set()
return html.Ul([id_to_link(d) for d in ids if not (d in seen or seen.add(d))])
# -------------------------
# JOBS TABLE CLICK
# -------------------------
if triggered == "jobs-table" and jobs_cell:
row = jobs_data[jobs_cell["row"]]
col_id = jobs_cell["column_id"]
return True, build_modal(row, col_id), None, None
# -------------------------
# REVENUE TABLE CLICK
# -------------------------
if triggered == "revenue-table" and revenue_cell:
row = revenue_data[revenue_cell["row"]]
col_id = revenue_cell["column_id"]
return True, build_modal(row, col_id), None, None
# -------------------------
# DEFAULT
# -------------------------
return is_open, "", None, None

View file

@ -0,0 +1,125 @@
# pages/sales_forecast.py
import dash
from dash import html, dcc, dash_table, Input, Output
import dash_bootstrap_components as dbc
import pandas as pd
from datetime import datetime
from dashboard.services.file_manager import FileManager
from dashboard.services.json_reader import jsonReader
from dashboard.components.pivot_charts import week_start_monday
dash.register_page(__name__, path="/sales-forecast", name="Sales Forecast")
# -----------------------
# Load base dataframe
# -----------------------
df = pd.DataFrame({
"hubspot_id": [1, 2, 3, 4, 5, 6],
"product_type": ["Solar", "Cavity", "Solar", "Loft", "Cavity", "Solar"],
"price": [5000, 1200, 7000, 800, 2200, 6500],
"Planned Week": [
"2025-01-06",
"2025-01-06",
"2025-01-13",
"2025-01-13",
"2025-01-20",
"2025-01-20",
]
})
# -----------------------
# Page Layout
# -----------------------
layout = html.Div([
html.H1("Sales Forecast", className="text-center"),
html.P(
"This page projects expected revenue and job volume into future weeks "
"based on existing HubSpot data.",
className="text-center text-muted"
),
html.Hr(),
dcc.Dropdown(
id="forecast-product-filter",
options=[{"label": p, "value": p} for p in sorted(df["product_type"].unique())],
multi=True,
placeholder="Filter by product type…",
style={"width": "400px", "margin": "0 auto"},
),
html.Br(),
dash_table.DataTable(
id="forecast-table",
page_size=20,
style_table={"overflowX": "auto"},
style_cell={"textAlign": "center"},
),
html.Hr(),
html.H2("Forecasted Revenue (£)", className="text-center"),
dcc.Graph(id="forecast-revenue-graph"),
html.H2("Forecasted Job Volume", className="text-center mt-4"),
dcc.Graph(id="forecast-volume-graph"),
])
# -----------------------
# Callbacks
# -----------------------
@dash.callback(
Output("forecast-table", "data"),
Output("forecast-table", "columns"),
Output("forecast-revenue-graph", "figure"),
Output("forecast-volume-graph", "figure"),
Input("forecast-product-filter", "value"),
)
def build_forecast(products):
df_filtered = df.copy()
if products:
df_filtered = df_filtered[df_filtered["product_type"].isin(products)]
# ----------------------------------------
# Basic aggregation per week (extend later)
# ----------------------------------------
weekly = df_filtered.groupby("Planned Week").agg(
jobs=("hubspot_id", "count"),
revenue=("price", "sum")
).reset_index()
weekly = weekly.sort_values("Planned Week")
# ----------------------------------------
# TABLE
# ----------------------------------------
columns = [{"name": c, "id": c} for c in weekly.columns]
data = weekly.to_dict("records")
# ----------------------------------------
# GRAPHS
# ----------------------------------------
import plotly.express as px
revenue_fig = px.line(
weekly,
x="Planned Week",
y="revenue",
title="Expected Revenue per Week"
)
volume_fig = px.line(
weekly,
x="Planned Week",
y="jobs",
title="Expected Job Count per Week"
)
return data, columns, revenue_fig, volume_fig