Phase 1 project prototype
This commit is contained in:
parent
29215e2bd2
commit
2c9ae1c312
29 changed files with 2967 additions and 22 deletions
118
src/admin_analytics/dashboard/pages/headcount.py
Normal file
118
src/admin_analytics/dashboard/pages/headcount.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
"""Page 4: Current Admin Headcount (from scraper)."""
|
||||
|
||||
import duckdb
|
||||
from dash import html, dcc, dash_table
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
|
||||
from admin_analytics.dashboard.queries import (
|
||||
query_admin_headcount,
|
||||
query_headcount_summary,
|
||||
)
|
||||
|
||||
_NO_DATA = html.Div(
|
||||
"No headcount data loaded. Run: admin-analytics ingest scrape",
|
||||
style={"textAlign": "center", "padding": "40px", "color": "#888"},
|
||||
)
|
||||
|
||||
|
||||
def _kpi_card(title: str, value: str) -> html.Div:
|
||||
return html.Div(
|
||||
[
|
||||
html.H4(title, style={"margin": "0", "color": "#666", "fontSize": "14px"}),
|
||||
html.H2(value, style={"margin": "5px 0", "color": "#00539F"}),
|
||||
],
|
||||
style={
|
||||
"flex": "1",
|
||||
"padding": "20px",
|
||||
"backgroundColor": "#f8f9fa",
|
||||
"borderRadius": "8px",
|
||||
"textAlign": "center",
|
||||
"margin": "0 8px",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def layout(conn: duckdb.DuckDBPyConnection):
|
||||
detail_df = query_admin_headcount(conn)
|
||||
if detail_df.height == 0:
|
||||
return _NO_DATA
|
||||
|
||||
summary_df = query_headcount_summary(conn)
|
||||
detail_pd = detail_df.to_pandas()
|
||||
summary_pd = summary_df.to_pandas()
|
||||
|
||||
total = len(detail_pd)
|
||||
overhead_count = int(detail_pd["is_overhead"].sum()) if "is_overhead" in detail_pd.columns else 0
|
||||
overhead_pct = round(overhead_count * 100 / total, 1) if total > 0 else 0
|
||||
|
||||
# KPI cards
|
||||
kpi_row = html.Div(
|
||||
[
|
||||
_kpi_card("Total Staff Scraped", str(total)),
|
||||
_kpi_card("Overhead Staff", str(overhead_count)),
|
||||
_kpi_card("Overhead %", f"{overhead_pct}%"),
|
||||
],
|
||||
style={"display": "flex", "marginBottom": "24px"},
|
||||
)
|
||||
|
||||
# Staff by unit bar chart
|
||||
unit_counts = summary_pd.groupby("unit")["count"].sum().reset_index().sort_values("count")
|
||||
unit_fig = px.bar(
|
||||
unit_counts, x="count", y="unit", orientation="h",
|
||||
title="Staff Count by Unit",
|
||||
labels={"count": "Staff", "unit": ""},
|
||||
color_discrete_sequence=["#00539F"],
|
||||
)
|
||||
unit_fig.update_layout(template="plotly_white", height=max(300, len(unit_counts) * 30 + 100))
|
||||
|
||||
# Overhead pie
|
||||
oh_data = detail_pd["is_overhead"].value_counts()
|
||||
oh_labels = {True: "Overhead", False: "Non-Overhead"}
|
||||
pie_fig = px.pie(
|
||||
names=[oh_labels.get(k, "Debatable") for k in oh_data.index],
|
||||
values=oh_data.values,
|
||||
title="Overhead vs Non-Overhead",
|
||||
color_discrete_sequence=["#E07A5F", "#7FB069", "#999"],
|
||||
)
|
||||
pie_fig.update_layout(template="plotly_white", height=350)
|
||||
|
||||
# Category distribution per unit
|
||||
cat_fig = px.bar(
|
||||
summary_pd, x="count", y="unit", color="category", orientation="h",
|
||||
title="Category Distribution by Unit",
|
||||
labels={"count": "Staff", "unit": "", "category": "Category"},
|
||||
)
|
||||
cat_fig.update_layout(template="plotly_white", height=max(300, len(unit_counts) * 30 + 100))
|
||||
|
||||
# Detail table
|
||||
table = dash_table.DataTable(
|
||||
columns=[
|
||||
{"name": "Unit", "id": "unit"},
|
||||
{"name": "Name", "id": "person_name"},
|
||||
{"name": "Title", "id": "title"},
|
||||
{"name": "Category", "id": "category"},
|
||||
{"name": "Overhead", "id": "is_overhead"},
|
||||
],
|
||||
data=detail_pd.to_dict("records"),
|
||||
page_size=20,
|
||||
sort_action="native",
|
||||
filter_action="native",
|
||||
style_table={"overflowX": "auto"},
|
||||
style_cell={"textAlign": "left", "padding": "8px", "fontSize": "13px"},
|
||||
style_header={"fontWeight": "bold", "backgroundColor": "#f0f0f0"},
|
||||
)
|
||||
|
||||
return html.Div([
|
||||
kpi_row,
|
||||
html.Div(
|
||||
[
|
||||
html.Div(dcc.Graph(figure=unit_fig), style={"flex": "1"}),
|
||||
html.Div(dcc.Graph(figure=pie_fig), style={"flex": "1"}),
|
||||
],
|
||||
style={"display": "flex", "gap": "16px"},
|
||||
),
|
||||
dcc.Graph(figure=cat_fig),
|
||||
html.H3("Staff Directory Detail", style={"marginTop": "24px"}),
|
||||
table,
|
||||
])
|
||||
Loading…
Add table
Add a link
Reference in a new issue