Polish mobile and web experience
This commit is contained in:
@@ -2,7 +2,6 @@ import { router } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
@@ -15,6 +14,7 @@ import { FloatingActionButton } from "@/components/FloatingActionButton";
|
||||
import { GlassSurface } from "@/components/GlassSurface";
|
||||
import { LoadingScreen } from "@/components/LoadingScreen";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { PullToRefresh } from "@/components/PullToRefresh";
|
||||
import { SwipeableRow } from "@/components/SwipeableRow";
|
||||
import { TabPage } from "@/components/TabPage";
|
||||
import { TabScrollView } from "@/components/TabScrollView";
|
||||
@@ -71,8 +71,7 @@ export default function EntitiesScreen() {
|
||||
const businesses = businessesQuery.data ?? [];
|
||||
|
||||
function refresh() {
|
||||
if (tab === "clients") void clientsQuery.refetch();
|
||||
else void businessesQuery.refetch();
|
||||
return tab === "clients" ? clientsQuery.refetch() : businessesQuery.refetch();
|
||||
}
|
||||
|
||||
function confirmDelete(id: string, name: string) {
|
||||
@@ -100,8 +99,7 @@ export default function EntitiesScreen() {
|
||||
/>
|
||||
}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={activeQuery.isRefetching}
|
||||
<PullToRefresh
|
||||
onRefresh={refresh}
|
||||
tintColor={colors.primary}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
import { Pressable, RefreshControl, StyleSheet, Text, View } from "react-native";
|
||||
import { Pressable, StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import { AppBackground } from "@/components/AppBackground";
|
||||
import { GlassSurface } from "@/components/GlassSurface";
|
||||
import { LoadingScreen } from "@/components/LoadingScreen";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { PullToRefresh } from "@/components/PullToRefresh";
|
||||
import { Screen } from "@/components/Screen";
|
||||
import { StatCard } from "@/components/StatCard";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
@@ -152,12 +153,8 @@ export default function DashboardScreen() {
|
||||
<TabScrollView
|
||||
header={<PageHeader title={`Hello, ${firstName}`} subtitle="What needs attention now" />}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={statsQuery.isRefetching || runningQuery.isRefetching}
|
||||
onRefresh={() => {
|
||||
void statsQuery.refetch();
|
||||
void runningQuery.refetch();
|
||||
}}
|
||||
<PullToRefresh
|
||||
onRefresh={() => Promise.all([statsQuery.refetch(), runningQuery.refetch()])}
|
||||
tintColor={colors.primary}
|
||||
/>
|
||||
}
|
||||
@@ -221,7 +218,10 @@ export default function DashboardScreen() {
|
||||
</Card>
|
||||
|
||||
{running ? (
|
||||
<Pressable onPress={() => router.push("/(app)/timer")}>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
onPress={() => router.push("/(app)/timer")}
|
||||
>
|
||||
<GlassSurface style={styles.runningGlass}>
|
||||
<View style={styles.runningRow}>
|
||||
<View style={styles.runningDot} />
|
||||
@@ -299,6 +299,11 @@ export default function DashboardScreen() {
|
||||
{stats.currentDraft ? (
|
||||
<GlassSurface style={styles.draftGlass}>
|
||||
<Pressable
|
||||
accessible
|
||||
accessibilityLabel={`Current draft, ${
|
||||
stats.currentDraft.client?.name ?? "Client"
|
||||
}, ${formatCurrency(stats.currentDraft.totalAmount)}, ${stats.currentDraft.totalHours.toFixed(1)} hours logged`}
|
||||
accessibilityRole="button"
|
||||
style={styles.draftBanner}
|
||||
onPress={() => router.push(`/(app)/invoices/${stats.currentDraft!.id}`)}
|
||||
>
|
||||
@@ -325,7 +330,12 @@ export default function DashboardScreen() {
|
||||
<View style={styles.statCell}>
|
||||
<StatCard label="Overdue" value={String(stats.overdueCount)} />
|
||||
</View>
|
||||
<Pressable style={styles.statCell} onPress={() => router.push("/(app)/entities")}>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Clients, ${stats.totalClients}`}
|
||||
style={styles.statCell}
|
||||
onPress={() => router.push("/(app)/entities")}
|
||||
>
|
||||
<StatCard label="Clients" value={String(stats.totalClients)} />
|
||||
</Pressable>
|
||||
</View>
|
||||
@@ -354,6 +364,14 @@ export default function DashboardScreen() {
|
||||
const status = getInvoiceStatus(invoice);
|
||||
return (
|
||||
<Pressable
|
||||
accessible
|
||||
accessibilityLabel={`${invoice.invoicePrefix}${invoice.invoiceNumber}, ${
|
||||
invoice.client?.name ?? "Client"
|
||||
}, ${formatDate(invoice.issueDate)}, ${formatCurrency(
|
||||
invoice.totalAmount,
|
||||
invoice.currency,
|
||||
)}, ${status}`}
|
||||
accessibilityRole="button"
|
||||
key={invoice.id}
|
||||
style={({ pressed }) => [styles.recentRow, pressed && styles.pressed]}
|
||||
onPress={() => router.push(`/(app)/invoices/${invoice.id}`)}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { router } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
@@ -15,6 +14,7 @@ import { FloatingActionButton } from "@/components/FloatingActionButton";
|
||||
import { GlassSurface } from "@/components/GlassSurface";
|
||||
import { LoadingScreen } from "@/components/LoadingScreen";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { PullToRefresh } from "@/components/PullToRefresh";
|
||||
import { SwipeableRow } from "@/components/SwipeableRow";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { TabPage } from "@/components/TabPage";
|
||||
@@ -121,8 +121,7 @@ export default function InvoicesScreen() {
|
||||
<PageHeader title="Invoices" subtitle="Review status, amounts, and due dates" />
|
||||
}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={invoicesQuery.isRefetching}
|
||||
<PullToRefresh
|
||||
onRefresh={() => invoicesQuery.refetch()}
|
||||
tintColor={colors.primary}
|
||||
/>
|
||||
|
||||
@@ -29,6 +29,7 @@ import { formatCurrency } from "@/lib/format";
|
||||
import {
|
||||
isRequiredString,
|
||||
isValidTaxRate,
|
||||
useFieldVisibility,
|
||||
validateLineItems,
|
||||
} from "@/lib/form-validation";
|
||||
import { resolveInvoiceBusinessId } from "@/lib/invoice-business";
|
||||
@@ -70,6 +71,7 @@ export default function NewInvoiceScreen() {
|
||||
);
|
||||
const [section, setSection] = useState<InvoiceEditorSection>("setup");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { touch, visible, markSubmitted } = useFieldVisibility();
|
||||
|
||||
useEffect(() => {
|
||||
if (businessId || !businessesQuery.data?.length) return;
|
||||
@@ -184,10 +186,12 @@ export default function NewInvoiceScreen() {
|
||||
}
|
||||
|
||||
function updateItem(index: number, patch: Partial<EditableLineItem>) {
|
||||
touch("lineItems");
|
||||
setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item)));
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
touch("lineItems");
|
||||
setItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
@@ -200,10 +204,12 @@ export default function NewInvoiceScreen() {
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
touch("lineItems");
|
||||
setItems((prev) => prev.filter((_, i) => i !== index));
|
||||
}
|
||||
|
||||
function duplicateItem(index: number) {
|
||||
touch("lineItems");
|
||||
setItems((prev) => {
|
||||
const source = prev[index];
|
||||
if (!source) return prev;
|
||||
@@ -213,6 +219,7 @@ export default function NewInvoiceScreen() {
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
markSubmitted();
|
||||
if (!canCreate) return;
|
||||
setError(null);
|
||||
|
||||
@@ -296,27 +303,31 @@ export default function NewInvoiceScreen() {
|
||||
businessId={businessId}
|
||||
onBusinessIdChange={setBusinessId}
|
||||
businessOptions={businessOptions}
|
||||
businessError={businessError}
|
||||
businessError={visible("business") ? businessError : undefined}
|
||||
onBusinessBlur={() => touch("business")}
|
||||
clientId={clientId}
|
||||
onClientIdChange={setClientId}
|
||||
clientOptions={clientOptions}
|
||||
clientError={clientError}
|
||||
clientError={visible("client") ? clientError : undefined}
|
||||
onClientBlur={() => touch("client")}
|
||||
invoiceNumber={invoiceNumber}
|
||||
onInvoiceNumberChange={setInvoiceNumber}
|
||||
invoiceNumberError={
|
||||
visible("invoiceNumber") ? invoiceNumberError : undefined
|
||||
}
|
||||
onInvoiceNumberBlur={() => touch("invoiceNumber")}
|
||||
issueDate={issueDate}
|
||||
onIssueDateChange={setIssueDate}
|
||||
dueDate={dueDate}
|
||||
onDueDateChange={setDueDate}
|
||||
taxRate={taxRate}
|
||||
onTaxRateChange={setTaxRate}
|
||||
taxRateError={visible("taxRate") ? taxError : undefined}
|
||||
onTaxRateBlur={() => touch("taxRate")}
|
||||
notes={notes}
|
||||
onNotesChange={setNotes}
|
||||
/>
|
||||
)}
|
||||
{taxError ? <Text style={styles.error}>{taxError}</Text> : null}
|
||||
{invoiceNumberError ? (
|
||||
<Text style={styles.error}>{invoiceNumberError}</Text>
|
||||
) : null}
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
@@ -354,11 +365,13 @@ export default function NewInvoiceScreen() {
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{lineItemsError ? <Text style={styles.error}>{lineItemsError}</Text> : null}
|
||||
{visible("lineItems") && lineItemsError ? (
|
||||
<Text selectable style={styles.error}>{lineItemsError}</Text>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
|
||||
{error ? <Text style={styles.error}>{error}</Text> : null}
|
||||
{error ? <Text selectable style={styles.error}>{error}</Text> : null}
|
||||
|
||||
<InvoiceEditorFooter
|
||||
primaryTitle={isBlank ? "Create blank invoice" : "Create invoice"}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
@@ -15,6 +14,7 @@ import { AppBackground } from "@/components/AppBackground";
|
||||
import { FilterChip } from "@/components/FilterChip";
|
||||
import { LoadingScreen } from "@/components/LoadingScreen";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { PullToRefresh } from "@/components/PullToRefresh";
|
||||
import { SwipeableRow } from "@/components/SwipeableRow";
|
||||
import { TabPage } from "@/components/TabPage";
|
||||
import { TabScrollView } from "@/components/TabScrollView";
|
||||
@@ -110,9 +110,8 @@ export default function ExpensesScreen() {
|
||||
</View>
|
||||
}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={expensesQuery.isRefetching}
|
||||
onRefresh={() => void expensesQuery.refetch()}
|
||||
<PullToRefresh
|
||||
onRefresh={() => expensesQuery.refetch()}
|
||||
tintColor={colors.primary}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { RefreshControl, StyleSheet, Text, View } from "react-native";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import { AppBackground } from "@/components/AppBackground";
|
||||
import { LoadingScreen } from "@/components/LoadingScreen";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { PullToRefresh } from "@/components/PullToRefresh";
|
||||
import { SwipeableRow } from "@/components/SwipeableRow";
|
||||
import { TabPage } from "@/components/TabPage";
|
||||
import { TabScrollView } from "@/components/TabScrollView";
|
||||
@@ -46,9 +47,8 @@ export default function RecurringScreen() {
|
||||
/>
|
||||
}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={query.isRefetching}
|
||||
onRefresh={() => void query.refetch()}
|
||||
<PullToRefresh
|
||||
onRefresh={() => query.refetch()}
|
||||
tintColor={colors.primary}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { RefreshControl, StyleSheet, Text, View } from "react-native";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import { AppBackground } from "@/components/AppBackground";
|
||||
import { LoadingScreen } from "@/components/LoadingScreen";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { PullToRefresh } from "@/components/PullToRefresh";
|
||||
import { StatCard } from "@/components/StatCard";
|
||||
import { TabPage } from "@/components/TabPage";
|
||||
import { TabScrollView } from "@/components/TabScrollView";
|
||||
@@ -48,13 +49,14 @@ export default function ReportsScreen() {
|
||||
<TabScrollView
|
||||
header={<PageHeader title="Reports" subtitle="Business performance snapshot" />}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={statsQuery.isRefetching}
|
||||
onRefresh={() => {
|
||||
void statsQuery.refetch();
|
||||
void expensesQuery.refetch();
|
||||
void summaryQuery.refetch();
|
||||
}}
|
||||
<PullToRefresh
|
||||
onRefresh={() =>
|
||||
Promise.all([
|
||||
statsQuery.refetch(),
|
||||
expensesQuery.refetch(),
|
||||
summaryQuery.refetch(),
|
||||
])
|
||||
}
|
||||
tintColor={colors.primary}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { RefreshControl, StyleSheet, Text, View } from "react-native";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import { AppBackground } from "@/components/AppBackground";
|
||||
import { LoadingScreen } from "@/components/LoadingScreen";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { PullToRefresh } from "@/components/PullToRefresh";
|
||||
import { SwipeableRow } from "@/components/SwipeableRow";
|
||||
import { TabPage } from "@/components/TabPage";
|
||||
import { TabScrollView } from "@/components/TabScrollView";
|
||||
@@ -74,9 +75,8 @@ export default function TimeEntriesScreen() {
|
||||
<PageHeader title="Time entries" subtitle={`${completed.length} completed entries`} />
|
||||
}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={entriesQuery.isRefetching}
|
||||
onRefresh={() => void entriesQuery.refetch()}
|
||||
<PullToRefresh
|
||||
onRefresh={() => entriesQuery.refetch()}
|
||||
tintColor={colors.primary}
|
||||
/>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user