6d2711e36e
Default to beenvoice.soconnor.dev with server settings hidden behind Advanced; add Entities tab with clients/businesses, invoice creation, UI fixes for dashboard layout, date fields, FAB position, and card-matched button radius. Co-authored-by: Cursor <cursoragent@cursor.com>
256 lines
7.7 KiB
TypeScript
256 lines
7.7 KiB
TypeScript
import { router } from "expo-router";
|
|
import { useState } from "react";
|
|
import {
|
|
Pressable,
|
|
RefreshControl,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from "react-native";
|
|
|
|
import { AppBackground } from "@/components/AppBackground";
|
|
import { FilterChip } from "@/components/FilterChip";
|
|
import { FloatingActionButton } from "@/components/FloatingActionButton";
|
|
import { GlassSurface } from "@/components/GlassSurface";
|
|
import { LoadingScreen } from "@/components/LoadingScreen";
|
|
import { PageHeader } from "@/components/PageHeader";
|
|
import { TabPage } from "@/components/TabPage";
|
|
import { TabScrollView } from "@/components/TabScrollView";
|
|
import { fonts, spacing } from "@/constants/theme";
|
|
import { useAppTheme } from "@/contexts/ThemeContext";
|
|
import { formatCurrency } from "@/lib/format";
|
|
import type { ThemeColors } from "@/lib/theme-palette";
|
|
import { useThemedStyles } from "@/lib/use-themed-styles";
|
|
import { api } from "@/lib/trpc";
|
|
|
|
type EntityTab = "clients" | "businesses";
|
|
|
|
const tabs: Array<{ label: string; value: EntityTab }> = [
|
|
{ label: "Clients", value: "clients" },
|
|
{ label: "Businesses", value: "businesses" },
|
|
];
|
|
|
|
export default function EntitiesScreen() {
|
|
const { colors } = useAppTheme();
|
|
const styles = useThemedStyles(createEntitiesStyles);
|
|
const [tab, setTab] = useState<EntityTab>("clients");
|
|
|
|
const clientsQuery = api.clients.getAll.useQuery();
|
|
const businessesQuery = api.businesses.getAll.useQuery();
|
|
|
|
const activeQuery = tab === "clients" ? clientsQuery : businessesQuery;
|
|
const isLoading =
|
|
clientsQuery.isLoading || (tab === "businesses" && businessesQuery.isLoading);
|
|
|
|
if (isLoading) {
|
|
return <LoadingScreen message="Loading…" />;
|
|
}
|
|
|
|
if (activeQuery.error) {
|
|
return (
|
|
<AppBackground>
|
|
<TabPage>
|
|
<View style={styles.errorBox}>
|
|
<Text style={styles.errorTitle}>Could not load {tab}</Text>
|
|
<Text style={styles.errorText}>{activeQuery.error.message}</Text>
|
|
</View>
|
|
</TabPage>
|
|
</AppBackground>
|
|
);
|
|
}
|
|
|
|
const clients = clientsQuery.data ?? [];
|
|
const businesses = businessesQuery.data ?? [];
|
|
|
|
function refresh() {
|
|
if (tab === "clients") void clientsQuery.refetch();
|
|
else void businessesQuery.refetch();
|
|
}
|
|
|
|
return (
|
|
<AppBackground>
|
|
<TabPage>
|
|
<TabScrollView
|
|
header={
|
|
<PageHeader
|
|
title="Entities"
|
|
subtitle="Clients you bill and businesses you send from"
|
|
/>
|
|
}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={activeQuery.isRefetching}
|
|
onRefresh={refresh}
|
|
tintColor={colors.primary}
|
|
/>
|
|
}
|
|
>
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
style={styles.tabScroll}
|
|
contentContainerStyle={styles.tabs}
|
|
>
|
|
{tabs.map((item) => (
|
|
<FilterChip
|
|
key={item.value}
|
|
label={item.label}
|
|
active={tab === item.value}
|
|
onPress={() => setTab(item.value)}
|
|
/>
|
|
))}
|
|
</ScrollView>
|
|
|
|
{tab === "clients" ? (
|
|
clients.length === 0 ? (
|
|
<View style={styles.empty}>
|
|
<Text style={styles.emptyTitle}>No clients yet</Text>
|
|
<Text style={styles.emptyText}>
|
|
Add a client to start creating invoices.
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
clients.map((client) => (
|
|
<Pressable
|
|
key={client.id}
|
|
onPress={() => router.push(`/(app)/entities/clients/${client.id}`)}
|
|
>
|
|
<GlassSurface style={styles.card}>
|
|
<View style={styles.cardInner}>
|
|
<Text style={styles.name}>{client.name}</Text>
|
|
{client.email ? (
|
|
<Text style={styles.meta}>{client.email}</Text>
|
|
) : null}
|
|
{client.defaultHourlyRate != null ? (
|
|
<Text style={styles.meta}>
|
|
{formatCurrency(client.defaultHourlyRate, client.currency ?? "USD")}
|
|
/hr
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
</GlassSurface>
|
|
</Pressable>
|
|
))
|
|
)
|
|
) : businesses.length === 0 ? (
|
|
<View style={styles.empty}>
|
|
<Text style={styles.emptyTitle}>No businesses yet</Text>
|
|
<Text style={styles.emptyText}>
|
|
Add your business profile for invoices and email sending.
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
businesses.map((business) => (
|
|
<Pressable
|
|
key={business.id}
|
|
onPress={() => router.push(`/(app)/entities/businesses/${business.id}`)}
|
|
>
|
|
<GlassSurface style={styles.card}>
|
|
<View style={styles.cardInner}>
|
|
<View style={styles.nameRow}>
|
|
<Text style={styles.name}>{business.name}</Text>
|
|
{business.isDefault ? (
|
|
<Text style={styles.badge}>Default</Text>
|
|
) : null}
|
|
</View>
|
|
{business.nickname ? (
|
|
<Text style={styles.meta}>{business.nickname}</Text>
|
|
) : null}
|
|
{business.email ? <Text style={styles.meta}>{business.email}</Text> : null}
|
|
</View>
|
|
</GlassSurface>
|
|
</Pressable>
|
|
))
|
|
)}
|
|
</TabScrollView>
|
|
<FloatingActionButton
|
|
accessibilityLabel={tab === "clients" ? "Add client" : "Add business"}
|
|
onPress={() =>
|
|
router.push(
|
|
tab === "clients"
|
|
? "/(app)/entities/clients/new"
|
|
: "/(app)/entities/businesses/new",
|
|
)
|
|
}
|
|
/>
|
|
</TabPage>
|
|
</AppBackground>
|
|
);
|
|
}
|
|
|
|
const createEntitiesStyles = (colors: ThemeColors, isDark: boolean) =>
|
|
StyleSheet.create({
|
|
tabScroll: {
|
|
flexGrow: 0,
|
|
marginBottom: spacing.sm,
|
|
},
|
|
tabs: {
|
|
gap: spacing.sm,
|
|
paddingRight: spacing.md,
|
|
},
|
|
card: {},
|
|
cardInner: {
|
|
padding: spacing.md,
|
|
gap: 4,
|
|
},
|
|
nameRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: spacing.sm,
|
|
flexWrap: "wrap",
|
|
},
|
|
name: {
|
|
fontSize: 16,
|
|
fontFamily: fonts.bodySemiBold,
|
|
color: colors.foreground,
|
|
},
|
|
badge: {
|
|
fontSize: 11,
|
|
fontFamily: fonts.bodySemiBold,
|
|
color: colors.primary,
|
|
backgroundColor: isDark ? "rgba(74, 222, 128, 0.15)" : colors.muted,
|
|
paddingHorizontal: spacing.sm,
|
|
paddingVertical: 2,
|
|
borderRadius: 999,
|
|
overflow: "hidden",
|
|
},
|
|
meta: {
|
|
fontSize: 14,
|
|
fontFamily: fonts.body,
|
|
color: colors.mutedForeground,
|
|
},
|
|
empty: {
|
|
padding: spacing.lg,
|
|
alignItems: "center",
|
|
gap: spacing.sm,
|
|
},
|
|
emptyTitle: {
|
|
fontSize: 18,
|
|
fontFamily: fonts.bodySemiBold,
|
|
color: colors.foreground,
|
|
},
|
|
emptyText: {
|
|
textAlign: "center",
|
|
color: colors.mutedForeground,
|
|
fontFamily: fonts.body,
|
|
lineHeight: 20,
|
|
},
|
|
errorBox: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
padding: spacing.lg,
|
|
gap: spacing.sm,
|
|
},
|
|
errorTitle: {
|
|
fontSize: 18,
|
|
fontFamily: fonts.bodySemiBold,
|
|
color: colors.foreground,
|
|
},
|
|
errorText: {
|
|
color: colors.mutedForeground,
|
|
fontFamily: fonts.body,
|
|
lineHeight: 20,
|
|
},
|
|
});
|