Move production to beenvoice.app with migrated accounts, refreshed auth and timer UX, and expanded invoice flows.
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>
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user