Fix Weekly: store week as string to prevent useCallback infinite loop
This commit is contained in:
parent
fc92bcd897
commit
46201c6592
1 changed files with 53 additions and 52 deletions
|
|
@ -3,24 +3,32 @@ import { ChevronLeft, ChevronRight, Download } from 'lucide-react'
|
||||||
import { getActuals, getNetSales, getBudgets, downloadExport } from '../api'
|
import { getActuals, getNetSales, getBudgets, downloadExport } from '../api'
|
||||||
import type { DeptActuals, WageBudget } from '../types'
|
import type { DeptActuals, WageBudget } from '../types'
|
||||||
|
|
||||||
function startOfWeek(d: Date): Date {
|
function mondayOf(d: Date): string {
|
||||||
const day = d.getDay()
|
const day = d.getDay()
|
||||||
const diff = (day === 0 ? -6 : 1 - day) // Mon = start
|
|
||||||
const r = new Date(d)
|
const r = new Date(d)
|
||||||
r.setDate(d.getDate() + diff)
|
r.setDate(d.getDate() + (day === 0 ? -6 : 1 - day))
|
||||||
r.setHours(0, 0, 0, 0)
|
r.setHours(0, 0, 0, 0)
|
||||||
return r
|
return r.toISOString().slice(0, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDays(d: Date, n: number): Date {
|
function addDaysStr(dateStr: string, n: number): string {
|
||||||
const r = new Date(d)
|
const d = new Date(dateStr + 'T00:00:00')
|
||||||
r.setDate(r.getDate() + n)
|
d.setDate(d.getDate() + n)
|
||||||
return r
|
return d.toISOString().slice(0, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmt(d: Date): string { return d.toISOString().slice(0, 10) }
|
function daysInMonthFor(dateStr: string): number {
|
||||||
function fmtMoney(n: number): string { return `£${n.toLocaleString('en-GB', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}` }
|
const d = new Date(dateStr + 'T00:00:00')
|
||||||
function daysInMonth(date: Date): number { return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate() }
|
return new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtDisplay(dateStr: string): string {
|
||||||
|
return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtMoney(n: number): string {
|
||||||
|
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 ''
|
||||||
|
|
@ -30,7 +38,11 @@ function pctClass(pct: number | null): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Weekly() {
|
export default function Weekly() {
|
||||||
const [weekStart, setWeekStart] = useState<Date>(() => startOfWeek(new Date()))
|
const [fromStr, setFromStr] = useState<string>(() => mondayOf(new Date()))
|
||||||
|
|
||||||
|
const toStr = addDaysStr(fromStr, 6)
|
||||||
|
const todayStr = new Date().toISOString().slice(0, 10)
|
||||||
|
|
||||||
const [depts, setDepts] = useState<DeptActuals[]>([])
|
const [depts, setDepts] = useState<DeptActuals[]>([])
|
||||||
const [netSales, setNetSales] = useState(0)
|
const [netSales, setNetSales] = useState(0)
|
||||||
const [pySales, setPySales] = useState(0)
|
const [pySales, setPySales] = useState(0)
|
||||||
|
|
@ -39,10 +51,6 @@ export default function Weekly() {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
const weekEnd = addDays(weekStart, 6)
|
|
||||||
const fromStr = fmt(weekStart)
|
|
||||||
const toStr = fmt(weekEnd)
|
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true); setError(null)
|
setLoading(true); setError(null)
|
||||||
try {
|
try {
|
||||||
|
|
@ -53,23 +61,20 @@ export default function Weekly() {
|
||||||
])
|
])
|
||||||
setDepts(actRes.departments)
|
setDepts(actRes.departments)
|
||||||
setShowOncosts(actRes.show_oncosts)
|
setShowOncosts(actRes.show_oncosts)
|
||||||
|
setNetSales(salesRes.days.reduce((s, d) => s + d.net_sales, 0))
|
||||||
|
setPySales(salesRes.days.reduce((s, d) => s + d.py_sales, 0))
|
||||||
|
|
||||||
const totalSales = salesRes.days.reduce((s, d) => s + d.net_sales, 0)
|
const d0 = new Date(fromStr + 'T00:00:00')
|
||||||
const totalPY = salesRes.days.reduce((s, d) => s + d.py_sales, 0)
|
const monthKey = `${d0.getFullYear()}-${String(d0.getMonth() + 1).padStart(2, '0')}-01`
|
||||||
setNetSales(totalSales)
|
|
||||||
setPySales(totalPY)
|
|
||||||
|
|
||||||
// Find budget for the month of weekStart
|
|
||||||
const monthKey = `${weekStart.getFullYear()}-${String(weekStart.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) {
|
if (bRow) {
|
||||||
const dim = daysInMonth(weekStart)
|
const dim = daysInMonthFor(fromStr)
|
||||||
// Pro-rata: days in the selected week ÷ days in month
|
// Pro-rata: how many days of this week are <= today
|
||||||
const today = new Date()
|
const effectiveTo = todayStr < toStr ? todayStr : toStr
|
||||||
let weekDays = 7
|
const effectiveFrom = fromStr > todayStr ? todayStr : fromStr
|
||||||
if (weekStart <= today && today <= weekEnd) {
|
const weekDays = effectiveTo >= effectiveFrom
|
||||||
weekDays = Math.ceil((today.getTime() - weekStart.getTime()) / 86_400_000) + 1
|
? Math.round((new Date(effectiveTo + 'T00:00:00').getTime() - new Date(effectiveFrom + 'T00:00:00').getTime()) / 86_400_000) + 1
|
||||||
}
|
: 7
|
||||||
setBudget(bRow.budget_amount * (weekDays / dim))
|
setBudget(bRow.budget_amount * (weekDays / dim))
|
||||||
} else {
|
} else {
|
||||||
setBudget(null)
|
setBudget(null)
|
||||||
|
|
@ -79,36 +84,33 @@ export default function Weekly() {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [fromStr, toStr, weekStart, weekEnd])
|
}, [fromStr, toStr, todayStr]) // all primitive strings — stable refs
|
||||||
|
|
||||||
useEffect(() => { load() }, [load])
|
useEffect(() => { load() }, [load])
|
||||||
|
|
||||||
const prev = () => setWeekStart(d => addDays(d, -7))
|
const prev = () => setFromStr(s => addDaysStr(s, -7))
|
||||||
const next = () => setWeekStart(d => addDays(d, 7))
|
const next = () => setFromStr(s => addDaysStr(s, 7))
|
||||||
const isCurrentWeek = fmt(startOfWeek(new Date())) === fmt(weekStart)
|
const isCurrentWeek = fromStr === mondayOf(new Date())
|
||||||
|
|
||||||
// Totals
|
const deptTotals = depts.map(dep => ({
|
||||||
const deptTotals = depts.map(dep => {
|
department_name: dep.department_name,
|
||||||
const cost = Object.values(dep.days).reduce((s, d) => s + d.cost, 0)
|
cost: Object.values(dep.days).reduce((s, d) => s + d.cost, 0),
|
||||||
return { department_name: dep.department_name, cost }
|
})).sort((a, b) => b.cost - a.cost)
|
||||||
}).sort((a, b) => b.cost - a.cost)
|
|
||||||
|
|
||||||
const totalWages = deptTotals.reduce((s, d) => s + d.cost, 0)
|
const totalWages = deptTotals.reduce((s, d) => s + d.cost, 0)
|
||||||
const pctBudget = budget != null && budget > 0 ? (totalWages / budget) * 100 : null
|
const pctBudget = budget != null && budget > 0 ? (totalWages / budget) * 100 : null
|
||||||
const pctSales = netSales > 0 ? (totalWages / netSales) * 100 : null
|
const pctSales = netSales > 0 ? (totalWages / netSales) * 100 : null
|
||||||
|
|
||||||
const weekLabel = `${weekStart.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })} – ${weekEnd.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}`
|
const weekLabel = `${fmtDisplay(fromStr)} – ${fmtDisplay(toStr)} ${new Date(toStr + 'T00:00:00').getFullYear()}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h1 className="page-title">Weekly Wages</h1>
|
<h1 className="page-title">Weekly Wages</h1>
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
||||||
<button className="btn btn-secondary" onClick={() => downloadExport('weekly', fromStr, toStr)}>
|
<button className="btn btn-secondary" onClick={() => downloadExport('weekly', fromStr, toStr)}>
|
||||||
<Download size={14} strokeWidth={1.75} /> CSV
|
<Download size={14} strokeWidth={1.75} /> CSV
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="period-nav" style={{ marginBottom: 20 }}>
|
<div className="period-nav" style={{ marginBottom: 20 }}>
|
||||||
<button className="btn btn-secondary" onClick={prev}><ChevronLeft size={16} strokeWidth={1.75} /></button>
|
<button className="btn btn-secondary" onClick={prev}><ChevronLeft size={16} strokeWidth={1.75} /></button>
|
||||||
|
|
@ -116,7 +118,6 @@ 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>
|
||||||
|
|
||||||
{/* Summary cards */}
|
|
||||||
<div className="summary-grid">
|
<div className="summary-grid">
|
||||||
<div className="summary-card">
|
<div className="summary-card">
|
||||||
<div className="label">Total Wages</div>
|
<div className="label">Total Wages</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue