Files
HausApp/apps/native/src/components/features/debts/AddDebtModal.tsx
René Schober 9ddc7c6d7a Production deployment setup + feature complete
- Dockerfile + deploy.sh for Hetzner server
- Email verification via Better Auth + Resend
- Invite code flow (6-digit OTP, generate/join)
- Settlement share percent fix (payer vs debtor)
- OCR scanner fixes (date display, retry, viewfinder)
- app.json icon/splash/adaptive-icon configured
- iOS deployment target 15.5 (ML Kit requirement)
- DB migration 0014: household_invitations table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 11:54:22 +01:00

188 lines
7.2 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import { useState } from "react";
import {
Modal,
Pressable,
ScrollView,
Text,
TextInput,
View,
} from "react-native";
import { useCreateDebt } from "@/src/hooks/useDebts";
import { useHouseholdMembers } from "@/src/hooks/useHouseholdMembers";
import { useAuthStore } from "@/src/stores/auth.store";
import { useTranslation } from "react-i18next";
import { ModalHeader } from "@/src/components/ui/ModalHeader";
import { Numpad } from "@/src/components/ui/Numpad";
import { handleNumpadKey, parseAmountStr } from "@/src/utils/numpad";
type Props = {
visible: boolean;
onClose: () => void;
};
export function AddDebtModal({ visible, onClose }: Props) {
const [label, setLabel] = useState("");
const [amountStr, setAmountStr] = useState("0");
const [notes, setNotes] = useState("");
// creditor: internal member OR free text
const [creditorUserId, setCreditorUserId] = useState<string | null>(null);
const [creditorText, setCreditorText] = useState("");
const [showMemberPicker, setShowMemberPicker] = useState(false);
const { mutate: createDebt, isPending } = useCreateDebt();
const { data: membersData } = useHouseholdMembers();
const myUserId = useAuthStore((s) => s.user?.id);
const { t } = useTranslation();
// Only other members (not myself)
const otherMembers = (membersData?.members ?? []).filter((m) => m.userId !== myUserId);
const selectedMember = otherMembers.find((m) => m.userId === creditorUserId) ?? null;
function handleNumpad(key: string) {
setAmountStr((prev) => handleNumpadKey(prev, key));
}
function handleSave() {
const amount = parseAmountStr(amountStr);
if (!label.trim() || !amount || amount <= 0) return;
createDebt(
{
label: label.trim(),
creditorUserId: creditorUserId ?? undefined,
creditor: !creditorUserId && creditorText.trim() ? creditorText.trim() : undefined,
totalAmount: amount,
notes: notes.trim() || undefined,
},
{ onSuccess: resetAndClose },
);
}
function resetAndClose() {
setLabel("");
setAmountStr("0");
setNotes("");
setCreditorUserId(null);
setCreditorText("");
setShowMemberPicker(false);
onClose();
}
const canSave = label.trim().length > 0 && parseAmountStr(amountStr) > 0;
return (
<Modal visible={visible} animationType="slide" presentationStyle="pageSheet" onRequestClose={resetAndClose}>
<View className="flex-1 bg-white">
{/* Header */}
<ModalHeader
title={t('debts.addTitle')}
onClose={resetAndClose}
closeLabel={t('common.cancel')}
onSave={handleSave}
saveLabel={t('common.save')}
saveDisabled={!canSave}
saveLoading={isPending}
saveColor="#7c3aed"
/>
<ScrollView keyboardShouldPersistTaps="handled" contentContainerStyle={{ paddingBottom: 24 }}>
{/* Amount display */}
<View className="items-center py-6">
<Text className="text-5xl font-bold text-gray-900"> {amountStr}</Text>
<Text className="text-sm text-gray-400 mt-1">{t('debts.totalAmount')}</Text>
</View>
{/* Fields */}
<View className="px-4 gap-3 mb-4">
<View>
<Text className="text-sm font-medium text-gray-700 mb-1">{t('debts.labelRequired')}</Text>
<TextInput
className="bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-base text-gray-900"
placeholder={t('debts.labelPlaceholder')}
value={label}
onChangeText={setLabel}
/>
</View>
{/* Creditor picker */}
<View>
<Text className="text-sm font-medium text-gray-700 mb-1">{t('debts.iOweMoneyTo')}</Text>
{/* Member select row */}
{otherMembers.length > 0 && (
<Pressable
onPress={() => setShowMemberPicker((v) => !v)}
className="flex-row items-center bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 mb-2"
>
<Ionicons name="people-outline" size={16} color="#7c3aed" style={{ marginRight: 8 }} />
<Text className="flex-1 text-base" style={{ color: selectedMember ? "#111827" : "#9ca3af" }}>
{selectedMember ? selectedMember.name : t('debts.selectMember')}
</Text>
{selectedMember ? (
<Pressable
onPress={(e) => { e.stopPropagation(); setCreditorUserId(null); }}
hitSlop={8}
>
<Ionicons name="close-circle" size={18} color="#9ca3af" />
</Pressable>
) : (
<Ionicons name={showMemberPicker ? "chevron-up" : "chevron-down"} size={14} color="#9ca3af" />
)}
</Pressable>
)}
{/* Member dropdown */}
{showMemberPicker && (
<View className="bg-white border border-gray-200 rounded-xl mb-2 overflow-hidden">
{otherMembers.map((m) => (
<Pressable
key={m.userId}
onPress={() => { setCreditorUserId(m.userId); setCreditorText(""); setShowMemberPicker(false); }}
className="flex-row items-center px-4 py-3 active:bg-gray-50"
style={{ borderBottomWidth: 1, borderBottomColor: "#f3f4f6" }}
>
<View className="w-7 h-7 rounded-full bg-purple-100 items-center justify-center mr-3">
<Text className="text-xs font-bold text-purple-700">
{m.name.charAt(0).toUpperCase()}
</Text>
</View>
<Text className="text-sm text-gray-800">{m.name}</Text>
{creditorUserId === m.userId && (
<Ionicons name="checkmark" size={16} color="#7c3aed" style={{ marginLeft: "auto" }} />
)}
</Pressable>
))}
</View>
)}
{/* Free-text fallback (only when no member selected) */}
{!creditorUserId && (
<TextInput
className="bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-base text-gray-900"
placeholder={t('debts.orEnterName')}
value={creditorText}
onChangeText={setCreditorText}
/>
)}
</View>
<View>
<Text className="text-sm font-medium text-gray-700 mb-1">{t('debts.noteOptional')}</Text>
<TextInput
className="bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-base text-gray-900"
placeholder={t('debts.notePlaceholder')}
value={notes}
onChangeText={setNotes}
/>
</View>
</View>
{/* Numpad */}
<Numpad onKeyPress={handleNumpad} />
</ScrollView>
</View>
</Modal>
);
}