/** * 首页待办条(spec §7.1):周期记账到期 / 信用卡还款提醒 / 未确认自动账单。 * 三类都无待办时不渲染。 */ import React, { useMemo } from 'react'; import { Alert, Pressable, StyleSheet, Text, View } from 'react-native'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useTheme } from '../theme'; import { useT } from '../i18n'; import { Card } from './Card'; import { useLedgerStore } from '../store/ledgerStore'; import { useMetadataStore } from '../store/metadataStore'; import { useAutomationStore } from '../store/automationStore'; import { getDueRecurring, instantiateRecurring, calculateNextDueDate } from '../domain/recurring'; import { calculateBillingPeriod } from '../domain/creditCards'; import type { CreditCard } from '../domain/creditCards'; import { toDateString } from '../domain/decimal'; import { shiftAnchor } from '../domain/periodNav'; export function TodoStrip() { const { theme } = useTheme(); const t = useT(); const router = useRouter(); const addTransaction = useLedgerStore(s => s.addTransaction); const recurringTransactions = useMetadataStore(s => s.recurringTransactions); const updateRecurringTransaction = useMetadataStore(s => s.updateRecurringTransaction); const creditCards = useMetadataStore(s => s.creditCards); const draftsCount = useAutomationStore(s => s.drafts.length); const todayStr = useMemo(() => toDateString(new Date()), []); const weekLater = useMemo(() => shiftAnchor(todayStr, 'weekly', 1), [todayStr]); // 下一还款日:当月还款日已过则取下月周期(月末跨月场景,filter 与展示共用) const nextDueDate = (card: CreditCard): string => { const now = new Date(); const period = calculateBillingPeriod(card, now); if (period.dueDate >= todayStr) return period.dueDate; return calculateBillingPeriod(card, new Date(now.getFullYear(), now.getMonth() + 1, 1)).dueDate; }; // autoConfirm 的由首页自动处理,待办条只列需手动确认的 const dueRecurring = useMemo( () => getDueRecurring(recurringTransactions, todayStr).filter(r => !r.autoConfirm), [recurringTransactions, todayStr], ); const dueCards = useMemo( () => creditCards.filter(card => { const due = nextDueDate(card); return due >= todayStr && due <= weekLater; }), // eslint-disable-next-line react-hooks/exhaustive-deps [creditCards, todayStr, weekLater], ); if (dueRecurring.length === 0 && dueCards.length === 0 && draftsCount === 0) return null; const handleConfirmRecurring = async (rec: (typeof dueRecurring)[number]) => { try { const draft = instantiateRecurring(rec); await addTransaction(draft); const nextDueDate = calculateNextDueDate(rec.frequency, rec.interval, rec.nextDueDate); updateRecurringTransaction(rec.id, { nextDueDate }); Alert.alert(t('home.recurringSuccess'), t('home.recurringSuccessDesc', { name: rec.name })); } catch (e) { Alert.alert(t('home.recurringFail'), String(e)); } }; const renderRow = (icon: keyof typeof Ionicons.glyphMap, text: string, key: string, action?: { label: string; onPress: () => void }) => ( {text} {action ? ( {action.label} ) : ( )} ); return ( {dueRecurring.map(rec => renderRow('repeat-outline', t('todo.dueRecurring', { name: rec.name }), `rec-${rec.id}`, { label: t('todo.confirm'), onPress: () => handleConfirmRecurring(rec) }), )} {dueCards.map(card => renderRow('card-outline', t('todo.creditCardDue', { name: card.name, date: nextDueDate(card).slice(5) }), `card-${card.id}`, { label: t('todo.view'), onPress: () => router.push('/credit-card') }), )} {draftsCount > 0 && renderRow('flash-outline', t('todo.pendingDrafts', { count: draftsCount }), 'drafts', { label: t('todo.view'), onPress: () => router.push('/automation') })} ); } const styles = StyleSheet.create({ row: { flexDirection: 'row', alignItems: 'center', gap: 10, paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth }, rowText: { flex: 1 }, actionBtn: { paddingHorizontal: 12, paddingVertical: 5 }, actionText: { fontSize: 12, fontWeight: '700' }, });