import { QuickAddModal } from "@/src/components/features/transactions/QuickAddModal"; import { TransactionItem } from "@/src/components/features/transactions/TransactionItem"; import { EditTransactionModal } from "@/src/components/features/transactions/EditTransactionModal"; import { CarryOverBanner } from "@/src/components/features/transactions/CarryOverBanner"; import { MonthSummaryHeader } from "@/src/components/features/transactions/MonthSummaryHeader"; import { AddCategoryModal } from "@/src/components/features/categories/AddCategoryModal"; import { ModalHeader } from "@/src/components/ui/ModalHeader"; import { EmptyState } from "@/src/components/ui/EmptyState"; import { Numpad } from "@/src/components/ui/Numpad"; import { useTransactions, useActivateFixed, useMonthBalance, useDeleteTransaction } from "@/src/hooks/useTransactions"; import type { TransactionWithCategory } from "@/src/hooks/useTransactions"; import { useAuthStore } from "@/src/stores/auth.store"; import { useSettlementV2, useCreateMonthlyTransfer, useNettoMonth, type MonthlyTransfer } from "@/src/hooks/useFixedCosts"; import { useHouseholdSettings } from "@/src/hooks/useHouseholdSettings"; import { useMonthStatus } from "@/src/hooks/useMonthStatus"; import { currentMonthStr, addMonths, monthLabel, monthDateRange } from "@/src/utils/date"; import { formatEur } from "@/src/utils/format"; import { handleNumpadKey, parseAmountStr } from "@/src/utils/numpad"; import { useRouter } from "expo-router"; import { Ionicons } from "@expo/vector-icons"; import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import type { Category } from "@/src/hooks/useCategories"; import { ActivityIndicator, Alert, FlatList, Modal, Pressable, RefreshControl, Text, TextInput, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { TAB_COLORS } from "@/src/constants/colors"; const ACCENT = TAB_COLORS.household; // ── Record Transfer Modal ───────────────────────────────────────────────────── function RecordTransferModal({ month, toUserId, onClose, }: { month: string; toUserId: string; onClose: () => void; }) { const [amountStr, setAmountStr] = useState("0"); const [note, setNote] = useState(""); const { t } = useTranslation(); const { mutate: createTransfer, isPending } = useCreateMonthlyTransfer(); function handleNumpad(key: string) { setAmountStr((prev) => handleNumpadKey(prev, key)); } function handleSave() { const amount = parseAmountStr(amountStr); if (!amount || amount <= 0) return; createTransfer( { month, toUserId, amount, note: note.trim() || undefined }, { onSuccess: onClose }, ); } const canSave = parseAmountStr(amountStr) > 0; return ( € {amountStr} {t('household.settlement.transferAmount')} ); } // ── Settlement Banner V2 ────────────────────────────────────────────────────── function SettlementBanner({ month, isCurrent }: { month: string; isCurrent: boolean }) { const userId = useAuthStore((s) => s.user?.id); const router = useRouter(); const { t } = useTranslation(); const { data: settlement, isLoading } = useSettlementV2(month); const { data: hhSettings } = useHouseholdSettings(); const { data: monthStatus } = useMonthStatus(month); const [expanded, setExpanded] = useState(false); const [showTransferModal, setShowTransferModal] = useState(false); const isClosed = monthStatus?.status === "closed"; if (isLoading) { return ( ); } if (!settlement || settlement.memberCount <= 1) return null; // Closed month — show lock banner if (isClosed && monthStatus) { const closedDate = monthStatus.closedAt ? new Date(monthStatus.closedAt).toLocaleDateString("de-DE", { day: "numeric", month: "long", year: "numeric" }) : ""; return ( {t('household.settlement.closed')} ✓ {closedDate} {monthStatus.finalAmount != null && monthStatus.finalAmount > 0 ? ` · ${formatEur(monthStatus.finalAmount)}` : ""} ); } const remaining = settlement.remaining; const isOwing = remaining > 0.005; const isReceiving = remaining < -0.005; const isEven = !isOwing && !isReceiving; const bannerBg = isOwing ? "#fff7ed" : isReceiving ? "#f0fdf4" : "#f9fafb"; const bannerBorder = isOwing ? "#fed7aa" : isReceiving ? "#bbf7d0" : "#e5e7eb"; const amountColor = isOwing ? "#ea580c" : isReceiving ? "#16a34a" : "#6b7280"; const others = settlement.members.filter((m) => m.userId !== userId); const otherName = hhSettings?.partnerName ?? others[0]?.name ?? "den anderen"; const otherUserId = others[0]?.userId ?? ""; let mainText = t('household.settlement.allSettled'); if (isOwing) mainText = t('household.settlement.youOwe', { name: otherName }); else if (isReceiving) mainText = t('household.settlement.theyOwe', { name: otherName }); return ( <> {/* Summary row */} setExpanded((v) => !v)} className="flex-row items-center px-4 py-3 active:opacity-80" > {t('household.settlement.monthlySettlement')} {mainText} {!isEven && ( {formatEur(Math.abs(remaining))} )} {isEven && ( {t('household.settlement.allSettled')} )} {/* Expandable detail */} {expanded && ( {/* Haushalt breakdown */} {t('household.settlement.householdExpenses')} {formatEur(settlement.householdExpenses)} {settlement.householdIncome > 0 && ( {t('household.settlement.householdIncome')} −{formatEur(settlement.householdIncome)} )} {t('household.settlement.yourShare', { percent: settlement.userSharePercent ?? 50 })} {formatEur(settlement.perMemberShare)} {/* Who paid what */} {settlement.members.map((mem) => { const isMe = mem.userId === userId; const name = isMe ? "Du" : otherName; const paidAmount = isMe ? settlement.myOwnExpenses : (settlement.householdExpenses - settlement.myOwnExpenses); if (paidAmount < 0.01) return null; return ( {t('household.settlement.paidBy', { name })} {formatEur(paidAmount)} ); })} {/* Fixed transfer items — summarised */} {settlement.lineItemsTotal > 0 && ( <> + {t('household.settlement.fixedTransfers')} {formatEur(settlement.lineItemsTotal)} )} {/* Total owed */} {t('household.settlement.toTransfer')} {formatEur(settlement.totalOwed)} {/* Already transferred */} {t('household.settlement.alreadyTransferred')} {formatEur(settlement.alreadyTransferred)} {isOwing && ( setShowTransferModal(true)} style={{ backgroundColor: ACCENT }} className="px-4 py-2 rounded-xl active:opacity-80" > + {t('household.settlement.book')} )} {/* Close month button */} {isCurrent && ( router.push({ pathname: "/(app)/months/close", params: { month } })} className="mt-3 flex-row items-center justify-center gap-2 py-2.5 rounded-xl active:opacity-80" style={{ backgroundColor: "#f3f4f6", borderWidth: 1, borderColor: "#e5e7eb" }} > {t('household.settlement.closeMonth')} )} {/* Transfer history */} {settlement.transfers.length > 0 && ( {settlement.transfers.map((t: MonthlyTransfer) => ( {new Date(t.createdAt).toLocaleDateString("de-DE", { day: "numeric", month: "short" })} {t.note ? ` · ${t.note}` : ""} {formatEur(t.amount)} ))} )} )} {showTransferModal && ( setShowTransferModal(false)} /> )} ); } // ── Netto Card ──────────────────────────────────────────────────────────────── function NettoCard({ month }: { month: string }) { const { data, isLoading } = useNettoMonth(month); const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); if (isLoading) { return ( ); } if (!data) return null; const isPositive = data.netto >= 0; const nettoColor = isPositive ? "#16a34a" : "#dc2626"; const nettoIcon = isPositive ? "trending-up" : "trending-down"; return ( setExpanded((v) => !v)} className="flex-row items-center px-4 py-3 active:opacity-80" > ["name"]} size={18} color={nettoColor} /> {t('household.nettoMonth')} {isPositive ? "+" : "−"}{formatEur(Math.abs(data.netto))} {t('household.income')} +{formatEur(data.totalIncome)} {expanded && ( {/* Income breakdown */} {data.incomeByScope.length > 0 ? ( <> Einnahmen nach Bereich {data.incomeByScope.map((s) => ( {s.label} +{formatEur(s.amount)} ))} ) : ( Keine Einnahmen gebucht )} {/* Expenses */} Ausgaben (alle Bereiche) −{formatEur(data.totalExpenses)} Netto {isPositive ? "+" : "−"}{formatEur(Math.abs(data.netto))} )} ); } // ── Month Switcher ───────────────────────────────────────────────────────────── function MonthSwitcher({ month, isLocked, onPrev, onNext, }: { month: string; isLocked: boolean; onPrev: () => void; onNext: () => void; }) { const isCurrent = month === currentMonthStr(); return ( {isLocked && } {monthLabel(month)} ); } // ── Main Screen ─────────────────────────────────────────────────────────────── type FilterType = "all" | "income" | "expense"; export default function HaushaltScreen() { const insets = useSafeAreaInsets(); const { t } = useTranslation(); const router = useRouter(); const [month, setMonth] = useState(currentMonthStr()); const [filter, setFilter] = useState("all"); const [showAddModal, setShowAddModal] = useState(false); const [showFabMenu, setShowFabMenu] = useState(false); const [showAddCategory, setShowAddCategory] = useState(false); const [addCategoryType, setAddCategoryType] = useState<"expense" | "income">("expense"); const [newCategory, setNewCategory] = useState(null); const [editTransaction, setEditTransaction] = useState(null); const { mutate: deleteTransaction } = useDeleteTransaction(); const { data: monthStatus } = useMonthStatus(month); const isLocked = monthStatus?.status === "closed"; const isCurrent = month === currentMonthStr(); const { mutate: activateFixed } = useActivateFixed(); useEffect(() => { if (isCurrent) { activateFixed({ month, scope: "household" }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [month]); const [fromDate, toDate] = monthDateRange(month); const txFilter = { scope: "household" as const, from: fromDate, to: toDate, ...(filter !== "all" ? { type: filter as "income" | "expense" } : {}), }; const { data: transactions = [], isLoading, refetch, isRefetching } = useTransactions(txFilter); const { data: balance, isLoading: balanceLoading } = useMonthBalance("household", month); function renderEmpty() { if (isLoading) return null; return ( ); } return ( {/* Header */} setMonth((m) => addMonths(m, -1))} onNext={() => setMonth((m) => addMonths(m, 1))} /> item.id} renderItem={({ item }) => ( {} : setEditTransaction} onDelete={isLocked ? () => {} : (t) => deleteTransaction(t.id)} locked={isLocked} /> )} ListHeaderComponent={ {/* Filter Bar */} {(["all", "expense", "income"] as const).map((f) => ( setFilter(f)} style={{ backgroundColor: filter === f ? ACCENT : "#f3f4f6" }} className="px-4 py-1.5 rounded-full" > {f === "all" ? t('household.all') : f === "expense" ? t('household.expenses') : t('household.income')} ))} } ListEmptyComponent={renderEmpty} refreshControl={ void refetch()} tintColor={ACCENT} /> } ItemSeparatorComponent={() => } contentContainerStyle={transactions.length === 0 ? { flex: 1 } : undefined} /> {isLoading && ( )} {/* FAB — hidden for locked months */} {!isLocked && ( <> {/* Backdrop */} {showFabMenu && ( setShowFabMenu(false)} /> )} {/* FAB menu — anchored above FAB, zIndex above backdrop */} {showFabMenu && ( { setShowFabMenu(false); setShowAddModal(true); }} className="flex-row items-center gap-3 px-5 py-4 active:bg-gray-50" > {t("scanner.manualEntry")} { setShowFabMenu(false); router.push("/(app)/scanner"); }} className="flex-row items-center gap-3 px-5 py-4 active:bg-gray-50" > {t("scanner.scanReceipt")} )} {/* FAB button */} setShowFabMenu((v) => !v)} style={{ backgroundColor: ACCENT, bottom: insets.bottom + 20, zIndex: 101 }} className="absolute right-6 w-14 h-14 rounded-full items-center justify-center shadow-lg active:opacity-80" > )} { setShowAddModal(false); setNewCategory(null); }} onRequestAddCategory={(t) => { setAddCategoryType(t); setShowAddModal(false); setShowAddCategory(true); }} newCategory={newCategory} defaultScope="household" /> { setShowAddCategory(false); setShowAddModal(true); }} defaultType={addCategoryType} onCreated={(cat) => { setNewCategory(cat); setShowAddCategory(false); setShowAddModal(true); }} /> {editTransaction && ( setEditTransaction(null)} /> )} ); }