200 lines
7.8 KiB
TypeScript
200 lines
7.8 KiB
TypeScript
import React, { useState, useMemo } from "react";
|
|
import {
|
|
View,
|
|
Pressable,
|
|
Modal,
|
|
ScrollView,
|
|
ActivityIndicator,
|
|
TextInput,
|
|
Dimensions,
|
|
} from "react-native";
|
|
import { useSirouRouter } from "@sirou/react-native";
|
|
import { Text } from "@/components/ui/text";
|
|
import { Search, X, Plus, User, Building2, ChevronDown } from "@/lib/icons";
|
|
import { api } from "@/lib/api";
|
|
import { useColorScheme } from "nativewind";
|
|
|
|
const { height: SCREEN_HEIGHT } = Dimensions.get("window");
|
|
|
|
interface CustomerData {
|
|
name: string;
|
|
email: string;
|
|
phone: string;
|
|
}
|
|
|
|
interface CustomerPickerProps {
|
|
value: string;
|
|
onSelect: (c: CustomerData) => void;
|
|
placeholder?: string;
|
|
}
|
|
|
|
export function CustomerPicker({ value, onSelect, placeholder }: CustomerPickerProps) {
|
|
const nav = useSirouRouter<any>();
|
|
const { colorScheme } = useColorScheme();
|
|
const isDark = colorScheme === "dark";
|
|
|
|
const [show, setShow] = useState(false);
|
|
const [customers, setCustomers] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [search, setSearch] = useState("");
|
|
|
|
const openPicker = async () => {
|
|
setShow(true);
|
|
setSearch("");
|
|
setLoading(true);
|
|
try {
|
|
const res = await api.customers.getAll({ query: { page: 1, limit: 50 } });
|
|
setCustomers(res?.data || []);
|
|
} catch {
|
|
setCustomers([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const filtered = useMemo(() => {
|
|
if (!search.trim()) return customers;
|
|
const q = search.toLowerCase();
|
|
return customers.filter(
|
|
(c: any) =>
|
|
(c.displayName || "")?.toLowerCase().includes(q) ||
|
|
(c.firstName || "")?.toLowerCase().includes(q) ||
|
|
(c.lastName || "")?.toLowerCase().includes(q) ||
|
|
(c.email || "")?.toLowerCase().includes(q) ||
|
|
(c.phone || "")?.toLowerCase().includes(q),
|
|
);
|
|
}, [customers, search]);
|
|
|
|
return (
|
|
<>
|
|
<Pressable
|
|
onPress={openPicker}
|
|
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between bg-card"
|
|
>
|
|
<Text className={`text-xs font-sans-medium ${value ? "text-foreground" : "text-muted-foreground"}`}>
|
|
{value || placeholder || "Select a customer"}
|
|
</Text>
|
|
<ChevronDown size={14} color="#94a3b8" strokeWidth={2.5} />
|
|
</Pressable>
|
|
|
|
<Modal
|
|
visible={show}
|
|
transparent
|
|
animationType="slide"
|
|
onRequestClose={() => setShow(false)}
|
|
>
|
|
<Pressable className="flex-1 bg-black/40" onPress={() => setShow(false)}>
|
|
<View className="flex-1 justify-end">
|
|
<Pressable
|
|
className="bg-card rounded-t-[36px] overflow-hidden border-t-[3px] border-border/20"
|
|
style={{ maxHeight: SCREEN_HEIGHT * 0.8 }}
|
|
onPress={(e) => e.stopPropagation()}
|
|
>
|
|
<View className="px-6 pb-4 pt-4 flex-row justify-between items-center">
|
|
<View className="w-10" />
|
|
<Text className="text-foreground font-sans-bold text-[18px]">Customers</Text>
|
|
<Pressable
|
|
onPress={() => setShow(false)}
|
|
className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center border border-border/10"
|
|
>
|
|
<X size={14} color={isDark ? "#f1f5f9" : "#0f172a"} strokeWidth={2.5} />
|
|
</Pressable>
|
|
</View>
|
|
|
|
<View className="px-5 pb-4">
|
|
<View className="bg-background rounded-[6px] border border-border flex-row items-center px-3.5 py-2.5">
|
|
<Search size={15} color="#94a3b8" strokeWidth={2} />
|
|
<TextInput
|
|
className="flex-1 ml-2.5 text-foreground font-sans-medium text-sm"
|
|
placeholder="Search customers..."
|
|
placeholderTextColor="#94a3b8"
|
|
value={search}
|
|
onChangeText={setSearch}
|
|
/>
|
|
{search.length > 0 && (
|
|
<Pressable onPress={() => setSearch("")}>
|
|
<X size={14} color="#94a3b8" strokeWidth={2.5} />
|
|
</Pressable>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Add New Customer */}
|
|
<View className="px-5 pb-5">
|
|
<Pressable
|
|
onPress={() => { setShow(false); nav.go("customers/create"); }}
|
|
className="bg-primary rounded-[6px] py-3.5 flex-row items-center justify-center gap-2"
|
|
>
|
|
<Plus size={16} color="white" strokeWidth={2.5} />
|
|
<Text className="text-white font-sans-bold text-sm">Add New Customer</Text>
|
|
</Pressable>
|
|
</View>
|
|
|
|
<ScrollView
|
|
className="px-5"
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={{ paddingBottom: 40 }}
|
|
>
|
|
{loading ? (
|
|
<View className="py-8 items-center">
|
|
<ActivityIndicator color="#E46212" size="small" />
|
|
</View>
|
|
) : filtered.length === 0 ? (
|
|
<View className="py-8 items-center">
|
|
<Text className="text-muted-foreground text-sm font-sans-medium">
|
|
{search ? "No customers match your search" : "No customers found"}
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
filtered.map((c: any) => {
|
|
const isCompany = c.type === "COMPANY";
|
|
return (
|
|
<Pressable
|
|
key={c.id}
|
|
onPress={() => {
|
|
setShow(false);
|
|
onSelect({
|
|
name: c.displayName || `${c.firstName || ""} ${c.lastName || ""}`.trim() || c.companyName || "",
|
|
email: c.email || "",
|
|
phone: c.phone || "",
|
|
});
|
|
}}
|
|
className="bg-card rounded-[6px] border border-border p-4 mb-3"
|
|
>
|
|
<View className="flex-row items-center gap-3">
|
|
<View className={`h-9 w-9 rounded-full items-center justify-center ${isCompany ? "bg-blue-500/10" : "bg-primary/10"}`}>
|
|
{isCompany ? (
|
|
<Building2 color="#2563eb" size={16} strokeWidth={2} />
|
|
) : (
|
|
<User color="#E46212" size={16} strokeWidth={2} />
|
|
)}
|
|
</View>
|
|
<View className="flex-1">
|
|
<Text className="text-foreground font-sans-bold text-sm">
|
|
{c.displayName || `${c.firstName || ""} ${c.lastName || ""}`.trim() || c.companyName || "—"}
|
|
</Text>
|
|
{c.email && (
|
|
<Text className="text-muted-foreground text-[11px] font-sans-medium mt-0.5">
|
|
{c.email}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
<View className={`px-2 py-0.5 rounded-[3px] ${isCompany ? "bg-blue-500/10" : "bg-primary/10"}`}>
|
|
<Text className={`text-[8px] font-sans-bold uppercase tracking-widest ${isCompany ? "text-blue-600" : "text-primary"}`}>
|
|
{isCompany ? "Company" : "Individual"}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</Pressable>
|
|
);
|
|
})
|
|
)}
|
|
</ScrollView>
|
|
</Pressable>
|
|
</View>
|
|
</Pressable>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|