440 lines
17 KiB
TypeScript
440 lines
17 KiB
TypeScript
import React, { useState, useCallback } from "react";
|
|
import {
|
|
View,
|
|
ScrollView,
|
|
ActivityIndicator,
|
|
Linking,
|
|
Pressable,
|
|
useColorScheme,
|
|
} 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 {
|
|
Calendar,
|
|
Trash2,
|
|
AlertCircle,
|
|
FileText,
|
|
ExternalLink,
|
|
Download,
|
|
ArrowLeft,
|
|
Edit,
|
|
} from "@/lib/icons";
|
|
import { ScreenWrapper } from "@/components/ScreenWrapper";
|
|
import { StandardHeader } from "@/components/StandardHeader";
|
|
import { api } from "@/lib/api";
|
|
import { toast } from "@/lib/toast-store";
|
|
import { ActionModal } from "@/components/ActionModal";
|
|
|
|
const STATUS_COLORS: Record<string, { bg: string; text: string; dot: string }> = {
|
|
DRAFT: { bg: "bg-slate-500/10", text: "text-slate-500", dot: "bg-slate-500" },
|
|
SUBMITTED: { bg: "bg-amber-500/10", text: "text-amber-500", dot: "bg-amber-500" },
|
|
PAID: { bg: "bg-emerald-500/10", text: "text-emerald-500", dot: "bg-emerald-500" },
|
|
CANCELLED: { bg: "bg-red-500/10", text: "text-red-500", dot: "bg-red-500" },
|
|
};
|
|
|
|
export default function DeclarationDetailScreen() {
|
|
const nav = useSirouRouter<AppRoutes>();
|
|
const { id } = useLocalSearchParams();
|
|
const colorScheme = useColorScheme();
|
|
const isDark = colorScheme === "dark";
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [declaration, setDeclaration] = useState<any>(null);
|
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
fetchDeclaration();
|
|
}, [id]),
|
|
);
|
|
|
|
const fetchDeclaration = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const declId = Array.isArray(id) ? id[0] : id;
|
|
if (!declId) throw new Error("No ID provided");
|
|
|
|
const data = await api.declarations.getById({ params: { id: declId } });
|
|
setDeclaration(data);
|
|
} catch (error: any) {
|
|
console.error("[DeclarationDetail] Error:", error);
|
|
toast.error("Error", "Failed to load declaration");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const confirmDelete = async () => {
|
|
try {
|
|
await api.declarations.create({
|
|
body: { id: Array.isArray(id) ? id[0] : id, isActive: false },
|
|
});
|
|
toast.success("Success", "Declaration removed");
|
|
setShowDeleteModal(false);
|
|
nav.back();
|
|
} catch (error: any) {
|
|
toast.error("Error", error?.message || "Failed to delete");
|
|
setShowDeleteModal(false);
|
|
}
|
|
};
|
|
|
|
const openUrl = async (url: string) => {
|
|
if (url) {
|
|
try {
|
|
await Linking.openURL(url);
|
|
} catch {
|
|
toast.error("Error", "Could not open file");
|
|
}
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<Stack.Screen options={{ headerShown: false }} />
|
|
<StandardHeader title="Declaration" showBack />
|
|
<View className="flex-1 justify-center items-center">
|
|
<ActivityIndicator color="#ea580c" size="large" />
|
|
</View>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
if (!declaration) {
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<Stack.Screen options={{ headerShown: false }} />
|
|
<StandardHeader title="Declaration" showBack />
|
|
<View className="flex-1 justify-center items-center">
|
|
<AlertCircle size={48} color="#ef4444" className="mb-4" />
|
|
<Text variant="h4" className="mb-1">Not Found</Text>
|
|
<Text variant="muted">This declaration could not be retrieved.</Text>
|
|
</View>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
const st =
|
|
STATUS_COLORS[(declaration.status || "DRAFT").toUpperCase()] ||
|
|
STATUS_COLORS.DRAFT;
|
|
|
|
const typeBadge =
|
|
declaration.type === "VAT"
|
|
? { bg: "bg-blue-500/10", text: "text-blue-600" }
|
|
: { bg: "bg-purple-500/10", text: "text-purple-600" };
|
|
|
|
const formatDate = (d: string | null) =>
|
|
d
|
|
? new Date(d).toLocaleDateString("en-GB", {
|
|
day: "numeric",
|
|
month: "short",
|
|
year: "numeric",
|
|
})
|
|
: "—";
|
|
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<Stack.Screen options={{ headerShown: false }} />
|
|
<View className="px-5 pt-4 flex-row justify-between items-center">
|
|
<Pressable
|
|
onPress={() => nav.back()}
|
|
className="h-9 w-9 rounded-[10px] bg-card items-center justify-center border border-border"
|
|
>
|
|
<ArrowLeft
|
|
color={isDark ? "#f8fafc" : "#0f172a"}
|
|
size={18}
|
|
/>
|
|
</Pressable>
|
|
<Text className="text-foreground text-[16px] font-sans-bold">
|
|
Declaration Details
|
|
</Text>
|
|
<Pressable
|
|
onPress={() => nav.go("declarations/edit", { id: declaration?.id })}
|
|
className="h-9 w-9 rounded-[10px] bg-card items-center justify-center border border-border"
|
|
>
|
|
<Edit color="#E46212" size={16} strokeWidth={2} />
|
|
</Pressable>
|
|
</View>
|
|
|
|
<ScrollView
|
|
className="flex-1"
|
|
contentContainerStyle={{ paddingBottom: 120 }}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{/* Hero */}
|
|
<View className="px-5 pt-6 mb-6">
|
|
<View className="flex-row items-start justify-between mb-1">
|
|
<View className="flex-1 mr-4">
|
|
<View className="flex-row items-center gap-2 mb-1">
|
|
<View className={`px-2 py-0.5 rounded-[4px] ${typeBadge.bg}`}>
|
|
<Text className={`text-[8px] font-sans-bold uppercase tracking-widest ${typeBadge.text}`}>
|
|
{declaration.type?.replace("_", " ")}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<Text className="text-foreground text-xl font-sans-black tracking-tight mt-1">
|
|
{declaration.title || "Declaration"}
|
|
</Text>
|
|
<View className="flex-row items-center mt-1">
|
|
<Text className="text-muted-foreground text-xs font-sans-medium">
|
|
#{declaration.declarationNumber}
|
|
</Text>
|
|
{declaration.period && (
|
|
<>
|
|
<Text className="text-muted-foreground text-xs mx-1.5">·</Text>
|
|
<Text className="text-muted-foreground text-xs font-sans-medium">
|
|
{declaration.period}
|
|
</Text>
|
|
</>
|
|
)}
|
|
</View>
|
|
</View>
|
|
<View className={`px-2.5 py-1 rounded-[4px] ${st.bg}`}>
|
|
<View className="flex-row items-center gap-1.5">
|
|
<View className={`w-1.5 h-1.5 rounded-full ${st.dot}`} />
|
|
<Text className={`text-[9px] font-sans-bold uppercase tracking-widest ${st.text}`}>
|
|
{declaration.status || "DRAFT"}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{declaration.daysUntilDue != null && (
|
|
<View
|
|
className={`mt-3 flex-row items-center gap-2 px-3 py-2.5 rounded-[6px] ${
|
|
declaration.isOverdue
|
|
? "bg-red-500/10"
|
|
: "bg-card border border-border"
|
|
}`}
|
|
>
|
|
{declaration.isOverdue ? (
|
|
<AlertCircle size={14} color="#ef4444" strokeWidth={2} />
|
|
) : (
|
|
<Calendar size={14} color="#f59e0b" strokeWidth={2} />
|
|
)}
|
|
<Text
|
|
className={`text-xs font-sans-bold ${
|
|
declaration.isOverdue ? "text-red-500" : "text-amber-500"
|
|
}`}
|
|
>
|
|
{declaration.isOverdue
|
|
? `Overdue by ${Math.abs(declaration.daysUntilDue)} days`
|
|
: `${declaration.daysUntilDue} days until due`}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Period Dates (two-column card like invoice dates) */}
|
|
<View className="px-5 mb-6">
|
|
<View className="bg-card rounded-[6px] border border-border p-4">
|
|
<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.5">
|
|
Period Start
|
|
</Text>
|
|
<View className="flex-row items-center gap-1.5">
|
|
<Calendar size={13} color="#94a3b8" strokeWidth={2} />
|
|
<Text className="text-foreground font-sans-bold text-sm">
|
|
{formatDate(declaration.periodStart)}
|
|
</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.5">
|
|
Period End
|
|
</Text>
|
|
<View className="flex-row items-center gap-1.5">
|
|
<Calendar size={13} color="#94a3b8" strokeWidth={2} />
|
|
<Text className="text-foreground font-sans-bold text-sm">
|
|
{formatDate(declaration.periodEnd)}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Tax Details */}
|
|
<View className="px-5 mb-6">
|
|
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
|
|
Tax Details
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] border border-border overflow-hidden">
|
|
{declaration.tin && (
|
|
<DetailRow label="TIN" value={declaration.tin} isLast={!declaration.taxAccountNumber && !declaration.taxCentre} />
|
|
)}
|
|
{declaration.taxAccountNumber && (
|
|
<DetailRow label="Tax Account" value={declaration.taxAccountNumber} isLast={!declaration.taxCentre} />
|
|
)}
|
|
{declaration.taxCentre && (
|
|
<DetailRow label="Tax Centre" value={declaration.taxCentre} isLast />
|
|
)}
|
|
{!declaration.tin && !declaration.taxAccountNumber && !declaration.taxCentre && (
|
|
<View className="px-4 py-3">
|
|
<Text className="text-muted-foreground text-xs font-sans-medium">
|
|
No tax details available
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Submission Info (if available) */}
|
|
{(declaration.submissionNumber || declaration.submissionDate) && (
|
|
<View className="px-5 mb-6">
|
|
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
|
|
Submission
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] border border-border overflow-hidden">
|
|
{declaration.submissionNumber && (
|
|
<DetailRow label="Submission #" value={declaration.submissionNumber} isLast={!declaration.submissionDate} />
|
|
)}
|
|
{declaration.submissionDate && (
|
|
<DetailRow label="Submitted" value={formatDate(declaration.submissionDate)} isLast />
|
|
)}
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Files */}
|
|
{declaration.fileUrl && (
|
|
<View className="px-5 mb-6">
|
|
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
|
|
Declaration File
|
|
</Text>
|
|
<Pressable
|
|
onPress={() => openUrl(declaration.fileUrl)}
|
|
className="bg-card rounded-[6px] border border-border p-3.5 flex-row items-center"
|
|
>
|
|
<View className="h-9 w-9 rounded-[6px] bg-primary/10 items-center justify-center mr-3">
|
|
<FileText size={16} color="#E46212" strokeWidth={2} />
|
|
</View>
|
|
<View className="flex-1">
|
|
<Text className="text-foreground font-sans-bold text-sm">
|
|
{declaration.fileName || "Declaration PDF"}
|
|
</Text>
|
|
{declaration.fileSize && (
|
|
<Text className="text-muted-foreground text-[10px] font-sans-medium mt-0.5">
|
|
{(declaration.fileSize / 1024).toFixed(1)} KB
|
|
</Text>
|
|
)}
|
|
</View>
|
|
<ExternalLink size={15} color="#E46212" strokeWidth={2} />
|
|
</Pressable>
|
|
</View>
|
|
)}
|
|
|
|
{/* Receipts */}
|
|
{declaration.receiptFiles?.length > 0 && (
|
|
<View className="px-5 mb-6">
|
|
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
|
|
Receipts ({declaration.receiptFiles.length})
|
|
</Text>
|
|
<View className="gap-2">
|
|
{declaration.receiptFiles.map((rf: any, idx: number) => (
|
|
<Pressable
|
|
key={idx}
|
|
onPress={() => openUrl(rf.url)}
|
|
className="bg-card rounded-[6px] border border-border p-3 flex-row items-center"
|
|
>
|
|
<View className="h-8 w-8 rounded-[6px] bg-secondary items-center justify-center mr-3">
|
|
<Download size={14} color={colorScheme === "dark" ? "#cbd5e1" : "#475569"} strokeWidth={2} />
|
|
</View>
|
|
<View className="flex-1">
|
|
<Text className="text-foreground font-sans-bold text-xs" numberOfLines={1}>
|
|
{rf.fileName || `Receipt ${idx + 1}`}
|
|
</Text>
|
|
</View>
|
|
<ExternalLink size={14} color="#94a3b8" strokeWidth={2} />
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Linked Invoices */}
|
|
{declaration.invoices?.length > 0 && (
|
|
<View className="px-5 mb-6">
|
|
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
|
|
Linked Invoices ({declaration.invoices.length})
|
|
</Text>
|
|
<View className="gap-2">
|
|
{declaration.invoices.map((inv: any) => (
|
|
<Pressable
|
|
key={inv.id}
|
|
onPress={() => nav.go("invoices/[id]", { id: inv.id })}
|
|
className="bg-card rounded-[6px] border border-border p-3 flex-row items-center"
|
|
>
|
|
<View className="h-8 w-8 rounded-[6px] bg-primary/10 items-center justify-center mr-3">
|
|
<FileText size={14} color="#E46212" strokeWidth={2} />
|
|
</View>
|
|
<View className="flex-1">
|
|
<Text className="text-foreground font-sans-bold text-xs">
|
|
{inv.customerName || "Invoice"}
|
|
</Text>
|
|
<Text className="text-muted-foreground text-[10px] font-sans-medium mt-0.5">
|
|
{inv.invoiceNumber} · {Number(inv.amount?.value || inv.amount || 0).toLocaleString()} {inv.currency}
|
|
</Text>
|
|
</View>
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Notes */}
|
|
{declaration.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] border border-border p-3.5">
|
|
<Text className="text-foreground text-sm font-sans-medium leading-5">
|
|
{declaration.notes}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Delete */}
|
|
<View className="px-5 mb-8">
|
|
<Pressable
|
|
onPress={() => setShowDeleteModal(true)}
|
|
className="h-11 rounded-[6px] bg-red-500 flex-row items-center justify-center gap-2"
|
|
>
|
|
<Trash2 color="white" size={15} strokeWidth={2.5} />
|
|
<Text className="text-white font-sans-bold text-[11px] uppercase tracking-widest">
|
|
Delete Declaration
|
|
</Text>
|
|
</Pressable>
|
|
</View>
|
|
</ScrollView>
|
|
|
|
<ActionModal
|
|
visible={showDeleteModal}
|
|
onClose={() => setShowDeleteModal(false)}
|
|
onConfirm={confirmDelete}
|
|
title="Delete Declaration"
|
|
description="Are you sure you want to delete this declaration?"
|
|
confirmText="Delete"
|
|
confirmVariant="destructive"
|
|
icon={Trash2}
|
|
iconColor="#ef4444"
|
|
/>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
function DetailRow({ label, value, isLast }: { label: string; value: string; isLast?: boolean }) {
|
|
return (
|
|
<View className={`flex-row items-center px-4 py-3 ${!isLast ? "border-b border-border/40" : ""}`}>
|
|
<Text className="text-muted-foreground text-xs font-sans-medium flex-1">{label}</Text>
|
|
<Text className="text-foreground text-sm font-sans-bold">{value}</Text>
|
|
</View>
|
|
);
|
|
}
|