import { Ionicons } from "@expo/vector-icons"; import { useState } from "react"; import { ActivityIndicator, Alert, Modal, Pressable, ScrollView, SectionList, Text, TextInput, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useRouter } from "expo-router"; import { useTranslation } from "react-i18next"; import { useFixedCosts, useCreateFixedCost, useUpdateFixedCost, useDeleteFixedCost, type FixedCost, type CreateFixedCostInput, } from "@/src/hooks/useFixedCosts"; import { useCategories } from "@/src/hooks/useCategories"; import { ModalHeader } from "@/src/components/ui/ModalHeader"; import { Numpad } from "@/src/components/ui/Numpad"; import { formatEur } from "@/src/utils/format"; import { handleNumpadKey, parseAmountStr } from "@/src/utils/numpad"; const SCOPE_LABEL_KEYS: Record = { household: "fixedCosts.household", private: "fixedCosts.me", child: "fixedCosts.children", }; // ── Add / Edit Modal ────────────────────────────────────────────────────────── type ModalMode = | { kind: "add"; scope: "household" | "private" | "child" } | { kind: "edit"; item: FixedCost }; function FixedCostModal({ mode, onClose, }: { mode: ModalMode; onClose: () => void; }) { const isEdit = mode.kind === "edit"; const { data: categories = [] } = useCategories(); const { mutate: create, isPending: creating } = useCreateFixedCost(); const { mutate: update, isPending: updating } = useUpdateFixedCost(); const { t: tFn } = useTranslation(); const [label, setLabel] = useState(isEdit ? mode.item.label : ""); const [amountStr, setAmountStr] = useState( isEdit ? String(mode.item.amount).replace(".", ",") : "0", ); const [type, setType] = useState<"expense" | "income">( isEdit ? mode.item.type : "expense", ); const [categoryId, setCategoryId] = useState( isEdit ? (mode.item.categoryId ?? null) : null, ); const [showCategoryPicker, setShowCategoryPicker] = useState(false); const filteredCategories = categories.filter((c) => c.type === type); const selectedCategory = categories.find((c) => c.id === categoryId) ?? null; const isPending = creating || updating; function handleNumpad(key: string) { setAmountStr((prev) => handleNumpadKey(prev, key)); } function handleSave() { const amount = parseAmountStr(amountStr); if (!label.trim() || !amount || amount <= 0) return; if (isEdit) { update( { id: mode.item.id, input: { label: label.trim(), amount, categoryId } }, { onSuccess: onClose }, ); } else { const input: CreateFixedCostInput = { scope: mode.scope, label: label.trim(), amount, type, categoryId: categoryId ?? undefined, }; create(input, { onSuccess: onClose }); } } const canSave = label.trim().length > 0 && parseAmountStr(amountStr) > 0; return ( {/* Amount */} € {amountStr} {/* Type toggle (only for new) */} {!isEdit && ( {(["expense", "income"] as const).map((t) => ( { setType(t); setCategoryId(null); }} className={`flex-1 py-2 rounded-lg items-center ${type === t ? "bg-white shadow-sm" : ""}`} > {t === "expense" ? tFn('fixedCosts.expenseType') : tFn('fixedCosts.incomeType')} ))} )} {/* Label */} {tFn('fixedCosts.labelRequired')} {/* Category */} {tFn('fixedCosts.categoryOptional')} setShowCategoryPicker((v) => !v)} className="flex-row items-center bg-gray-50 border border-gray-200 rounded-xl px-4 py-3" > {selectedCategory ? selectedCategory.name : tFn('common.select')} {selectedCategory ? ( { e.stopPropagation(); setCategoryId(null); }} hitSlop={8}> ) : ( )} {showCategoryPicker && ( {filteredCategories.map((cat) => ( { setCategoryId(cat.id); setShowCategoryPicker(false); }} className="flex-row items-center px-4 py-3 active:bg-gray-50" style={{ borderBottomWidth: 1, borderBottomColor: "#f3f4f6" }} > ["name"]} size={12} color="#fff" /> {cat.name} {categoryId === cat.id && } ))} )} {/* Numpad */} ); } // ── Row ─────────────────────────────────────────────────────────────────────── function FixedCostRow({ item, onEdit, onDelete, }: { item: FixedCost; onEdit: (item: FixedCost) => void; onDelete: (item: FixedCost) => void; }) { const { t } = useTranslation(); return ( {item.label} {item.type === "expense" ? t('fixedCosts.expenseType') : t('fixedCosts.incomeType')} · {t('common.monthly')} {formatEur(item.amount, false)} onEdit(item)} hitSlop={8} className="mr-2 p-1"> onDelete(item)} hitSlop={8} className="p-1"> ); } // ── Screen ──────────────────────────────────────────────────────────────────── export default function FixedCostsScreen() { const insets = useSafeAreaInsets(); const router = useRouter(); const { t } = useTranslation(); const { data: allFixedCosts = [], isLoading } = useFixedCosts(); const { mutate: deleteCost } = useDeleteFixedCost(); const [modalMode, setModalMode] = useState(null); const active = allFixedCosts.filter((fc) => fc.isActive); const sections = (["household", "private", "child"] as const) .map((scope) => ({ scope, title: t(SCOPE_LABEL_KEYS[scope] ?? scope), data: active.filter((fc) => fc.scope === scope), })) .filter((s) => s.data.length > 0 || true); // always show all scopes function handleDelete(item: FixedCost) { Alert.alert( t('fixedCosts.pauseTitle'), t('fixedCosts.pauseMessage', { label: item.label }), [ { text: t('common.cancel'), style: "cancel" }, { text: t('fixedCosts.pause'), style: "destructive", onPress: () => deleteCost(item.id) }, ], ); } return ( {/* Header */} router.push("/(app)/settings")} className="mr-3 p-1"> {t('fixedCosts.title')} {isLoading ? ( ) : ( item.id} renderSectionHeader={({ section }) => ( {section.title} setModalMode({ kind: "add", scope: section.scope })} className="flex-row items-center gap-1 px-3 py-1.5 rounded-full" style={{ backgroundColor: "#dbeafe" }} > {t('common.new')} )} renderItem={({ item }) => ( setModalMode({ kind: "edit", item: i })} onDelete={handleDelete} /> )} renderSectionFooter={({ section }) => section.data.length === 0 ? ( {t('fixedCosts.noItems')} ) : null } contentContainerStyle={{ paddingBottom: insets.bottom + 24 }} /> )} {modalMode && ( setModalMode(null)} /> )} ); }