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>
This commit is contained in:
234
apps/native/app/(app)/kinder/index.tsx
Normal file
234
apps/native/app/(app)/kinder/index.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user