Files
beenvoice-app/components/expenses/ExpenseFormFields.tsx
T

220 lines
5.6 KiB
TypeScript

import { Switch, StyleSheet, Text, View } from "react-native";
import { DateTimeField } from "@/components/ui/DateTimeField";
import { Input } from "@/components/ui/Input";
import { SelectField, type SelectOption } from "@/components/ui/SelectField";
import { fonts, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
import { EXPENSE_CATEGORIES } from "@/lib/expense-categories";
const NONE = "__none__";
export type ExpenseFormState = {
description: string;
amountText: string;
date: Date;
category: string;
businessId: string;
clientId: string;
billable: boolean;
reimbursable: boolean;
taxDeductible: boolean;
notes: string;
};
type ExpenseFormFieldsProps = {
value: ExpenseFormState;
businesses: Array<{ id: string; name: string; isDefault?: boolean | null }>;
clients: Array<{ id: string; name: string }>;
onChange: (value: ExpenseFormState) => void;
notesLabel?: string;
notesPlaceholder?: string;
};
export function defaultExpenseFormState(
defaultBusinessId = "",
): ExpenseFormState {
return {
description: "",
amountText: "",
date: new Date(),
category: "",
businessId: defaultBusinessId,
clientId: "",
billable: false,
reimbursable: false,
taxDeductible: false,
notes: "",
};
}
export function ExpenseFormFields({
value,
businesses,
clients,
onChange,
notesLabel = "Notes",
notesPlaceholder = "Internal details or receipt OCR text",
}: ExpenseFormFieldsProps) {
const { colors } = useAppTheme();
const businessOptions: SelectOption[] = [
{ label: "Default business", value: NONE },
...businesses.map((business) => ({
label: business.isDefault ? `${business.name} (default)` : business.name,
value: business.id,
})),
];
const clientOptions: SelectOption[] = [
{ label: "No client", value: NONE },
...clients.map((client) => ({ label: client.name, value: client.id })),
];
const categoryOptions: SelectOption[] = [
{ label: "No category", value: NONE },
...EXPENSE_CATEGORIES.map((category) => ({
label: category,
value: category,
})),
];
const setField = <K extends keyof ExpenseFormState>(
field: K,
nextValue: ExpenseFormState[K],
) => onChange({ ...value, [field]: nextValue });
return (
<View style={styles.stack}>
<Input
label="Description"
required
value={value.description}
onChangeText={(text) => setField("description", text)}
placeholder="e.g. Client lunch"
/>
<Input
label="Amount"
required
value={value.amountText}
onChangeText={(text) => setField("amountText", text)}
keyboardType="decimal-pad"
placeholder="0.00"
/>
<DateTimeField
label="Date"
value={value.date}
onChange={(date) => setField("date", date)}
mode="date"
/>
<SelectField
label="Category"
placeholder="No category"
value={value.category || NONE}
options={categoryOptions}
onValueChange={(next) =>
setField("category", next === NONE ? "" : next)
}
/>
<SelectField
label="Business"
placeholder="Default business"
value={value.businessId || NONE}
options={businessOptions}
onValueChange={(next) =>
setField("businessId", next === NONE ? "" : next)
}
/>
<SelectField
label="Client"
placeholder="No client"
value={value.clientId || NONE}
options={clientOptions}
onValueChange={(next) =>
setField("clientId", next === NONE ? "" : next)
}
/>
<View style={styles.flags}>
<FlagSwitch
label="Billable"
value={value.billable}
onValueChange={(next) => setField("billable", next)}
/>
<FlagSwitch
label="Reimbursable"
value={value.reimbursable}
onValueChange={(next) => setField("reimbursable", next)}
/>
<FlagSwitch
label="Tax deductible"
value={value.taxDeductible}
onValueChange={(next) => setField("taxDeductible", next)}
/>
</View>
<Input
label={notesLabel}
value={value.notes}
onChangeText={(text) => setField("notes", text)}
placeholder={notesPlaceholder}
multiline
style={styles.notesInput}
/>
</View>
);
function FlagSwitch({
label,
value: checked,
onValueChange,
}: {
label: string;
value: boolean;
onValueChange: (value: boolean) => void;
}) {
return (
<View style={[styles.flagRow, { borderColor: colors.borderGlass }]}>
<Text style={[styles.flagLabel, { color: colors.foreground }]}>
{label}
</Text>
<Switch
value={checked}
onValueChange={onValueChange}
trackColor={{
true: colors.switchTrackOn,
false: colors.switchTrackOff,
}}
thumbColor={colors.switchThumb}
ios_backgroundColor={colors.switchIosBackground}
/>
</View>
);
}
}
const styles = StyleSheet.create({
stack: {
gap: spacing.md,
},
flags: {
gap: spacing.sm,
},
flagRow: {
minHeight: 48,
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: spacing.md,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
flagLabel: {
fontFamily: fonts.bodyMedium,
fontSize: 14,
},
notesInput: {
minHeight: 96,
textAlignVertical: "top",
paddingTop: spacing.md,
},
});