Yaltopia-Tickets-App/app/payment-requests/[id].tsx
2026-06-05 13:39:37 +03:00

374 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useCallback } from "react";
import { View, ScrollView, ActivityIndicator } from "react-native";
import { useSirouRouter } from "@sirou/react-native";
import { AppRoutes } from "@/lib/routes";
import { Stack, useLocalSearchParams, useFocusEffect } from "expo-router";
import { Text } from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import { User, Calendar, Clock, Building2, Hash, Send } from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { StandardHeader } from "@/components/StandardHeader";
import { api } from "@/lib/api";
import { useColorScheme } from "nativewind";
import { toast } from "@/lib/toast-store";
const STATUS_THEME: Record<
string,
{ label: string; bg: string; text: string; dot: string }
> = {
DRAFT: {
label: "Draft",
bg: "bg-slate-500/10",
text: "text-slate-600",
dot: "bg-slate-500",
},
SENT: {
label: "Sent",
bg: "bg-primary/10",
text: "text-primary",
dot: "bg-primary",
},
OPENED: {
label: "Opened",
bg: "bg-blue-500/10",
text: "text-blue-600",
dot: "bg-blue-500",
},
PAID: {
label: "Paid",
bg: "bg-emerald-500/10",
text: "text-emerald-600",
dot: "bg-emerald-500",
},
EXPIRED: {
label: "Expired",
bg: "bg-red-500/10",
text: "text-red-600",
dot: "bg-red-500",
},
CANCELLED: {
label: "Cancelled",
bg: "bg-slate-500/10",
text: "text-slate-600",
dot: "bg-slate-500",
},
};
function safeVal(v: any): number {
if (v == null) return 0;
if (typeof v === "object") return Number(v.value) || 0;
return Number(v) || 0;
}
function fmt(v: number, currency = "ETB") {
return `${currency} ${v.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
export default function PaymentRequestDetailScreen() {
const nav = useSirouRouter<AppRoutes>();
const { id } = useLocalSearchParams();
const { colorScheme } = useColorScheme();
const isDark = colorScheme === "dark";
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [sending, setSending] = useState(false);
const fetch = useCallback(async () => {
try {
setLoading(true);
const reqId = Array.isArray(id) ? id[0] : id;
if (!reqId) return;
const result = await api.paymentRequests.getById({
params: { id: reqId },
});
setData(result);
await api.paymentRequests.open({ params: { id: reqId } }).catch(() => {});
} catch (err: any) {
toast.error("Error", "Failed to load payment request");
} finally {
setLoading(false);
}
}, [id]);
useFocusEffect(
useCallback(() => {
fetch();
}, [fetch]),
);
const handleSendEmail = async () => {
try {
setSending(true);
const reqId = Array.isArray(id) ? id[0] : id;
await api.paymentRequests.sendEmail({ params: { id: reqId } });
toast.success("Sent", "Payment request emailed to customer");
} catch (err: any) {
toast.error("Error", err?.message || "Failed to send email");
} finally {
setSending(false);
}
};
if (loading || !data) {
return (
<ScreenWrapper className="bg-background">
<StandardHeader title="Payment Request" showBack />
<View className="flex-1 items-center justify-center">
<ActivityIndicator size="large" color="#E46212" />
</View>
</ScreenWrapper>
);
}
const statusKey = (data.status || "DRAFT").toUpperCase();
const theme = STATUS_THEME[statusKey] || STATUS_THEME.DRAFT;
const items: any[] = data.items || [];
const accounts: any[] = data.accounts || [];
const currency = data.currency || "ETB";
const amount = safeVal(data.amount);
const subtotal = items.reduce((s: number, i: any) => s + safeVal(i.total), 0);
const tax = safeVal(data.taxAmount);
const discount = safeVal(data.discountAmount);
const total = amount || subtotal + tax - discount;
return (
<ScreenWrapper className="bg-background">
<Stack.Screen options={{ headerShown: false }} />
<StandardHeader title="Payment Request" showBack />
<ScrollView
className="flex-1"
contentContainerStyle={{ paddingBottom: 120 }}
showsVerticalScrollIndicator={false}
>
{/* Customer + Dates cluster */}
<View className="px-5 mt-6 mb-6">
<View className="bg-card rounded-[6px] border border-border p-4 gap-4">
<View className="flex-row items-center gap-3">
<View className="h-9 w-9 rounded-full bg-primary/10 items-center justify-center">
<User color="#E46212" size={17} strokeWidth={2} />
</View>
<View className="flex-1">
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground">
Customer
</Text>
<Text className="text-foreground font-sans-bold text-sm mt-px">
{data.customerName || "—"}
</Text>
{(data.customerEmail || data.customerPhone) && (
<Text className="text-muted-foreground text-[11px] font-sans-medium mt-0.5">
{[data.customerEmail, data.customerPhone]
.filter(Boolean)
.join(" · ")}
</Text>
)}
</View>
</View>
<View className="border-t border-border" />
<View className="flex-row gap-4">
<View className="flex-1">
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-1">
Issued
</Text>
<View className="flex-row items-center gap-1.5">
<Calendar size={12} color="#94a3b8" strokeWidth={2} />
<Text className="text-foreground font-sans-bold text-sm">
{data.issueDate
? new Date(data.issueDate).toLocaleDateString()
: "—"}
</Text>
</View>
</View>
<View className="w-px bg-border" />
<View className="flex-1">
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-1">
Due
</Text>
<View className="flex-row items-center gap-1.5">
<Clock size={12} color="#94a3b8" strokeWidth={2} />
<Text className="text-foreground font-sans-bold text-sm">
{data.dueDate
? new Date(data.dueDate).toLocaleDateString()
: "—"}
</Text>
</View>
</View>
</View>
</View>
</View>
{/* Description */}
{data.description ? (
<View className="px-5 mb-6">
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
Description
</Text>
<View className="bg-card rounded-[6px] p-3.5 border border-border">
<Text className="text-foreground text-sm font-sans-medium leading-5">
{data.description}
</Text>
</View>
</View>
) : null}
{/* Items */}
{items.length > 0 && (
<View className="px-5 mb-6">
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
Items ({items.length})
</Text>
<View className="bg-card rounded-[6px] border border-border overflow-hidden">
{items.map((item: any, idx: number) => (
<View
key={item.id || idx}
className={`px-4 py-3 ${idx < items.length - 1 ? "border-b border-border" : ""}`}
>
<View className="flex-row justify-between items-start mb-0.5">
<Text
className="text-foreground font-sans-bold text-sm flex-1 mr-3"
numberOfLines={1}
>
{item.description || `Item ${idx + 1}`}
</Text>
<Text className="text-foreground font-sans-bold text-sm">
{fmt(safeVal(item.total), currency)}
</Text>
</View>
<Text className="text-muted-foreground text-[11px] font-sans-medium">
{safeVal(item.quantity)} ×{" "}
{fmt(safeVal(item.unitPrice), currency)}
</Text>
</View>
))}
</View>
</View>
)}
{/* Totals */}
<View className="px-5 mb-6">
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
Summary
</Text>
<View className="bg-card rounded-[6px] p-4 border border-border gap-2.5">
{subtotal > 0 && (
<View className="flex-row justify-between">
<Text className="text-muted-foreground text-sm font-sans-medium">
Subtotal
</Text>
<Text className="text-foreground text-sm font-sans-bold">
{fmt(subtotal, currency)}
</Text>
</View>
)}
{tax > 0 && (
<View className="flex-row justify-between">
<Text className="text-muted-foreground text-sm font-sans-medium">
Tax
</Text>
<Text className="text-foreground text-sm font-sans-bold">
{fmt(tax, currency)}
</Text>
</View>
)}
{discount > 0 && (
<View className="flex-row justify-between">
<Text className="text-muted-foreground text-sm font-sans-medium">
Discount
</Text>
<Text className="text-foreground text-sm font-sans-bold">
-{fmt(discount, currency)}
</Text>
</View>
)}
<View className="border-t border-border/60 pt-2.5 flex-row justify-between">
<Text className="text-foreground font-sans-black text-base">
Total
</Text>
<Text className="text-primary font-sans-black text-base">
{fmt(total, currency)}
</Text>
</View>
</View>
</View>
{/* Accounts */}
{accounts.length > 0 && (
<View className="px-5 mb-6">
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
Bank Accounts ({accounts.length})
</Text>
<View className="gap-2">
{accounts.map((acc: any, idx: number) => (
<View
key={acc.id || idx}
className="bg-card rounded-[6px] p-3.5 border border-border"
>
<View className="flex-row items-center gap-2 mb-2">
<Building2 size={14} color="#E46212" strokeWidth={2} />
<Text className="text-foreground font-sans-bold text-sm flex-1">
{acc.bankName || "Bank"}
</Text>
<View className="bg-primary/10 px-2 py-0.5 rounded-[4px]">
<Text className="text-primary text-[10px] font-sans-bold">
{acc.currency || currency}
</Text>
</View>
</View>
<Text className="text-muted-foreground text-xs font-sans-medium">
{acc.accountName || "—"}
</Text>
<View className="flex-row items-center gap-1.5 mt-0.5">
<Text className="text-foreground text-xs font-sans-bold">
{acc.accountNumber || "—"}
</Text>
</View>
</View>
))}
</View>
</View>
)}
{/* Notes */}
{data.notes ? (
<View className="px-5 mb-6">
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
Notes
</Text>
<View className="bg-card rounded-[6px] p-3.5 border border-border">
<Text className="text-foreground text-sm font-sans-medium leading-5">
{data.notes}
</Text>
</View>
</View>
) : null}
{/* Actions */}
<View className="px-5 gap-3">
<Button
className="h-10 rounded-[6px] bg-primary"
onPress={handleSendEmail}
disabled={sending || !data.customerEmail}
>
{sending ? (
<ActivityIndicator color="#ffffff" size="small" />
) : (
<>
<Send color="#ffffff" size={16} strokeWidth={2.5} />
<Text className="ml-2 text-white text-[11px] font-sans-bold uppercase tracking-widest">
Send Email
</Text>
</>
)}
</Button>
{!data.customerEmail && (
<Text className="text-[11px] text-muted-foreground font-sans-medium text-center">
No customer email on file
</Text>
)}
</View>
</ScrollView>
</ScreenWrapper>
);
}