Refresh home and more navigation
This commit is contained in:
+449
-266
@@ -1,31 +1,43 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
import { Pressable, RefreshControl, StyleSheet, Text, View } from "react-native";
|
||||
import { Screen } from "@/components/Screen";
|
||||
import { TabPage } from "@/components/TabPage";
|
||||
import { TabScrollView } from "@/components/TabScrollView";
|
||||
|
||||
import { AppBackground } from "@/components/AppBackground";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { GlassSurface } from "@/components/GlassSurface";
|
||||
import { LoadingScreen } from "@/components/LoadingScreen";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { Screen } from "@/components/Screen";
|
||||
import { StatCard } from "@/components/StatCard";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { TabPage } from "@/components/TabPage";
|
||||
import { TabScrollView } from "@/components/TabScrollView";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { fonts, radii, spacing } from "@/constants/theme";
|
||||
import { useSession } from "@/contexts/AuthContext";
|
||||
import { useAppTheme } from "@/contexts/ThemeContext";
|
||||
import { formatCurrency, formatDate } from "@/lib/format";
|
||||
import type { ThemeColors } from "@/lib/theme-palette";
|
||||
import { useThemedStyles } from "@/lib/use-themed-styles";
|
||||
import { getInvoiceStatus } from "@/lib/invoice-status";
|
||||
import type { ThemeColors } from "@/lib/theme-palette";
|
||||
import { formatElapsedHoursMinutes, resolveClockDescription } from "@/lib/time-clock";
|
||||
import { useRunningElapsed } from "@/lib/use-running-elapsed";
|
||||
import { formatTrpcErrorMessage } from "@/lib/trpc-errors";
|
||||
import { useRunningElapsed } from "@/lib/use-running-elapsed";
|
||||
import { useThemedStyles } from "@/lib/use-themed-styles";
|
||||
import { api } from "@/lib/trpc";
|
||||
|
||||
type ActionItem = {
|
||||
key: string;
|
||||
title: string;
|
||||
detail: string;
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
tone: "warning" | "primary" | "success";
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
export default function DashboardScreen() {
|
||||
const { colors } = useAppTheme();
|
||||
const styles = useThemedStyles(createDashboardStyles);
|
||||
const { data: session } = useSession();
|
||||
const statsQuery = api.dashboard.getStats.useQuery();
|
||||
const runningQuery = api.timeEntries.getRunning.useQuery(undefined, {
|
||||
refetchInterval: 30_000,
|
||||
@@ -33,7 +45,7 @@ export default function DashboardScreen() {
|
||||
const runningElapsed = useRunningElapsed(runningQuery.data?.startedAt);
|
||||
|
||||
if (statsQuery.isLoading) {
|
||||
return <LoadingScreen message="Loading dashboard…" />;
|
||||
return <LoadingScreen message="Loading home…" />;
|
||||
}
|
||||
|
||||
if (statsQuery.error) {
|
||||
@@ -41,7 +53,7 @@ export default function DashboardScreen() {
|
||||
<AppBackground>
|
||||
<Screen>
|
||||
<View style={styles.errorBox}>
|
||||
<Text style={styles.errorTitle}>Could not load dashboard</Text>
|
||||
<Text style={styles.errorTitle}>Could not load home</Text>
|
||||
<Text style={styles.errorText}>{formatTrpcErrorMessage(statsQuery.error)}</Text>
|
||||
</View>
|
||||
</Screen>
|
||||
@@ -51,27 +63,94 @@ export default function DashboardScreen() {
|
||||
|
||||
const stats = statsQuery.data;
|
||||
if (!stats) {
|
||||
return <LoadingScreen message="Loading dashboard…" />;
|
||||
return <LoadingScreen message="Loading home…" />;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const running = runningQuery.data;
|
||||
const revenueChange =
|
||||
stats.revenueChange > 0
|
||||
? `+${stats.revenueChange.toFixed(0)}% vs last month`
|
||||
: stats.revenueChange < 0
|
||||
? `${stats.revenueChange.toFixed(0)}% vs last month`
|
||||
: "No change vs last month";
|
||||
|
||||
const runningClient = running?.client?.name ?? "No client";
|
||||
const monthInvoices = stats.monthInvoices ?? [];
|
||||
const monthTotal = monthInvoices.reduce((sum, invoice) => sum + invoice.totalAmount, 0);
|
||||
const maxRevenue = Math.max(...stats.revenueChartData.map((d) => d.revenue), 1);
|
||||
const sendReminderDue = stats.recentInvoices.filter((inv) => inv.status === "draft");
|
||||
const drafts = stats.recentInvoices.filter((invoice) => invoice.status === "draft");
|
||||
const pendingInvoices = monthInvoices.filter((invoice) => {
|
||||
const status = getInvoiceStatus(invoice);
|
||||
return status === "sent" || status === "overdue";
|
||||
});
|
||||
const overdueInvoices = monthInvoices.filter((invoice) => getInvoiceStatus(invoice) === "overdue");
|
||||
const displayName = session?.user.name?.trim();
|
||||
const firstName =
|
||||
(displayName ? displayName.split(/\s+/)[0] : undefined) ??
|
||||
session?.user.email?.split("@")[0] ??
|
||||
"there";
|
||||
|
||||
const actionItems: ActionItem[] = [
|
||||
...(running
|
||||
? [
|
||||
{
|
||||
key: "running",
|
||||
title: "Timer running",
|
||||
detail: `${formatElapsedHoursMinutes(runningElapsed)} on ${runningClient}`,
|
||||
icon: "timer-outline" as const,
|
||||
tone: "success" as const,
|
||||
onPress: () => router.push("/(app)/timer"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(overdueInvoices.length > 0
|
||||
? [
|
||||
{
|
||||
key: "overdue",
|
||||
title: `${overdueInvoices.length} overdue ${overdueInvoices.length === 1 ? "invoice" : "invoices"}`,
|
||||
detail: `${formatCurrency(overdueInvoices.reduce((sum, invoice) => sum + invoice.totalAmount, 0))} needs follow-up`,
|
||||
icon: "alert-circle-outline" as const,
|
||||
tone: "warning" as const,
|
||||
onPress: () => router.push("/(app)/invoices"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(drafts.length > 0
|
||||
? [
|
||||
{
|
||||
key: "drafts",
|
||||
title: `${drafts.length} draft ${drafts.length === 1 ? "invoice" : "invoices"}`,
|
||||
detail: "Review and send when ready",
|
||||
icon: "document-text-outline" as const,
|
||||
tone: "primary" as const,
|
||||
onPress: () => router.push("/(app)/invoices"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(pendingInvoices.length > 0
|
||||
? [
|
||||
{
|
||||
key: "pending",
|
||||
title: `${pendingInvoices.length} awaiting payment`,
|
||||
detail: `${formatCurrency(pendingInvoices.reduce((sum, invoice) => sum + invoice.totalAmount, 0))} outstanding this month`,
|
||||
icon: "card-outline" as const,
|
||||
tone: "primary" as const,
|
||||
onPress: () => router.push("/(app)/invoices"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
if (actionItems.length === 0) {
|
||||
actionItems.push({
|
||||
key: "clear",
|
||||
title: "No urgent action items",
|
||||
detail: "You are clear for the moment",
|
||||
icon: "checkmark-circle-outline",
|
||||
tone: "success",
|
||||
onPress: () => router.push("/(app)/invoices"),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<AppBackground>
|
||||
<TabPage>
|
||||
<TabScrollView
|
||||
header={
|
||||
<PageHeader title="Overview" subtitle="Your invoicing at a glance" />
|
||||
}
|
||||
header={<PageHeader title={`Hello, ${firstName}`} subtitle="What needs attention now" />}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={statsQuery.isRefetching || runningQuery.isRefetching}
|
||||
@@ -83,6 +162,64 @@ export default function DashboardScreen() {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<View style={styles.quickActions}>
|
||||
<Button title="Start timer" onPress={() => router.push("/(app)/timer")} />
|
||||
<Button
|
||||
title="Invoices"
|
||||
variant="secondary"
|
||||
onPress={() => router.push("/(app)/invoices")}
|
||||
/>
|
||||
<Button
|
||||
title="Reports"
|
||||
variant="secondary"
|
||||
onPress={() => router.push("/(app)/more/reports" as never)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Card title="Action items">
|
||||
<View style={styles.actionList}>
|
||||
{actionItems.map((item) => (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
key={item.key}
|
||||
onPress={item.onPress}
|
||||
style={({ pressed }) => [styles.actionRow, pressed && styles.pressed]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.actionIcon,
|
||||
{
|
||||
backgroundColor:
|
||||
item.tone === "warning"
|
||||
? colors.warningBg
|
||||
: item.tone === "success"
|
||||
? colors.successBg
|
||||
: colors.muted,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={20}
|
||||
color={
|
||||
item.tone === "warning"
|
||||
? colors.warning
|
||||
: item.tone === "success"
|
||||
? colors.success
|
||||
: colors.primary
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.actionCopy}>
|
||||
<Text style={styles.actionTitle}>{item.title}</Text>
|
||||
<Text style={styles.actionDetail}>{item.detail}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.mutedForeground} />
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{running ? (
|
||||
<Pressable onPress={() => router.push("/(app)/timer")}>
|
||||
<GlassSurface style={styles.runningGlass}>
|
||||
@@ -93,7 +230,7 @@ export default function DashboardScreen() {
|
||||
{resolveClockDescription(running.description)}
|
||||
</Text>
|
||||
<Text style={styles.runningSub}>
|
||||
{running.client?.name ?? "No client"}
|
||||
{runningClient}
|
||||
{running.invoice
|
||||
? ` · ${running.invoice.invoicePrefix ?? "#"}${running.invoice.invoiceNumber}`
|
||||
: ""}
|
||||
@@ -107,95 +244,93 @@ export default function DashboardScreen() {
|
||||
</Pressable>
|
||||
) : null}
|
||||
|
||||
{stats.overdueCount > 0 ? (
|
||||
<GlassSurface style={styles.alertGlass}>
|
||||
<View style={styles.alertBanner}>
|
||||
<Text style={styles.alertTitle}>
|
||||
{stats.overdueCount} overdue {stats.overdueCount === 1 ? "invoice" : "invoices"}
|
||||
</Text>
|
||||
<Text style={styles.alertText}>
|
||||
Follow up on outstanding payments from the Invoices tab.
|
||||
<Card
|
||||
title={now.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
>
|
||||
<View style={styles.monthSummary}>
|
||||
<View>
|
||||
<Text style={styles.monthValue}>{formatCurrency(monthTotal)}</Text>
|
||||
<Text style={styles.monthLabel}>
|
||||
{monthInvoices.length} {monthInvoices.length === 1 ? "invoice" : "invoices"} this month
|
||||
</Text>
|
||||
</View>
|
||||
</GlassSurface>
|
||||
) : null}
|
||||
<Button
|
||||
title="New invoice"
|
||||
variant="secondary"
|
||||
onPress={() => router.push("/(app)/invoices/new")}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{sendReminderDue.length > 0 ? (
|
||||
<GlassSurface style={styles.alertGlass}>
|
||||
<View style={styles.alertBanner}>
|
||||
<Text style={styles.alertTitle}>
|
||||
{sendReminderDue.length} draft{" "}
|
||||
{sendReminderDue.length === 1 ? "invoice" : "invoices"} ready to send
|
||||
</Text>
|
||||
<Text style={styles.alertText}>
|
||||
{sendReminderDue
|
||||
.slice(0, 2)
|
||||
.map(
|
||||
(inv) =>
|
||||
`${inv.invoicePrefix ?? "#"}${inv.invoiceNumber} (${inv.client?.name ?? "Client"})`,
|
||||
)
|
||||
.join(" · ")}
|
||||
</Text>
|
||||
</View>
|
||||
</GlassSurface>
|
||||
) : null}
|
||||
|
||||
<View style={styles.quickActions}>
|
||||
<Button title="Start timer" onPress={() => router.push("/(app)/timer")} />
|
||||
<Button
|
||||
title="View invoices"
|
||||
variant="secondary"
|
||||
onPress={() => router.push("/(app)/invoices")}
|
||||
/>
|
||||
<Button
|
||||
title="Reports"
|
||||
variant="secondary"
|
||||
onPress={() => router.push("/(app)/more/reports" as never)}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.monthList}>
|
||||
{monthInvoices.slice(0, 4).map((invoice) => {
|
||||
const status = getInvoiceStatus(invoice);
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
key={invoice.id}
|
||||
onPress={() => router.push(`/(app)/invoices/${invoice.id}`)}
|
||||
style={({ pressed }) => [styles.monthInvoiceRow, pressed && styles.pressed]}
|
||||
>
|
||||
<View style={styles.invoiceMeta}>
|
||||
<Text style={styles.invoiceTitle}>
|
||||
{invoice.invoicePrefix}
|
||||
{invoice.invoiceNumber}
|
||||
</Text>
|
||||
<Text style={styles.invoiceClient}>{invoice.client?.name ?? "Client"}</Text>
|
||||
</View>
|
||||
<View style={styles.invoiceRight}>
|
||||
<Text style={styles.invoiceAmount}>
|
||||
{formatCurrency(invoice.totalAmount, invoice.currency)}
|
||||
</Text>
|
||||
<StatusBadge status={status} />
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
{monthInvoices.length === 0 ? (
|
||||
<Text style={styles.empty}>No invoices in this month yet.</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{stats.currentDraft ? (
|
||||
<GlassSurface style={styles.alertGlass}>
|
||||
<GlassSurface style={styles.draftGlass}>
|
||||
<Pressable
|
||||
style={styles.alertBanner}
|
||||
style={styles.draftBanner}
|
||||
onPress={() => router.push(`/(app)/invoices/${stats.currentDraft!.id}`)}
|
||||
>
|
||||
<Text style={styles.alertTitle}>
|
||||
Draft {stats.currentDraft.invoiceNumber}
|
||||
</Text>
|
||||
<Text style={styles.alertText}>
|
||||
{stats.currentDraft.client?.name ?? "Client"} ·{" "}
|
||||
{formatCurrency(stats.currentDraft.totalAmount)} ·{" "}
|
||||
{stats.currentDraft.totalHours.toFixed(1)}h logged
|
||||
</Text>
|
||||
<View style={styles.draftCopy}>
|
||||
<Text style={styles.draftTitle}>Current draft</Text>
|
||||
<Text style={styles.draftText}>
|
||||
{stats.currentDraft.client?.name ?? "Client"} ·{" "}
|
||||
{formatCurrency(stats.currentDraft.totalAmount)} ·{" "}
|
||||
{stats.currentDraft.totalHours.toFixed(1)}h logged
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.mutedForeground} />
|
||||
</Pressable>
|
||||
</GlassSurface>
|
||||
) : null}
|
||||
|
||||
<View style={styles.statsGrid}>
|
||||
<View style={styles.statCell}>
|
||||
<StatCard label="Total revenue" value={formatCurrency(stats.totalRevenue)} />
|
||||
<StatCard label="Revenue" value={formatCurrency(stats.totalRevenue)} />
|
||||
</View>
|
||||
<View style={styles.statCell}>
|
||||
<StatCard label="Pending" value={formatCurrency(stats.pendingAmount)} />
|
||||
</View>
|
||||
<View style={styles.statCell}>
|
||||
<StatCard
|
||||
label="Overdue"
|
||||
value={String(stats.overdueCount)}
|
||||
hint={stats.overdueCount === 1 ? "invoice" : "invoices"}
|
||||
/>
|
||||
<StatCard label="Overdue" value={String(stats.overdueCount)} />
|
||||
</View>
|
||||
<Pressable style={styles.statCell} onPress={() => router.push("/(app)/entities")}>
|
||||
<StatCard
|
||||
label="Clients"
|
||||
value={String(stats.totalClients)}
|
||||
hint={revenueChange}
|
||||
/>
|
||||
<StatCard label="Clients" value={String(stats.totalClients)} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<Card title="Revenue (6 months)">
|
||||
<Card title="Revenue trend">
|
||||
<View style={styles.chart}>
|
||||
{stats.revenueChartData.map((point) => {
|
||||
const barHeight = Math.max(4, (point.revenue / maxRevenue) * 80);
|
||||
@@ -205,26 +340,12 @@ export default function DashboardScreen() {
|
||||
<View style={[styles.chartBar, { height: barHeight }]} />
|
||||
</View>
|
||||
<Text style={styles.chartLabel}>{point.monthLabel}</Text>
|
||||
<Text style={styles.chartValue}>
|
||||
{point.revenue > 0 ? formatCurrency(point.revenue) : "—"}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
<Card title="Invoice status">
|
||||
{(stats.statusChartData ?? []).map((item) => (
|
||||
<View key={item.status} style={styles.invoiceRow}>
|
||||
<Text style={styles.invoiceClient}>{item.name}</Text>
|
||||
<Text style={styles.invoiceDate}>
|
||||
{item.count} · {formatCurrency(item.value)}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
<Card title="Recent invoices">
|
||||
{stats.recentInvoices.length === 0 ? (
|
||||
<Text style={styles.empty}>No invoices yet. Create one from the Invoices tab.</Text>
|
||||
@@ -234,7 +355,7 @@ export default function DashboardScreen() {
|
||||
return (
|
||||
<Pressable
|
||||
key={invoice.id}
|
||||
style={styles.invoiceRow}
|
||||
style={({ pressed }) => [styles.recentRow, pressed && styles.pressed]}
|
||||
onPress={() => router.push(`/(app)/invoices/${invoice.id}`)}
|
||||
>
|
||||
<View style={styles.invoiceMeta}>
|
||||
@@ -242,9 +363,7 @@ export default function DashboardScreen() {
|
||||
{invoice.invoicePrefix}
|
||||
{invoice.invoiceNumber}
|
||||
</Text>
|
||||
<Text style={styles.invoiceClient}>
|
||||
{invoice.client?.name ?? "Client"}
|
||||
</Text>
|
||||
<Text style={styles.invoiceClient}>{invoice.client?.name ?? "Client"}</Text>
|
||||
<Text style={styles.invoiceDate}>{formatDate(invoice.issueDate)}</Text>
|
||||
</View>
|
||||
<View style={styles.invoiceRight}>
|
||||
@@ -266,164 +385,228 @@ export default function DashboardScreen() {
|
||||
|
||||
const createDashboardStyles = (colors: ThemeColors, isDark: boolean) =>
|
||||
StyleSheet.create({
|
||||
safe: {
|
||||
flex: 1,
|
||||
},
|
||||
runningGlass: {
|
||||
borderColor: isDark ? "rgba(74, 222, 128, 0.35)" : "#BBF7D0",
|
||||
},
|
||||
runningRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.md,
|
||||
padding: spacing.md,
|
||||
},
|
||||
runningDot: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.success,
|
||||
},
|
||||
runningMeta: {
|
||||
flex: 1,
|
||||
gap: 2,
|
||||
},
|
||||
runningTitle: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
color: colors.foreground,
|
||||
fontSize: 14,
|
||||
},
|
||||
runningSub: {
|
||||
fontFamily: fonts.body,
|
||||
color: colors.mutedForeground,
|
||||
fontSize: 12,
|
||||
},
|
||||
runningTime: {
|
||||
fontFamily: fonts.mono,
|
||||
fontSize: 18,
|
||||
color: colors.success,
|
||||
},
|
||||
alertBanner: {
|
||||
padding: spacing.md,
|
||||
gap: 4,
|
||||
},
|
||||
alertGlass: {
|
||||
borderColor: isDark ? "rgba(251, 191, 36, 0.4)" : "#FDE68A",
|
||||
},
|
||||
alertTitle: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
color: colors.warning,
|
||||
fontSize: 14,
|
||||
},
|
||||
alertText: {
|
||||
fontFamily: fonts.body,
|
||||
color: colors.mutedForeground,
|
||||
fontSize: 13,
|
||||
},
|
||||
quickActions: {
|
||||
flexDirection: "row",
|
||||
gap: spacing.sm,
|
||||
},
|
||||
statsGrid: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: spacing.md,
|
||||
alignContent: "flex-start",
|
||||
},
|
||||
statCell: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexBasis: "47%",
|
||||
},
|
||||
chart: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
gap: spacing.xs,
|
||||
},
|
||||
chartColumn: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
},
|
||||
chartBarTrack: {
|
||||
width: "100%",
|
||||
height: 80,
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
},
|
||||
chartBar: {
|
||||
width: "70%",
|
||||
minHeight: 4,
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: radii.sm,
|
||||
},
|
||||
chartLabel: {
|
||||
fontSize: 10,
|
||||
fontFamily: fonts.bodyMedium,
|
||||
color: colors.mutedForeground,
|
||||
},
|
||||
chartValue: {
|
||||
fontSize: 9,
|
||||
fontFamily: fonts.body,
|
||||
color: colors.mutedForeground,
|
||||
textAlign: "center",
|
||||
},
|
||||
empty: {
|
||||
color: colors.mutedForeground,
|
||||
fontSize: 14,
|
||||
fontFamily: fonts.body,
|
||||
lineHeight: 20,
|
||||
},
|
||||
invoiceRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
gap: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
invoiceMeta: {
|
||||
flex: 1,
|
||||
gap: 2,
|
||||
},
|
||||
invoiceTitle: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
color: colors.foreground,
|
||||
fontSize: 15,
|
||||
},
|
||||
invoiceClient: {
|
||||
color: colors.mutedForeground,
|
||||
fontSize: 14,
|
||||
fontFamily: fonts.body,
|
||||
},
|
||||
invoiceDate: {
|
||||
color: colors.mutedForeground,
|
||||
fontSize: 12,
|
||||
fontFamily: fonts.body,
|
||||
},
|
||||
invoiceRight: {
|
||||
alignItems: "flex-end",
|
||||
gap: spacing.sm,
|
||||
},
|
||||
invoiceAmount: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
color: colors.foreground,
|
||||
fontSize: 15,
|
||||
},
|
||||
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,
|
||||
},
|
||||
});
|
||||
actionList: {
|
||||
gap: spacing.xs,
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.md,
|
||||
minHeight: 58,
|
||||
paddingVertical: spacing.xs,
|
||||
},
|
||||
actionIcon: {
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: radii.md,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
actionCopy: {
|
||||
flex: 1,
|
||||
gap: 2,
|
||||
},
|
||||
actionTitle: {
|
||||
color: colors.foreground,
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
},
|
||||
actionDetail: {
|
||||
color: colors.mutedForeground,
|
||||
fontFamily: fonts.body,
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
},
|
||||
runningGlass: {
|
||||
borderColor: isDark ? "rgba(74, 222, 128, 0.35)" : "#BBF7D0",
|
||||
},
|
||||
runningRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.md,
|
||||
padding: spacing.md,
|
||||
},
|
||||
runningDot: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.success,
|
||||
},
|
||||
runningMeta: {
|
||||
flex: 1,
|
||||
gap: 2,
|
||||
},
|
||||
runningTitle: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
color: colors.foreground,
|
||||
fontSize: 14,
|
||||
},
|
||||
runningSub: {
|
||||
fontFamily: fonts.body,
|
||||
color: colors.mutedForeground,
|
||||
fontSize: 12,
|
||||
},
|
||||
runningTime: {
|
||||
fontFamily: fonts.mono,
|
||||
fontSize: 18,
|
||||
color: colors.success,
|
||||
},
|
||||
monthSummary: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: spacing.md,
|
||||
},
|
||||
monthValue: {
|
||||
color: colors.foreground,
|
||||
fontFamily: fonts.heading,
|
||||
fontSize: 24,
|
||||
lineHeight: 30,
|
||||
},
|
||||
monthLabel: {
|
||||
color: colors.mutedForeground,
|
||||
fontFamily: fonts.bodyMedium,
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
},
|
||||
monthList: {
|
||||
gap: spacing.xs,
|
||||
},
|
||||
monthInvoiceRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: spacing.md,
|
||||
paddingTop: spacing.sm,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
quickActions: {
|
||||
flexDirection: "row",
|
||||
gap: spacing.sm,
|
||||
},
|
||||
draftGlass: {
|
||||
borderColor: isDark ? "rgba(59, 130, 246, 0.32)" : "#BFDBFE",
|
||||
},
|
||||
draftBanner: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.md,
|
||||
padding: spacing.md,
|
||||
},
|
||||
draftCopy: {
|
||||
flex: 1,
|
||||
gap: 3,
|
||||
},
|
||||
draftTitle: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
color: colors.primary,
|
||||
fontSize: 14,
|
||||
},
|
||||
draftText: {
|
||||
fontFamily: fonts.body,
|
||||
color: colors.mutedForeground,
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
statsGrid: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: spacing.md,
|
||||
alignContent: "flex-start",
|
||||
},
|
||||
statCell: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexBasis: "47%",
|
||||
},
|
||||
chart: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
gap: spacing.xs,
|
||||
},
|
||||
chartColumn: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
},
|
||||
chartBarTrack: {
|
||||
width: "100%",
|
||||
height: 80,
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
},
|
||||
chartBar: {
|
||||
width: "70%",
|
||||
minHeight: 4,
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: radii.sm,
|
||||
},
|
||||
chartLabel: {
|
||||
fontSize: 10,
|
||||
fontFamily: fonts.bodyMedium,
|
||||
color: colors.mutedForeground,
|
||||
},
|
||||
empty: {
|
||||
color: colors.mutedForeground,
|
||||
fontSize: 14,
|
||||
fontFamily: fonts.body,
|
||||
lineHeight: 20,
|
||||
},
|
||||
recentRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
gap: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
invoiceMeta: {
|
||||
flex: 1,
|
||||
gap: 2,
|
||||
},
|
||||
invoiceTitle: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
color: colors.foreground,
|
||||
fontSize: 15,
|
||||
},
|
||||
invoiceClient: {
|
||||
color: colors.mutedForeground,
|
||||
fontSize: 14,
|
||||
fontFamily: fonts.body,
|
||||
},
|
||||
invoiceDate: {
|
||||
color: colors.mutedForeground,
|
||||
fontSize: 12,
|
||||
fontFamily: fonts.body,
|
||||
},
|
||||
invoiceRight: {
|
||||
alignItems: "flex-end",
|
||||
gap: spacing.sm,
|
||||
},
|
||||
invoiceAmount: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
color: colors.foreground,
|
||||
fontSize: 15,
|
||||
},
|
||||
pressed: {
|
||||
opacity: 0.85,
|
||||
},
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user