Official URL migration preserves sessions, shortcuts prefs, and last clock-in client; auth screens match web with legal links; time clock and invoice editor/send flows are updated for the new domain and UI patterns. Co-authored-by: Cursor <cursoragent@cursor.com>
230 lines
6.1 KiB
TypeScript
230 lines
6.1 KiB
TypeScript
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
|
|
import { DateTimeField } from "@/components/ui/DateTimeField";
|
|
import { Input } from "@/components/ui/Input";
|
|
import { SelectField } from "@/components/ui/SelectField";
|
|
import { fonts, spacing } from "@/constants/theme";
|
|
import { useAppTheme } from "@/contexts/ThemeContext";
|
|
import { defaultDueDate } from "@/lib/invoice-number";
|
|
|
|
type SelectOption = { label: string; value: string };
|
|
|
|
type InvoiceSetupFormProps = {
|
|
businessId: string;
|
|
onBusinessIdChange: (value: string) => void;
|
|
businessOptions: SelectOption[];
|
|
businessError?: string;
|
|
businessReadOnly?: boolean;
|
|
clientId: string;
|
|
onClientIdChange: (value: string) => void;
|
|
clientOptions: SelectOption[];
|
|
clientError?: string;
|
|
clientReadOnly?: boolean;
|
|
invoiceNumber: string;
|
|
onInvoiceNumberChange?: (value: string) => void;
|
|
invoiceNumberReadOnly?: boolean;
|
|
issueDate: Date;
|
|
onIssueDateChange?: (date: Date) => void;
|
|
issueDateReadOnly?: boolean;
|
|
dueDate: Date;
|
|
onDueDateChange: (date: Date) => void;
|
|
taxRate: string;
|
|
onTaxRateChange?: (value: string) => void;
|
|
taxRateReadOnly?: boolean;
|
|
notes: string;
|
|
onNotesChange: (value: string) => void;
|
|
sendReminderAt?: Date | null;
|
|
onSendReminderAtChange?: (date: Date | null) => void;
|
|
showSendReminder?: boolean;
|
|
};
|
|
|
|
export function InvoiceSetupForm({
|
|
businessId,
|
|
onBusinessIdChange,
|
|
businessOptions,
|
|
businessError,
|
|
businessReadOnly = false,
|
|
clientId,
|
|
onClientIdChange,
|
|
clientOptions,
|
|
clientError,
|
|
clientReadOnly = false,
|
|
invoiceNumber,
|
|
onInvoiceNumberChange,
|
|
invoiceNumberReadOnly = false,
|
|
issueDate,
|
|
onIssueDateChange,
|
|
issueDateReadOnly = false,
|
|
dueDate,
|
|
onDueDateChange,
|
|
taxRate,
|
|
onTaxRateChange,
|
|
taxRateReadOnly = false,
|
|
notes,
|
|
onNotesChange,
|
|
sendReminderAt,
|
|
onSendReminderAtChange,
|
|
showSendReminder = false,
|
|
}: InvoiceSetupFormProps) {
|
|
const { colors } = useAppTheme();
|
|
|
|
return (
|
|
<View style={styles.form}>
|
|
{businessOptions.length === 0 ? (
|
|
<Text style={[styles.hint, { color: colors.mutedForeground }]}>
|
|
Add a business in Entities before invoicing.
|
|
</Text>
|
|
) : (
|
|
<SelectField
|
|
label="Business"
|
|
placeholder="Select business…"
|
|
value={businessId}
|
|
options={businessOptions}
|
|
required
|
|
error={businessError}
|
|
disabled={businessReadOnly}
|
|
onValueChange={onBusinessIdChange}
|
|
/>
|
|
)}
|
|
|
|
{clientOptions.length === 0 ? (
|
|
<Text style={[styles.hint, { color: colors.mutedForeground }]}>
|
|
Add a client in Entities before invoicing.
|
|
</Text>
|
|
) : (
|
|
<SelectField
|
|
label="Client"
|
|
placeholder="Select client…"
|
|
value={clientId}
|
|
options={clientOptions}
|
|
required
|
|
error={clientError}
|
|
disabled={clientReadOnly}
|
|
onValueChange={onClientIdChange}
|
|
/>
|
|
)}
|
|
|
|
{invoiceNumberReadOnly ? (
|
|
<View style={styles.readOnlyField}>
|
|
<Text style={[styles.readOnlyLabel, { color: colors.mutedForeground }]}>
|
|
Invoice number
|
|
</Text>
|
|
<Text style={[styles.readOnlyValue, { color: colors.foreground }]}>
|
|
{invoiceNumber}
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
<Input
|
|
label="Invoice number"
|
|
value={invoiceNumber}
|
|
onChangeText={onInvoiceNumberChange}
|
|
autoCapitalize="characters"
|
|
required
|
|
/>
|
|
)}
|
|
|
|
{issueDateReadOnly ? (
|
|
<View style={styles.readOnlyField}>
|
|
<Text style={[styles.readOnlyLabel, { color: colors.mutedForeground }]}>
|
|
Issue date
|
|
</Text>
|
|
<Text style={[styles.readOnlyValue, { color: colors.foreground }]}>
|
|
{issueDate.toLocaleDateString()}
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
<DateTimeField
|
|
label="Issue date"
|
|
mode="date"
|
|
value={issueDate}
|
|
onChange={(date) => {
|
|
onIssueDateChange?.(date);
|
|
if (dueDate < date) onDueDateChange(defaultDueDate(date));
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
<DateTimeField label="Due date" mode="date" value={dueDate} onChange={onDueDateChange} />
|
|
|
|
{taxRateReadOnly ? (
|
|
<View style={styles.readOnlyField}>
|
|
<Text style={[styles.readOnlyLabel, { color: colors.mutedForeground }]}>
|
|
Tax rate
|
|
</Text>
|
|
<Text style={[styles.readOnlyValue, { color: colors.foreground }]}>
|
|
{taxRate}%
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
<Input
|
|
label="Tax rate (%)"
|
|
value={taxRate}
|
|
onChangeText={onTaxRateChange}
|
|
keyboardType="decimal-pad"
|
|
/>
|
|
)}
|
|
|
|
{showSendReminder && onSendReminderAtChange ? (
|
|
<>
|
|
<DateTimeField
|
|
label="Remind me to send"
|
|
mode="date"
|
|
value={sendReminderAt ?? dueDate}
|
|
minimumDate={new Date()}
|
|
maximumDate={new Date(2100, 0, 1)}
|
|
onChange={onSendReminderAtChange}
|
|
/>
|
|
{sendReminderAt ? (
|
|
<Pressable onPress={() => onSendReminderAtChange(null)}>
|
|
<Text style={[styles.clearReminder, { color: colors.primary }]}>
|
|
Clear send reminder
|
|
</Text>
|
|
</Pressable>
|
|
) : null}
|
|
</>
|
|
) : null}
|
|
|
|
<Input
|
|
label="Notes"
|
|
value={notes}
|
|
onChangeText={onNotesChange}
|
|
placeholder="Optional notes for the client"
|
|
multiline
|
|
style={styles.notesInput}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
form: {
|
|
gap: spacing.sm,
|
|
},
|
|
hint: {
|
|
fontFamily: fonts.body,
|
|
fontSize: 14,
|
|
lineHeight: 20,
|
|
},
|
|
readOnlyField: {
|
|
gap: 4,
|
|
paddingVertical: 4,
|
|
},
|
|
readOnlyLabel: {
|
|
fontFamily: fonts.bodyMedium,
|
|
fontSize: 13,
|
|
},
|
|
readOnlyValue: {
|
|
fontFamily: fonts.body,
|
|
fontSize: 15,
|
|
},
|
|
notesInput: {
|
|
minHeight: 72,
|
|
textAlignVertical: "top",
|
|
},
|
|
clearReminder: {
|
|
fontFamily: fonts.bodyMedium,
|
|
fontSize: 13,
|
|
marginBottom: spacing.xs,
|
|
},
|
|
});
|