Add vs Prev column to monthly table; weekly two-row cards + vs PY + vs Prev Wk
Monthly: fetches previous month actuals; adds % delta vs prior month full alongside existing vs PY column in the dept breakdown table. Weekly: expanded 14-day actuals fetch (prev+current week) so prior-week per-dept data is available for both forecast and comparison. Two-row summary cards for current week (WTD actuals + full-week forecast). Past weeks keep single-row layout. Dept table gains vs PY and vs Prev Wk delta columns; budget columns use WTD pro-rata for current week, 7-day proportion for past. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
27f9190dc5
commit
3a068ece23
2 changed files with 275 additions and 175 deletions
|
|
@ -42,8 +42,9 @@ export default function Monthly() {
|
|||
const [pyWages, setPyWages] = useState<number | null>(null)
|
||||
const [pyWagesFull, setPyWagesFull] = useState<number | null>(null)
|
||||
const [pyDepts, setPyDepts] = useState<{id: string; name: string; cost: number; costFull: number}[]>([])
|
||||
const [pyTableOpen, setPyTableOpen] = useState(false)
|
||||
const [budget, setBudget] = useState<number | null>(null)
|
||||
const [pyTableOpen, setPyTableOpen] = useState(false)
|
||||
const [prevMonthDepts, setPrevMonthDepts] = useState<{id: string; cost: number}[]>([])
|
||||
const [budget, setBudget] = useState<number | null>(null)
|
||||
const [deptPcts, setDeptPcts] = useState<Record<string, number>>({})
|
||||
const [showOncosts, setShowOncosts] = useState(true)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
|
@ -63,15 +64,23 @@ export default function Monthly() {
|
|||
const pyFromStr = `${year - 1}-${String(month).padStart(2, '0')}-01`
|
||||
const pyToStr = `${year - 1}-${String(month).padStart(2, '0')}-${String(pyDim).padStart(2, '0')}`
|
||||
|
||||
// Previous month: always fetch full month for comparison
|
||||
const pmYear = month === 1 ? year - 1 : year
|
||||
const pmMonth = month === 1 ? 12 : month - 1
|
||||
const pmDim = daysInMonth(pmYear, pmMonth)
|
||||
const pmFromStr = `${pmYear}-${String(pmMonth).padStart(2, '0')}-01`
|
||||
const pmToStr = `${pmYear}-${String(pmMonth).padStart(2, '0')}-${String(pmDim).padStart(2, '0')}`
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true); setError(null)
|
||||
try {
|
||||
// Net sales: full month — OTB/forecast for future dates, actuals for past dates
|
||||
const [actRes, salesRes, budRes, pyActRes] = await Promise.all([
|
||||
const [actRes, salesRes, budRes, pyActRes, pmActRes] = await Promise.all([
|
||||
getActuals(fromStr, toStr),
|
||||
getNetSales(fromStr, toStr),
|
||||
getBudgets(),
|
||||
getActuals(pyFromStr, pyToStr),
|
||||
getActuals(pmFromStr, pmToStr),
|
||||
])
|
||||
setDepts(actRes.departments)
|
||||
setShowOncosts(actRes.show_oncosts)
|
||||
|
|
@ -104,12 +113,19 @@ export default function Monthly() {
|
|||
|
||||
const bRow = budRes.budgets.find((b: WageBudget) => b.month === fromStr)
|
||||
setBudget(bRow ? bRow.budget_amount : null)
|
||||
|
||||
// Previous month per-dept totals (full month) for vs Prev column
|
||||
const pmDeptList = pmActRes.departments.map(dep => ({
|
||||
id: dep.department_id,
|
||||
cost: Object.values(dep.days).reduce((s, v) => s + v.cost, 0),
|
||||
}))
|
||||
setPrevMonthDepts(pmDeptList)
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to load')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [fromStr, toStr, pyFromStr, pyToStr, isCurrentMonth, yesterdayStr, year, month, pyDim])
|
||||
}, [fromStr, toStr, pyFromStr, pyToStr, pmFromStr, pmToStr, isCurrentMonth, yesterdayStr, year, month, pyDim])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
|
|
@ -157,6 +173,7 @@ export default function Monthly() {
|
|||
const totalForecast = deptSummary.reduce((s, d) => s + d.forecast_eom, 0)
|
||||
const pyTotalWages = pyDepts.reduce((s, d) => s + d.cost, 0)
|
||||
const pyTotalWagesFull = pyDepts.reduce((s, d) => s + d.costFull, 0)
|
||||
const prevMonthTotal = prevMonthDepts.reduce((s, d) => s + d.cost, 0)
|
||||
|
||||
// MTD row: pro-rata budget to yesterday
|
||||
const yDay = isCurrentMonth ? parseInt(yesterdayStr.slice(8)) : dim
|
||||
|
|
@ -351,6 +368,7 @@ export default function Monthly() {
|
|||
{isCurrentMonth && <th className="right">Forecast → EOM</th>}
|
||||
<th className="right">% of Total</th>
|
||||
<th className="right">vs PY</th>
|
||||
<th className="right">vs Prev</th>
|
||||
<th className="right">Budget</th>
|
||||
<th className="right">% Budget</th>
|
||||
<th className="right">Variance</th>
|
||||
|
|
@ -370,6 +388,10 @@ export default function Monthly() {
|
|||
const pyDept = pyDepts.find(p => p.id === dep.department_id)
|
||||
const pyCost = pyDept ? (isCurrentMonth ? pyDept.cost : pyDept.costFull) : null
|
||||
const pyDelta = pyCost != null && pyCost > 0 ? ((dep.actual_mtd - pyCost) / pyCost) * 100 : null
|
||||
// Prev month: compare actual_mtd vs prev month full total per dept
|
||||
const pmDept = prevMonthDepts.find(p => p.id === dep.department_id)
|
||||
const pmCost = pmDept && pmDept.cost > 0 ? pmDept.cost : null
|
||||
const pmDelta = pmCost != null ? ((dep.actual_mtd - pmCost) / pmCost) * 100 : null
|
||||
return (
|
||||
<tr key={dep.department_id} style={{ cursor: 'pointer' }}
|
||||
onClick={() => setModal({ deptId: dep.department_id, deptName: dep.department_name })}>
|
||||
|
|
@ -387,6 +409,11 @@ export default function Monthly() {
|
|||
? <span className={pyDelta > 0 ? 'variance-over' : 'variance-under'}>{pyDelta > 0 ? '+' : ''}{pyDelta.toFixed(1)}%</span>
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="right">
|
||||
{pmDelta != null
|
||||
? <span className={pmDelta > 0 ? 'variance-over' : 'variance-under'}>{pmDelta > 0 ? '+' : ''}{pmDelta.toFixed(1)}%</span>
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="right">{depBudg != null ? fmtMoney(depBudg) : '—'}</td>
|
||||
<td className="right">
|
||||
{dp != null ? <span className={`pct-badge ${pctClass(dp)}`}>{fmtDelta(dp)}</span> : '—'}
|
||||
|
|
@ -410,6 +437,13 @@ export default function Monthly() {
|
|||
return <span className={d > 0 ? 'variance-over' : 'variance-under'}>{d > 0 ? '+' : ''}{d.toFixed(1)}%</span>
|
||||
})()}
|
||||
</td>
|
||||
<td className="right">
|
||||
{(() => {
|
||||
if (prevMonthTotal <= 0) return '—'
|
||||
const d = ((totalActual - prevMonthTotal) / prevMonthTotal) * 100
|
||||
return <span className={d > 0 ? 'variance-over' : 'variance-under'}>{d > 0 ? '+' : ''}{d.toFixed(1)}%</span>
|
||||
})()}
|
||||
</td>
|
||||
<td className="right">{budget != null ? fmtMoney(budget) : '—'}</td>
|
||||
<td className="right">
|
||||
{pctBudgFull != null ? <span className={`pct-badge ${pctClass(pctBudgFull)}`}>{fmtDelta(pctBudgFull)}</span> : '—'}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue