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:
René Schober
2026-03-20 11:54:22 +01:00
parent 4e34270786
commit 9ddc7c6d7a
194 changed files with 55961 additions and 305 deletions

View File

@@ -0,0 +1,109 @@
import { QuickAddModal } from "@/src/components/features/transactions/QuickAddModal";
import { SummaryHeader } from "@/src/components/features/transactions/SummaryHeader";
import { TransactionItem } from "@/src/components/features/transactions/TransactionItem";
import { EditTransactionModal } from "@/src/components/features/transactions/EditTransactionModal";
import { useTransactions, useTransactionSummary, useDeleteTransaction, type TransactionWithCategory } from "@/src/hooks/useTransactions";
import { Ionicons } from "@expo/vector-icons";
import { useState } from "react";
import {
ActivityIndicator,
FlatList,
Pressable,
RefreshControl,
Text,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
type FilterType = "all" | "income" | "expense";
export default function TransactionsScreen() {
const insets = useSafeAreaInsets();
const [filter, setFilter] = useState<FilterType>("all");
const [showAddModal, setShowAddModal] = useState(false);
const [editTransaction, setEditTransaction] = useState<TransactionWithCategory | null>(null);
const { mutate: deleteTransaction } = useDeleteTransaction();
const transactionFilter = filter === "all" ? undefined : { type: filter as "income" | "expense" };
const { data: transactions = [], isLoading, refetch, isRefetching } = useTransactions(transactionFilter);
const { data: summary, isLoading: summaryLoading } = useTransactionSummary();
function renderEmpty() {
if (isLoading) return null;
return (
<View className="flex-1 items-center justify-center py-20">
<Ionicons name="wallet-outline" size={48} color="#d1d5db" style={{ marginBottom: 12 }} />
<Text className="text-base font-medium text-gray-700 mb-1">Noch keine Buchungen</Text>
<Text className="text-sm text-gray-400">Tippe auf + um deine erste Buchung einzutragen</Text>
</View>
);
}
return (
<View className="flex-1 bg-white">
<View className="bg-blue-600" style={{ paddingTop: insets.top }}>
<SummaryHeader summary={summary} isLoading={summaryLoading} />
</View>
{/* Filter Bar */}
<View className="flex-row px-4 py-3 gap-2 border-b border-gray-100">
{(["all", "expense", "income"] as const).map((f) => (
<Pressable
key={f}
onPress={() => setFilter(f)}
className={`px-4 py-1.5 rounded-full ${filter === f ? "bg-blue-600" : "bg-gray-100"}`}
>
<Text className={`text-sm font-medium ${filter === f ? "text-white" : "text-gray-600"}`}>
{f === "all" ? "Alle" : f === "expense" ? "Ausgaben" : "Einnahmen"}
</Text>
</Pressable>
))}
</View>
{/* Transaction List */}
{isLoading ? (
<View className="flex-1 items-center justify-center">
<ActivityIndicator size="large" color="#2563EB" />
</View>
) : (
<FlatList
data={transactions}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TransactionItem
transaction={item}
onPress={setEditTransaction}
onDelete={(t) => deleteTransaction(t.id)}
/>
)}
ListEmptyComponent={renderEmpty}
refreshControl={
<RefreshControl refreshing={isRefetching} onRefresh={() => void refetch()} />
}
ItemSeparatorComponent={() => <View className="h-px bg-gray-50 ml-16" />}
contentContainerStyle={transactions.length === 0 ? { flex: 1 } : undefined}
/>
)}
{/* FAB */}
<Pressable
onPress={() => setShowAddModal(true)}
className="absolute bottom-6 right-6 w-14 h-14 bg-blue-600 rounded-full items-center justify-center shadow-lg active:opacity-80"
>
<Ionicons name="add" size={28} color="#fff" />
</Pressable>
<QuickAddModal
visible={showAddModal}
onClose={() => setShowAddModal(false)}
onRequestAddCategory={() => {}}
/>
{editTransaction && (
<EditTransactionModal
transaction={editTransaction}
onClose={() => setEditTransaction(null)}
/>
)}
</View>
);
}