diff --git a/frontend/src/index.css b/frontend/src/index.css index c9cbc2a..f7ef6d7 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -366,6 +366,17 @@ input[type="text"]:focus { font-size: 14px; } +/* ── Section row label (Monthly two-row cards) ──────────────────── */ +.section-row-label { + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 8px; + margin-top: 4px; +} + /* ── PY collapse toggle ─────────────────────────────────────────── */ .py-collapse-toggle { display: flex; diff --git a/frontend/src/pages/Monthly.tsx b/frontend/src/pages/Monthly.tsx index d84e9b8..709f646 100644 --- a/frontend/src/pages/Monthly.tsx +++ b/frontend/src/pages/Monthly.tsx @@ -3,7 +3,7 @@ import { ChevronLeft, ChevronRight, ChevronDown, Download } from 'lucide-react' import { BarChart, Bar, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, Cell, } from 'recharts' -import { getActuals, getScheduled, getNetSales, getBudgets, getDeptDetail, downloadExport } from '../api' +import { getActuals, getNetSales, getBudgets, getDeptDetail, downloadExport } from '../api' import type { DeptActuals, WageBudget, EmployeeDetail } from '../types' import { DeptDetailModal } from '../components/DeptDetailModal' @@ -15,49 +15,50 @@ function daysInMonth(y: number, m: number): number { return new Date(y, m, 0).ge function fmtMoney(n: number): string { return `£${Math.round(n).toLocaleString('en-GB')}` } function pctClass(pct: number): string { return pct <= 100 ? 'pct-green' : pct <= 110 ? 'pct-amber' : 'pct-red' } function fmtDelta(pct: number): string { const d = pct - 100; return `${d >= 0 ? '+' : ''}${d.toFixed(1)}%` } +function fmtDisplay(dateStr: string): string { + return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) +} +function budgetColour(pct: number): string { + return pct <= 100 ? 'var(--app-primary)' : pct <= 110 ? '#b45309' : '#dc2626' +} const DEPT_COLORS = ['#065f46','#059669','#0891b2','#7c3aed','#c2410c','#b45309','#0f766e','#4338ca','#be185d','#15803d'] export default function Monthly() { - const todayStr = fmt(new Date()) - const todayYear = parseInt(todayStr.slice(0, 4)) - const todayMonth = parseInt(todayStr.slice(5, 7)) + const today = new Date() + const todayStr = fmt(today) + const yesterdayStr = fmt(addDays(today, -1)) + const todayYear = parseInt(todayStr.slice(0, 4)) + const todayMonth = parseInt(todayStr.slice(5, 7)) const [year, setYear] = useState(todayYear) const [month, setMonth] = useState(todayMonth) - const [depts, setDepts] = useState([]) - const [scheduled, setScheduled] = useState>>({}) - const [netSales, setNetSales] = useState(0) - const [pySales, setPySales] = useState(0) - const [pyWages, setPyWages] = useState(null) - const [pyWagesFull, setPyWagesFull] = useState(null) - const [pyDepts, setPyDepts] = useState<{id: string; name: string; cost: number; costFull: number}[]>([]) - const [pyTableOpen, setPyTableOpen] = useState(false) - const [budget, setBudget] = useState(null) - const [deptPcts, setDeptPcts] = useState>({}) - const [showOncosts, setShowOncosts] = useState(true) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [modal, setModal] = useState<{ deptId: string; deptName: string } | null>(null) - const [modalEmps, setModalEmps] = useState(null) - const [modalLoad, setModalLoad] = useState(false) + const [depts, setDepts] = useState([]) + const [netSalesMTD, setNetSalesMTD] = useState(0) + const [netSalesFull, setNetSalesFull] = useState(0) + const [pySalesMTD, setPySalesMTD] = useState(0) + const [pySalesFull, setPySalesFull] = useState(0) + const [pyWages, setPyWages] = useState(null) + const [pyWagesFull, setPyWagesFull] = useState(null) + const [pyDepts, setPyDepts] = useState<{id: string; name: string; cost: number; costFull: number}[]>([]) + const [pyTableOpen, setPyTableOpen] = useState(false) + const [budget, setBudget] = useState(null) + const [deptPcts, setDeptPcts] = useState>({}) + const [showOncosts, setShowOncosts] = useState(true) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [modal, setModal] = useState<{ deptId: string; deptName: string } | null>(null) + const [modalEmps, setModalEmps] = useState(null) + const [modalLoad, setModalLoad] = useState(false) - const dim = daysInMonth(year, month) - const monthStr = `${year}-${String(month).padStart(2, '0')}` - const fromStr = `${monthStr}-01` - const toStr = `${monthStr}-${String(dim).padStart(2, '0')}` + const dim = daysInMonth(year, month) + const monthStr = `${year}-${String(month).padStart(2, '0')}` + const fromStr = `${monthStr}-01` + const toStr = `${monthStr}-${String(dim).padStart(2, '0')}` const isCurrentMonth = year === todayYear && month === todayMonth - const salesTo = isCurrentMonth ? todayStr : toStr - const schedFrom = todayStr < toStr ? todayStr : toStr - // Current Mon–Sun: only use rota within this window; beyond it use prior-week actuals - const currentWeekEndStr = (() => { - const d = new Date(todayStr + 'T00:00:00') - const day = d.getDay() - d.setDate(d.getDate() + (day === 0 ? 0 : 7 - day)) - return fmt(d) - })() - // Prior-year: always fetch full month so we can show PY EOM alongside PY MTD + + // Prior year: always fetch full month (derive both MTD and full totals from one call) const pyDim = daysInMonth(year - 1, month) const pyFromStr = `${year - 1}-${String(month).padStart(2, '0')}-01` const pyToStr = `${year - 1}-${String(month).padStart(2, '0')}-${String(pyDim).padStart(2, '0')}` @@ -65,10 +66,10 @@ export default function Monthly() { const load = useCallback(async () => { setLoading(true); setError(null) try { - const [actRes, schRes, salesRes, budRes, pyActRes] = await Promise.all([ + // Net sales: full month — OTB/forecast for future dates, actuals for past dates + const [actRes, salesRes, budRes, pyActRes] = await Promise.all([ getActuals(fromStr, toStr), - getScheduled(schedFrom, toStr), - getNetSales(fromStr, salesTo), + getNetSales(fromStr, toStr), getBudgets(), getActuals(pyFromStr, pyToStr), ]) @@ -76,23 +77,20 @@ export default function Monthly() { setShowOncosts(actRes.show_oncosts) setDeptPcts(actRes.dept_pcts) - const schMap: Record> = {} - for (const dep of schRes.departments) { - schMap[dep.department_id] = {} - for (const [date, val] of Object.entries(dep.days)) { - schMap[dep.department_id][date] = val.cost - } - } - setScheduled(schMap) + // Cut-off for MTD = yesterday (avoid partial clockins today) + const cutoff = isCurrentMonth ? yesterdayStr : toStr - setNetSales(salesRes.days.reduce((s, d) => s + d.net_sales, 0)) - setPySales(salesRes.days.reduce((s, d) => s + d.py_sales, 0)) + setNetSalesMTD(salesRes.days.filter(d => d.date <= cutoff).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 <= cutoff).reduce((s, d) => s + d.py_sales, 0)) + setPySalesFull(salesRes.days.reduce((s, d) => s + d.py_sales, 0)) - // PY MTD limit: cap at same day number as today (or month end for past months) - const todayDay = parseInt(todayStr.slice(8)) + // PY MTD: same day number as yesterday in PY (or full month for past months) + const yDay = parseInt(yesterdayStr.slice(8)) const pyMtdLimit = isCurrentMonth - ? `${year - 1}-${String(month).padStart(2, '0')}-${String(Math.min(todayDay, pyDim)).padStart(2, '0')}` + ? `${year - 1}-${String(month).padStart(2, '0')}-${String(Math.min(yDay, pyDim)).padStart(2, '0')}` : pyToStr + const pyDeptList = pyActRes.departments.map(dep => { const mtdCost = Object.entries(dep.days).filter(([d]) => d <= pyMtdLimit).reduce((s, [, v]) => s + v.cost, 0) const fullCost = Object.values(dep.days).reduce((s, v) => s + v.cost, 0) @@ -111,42 +109,35 @@ export default function Monthly() { } finally { setLoading(false) } - }, [fromStr, toStr, schedFrom, salesTo, pyFromStr, pyToStr]) + }, [fromStr, toStr, pyFromStr, pyToStr, isCurrentMonth, yesterdayStr, year, month, pyDim]) useEffect(() => { load() }, [load]) useEffect(() => { if (!modal) { setModalEmps(null); return } setModalLoad(true) - getDeptDetail(modal.deptId, fromStr, isCurrentMonth ? todayStr : toStr) + getDeptDetail(modal.deptId, fromStr, isCurrentMonth ? yesterdayStr : toStr) .then(r => setModalEmps(r.employees)) .catch(() => setModalEmps([])) .finally(() => setModalLoad(false)) - }, [modal, fromStr, toStr, todayStr, isCurrentMonth]) + }, [modal, fromStr, toStr, yesterdayStr, 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) } } - function getSchCost(deptId: string, date: string): number | null { - return scheduled[deptId]?.[date] ?? null - } + // Actuals cut off at yesterday; remaining days forecast via prior-week same-day actual + const cutoff = isCurrentMonth ? yesterdayStr : toStr - // Build dept summary: actual MTD + forecast EOM const deptSummary = depts.map((dep, idx) => { const actualMTD = Object.entries(dep.days) - .filter(([d]) => d <= todayStr && d >= fromStr && d <= toStr) + .filter(([d]) => d >= fromStr && d <= cutoff) .reduce((s, [, v]) => s + v.cost, 0) let forecastRem = 0 if (isCurrentMonth) { for (let day = 1; day <= dim; day++) { const dateStr = `${monthStr}-${String(day).padStart(2, '0')}` - if (dateStr <= todayStr) continue - // Only trust rota for current Mon–Sun week; beyond that use prior-week actuals - if (dateStr <= currentWeekEndStr) { - const rota = getSchCost(dep.department_id, dateStr) - if (rota != null) { forecastRem += rota; continue } - } + if (dateStr <= yesterdayStr) continue const priorStr = fmt(addDays(new Date(dateStr + 'T00:00:00'), -7)) const priorCost = dep.days[priorStr]?.cost if (priorCost != null) forecastRem += priorCost @@ -165,9 +156,18 @@ export default function Monthly() { const totalActual = deptSummary.reduce((s, d) => s + d.actual_mtd, 0) const totalForecast = deptSummary.reduce((s, d) => s + d.forecast_eom, 0) const pyTotalWages = pyDepts.reduce((s, d) => s + d.cost, 0) - 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 + + // MTD row: pro-rata budget to yesterday + const yDay = isCurrentMonth ? parseInt(yesterdayStr.slice(8)) : dim + const budgetMTD = budget != null ? budget * yDay / dim : null + const varianceMTD = budgetMTD != null ? totalActual - budgetMTD : null + const pctBudgMTD = budgetMTD != null && budgetMTD > 0 ? (totalActual / budgetMTD) * 100 : null + const pctSalesMTD = netSalesMTD > 0 ? (totalActual / netSalesMTD) * 100 : null + + // Full month row + const varianceFull = budget != null ? totalForecast - budget : null + const pctBudgFull = budget != null && budget > 0 ? (totalForecast / budget) * 100 : null + const pctSalesFull = netSalesFull > 0 ? (totalForecast / netSalesFull) * 100 : null function pyPct(current: number, py: number): string { if (py <= 0 || current <= 0) return '' @@ -175,32 +175,26 @@ export default function Monthly() { return ` (${p >= 0 ? '+' : ''}${p.toFixed(1)}%)` } - // Build chart data with per-dept costs per week so Bar dataKey works + // Chart: past weeks solid, current/future weeks lighter type WeekEntry = { label: string; isPast: boolean; [dept: string]: number | boolean | string } const weeks: WeekEntry[] = [] for (let w = 0; w * 7 < dim; w++) { - const wStart = w * 7 + 1 - const wEnd = Math.min(wStart + 6, dim) + const wStart = w * 7 + 1 + const wEnd = Math.min(wStart + 6, dim) const wEndStr = `${monthStr}-${String(wEnd).padStart(2, '0')}` - const isPast = wEndStr < todayStr + const isPast = wEndStr <= yesterdayStr const entry: WeekEntry = { label: `W${w + 1}`, isPast } - for (const dep of deptSummary) { const srcDep = depts.find(d => d.department_id === dep.department_id) let deptCost = 0 for (let day = wStart; day <= wEnd; day++) { const dateStr = `${monthStr}-${String(day).padStart(2, '0')}` - if (dateStr <= todayStr) { + if (dateStr <= cutoff) { deptCost += srcDep?.days[dateStr]?.cost ?? 0 } else { - const rota = dateStr <= currentWeekEndStr ? getSchCost(dep.department_id, dateStr) : null - if (rota != null) { - deptCost += rota - } else { - const priorStr = fmt(addDays(new Date(dateStr + 'T00:00:00'), -7)) - deptCost += srcDep?.days[priorStr]?.cost ?? 0 - } + const priorStr = fmt(addDays(new Date(dateStr + 'T00:00:00'), -7)) + deptCost += srcDep?.days[priorStr]?.cost ?? 0 } } entry[dep.department_name] = deptCost @@ -225,51 +219,103 @@ export default function Monthly() { -
-
-
{isCurrentMonth ? 'Actual MTD' : 'Actual'}
-
{fmtMoney(totalActual)}
- {pyWages != null &&
{isCurrentMonth ? 'PY MTD' : 'PY'} {fmtMoney(pyWages)}{pyPct(totalActual, pyWages)}
} -
- {isCurrentMonth && ( + {/* ── Summary cards ─────────────────────────────────────────── */} + {isCurrentMonth ? ( + <> +
Month to {fmtDisplay(yesterdayStr)}
+
+
+
Actual MTD
+
{fmtMoney(totalActual)}
+ {pyWages != null &&
PY MTD {fmtMoney(pyWages)}{pyPct(totalActual, pyWages)}
} +
+
+
Budget MTD
+
{budgetMTD != null ? fmtMoney(budgetMTD) : '—'}
+
pro-rata {yDay}/{dim} days
+
+
+
% Budget MTD
+
+ {pctBudgMTD != null ? fmtDelta(pctBudgMTD) : '—'} +
+ {varianceMTD != null && ( +
0 ? 'variance-over' : 'variance-under'}`}> + {varianceMTD > 0 ? `+${fmtMoney(varianceMTD)} over` : `${fmtMoney(Math.abs(varianceMTD))} under`} +
+ )} +
+
+
% Net Sales MTD
+
{pctSalesMTD != null ? `${pctSalesMTD.toFixed(1)}%` : '—'}
+ {pyWages != null && pySalesMTD > 0 && ( +
PY MTD {((pyWages / pySalesMTD) * 100).toFixed(1)}%
+ )} +
+
+ +
Full month forecast
+
+
+
Forecast EOM
+
{fmtMoney(totalForecast)}
+ {pyWagesFull != null &&
PY {fmtMoney(pyWagesFull)}{pyPct(totalForecast, pyWagesFull)}
} +
+
+
Monthly Budget
+
{budget != null ? fmtMoney(budget) : '—'}
+
+
+
% Budget (Forecast)
+
+ {pctBudgFull != null ? fmtDelta(pctBudgFull) : '—'} +
+ {varianceFull != null && ( +
0 ? 'variance-over' : 'variance-under'}`}> + {varianceFull > 0 ? `+${fmtMoney(varianceFull)} over` : `${fmtMoney(Math.abs(varianceFull))} under`} +
+ )} +
+
+
% Net Sales (OTB)
+
{pctSalesFull != null ? `${pctSalesFull.toFixed(1)}%` : '—'}
+ {pyWagesFull != null && pySalesFull > 0 && ( +
PY {((pyWagesFull / pySalesFull) * 100).toFixed(1)}%
+ )} +
+
+ + ) : ( +
-
Forecast EOM
-
{fmtMoney(totalForecast)}
-
- {pyWagesFull != null - ? `PY ${fmtMoney(pyWagesFull)}${pyPct(totalForecast, pyWagesFull)}` - : 'rota + prior-week actual'} -
+
Actual
+
{fmtMoney(totalActual)}
+ {pyWagesFull != null &&
PY {fmtMoney(pyWagesFull)}{pyPct(totalActual, pyWagesFull)}
}
- )} -
-
Monthly Budget
-
{budget != null ? fmtMoney(budget) : '—'}
-
-
-
% Budget {isCurrentMonth ? '(Forecast)' : ''}
-
- {pctBudget != null ? fmtDelta(pctBudget) : '—'} +
+
Monthly Budget
+
{budget != null ? fmtMoney(budget) : '—'}
- {variance != null && ( -
0 ? 'variance-over' : 'variance-under'}`}> - {variance > 0 ? `+${fmtMoney(variance)} over` : `${fmtMoney(Math.abs(variance))} under`} +
+
% Budget
+
+ {pctBudgFull != null ? fmtDelta(pctBudgFull) : '—'}
- )} + {varianceFull != null && ( +
0 ? 'variance-over' : 'variance-under'}`}> + {varianceFull > 0 ? `+${fmtMoney(varianceFull)} over` : `${fmtMoney(Math.abs(varianceFull))} under`} +
+ )} +
+
+
% Net Sales
+
{pctSalesFull != null ? `${pctSalesFull.toFixed(1)}%` : '—'}
+ {pyWagesFull != null && pySalesFull > 0 && ( +
PY {((pyWagesFull / pySalesFull) * 100).toFixed(1)}%
+ )} +
-
-
{isCurrentMonth ? 'Net Sales MTD' : 'Net Sales'}
-
{fmtMoney(netSales)}
- {pySales > 0 &&
{isCurrentMonth ? 'PY MTD' : 'PY'} {fmtMoney(pySales)}{pyPct(netSales, pySales)}
} -
-
-
% Net Sales
-
{pctSales != null ? `${pctSales.toFixed(1)}%` : '—'}
- {pyWages != null && pySales > 0 && ( -
{isCurrentMonth ? 'PY MTD' : 'PY'} {((pyWages / pySales) * 100).toFixed(1)}%
- )} -
-
+ )} {loading &&
Loading…
} {error &&
{error}
} @@ -321,7 +367,10 @@ export default function Monthly() { return ( setModal({ deptId: dep.department_id, deptName: dep.department_name })}> - {dep.department_name} + + + {dep.department_name} + {fmtMoney(dep.actual_mtd)} {isCurrentMonth && {fmtMoney(dep.forecast_eom)}} @@ -344,10 +393,10 @@ export default function Monthly() { 100% {budget != null ? fmtMoney(budget) : '—'} - {pctBudget != null ? {fmtDelta(pctBudget)} : '—'} + {pctBudgFull != null ? {fmtDelta(pctBudgFull)} : '—'} - {variance != null && 0 ? 'variance-over' : 'variance-under'}>{variance > 0 ? '+' : ''}{fmtMoney(variance)}} + {varianceFull != null && 0 ? 'variance-over' : 'variance-under'}>{varianceFull > 0 ? '+' : ''}{fmtMoney(varianceFull)}} @@ -376,7 +425,7 @@ export default function Monthly() { {pyDepts.map(dep => { const ofTotal = pyTotalWages > 0 ? (dep.cost / pyTotalWages) * 100 : null - const ofSales = pySales > 0 ? (dep.cost / pySales) * 100 : null + const ofSales = pySalesMTD > 0 ? (dep.cost / pySalesMTD) * 100 : null return ( {dep.name} @@ -390,7 +439,7 @@ export default function Monthly() { Total {fmtMoney(pyTotalWages)} 100% - {pySales > 0 ? `${((pyTotalWages / pySales) * 100).toFixed(1)}%` : '—'} + {pySalesMTD > 0 ? `${((pyTotalWages / pySalesMTD) * 100).toFixed(1)}%` : '—'} @@ -403,7 +452,7 @@ export default function Monthly() { {modal && ( setModal(null)} diff --git a/frontend/src/pages/Weekly.tsx b/frontend/src/pages/Weekly.tsx index 8823319..021b8c6 100644 --- a/frontend/src/pages/Weekly.tsx +++ b/frontend/src/pages/Weekly.tsx @@ -51,11 +51,13 @@ export default function Weekly() { const toStr = addDaysStr(fromStr, 6) const todayStr = localStr(new Date()) + const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1) + const yesterdayStr = localStr(yesterday) const isCurrentWeek = fromStr === mondayOf(new Date()) const pyFromStr = addDaysStr(fromStr, -364) - // For current (partial) week cap PY at equivalent elapsed day — fair WTD comparison + // Cut-off at yesterday: avoids partial clockins/open timesheets skewing today's figures const daysElapsed = isCurrentWeek - ? Math.round((new Date(todayStr + '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 const pyToStr = addDaysStr(fromStr, -364 + daysElapsed) @@ -87,12 +89,16 @@ export default function Weekly() { setDepts(actRes.departments) setShowOncosts(actRes.show_oncosts) setDeptPcts(actRes.dept_pcts) - setNetSales(salesRes.days.reduce((s, d) => s + d.net_sales, 0)) - // For current week only sum PY sales for elapsed days (WTD = apples to apples) + // 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 <= todayStr).reduce((s, d) => s + d.py_sales, 0) + ? 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) ) @@ -110,10 +116,10 @@ export default function Weekly() { const bRow = (budgetRes.budgets as WageBudget[]).find(b => b.month === monthKey) if (bRow) { const dim = daysInMonthFor(fromStr) - const effectiveTo = todayStr < toStr ? todayStr : toStr - const effectiveFrom = fromStr > todayStr ? todayStr : fromStr - const weekDays = effectiveTo >= effectiveFrom - ? Math.round((new Date(effectiveTo + 'T00:00:00').getTime() - new Date(effectiveFrom + 'T00:00:00').getTime()) / 86_400_000) + 1 + 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) @@ -151,7 +157,9 @@ export default function Weekly() { const deptTotals = depts.map(dep => ({ department_id: dep.department_id, department_name: dep.department_name, - cost: Object.values(dep.days).reduce((s, d) => s + d.cost, 0), + cost: Object.entries(dep.days) + .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)