241 lines
9.2 KiB
TypeScript
241 lines
9.2 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, Check } from "@/lib/icons";
|
|
import { api } from "@/lib/api";
|
|
import { useColorScheme } from "nativewind";
|
|
|
|
const { height: SCREEN_HEIGHT } = Dimensions.get("window");
|
|
|
|
interface CustomerData {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
phone: string;
|
|
}
|
|
|
|
interface CustomerPickerProps {
|
|
selectedIds: string[];
|
|
selectedCustomers: CustomerData[];
|
|
onSelect: (ids: string[], customers: CustomerData[]) => void;
|
|
placeholder?: string;
|
|
}
|
|
|
|
export function CustomerPicker({ selectedIds, selectedCustomers, 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 [tempIds, setTempIds] = useState<string[]>(selectedIds);
|
|
const [tempCustomers, setTempCustomers] = useState<CustomerData[]>(selectedCustomers);
|
|
|
|
const openPicker = async () => {
|
|
setShow(true);
|
|
setTempIds(selectedIds);
|
|
setTempCustomers(selectedCustomers);
|
|
setSearch("");
|
|
setLoading(true);
|
|
try {
|
|
const res = await api.customers.getAll({ query: { page: 1, limit: 50 } });
|
|
setCustomers(res?.data || []);
|
|
} catch {
|
|
setCustomers([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const toggleCustomer = (c: any) => {
|
|
const id = String(c.id);
|
|
const name = c.displayName || `${c.firstName || ""} ${c.lastName || ""}`.trim() || c.companyName || "";
|
|
|
|
let newIds: string[];
|
|
let newCustomers: CustomerData[];
|
|
|
|
if (tempIds.includes(id)) {
|
|
newIds = tempIds.filter((i) => i !== id);
|
|
newCustomers = tempCustomers.filter((p) => p.id !== id);
|
|
} else {
|
|
newIds = [...tempIds, id];
|
|
newCustomers = [
|
|
...tempCustomers,
|
|
{ id, name, email: c.email || "", phone: c.phone || "" },
|
|
];
|
|
}
|
|
|
|
setTempIds(newIds);
|
|
setTempCustomers(newCustomers);
|
|
onSelect(newIds, newCustomers);
|
|
};
|
|
|
|
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]);
|
|
|
|
const triggerLabel = selectedIds.length === 0
|
|
? (placeholder || "Select customers")
|
|
: selectedIds.length === 1
|
|
? selectedCustomers[0]?.name || placeholder
|
|
: `${selectedIds.length} customers selected`;
|
|
|
|
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 flex-1 mr-2 ${
|
|
selectedIds.length > 0 ? "text-foreground" : "text-muted-foreground"
|
|
}`}
|
|
numberOfLines={1}
|
|
>
|
|
{triggerLabel}
|
|
</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>
|
|
|
|
<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";
|
|
const isSelected = tempIds.includes(String(c.id));
|
|
return (
|
|
<Pressable
|
|
key={c.id}
|
|
onPress={() => toggleCustomer(c)}
|
|
className="bg-card rounded-[6px] border border-border p-4 mb-3 flex-row items-center"
|
|
>
|
|
<View className="flex-row items-center gap-3 flex-1">
|
|
<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>
|
|
<View
|
|
className={`h-5 w-5 rounded-full border-2 items-center justify-center ml-3 ${
|
|
isSelected ? "bg-primary border-primary" : "border-muted-foreground/40"
|
|
}`}
|
|
>
|
|
{isSelected && <Check size={12} color="white" strokeWidth={3} />}
|
|
</View>
|
|
</Pressable>
|
|
);
|
|
})
|
|
)}
|
|
</ScrollView>
|
|
</Pressable>
|
|
</View>
|
|
</Pressable>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|