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:
parent
91936ab131
commit
5372111f52
9 changed files with 289 additions and 16 deletions
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue