diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/__init__.py b/backend/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/dashboard/app.py b/backend/src/dashboard/app.py new file mode 100644 index 0000000..46770f8 --- /dev/null +++ b/backend/src/dashboard/app.py @@ -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) diff --git a/backend/src/dashboard/pages/planned_vs_complete.py b/backend/src/dashboard/pages/planned_vs_complete.py new file mode 100644 index 0000000..f08c412 --- /dev/null +++ b/backend/src/dashboard/pages/planned_vs_complete.py @@ -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 diff --git a/backend/src/dashboard/pages/sales_forecast.py b/backend/src/dashboard/pages/sales_forecast.py new file mode 100644 index 0000000..7aefc06 --- /dev/null +++ b/backend/src/dashboard/pages/sales_forecast.py @@ -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