107 lines
4.5 KiB
JavaScript
107 lines
4.5 KiB
JavaScript
import { todayStr } from '@/lib/utils';
|
|
|
|
export const MONTHS = [
|
|
'January','February','March','April','May','June',
|
|
'July','August','September','October','November','December',
|
|
];
|
|
export const FILTER_ALL = 'all';
|
|
// Sentinel for the "no method" select option — empty string crashes Radix Select
|
|
export const METHOD_NONE = 'none';
|
|
|
|
export const ROW_STATUS_CLS = {
|
|
paid: 'bg-emerald-500/[0.04] dark:bg-emerald-400/[0.02]',
|
|
autodraft: 'bg-sky-500/[0.04] dark:bg-sky-400/[0.018]',
|
|
upcoming: '',
|
|
due_soon: 'bg-amber-400/[0.07] dark:bg-amber-300/[0.016]',
|
|
late: 'border-l-4 border-l-orange-400 bg-orange-500/[0.16] ring-1 ring-inset ring-orange-400/25 dark:bg-orange-400/[0.11] dark:ring-orange-300/25',
|
|
missed: 'border-l-4 border-l-rose-400 bg-rose-500/[0.18] ring-1 ring-inset ring-rose-400/30 dark:bg-rose-400/[0.13] dark:ring-rose-300/30',
|
|
};
|
|
|
|
export const STATUS_META = {
|
|
paid: { label: 'Paid', cls: 'bg-emerald-500/15 text-emerald-500 border border-emerald-500/30 dark:bg-emerald-300/10 dark:text-emerald-200 dark:border-emerald-300/30' },
|
|
upcoming: { label: 'Upcoming', cls: 'bg-secondary text-muted-foreground border border-border' },
|
|
due_soon: { label: 'Due Soon', cls: 'bg-amber-400/15 text-amber-500 border border-amber-400/30 dark:bg-amber-300/10 dark:text-amber-200 dark:border-amber-300/28' },
|
|
late: { label: 'Late', cls: 'bg-orange-500/30 text-orange-800 border border-orange-500/60 shadow-sm shadow-orange-950/10 dark:bg-orange-400/25 dark:text-orange-100 dark:border-orange-300/60' },
|
|
missed: { label: 'Missed', cls: 'bg-rose-500/30 text-rose-800 border border-rose-500/70 shadow-sm shadow-rose-950/10 dark:bg-rose-400/25 dark:text-rose-100 dark:border-rose-300/60' },
|
|
autodraft: { label: 'Autodraft', cls: 'bg-sky-400/15 text-sky-500 border border-sky-400/30 dark:bg-sky-300/10 dark:text-sky-200 dark:border-sky-300/28' },
|
|
skipped: { label: 'Skipped', cls: 'bg-muted text-muted-foreground border border-border' },
|
|
};
|
|
|
|
export function paymentDateForTrackerMonth(year, month, dueDay) {
|
|
const now = new Date();
|
|
if (year === now.getFullYear() && month === now.getMonth() + 1) {
|
|
return todayStr();
|
|
}
|
|
|
|
const daysInMonth = new Date(year, month, 0).getDate();
|
|
const day = Number.isInteger(Number(dueDay))
|
|
? Math.min(Math.max(Number(dueDay), 1), daysInMonth)
|
|
: 1;
|
|
|
|
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
}
|
|
|
|
export function amountSearchText(...values) {
|
|
return values
|
|
.filter(value => value !== null && value !== undefined && Number.isFinite(Number(value)))
|
|
.flatMap(value => {
|
|
const num = Number(value);
|
|
return [String(num), num.toFixed(2), `$${num.toFixed(2)}`];
|
|
})
|
|
.join(' ');
|
|
}
|
|
|
|
export function rowThreshold(row) {
|
|
return row.actual_amount != null ? row.actual_amount : row.expected_amount;
|
|
}
|
|
|
|
export function rowEffectiveStatus(row) {
|
|
if (row.is_skipped) return 'skipped';
|
|
const threshold = rowThreshold(row);
|
|
const isPaidByThreshold = row.total_paid > 0 && row.total_paid >= threshold;
|
|
return (isPaidByThreshold && row.status !== 'paid' && row.status !== 'autodraft') ? 'paid' : row.status;
|
|
}
|
|
|
|
export function rowIsPaid(row) {
|
|
const status = rowEffectiveStatus(row);
|
|
if (row.autopay_suggestion && status === 'autodraft') return false;
|
|
return status === 'paid' || status === 'autodraft';
|
|
}
|
|
|
|
export function rowIsDebt(row) {
|
|
const category = String(row.category_name || '').toLowerCase();
|
|
return Number(row.current_balance) > 0
|
|
|| row.minimum_payment != null
|
|
|| ['credit card', 'credit cards', 'loan', 'loans', 'debt', 'mortgage'].some(token => category.includes(token));
|
|
}
|
|
|
|
export function moveInArray(items, fromIndex, toIndex) {
|
|
const next = [...items];
|
|
const [moved] = next.splice(fromIndex, 1);
|
|
next.splice(toIndex, 0, moved);
|
|
return next;
|
|
}
|
|
|
|
export function paymentSummary(row, threshold) {
|
|
const target = Number(threshold) || 0;
|
|
const paid = Number(row.total_paid) || 0;
|
|
const paidTowardDue = Number.isFinite(Number(row.paid_toward_due))
|
|
? Number(row.paid_toward_due)
|
|
: Math.min(paid, target);
|
|
const overpaid = Number.isFinite(Number(row.overpaid_amount))
|
|
? Number(row.overpaid_amount)
|
|
: Math.max(paid - target, 0);
|
|
const remaining = Math.max(target - paidTowardDue, 0);
|
|
const percent = target > 0 ? Math.min(100, Math.round((paidTowardDue / target) * 100)) : 0;
|
|
return {
|
|
target,
|
|
paid,
|
|
paidTowardDue,
|
|
overpaid,
|
|
remaining,
|
|
percent,
|
|
count: Array.isArray(row.payments) ? row.payments.length : 0,
|
|
partial: paid > 0 && remaining > 0,
|
|
};
|
|
}
|