Yaltopia-Tickets-App/components/CustomerPicker.tsx
2026-06-17 15:16:40 +03:00

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>
</>
);
}