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>
This commit is contained in:
@@ -23,6 +23,7 @@ import { useAppTheme } from "@/contexts/ThemeContext";
|
||||
import { formatCurrency } from "@/lib/format";
|
||||
import { getInvoiceStatus } from "@/lib/invoice-status";
|
||||
import { validateLineItems } from "@/lib/form-validation";
|
||||
import { ensureNotificationPermissions } from "@/lib/invoice-send-reminders";
|
||||
import { useTabBarScrollPadding } from "@/lib/tab-bar-insets";
|
||||
import type { ThemeColors } from "@/lib/theme-palette";
|
||||
import { useThemedStyles } from "@/lib/use-themed-styles";
|
||||
@@ -42,6 +43,7 @@ export default function InvoiceEditScreen() {
|
||||
|
||||
const [notes, setNotes] = useState("");
|
||||
const [dueDate, setDueDate] = useState(() => new Date());
|
||||
const [sendReminderAt, setSendReminderAt] = useState<Date | null>(null);
|
||||
const [items, setItems] = useState<EditableLineItem[]>([]);
|
||||
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -51,6 +53,7 @@ export default function InvoiceEditScreen() {
|
||||
if (!invoice) return;
|
||||
setNotes(invoice.notes ?? "");
|
||||
setDueDate(new Date(invoice.dueDate));
|
||||
setSendReminderAt(invoice.sendReminderAt ? new Date(invoice.sendReminderAt) : null);
|
||||
setItems(
|
||||
invoice.items.map((item) => ({
|
||||
id: item.id,
|
||||
@@ -67,6 +70,7 @@ export default function InvoiceEditScreen() {
|
||||
onSuccess: () => {
|
||||
void utils.invoices.getById.invalidate({ id: id ?? "" });
|
||||
void utils.invoices.getAll.invalidate();
|
||||
void utils.invoices.getAll.invalidate({ status: "draft" });
|
||||
void utils.dashboard.getStats.invalidate();
|
||||
Alert.alert("Saved", "Invoice updated", [
|
||||
{ text: "OK", onPress: () => router.back() },
|
||||
@@ -86,6 +90,7 @@ export default function InvoiceEditScreen() {
|
||||
});
|
||||
|
||||
const invoice = invoiceQuery.data;
|
||||
const isDraft = invoice?.status === "draft";
|
||||
|
||||
const subtotal = useMemo(
|
||||
() =>
|
||||
@@ -101,8 +106,8 @@ export default function InvoiceEditScreen() {
|
||||
const taxAmount = subtotal * (taxRate / 100);
|
||||
const total = subtotal + taxAmount;
|
||||
const currency = invoice?.currency ?? "USD";
|
||||
const lineItemsError = validateLineItems(items);
|
||||
const canSave = !lineItemsError;
|
||||
const lineItemsError = isDraft ? validateLineItems(items) : null;
|
||||
const canSave = isDraft ? !lineItemsError : true;
|
||||
|
||||
if (!id) {
|
||||
return <LoadingScreen message="Invalid invoice" />;
|
||||
@@ -172,10 +177,20 @@ export default function InvoiceEditScreen() {
|
||||
});
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
async function handleSave() {
|
||||
if (!canSave) return;
|
||||
setError(null);
|
||||
|
||||
if (isDraft && sendReminderAt) {
|
||||
const granted = await ensureNotificationPermissions();
|
||||
if (!granted) {
|
||||
Alert.alert(
|
||||
"Notifications disabled",
|
||||
"Turn on notifications in Settings to get reminded when it's time to send this invoice.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const parsedItems: Array<{
|
||||
date: Date;
|
||||
description: string;
|
||||
@@ -196,7 +211,12 @@ export default function InvoiceEditScreen() {
|
||||
id,
|
||||
notes,
|
||||
dueDate,
|
||||
items: parsedItems,
|
||||
sendReminderAt,
|
||||
...(isDraft
|
||||
? {
|
||||
items: parsedItems,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -223,6 +243,25 @@ export default function InvoiceEditScreen() {
|
||||
|
||||
<Card>
|
||||
<DateTimeField label="Due date" mode="date" value={dueDate} onChange={setDueDate} />
|
||||
{isDraft ? (
|
||||
<>
|
||||
<DateTimeField
|
||||
label="Remind me to send"
|
||||
mode="date"
|
||||
value={sendReminderAt ?? dueDate}
|
||||
minimumDate={new Date()}
|
||||
maximumDate={new Date(2100, 0, 1)}
|
||||
onChange={setSendReminderAt}
|
||||
/>
|
||||
{sendReminderAt ? (
|
||||
<Pressable onPress={() => setSendReminderAt(null)}>
|
||||
<Text style={[styles.clearReminder, { color: colors.primary }]}>
|
||||
Clear send reminder
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
<Input
|
||||
label="Notes"
|
||||
value={notes}
|
||||
@@ -234,6 +273,12 @@ export default function InvoiceEditScreen() {
|
||||
</Card>
|
||||
|
||||
<Card title="Line items">
|
||||
{!isDraft ? (
|
||||
<Text style={styles.lockedHint}>
|
||||
Line items are locked after an invoice is sent. Mark as draft on the invoice
|
||||
screen to edit entries.
|
||||
</Text>
|
||||
) : null}
|
||||
{items.map((item, index) => (
|
||||
<LineItemEditor
|
||||
key={item.id ?? `new-${index}`}
|
||||
@@ -245,12 +290,15 @@ export default function InvoiceEditScreen() {
|
||||
}
|
||||
onChange={(patch) => updateItem(index, patch)}
|
||||
onRemove={() => removeItem(index)}
|
||||
readOnly={!isDraft}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Pressable accessibilityRole="button" onPress={addItem} style={styles.addLine}>
|
||||
<Text style={styles.addLineText}>+ Add line</Text>
|
||||
</Pressable>
|
||||
{isDraft ? (
|
||||
<Pressable accessibilityRole="button" onPress={addItem} style={styles.addLine}>
|
||||
<Text style={styles.addLineText}>+ Add line</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
|
||||
<View style={styles.totals}>
|
||||
<TotalRow label="Subtotal" value={formatCurrency(subtotal, currency)} />
|
||||
@@ -369,6 +417,17 @@ const createInvoiceEditStyles = (colors: ThemeColors, _isDark: boolean) =>
|
||||
minHeight: 72,
|
||||
textAlignVertical: "top",
|
||||
},
|
||||
lockedHint: {
|
||||
fontFamily: fonts.body,
|
||||
fontSize: 13,
|
||||
color: colors.mutedForeground,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
clearReminder: {
|
||||
fontFamily: fonts.bodyMedium,
|
||||
fontSize: 13,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
addLine: {
|
||||
paddingTop: spacing.sm,
|
||||
paddingBottom: spacing.xs,
|
||||
|
||||
Reference in New Issue
Block a user