Add beenvoice mobile companion app with full dark mode support.
Expo app with dashboard, time clock, invoices, and settings — native tabs, glass UI, theme-aware components, and iOS Live Activities. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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 { 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}>
|
||||
<Input
|
||||
label="Hours"
|
||||
value={item.hours}
|
||||
onChangeText={(hours) => onChange({ hours })}
|
||||
keyboardType="decimal-pad"
|
||||
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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user