feat: implement client search hook for phone input validation in CreateSessionModal

This commit is contained in:
2025-06-25 17:10:02 +05:00
parent ee837e5930
commit c82f1dfbb5
4 changed files with 73 additions and 22 deletions
+3
View File
@@ -0,0 +1,3 @@
export default function Selector() {
return <div>Selector</div>;
}
+9 -3
View File
@@ -25,7 +25,14 @@ function ClientModal({ client }: { client: Client }) {
const { mutate: updateClientData, isPending } = useMutation({ const { mutate: updateClientData, isPending } = useMutation({
mutationKey: ["clients", client.id], mutationKey: ["clients", client.id],
mutationFn: () => api.put(`clients/${client.id}`, { json: clientData }), mutationFn: () =>
api.put(`clients/${client.id}`, {
json: {
name: clientData.name,
phone: clientData.phone.replace(/\D/g, ""),
email: clientData.email,
},
}),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["clients"] }); queryClient.invalidateQueries({ queryKey: ["clients"] });
}, },
@@ -58,7 +65,6 @@ function ClientModal({ client }: { client: Client }) {
onChange={(e) => { onChange={(e) => {
setClientData({ ...clientData, name: e.target.value }); setClientData({ ...clientData, name: e.target.value });
}} }}
className="relative"
required required
> >
<span <span
@@ -76,8 +82,8 @@ function ClientModal({ client }: { client: Client }) {
onChange={(e) => { onChange={(e) => {
setClientData({ ...clientData, phone: e.target.value }); setClientData({ ...clientData, phone: e.target.value });
}} }}
className="relative"
required required
mask="+7 (999) 999-99-99"
> >
<span <span
className="absolute top-[1.25vw] left-[17.917vw] size-[1.389vw] text-[#7D7D7D] cursor-pointer" className="absolute top-[1.25vw] left-[17.917vw] size-[1.389vw] text-[#7D7D7D] cursor-pointer"
+14 -19
View File
@@ -11,8 +11,8 @@ import StartSessionIcon from "../icons/StartSessionIcon.tsx";
import Button from "../Button.tsx"; import Button from "../Button.tsx";
import ProjectSelector from "../ProjectSelector.tsx"; import ProjectSelector from "../ProjectSelector.tsx";
import { useQueryClient, useMutation, useQuery } from "@tanstack/react-query"; import { useQueryClient, useMutation, useQuery } from "@tanstack/react-query";
import { useDebounce } from "@uidotdev/usehooks";
import { AnimatePresence, motion } from "motion/react"; import { AnimatePresence, motion } from "motion/react";
import useClientSearch from "../../hooks/useClientSearch.tsx";
interface Props { interface Props {
targetServerId: string | null; targetServerId: string | null;
@@ -24,6 +24,7 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
const [name, setName] = useState<string | null>(client?.name || null); const [name, setName] = useState<string | null>(client?.name || null);
const [phone, setPhone] = useState<string | null>(client?.phone || null); const [phone, setPhone] = useState<string | null>(client?.phone || null);
const [email, setEmail] = useState<string | null>(client?.email || null); const [email, setEmail] = useState<string | null>(client?.email || null);
const [isFullPhone, setIsFullPhone] = useState(false);
const [isSessionExists, setIsSessionExists] = useState(false); const [isSessionExists, setIsSessionExists] = useState(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -43,6 +44,8 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
); );
const [selectedApp, setSelectedApp] = useState<App | null>(null); const [selectedApp, setSelectedApp] = useState<App | null>(null);
const { data, isLoading, error } = useClientSearch(phone);
useEffect(() => { useEffect(() => {
setSelectedApp( setSelectedApp(
selectedServer?.sessions?.[0]?.app || selectedServer?.sessions?.[0]?.app ||
@@ -51,28 +54,15 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
); );
}, [selectedServer]); }, [selectedServer]);
const debouncedPhone = useDebounce(phone, 500);
const { data, isLoading, error } = useQuery({
queryKey: ["get-user-by-phone", debouncedPhone],
queryFn: () =>
api
.get("clients/by-phone", {
searchParams: debouncedPhone ? { phone: debouncedPhone } : {},
})
.json<Client>(),
enabled: !!debouncedPhone,
});
useEffect(() => { useEffect(() => {
if (!error && data) { if (!error && data) {
setName(data.name); setName(data.name);
setEmail(data.email); setEmail(data.email);
} else { } else {
setName(null); setName(isFullPhone ? name : null);
setEmail(null); setEmail(isFullPhone ? email : null);
} }
}, [data, error]); }, [data, error, isFullPhone, name, email]);
const { mutate: createClient } = useMutation({ const { mutate: createClient } = useMutation({
mutationFn: () => mutationFn: () =>
@@ -206,14 +196,19 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
<div className="flex flex-col gap-y-[0.556vw]"> <div className="flex flex-col gap-y-[0.556vw]">
<Input <Input
value={phone || ""} value={phone || ""}
onChange={(e) => setPhone(e.target.value)} onChange={(e) => {
setPhone(e.target.value);
if (e.target.value.replace(/\D/g, "").length === 11) {
setIsFullPhone(true);
}
}}
placeholder="Номер телефона" placeholder="Номер телефона"
mask="+7 (999) 999-99-99" mask="+7 (999) 999-99-99"
required required
isLoading={isLoading} isLoading={isLoading}
/> />
<AnimatePresence> <AnimatePresence>
{phone && ( {isFullPhone && (
<> <>
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
+47
View File
@@ -0,0 +1,47 @@
import { useQuery } from "@tanstack/react-query";
import { useDebounce } from "@uidotdev/usehooks";
import { useEffect, useState } from "react";
import { Client } from "../types/Client";
import api from "../utils/api";
function useClientSearch(phone: string | null) {
const [isSearching, setIsSearching] = useState(false);
const isPhoneComplete = Boolean(
phone && phone.replace(/\D/g, "").length === 11
);
useEffect(() => {
setIsSearching(isPhoneComplete);
}, [isPhoneComplete]);
const debouncedPhone = useDebounce(phone, 500);
const { data, isLoading, error } = useQuery({
queryKey: ["get-user-by-phone", debouncedPhone],
queryFn: () =>
api
.get("clients/by-phone", {
searchParams:
debouncedPhone && debouncedPhone.replace(/\D/g, "").length === 11
? { phone: debouncedPhone.replace(/\D/g, "") }
: {},
})
.json<Client>(),
enabled: Boolean(debouncedPhone && isPhoneComplete),
});
useEffect(() => {
if (!isLoading && isSearching) {
setIsSearching(false);
}
}, [isLoading, isSearching]);
return {
data,
isLoading: isSearching || isLoading,
error,
};
}
export default useClientSearch;