116 lines
3.9 KiB
Python
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),
|
|
])
|