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:
109
apps/native/app/(app)/transactions/index.tsx
Normal file
109
apps/native/app/(app)/transactions/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user