Yaltopia-Tickets-App/app/proforma/[id].tsx
2026-05-21 16:13:16 +03:00

429 lines
14 KiB
TypeScript

import React, { useState, useEffect } from "react";
import {
View,
ScrollView,
ActivityIndicator,
Alert,
Linking,
useColorScheme,
Pressable,
} from "react-native";
import { useSirouRouter } from "@sirou/react-native";
import { AppRoutes } from "@/lib/routes";
import { Stack, useLocalSearchParams } from "expo-router";
import { Text } from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
FileText,
Calendar,
Share2,
Download,
Trash2,
Package,
Clock,
ExternalLink,
ChevronRight,
User,
CreditCard,
Hash,
AlertCircle,
} from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { ShadowWrapper } from "@/components/ShadowWrapper";
import { StandardHeader } from "@/components/StandardHeader";
import { api, BASE_URL } from "@/lib/api";
import { toast } from "@/lib/toast-store";
import { useAuthStore } from "@/lib/auth-store";
export default function ProformaDetailScreen() {
const nav = useSirouRouter<AppRoutes>();
const { id } = useLocalSearchParams();
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
const [loading, setLoading] = useState(true);
const [proforma, setProforma] = useState<any>(null);
useEffect(() => {
fetchProforma();
}, [id]);
const fetchProforma = async () => {
try {
setLoading(true);
// Ensure id is a string if useLocalSearchParams returns an array
const proformaId = Array.isArray(id) ? id[0] : id;
if (!proformaId) throw new Error("No ID provided");
const data = await api.proforma.getById({ params: { id: proformaId } });
setProforma(data);
} catch (error: any) {
console.error("[ProformaDetail] Error:", error);
toast.error("Error", "Failed to load proforma details");
} finally {
setLoading(false);
}
};
const handleGetPdf = async () => {
try {
const { token } = useAuthStore.getState();
const pdfUrl = `${BASE_URL}proforma/${id}/pdf?token=${token}`;
await Linking.openURL(pdfUrl);
} catch (error) {
console.error("[ProformaDetail] PDF Error:", error);
toast.error("Error", "Failed to open PDF");
}
};
const handleDelete = async () => {
Alert.alert(
"Delete Proforma",
"Are you sure you want to delete this proforma? This action cannot be undone.",
[
{ text: "Cancel", style: "cancel" },
{
text: "Delete",
style: "destructive",
onPress: async () => {
try {
setLoading(true);
const proformaId = Array.isArray(id) ? id[0] : id;
await api.proforma.delete({
params: { id: proformaId as string },
});
toast.success("Success", "Proforma deleted successfully");
nav.back();
} catch (error) {
console.error("[ProformaDetail] Delete Error:", error);
toast.error("Error", "Failed to delete proforma");
setLoading(false);
}
},
},
],
);
};
if (loading) {
return (
<ScreenWrapper className="bg-background">
<Stack.Screen options={{ headerShown: false }} />
<StandardHeader title="Proforma" showBack />
<View className="flex-1 justify-center items-center">
<ActivityIndicator color="#ea580c" size="large" />
</View>
</ScreenWrapper>
);
}
if (!proforma) {
return (
<ScreenWrapper className="bg-background">
<Stack.Screen options={{ headerShown: false }} />
<StandardHeader title="Proforma" showBack />
<View className="flex-1 justify-center items-center">
<AlertCircle size={48} color="#ef4444" className="mb-4" />
<Text variant="h4" className="mb-1">
Proforma Not Found
</Text>
<Text variant="muted">
The requested document could not be retrieved.
</Text>
</View>
</ScreenWrapper>
);
}
const amountValue = Number(
typeof proforma.amount === "object"
? proforma.amount.value
: proforma.amount,
);
const taxAmountValue = Number(
typeof proforma.taxAmount === "object"
? proforma.taxAmount?.value
: proforma.taxAmount || 0,
);
const discountValue = Number(
typeof proforma.discountAmount === "object"
? proforma.discountAmount?.value
: proforma.discountAmount || 0,
);
const subtotalValue = amountValue - taxAmountValue + discountValue;
const items = proforma.items || [];
const statusColors = {
PAID: {
bg: "bg-emerald-500/10",
text: "text-emerald-500",
dot: "bg-emerald-500",
},
PENDING: {
bg: "bg-amber-500/10",
text: "text-amber-500",
dot: "bg-amber-500",
},
DRAFT: { bg: "bg-blue-500/10", text: "text-blue-500", dot: "bg-blue-500" },
DEFAULT: {
bg: "bg-slate-500/10",
text: "text-slate-500",
dot: "bg-slate-500",
},
};
const status = (proforma.status || "PENDING").toUpperCase();
const colors =
statusColors[status as keyof typeof statusColors] || statusColors.DEFAULT;
return (
<ScreenWrapper className="bg-background">
<Stack.Screen options={{ headerShown: false }} />
<StandardHeader
title={"Proforma Detail"}
showBack
// rightAction="edit"
// onRightActionPress={() => nav.go("proforma/edit", { id: proforma.id })}
/>
<ScrollView
className="flex-1"
contentContainerStyle={{ paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
>
{/* Modern Hero Area */}
<View className="px-5 pt-4">
<Text
variant="muted"
className="text-xs font-bold uppercase tracking-wider mb-1"
>
Proforma Estimated Total
</Text>
<View className="flex-row items-end gap-2 mb-6">
<Text variant="h1" className="text-4xl font-black text-foreground">
{amountValue.toLocaleString(undefined, {
minimumFractionDigits: 2,
})}
</Text>
<Text className="text-xl font-bold text-primary mb-2">
{proforma.currency || "ETB"}
</Text>
</View>
{/* Quick Stats Grid */}
<View className="flex-row gap-3 mb-6">
<View className="flex-1 bg-card rounded-[6px] p-4 border border-border/40">
<Calendar size={16} color="#ea580c" className="mb-2" />
<Text
variant="muted"
className="text-[10px] uppercase font-bold tracking-tighter mb-0.5"
>
Issue Date
</Text>
<Text className="text-foreground font-bold text-sm">
{new Date(
proforma.issueDate || proforma.createdAt,
).toLocaleDateString()}
</Text>
</View>
<View className="flex-1 bg-card rounded-[6px] p-4 border border-border/40">
<Clock size={16} color="#ef4444" className="mb-2" />
<Text
variant="muted"
className="text-[10px] uppercase font-bold tracking-tighter mb-0.5"
>
Due Date
</Text>
<Text className="text-foreground font-bold text-sm">
{new Date(proforma.dueDate).toLocaleDateString()}
</Text>
</View>
</View>
</View>
{/* Client Box */}
<View className="px-5 mb-6">
<View className="bg-primary/5 rounded-[6px] p-5 border border-primary/10">
<View className="flex-row items-center gap-3 mb-4">
<View className="h-10 w-10 rounded-full bg-primary/20 items-center justify-center">
<User color="#ea580c" size={20} />
</View>
<View>
<Text
variant="muted"
className="text-[10px] uppercase font-bold"
>
Quotation For
</Text>
<Text variant="p" className="text-foreground font-bold text-lg">
{proforma.customerName || "Interested Client"}
</Text>
</View>
</View>
<View className="flex-row flex-wrap gap-4 pt-4 border-t border-primary/10">
{proforma.customerEmail && (
<View className="flex-row items-center gap-2">
<CreditCard size={12} color="#64748b" />
<Text className="text-muted-foreground text-xs">
{proforma.customerEmail}
</Text>
</View>
)}
<View className="flex-row items-center gap-2">
<Hash size={12} color="#64748b" />
<Text className="text-muted-foreground text-xs">
#{proforma.id.split("-")[0]}
</Text>
</View>
</View>
</View>
</View>
{/* Detailed Items Table */}
<View className="px-5 mb-6">
<Text variant="h4" className="font-bold mb-4 px-1">
Estimate Summary
</Text>
<Card className="bg-card rounded-[6px] overflow-hidden border-border/60">
{items.map((item: any, idx: number) => (
<View
key={idx}
className={`p-4 ${idx !== items.length - 1 ? "border-b border-border/40" : ""}`}
>
<View className="flex-row justify-between items-start mb-1">
<Text className="text-foreground font-bold flex-1 mr-4">
{item.description}
</Text>
<Text className="text-foreground font-black">
{Number(
item.total?.value || item.total || 0,
).toLocaleString()}
</Text>
</View>
<Text className="text-muted-foreground text-xs">
{item.quantity} units x{" "}
{Number(
item.unitPrice?.value || item.unitPrice || 0,
).toLocaleString()}{" "}
{proforma.currency}
</Text>
</View>
))}
{items.length === 0 && (
<View className="p-8 items-center bg-muted/20">
<Package size={32} color="#cbd5e1" className="mb-2" />
<Text variant="muted">Empty line items list</Text>
</View>
)}
</Card>
</View>
{/* Billing Breakdown */}
<View className="px-5 mb-6">
<Card className="bg-card rounded-[6px] p-5 shadow-sm shadow-black/5 border-border/60">
<View className="flex-row justify-between mb-1">
<Text className="text-muted-foreground font-medium">
Net Price
</Text>
<Text className="text-foreground font-bold">
{subtotalValue.toLocaleString()} {proforma.currency}
</Text>
</View>
{taxAmountValue > 0 && (
<View className="flex-row justify-between mb-1">
<Text className="text-muted-foreground font-medium">
Estimated Tax
</Text>
<Text className="text-emerald-500 font-bold">
+{taxAmountValue.toLocaleString()} {proforma.currency}
</Text>
</View>
)}
{discountValue > 0 && (
<View className="flex-row justify-between mb-1">
<Text className="text-muted-foreground font-medium">
Applicable Discount
</Text>
<Text className="text-rose-500 font-bold">
-{discountValue.toLocaleString()} {proforma.currency}
</Text>
</View>
)}
<View className="pt-1 border-t border-dashed border-border flex-row justify-between items-center">
<View>
<Text className="text-foreground font-black text-lg">
Estimated Total
</Text>
</View>
<Text className="text-primary font-black text-lg">
{amountValue.toLocaleString()} {proforma.currency}
</Text>
</View>
</Card>
</View>
{/* Notes */}
{proforma.notes && (
<View className="px-5 mb-10">
<Text
variant="muted"
className="text-[10px] uppercase font-bold mb-2"
>
Internal Notes
</Text>
<Text className="text-foreground font-medium italic opacity-80 leading-5">
" {proforma.notes} "
</Text>
</View>
)}
{/* Premium Actions */}
<View className="px-5 gap-3">
<View className="flex-row gap-3">
<Button
className="flex-1 h-10 rounded-[6px] bg-primary shadow-lg shadow-primary/20"
onPress={() => nav.go("proforma/edit", { id: proforma.id })}
>
<Share2 color="#ffffff" size={18} strokeWidth={2.5} />
<Text className="ml-2 text-white font-black uppercase tracking-widest text-xs">
Edit Detail
</Text>
</Button>
<Button
variant="outline"
className="flex-1 h-10 rounded-[6px] bg-card border-border/60"
onPress={handleGetPdf}
>
<Download
color={isDark ? "#f1f5f9" : "#0f172a"}
size={18}
strokeWidth={2.5}
/>
<Text className="ml-2 text-foreground font-black uppercase tracking-widest text-xs">
Export PDF
</Text>
</Button>
</View>
<Button
variant="ghost"
className="h-10 rounded-[6px] border border-red-500/60"
onPress={handleDelete}
>
<Trash2 color="#ef4444" size={18} />
<Text className="ml-2 text-rose-500 font-bold uppercase tracking-widest text-xs">
Delete Proforma
</Text>
</Button>
</View>
</ScrollView>
</ScreenWrapper>
);
}