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
|
|
@ -43,6 +43,7 @@ export default function Monthly() {
|
||||||
const [pyWagesFull, setPyWagesFull] = useState<number | null>(null)
|
const [pyWagesFull, setPyWagesFull] = useState<number | null>(null)
|
||||||
const [pyDepts, setPyDepts] = useState<{id: string; name: string; cost: number; costFull: number}[]>([])
|
const [pyDepts, setPyDepts] = useState<{id: string; name: string; cost: number; costFull: number}[]>([])
|
||||||
const [pyTableOpen, setPyTableOpen] = useState(false)
|
const [pyTableOpen, setPyTableOpen] = useState(false)
|
||||||
|
const [prevMonthDepts, setPrevMonthDepts] = useState<{id: string; cost: number}[]>([])
|
||||||
const [budget, setBudget] = useState<number | null>(null)
|
const [budget, setBudget] = useState<number | null>(null)
|
||||||
const [deptPcts, setDeptPcts] = useState<Record<string, number>>({})
|
const [deptPcts, setDeptPcts] = useState<Record<string, number>>({})
|
||||||
const [showOncosts, setShowOncosts] = useState(true)
|
const [showOncosts, setShowOncosts] = useState(true)
|
||||||
|
|
@ -63,15 +64,23 @@ export default function Monthly() {
|
||||||
const pyFromStr = `${year - 1}-${String(month).padStart(2, '0')}-01`
|
const pyFromStr = `${year - 1}-${String(month).padStart(2, '0')}-01`
|
||||||
const pyToStr = `${year - 1}-${String(month).padStart(2, '0')}-${String(pyDim).padStart(2, '0')}`
|
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 () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true); setError(null)
|
setLoading(true); setError(null)
|
||||||
try {
|
try {
|
||||||
// Net sales: full month — OTB/forecast for future dates, actuals for past dates
|
// 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),
|
getActuals(fromStr, toStr),
|
||||||
getNetSales(fromStr, toStr),
|
getNetSales(fromStr, toStr),
|
||||||
getBudgets(),
|
getBudgets(),
|
||||||
getActuals(pyFromStr, pyToStr),
|
getActuals(pyFromStr, pyToStr),
|
||||||
|
getActuals(pmFromStr, pmToStr),
|
||||||
])
|
])
|
||||||
setDepts(actRes.departments)
|
setDepts(actRes.departments)
|
||||||
setShowOncosts(actRes.show_oncosts)
|
setShowOncosts(actRes.show_oncosts)
|
||||||
|
|
@ -104,12 +113,19 @@ export default function Monthly() {
|
||||||
|
|
||||||
const bRow = budRes.budgets.find((b: WageBudget) => b.month === fromStr)
|
const bRow = budRes.budgets.find((b: WageBudget) => b.month === fromStr)
|
||||||
setBudget(bRow ? bRow.budget_amount : null)
|
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) {
|
} catch (e: unknown) {
|
||||||
setError(e instanceof Error ? e.message : 'Failed to load')
|
setError(e instanceof Error ? e.message : 'Failed to load')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
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])
|
useEffect(() => { load() }, [load])
|
||||||
|
|
||||||
|
|
@ -157,6 +173,7 @@ export default function Monthly() {
|
||||||
const totalForecast = deptSummary.reduce((s, d) => s + d.forecast_eom, 0)
|
const totalForecast = deptSummary.reduce((s, d) => s + d.forecast_eom, 0)
|
||||||
const pyTotalWages = pyDepts.reduce((s, d) => s + d.cost, 0)
|
const pyTotalWages = pyDepts.reduce((s, d) => s + d.cost, 0)
|
||||||
const pyTotalWagesFull = pyDepts.reduce((s, d) => s + d.costFull, 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
|
// MTD row: pro-rata budget to yesterday
|
||||||
const yDay = isCurrentMonth ? parseInt(yesterdayStr.slice(8)) : dim
|
const yDay = isCurrentMonth ? parseInt(yesterdayStr.slice(8)) : dim
|
||||||
|
|
@ -351,6 +368,7 @@ export default function Monthly() {
|
||||||
{isCurrentMonth && <th className="right">Forecast → EOM</th>}
|
{isCurrentMonth && <th className="right">Forecast → EOM</th>}
|
||||||
<th className="right">% of Total</th>
|
<th className="right">% of Total</th>
|
||||||
<th className="right">vs PY</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">% Budget</th>
|
<th className="right">% Budget</th>
|
||||||
<th className="right">Variance</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 pyDept = pyDepts.find(p => p.id === dep.department_id)
|
||||||
const pyCost = pyDept ? (isCurrentMonth ? pyDept.cost : pyDept.costFull) : null
|
const pyCost = pyDept ? (isCurrentMonth ? pyDept.cost : pyDept.costFull) : null
|
||||||
const pyDelta = pyCost != null && pyCost > 0 ? ((dep.actual_mtd - pyCost) / pyCost) * 100 : 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 (
|
return (
|
||||||
<tr key={dep.department_id} style={{ cursor: 'pointer' }}
|
<tr key={dep.department_id} style={{ cursor: 'pointer' }}
|
||||||
onClick={() => setModal({ deptId: dep.department_id, deptName: dep.department_name })}>
|
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>
|
? <span className={pyDelta > 0 ? 'variance-over' : 'variance-under'}>{pyDelta > 0 ? '+' : ''}{pyDelta.toFixed(1)}%</span>
|
||||||
: '—'}
|
: '—'}
|
||||||
</td>
|
</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">{depBudg != null ? fmtMoney(depBudg) : '—'}</td>
|
||||||
<td className="right">
|
<td className="right">
|
||||||
{dp != null ? <span className={`pct-badge ${pctClass(dp)}`}>{fmtDelta(dp)}</span> : '—'}
|
{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>
|
return <span className={d > 0 ? 'variance-over' : 'variance-under'}>{d > 0 ? '+' : ''}{d.toFixed(1)}%</span>
|
||||||
})()}
|
})()}
|
||||||
</td>
|
</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">{budget != null ? fmtMoney(budget) : '—'}</td>
|
||||||
<td className="right">
|
<td className="right">
|
||||||
{pctBudgFull != null ? <span className={`pct-badge ${pctClass(pctBudgFull)}`}>{fmtDelta(pctBudgFull)}</span> : '—'}
|
{pctBudgFull != null ? <span className={`pct-badge ${pctClass(pctBudgFull)}`}>{fmtDelta(pctBudgFull)}</span> : '—'}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { ChevronLeft, ChevronRight, ChevronDown, Download } from 'lucide-react'
|
import { ChevronLeft, ChevronRight, Download } from 'lucide-react'
|
||||||
import { getActuals, getNetSales, getBudgets, getDeptDetail, downloadExport } from '../api'
|
import { getActuals, getNetSales, getBudgets, getDeptDetail, downloadExport } from '../api'
|
||||||
import type { DeptActuals, WageBudget, EmployeeDetail } from '../types'
|
import type { DeptActuals, WageBudget, EmployeeDetail } from '../types'
|
||||||
import { DeptDetailModal } from '../components/DeptDetailModal'
|
import { DeptDetailModal } from '../components/DeptDetailModal'
|
||||||
|
|
@ -7,7 +7,6 @@ import { DeptDetailModal } from '../components/DeptDetailModal'
|
||||||
function localStr(d: Date): string {
|
function localStr(d: Date): string {
|
||||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function mondayOf(d: Date): string {
|
function mondayOf(d: Date): string {
|
||||||
const day = d.getDay()
|
const day = d.getDay()
|
||||||
const r = new Date(d)
|
const r = new Date(d)
|
||||||
|
|
@ -15,59 +14,58 @@ function mondayOf(d: Date): string {
|
||||||
r.setHours(0, 0, 0, 0)
|
r.setHours(0, 0, 0, 0)
|
||||||
return localStr(r)
|
return localStr(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDaysStr(dateStr: string, n: number): string {
|
function addDaysStr(dateStr: string, n: number): string {
|
||||||
const d = new Date(dateStr + 'T00:00:00')
|
const d = new Date(dateStr + 'T00:00:00')
|
||||||
d.setDate(d.getDate() + n)
|
d.setDate(d.getDate() + n)
|
||||||
return localStr(d)
|
return localStr(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
function daysInMonthFor(dateStr: string): number {
|
function daysInMonthFor(dateStr: string): number {
|
||||||
const d = new Date(dateStr + 'T00:00:00')
|
const d = new Date(dateStr + 'T00:00:00')
|
||||||
return new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate()
|
return new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate()
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtDisplay(dateStr: string): string {
|
function fmtDisplay(dateStr: string): string {
|
||||||
return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })
|
return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtMoney(n: number): string {
|
function fmtMoney(n: number): string {
|
||||||
return `£${n.toLocaleString('en-GB', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`
|
return `£${n.toLocaleString('en-GB', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function pctClass(pct: number | null): string {
|
function pctClass(pct: number | null): string {
|
||||||
if (pct == null) return ''
|
if (pct == null) return ''
|
||||||
if (pct <= 100) return 'pct-green'
|
return pct <= 100 ? 'pct-green' : pct <= 110 ? 'pct-amber' : 'pct-red'
|
||||||
if (pct <= 110) return 'pct-amber'
|
|
||||||
return 'pct-red'
|
|
||||||
}
|
}
|
||||||
function fmtDelta(pct: number): string {
|
function fmtDelta(pct: number): string {
|
||||||
const d = pct - 100
|
const d = pct - 100
|
||||||
return `${d >= 0 ? '+' : ''}${d.toFixed(1)}%`
|
return `${d >= 0 ? '+' : ''}${d.toFixed(1)}%`
|
||||||
}
|
}
|
||||||
|
function budgetColour(pct: number): string {
|
||||||
|
return pct <= 100 ? 'var(--app-primary)' : pct <= 110 ? '#b45309' : '#dc2626'
|
||||||
|
}
|
||||||
|
|
||||||
export default function Weekly() {
|
export default function Weekly() {
|
||||||
const [fromStr, setFromStr] = useState<string>(() => mondayOf(new Date()))
|
const [fromStr, setFromStr] = useState<string>(() => mondayOf(new Date()))
|
||||||
|
|
||||||
const toStr = addDaysStr(fromStr, 6)
|
const toStr = addDaysStr(fromStr, 6)
|
||||||
const todayStr = localStr(new Date())
|
|
||||||
const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1)
|
const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1)
|
||||||
const yesterdayStr = localStr(yesterday)
|
const yesterdayStr = localStr(yesterday)
|
||||||
const isCurrentWeek = fromStr === mondayOf(new Date())
|
const isCurrentWeek = fromStr === mondayOf(new Date())
|
||||||
const pyFromStr = addDaysStr(fromStr, -364)
|
|
||||||
// Cut-off at yesterday: avoids partial clockins/open timesheets skewing today's figures
|
// Previous week range (for expanded actuals fetch + comparison)
|
||||||
|
const prevWeekFrom = addDaysStr(fromStr, -7)
|
||||||
|
const prevWeekTo = addDaysStr(fromStr, -1)
|
||||||
|
|
||||||
|
// PY period: same elapsed days as current WTD, or full week for past weeks
|
||||||
const daysElapsed = isCurrentWeek
|
const daysElapsed = isCurrentWeek
|
||||||
? Math.round((new Date(yesterdayStr + 'T00:00:00').getTime() - new Date(fromStr + 'T00:00:00').getTime()) / 86_400_000)
|
? Math.round((new Date(yesterdayStr + 'T00:00:00').getTime() - new Date(fromStr + 'T00:00:00').getTime()) / 86_400_000)
|
||||||
: 6
|
: 6
|
||||||
|
const pyFromStr = addDaysStr(fromStr, -364)
|
||||||
const pyToStr = addDaysStr(fromStr, -364 + daysElapsed)
|
const pyToStr = addDaysStr(fromStr, -364 + daysElapsed)
|
||||||
|
|
||||||
const [depts, setDepts] = useState<DeptActuals[]>([])
|
const [depts, setDepts] = useState<DeptActuals[]>([])
|
||||||
const [netSales, setNetSales] = useState(0)
|
const [netSalesMTD, setNetSalesMTD] = useState(0)
|
||||||
const [pySales, setPySales] = useState(0)
|
const [netSalesFull, setNetSalesFull] = useState(0)
|
||||||
|
const [pySalesMTD, setPySalesMTD] = useState(0)
|
||||||
const [pyWages, setPyWages] = useState<number | null>(null)
|
const [pyWages, setPyWages] = useState<number | null>(null)
|
||||||
const [pyDepts, setPyDepts] = useState<{id: string; name: string; cost: number}[]>([])
|
const [pyDepts, setPyDepts] = useState<{id: string; name: string; cost: number}[]>([])
|
||||||
const [pyTableOpen, setPyTableOpen] = useState(false)
|
|
||||||
const [budget, setBudget] = useState<number | null>(null)
|
|
||||||
const [monthlyBudg, setMonthlyBudg] = useState<number | null>(null)
|
const [monthlyBudg, setMonthlyBudg] = useState<number | null>(null)
|
||||||
const [deptPcts, setDeptPcts] = useState<Record<string, number>>({})
|
const [deptPcts, setDeptPcts] = useState<Record<string, number>>({})
|
||||||
const [showOncosts, setShowOncosts] = useState(true)
|
const [showOncosts, setShowOncosts] = useState(true)
|
||||||
|
|
@ -80,8 +78,10 @@ export default function Weekly() {
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true); setError(null)
|
setLoading(true); setError(null)
|
||||||
try {
|
try {
|
||||||
|
const isCurrentWk = fromStr === mondayOf(new Date())
|
||||||
const [actRes, salesRes, budgetRes, pyActRes] = await Promise.all([
|
const [actRes, salesRes, budgetRes, pyActRes] = await Promise.all([
|
||||||
getActuals(fromStr, toStr),
|
// 14-day fetch: prev week + current week so prev-week data is in dep.days for forecast + comparison
|
||||||
|
getActuals(prevWeekFrom, toStr),
|
||||||
getNetSales(fromStr, toStr),
|
getNetSales(fromStr, toStr),
|
||||||
getBudgets(),
|
getBudgets(),
|
||||||
getActuals(pyFromStr, pyToStr),
|
getActuals(pyFromStr, pyToStr),
|
||||||
|
|
@ -89,91 +89,117 @@ export default function Weekly() {
|
||||||
setDepts(actRes.departments)
|
setDepts(actRes.departments)
|
||||||
setShowOncosts(actRes.show_oncosts)
|
setShowOncosts(actRes.show_oncosts)
|
||||||
setDeptPcts(actRes.dept_pcts)
|
setDeptPcts(actRes.dept_pcts)
|
||||||
// Current week cut-off at yesterday to avoid partial clockins
|
|
||||||
const isCurrentWk = fromStr === mondayOf(new Date())
|
|
||||||
setNetSales(
|
|
||||||
isCurrentWk
|
|
||||||
? salesRes.days.filter(d => d.date <= yesterdayStr).reduce((s, d) => s + d.net_sales, 0)
|
|
||||||
: salesRes.days.reduce((s, d) => s + d.net_sales, 0)
|
|
||||||
)
|
|
||||||
setPySales(
|
|
||||||
isCurrentWk
|
|
||||||
? salesRes.days.filter(d => d.date <= yesterdayStr).reduce((s, d) => s + d.py_sales, 0)
|
|
||||||
: salesRes.days.reduce((s, d) => s + d.py_sales, 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
// Net sales: WTD to yesterday for current week, full for past weeks
|
||||||
|
const cutoffDate = isCurrentWk ? yesterdayStr : toStr
|
||||||
|
setNetSalesMTD(salesRes.days.filter(d => d.date <= cutoffDate).reduce((s, d) => s + d.net_sales, 0))
|
||||||
|
setNetSalesFull(salesRes.days.reduce((s, d) => s + d.net_sales, 0))
|
||||||
|
setPySalesMTD(salesRes.days.filter(d => d.date <= cutoffDate).reduce((s, d) => s + d.py_sales, 0))
|
||||||
|
|
||||||
|
// PY dept breakdown
|
||||||
const pyDeptList = pyActRes.departments.map(dep => ({
|
const pyDeptList = pyActRes.departments.map(dep => ({
|
||||||
id: dep.department_id,
|
id: dep.department_id,
|
||||||
name: dep.department_name,
|
name: dep.department_name,
|
||||||
cost: Object.values(dep.days).reduce((s, d) => s + d.cost, 0),
|
cost: Object.values(dep.days).reduce((s, d) => s + d.cost, 0),
|
||||||
})).filter(d => d.cost > 0).sort((a, b) => b.cost - a.cost)
|
})).filter(d => d.cost > 0)
|
||||||
setPyDepts(pyDeptList)
|
setPyDepts(pyDeptList)
|
||||||
const pyTotal = pyDeptList.reduce((s, d) => s + d.cost, 0)
|
const pyTotal = pyDeptList.reduce((s, d) => s + d.cost, 0)
|
||||||
setPyWages(pyTotal > 0 ? pyTotal : null)
|
setPyWages(pyTotal > 0 ? pyTotal : null)
|
||||||
|
|
||||||
|
// Budget: WTD pro-rata (to yesterday) stored; full-week derived in component body
|
||||||
const d0 = new Date(fromStr + 'T00:00:00')
|
const d0 = new Date(fromStr + 'T00:00:00')
|
||||||
const monthKey = `${d0.getFullYear()}-${String(d0.getMonth() + 1).padStart(2, '0')}-01`
|
const monthKey = `${d0.getFullYear()}-${String(d0.getMonth() + 1).padStart(2, '0')}-01`
|
||||||
const bRow = (budgetRes.budgets as WageBudget[]).find(b => b.month === monthKey)
|
const bRow = (budgetRes.budgets as WageBudget[]).find(b => b.month === monthKey)
|
||||||
if (bRow) {
|
setMonthlyBudg(bRow ? bRow.budget_amount : null)
|
||||||
const dim = daysInMonthFor(fromStr)
|
|
||||||
const cutoff = isCurrentWk ? yesterdayStr : toStr
|
|
||||||
const effectiveTo = cutoff < toStr ? cutoff : toStr
|
|
||||||
const weekDays = effectiveTo >= fromStr
|
|
||||||
? Math.round((new Date(effectiveTo + 'T00:00:00').getTime() - new Date(fromStr + 'T00:00:00').getTime()) / 86_400_000) + 1
|
|
||||||
: 7
|
|
||||||
const ratio = weekDays / dim
|
|
||||||
setMonthlyBudg(bRow.budget_amount)
|
|
||||||
setBudget(bRow.budget_amount * ratio)
|
|
||||||
} else {
|
|
||||||
setMonthlyBudg(null)
|
|
||||||
setBudget(null)
|
|
||||||
}
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
setError(e instanceof Error ? e.message : 'Failed to load')
|
setError(e instanceof Error ? e.message : 'Failed to load')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [fromStr, toStr, todayStr, pyFromStr, pyToStr])
|
}, [fromStr, toStr, prevWeekFrom, pyFromStr, pyToStr, yesterdayStr])
|
||||||
|
|
||||||
useEffect(() => { load() }, [load])
|
useEffect(() => { load() }, [load])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!modal) { setModalEmps(null); return }
|
if (!modal) { setModalEmps(null); return }
|
||||||
setModalLoad(true)
|
setModalLoad(true)
|
||||||
getDeptDetail(modal.deptId, fromStr, isCurrentWeek ? todayStr : toStr)
|
getDeptDetail(modal.deptId, fromStr, isCurrentWeek ? yesterdayStr : toStr)
|
||||||
.then(r => setModalEmps(r.employees))
|
.then(r => setModalEmps(r.employees))
|
||||||
.catch(() => setModalEmps([]))
|
.catch(() => setModalEmps([]))
|
||||||
.finally(() => setModalLoad(false))
|
.finally(() => setModalLoad(false))
|
||||||
}, [modal, fromStr, toStr, todayStr, isCurrentWeek])
|
}, [modal, fromStr, toStr, yesterdayStr, isCurrentWeek])
|
||||||
|
|
||||||
const prev = () => setFromStr(s => addDaysStr(s, -7))
|
const prev = () => setFromStr(s => addDaysStr(s, -7))
|
||||||
const next = () => setFromStr(s => addDaysStr(s, 7))
|
const next = () => setFromStr(s => addDaysStr(s, 7))
|
||||||
|
|
||||||
const deptWeekBudget = (deptId: string) =>
|
// Budget derived from monthlyBudg
|
||||||
budget != null && monthlyBudg != null && (deptPcts[deptId] ?? 0) > 0
|
const dim = daysInMonthFor(fromStr)
|
||||||
? budget * (deptPcts[deptId] / 100)
|
const weekDaysWTD = daysElapsed + 1 // Mon to yesterday inclusive (or 7 for past weeks)
|
||||||
: null
|
const budgetWTD = monthlyBudg != null ? monthlyBudg * weekDaysWTD / dim : null
|
||||||
|
const budgetFull = monthlyBudg != null ? monthlyBudg * 7 / dim : null
|
||||||
|
|
||||||
const deptTotals = depts.map(dep => ({
|
// Per-dept rows computed from 14-day actuals
|
||||||
department_id: dep.department_id,
|
const wtdCutoff = isCurrentWeek ? yesterdayStr : toStr
|
||||||
department_name: dep.department_name,
|
// Prev week comparison cutoff: same elapsed days as WTD (Mon-N of prev week)
|
||||||
cost: Object.entries(dep.days)
|
const prevCutoff = isCurrentWeek ? addDaysStr(yesterdayStr, -7) : prevWeekTo
|
||||||
.filter(([d]) => !isCurrentWeek || d <= yesterdayStr)
|
|
||||||
.reduce((s, [, v]) => s + v.cost, 0),
|
|
||||||
})).sort((a, b) => b.cost - a.cost)
|
|
||||||
|
|
||||||
const totalWages = deptTotals.reduce((s, d) => s + d.cost, 0)
|
type DeptRow = {
|
||||||
const pyTotalWages = pyDepts.reduce((s, d) => s + d.cost, 0)
|
department_id: string
|
||||||
const pctBudget = budget != null && budget > 0 ? (totalWages / budget) * 100 : null
|
department_name: string
|
||||||
const pctSales = netSales > 0 ? (totalWages / netSales) * 100 : null
|
wtdCost: number // actual WTD (or full week for past weeks)
|
||||||
|
forecastFull: number // full week forecast via prior-week same-day actuals
|
||||||
|
prevCost: number // prev week same elapsed days (or full prev week for past weeks)
|
||||||
|
}
|
||||||
|
const deptRows: DeptRow[] = depts.map(dep => {
|
||||||
|
const wtdCost = Object.entries(dep.days)
|
||||||
|
.filter(([d]) => d >= fromStr && d <= wtdCutoff)
|
||||||
|
.reduce((s, [, v]) => s + v.cost, 0)
|
||||||
|
|
||||||
|
// Forecast: WTD + prior-week same-day actual for each remaining day
|
||||||
|
let forecastFull = wtdCost
|
||||||
|
if (isCurrentWeek) {
|
||||||
|
for (let i = 0; i <= 6; i++) {
|
||||||
|
const dateStr = addDaysStr(fromStr, i)
|
||||||
|
if (dateStr <= yesterdayStr) continue
|
||||||
|
const priorStr = addDaysStr(dateStr, -7) // same day last week — in dep.days (14-day fetch)
|
||||||
|
forecastFull += dep.days[priorStr]?.cost ?? 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevCost = Object.entries(dep.days)
|
||||||
|
.filter(([d]) => d >= prevWeekFrom && d <= prevCutoff)
|
||||||
|
.reduce((s, [, v]) => s + v.cost, 0)
|
||||||
|
|
||||||
|
return { department_id: dep.department_id, department_name: dep.department_name, wtdCost, forecastFull, prevCost }
|
||||||
|
}).sort((a, b) => b.forecastFull - a.forecastFull)
|
||||||
|
|
||||||
|
const totalWTD = deptRows.reduce((s, d) => s + d.wtdCost, 0)
|
||||||
|
const totalForecast = deptRows.reduce((s, d) => s + d.forecastFull, 0)
|
||||||
|
const totalPrev = deptRows.reduce((s, d) => s + d.prevCost, 0)
|
||||||
|
const totalPyWages = pyWages ?? 0
|
||||||
|
|
||||||
|
const pctBudgWTD = budgetWTD != null && budgetWTD > 0 ? (totalWTD / budgetWTD) * 100 : null
|
||||||
|
const pctBudgFull = budgetFull != null && budgetFull > 0 ? (totalForecast / budgetFull) * 100 : null
|
||||||
|
const pctSalesMTD = netSalesMTD > 0 ? (totalWTD / netSalesMTD) * 100 : null
|
||||||
|
const pctSalesFull = netSalesFull > 0 ? (totalForecast / netSalesFull) * 100 : null
|
||||||
|
|
||||||
function pyPct(current: number, py: number): string {
|
function pyPct(current: number, py: number): string {
|
||||||
if (py <= 0 || current <= 0) return ''
|
if (py <= 0 || current <= 0) return ''
|
||||||
const p = ((current - py) / py) * 100
|
const p = ((current - py) / py) * 100
|
||||||
return ` (${p >= 0 ? '+' : ''}${p.toFixed(1)}%)`
|
return ` (${p >= 0 ? '+' : ''}${p.toFixed(1)}%)`
|
||||||
}
|
}
|
||||||
|
function deltaPct(current: number, prior: number): number | null {
|
||||||
|
if (prior <= 0) return null
|
||||||
|
return ((current - prior) / prior) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
const deptBudg = (deptId: string, b: number | null): number | null =>
|
||||||
|
b != null && (deptPcts[deptId] ?? 0) > 0 ? b * (deptPcts[deptId] / 100) : null
|
||||||
|
|
||||||
const weekLabel = `${fmtDisplay(fromStr)} – ${fmtDisplay(toStr)} ${new Date(toStr + 'T00:00:00').getFullYear()}`
|
const weekLabel = `${fmtDisplay(fromStr)} – ${fmtDisplay(toStr)} ${new Date(toStr + 'T00:00:00').getFullYear()}`
|
||||||
|
const activeBudget = isCurrentWeek ? budgetWTD : budgetFull
|
||||||
|
const activePctBudg = isCurrentWeek ? pctBudgWTD : pctBudgFull
|
||||||
|
const activePctSales = isCurrentWeek ? pctSalesMTD : pctSalesFull
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -190,91 +216,175 @@ export default function Weekly() {
|
||||||
<button className="btn btn-secondary" onClick={next} disabled={isCurrentWeek}><ChevronRight size={16} strokeWidth={1.75} /></button>
|
<button className="btn btn-secondary" onClick={next} disabled={isCurrentWeek}><ChevronRight size={16} strokeWidth={1.75} /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="summary-grid">
|
{/* ── Summary cards ──────────────────────────────────────────── */}
|
||||||
|
{isCurrentWeek ? (
|
||||||
|
<>
|
||||||
|
<div className="section-row-label">Week to {fmtDisplay(yesterdayStr)}</div>
|
||||||
|
<div className="summary-grid" style={{ marginBottom: 8 }}>
|
||||||
<div className="summary-card">
|
<div className="summary-card">
|
||||||
<div className="label">Total Wages</div>
|
<div className="label">Actual WTD</div>
|
||||||
<div className="value">{fmtMoney(totalWages)}</div>
|
<div className="value">{fmtMoney(totalWTD)}</div>
|
||||||
{pyWages != null
|
{pyWages != null && <div className="sub">PY WTD {fmtMoney(totalPyWages)}{pyPct(totalWTD, totalPyWages)}</div>}
|
||||||
? <div className="sub">{isCurrentWeek ? 'PY WTD' : 'PY'} {fmtMoney(pyWages)}{pyPct(totalWages, pyWages)}</div>
|
|
||||||
: <div className="sub">{showOncosts ? 'incl. on-costs' : 'base cost'}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="summary-card">
|
<div className="summary-card">
|
||||||
<div className="label">Pro-rata Budget</div>
|
<div className="label">Budget WTD</div>
|
||||||
<div className="value">{budget != null ? fmtMoney(budget) : '—'}</div>
|
<div className="value">{budgetWTD != null ? fmtMoney(budgetWTD) : '—'}</div>
|
||||||
<div className="sub">proportion of monthly</div>
|
<div className="sub">proportion of monthly</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="summary-card">
|
||||||
|
<div className="label">% Budget WTD</div>
|
||||||
|
<div className="value" style={pctBudgWTD != null ? { color: budgetColour(pctBudgWTD) } : {}}>
|
||||||
|
{pctBudgWTD != null ? fmtDelta(pctBudgWTD) : '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="summary-card">
|
||||||
|
<div className="label">% Net Sales WTD</div>
|
||||||
|
<div className="value">{pctSalesMTD != null ? `${pctSalesMTD.toFixed(1)}%` : '—'}</div>
|
||||||
|
{pySalesMTD > 0 && pyWages != null && (
|
||||||
|
<div className="sub">PY WTD {((totalPyWages / pySalesMTD) * 100).toFixed(1)}%</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="section-row-label">Full week forecast</div>
|
||||||
|
<div className="summary-grid" style={{ marginBottom: 20 }}>
|
||||||
|
<div className="summary-card">
|
||||||
|
<div className="label">Forecast Full Week</div>
|
||||||
|
<div className="value">{fmtMoney(totalForecast)}</div>
|
||||||
|
{totalPrev > 0 && <div className="sub">Prev wk {fmtMoney(totalPrev)}{pyPct(totalForecast, totalPrev)}</div>}
|
||||||
|
</div>
|
||||||
|
<div className="summary-card">
|
||||||
|
<div className="label">Budget Full Week</div>
|
||||||
|
<div className="value">{budgetFull != null ? fmtMoney(budgetFull) : '—'}</div>
|
||||||
|
<div className="sub">7-day proportion</div>
|
||||||
|
</div>
|
||||||
|
<div className="summary-card">
|
||||||
|
<div className="label">% Budget (Forecast)</div>
|
||||||
|
<div className="value" style={pctBudgFull != null ? { color: budgetColour(pctBudgFull) } : {}}>
|
||||||
|
{pctBudgFull != null ? fmtDelta(pctBudgFull) : '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="summary-card">
|
||||||
|
<div className="label">% Net Sales (OTB)</div>
|
||||||
|
<div className="value">{pctSalesFull != null ? `${pctSalesFull.toFixed(1)}%` : '—'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="summary-grid" style={{ marginBottom: 20 }}>
|
||||||
|
<div className="summary-card">
|
||||||
|
<div className="label">Total Wages</div>
|
||||||
|
<div className="value">{fmtMoney(totalWTD)}</div>
|
||||||
|
{pyWages != null && <div className="sub">PY {fmtMoney(totalPyWages)}{pyPct(totalWTD, totalPyWages)}</div>}
|
||||||
|
</div>
|
||||||
|
<div className="summary-card">
|
||||||
|
<div className="label">Budget</div>
|
||||||
|
<div className="value">{budgetFull != null ? fmtMoney(budgetFull) : '—'}</div>
|
||||||
|
<div className="sub">7-day proportion</div>
|
||||||
|
</div>
|
||||||
<div className="summary-card">
|
<div className="summary-card">
|
||||||
<div className="label">% vs Budget</div>
|
<div className="label">% vs Budget</div>
|
||||||
<div className="value" style={pctBudget != null ? { color: pctBudget <= 100 ? 'var(--app-primary)' : pctBudget <= 110 ? '#b45309' : '#dc2626' } : {}}>
|
<div className="value" style={pctBudgFull != null ? { color: budgetColour(pctBudgFull) } : {}}>
|
||||||
{pctBudget != null ? fmtDelta(pctBudget) : '—'}
|
{pctBudgFull != null ? fmtDelta(pctBudgFull) : '—'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="summary-card">
|
<div className="summary-card">
|
||||||
<div className="label">Net Sales</div>
|
<div className="label">Net Sales</div>
|
||||||
<div className="value">{fmtMoney(netSales)}</div>
|
<div className="value">{fmtMoney(netSalesFull)}</div>
|
||||||
{pySales > 0 && <div className="sub">{isCurrentWeek ? 'PY WTD' : 'PY'} {fmtMoney(pySales)}{pyPct(netSales, pySales)}</div>}
|
{pySalesMTD > 0 && <div className="sub">PY {fmtMoney(pySalesMTD)}{pyPct(netSalesFull, pySalesMTD)}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="summary-card">
|
<div className="summary-card">
|
||||||
<div className="label">% of Net Sales</div>
|
<div className="label">% of Net Sales</div>
|
||||||
<div className="value">{pctSales != null ? `${pctSales.toFixed(1)}%` : '—'}</div>
|
<div className="value">{pctSalesFull != null ? `${pctSalesFull.toFixed(1)}%` : '—'}</div>
|
||||||
{pyWages != null && pySales > 0 && (
|
{pyWages != null && pySalesMTD > 0 && (
|
||||||
<div className="sub">{isCurrentWeek ? 'PY WTD' : 'PY'} {((pyWages / pySales) * 100).toFixed(1)}%</div>
|
<div className="sub">PY {((totalPyWages / pySalesMTD) * 100).toFixed(1)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading && <div className="state-center">Loading…</div>}
|
{loading && <div className="state-center">Loading…</div>}
|
||||||
{error && <div className="state-center" style={{ color: '#dc2626' }}>{error}</div>}
|
{error && <div className="state-center" style={{ color: '#dc2626' }}>{error}</div>}
|
||||||
|
|
||||||
{!loading && !error && (
|
{!loading && !error && (
|
||||||
<>
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<table className="data-table">
|
<table className="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Department</th>
|
<th>Department</th>
|
||||||
<th className="right">Wages</th>
|
<th className="right">{isCurrentWeek ? 'WTD' : 'Wages'}</th>
|
||||||
<th className="right">% of Total</th>
|
<th className="right">% of Total</th>
|
||||||
<th className="right">Budget (pro-rata)</th>
|
<th className="right">vs PY</th>
|
||||||
|
<th className="right">vs Prev Wk</th>
|
||||||
|
<th className="right">Budget</th>
|
||||||
<th className="right">% Budget</th>
|
<th className="right">% Budget</th>
|
||||||
<th className="right">% Net Sales</th>
|
<th className="right">% Net Sales</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{deptTotals.map(dep => {
|
{deptRows.map(dep => {
|
||||||
const depBudg = deptWeekBudget(dep.department_id)
|
const b = deptBudg(dep.department_id, activeBudget)
|
||||||
const depPct = depBudg != null && depBudg > 0 ? (dep.cost / depBudg) * 100 : null
|
const depPct = b != null && b > 0 ? (dep.wtdCost / b) * 100 : null
|
||||||
const depOfTotal = totalWages > 0 ? (dep.cost / totalWages) * 100 : null
|
const depOfTotal = totalWTD > 0 ? (dep.wtdCost / totalWTD) * 100 : null
|
||||||
const depSPct = netSales > 0 ? (dep.cost / netSales) * 100 : null
|
const depSales = isCurrentWeek ? netSalesMTD : netSalesFull
|
||||||
|
const depSPct = depSales > 0 ? (dep.wtdCost / depSales) * 100 : null
|
||||||
|
const pyDept = pyDepts.find(p => p.id === dep.department_id)
|
||||||
|
const pyDelta = pyDept ? deltaPct(dep.wtdCost, pyDept.cost) : null
|
||||||
|
const prevDelta = deltaPct(dep.wtdCost, dep.prevCost)
|
||||||
return (
|
return (
|
||||||
<tr key={dep.department_name} style={{ cursor: 'pointer' }}
|
<tr key={dep.department_id} style={{ cursor: 'pointer' }}
|
||||||
onClick={() => setModal({ deptId: dep.department_id, deptName: dep.department_name })}>
|
onClick={() => setModal({ deptId: dep.department_id, deptName: dep.department_name })}>
|
||||||
<td>{dep.department_name}</td>
|
<td>{dep.department_name}</td>
|
||||||
<td className="right">{fmtMoney(dep.cost)}</td>
|
<td className="right">{fmtMoney(dep.wtdCost)}</td>
|
||||||
<td className="right" style={{ color: 'var(--text-muted)' }}>
|
<td className="right" style={{ color: 'var(--text-muted)' }}>
|
||||||
{depOfTotal != null ? `${depOfTotal.toFixed(1)}%` : '—'}
|
{depOfTotal != null ? `${depOfTotal.toFixed(1)}%` : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="right">{depBudg != null ? fmtMoney(depBudg) : '—'}</td>
|
|
||||||
<td className="right">
|
<td className="right">
|
||||||
{depPct != null
|
{pyDelta != null
|
||||||
? <span className={`pct-badge ${pctClass(depPct)}`}>{fmtDelta(depPct)}</span>
|
? <span className={pyDelta > 0 ? 'variance-over' : 'variance-under'}>{pyDelta >= 0 ? '+' : ''}{pyDelta.toFixed(1)}%</span>
|
||||||
: '—'}
|
: '—'}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="right">
|
||||||
|
{prevDelta != null
|
||||||
|
? <span className={prevDelta > 0 ? 'variance-over' : 'variance-under'}>{prevDelta >= 0 ? '+' : ''}{prevDelta.toFixed(1)}%</span>
|
||||||
|
: '—'}
|
||||||
|
</td>
|
||||||
|
<td className="right">{b != null ? fmtMoney(b) : '—'}</td>
|
||||||
|
<td className="right">
|
||||||
|
{depPct != null ? <span className={`pct-badge ${pctClass(depPct)}`}>{fmtDelta(depPct)}</span> : '—'}
|
||||||
|
</td>
|
||||||
<td className="right">{depSPct != null ? `${depSPct.toFixed(1)}%` : '—'}</td>
|
<td className="right">{depSPct != null ? `${depSPct.toFixed(1)}%` : '—'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<tr className="total-row">
|
<tr className="total-row">
|
||||||
<td>Total</td>
|
<td>Total</td>
|
||||||
<td className="right">{fmtMoney(totalWages)}</td>
|
<td className="right">{fmtMoney(totalWTD)}</td>
|
||||||
<td className="right">100%</td>
|
<td className="right">100%</td>
|
||||||
<td className="right">{budget != null ? fmtMoney(budget) : '—'}</td>
|
|
||||||
<td className="right">
|
<td className="right">
|
||||||
{pctBudget != null
|
{(() => {
|
||||||
? <span className={`pct-badge ${pctClass(pctBudget)}`}>{fmtDelta(pctBudget)}</span>
|
if (totalPyWages <= 0) return '—'
|
||||||
|
const d = deltaPct(totalWTD, totalPyWages)
|
||||||
|
if (d == null) return '—'
|
||||||
|
return <span className={d > 0 ? 'variance-over' : 'variance-under'}>{d >= 0 ? '+' : ''}{d.toFixed(1)}%</span>
|
||||||
|
})()}
|
||||||
|
</td>
|
||||||
|
<td className="right">
|
||||||
|
{(() => {
|
||||||
|
const d = deltaPct(totalWTD, totalPrev)
|
||||||
|
if (d == null) return '—'
|
||||||
|
return <span className={d > 0 ? 'variance-over' : 'variance-under'}>{d >= 0 ? '+' : ''}{d.toFixed(1)}%</span>
|
||||||
|
})()}
|
||||||
|
</td>
|
||||||
|
<td className="right">{activeBudget != null ? fmtMoney(activeBudget) : '—'}</td>
|
||||||
|
<td className="right">
|
||||||
|
{activePctBudg != null
|
||||||
|
? <span className={`pct-badge ${pctClass(activePctBudg)}`}>{fmtDelta(activePctBudg)}</span>
|
||||||
: '—'}
|
: '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="right">{pctSales != null ? `${pctSales.toFixed(1)}%` : '—'}</td>
|
<td className="right">
|
||||||
|
{activePctSales != null ? `${activePctSales.toFixed(1)}%` : '—'}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -282,56 +392,12 @@ export default function Weekly() {
|
||||||
<p className="footnote">Includes estimated employer on-costs. Final payroll figures are in Sage.</p>
|
<p className="footnote">Includes estimated employer on-costs. Final payroll figures are in Sage.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{pyDepts.length > 0 && (
|
|
||||||
<div className="card">
|
|
||||||
<button className="py-collapse-toggle" onClick={() => setPyTableOpen(o => !o)}>
|
|
||||||
{pyTableOpen
|
|
||||||
? <ChevronDown size={14} strokeWidth={1.75} />
|
|
||||||
: <ChevronRight size={14} strokeWidth={1.75} />}
|
|
||||||
{isCurrentWeek ? 'PY WTD' : 'PY'} Dept Breakdown ({fmtDisplay(pyFromStr)} – {fmtDisplay(pyToStr)})
|
|
||||||
</button>
|
|
||||||
{pyTableOpen && (
|
|
||||||
<table className="data-table" style={{ marginTop: 12 }}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Department</th>
|
|
||||||
<th className="right">{isCurrentWeek ? 'PY WTD' : 'PY'} Wages</th>
|
|
||||||
<th className="right">% of Total</th>
|
|
||||||
<th className="right">% Net Sales</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{pyDepts.map(dep => {
|
|
||||||
const ofTotal = pyTotalWages > 0 ? (dep.cost / pyTotalWages) * 100 : null
|
|
||||||
const ofSales = pySales > 0 ? (dep.cost / pySales) * 100 : null
|
|
||||||
return (
|
|
||||||
<tr key={dep.id}>
|
|
||||||
<td>{dep.name}</td>
|
|
||||||
<td className="right">{fmtMoney(dep.cost)}</td>
|
|
||||||
<td className="right" style={{ color: 'var(--text-muted)' }}>{ofTotal != null ? `${ofTotal.toFixed(1)}%` : '—'}</td>
|
|
||||||
<td className="right">{ofSales != null ? `${ofSales.toFixed(1)}%` : '—'}</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<tr className="total-row">
|
|
||||||
<td>Total</td>
|
|
||||||
<td className="right">{fmtMoney(pyTotalWages)}</td>
|
|
||||||
<td className="right">100%</td>
|
|
||||||
<td className="right">{pySales > 0 ? `${((pyTotalWages / pySales) * 100).toFixed(1)}%` : '—'}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{modal && (
|
{modal && (
|
||||||
<DeptDetailModal
|
<DeptDetailModal
|
||||||
deptName={modal.deptName}
|
deptName={modal.deptName}
|
||||||
period={`${fmtDisplay(fromStr)} – ${fmtDisplay(isCurrentWeek ? todayStr : toStr)}`}
|
period={`${fmtDisplay(fromStr)} – ${fmtDisplay(isCurrentWeek ? yesterdayStr : toStr)}`}
|
||||||
employees={modalEmps}
|
employees={modalEmps}
|
||||||
loading={modalLoad}
|
loading={modalLoad}
|
||||||
onClose={() => setModal(null)}
|
onClose={() => setModal(null)}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue