- 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>
235 lines
7.3 KiB
TypeScript
235 lines
7.3 KiB
TypeScript
import { Ionicons } from "@expo/vector-icons";
|
|
import React, { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
ActivityIndicator,
|
|
Modal,
|
|
Pressable,
|
|
ScrollView,
|
|
Text,
|
|
TextInput,
|
|
View,
|
|
} from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import { TransactionScreen } from "@/src/components/features/transactions/TransactionScreen";
|
|
import { useChildren, useCreateChild, type Child } from "@/src/hooks/useChildren";
|
|
import { ModalHeader } from "@/src/components/ui/ModalHeader";
|
|
|
|
const CHILD_COLORS = [
|
|
"#ec4899",
|
|
"#f59e0b",
|
|
"#10b981",
|
|
"#2563EB",
|
|
"#7c3aed",
|
|
"#ef4444",
|
|
"#0ea5e9",
|
|
"#378ADD",
|
|
];
|
|
|
|
function AddChildModal({
|
|
visible,
|
|
onClose,
|
|
onCreated,
|
|
}: {
|
|
visible: boolean;
|
|
onClose: () => void;
|
|
onCreated: (child: Child) => void;
|
|
}) {
|
|
const [name, setName] = useState("");
|
|
const [color, setColor] = useState(CHILD_COLORS[0]!);
|
|
const { t } = useTranslation();
|
|
const { mutate: createChild, isPending } = useCreateChild();
|
|
|
|
function handleSave() {
|
|
const trimmed = name.trim();
|
|
if (!trimmed) return;
|
|
createChild(
|
|
{ name: trimmed, color },
|
|
{
|
|
onSuccess: (data) => {
|
|
onCreated(data.child);
|
|
setName("");
|
|
setColor(CHILD_COLORS[0]!);
|
|
onClose();
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
function handleClose() {
|
|
setName("");
|
|
setColor(CHILD_COLORS[0]!);
|
|
onClose();
|
|
}
|
|
|
|
return (
|
|
<Modal visible={visible} animationType="slide" presentationStyle="pageSheet" onRequestClose={handleClose}>
|
|
<View className="flex-1 bg-white">
|
|
{/* Header */}
|
|
<ModalHeader
|
|
title={t('children.addChild')}
|
|
onClose={handleClose}
|
|
closeLabel={t('common.cancel')}
|
|
onSave={handleSave}
|
|
saveLabel={t('common.save')}
|
|
saveDisabled={!name.trim()}
|
|
saveLoading={isPending}
|
|
saveColor="#ec4899"
|
|
/>
|
|
|
|
<View className="px-4 mt-6">
|
|
{/* Name Input */}
|
|
<Text className="text-sm font-medium text-gray-700 mb-2">Name</Text>
|
|
<TextInput
|
|
className="bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-base text-gray-900 mb-6"
|
|
placeholder="z.B. Emma"
|
|
value={name}
|
|
onChangeText={setName}
|
|
autoFocus
|
|
/>
|
|
|
|
{/* Color Picker */}
|
|
<Text className="text-sm font-medium text-gray-700 mb-3">Farbe</Text>
|
|
<View className="flex-row flex-wrap gap-3">
|
|
{CHILD_COLORS.map((c) => (
|
|
<Pressable
|
|
key={c}
|
|
onPress={() => setColor(c)}
|
|
style={{
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 20,
|
|
backgroundColor: c,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
borderWidth: color === c ? 3 : 0,
|
|
borderColor: "#fff",
|
|
shadowColor: color === c ? c : "transparent",
|
|
shadowOpacity: color === c ? 0.5 : 0,
|
|
shadowRadius: 4,
|
|
elevation: color === c ? 4 : 0,
|
|
}}
|
|
>
|
|
{color === c && (
|
|
<Ionicons name="checkmark" size={20} color="#fff" />
|
|
)}
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
export default function KinderScreen() {
|
|
const insets = useSafeAreaInsets();
|
|
const { t } = useTranslation();
|
|
const { data: children = [], isLoading } = useChildren();
|
|
const [activeChildId, setActiveChildId] = useState<string | null>(null);
|
|
const [showAddModal, setShowAddModal] = useState(false);
|
|
|
|
// Determine active child — fall back to first child when list loads
|
|
const activeChild =
|
|
children.find((c) => c.id === activeChildId) ?? children[0] ?? null;
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<View className="flex-1 bg-white items-center justify-center">
|
|
<ActivityIndicator size="large" color="#ec4899" />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View className="flex-1 bg-white">
|
|
{/* Empty State */}
|
|
{children.length === 0 && (
|
|
<View className="flex-1 items-center justify-center px-8">
|
|
<Ionicons name="happy-outline" size={72} color="#d1d5db" style={{ marginBottom: 16 }} />
|
|
<Text className="text-lg font-semibold text-gray-700 mb-2 text-center">
|
|
{t('children.noChildren')}
|
|
</Text>
|
|
<Text className="text-sm text-gray-400 text-center mb-8">
|
|
{t('children.noChildrenHint')}
|
|
</Text>
|
|
<Pressable
|
|
onPress={() => setShowAddModal(true)}
|
|
className="px-6 py-3 rounded-full items-center justify-center"
|
|
style={{ backgroundColor: "#ec4899" }}
|
|
>
|
|
<Text className="text-white font-semibold text-base">+ {t('children.addChild')}</Text>
|
|
</Pressable>
|
|
</View>
|
|
)}
|
|
|
|
{/* Children Tab Switcher + Content */}
|
|
{children.length > 0 && activeChild && (
|
|
<TransactionScreen
|
|
scope="child"
|
|
childId={activeChild.id}
|
|
accentColor={activeChild.color}
|
|
emptyTitle={t('children.noTransactions', { name: activeChild.name })}
|
|
emptySubtitle={t('children.noTransactionsHint')}
|
|
headerExtra={
|
|
<View className="bg-white border-b border-gray-100">
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
contentContainerStyle={{ paddingHorizontal: 12, paddingVertical: 10, gap: 8, flexDirection: "row", alignItems: "center" }}
|
|
>
|
|
{children.map((child) => {
|
|
const isActive = child.id === activeChild.id;
|
|
return (
|
|
<Pressable
|
|
key={child.id}
|
|
onPress={() => setActiveChildId(child.id)}
|
|
style={{
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 7,
|
|
borderRadius: 20,
|
|
backgroundColor: isActive ? child.color : "#f3f4f6",
|
|
}}
|
|
>
|
|
<Text
|
|
style={{
|
|
fontSize: 14,
|
|
fontWeight: "600",
|
|
color: isActive ? "#fff" : "#4b5563",
|
|
}}
|
|
>
|
|
{child.name}
|
|
</Text>
|
|
</Pressable>
|
|
);
|
|
})}
|
|
|
|
{/* Add Child Button */}
|
|
<Pressable
|
|
onPress={() => setShowAddModal(true)}
|
|
style={{
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 16,
|
|
backgroundColor: "#fce7f3",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
<Ionicons name="add" size={20} color="#ec4899" />
|
|
</Pressable>
|
|
</ScrollView>
|
|
</View>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
<AddChildModal
|
|
visible={showAddModal}
|
|
onClose={() => setShowAddModal(false)}
|
|
onCreated={(child) => setActiveChildId(child.id)}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|