Polish mobile and web experience
This commit is contained in:
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user