334 lines
13 KiB
TypeScript
334 lines
13 KiB
TypeScript
import React, { useState, useCallback } from "react";
|
|
import {
|
|
View,
|
|
ScrollView,
|
|
ActivityIndicator,
|
|
Pressable,
|
|
Modal,
|
|
} 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 {
|
|
User,
|
|
Building2,
|
|
Mail,
|
|
Phone,
|
|
MapPin,
|
|
Hash,
|
|
Tag,
|
|
ShieldCheck,
|
|
BookOpen,
|
|
Pencil,
|
|
Trash2,
|
|
} from "@/lib/icons";
|
|
import { ScreenWrapper } from "@/components/ScreenWrapper";
|
|
import { StandardHeader } from "@/components/StandardHeader";
|
|
import { api } from "@/lib/api";
|
|
import { toast } from "@/lib/toast-store";
|
|
|
|
export default function CustomerDetailScreen() {
|
|
const nav = useSirouRouter<AppRoutes>();
|
|
const { id } = useLocalSearchParams();
|
|
|
|
const [data, setData] = useState<any>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [deleting, setDeleting] = useState(false);
|
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
|
|
const fetch = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
const cId = Array.isArray(id) ? id[0] : id;
|
|
if (!cId) return;
|
|
const response = await api.customers.getById({ params: { id: cId } });
|
|
setData(response || null);
|
|
} catch {
|
|
toast.error("Error", "Failed to load customer");
|
|
setData(null);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [id]);
|
|
|
|
useFocusEffect(useCallback(() => { fetch(); }, [fetch]));
|
|
|
|
const handleDelete = async () => {
|
|
try {
|
|
setDeleting(true);
|
|
const cId = Array.isArray(id) ? id[0] : id;
|
|
await api.customers.delete({ params: { id: cId } });
|
|
toast.success("Deleted", "Customer has been deleted");
|
|
setShowDeleteModal(false);
|
|
nav.back();
|
|
} catch (err: any) {
|
|
toast.error("Error", err?.message || "Failed to delete customer");
|
|
} finally {
|
|
setDeleting(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<StandardHeader title="Customer" showBack />
|
|
<View className="flex-1 items-center justify-center">
|
|
<ActivityIndicator size="large" color="#E46212" />
|
|
</View>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
if (!data) {
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<StandardHeader title="Customer" showBack />
|
|
<View className="flex-1 items-center justify-center px-8">
|
|
<Text className="text-muted-foreground text-center font-sans-medium">
|
|
Failed to load customer details.
|
|
</Text>
|
|
<Pressable onPress={fetch} className="mt-4 px-6 py-2 bg-primary rounded-[6px]">
|
|
<Text className="text-white font-sans-bold text-sm">Retry</Text>
|
|
</Pressable>
|
|
</View>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
const isCompany = data?.type === "COMPANY";
|
|
const d = data || {};
|
|
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<Stack.Screen options={{ headerShown: false }} />
|
|
<StandardHeader title="Customer" showBack />
|
|
|
|
<ScrollView
|
|
className="flex-1"
|
|
contentContainerStyle={{ paddingBottom: 120 }}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{/* Identity Header */}
|
|
<View className="px-5 pt-6 pb-2">
|
|
<View className="flex-row items-center gap-4 mb-4">
|
|
<View className={`h-14 w-14 rounded-full items-center justify-center ${isCompany ? "bg-blue-500/10" : "bg-primary/10"}`}>
|
|
{isCompany ? (
|
|
<Building2 color="#2563eb" size={26} strokeWidth={2} />
|
|
) : (
|
|
<User color="#E46212" size={26} strokeWidth={2} />
|
|
)}
|
|
</View>
|
|
<View className="flex-1">
|
|
<View className="flex-row items-center gap-2">
|
|
<Text className="text-xl font-sans-black text-foreground tracking-tight flex-1">
|
|
{d.displayName || "—"}
|
|
</Text>
|
|
<View className={`px-2.5 py-1 rounded-[4px] ${isCompany ? "bg-blue-500/10" : "bg-primary/10"}`}>
|
|
<Text className={`text-[9px] font-sans-bold uppercase tracking-widest ${isCompany ? "text-blue-600" : "text-primary"}`}>
|
|
{isCompany ? "Company" : "Individual"}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<Text className="text-muted-foreground text-xs font-sans-medium mt-0.5">
|
|
Created {d.createdAt ? new Date(d.createdAt).toLocaleDateString() : "—"}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Contact */}
|
|
{(d.email || d.phone) && (
|
|
<View className="px-5 mb-5">
|
|
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
|
|
Contact
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] border border-border p-4 gap-3">
|
|
<InfoRow icon={<Mail color="#E46212" size={14} strokeWidth={2} />} label="Email" value={d.email} />
|
|
{d.email && d.phone ? <View className="border-b border-border/40" /> : null}
|
|
<InfoRow icon={<Phone color="#E46212" size={14} strokeWidth={2} />} label="Phone" value={d.phone} />
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Identity Details */}
|
|
{(d.firstName || d.lastName || d.companyName) && (
|
|
<View className="px-5 mb-5">
|
|
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
|
|
Identity Details
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] border border-border p-4 gap-3">
|
|
{isCompany ? (
|
|
<InfoRow icon={<Building2 color="#2563eb" size={14} strokeWidth={2} />} label="Company Name" value={d.companyName} />
|
|
) : (
|
|
<>
|
|
<InfoRow icon={<User color="#E46212" size={14} strokeWidth={2} />} label="First Name" value={d.firstName} />
|
|
{d.firstName && d.lastName ? <View className="border-b border-border/40" /> : null}
|
|
<InfoRow icon={<User color="#E46212" size={14} strokeWidth={2} />} label="Last Name" value={d.lastName} />
|
|
</>
|
|
)}
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Documents */}
|
|
{(d.tin || d.vatRegistrationNumber || d.businessLicenseNumber) && (
|
|
<View className="px-5 mb-5">
|
|
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
|
|
Documents
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] border border-border p-4 gap-3">
|
|
<InfoRow icon={<Hash color="#E46212" size={14} strokeWidth={2} />} label="TIN Number" value={d.tin} />
|
|
{d.tin && d.vatRegistrationNumber ? <View className="border-b border-border/40" /> : null}
|
|
<InfoRow icon={<Tag color="#E46212" size={14} strokeWidth={2} />} label="VAT Registration" value={d.vatRegistrationNumber} />
|
|
{(d.tin || d.vatRegistrationNumber) && d.businessLicenseNumber ? <View className="border-b border-border/40" /> : null}
|
|
<InfoRow icon={<ShieldCheck color="#E46212" size={14} strokeWidth={2} />} label="Business License" value={d.businessLicenseNumber} />
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Address */}
|
|
{d.address && (
|
|
<View className="px-5 mb-5">
|
|
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground mb-2">
|
|
Address
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] border border-border p-4">
|
|
<View className="flex-row items-start gap-3">
|
|
<View className="w-8 h-8 rounded-full bg-primary/10 items-center justify-center mt-0.5">
|
|
<MapPin color="#E46212" size={14} strokeWidth={2} />
|
|
</View>
|
|
<Text className="text-foreground font-sans-medium text-sm leading-5 flex-1">
|
|
{d.address}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Notes */}
|
|
{d.notes && (
|
|
<View className="px-5 mb-5">
|
|
<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-4">
|
|
<View className="flex-row items-start gap-3">
|
|
<View className="w-8 h-8 rounded-full bg-primary/10 items-center justify-center mt-0.5">
|
|
<BookOpen color="#E46212" size={14} strokeWidth={2} />
|
|
</View>
|
|
<Text className="text-foreground font-sans-medium text-sm leading-5 flex-1">
|
|
{d.notes}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Divider */}
|
|
<View className="mx-5 mb-5 border-t border-border/60" />
|
|
|
|
{/* Action Buttons */}
|
|
<View className="px-5 mb-6 gap-3">
|
|
<Pressable
|
|
onPress={() => {
|
|
const cId = Array.isArray(id) ? id[0] : id;
|
|
nav.go("customers/edit", { id: cId });
|
|
}}
|
|
className="bg-primary h-11 rounded-[6px] flex-row items-center justify-center gap-2"
|
|
>
|
|
<Pencil color="white" size={16} strokeWidth={2.5} />
|
|
<Text className="text-white font-sans-bold text-[11px] uppercase tracking-widest">
|
|
Edit Customer
|
|
</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
onPress={() => setShowDeleteModal(true)}
|
|
className="bg-red-500 h-11 rounded-[6px] flex-row items-center justify-center gap-2"
|
|
>
|
|
<Trash2 color="white" size={16} strokeWidth={2.5} />
|
|
<Text className="text-white font-sans-bold text-[11px] uppercase tracking-widest">
|
|
Delete Customer
|
|
</Text>
|
|
</Pressable>
|
|
</View>
|
|
</ScrollView>
|
|
|
|
{/* Delete Confirmation Modal */}
|
|
<Modal
|
|
visible={showDeleteModal}
|
|
transparent
|
|
animationType="fade"
|
|
onRequestClose={() => setShowDeleteModal(false)}
|
|
>
|
|
<Pressable
|
|
className="flex-1 bg-black/50 items-center justify-center px-8"
|
|
onPress={() => setShowDeleteModal(false)}
|
|
>
|
|
<Pressable
|
|
className="bg-card rounded-2xl p-6 w-full border border-border"
|
|
onPress={(e) => e.stopPropagation()}
|
|
>
|
|
<View className="items-center mb-5">
|
|
<View className="w-14 h-14 rounded-full bg-red-500/10 items-center justify-center mb-4">
|
|
<Trash2 color="#EF4444" size={24} strokeWidth={2} />
|
|
</View>
|
|
<Text className="text-[18px] font-sans-bold text-foreground text-center">
|
|
Delete Customer?
|
|
</Text>
|
|
<Text className="text-muted-foreground text-sm font-sans-medium text-center mt-2 leading-5">
|
|
This will permanently delete{" "}
|
|
<Text className="font-sans-bold text-foreground">
|
|
{d.displayName}
|
|
</Text>{" "}
|
|
and all associated data. This action cannot be undone.
|
|
</Text>
|
|
</View>
|
|
|
|
<View className="gap-3">
|
|
<Pressable
|
|
onPress={handleDelete}
|
|
disabled={deleting}
|
|
className="bg-red-500 h-12 rounded-[6px] items-center justify-center"
|
|
>
|
|
{deleting ? (
|
|
<ActivityIndicator color="#ffffff" size="small" />
|
|
) : (
|
|
<Text className="text-white font-sans-bold text-sm">
|
|
Yes, Delete
|
|
</Text>
|
|
)}
|
|
</Pressable>
|
|
<Pressable
|
|
onPress={() => setShowDeleteModal(false)}
|
|
className="bg-secondary h-12 rounded-[6px] items-center justify-center border border-border"
|
|
>
|
|
<Text className="text-foreground font-sans-bold text-sm">
|
|
Cancel
|
|
</Text>
|
|
</Pressable>
|
|
</View>
|
|
</Pressable>
|
|
</Pressable>
|
|
</Modal>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
function InfoRow({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) {
|
|
if (!value) return null;
|
|
return (
|
|
<View className="flex-row items-center gap-3">
|
|
<View className="w-8 h-8 rounded-full bg-primary/10 items-center justify-center">
|
|
{icon}
|
|
</View>
|
|
<View className="flex-1">
|
|
<Text className="text-[10px] font-sans-bold uppercase tracking-wider text-muted-foreground">
|
|
{label}
|
|
</Text>
|
|
<Text className="text-foreground font-sans-bold text-sm mt-px">{value}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|