mirror of
https://github.com/Hestia-Homes/insight.git
synced 2026-06-08 11:17:25 +00:00
added new files
This commit is contained in:
parent
d5d9bad73a
commit
4f992e51eb
5 changed files with 454 additions and 0 deletions
0
backend/__init__.py
Normal file
0
backend/__init__.py
Normal file
0
backend/src/__init__.py
Normal file
0
backend/src/__init__.py
Normal file
27
backend/src/dashboard/app.py
Normal file
27
backend/src/dashboard/app.py
Normal 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)
|
||||
302
backend/src/dashboard/pages/planned_vs_complete.py
Normal file
302
backend/src/dashboard/pages/planned_vs_complete.py
Normal 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
|
||||
125
backend/src/dashboard/pages/sales_forecast.py
Normal file
125
backend/src/dashboard/pages/sales_forecast.py
Normal 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
|
||||
Loading…
Add table
Reference in a new issue