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)}
/>
)}
);
}