Compare commits
2
Commits
530d0dc34d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa3b9bf6a0 | ||
|
|
ae7350dae6 |
@@ -10,7 +10,7 @@
|
|||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"bundleIdentifier": "com.beenvoice.app",
|
"bundleIdentifier": "com.beenvoice.app",
|
||||||
"buildNumber": "22",
|
"buildNumber": "23",
|
||||||
"icon": "./assets/beenvoice.icon",
|
"icon": "./assets/beenvoice.icon",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"ITSAppUsesNonExemptEncryption": false,
|
"ITSAppUsesNonExemptEncryption": false,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { router } from "expo-router";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Pressable,
|
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -155,23 +154,22 @@ export default function EntitiesScreen() {
|
|||||||
onPress: () => confirmDelete(client.id, client.name),
|
onPress: () => confirmDelete(client.id, client.name),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
onPress={() => router.push(`/(app)/entities/clients/${client.id}`)}
|
||||||
>
|
>
|
||||||
<Pressable onPress={() => router.push(`/(app)/entities/clients/${client.id}`)}>
|
<GlassSurface style={styles.card}>
|
||||||
<GlassSurface style={styles.card}>
|
<View style={styles.cardInner}>
|
||||||
<View style={styles.cardInner}>
|
<Text style={styles.name}>{client.name}</Text>
|
||||||
<Text style={styles.name}>{client.name}</Text>
|
{client.email ? (
|
||||||
{client.email ? (
|
<Text style={styles.meta}>{client.email}</Text>
|
||||||
<Text style={styles.meta}>{client.email}</Text>
|
) : null}
|
||||||
) : null}
|
{client.defaultHourlyRate != null ? (
|
||||||
{client.defaultHourlyRate != null ? (
|
<Text style={styles.meta}>
|
||||||
<Text style={styles.meta}>
|
{formatCurrency(client.defaultHourlyRate, client.currency ?? "USD")}
|
||||||
{formatCurrency(client.defaultHourlyRate, client.currency ?? "USD")}
|
/hr
|
||||||
/hr
|
</Text>
|
||||||
</Text>
|
) : null}
|
||||||
) : null}
|
</View>
|
||||||
</View>
|
</GlassSurface>
|
||||||
</GlassSurface>
|
|
||||||
</Pressable>
|
|
||||||
</SwipeableRow>
|
</SwipeableRow>
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
@@ -205,23 +203,22 @@ export default function EntitiesScreen() {
|
|||||||
onPress: () => confirmDelete(business.id, business.name),
|
onPress: () => confirmDelete(business.id, business.name),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
onPress={() => router.push(`/(app)/entities/businesses/${business.id}`)}
|
||||||
>
|
>
|
||||||
<Pressable onPress={() => router.push(`/(app)/entities/businesses/${business.id}`)}>
|
<GlassSurface style={styles.card}>
|
||||||
<GlassSurface style={styles.card}>
|
<View style={styles.cardInner}>
|
||||||
<View style={styles.cardInner}>
|
<View style={styles.nameRow}>
|
||||||
<View style={styles.nameRow}>
|
<Text style={styles.name}>{business.name}</Text>
|
||||||
<Text style={styles.name}>{business.name}</Text>
|
{business.isDefault ? (
|
||||||
{business.isDefault ? (
|
<Text style={styles.badge}>Default</Text>
|
||||||
<Text style={styles.badge}>Default</Text>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
{business.nickname ? (
|
|
||||||
<Text style={styles.meta}>{business.nickname}</Text>
|
|
||||||
) : null}
|
) : null}
|
||||||
{business.email ? <Text style={styles.meta}>{business.email}</Text> : null}
|
|
||||||
</View>
|
</View>
|
||||||
</GlassSurface>
|
{business.nickname ? (
|
||||||
</Pressable>
|
<Text style={styles.meta}>{business.nickname}</Text>
|
||||||
|
) : null}
|
||||||
|
{business.email ? <Text style={styles.meta}>{business.email}</Text> : null}
|
||||||
|
</View>
|
||||||
|
</GlassSurface>
|
||||||
</SwipeableRow>
|
</SwipeableRow>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { router } from "expo-router";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Pressable,
|
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -205,31 +204,32 @@ export default function InvoicesScreen() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SwipeableRow key={invoice.id} actions={actions} backgroundColor={colors.cardGlass}>
|
<SwipeableRow
|
||||||
<Pressable
|
key={invoice.id}
|
||||||
onPress={() => router.push(`/(app)/invoices/${invoice.id}`)}
|
actions={actions}
|
||||||
onLongPress={() => promptStatusChange(invoice.id, status)}
|
backgroundColor={colors.cardGlass}
|
||||||
>
|
onPress={() => router.push(`/(app)/invoices/${invoice.id}`)}
|
||||||
<GlassSurface style={styles.card}>
|
onLongPress={() => promptStatusChange(invoice.id, status)}
|
||||||
<View style={styles.cardInner}>
|
>
|
||||||
<View style={styles.cardTop}>
|
<GlassSurface style={styles.card}>
|
||||||
<View style={styles.cardMeta}>
|
<View style={styles.cardInner}>
|
||||||
<Text style={styles.invoiceNumber}>{label}</Text>
|
<View style={styles.cardTop}>
|
||||||
<Text style={styles.clientName}>
|
<View style={styles.cardMeta}>
|
||||||
{invoice.client?.name ?? "Client"}
|
<Text style={styles.invoiceNumber}>{label}</Text>
|
||||||
</Text>
|
<Text style={styles.clientName}>
|
||||||
</View>
|
{invoice.client?.name ?? "Client"}
|
||||||
<Text style={styles.amount}>
|
|
||||||
{formatCurrency(invoice.totalAmount, invoice.currency)}
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.cardBottom}>
|
<Text style={styles.amount}>
|
||||||
<Text style={styles.date}>Due {formatDate(invoice.dueDate)}</Text>
|
{formatCurrency(invoice.totalAmount, invoice.currency)}
|
||||||
<StatusBadge status={status} />
|
</Text>
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</GlassSurface>
|
<View style={styles.cardBottom}>
|
||||||
</Pressable>
|
<Text style={styles.date}>Due {formatDate(invoice.dueDate)}</Text>
|
||||||
|
<StatusBadge status={status} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</GlassSurface>
|
||||||
</SwipeableRow>
|
</SwipeableRow>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Pressable,
|
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -225,6 +224,12 @@ function ExpenseRow({ expense, onDelete }: { expense: Expense; onDelete: () => v
|
|||||||
return (
|
return (
|
||||||
<SwipeableRow
|
<SwipeableRow
|
||||||
backgroundColor={colors.cardGlass}
|
backgroundColor={colors.cardGlass}
|
||||||
|
contentStyle={({ pressed }) => [
|
||||||
|
styles.row,
|
||||||
|
{ borderColor: colors.border },
|
||||||
|
pressed && styles.rowPressed,
|
||||||
|
]}
|
||||||
|
onPress={() => router.push(`/(app)/more/expenses/${expense.id}` as never)}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
key: "open",
|
key: "open",
|
||||||
@@ -244,67 +249,57 @@ function ExpenseRow({ expense, onDelete }: { expense: Expense; onDelete: () => v
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Pressable
|
<View style={[styles.categoryIcon, { backgroundColor: colors.muted }]}>
|
||||||
accessibilityRole="button"
|
<Ionicons name={expenseIcon(expense.category)} size={18} color={colors.primary} />
|
||||||
onPress={() => router.push(`/(app)/more/expenses/${expense.id}` as never)}
|
</View>
|
||||||
style={({ pressed }) => [
|
<View style={styles.meta}>
|
||||||
styles.row,
|
<View style={styles.titleRow}>
|
||||||
{ borderColor: colors.border },
|
<Text style={[styles.title, { color: colors.foreground }]} numberOfLines={1}>
|
||||||
pressed && styles.rowPressed,
|
{expense.description}
|
||||||
]}
|
</Text>
|
||||||
>
|
{expense.receiptCount ? (
|
||||||
<View style={[styles.categoryIcon, { backgroundColor: colors.muted }]}>
|
<View
|
||||||
<Ionicons name={expenseIcon(expense.category)} size={18} color={colors.primary} />
|
style={[
|
||||||
|
styles.receiptPill,
|
||||||
|
{ borderColor: colors.border, backgroundColor: colors.background },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Ionicons name="document-attach-outline" size={13} color={colors.primary} />
|
||||||
|
<Text style={[styles.receiptPillText, { color: colors.primary }]}>
|
||||||
|
{expense.receiptCount}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.meta}>
|
<Text style={[styles.sub, { color: colors.mutedForeground }]} numberOfLines={1}>
|
||||||
<View style={styles.titleRow}>
|
{formatDate(expense.date)}
|
||||||
<Text style={[styles.title, { color: colors.foreground }]} numberOfLines={1}>
|
{expense.category ? ` · ${expense.category}` : ""}
|
||||||
{expense.description}
|
{expense.client?.name ? ` · ${expense.client.name}` : ""}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.tagRow}>
|
||||||
|
{expense.billable ? (
|
||||||
|
<Text style={[styles.tag, { color: colors.primary, borderColor: colors.border }]}>
|
||||||
|
Billable
|
||||||
</Text>
|
</Text>
|
||||||
{expense.receiptCount ? (
|
) : null}
|
||||||
<View
|
{expense.reimbursable ? (
|
||||||
style={[
|
<Text style={[styles.tag, { color: colors.foreground, borderColor: colors.border }]}>
|
||||||
styles.receiptPill,
|
Reimbursable
|
||||||
{ borderColor: colors.border, backgroundColor: colors.background },
|
</Text>
|
||||||
]}
|
) : null}
|
||||||
>
|
{expense.taxDeductible ? (
|
||||||
<Ionicons name="document-attach-outline" size={13} color={colors.primary} />
|
<Text style={[styles.tag, { color: colors.success, borderColor: colors.border }]}>
|
||||||
<Text style={[styles.receiptPillText, { color: colors.primary }]}>
|
Tax
|
||||||
{expense.receiptCount}
|
</Text>
|
||||||
</Text>
|
) : null}
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
<Text style={[styles.sub, { color: colors.mutedForeground }]} numberOfLines={1}>
|
|
||||||
{formatDate(expense.date)}
|
|
||||||
{expense.category ? ` · ${expense.category}` : ""}
|
|
||||||
{expense.client?.name ? ` · ${expense.client.name}` : ""}
|
|
||||||
</Text>
|
|
||||||
<View style={styles.tagRow}>
|
|
||||||
{expense.billable ? (
|
|
||||||
<Text style={[styles.tag, { color: colors.primary, borderColor: colors.border }]}>
|
|
||||||
Billable
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
{expense.reimbursable ? (
|
|
||||||
<Text style={[styles.tag, { color: colors.foreground, borderColor: colors.border }]}>
|
|
||||||
Reimbursable
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
{expense.taxDeductible ? (
|
|
||||||
<Text style={[styles.tag, { color: colors.success, borderColor: colors.border }]}>
|
|
||||||
Tax
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.amountStack}>
|
</View>
|
||||||
<Text style={[styles.amount, { color: colors.foreground }]}>
|
<View style={styles.amountStack}>
|
||||||
{formatCurrency(expense.amount, expense.currency)}
|
<Text style={[styles.amount, { color: colors.foreground }]}>
|
||||||
</Text>
|
{formatCurrency(expense.amount, expense.currency)}
|
||||||
<Ionicons name="chevron-forward" size={16} color={colors.mutedForeground} />
|
</Text>
|
||||||
</View>
|
<Ionicons name="chevron-forward" size={16} color={colors.mutedForeground} />
|
||||||
</Pressable>
|
</View>
|
||||||
</SwipeableRow>
|
</SwipeableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { ReactNode, useRef } from "react";
|
import { ReactNode, useRef } from "react";
|
||||||
import { Pressable, StyleSheet, Text, View } from "react-native";
|
import { Pressable, type PressableProps, StyleSheet, Text, View } from "react-native";
|
||||||
import Swipeable, {
|
import Swipeable, {
|
||||||
type SwipeableMethods,
|
type SwipeableMethods,
|
||||||
} from "react-native-gesture-handler/ReanimatedSwipeable";
|
} from "react-native-gesture-handler/ReanimatedSwipeable";
|
||||||
@@ -24,6 +24,9 @@ type SwipeableRowProps = {
|
|||||||
actions: SwipeAction[];
|
actions: SwipeAction[];
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
onPress?: () => void;
|
||||||
|
onLongPress?: () => void;
|
||||||
|
contentStyle?: PressableProps["style"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SwipeableRow({
|
export function SwipeableRow({
|
||||||
@@ -31,11 +34,63 @@ export function SwipeableRow({
|
|||||||
actions,
|
actions,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
|
onPress,
|
||||||
|
onLongPress,
|
||||||
|
contentStyle,
|
||||||
}: SwipeableRowProps) {
|
}: SwipeableRowProps) {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = useThemedStyles(createSwipeableRowStyles);
|
const styles = useThemedStyles(createSwipeableRowStyles);
|
||||||
const rowBackground = backgroundColor ?? colors.background;
|
const rowBackground = backgroundColor ?? colors.background;
|
||||||
const swipeRef = useRef<SwipeableMethods>(null);
|
const swipeRef = useRef<SwipeableMethods>(null);
|
||||||
|
const rowOpenRef = useRef(false);
|
||||||
|
const suppressPressUntilRef = useRef(0);
|
||||||
|
|
||||||
|
function suppressContentPress() {
|
||||||
|
suppressPressUntilRef.current = Date.now() + 350;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContentPress() {
|
||||||
|
if (!onPress) return;
|
||||||
|
|
||||||
|
if (rowOpenRef.current || Date.now() < suppressPressUntilRef.current) {
|
||||||
|
swipeRef.current?.close();
|
||||||
|
rowOpenRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPress();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContent() {
|
||||||
|
if (!onPress && !onLongPress) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.row,
|
||||||
|
{ backgroundColor: rowBackground },
|
||||||
|
typeof contentStyle === "function" ? undefined : contentStyle,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
onPress={handleContentPress}
|
||||||
|
onLongPress={onLongPress}
|
||||||
|
style={(state) => [
|
||||||
|
styles.row,
|
||||||
|
{ backgroundColor: rowBackground },
|
||||||
|
typeof contentStyle === "function" ? contentStyle(state) : contentStyle,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function renderRightActions() {
|
function renderRightActions() {
|
||||||
return (
|
return (
|
||||||
@@ -45,7 +100,9 @@ export function SwipeableRow({
|
|||||||
key={action.key}
|
key={action.key}
|
||||||
style={[styles.actionButton, { backgroundColor: action.backgroundColor }]}
|
style={[styles.actionButton, { backgroundColor: action.backgroundColor }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
suppressContentPress();
|
||||||
swipeRef.current?.close();
|
swipeRef.current?.close();
|
||||||
|
rowOpenRef.current = false;
|
||||||
action.onPress();
|
action.onPress();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -58,12 +115,26 @@ export function SwipeableRow({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!enabled || actions.length === 0) {
|
if (!enabled || actions.length === 0) {
|
||||||
return <View style={[styles.row, { backgroundColor: rowBackground }]}>{children}</View>;
|
return renderContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Swipeable ref={swipeRef} renderRightActions={renderRightActions} overshootRight={false}>
|
<Swipeable
|
||||||
<View style={[styles.row, { backgroundColor: rowBackground }]}>{children}</View>
|
ref={swipeRef}
|
||||||
|
renderRightActions={renderRightActions}
|
||||||
|
overshootRight={false}
|
||||||
|
onSwipeableOpenStartDrag={suppressContentPress}
|
||||||
|
onSwipeableCloseStartDrag={suppressContentPress}
|
||||||
|
onSwipeableWillOpen={() => {
|
||||||
|
suppressContentPress();
|
||||||
|
rowOpenRef.current = true;
|
||||||
|
}}
|
||||||
|
onSwipeableWillClose={suppressContentPress}
|
||||||
|
onSwipeableClose={() => {
|
||||||
|
rowOpenRef.current = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderContent()}
|
||||||
</Swipeable>
|
</Swipeable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user