Files
beenvoice/components/invoices/LineItemEditor.tsx
T
soconnorandCursor 06bc91ac13 Redesign mobile time clock, add shortcuts, and improve account management.
Add iOS Shortcuts/Siri intents, local send-reminder notifications, stable
client picker with last-client defaults, account refresh/remove, and softer
session handling on unauthorized API responses.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 16:06:17 -04:00

196 lines
5.1 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;
readOnly?: boolean;
};
export function LineItemEditor({
item,
currency,
expanded,
onToggle,
onChange,
onRemove,
readOnly = false,
}: 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 || readOnly) {
return (
<Pressable
accessibilityRole="button"
onPress={readOnly ? undefined : onToggle}
disabled={readOnly}
style={({ pressed }) => [styles.row, borderStyle, pressed && !readOnly && 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>
{!readOnly ? (
<Ionicons name="chevron-down" size={16} color={colors.mutedForeground} />
) : null}
</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,
},
});