mega-plant/components/InvoiceMonthFilter.tsx

20 lines
1.4 KiB
TypeScript
Raw Normal View History

feat(income): posted income per site, and the LIVE month on the overview Two changes to the income overview, sharing one rule for what "the month" means (lib/income-month.ts). POSTED INCOME PER SITE /income gains Vehicles / Sites views, and /sites defaults to the SAME posted-income view with a separate Internal billed invoices view. buildSiteIncomeRows (lib/site-income.ts) is the shared calculation: actual monthly_income supplies the amount, year and month, and the linked invoice supplies its historical site. It never uses assets.site, fuel-log locations or today's map assignment. - Unposted invoice lines are billing, not posted income. - Historical workbook income with no site evidence stays in Unallocated historical income; external income, whose site field names the SUPPLIER, stays in Unallocated external income. - Missing source references stay visible as unallocated invoice income. No group is dropped to make a site list look cleaner and no site is guessed to force a total. - No site profit is calculated without an explicit cost-allocation rule. - Invoice writes use revalidateInvoicePages, which invalidates both registers, Income, Sites/details, Charts and Dashboard. THE LIVE MONTH The tiles show the year-to-date income and profit AND the current month, and every vehicle card shows the current month income beside its year figure. - The month is the live CALENDAR month and it is shown even at R0. Income for a month is usually entered near the end of it, so the current month is a figure that starts at R0 and grows. - The month index is computed on the SERVER and passed down as a number. The browser never works it out for itself: two clocks disagreeing would show two different months on the same page. - A month profit is a dash until that month costs are posted. September and October carry invoice income with no posted fixed costs (their costs exist only as forecast rows), so a profit there would be income minus zero - a 100% margin that does not tie back to the year-to-date profit. It becomes a number by itself the moment the workbook posts that month. - The 12-month strip shows the live month as R0 rather than a dash, so a card cannot contradict itself. Months after it stay a dash. - The sparkline on a collapsed card is hidden below xl: the content column is only ~700px wide between md and xl, and the vehicle name must not be truncated to pay for a decorative trend.
2026-11-04 22:19:06 +00:00
"use client";
import {usePathname,useRouter,useSearchParams} from 'next/navigation';
import {useTransition} from 'react';
export function InvoiceMonthFilter({selected,current,periods}:{selected:string;current:string;periods:string[]}){
const router=useRouter(),path=usePathname(),params=useSearchParams();
const [pending,startTransition]=useTransition();
const change=(value:string)=>{const query=new URLSearchParams(params.toString());query.set('month',value);startTransition(()=>router.push(`${path}?${query}`));};
const label=(period:string)=>new Date(`${period}-01T12:00:00Z`).toLocaleDateString('en-ZA',{month:'long',year:'numeric',timeZone:'Africa/Johannesburg'});
return <div className="mb-5 flex flex-wrap items-end gap-3" aria-busy={pending}>
<label className="flex min-w-0 flex-col gap-1 text-sm text-gray-600 dark:text-gray-300">Booked month
<select aria-label="Booked month" value={selected} onChange={e=>change(e.target.value)} className="min-h-[44px] max-w-full rounded-lg border border-gray-200 bg-white px-3 text-sm text-[#003D5B] dark:border-white/10 dark:bg-surface dark:text-white">
<option value="all">All months</option>
{periods.map(p=><option key={p} value={p}>{label(p)}{p===current?' (current month)':''}</option>)}
</select>
</label>
<p className="pb-3 text-xs text-gray-500 dark:text-gray-400">{pending?'Updating invoices…':'Totals and booking status follow the selected month.'}</p>
</div>;
}