Files
beenvoice-app/components/invoices/LineItemEditor.tsx
T
soconnor 6d2711e36e Polish mobile app for App Store review and expand CRUD.
Default to beenvoice.soconnor.dev with server settings hidden behind Advanced; add Entities tab with clients/businesses, invoice creation, UI fixes for dashboard layout, date fields, FAB position, and card-matched button radius.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 23:14:58 -04:00

191 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Ionicons } from "@expo/vector-icons";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { DateTimeField } from "@/components/ui/DateTimeField";
import { Input } from "@/components/ui/Input";
import { StepperInput } from "@/components/ui/StepperInput";
import { fonts, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
import { formatCurrency, formatDate } from "@/lib/format";
export type EditableLineItem = {
id?: string;
date: Date;
description: string;
hours: string;
rate: string;
};
type LineItemEditorProps = {
item: EditableLineItem;
currency: string;
expanded: boolean;
onToggle: () => void;
onChange: (patch: Partial<EditableLineItem>) => void;
onRemove: () => void;
};
export function LineItemEditor({
item,
currency,
expanded,
onToggle,
onChange,
onRemove,
}: LineItemEditorProps) {
const { colors } = useAppTheme();
const hours = Number(item.hours) || 0;
const rate = Number(item.rate) || 0;
const amount = hours * rate;
const borderStyle = { borderTopColor: colors.border };
if (!expanded) {
return (
<Pressable
accessibilityRole="button"
onPress={onToggle}
style={({ pressed }) => [styles.row, borderStyle, pressed && styles.rowPressed]}
>
<View style={styles.rowMain}>
<Text style={[styles.rowTitle, { color: colors.foreground }]} numberOfLines={1}>
{item.description.trim() || "Untitled line"}
</Text>
<Text style={[styles.rowSub, { color: colors.mutedForeground }]}>
{formatDate(item.date)} · {hours}h × {formatCurrency(rate, currency)}
</Text>
</View>
<Text style={[styles.rowAmount, { color: colors.foreground }]}>
{formatCurrency(amount, currency)}
</Text>
<Ionicons name="chevron-down" size={16} color={colors.mutedForeground} />
</Pressable>
);
}
return (
<View style={[styles.expanded, borderStyle]}>
<View style={styles.expandedHeader}>
<Text style={[styles.expandedLabel, { color: colors.mutedForeground }]}>Line item</Text>
<Pressable accessibilityRole="button" onPress={onToggle} hitSlop={8}>
<Ionicons name="chevron-up" size={18} color={colors.mutedForeground} />
</Pressable>
</View>
<Input
label="Description"
value={item.description}
onChangeText={(description) => onChange({ description })}
placeholder="What was done"
/>
<View style={styles.inlineRow}>
<View style={styles.inlineField}>
<StepperInput
label="Hours"
value={item.hours}
onChangeText={(hours) => onChange({ hours })}
placeholder="0"
/>
</View>
<View style={styles.inlineField}>
<Input
label="Rate"
value={item.rate}
onChangeText={(rate) => onChange({ rate })}
keyboardType="decimal-pad"
placeholder="0"
/>
</View>
</View>
<DateTimeField
label="Date"
mode="date"
value={item.date}
onChange={(date) => onChange({ date })}
/>
<View style={styles.expandedFooter}>
<Text style={[styles.lineTotal, { color: colors.foreground }]}>
{formatCurrency(amount, currency)}
</Text>
<Pressable accessibilityRole="button" onPress={onRemove} style={styles.removeButton}>
<Ionicons name="trash-outline" size={16} color={colors.destructive} />
<Text style={[styles.removeLabel, { color: colors.destructive }]}>Remove</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
row: {
flexDirection: "row",
alignItems: "center",
gap: spacing.sm,
paddingVertical: spacing.sm,
borderTopWidth: 1,
},
rowPressed: {
opacity: 0.9,
},
rowMain: {
flex: 1,
gap: 2,
},
rowTitle: {
fontFamily: fonts.bodyMedium,
fontSize: 15,
},
rowSub: {
fontFamily: fonts.body,
fontSize: 12,
},
rowAmount: {
fontFamily: fonts.bodySemiBold,
fontSize: 14,
},
expanded: {
gap: spacing.sm,
paddingVertical: spacing.sm,
borderTopWidth: 1,
},
expandedHeader: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
expandedLabel: {
fontFamily: fonts.bodySemiBold,
fontSize: 13,
textTransform: "uppercase",
letterSpacing: 0.3,
},
inlineRow: {
flexDirection: "row",
gap: spacing.md,
},
inlineField: {
flex: 1,
},
expandedFooter: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
lineTotal: {
fontFamily: fonts.bodySemiBold,
fontSize: 16,
},
removeButton: {
flexDirection: "row",
alignItems: "center",
gap: 4,
paddingVertical: spacing.xs,
},
removeLabel: {
fontFamily: fonts.bodyMedium,
fontSize: 13,
},
});