Add per-employee dept breakdown via modal popup

Stores per-employee shift costs in wage_actuals_detail (APPROVED shifts
only). Sync fetches employee name map from /api/v2/users alongside dept
names. Clicking any dept row in Weekly or Monthly opens a modal showing
Employee · Shifts · Cost · % of Dept, fetched on demand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-23 11:02:11 +00:00
parent 91936ab131
commit 5372111f52
9 changed files with 289 additions and 16 deletions

View file

@ -3,8 +3,9 @@ import { ChevronLeft, ChevronRight, ChevronDown, Download } from 'lucide-react'
import {
BarChart, Bar, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, Cell,
} from 'recharts'
import { getActuals, getScheduled, getNetSales, getBudgets, downloadExport } from '../api'
import type { DeptActuals, WageBudget } from '../types'
import { getActuals, getScheduled, getNetSales, getBudgets, getDeptDetail, downloadExport } from '../api'
import type { DeptActuals, WageBudget, EmployeeDetail } from '../types'
import { DeptDetailModal } from '../components/DeptDetailModal'
function fmt(d: Date): string {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
@ -36,6 +37,9 @@ export default function Monthly() {
const [showOncosts, setShowOncosts] = useState(true)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [modal, setModal] = useState<{ deptId: string; deptName: string } | null>(null)
const [modalEmps, setModalEmps] = useState<EmployeeDetail[] | null>(null)
const [modalLoad, setModalLoad] = useState(false)
const dim = daysInMonth(year, month)
const monthStr = `${year}-${String(month).padStart(2, '0')}`
@ -97,6 +101,15 @@ export default function Monthly() {
useEffect(() => { load() }, [load])
useEffect(() => {
if (!modal) { setModalEmps(null); return }
setModalLoad(true)
getDeptDetail(modal.deptId, fromStr, isCurrentMonth ? todayStr : toStr)
.then(r => setModalEmps(r.employees))
.catch(() => setModalEmps([]))
.finally(() => setModalLoad(false))
}, [modal, fromStr, toStr, todayStr, isCurrentMonth])
const prev = () => { if (month === 1) { setYear(y => y - 1); setMonth(12) } else { setMonth(m => m - 1) } }
const next = () => { if (month === 12) { setYear(y => y + 1); setMonth(1) } else { setMonth(m => m + 1) } }
@ -287,7 +300,8 @@ export default function Monthly() {
const dv = depBudg != null ? displayCost - depBudg : null
const depOfTotal = totalDisplay > 0 ? (displayCost / totalDisplay) * 100 : null
return (
<tr key={dep.department_id}>
<tr key={dep.department_id} style={{ cursor: 'pointer' }}
onClick={() => setModal({ deptId: dep.department_id, deptName: dep.department_name })}>
<td><span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: '50%', background: dep.color, marginRight: 8 }} />{dep.department_name}</td>
<td className="right">{fmtMoney(dep.actual_mtd)}</td>
{isCurrentMonth && <td className="right">{fmtMoney(dep.forecast_eom)}</td>}
@ -366,6 +380,16 @@ export default function Monthly() {
)}
</>
)}
{modal && (
<DeptDetailModal
deptName={modal.deptName}
period={monthLabel}
employees={modalEmps}
loading={modalLoad}
onClose={() => setModal(null)}
/>
)}
</div>
)
}