AdminAnalytics/src/admin_analytics/dashboard/pages/headcount.py
2026-03-30 20:42:08 -04:00

116 lines
3.9 KiB
Python

"""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),
])