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:
@@ -25,28 +25,10 @@ type LineItemEditorProps = {
|
||||
isLast?: boolean;
|
||||
};
|
||||
|
||||
export function LineItemsTableHeader() {
|
||||
function FieldLabel({ children }: { children: string }) {
|
||||
const { colors } = useAppTheme();
|
||||
|
||||
return (
|
||||
<View style={[headerStyles.row, { borderBottomColor: colors.border }]}>
|
||||
<Text style={[headerStyles.cell, headerStyles.desc, { color: colors.mutedForeground }]}>
|
||||
Description
|
||||
</Text>
|
||||
<Text style={[headerStyles.cell, headerStyles.date, { color: colors.mutedForeground }]}>
|
||||
Date
|
||||
</Text>
|
||||
<Text style={[headerStyles.cell, headerStyles.hours, { color: colors.mutedForeground }]}>
|
||||
Hrs
|
||||
</Text>
|
||||
<Text style={[headerStyles.cell, headerStyles.rate, { color: colors.mutedForeground }]}>
|
||||
Rate
|
||||
</Text>
|
||||
<Text style={[headerStyles.cell, headerStyles.amt, { color: colors.mutedForeground }]}>
|
||||
Amt
|
||||
</Text>
|
||||
<View style={headerStyles.spacer} />
|
||||
</View>
|
||||
<Text style={[styles.fieldLabel, { color: colors.mutedForeground }]}>{children}</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,20 +50,20 @@ export function LineItemEditor({
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.row,
|
||||
!isLast && { borderBottomColor: colors.border, borderBottomWidth: 1 },
|
||||
styles.readBlock,
|
||||
!isLast && { borderBottomColor: colors.border, borderBottomWidth: StyleSheet.hairlineWidth },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.index, { color: colors.mutedForeground }]}>{index + 1}</Text>
|
||||
<View style={styles.descCol}>
|
||||
<Text style={[styles.readTitle, { color: colors.foreground }]} numberOfLines={2}>
|
||||
{item.description.trim() || "Untitled line"}
|
||||
</Text>
|
||||
<Text style={[styles.readSub, { color: colors.mutedForeground }]}>
|
||||
{formatShortDate(item.date)} · {hours}h × {formatCurrency(rate, currency)}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.amount, { color: colors.foreground }]}>
|
||||
<Text style={[styles.readIndex, { color: colors.mutedForeground }]}>
|
||||
Line {index + 1}
|
||||
</Text>
|
||||
<Text style={[styles.readTitle, { color: colors.foreground }]} numberOfLines={3}>
|
||||
{item.description.trim() || "Untitled line"}
|
||||
</Text>
|
||||
<Text style={[styles.readSub, { color: colors.mutedForeground }]}>
|
||||
{formatShortDate(item.date)} · {hours}h × {formatCurrency(rate, currency)}
|
||||
</Text>
|
||||
<Text style={[styles.readAmount, { color: colors.foreground }]}>
|
||||
{formatCurrency(amount, currency)}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -92,154 +74,159 @@ export function LineItemEditor({
|
||||
<View
|
||||
style={[
|
||||
styles.editBlock,
|
||||
!isLast && { borderBottomColor: colors.border, borderBottomWidth: 1 },
|
||||
!isLast && { borderBottomColor: colors.border, borderBottomWidth: StyleSheet.hairlineWidth },
|
||||
]}
|
||||
>
|
||||
<View style={styles.editTop}>
|
||||
<Text style={[styles.index, { color: colors.mutedForeground }]}>{index + 1}</Text>
|
||||
<TextInput
|
||||
value={item.description}
|
||||
onChangeText={(description) => onChange({ description })}
|
||||
placeholder="What was done?"
|
||||
placeholderTextColor={colors.mutedForeground}
|
||||
style={[
|
||||
styles.descriptionInput,
|
||||
{
|
||||
color: colors.foreground,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.cardGlass,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[styles.lineLabel, { color: colors.mutedForeground }]}>Line {index + 1}</Text>
|
||||
|
||||
<View style={styles.metricsRow}>
|
||||
<CompactDateField
|
||||
value={item.date}
|
||||
onChange={(date) => onChange({ date })}
|
||||
style={styles.dateField}
|
||||
/>
|
||||
<CompactStepperInput
|
||||
value={item.hours}
|
||||
onChangeText={(hours) => onChange({ hours })}
|
||||
step={0.25}
|
||||
style={styles.hoursField}
|
||||
/>
|
||||
<View style={[styles.rateField, { borderColor: colors.border, backgroundColor: colors.cardGlass }]}>
|
||||
<Text style={[styles.ratePrefix, { color: colors.mutedForeground }]}>$</Text>
|
||||
<TextInput
|
||||
value={item.rate}
|
||||
onChangeText={(rate) => onChange({ rate })}
|
||||
keyboardType="decimal-pad"
|
||||
placeholder="0"
|
||||
placeholderTextColor={colors.mutedForeground}
|
||||
style={[styles.rateInput, { color: colors.foreground }]}
|
||||
<TextInput
|
||||
value={item.description}
|
||||
onChangeText={(description) => onChange({ description })}
|
||||
placeholder="What was done?"
|
||||
placeholderTextColor={colors.mutedForeground}
|
||||
style={[
|
||||
styles.descriptionInput,
|
||||
{
|
||||
color: colors.foreground,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.cardGlass,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<View style={styles.fieldsRow}>
|
||||
<View style={styles.fieldCol}>
|
||||
<FieldLabel>Date</FieldLabel>
|
||||
<CompactDateField
|
||||
value={item.date}
|
||||
onChange={(date) => onChange({ date })}
|
||||
style={styles.fieldControl}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[styles.amount, styles.amountEdit, { color: colors.foreground }]}>
|
||||
{formatCurrency(amount, currency)}
|
||||
</Text>
|
||||
<View style={styles.fieldCol}>
|
||||
<FieldLabel>Hours</FieldLabel>
|
||||
<CompactStepperInput
|
||||
value={item.hours}
|
||||
onChangeText={(hours) => onChange({ hours })}
|
||||
step={0.25}
|
||||
style={styles.fieldControl}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.fieldCol}>
|
||||
<FieldLabel>Rate</FieldLabel>
|
||||
<View
|
||||
style={[
|
||||
styles.rateField,
|
||||
{ borderColor: colors.border, backgroundColor: colors.cardGlass },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.ratePrefix, { color: colors.mutedForeground }]}>$</Text>
|
||||
<TextInput
|
||||
value={item.rate}
|
||||
onChangeText={(rate) => onChange({ rate })}
|
||||
keyboardType="decimal-pad"
|
||||
placeholder="0"
|
||||
placeholderTextColor={colors.mutedForeground}
|
||||
style={[styles.rateInput, { color: colors.foreground }]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.footerRow}>
|
||||
<View style={styles.amountGroup}>
|
||||
<Text style={[styles.amountLabel, { color: colors.mutedForeground }]}>Amount</Text>
|
||||
<Text style={[styles.amountValue, { color: colors.foreground }]}>
|
||||
{formatCurrency(amount, currency)}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Remove line item"
|
||||
onPress={onRemove}
|
||||
hitSlop={8}
|
||||
style={({ pressed }) => [styles.remove, pressed && styles.removePressed]}
|
||||
style={({ pressed }) => [
|
||||
styles.remove,
|
||||
{ borderColor: colors.border, backgroundColor: colors.cardGlass },
|
||||
pressed && styles.removePressed,
|
||||
]}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={17} color={colors.destructive} />
|
||||
<Ionicons name="trash-outline" size={18} color={colors.destructive} />
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const headerStyles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
const styles = StyleSheet.create({
|
||||
readBlock: {
|
||||
paddingVertical: spacing.md,
|
||||
gap: spacing.xs,
|
||||
paddingBottom: spacing.xs,
|
||||
marginBottom: spacing.xs,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
cell: {
|
||||
readIndex: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
fontSize: 11,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.4,
|
||||
},
|
||||
desc: { flex: 1, paddingLeft: 22 },
|
||||
date: { width: 72 },
|
||||
hours: { width: 88, textAlign: "center" },
|
||||
rate: { width: 72, textAlign: "center" },
|
||||
amt: { width: 64, textAlign: "right" },
|
||||
spacer: { width: 32 },
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.xs,
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
editBlock: {
|
||||
paddingVertical: spacing.sm,
|
||||
gap: spacing.xs,
|
||||
},
|
||||
editTop: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.xs,
|
||||
},
|
||||
index: {
|
||||
width: 18,
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
fontSize: 12,
|
||||
textAlign: "center",
|
||||
},
|
||||
descCol: {
|
||||
flex: 1,
|
||||
gap: 2,
|
||||
},
|
||||
readTitle: {
|
||||
fontFamily: fonts.bodyMedium,
|
||||
fontSize: 14,
|
||||
lineHeight: 18,
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
},
|
||||
readSub: {
|
||||
fontFamily: fonts.body,
|
||||
fontSize: 13,
|
||||
},
|
||||
readAmount: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
fontSize: 15,
|
||||
marginTop: 2,
|
||||
},
|
||||
editBlock: {
|
||||
paddingVertical: spacing.md,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
lineLabel: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
fontSize: 11,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.4,
|
||||
},
|
||||
descriptionInput: {
|
||||
flex: 1,
|
||||
minHeight: 36,
|
||||
width: "100%",
|
||||
minHeight: 40,
|
||||
borderWidth: 1,
|
||||
borderRadius: radii.md,
|
||||
paddingHorizontal: spacing.sm,
|
||||
fontFamily: fonts.body,
|
||||
fontSize: 14,
|
||||
paddingVertical: 6,
|
||||
fontSize: 15,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
metricsRow: {
|
||||
fieldsRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: spacing.xs,
|
||||
paddingLeft: 22,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
dateField: {
|
||||
width: 72,
|
||||
fieldCol: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
minWidth: 0,
|
||||
},
|
||||
hoursField: {
|
||||
width: 88,
|
||||
fieldLabel: {
|
||||
fontFamily: fonts.bodyMedium,
|
||||
fontSize: 11,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
fieldControl: {
|
||||
width: "100%",
|
||||
},
|
||||
rateField: {
|
||||
width: 72,
|
||||
minHeight: 36,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderRadius: radii.md,
|
||||
minHeight: 36,
|
||||
paddingHorizontal: spacing.xs,
|
||||
},
|
||||
ratePrefix: {
|
||||
@@ -248,23 +235,40 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
rateInput: {
|
||||
flex: 1,
|
||||
fontFamily: fonts.body,
|
||||
fontFamily: fonts.bodyMedium,
|
||||
fontSize: 13,
|
||||
paddingVertical: 4,
|
||||
textAlign: "right",
|
||||
minWidth: 0,
|
||||
},
|
||||
amount: {
|
||||
width: 64,
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
fontSize: 13,
|
||||
textAlign: "right",
|
||||
footerRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: spacing.sm,
|
||||
marginTop: 2,
|
||||
},
|
||||
amountEdit: {
|
||||
amountGroup: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "baseline",
|
||||
gap: spacing.sm,
|
||||
},
|
||||
amountLabel: {
|
||||
fontFamily: fonts.bodyMedium,
|
||||
fontSize: 12,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
amountValue: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
fontSize: 17,
|
||||
},
|
||||
remove: {
|
||||
width: 32,
|
||||
height: 36,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: radii.md,
|
||||
borderWidth: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user