import { router, Stack, useLocalSearchParams } from "expo-router";
import { useMemo, useState } from "react";
import {
Alert,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import { AppBackground } from "@/components/AppBackground";
import { InvoicePdfPreview } from "@/components/invoices/InvoicePdfPreview";
import { LoadingScreen } from "@/components/LoadingScreen";
import { Button } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import { Input } from "@/components/ui/Input";
import { DateTimeField } from "@/components/ui/DateTimeField";
import {
formatZonedDateTime,
getDefaultScheduledSendAt,
DEFAULT_TIME_ZONE,
toLocalDateTimeInputValue,
zonedDateTimeToInstant,
} from "@beenvoice/domain/time-zone";
import { fonts, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
import { formatCurrency, formatDate } from "@/lib/format";
import { getInvoiceStatus } from "@/lib/invoice-status";
import { buildPreviewPdfInputFromInvoice } from "@/lib/invoice-pdf-input";
import { useTabBarScrollPadding } from "@/lib/tab-bar-insets";
import type { ThemeColors } from "@/lib/theme-palette";
import { useThemedStyles } from "@/lib/use-themed-styles";
import { api } from "@/lib/trpc";
export default function InvoiceSendScreen() {
const { colors } = useAppTheme();
const styles = useThemedStyles(createSendStyles);
const { id } = useLocalSearchParams<{ id: string }>();
const utils = api.useUtils();
const scrollPadding = useTabBarScrollPadding();
const [customMessage, setCustomMessage] = useState("");
const [scheduledAt, setScheduledAt] = useState(() =>
getDefaultScheduledSendAt(),
);
const profileQuery = api.settings.getProfile.useQuery();
const timeZone = profileQuery.data?.timeZone ?? DEFAULT_TIME_ZONE;
const invoiceQuery = api.invoices.getById.useQuery(
{ id: id ?? "" },
{ enabled: Boolean(id) },
);
const sendInvoice = api.email.sendInvoice.useMutation({
onSuccess: (data) => {
void utils.invoices.getById.invalidate({ id: id ?? "" });
void utils.invoices.getAll.invalidate();
void utils.dashboard.getStats.invalidate();
Alert.alert("Invoice sent", data.message, [
{ text: "OK", onPress: () => router.replace(`/(app)/invoices/${id}`) },
]);
},
onError: (err) => Alert.alert("Could not send invoice", err.message),
});
const scheduleInvoice = api.email.scheduleInvoice.useMutation({
onSuccess: async (data) => {
await utils.invoices.getById.invalidate({ id: id ?? "" });
await utils.invoices.getAll.invalidate();
Alert.alert(
"Invoice scheduled",
`It will send ${formatZonedDateTime(data.scheduledAt, data.timeZone)}.`,
[
{
text: "OK",
onPress: () => router.replace(`/(app)/invoices/${id}`),
},
],
);
},
onError: (err) => Alert.alert("Could not schedule invoice", err.message),
});
const cancelScheduledInvoice = api.email.cancelScheduledInvoice.useMutation({
onSuccess: async () => {
await utils.invoices.getById.invalidate({ id: id ?? "" });
await utils.invoices.getAll.invalidate();
Alert.alert("Scheduled send cancelled");
},
onError: (err) =>
Alert.alert("Could not cancel scheduled send", err.message),
});
const previewInput = useMemo(
() =>
invoiceQuery.data
? buildPreviewPdfInputFromInvoice(invoiceQuery.data)
: null,
[invoiceQuery.data],
);
if (!id) {
return ;
}
if (invoiceQuery.isLoading) {
return ;
}
if (!invoiceQuery.data) {
return ;
}
const invoice = invoiceQuery.data;
const status = getInvoiceStatus(invoice);
const clientEmail = invoice.client?.email?.trim() ?? "";
const businessName = invoice.business?.name ?? "Your business";
const sendLabel = status === "draft" ? "Send invoice" : "Resend invoice";
function handleSend() {
if (!clientEmail) {
Alert.alert(
"No client email",
"Add an email address to this client before sending invoices.",
);
return;
}
if (invoice.items.length === 0) {
Alert.alert(
"No line items",
"Add line items or clock time to this invoice before sending.",
);
return;
}
sendInvoice.mutate({
invoiceId: invoice.id,
customMessage: customMessage.trim() || undefined,
});
}
function handleSchedule() {
if (!clientEmail || invoice.items.length === 0) return;
let instant: Date;
try {
instant = zonedDateTimeToInstant(
toLocalDateTimeInputValue(scheduledAt),
timeZone,
"earlier",
);
} catch (error) {
Alert.alert(
"Choose another time",
error instanceof Error ? error.message : "Invalid local time",
);
return;
}
if (instant.getTime() < Date.now() + 60_000) {
Alert.alert(
"Choose a future time",
"The scheduled time must be at least one minute from now.",
);
return;
}
scheduleInvoice.mutate({
invoiceId: invoice.id,
scheduledAt: instant,
timeZone,
customMessage: customMessage.trim() || undefined,
});
}
return (
Optional note included in the email body.
{invoice.scheduledSendStatus === "pending" &&
invoice.scheduledSendAt ? (
{formatZonedDateTime(
invoice.scheduledSendAt,
invoice.scheduledSendTimeZone ?? timeZone,
)}{" "}
({invoice.scheduledSendTimeZone ?? timeZone})
) : null}
Time zone: {timeZone}. The exact instant is preserved across
devices and daylight saving changes.
{!clientEmail ? (
Add a client email address before sending.
) : invoice.items.length === 0 ? (
Add line items before sending this invoice.
) : null}
);
}
function SummaryRow({
label,
value,
bold,
}: {
label: string;
value: string;
bold?: boolean;
}) {
const { colors } = useAppTheme();
return (
{label}
{value}
);
}
const summaryStyles = StyleSheet.create({
row: {
flexDirection: "row",
justifyContent: "space-between",
gap: spacing.md,
paddingVertical: 4,
},
label: {
fontFamily: fonts.body,
fontSize: 14,
},
value: {
fontFamily: fonts.bodyMedium,
fontSize: 14,
flex: 1,
textAlign: "right",
},
bold: {
fontFamily: fonts.bodySemiBold,
fontSize: 15,
},
});
const createSendStyles = (colors: ThemeColors) =>
StyleSheet.create({
flex: { flex: 1 },
container: {
padding: spacing.md,
gap: spacing.md,
},
messageHint: {
fontFamily: fonts.body,
fontSize: 13,
lineHeight: 18,
marginBottom: spacing.xs,
},
messageInput: {
minHeight: 96,
textAlignVertical: "top",
},
warning: {
fontFamily: fonts.body,
fontSize: 13,
textAlign: "center",
},
});