Polish mobile and web experience

This commit is contained in:
2026-08-17 00:18:55 -04:00
parent 6c74436092
commit 9929d7321d
28 changed files with 835 additions and 688 deletions
+2 -3
View File
@@ -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}
/>
+21 -8
View File
@@ -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"}