refactor(ts): BillsPage → TypeScript
Bill list/reorder/history-visibility page typed: Bill[] from useBills, BillFilters/DisplayPrefs/ModalState/HistoryRange local types, drag + move-controls typed via DragProps/MoveControls, makeBillDraft(...) as Partial<Bill> at the modal call sites, setQueryData<Bill[]> for optimistic list edits, errMessage(unknown) for strict catches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3b1e7e4da9
commit
aa9c357fdf
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
|
||||
import { useEffect, useState, useCallback, useMemo, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useBills, useCategories, useBillTemplates, useDeletedBills } from '@/hooks/useQueries';
|
||||
|
|
@ -20,14 +20,19 @@ import {
|
|||
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
|
||||
} from '@/components/ui/select';
|
||||
import { api } from '@/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cn, errMessage } from '@/lib/utils';
|
||||
import { useSearchPanelPreference } from '@/hooks/useSearchPanelPreference';
|
||||
import { moveInArray, movedItemId, reorderPayload } from '@/lib/reorder';
|
||||
import BillsTableInner from '@/components/BillsTableInner';
|
||||
import BillModal from '@/components/BillModal';
|
||||
import RecentlyDeletedBillsDialog from '@/components/RecentlyDeletedBillsDialog';
|
||||
import { makeBillDraft } from '@/lib/billDrafts';
|
||||
import { makeBillDraft, type SourceBill } from '@/lib/billDrafts';
|
||||
import { scheduleLabel, scheduleValue } from '@/lib/billingSchedule';
|
||||
import type { Bill } from '@/types';
|
||||
import type { DragProps, MoveControls } from '@/components/tracker/MobileTrackerRow';
|
||||
|
||||
interface BuiltInTemplate { id: string; name: string; data: SourceBill; categoryKeywords: string[] }
|
||||
interface BillTemplate { id: number | string; name: string; data?: SourceBill; categoryKeywords?: string[] }
|
||||
|
||||
const VISIBILITY_OPTIONS = [
|
||||
{ value: 'default', label: 'Default', description: 'Use the app default behavior for inactive bill history.' },
|
||||
|
|
@ -35,13 +40,13 @@ const VISIBILITY_OPTIONS = [
|
|||
{ value: 'none', label: 'Show no history', description: 'Hide historical tracker data for this inactive bill.' },
|
||||
{ value: 'ranges', label: 'Show only selected date ranges', description: 'Show history only for the ranges listed below.' },
|
||||
];
|
||||
const MONTH_OPTIONS = [
|
||||
const MONTH_OPTIONS: [string, string][] = [
|
||||
['1', 'Jan'], ['2', 'Feb'], ['3', 'Mar'], ['4', 'Apr'], ['5', 'May'], ['6', 'Jun'],
|
||||
['7', 'Jul'], ['8', 'Aug'], ['9', 'Sep'], ['10', 'Oct'], ['11', 'Nov'], ['12', 'Dec'],
|
||||
];
|
||||
const FILTER_ALL = 'all';
|
||||
|
||||
const BUILT_IN_TEMPLATES = [
|
||||
const BUILT_IN_TEMPLATES: BuiltInTemplate[] = [
|
||||
{
|
||||
id: 'builtin-utility',
|
||||
name: 'Utility',
|
||||
|
|
@ -122,7 +127,26 @@ const BUILT_IN_TEMPLATES = [
|
|||
|
||||
// BillModal is imported from @/components/BillModal
|
||||
|
||||
function blankRange() {
|
||||
interface HistoryRange {
|
||||
id?: number;
|
||||
_key: string;
|
||||
start_year: string;
|
||||
start_month: string;
|
||||
end_year: string;
|
||||
end_month: string;
|
||||
label: string;
|
||||
}
|
||||
interface RawRange {
|
||||
id?: number;
|
||||
_key?: string;
|
||||
start_year?: number | string | null;
|
||||
start_month?: number | string | null;
|
||||
end_year?: number | string | null;
|
||||
end_month?: number | string | null;
|
||||
label?: string | null;
|
||||
}
|
||||
|
||||
function blankRange(): HistoryRange {
|
||||
const year = String(new Date().getFullYear());
|
||||
return {
|
||||
_key: `new-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
|
|
@ -134,7 +158,7 @@ function blankRange() {
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeRange(range) {
|
||||
function normalizeRange(range: RawRange): HistoryRange {
|
||||
return {
|
||||
...range,
|
||||
_key: range._key || String(range.id),
|
||||
|
|
@ -146,7 +170,7 @@ function normalizeRange(range) {
|
|||
};
|
||||
}
|
||||
|
||||
function rangePayload(range) {
|
||||
function rangePayload(range: HistoryRange) {
|
||||
return {
|
||||
start_year: parseInt(range.start_year, 10),
|
||||
start_month: parseInt(range.start_month, 10),
|
||||
|
|
@ -156,7 +180,7 @@ function rangePayload(range) {
|
|||
};
|
||||
}
|
||||
|
||||
function validateRange(range) {
|
||||
function validateRange(range: HistoryRange): string | null {
|
||||
const payload = rangePayload(range);
|
||||
if (!Number.isInteger(payload.start_year) || payload.start_year < 2000 || payload.start_year > 2100) {
|
||||
return 'Start year must be between 2000 and 2100.';
|
||||
|
|
@ -167,7 +191,7 @@ function validateRange(range) {
|
|||
if ((payload.end_year == null) !== (payload.end_month == null)) {
|
||||
return 'End year and end month must both be provided or both left blank.';
|
||||
}
|
||||
if (payload.end_year != null) {
|
||||
if (payload.end_year != null && payload.end_month != null) {
|
||||
if (payload.end_year < 2000 || payload.end_year > 2100) {
|
||||
return 'End year must be between 2000 and 2100.';
|
||||
}
|
||||
|
|
@ -185,7 +209,20 @@ function validateRange(range) {
|
|||
|
||||
const PREFS_KEY = 'bills-display-prefs-v1';
|
||||
|
||||
const PREFS_DEFAULTS = {
|
||||
interface DisplayPrefs {
|
||||
showCategory: boolean;
|
||||
showDueDay: boolean;
|
||||
showAmount: boolean;
|
||||
showCycle: boolean;
|
||||
showApr: boolean;
|
||||
showBalance: boolean;
|
||||
showMinPayment: boolean;
|
||||
showAutopay: boolean;
|
||||
show2fa: boolean;
|
||||
showSubscription: boolean;
|
||||
}
|
||||
|
||||
const PREFS_DEFAULTS: DisplayPrefs = {
|
||||
showCategory: true,
|
||||
showDueDay: true,
|
||||
showAmount: true,
|
||||
|
|
@ -198,7 +235,7 @@ const PREFS_DEFAULTS = {
|
|||
showSubscription: true,
|
||||
};
|
||||
|
||||
const PREFS_LABELS = [
|
||||
const PREFS_LABELS: [keyof DisplayPrefs, string][] = [
|
||||
['showCategory', 'Category'],
|
||||
['showDueDay', 'Due day'],
|
||||
['showAmount', 'Amount'],
|
||||
|
|
@ -212,7 +249,7 @@ const PREFS_LABELS = [
|
|||
];
|
||||
|
||||
function useDisplayPrefs() {
|
||||
const [prefs, setPrefs] = useState(() => {
|
||||
const [prefs, setPrefs] = useState<DisplayPrefs>(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem(PREFS_KEY);
|
||||
return raw ? { ...PREFS_DEFAULTS, ...JSON.parse(raw) } : PREFS_DEFAULTS;
|
||||
|
|
@ -221,10 +258,10 @@ function useDisplayPrefs() {
|
|||
}
|
||||
});
|
||||
|
||||
const toggle = (key) => {
|
||||
const toggle = (key: keyof DisplayPrefs) => {
|
||||
setPrefs(prev => {
|
||||
const next = { ...prev, [key]: !prev[key] };
|
||||
try { localStorage.setItem(PREFS_KEY, JSON.stringify(next)); } catch {}
|
||||
try { localStorage.setItem(PREFS_KEY, JSON.stringify(next)); } catch { /* ignore quota errors */ }
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
|
@ -232,14 +269,14 @@ function useDisplayPrefs() {
|
|||
return { prefs, toggle };
|
||||
}
|
||||
|
||||
function DisplayPrefsPanel({ prefs, onToggle }) {
|
||||
function DisplayPrefsPanel({ prefs, onToggle }: { prefs: DisplayPrefs; onToggle: (key: keyof DisplayPrefs) => void }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e) => {
|
||||
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
|
|
@ -290,7 +327,7 @@ function DisplayPrefsPanel({ prefs, onToggle }) {
|
|||
);
|
||||
}
|
||||
|
||||
function amountSearchText(...values) {
|
||||
function amountSearchText(...values: (number | string | null | undefined)[]): string {
|
||||
return values
|
||||
.filter(value => value !== null && value !== undefined && Number.isFinite(Number(value)))
|
||||
.flatMap(value => {
|
||||
|
|
@ -300,14 +337,14 @@ function amountSearchText(...values) {
|
|||
.join(' ');
|
||||
}
|
||||
|
||||
function billIsDebt(bill) {
|
||||
function billIsDebt(bill: Bill): boolean {
|
||||
const category = String(bill.category_name || '').toLowerCase();
|
||||
return Number(bill.current_balance) > 0
|
||||
|| bill.minimum_payment != null
|
||||
|| ['credit card', 'credit cards', 'loan', 'loans', 'debt', 'mortgage'].some(token => category.includes(token));
|
||||
}
|
||||
|
||||
function FilterChip({ active, children, onClick }) {
|
||||
function FilterChip({ active, children, onClick }: { active: boolean; children: React.ReactNode; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -327,7 +364,7 @@ function FilterChip({ active, children, onClick }) {
|
|||
const BILLS_SORT_KEY = 'bills_sort_mode';
|
||||
const BILLS_CADENCE_ORDER = ['weekly', 'biweekly', 'monthly', 'quarterly', 'annual', 'other'];
|
||||
|
||||
function normalizedBillCadence(bill) {
|
||||
function normalizedBillCadence(bill: Bill): string {
|
||||
const raw = String(bill?.cycle_type || bill?.billing_cycle || '').toLowerCase();
|
||||
if (raw.includes('week') && raw.includes('bi')) return 'biweekly';
|
||||
if (raw === 'biweekly') return 'biweekly';
|
||||
|
|
@ -338,12 +375,12 @@ function normalizedBillCadence(bill) {
|
|||
return 'other';
|
||||
}
|
||||
|
||||
function billCadenceIndex(bill) {
|
||||
function billCadenceIndex(bill: Bill): number {
|
||||
const index = BILLS_CADENCE_ORDER.indexOf(normalizedBillCadence(bill));
|
||||
return index >= 0 ? index : BILLS_CADENCE_ORDER.length - 1;
|
||||
}
|
||||
|
||||
function sortBillsByCadence(items) {
|
||||
function sortBillsByCadence(items: Bill[]): Bill[] {
|
||||
return [...items].sort((a, b) => (
|
||||
billCadenceIndex(a) - billCadenceIndex(b)
|
||||
|| (Number(a.due_day) || 0) - (Number(b.due_day) || 0)
|
||||
|
|
@ -351,7 +388,7 @@ function sortBillsByCadence(items) {
|
|||
));
|
||||
}
|
||||
|
||||
function SortModeButton({ active, children, onClick }) {
|
||||
function SortModeButton({ active, children, onClick }: { active: boolean; children: React.ReactNode; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -371,10 +408,10 @@ function SortModeButton({ active, children, onClick }) {
|
|||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function HistoryVisibilityDialog({ bill, onClose, onSaved }) {
|
||||
const [visibility, setVisibility] = useState(bill?.history_visibility || 'default');
|
||||
const [ranges, setRanges] = useState([]);
|
||||
const [deletedIds, setDeletedIds] = useState([]);
|
||||
function HistoryVisibilityDialog({ bill, onClose, onSaved }: { bill: Bill; onClose: () => void; onSaved: () => void }) {
|
||||
const [visibility, setVisibility] = useState<string>((bill?.history_visibility as string) || 'default');
|
||||
const [ranges, setRanges] = useState<HistoryRange[]>([]);
|
||||
const [deletedIds, setDeletedIds] = useState<number[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
|
|
@ -382,25 +419,26 @@ function HistoryVisibilityDialog({ bill, onClose, onSaved }) {
|
|||
let mounted = true;
|
||||
setLoading(true);
|
||||
api.billHistoryRanges(bill.id)
|
||||
.then(data => {
|
||||
.then(raw => {
|
||||
if (!mounted) return;
|
||||
setVisibility(data.history_visibility || bill.history_visibility || 'default');
|
||||
const data = raw as { history_visibility?: string; ranges?: RawRange[] };
|
||||
setVisibility(data.history_visibility || (bill.history_visibility as string) || 'default');
|
||||
setRanges((data.ranges || []).map(normalizeRange));
|
||||
})
|
||||
.catch(err => {
|
||||
toast.error(err.message || 'Failed to load history visibility.');
|
||||
toast.error(errMessage(err, 'Failed to load history visibility.'));
|
||||
onClose();
|
||||
})
|
||||
.finally(() => mounted && setLoading(false));
|
||||
.finally(() => { if (mounted) setLoading(false); });
|
||||
return () => { mounted = false; };
|
||||
}, [bill, onClose]);
|
||||
|
||||
const updateRange = (key, patch) => {
|
||||
const updateRange = (key: string, patch: Partial<HistoryRange>) => {
|
||||
setRanges(prev => prev.map(r => r._key === key ? { ...r, ...patch } : r));
|
||||
};
|
||||
|
||||
const deleteRange = (range) => {
|
||||
if (range.id) setDeletedIds(prev => [...prev, range.id]);
|
||||
const deleteRange = (range: HistoryRange) => {
|
||||
if (range.id) setDeletedIds(prev => [...prev, range.id!]);
|
||||
setRanges(prev => prev.filter(r => r._key !== range._key));
|
||||
};
|
||||
|
||||
|
|
@ -437,7 +475,7 @@ function HistoryVisibilityDialog({ bill, onClose, onSaved }) {
|
|||
onSaved();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to save history visibility.');
|
||||
toast.error(errMessage(err, 'Failed to save history visibility.'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
|
|
@ -598,6 +636,18 @@ function HistoryVisibilityDialog({ bill, onClose, onSaved }) {
|
|||
);
|
||||
}
|
||||
|
||||
interface ModalState { bill: Bill | null; initialBill?: Partial<Bill> }
|
||||
type BoolFilterKey = 'autopay' | 'firstBucket' | 'fifteenthBucket' | 'debt' | 'inactive';
|
||||
interface BillFilters {
|
||||
category: string;
|
||||
cycle: string;
|
||||
autopay: boolean;
|
||||
firstBucket: boolean;
|
||||
fifteenthBucket: boolean;
|
||||
debt: boolean;
|
||||
inactive: boolean;
|
||||
}
|
||||
|
||||
// ── Bills Page ─────────────────────────────────────────────────────────────
|
||||
export default function BillsPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
|
@ -607,11 +657,12 @@ export default function BillsPage() {
|
|||
// list edits below write to the cache via setQueryData.
|
||||
const { data: bills = [], isPending: billsPending } = useBills();
|
||||
const { data: categories = [] } = useCategories();
|
||||
const { data: savedTemplates = [] } = useBillTemplates();
|
||||
const { data: savedTemplatesRaw = [] } = useBillTemplates();
|
||||
const savedTemplates = savedTemplatesRaw as BillTemplate[];
|
||||
const { data: deletedBills = [] } = useDeletedBills();
|
||||
const loading = billsPending;
|
||||
const setBills = useCallback(
|
||||
(updater) => queryClient.setQueryData(['bills'], prev => (
|
||||
(updater: Bill[] | ((prev: Bill[]) => Bill[])) => queryClient.setQueryData<Bill[]>(['bills'], prev => (
|
||||
typeof updater === 'function' ? updater(prev || []) : updater
|
||||
)),
|
||||
[queryClient],
|
||||
|
|
@ -625,10 +676,10 @@ export default function BillsPage() {
|
|||
const [showDeleted, setShowDeleted] = useState(false);
|
||||
const [showInactive, setShowInactive] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [draggingId, setDraggingId] = useState(null);
|
||||
const [dropTargetId, setDropTargetId] = useState(null);
|
||||
const [movingBillId, setMovingBillId] = useState(null);
|
||||
const [filters, setFilters] = useState({
|
||||
const [draggingId, setDraggingId] = useState<number | null>(null);
|
||||
const [dropTargetId, setDropTargetId] = useState<number | null>(null);
|
||||
const [movingBillId, setMovingBillId] = useState<number | string | null>(null);
|
||||
const [filters, setFilters] = useState<BillFilters>({
|
||||
category: FILTER_ALL,
|
||||
cycle: FILTER_ALL,
|
||||
autopay: false,
|
||||
|
|
@ -639,18 +690,18 @@ export default function BillsPage() {
|
|||
});
|
||||
|
||||
// modal state: null = closed, { bill: null } = add new, { bill: {...} } = edit
|
||||
const [modal, setModal] = useState(null);
|
||||
const [modal, setModal] = useState<ModalState | null>(null);
|
||||
// Bill pending deactivation confirmation (AlertDialog replaces window.confirm)
|
||||
const [deactivate, setDeactivate] = useState(null);
|
||||
const [deactivate, setDeactivate] = useState<Bill | null>(null);
|
||||
const [deactivateReason, setDeactivateReason] = useState('');
|
||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<Bill | null>(null);
|
||||
const [deleteConfirmed, setDeleteConfirmed] = useState(false);
|
||||
const [deleteBusy, setDeleteBusy] = useState(false);
|
||||
const [historyTarget, setHistoryTarget] = useState(null);
|
||||
const [historyTarget, setHistoryTarget] = useState<Bill | null>(null);
|
||||
|
||||
const { prefs, toggle: togglePref } = useDisplayPrefs();
|
||||
|
||||
const [billsSort, setBillsSort] = useState(() => (
|
||||
const [billsSort, setBillsSort] = useState<'custom' | 'cadence'>(() => (
|
||||
localStorage.getItem(BILLS_SORT_KEY) === 'cadence' ? 'cadence' : 'custom'
|
||||
));
|
||||
const [searchPanelCollapsed, setSearchPanelCollapsed] = useSearchPanelPreference();
|
||||
|
|
@ -674,8 +725,8 @@ export default function BillsPage() {
|
|||
if (filters.inactive) setShowInactive(true);
|
||||
}, [filters.inactive]);
|
||||
|
||||
const toggleFilter = (key) => setFilters(prev => ({ ...prev, [key]: !prev[key] }));
|
||||
const setFilterValue = (key, value) => setFilters(prev => ({ ...prev, [key]: value }));
|
||||
const toggleFilter = (key: BoolFilterKey) => setFilters(prev => ({ ...prev, [key]: !prev[key] }));
|
||||
const setFilterValue = (key: 'category' | 'cycle', value: string) => setFilters(prev => ({ ...prev, [key]: value }));
|
||||
const hasFilters = !!(
|
||||
search.trim()
|
||||
|| filters.category !== FILTER_ALL
|
||||
|
|
@ -700,34 +751,34 @@ export default function BillsPage() {
|
|||
});
|
||||
};
|
||||
|
||||
async function handleEdit(billId) {
|
||||
async function handleEdit(billId: number) {
|
||||
try {
|
||||
const bill = await api.bill(billId);
|
||||
setModal({ bill });
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
toast.error(errMessage(err, 'Failed to load bill'));
|
||||
}
|
||||
}
|
||||
|
||||
function handleDuplicateBill(bill) {
|
||||
setModal({ bill: null, initialBill: makeBillDraft(bill, { copy: true, categories }) });
|
||||
function handleDuplicateBill(bill: Bill) {
|
||||
setModal({ bill: null, initialBill: makeBillDraft(bill, { copy: true, categories }) as Partial<Bill> });
|
||||
}
|
||||
|
||||
function handleTemplateSelect(value) {
|
||||
function handleTemplateSelect(value: string) {
|
||||
if (!value || value === 'placeholder') return;
|
||||
const builtIn = BUILT_IN_TEMPLATES.find(template => template.id === value);
|
||||
if (builtIn) {
|
||||
setModal({ bill: null, initialBill: makeBillDraft(builtIn.data, { template: builtIn, categories }) });
|
||||
setModal({ bill: null, initialBill: makeBillDraft(builtIn.data, { template: builtIn, categories }) as Partial<Bill> });
|
||||
return;
|
||||
}
|
||||
|
||||
const saved = savedTemplates.find(template => `saved-${template.id}` === value);
|
||||
if (saved) {
|
||||
setModal({ bill: null, initialBill: makeBillDraft(saved.data, { template: saved, categories }) });
|
||||
setModal({ bill: null, initialBill: makeBillDraft(saved.data, { template: saved, categories }) as Partial<Bill> });
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggle(bill) {
|
||||
function handleToggle(bill: Bill) {
|
||||
if (bill.active) {
|
||||
// Prompt confirmation before deactivating
|
||||
setDeactivate(bill);
|
||||
|
|
@ -736,20 +787,20 @@ export default function BillsPage() {
|
|||
}
|
||||
}
|
||||
|
||||
async function doToggle(bill, reason) {
|
||||
async function doToggle(bill: Bill | null, reason?: string) {
|
||||
if (!bill) return;
|
||||
try {
|
||||
const payload = { active: bill.active ? 0 : 1 };
|
||||
const payload: { active: number; inactive_reason?: string } = { active: bill.active ? 0 : 1 };
|
||||
if (bill.active && reason) payload.inactive_reason = reason;
|
||||
await api.updateBill(bill.id, payload);
|
||||
toast.success(bill.active ? 'Bill deactivated' : 'Bill activated');
|
||||
refresh();
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
toast.error(errMessage(err, 'Failed to update bill'));
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteRequest(bill) {
|
||||
function handleDeleteRequest(bill: Bill) {
|
||||
setDeleteConfirmed(false);
|
||||
setDeleteTarget(bill);
|
||||
}
|
||||
|
|
@ -771,7 +822,7 @@ export default function BillsPage() {
|
|||
toast.success(`"${bill.name}" restored`);
|
||||
refresh();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to restore bill');
|
||||
toast.error(errMessage(err, 'Failed to restore bill'));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -780,19 +831,19 @@ export default function BillsPage() {
|
|||
setDeleteConfirmed(false);
|
||||
refresh();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to delete bill');
|
||||
toast.error(errMessage(err, 'Failed to delete bill'));
|
||||
} finally {
|
||||
setDeleteBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRestoreDeleted(bill) {
|
||||
async function handleRestoreDeleted(bill: Bill) {
|
||||
try {
|
||||
await api.restoreBill(bill.id);
|
||||
toast.success(`"${bill.name}" restored`);
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to restore bill');
|
||||
toast.error(errMessage(err, 'Failed to restore bill'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -805,7 +856,7 @@ export default function BillsPage() {
|
|||
}
|
||||
|
||||
const cycleOptions = useMemo(() => (
|
||||
Array.from(new Set(bills.map(scheduleValue))).sort()
|
||||
Array.from(new Set(bills.map(bill => scheduleValue(bill)))).sort()
|
||||
), [bills]);
|
||||
|
||||
const filteredBills = useMemo(() => {
|
||||
|
|
@ -842,7 +893,7 @@ export default function BillsPage() {
|
|||
const sortedActive = billsSort === 'cadence' ? sortBillsByCadence(active) : active;
|
||||
const sortedInactive = billsSort === 'cadence' ? sortBillsByCadence(inactive) : inactive;
|
||||
|
||||
async function persistBillOrder(nextBills, movedId) {
|
||||
async function persistBillOrder(nextBills: Bill[], movedId: string | number | null) {
|
||||
setBills(nextBills);
|
||||
setMovingBillId(movedId);
|
||||
try {
|
||||
|
|
@ -850,23 +901,23 @@ export default function BillsPage() {
|
|||
toast.success('Bill order saved');
|
||||
refresh();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to save bill order');
|
||||
toast.error(errMessage(err, 'Failed to save bill order'));
|
||||
refresh();
|
||||
} finally {
|
||||
setMovingBillId(null);
|
||||
}
|
||||
}
|
||||
|
||||
function reorderBillGroup(activeState, orderedGroup) {
|
||||
function reorderBillGroup(activeState: boolean, orderedGroup: Bill[]) {
|
||||
const sourceGroup = bills.filter(bill => !!bill.active === activeState);
|
||||
const replacements = [...orderedGroup];
|
||||
const nextBills = bills.map(bill => (
|
||||
!!bill.active === activeState ? replacements.shift() : bill
|
||||
!!bill.active === activeState ? replacements.shift()! : bill
|
||||
));
|
||||
persistBillOrder(nextBills, movedItemId(sourceGroup, orderedGroup));
|
||||
}
|
||||
|
||||
function moveControlsForGroup(group, activeState) {
|
||||
function moveControlsForGroup(group: Bill[], activeState: boolean): (bill: Bill, index: number) => MoveControls {
|
||||
return (bill, index) => ({
|
||||
enabled: reorderEnabled,
|
||||
moving: movingBillId === bill.id,
|
||||
|
|
@ -877,7 +928,7 @@ export default function BillsPage() {
|
|||
});
|
||||
}
|
||||
|
||||
function dragPropsForGroup(group, activeState) {
|
||||
function dragPropsForGroup(group: Bill[], activeState: boolean): (bill: Bill, index: number) => DragProps {
|
||||
return (bill, index) => {
|
||||
if (!reorderEnabled) return { draggable: false };
|
||||
return {
|
||||
Loading…
Reference in New Issue