fix(reports): guard table rendering against stale payloads

This commit is contained in:
2026-04-30 16:13:11 +03:30
parent e635fd9c2c
commit 0e8d43f1ea

View File

@@ -153,6 +153,17 @@ export function ReportsTablePanel({
if (!data) return null; if (!data) return null;
const days = Array.isArray(data.days) ? data.days : [];
const clients = Array.isArray(data.clients) ? data.clients : [];
const projects = Array.isArray(data.projects) ? data.projects : [];
const tags = Array.isArray(data.tags) ? data.tags : [];
const entries = Array.isArray(dayDetails?.entries) ? dayDetails.entries : [];
const summary = data.summary ?? {
billable_duration: "00:00:00",
non_billable_duration: "00:00:00",
income_totals: [],
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex flex-col gap-2 sm:flex-row sm:justify-end"> <div className="flex flex-col gap-2 sm:flex-row sm:justify-end">
@@ -188,7 +199,7 @@ export function ReportsTablePanel({
<div className="mb-4 text-sm font-semibold text-slate-900 dark:text-white">{labels.details}</div> <div className="mb-4 text-sm font-semibold text-slate-900 dark:text-white">{labels.details}</div>
<div className="space-y-3 sm:hidden"> <div className="space-y-3 sm:hidden">
{data.days.map((day) => { {days.map((day) => {
const isOpen = openDay === day.date; const isOpen = openDay === day.date;
return ( return (
<div key={day.date} className="rounded-2xl border border-slate-200 bg-slate-50/80 p-3 dark:border-slate-800 dark:bg-slate-950/70"> <div key={day.date} className="rounded-2xl border border-slate-200 bg-slate-50/80 p-3 dark:border-slate-800 dark:bg-slate-950/70">
@@ -229,7 +240,7 @@ export function ReportsTablePanel({
{isOpen && dayDetails?.day === day.date ? ( {isOpen && dayDetails?.day === day.date ? (
<div className="mt-3 space-y-2 border-t border-slate-200 pt-3 dark:border-slate-800"> <div className="mt-3 space-y-2 border-t border-slate-200 pt-3 dark:border-slate-800">
{dayDetails.entries.map((entry) => ( {entries.map((entry) => (
<div key={entry.id} className="rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-700 dark:bg-slate-900"> <div key={entry.id} className="rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-700 dark:bg-slate-900">
<div className="text-sm font-medium text-slate-900 dark:text-slate-100"> <div className="text-sm font-medium text-slate-900 dark:text-slate-100">
{entry.description || labels.noDescription} {entry.description || labels.noDescription}
@@ -249,9 +260,9 @@ export function ReportsTablePanel({
<div className="rounded-2xl border border-sky-200 bg-sky-50 p-3 dark:border-sky-500/20 dark:bg-sky-500/10"> <div className="rounded-2xl border border-sky-200 bg-sky-50 p-3 dark:border-sky-500/20 dark:bg-sky-500/10">
<div className="text-sm font-semibold text-slate-900 dark:text-white">{labels.total}</div> <div className="text-sm font-semibold text-slate-900 dark:text-white">{labels.total}</div>
<div className="mt-3 grid grid-cols-2 gap-3 text-xs text-slate-700 dark:text-slate-300"> <div className="mt-3 grid grid-cols-2 gap-3 text-xs text-slate-700 dark:text-slate-300">
<div>{labels.billableHours}: {localizeDigits(data.summary.billable_duration, lang)}</div> <div>{labels.billableHours}: {localizeDigits(summary.billable_duration, lang)}</div>
<div>{labels.nonBillableHours}: {localizeDigits(data.summary.non_billable_duration, lang)}</div> <div>{labels.nonBillableHours}: {localizeDigits(summary.non_billable_duration, lang)}</div>
<div>{labels.totalIncome}: {formatMoneyTotals(data.summary.income_totals, lang)}</div> <div>{labels.totalIncome}: {formatMoneyTotals(summary.income_totals, lang)}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -269,7 +280,7 @@ export function ReportsTablePanel({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.days.map((day) => { {days.map((day) => {
const isOpen = openDay === day.date; const isOpen = openDay === day.date;
return ( return (
<Fragment key={day.date}> <Fragment key={day.date}>
@@ -293,7 +304,7 @@ export function ReportsTablePanel({
<tr className="border-b border-slate-100 bg-slate-50/70 dark:border-slate-800/80 dark:bg-slate-950/70"> <tr className="border-b border-slate-100 bg-slate-50/70 dark:border-slate-800/80 dark:bg-slate-950/70">
<td colSpan={6} className="px-3 py-4"> <td colSpan={6} className="px-3 py-4">
<div className="space-y-2"> <div className="space-y-2">
{dayDetails.entries.map((entry) => ( {entries.map((entry) => (
<div key={entry.id} className="rounded-2xl border border-slate-200 bg-white px-4 py-3 dark:border-slate-700 dark:bg-slate-900"> <div key={entry.id} className="rounded-2xl border border-slate-200 bg-white px-4 py-3 dark:border-slate-700 dark:bg-slate-900">
<div className="flex flex-wrap items-center gap-2 text-sm"> <div className="flex flex-wrap items-center gap-2 text-sm">
<span className="font-medium text-slate-900 dark:text-slate-100">{entry.description || labels.noDescription}</span> <span className="font-medium text-slate-900 dark:text-slate-100">{entry.description || labels.noDescription}</span>
@@ -312,10 +323,10 @@ export function ReportsTablePanel({
})} })}
<tr className="bg-sky-50/80 font-semibold dark:bg-sky-500/10"> <tr className="bg-sky-50/80 font-semibold dark:bg-sky-500/10">
<td className="px-3 py-3">{labels.total}</td> <td className="px-3 py-3">{labels.total}</td>
<td className="px-3 py-3">{localizeDigits(data.summary.billable_duration, lang)}</td> <td className="px-3 py-3">{localizeDigits(summary.billable_duration, lang)}</td>
<td className="px-3 py-3">{localizeDigits(data.summary.non_billable_duration, lang)}</td> <td className="px-3 py-3">{localizeDigits(summary.non_billable_duration, lang)}</td>
<td className="px-3 py-3">-</td> <td className="px-3 py-3">-</td>
<td className="px-3 py-3">{formatMoneyTotals(data.summary.income_totals, lang)}</td> <td className="px-3 py-3">{formatMoneyTotals(summary.income_totals, lang)}</td>
<td className="px-3 py-3" /> <td className="px-3 py-3" />
</tr> </tr>
</tbody> </tbody>
@@ -323,9 +334,9 @@ export function ReportsTablePanel({
</div> </div>
</div> </div>
<BreakdownCards title={labels.clientsTable} rows={data.clients} labels={labels} lang={lang} /> <BreakdownCards title={labels.clientsTable} rows={clients} labels={labels} lang={lang} />
<BreakdownCards title={labels.projectsTable} rows={data.projects} labels={labels} lang={lang} /> <BreakdownCards title={labels.projectsTable} rows={projects} labels={labels} lang={lang} />
<BreakdownCards title={labels.tagsTable} rows={data.tags} labels={labels} lang={lang} /> <BreakdownCards title={labels.tagsTable} rows={tags} labels={labels} lang={lang} />
</div> </div>
); );
} }