Add PY wages/sales cards + dept % of total column
- Weekly/Monthly: PY Wages summary card (fetches equivalent week/month last year) - Weekly/Monthly: PY Net Sales as its own card (was a sub-label, now prominent) - Weekly/Monthly: dept table gains '% of Total' column - Monthly: PY comparison caps at same day-of-month for fair PYTD comparison Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
605feaab4e
commit
1d7cb229e4
2 changed files with 78 additions and 19 deletions
|
|
@ -27,6 +27,8 @@ export default function Monthly() {
|
|||
const [depts, setDepts] = useState<DeptActuals[]>([])
|
||||
const [scheduled, setScheduled] = useState<Record<string, Record<string, number>>>({})
|
||||
const [netSales, setNetSales] = useState(0)
|
||||
const [pySales, setPySales] = useState(0)
|
||||
const [pyWages, setPyWages] = useState<number | null>(null)
|
||||
const [budget, setBudget] = useState<number | null>(null)
|
||||
const [deptPcts, setDeptPcts] = useState<Record<string, number>>({})
|
||||
const [showOncosts, setShowOncosts] = useState(true)
|
||||
|
|
@ -38,19 +40,24 @@ export default function Monthly() {
|
|||
const fromStr = `${monthStr}-01`
|
||||
const toStr = `${monthStr}-${String(dim).padStart(2, '0')}`
|
||||
const isCurrentMonth = year === todayYear && month === todayMonth
|
||||
// For net sales: cap at today for current month, use month-end for past months
|
||||
const salesTo = isCurrentMonth ? todayStr : toStr
|
||||
// For scheduled: only relevant if month includes future dates
|
||||
const salesTo = isCurrentMonth ? todayStr : toStr
|
||||
const schedFrom = todayStr < toStr ? todayStr : toStr
|
||||
// Prior-year equivalent period
|
||||
const pyDim = daysInMonth(year - 1, month)
|
||||
const pyFromStr = `${year - 1}-${String(month).padStart(2, '0')}-01`
|
||||
const pyToStr = isCurrentMonth
|
||||
? `${year - 1}-${String(month).padStart(2, '0')}-${String(Math.min(parseInt(todayStr.slice(8)), pyDim)).padStart(2, '0')}`
|
||||
: `${year - 1}-${String(month).padStart(2, '0')}-${String(pyDim).padStart(2, '0')}`
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true); setError(null)
|
||||
try {
|
||||
const [actRes, schRes, salesRes, budRes] = await Promise.all([
|
||||
const [actRes, schRes, salesRes, budRes, pyActRes] = await Promise.all([
|
||||
getActuals(fromStr, toStr),
|
||||
getScheduled(schedFrom, toStr),
|
||||
getNetSales(fromStr, salesTo), // salesTo caps at today for current month
|
||||
getNetSales(fromStr, salesTo),
|
||||
getBudgets(),
|
||||
getActuals(pyFromStr, pyToStr),
|
||||
])
|
||||
setDepts(actRes.departments)
|
||||
setShowOncosts(actRes.show_oncosts)
|
||||
|
|
@ -66,6 +73,12 @@ export default function Monthly() {
|
|||
setScheduled(schMap)
|
||||
|
||||
setNetSales(salesRes.days.reduce((s, d) => s + d.net_sales, 0))
|
||||
setPySales(salesRes.days.reduce((s, d) => s + d.py_sales, 0))
|
||||
|
||||
const pyTotal = pyActRes.departments.reduce(
|
||||
(s, dep) => s + Object.values(dep.days).reduce((ds, d) => ds + d.cost, 0), 0
|
||||
)
|
||||
setPyWages(pyTotal > 0 ? pyTotal : null)
|
||||
|
||||
const bRow = budRes.budgets.find((b: WageBudget) => b.month === fromStr)
|
||||
setBudget(bRow ? bRow.budget_amount : null)
|
||||
|
|
@ -74,7 +87,7 @@ export default function Monthly() {
|
|||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [fromStr, toStr, schedFrom, salesTo])
|
||||
}, [fromStr, toStr, schedFrom, salesTo, pyFromStr, pyToStr])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
|
|
@ -118,6 +131,7 @@ export default function Monthly() {
|
|||
const pctBudget = budget != null && budget > 0 ? (totalForecast / budget) * 100 : null
|
||||
const pctSales = netSales > 0 ? (totalActual / netSales) * 100 : null
|
||||
const variance = budget != null ? totalForecast - budget : null
|
||||
const pyWagesDiff = pyWages != null && totalActual > 0 ? totalActual - pyWages : null
|
||||
|
||||
// Build chart data with per-dept costs per week so Bar dataKey works
|
||||
type WeekEntry = { label: string; isPast: boolean; [dept: string]: number | boolean | string }
|
||||
|
|
@ -174,6 +188,15 @@ export default function Monthly() {
|
|||
<div className="label">{isCurrentMonth ? 'Actual MTD' : 'Actual'}</div>
|
||||
<div className="value">{fmtMoney(totalActual)}</div>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<div className="label">{isCurrentMonth ? 'PY Wages MTD' : 'PY Wages'}</div>
|
||||
<div className="value">{pyWages != null ? fmtMoney(pyWages) : '—'}</div>
|
||||
{pyWagesDiff != null && (
|
||||
<div className={`sub ${pyWagesDiff > 0 ? 'variance-over' : 'variance-under'}`}>
|
||||
{pyWagesDiff > 0 ? '+' : ''}{fmtMoney(pyWagesDiff)} vs PY
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCurrentMonth && (
|
||||
<div className="summary-card">
|
||||
<div className="label">Forecast EOM</div>
|
||||
|
|
@ -202,6 +225,10 @@ export default function Monthly() {
|
|||
<div className="label">{isCurrentMonth ? 'Net Sales MTD' : 'Net Sales'}</div>
|
||||
<div className="value">{fmtMoney(netSales)}</div>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<div className="label">{isCurrentMonth ? 'PY Net Sales MTD' : 'PY Net Sales'}</div>
|
||||
<div className="value">{pySales > 0 ? fmtMoney(pySales) : '—'}</div>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<div className="label">% Net Sales</div>
|
||||
<div className="value">{pctSales != null ? `${pctSales.toFixed(1)}%` : '—'}</div>
|
||||
|
|
@ -239,6 +266,7 @@ export default function Monthly() {
|
|||
<th>Department</th>
|
||||
<th className="right">{isCurrentMonth ? 'Actual MTD' : 'Actual'}</th>
|
||||
{isCurrentMonth && <th className="right">Forecast → EOM</th>}
|
||||
<th className="right">% of Total</th>
|
||||
<th className="right">Budget</th>
|
||||
<th className="right">% Budget</th>
|
||||
<th className="right">Variance</th>
|
||||
|
|
@ -246,17 +274,22 @@ export default function Monthly() {
|
|||
</thead>
|
||||
<tbody>
|
||||
{deptSummary.map(dep => {
|
||||
const displayCost = isCurrentMonth ? dep.forecast_eom : dep.actual_mtd
|
||||
const displayCost = isCurrentMonth ? dep.forecast_eom : dep.actual_mtd
|
||||
const totalDisplay = isCurrentMonth ? totalForecast : totalActual
|
||||
const depBudg = budget != null && (deptPcts[dep.department_id] ?? 0) > 0
|
||||
? budget * (deptPcts[dep.department_id] / 100)
|
||||
: null
|
||||
const dp = depBudg != null && depBudg > 0 ? (displayCost / depBudg) * 100 : null
|
||||
const dv = depBudg != null ? displayCost - depBudg : null
|
||||
const dp = depBudg != null && depBudg > 0 ? (displayCost / depBudg) * 100 : null
|
||||
const dv = depBudg != null ? displayCost - depBudg : null
|
||||
const depOfTotal = totalDisplay > 0 ? (displayCost / totalDisplay) * 100 : null
|
||||
return (
|
||||
<tr key={dep.department_id}>
|
||||
<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>}
|
||||
<td className="right" style={{ color: 'var(--text-muted)' }}>
|
||||
{depOfTotal != null ? `${depOfTotal.toFixed(1)}%` : '—'}
|
||||
</td>
|
||||
<td className="right">{depBudg != null ? fmtMoney(depBudg) : '—'}</td>
|
||||
<td className="right">
|
||||
{dp != null ? <span className={`pct-badge ${pctClass(dp)}`}>{dp.toFixed(1)}%</span> : '—'}
|
||||
|
|
@ -271,6 +304,7 @@ export default function Monthly() {
|
|||
<td>Total</td>
|
||||
<td className="right">{fmtMoney(totalActual)}</td>
|
||||
{isCurrentMonth && <td className="right">{fmtMoney(totalForecast)}</td>}
|
||||
<td className="right">100%</td>
|
||||
<td className="right">{budget != null ? fmtMoney(budget) : '—'}</td>
|
||||
<td className="right">
|
||||
{pctBudget != null ? <span className={`pct-badge ${pctClass(pctBudget)}`}>{pctBudget.toFixed(1)}%</span> : '—'}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue