diff --git a/index.html b/index.html index 036f055d..337407cb 100644 --- a/index.html +++ b/index.html @@ -8,9 +8,9 @@ - - + + + html, body { - background: rgb(171, 37, 192); /* before page load */ + background: #ab25c0; /* before page load */ + /* background: rgb(171, 37, 192); before page load */ height: 100%; margin: 0; } diff --git a/public/pwa-64x64.png b/public/pwa-64x64.png new file mode 100644 index 00000000..2f8bede1 Binary files /dev/null and b/public/pwa-64x64.png differ diff --git a/src/App.tsx b/src/App.tsx index e28a13b9..924d1dd6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,19 +29,31 @@ const preventPinchZoom = (event: TouchEvent) => { if (event.touches.length > 1) event.preventDefault(); }; - //main file -function App() -{ - useEffect(() => { - const themeColorMetaTag = document.querySelector('meta[name="theme-color"]'); - if (themeColorMetaTag) { - const backgroundColor = getComputedStyle(document.body).backgroundColor; +function App() { + // ====================DON'T DELETE PLEASE========================================== + // useEffect(() => { + // const themeColorMetaTag = document.querySelector( + // 'meta[name="theme-color"]' + // ); + // if (themeColorMetaTag) { + // const backgroundColor = getComputedStyle(document.body).backgroundColor; - // If backgroundColor is transparent or the default color, set it to black - themeColorMetaTag.setAttribute('content', backgroundColor || '#000'); + // // If backgroundColor is transparent or the default color, set it to black + // themeColorMetaTag.setAttribute("content", backgroundColor || "#000"); + // } + // }, []); + + // ============================================================= + useEffect(() => { + const themeColorMetaTag = document.querySelector( + 'meta[name="theme-color"]' + ); + if (themeColorMetaTag) { + themeColorMetaTag.setAttribute("content", "#ab25c0"); // رنگ بنفش ثابت } }, []); + useSamsungBrowserWarning(); const queryClient = new QueryClient({ defaultOptions: { diff --git a/src/apps/appointment/routes/index.ts b/src/apps/appointment/routes/index.ts index a9986278..730a905a 100644 --- a/src/apps/appointment/routes/index.ts +++ b/src/apps/appointment/routes/index.ts @@ -43,6 +43,7 @@ export const appointmentRoutes = Object.freeze({ appointmentHistory: "/appointment-history", reserveAppointment_artistMobile: "/reserve-appointment/:artistMobile", reservedAppointments: "/reserved-appointments", + addReservedCustomer: "/reserved-appointments/add-customer", confirmReservationAppointment: "/confirm-reservation-appointment", confirmReservation: "/confirm-reservation/:reserveId", shareGiftCards: "/share-gift-cards", diff --git a/src/apps/appointment/routes/structure.tsx b/src/apps/appointment/routes/structure.tsx index e22496a5..064b3712 100644 --- a/src/apps/appointment/routes/structure.tsx +++ b/src/apps/appointment/routes/structure.tsx @@ -63,6 +63,7 @@ import ReservedAppointments from "@/apps/new-ui/pages/reserved-appointments"; import ModulesList from "@/apps/new-ui/pages/modules-list"; import CashbacksPage from "@/apps/new-ui/pages/CashBack"; import Transactions from "@/apps/new-ui/pages/transactions"; +import AddReservedCustomer from "@/apps/new-ui/pages/reserved-appointments/+components/AddReservedCustomer"; export const appointmentRoutesStructure = [ // { @@ -327,6 +328,10 @@ export const appointmentRoutesStructure = [ path: appointmentRoutes.reservedAppointments, element: , }, + { + path: appointmentRoutes.addReservedCustomer, + element: , + }, { path: appointmentRoutes.personel, element: , diff --git a/src/apps/new-ui/components/MobilePicker/CostPicker.tsx b/src/apps/new-ui/components/MobilePicker/CostPicker.tsx index a83df2c3..e3e8f16c 100644 --- a/src/apps/new-ui/components/MobilePicker/CostPicker.tsx +++ b/src/apps/new-ui/components/MobilePicker/CostPicker.tsx @@ -3,230 +3,308 @@ import { Modal } from "@/components/ui/modal"; import { Button } from "@/components/ui/button"; interface Props { - value?: string | null; - onChange: (newValue: string) => void; + value?: string | null; + onChange: (newValue: string) => void; } // تابع تبدیل عدد به حروف فارسی const convertToPersianWords = (num: string): string => { - const numericValue = num.replace(/\D/g, ""); - if (!numericValue) return ""; + const numericValue = num.replace(/\D/g, ""); + if (!numericValue) return ""; - const number = parseInt(numericValue, 10); - if (number === 0) return "صفر"; + const number = parseInt(numericValue, 10); + if (number === 0) return "صفر"; - const units = ["", "هزار", "میلیون", "میلیارد"]; - const ones = ["", "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه"]; - const tens = ["", "ده", "بیست", "سی", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود"]; - const hundreds = ["", "صد", "دویست", "سیصد", "چهارصد", "پانصد", "ششصد", "هفتاد", "هشتصد", "نهصد"]; - const teens = ["ده", "یازده", "دوازده", "سیزده", "چهارده", "پانزده", "شانزده", "هفده", "هجده", "نوزده"]; + const units = ["", "هزار", "میلیون", "میلیارد"]; + const ones = ["", "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه"]; + const tens = [ + "", + "ده", + "بیست", + "سی", + "چهل", + "پنجاه", + "شصت", + "هفتاد", + "هشتاد", + "نود", + ]; + const hundreds = [ + "", + "صد", + "دویست", + "سیصد", + "چهارصد", + "پانصد", + "ششصد", + "هفتاد", + "هشتصد", + "نهصد", + ]; + const teens = [ + "ده", + "یازده", + "دوازده", + "سیزده", + "چهارده", + "پانزده", + "شانزده", + "هفده", + "هجده", + "نوزده", + ]; - if (number < 10) return ones[number]; - if (number < 20) return teens[number - 10]; - if (number < 100) { - return tens[Math.floor(number / 10)] + (number % 10 !== 0 ? " و " + ones[number % 10] : ""); - } - if (number < 1000) { - return ( - hundreds[Math.floor(number / 100)] + - (number % 100 !== 0 ? " و " + convertToPersianWords((number % 100).toString()) : "") - ); + if (number < 10) return ones[number]; + if (number < 20) return teens[number - 10]; + if (number < 100) { + return ( + tens[Math.floor(number / 10)] + + (number % 10 !== 0 ? " و " + ones[number % 10] : "") + ); + } + if (number < 1000) { + return ( + hundreds[Math.floor(number / 100)] + + (number % 100 !== 0 + ? " و " + convertToPersianWords((number % 100).toString()) + : "") + ); + } + + let result = ""; + let chunkIndex = 0; + let tempNumber = number; + + while (tempNumber > 0) { + const chunk = tempNumber % 1000; + if (chunk !== 0) { + let chunkText = convertToPersianWords(chunk.toString()); + if (chunkIndex > 0) { + chunkText += " " + units[chunkIndex]; + } + result = chunkText + (result ? " و " + result : ""); } + tempNumber = Math.floor(tempNumber / 1000); + chunkIndex++; + } - let result = ""; - let chunkIndex = 0; - let tempNumber = number; - - while (tempNumber > 0) { - const chunk = tempNumber % 1000; - if (chunk !== 0) { - let chunkText = convertToPersianWords(chunk.toString()); - if (chunkIndex > 0) { - chunkText += " " + units[chunkIndex]; - } - result = chunkText + (result ? " و " + result : ""); - } - tempNumber = Math.floor(tempNumber / 1000); - chunkIndex++; - } - - return result + " تومان"; + return result + " تومان"; }; const CostPicker = ({ value, onChange }: Props) => { - const [modalOpen, setModalOpen] = useState(false); - const [inputValue, setInputValue] = useState(""); - const [numericOnly, setNumericOnly] = useState(""); - const [persianText, setPersianText] = useState(""); - const [modalPosition, setModalPosition] = useState<"center" | "topKeyboard">("center"); + const [modalOpen, setModalOpen] = useState(false); + const [inputValue, setInputValue] = useState(""); + const [numericOnly, setNumericOnly] = useState(""); + const [persianText, setPersianText] = useState(""); + const [modalPosition, setModalPosition] = useState<"center" | "topKeyboard">( + "center" + ); - const handleInputChange = (e: React.ChangeEvent) => { - const raw = e.target.value; - const digits = raw.replace(/\D/g, ""); + // const handleInputChange = (e: React.ChangeEvent) => { + // const raw = e.target.value; + // const digits = raw.replace(/\D/g, ""); - const trimmed = digits.replace(/^0+/, ""); + // const trimmed = digits.replace(/^0+/, ""); - if (!trimmed) { - setInputValue(""); - setNumericOnly(""); - setPersianText(""); - return; + // if (!trimmed) { + // setInputValue(""); + // setNumericOnly(""); + // setPersianText(""); + // return; + // } + + // const formattedValue = trimmed.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + + // setInputValue(formattedValue); + // setNumericOnly(trimmed); + + // setPersianText(convertToPersianWords(trimmed)); + // }; + + const handleInputChange = (e: React.ChangeEvent) => { + const raw = e.target.value; + + // 🟣 تبدیل اعداد فارسی و عربی به انگلیسی + const normalized = raw + .replace(/[۰-۹]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 1728)) + .replace(/[٠-٩]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 1632)); + + const digits = normalized.replace(/\D/g, ""); + const trimmed = digits.replace(/^0+/, ""); + + if (!trimmed) { + setInputValue(""); + setNumericOnly(""); + setPersianText(""); + return; + } + + const formattedValue = trimmed.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + setInputValue(formattedValue); + setNumericOnly(trimmed); + setPersianText(convertToPersianWords(trimmed)); + }; + + const isValid = (): boolean => { + return numericOnly.length >= 4 && !isNaN(Number(numericOnly)); + }; + + const onSubmit = () => { + if (!isValid()) return; + onChange(`${numericOnly}`); + setModalOpen(false); + }; + + useEffect(() => { + if (!modalOpen) return; + + const updateModalPosition = () => { + if (window.visualViewport) { + const visualViewport = window.visualViewport; + + if (visualViewport.height < window.screen.height * 0.7) { + setModalPosition("topKeyboard"); + } else { + setModalPosition("center"); } - - const formattedValue = trimmed.replace(/\B(?=(\d{3})+(?!\d))/g, ","); - - setInputValue(formattedValue); - setNumericOnly(trimmed); - - setPersianText(convertToPersianWords(trimmed)); + } }; - const isValid = (): boolean => { - return numericOnly.length >= 4 && !isNaN(Number(numericOnly)); + if (window.visualViewport) { + window.visualViewport.addEventListener("resize", updateModalPosition); + window.visualViewport.addEventListener("scroll", updateModalPosition); + } + + updateModalPosition(); + + return () => { + if (window.visualViewport) { + window.visualViewport.removeEventListener( + "resize", + updateModalPosition + ); + window.visualViewport.removeEventListener( + "scroll", + updateModalPosition + ); + } }; + }, [modalOpen]); - const onSubmit = () => { - if (!isValid()) return; - onChange(`${numericOnly}`); - setModalOpen(false); - }; + useEffect(() => { + if (modalOpen && value) { + // مقدار ورودی رو بدون صفرهای ابتدایی و با فرمت نمایش می‌دیم + const cleaned = value.replace(/\D/g, "").replace(/^0+/, ""); + const formattedValue = cleaned + ? cleaned.replace(/\B(?=(\d{3})+(?!\d))/g, ",") + : ""; + setInputValue(formattedValue); + setNumericOnly(cleaned); + setPersianText(cleaned ? convertToPersianWords(cleaned) : ""); + } else if (modalOpen) { + setInputValue(""); + setNumericOnly(""); + setPersianText(""); + } + }, [modalOpen, value]); - useEffect(() => { - if (!modalOpen) return; + return ( + <> +
setModalOpen(true)} + className={`h-20 w-20 shadow-md rounded-xl border-2 flex flex-col items-center justify-center cursor-pointer ${ + value + ? "border-[#AA00FF66] text-[#AA00FF]" + : "border-[#F7EEFF] text-[#2D0A48]" + }`} + > + {value ? ( + <> +

+ {Number(value).toLocaleString()} +

+ + ) : ( + <> +

+

+

دلخواه

+ + )} +
- const updateModalPosition = () => { - if (window.visualViewport) { - const visualViewport = window.visualViewport; + setModalOpen(false)} + className="p-6 rounded-lg w-full max-w-sm" + position={modalPosition} + > +
+ {/* عنوان */} +
+

مقدار تومان

+

+ مبلغ مورد نظر را وارد کنید +

+
- if (visualViewport.height < window.screen.height * 0.7) { - setModalPosition("topKeyboard"); - } else { - setModalPosition("center"); - } - } - }; - - if (window.visualViewport) { - window.visualViewport.addEventListener("resize", updateModalPosition); - window.visualViewport.addEventListener("scroll", updateModalPosition); - } - - updateModalPosition(); - - return () => { - if (window.visualViewport) { - window.visualViewport.removeEventListener("resize", updateModalPosition); - window.visualViewport.removeEventListener("scroll", updateModalPosition); - } - }; - }, [modalOpen]); - - useEffect(() => { - if (modalOpen && value) { - // مقدار ورودی رو بدون صفرهای ابتدایی و با فرمت نمایش می‌دیم - const cleaned = value.replace(/\D/g, "").replace(/^0+/, ""); - const formattedValue = cleaned ? cleaned.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : ""; - setInputValue(formattedValue); - setNumericOnly(cleaned); - setPersianText(cleaned ? convertToPersianWords(cleaned) : ""); - } else if (modalOpen) { - setInputValue(""); - setNumericOnly(""); - setPersianText(""); - } - }, [modalOpen, value]); - - return ( - <> -
setModalOpen(true)} - className={`h-20 w-20 shadow-md rounded-xl border-2 flex flex-col items-center justify-center cursor-pointer ${ - value ? "border-[#AA00FF66] text-[#AA00FF]" : "border-[#F7EEFF] text-[#2D0A48]" - }`} - > - {value ? ( - <> -

{Number(value).toLocaleString()}

- - ) : ( - <> -

+

-

دلخواه

- - )} + {/* اینپوت برای وارد کردن مقدار */} +
+
+ + + تومان +
- setModalOpen(false)} - className="p-6 rounded-lg w-full max-w-sm" - position={modalPosition} + {/* پیام خطا / راهنما */} + {!isValid() && numericOnly.length > 0 && ( +

+ مبلغ باید حداقل ۴ رقم باشد. +

+ )} + + {/* نمایش مبلغ به حروف */} + {persianText && isValid() && ( +
+

+ {persianText} +

+
+ )} +
+ + {/* دکمه‌های تایید و لغو */} +
+ - -
-
- - - ); + لغو + + +
+ +
+ + ); }; export default CostPicker; diff --git a/src/apps/new-ui/components/MobilePicker/MobileDayPicker.tsx b/src/apps/new-ui/components/MobilePicker/MobileDayPicker.tsx index ea236f8e..f2c08f18 100644 --- a/src/apps/new-ui/components/MobilePicker/MobileDayPicker.tsx +++ b/src/apps/new-ui/components/MobilePicker/MobileDayPicker.tsx @@ -30,8 +30,13 @@ const MobileDayPicker = ({ value, onChange }: Props) => { days: value?.replace(" روز", "") || "7", }); + // const onSubmit = () => { + // onChange(`${pickerValue.days}`); + // setModalOpen(false); + // }; + const onSubmit = () => { - onChange(`${pickerValue.days}`); + onChange(pickerValue.days); setModalOpen(false); }; @@ -41,7 +46,7 @@ const MobileDayPicker = ({ value, onChange }: Props) => { days: value.replace(" روز", ""), }); } - }, [modalOpen]); + }, [modalOpen, value]); // تولید اعداد از 1 تا 365 const dayOptions = Array.from({ length: 365 }, (_, i) => (i + 1).toString()); diff --git a/src/apps/new-ui/pages/CustomerClub/index.tsx b/src/apps/new-ui/pages/CustomerClub/index.tsx index 3b9504d9..29e26664 100644 --- a/src/apps/new-ui/pages/CustomerClub/index.tsx +++ b/src/apps/new-ui/pages/CustomerClub/index.tsx @@ -30,7 +30,7 @@ const CustomerClub = () => { > rose { - const { mutate: createGiftCard, isPending } = useCreateGiftCard(); - const navigate = useNavigate(); + const { mutate: createGiftCard, isPending } = useCreateGiftCard(); + const navigate = useNavigate(); - // state - const [selectedDay, setSelectedDay] = useState(null); - const [customDay, setCustomDay] = useState(null); - const [defaultGiftCard, setDefaultGiftCard] = useState(banners[0].banner); - const [selected, setSelected] = useState(100); - const [customAmount, setCustomAmount] = useState(null); - const [selectedValue, setSelectedValue] = useState(""); - const [cardType, setCardType] = useState("type5"); - const [options, setOptions] = useState(STATIC_GIFT_CARD_TEXTS); - const [selectedCardId, setSelectedCardId] = useState(banners[0].id); - const [selectedService, setSelectedService] = useState(() => localStorage.getItem("serviceTitle") || ""); + // state + const [selectedDay, setSelectedDay] = useState( + null + ); + const [customDay, setCustomDay] = useState(null); + const [defaultGiftCard, setDefaultGiftCard] = useState( + banners[0].banner + ); + const [selected, setSelected] = useState( + 100 + ); + const [customAmount, setCustomAmount] = useState(null); + const [selectedValue, setSelectedValue] = useState(""); + const [cardType, setCardType] = useState("type5"); + const [options, setOptions] = useState(STATIC_GIFT_CARD_TEXTS); + const [selectedCardId, setSelectedCardId] = useState(banners[0].id); + const [selectedService, setSelectedService] = useState( + () => localStorage.getItem("serviceTitle") || "" + ); - // sync other tabs - useEffect(() => { - const handleStorage = () => setSelectedService(localStorage.getItem("serviceTitle") || ""); - window.addEventListener("storage", handleStorage); - return () => window.removeEventListener("storage", handleStorage); - }, []); + // sync other tabs + useEffect(() => { + const handleStorage = () => + setSelectedService(localStorage.getItem("serviceTitle") || ""); + window.addEventListener("storage", handleStorage); + return () => window.removeEventListener("storage", handleStorage); + }, []); - // cleanup if left page - useEffect(() => { - return () => { - if (!window.location.pathname.includes("/gift-card")) localStorage.removeItem("serviceTitle"); - }; - }, []); + // cleanup if left page + useEffect(() => { + return () => { + if (!window.location.pathname.includes("/gift-card")) + localStorage.removeItem("serviceTitle"); + }; + }, []); - // derived - const displayedAmount = useMemo(() => computeDisplayedAmount(selected, customAmount), [selected, customAmount]); - const expiryText = useMemo(() => computeExpireText(selectedDay, customDay), [selectedDay, customDay]); + // derived + const displayedAmount = useMemo( + () => computeDisplayedAmount(selected, customAmount), + [selected, customAmount] + ); + const expiryText = useMemo( + () => computeExpireText(selectedDay, customDay), + [selectedDay, customDay] + ); - const btnTrigger = useMemo(() => { - return ( - selectedValue.length > 0 && - selectedService.length > 0 && - !!selectedDay && - !!selectedCardId && - customDay === null && - !!selected && - selected.toString().length > 0 && - selected.toString().length <= 6 - ); - }, [selectedValue, selectedService, selectedDay, selectedCardId, customDay, selected]); + const btnTrigger = useMemo(() => { + const hasValidDay = + (selectedDay === "custom" && customDay !== null) || + (typeof selectedDay === "number" && selectedDay > 0); - // handlers (passed to child components) - const handleSelectCard = useCallback((bannerId: number, bannerUrl: string, bannerCardType: any) => { - setDefaultGiftCard(bannerUrl); - setSelectedCardId(bannerId); - setCardType(bannerCardType); - }, []); - - const handleAddOption = useCallback((opt: string) => { - setOptions((prev) => [opt, ...prev]); - }, []); - - // Amount handlers - const onAmountPickerChange = useCallback((newValue: string | null) => { - if (!newValue) return; - const value = parseInt(newValue, 10); - if (value === 0) { - toast.error("مبلغ کارت هدیه نمی‌تواند ۰ باشد."); - return; - } - if (value.toString().length > 6) { - toast.error("مبلغ کارت هدیه نمی‌تواند بیش از 9 رقم باشد."); - return; - } - setCustomAmount(value); - setSelected("custom"); - }, []); - - const chooseAmount = useCallback((amount: number) => setSelected(amount), []); - // Day handlers - const onDayPickerChange = useCallback((newValue: string) => { - const dayValue = parseInt(newValue, 10); - setCustomDay(dayValue); - setSelectedDay("custom"); - }, []); - const chooseDay = useCallback((day: number) => setSelectedDay(day), []); - - const submit = useCallback(() => { - const amountString = - selected === "custom" && customAmount !== null - ? (customAmount * 1000).toString() - : typeof selected === "number" - ? (selected * 1000).toString() - : "0"; - - const expire_date = - selectedDay === "custom" && customDay !== null - ? getExpiryDate(customDay) - : typeof selectedDay === "number" - ? getExpiryDate(selectedDay) - : ""; - - createGiftCard({ - amount: amountString, - text: selectedValue || "", - service: selectedService || "", - expire_date, - card_type: cardType, - }); - }, [selected, customAmount, selectedDay, customDay, selectedValue, selectedService, cardType, createGiftCard]); + const hasValidAmount = + (selected === "custom" && customAmount !== null && customAmount > 0) || + (typeof selected === "number" && selected > 0); return ( -
-
-
- کارت هدیه -
-
- - - -
- - - - - -
- -
-
-
- خدمات -
- -
- - navigate("/")} - onSubmit={submit} - disabled={!btnTrigger} - isPending={isPending} - /> -
+ selectedValue.length > 0 && + selectedService.length > 0 && + hasValidDay && + !!selectedCardId && + hasValidAmount ); + }, [ + selectedValue, + selectedService, + selectedDay, + selectedCardId, + customDay, + selected, + customAmount, + ]); + + // handlers (passed to child components) + const handleSelectCard = useCallback( + (bannerId: number, bannerUrl: string, bannerCardType: any) => { + setDefaultGiftCard(bannerUrl); + setSelectedCardId(bannerId); + setCardType(bannerCardType); + }, + [] + ); + + const handleAddOption = useCallback((opt: string) => { + setOptions((prev) => [opt, ...prev]); + }, []); + + // Amount handlers + const onAmountPickerChange = useCallback((newValue: string | null) => { + if (!newValue) return; + + // تبدیل اعداد فارسی و عربی به انگلیسی + const normalized = newValue + .replace(/[۰-۹]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 1728)) + .replace(/[٠-٩]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 1632)); + + const digits = normalized.replace(/\D/g, ""); + const value = parseInt(digits, 10); + + if (!value || value <= 0) { + toast.error("مبلغ کارت هدیه نمی‌تواند ۰ باشد."); + return; + } + + if (!value || value.toString().length > 6) { + toast.error("مبلغ کارت هدیه نمی‌تواند بیش از 9 رقم باشد."); + return; + } + + setCustomAmount(value); + setSelected("custom"); + }, []); + + const chooseAmount = useCallback((amount: number) => setSelected(amount), []); + // Day handlers + const onDayPickerChange = useCallback((newValue: string) => { + const dayValue = parseInt(newValue, 10); + setCustomDay(dayValue); + setSelectedDay("custom"); + }, []); + const chooseDay = useCallback((day: number) => { + setSelectedDay(day); + setCustomDay(null); + }, []); + + const submit = useCallback(() => { + const amountString = + selected === "custom" && customAmount !== null + ? (customAmount ).toString() + : typeof selected === "number" + ? (selected * 1000).toString() + : "0"; + + const expire_date = + selectedDay === "custom" && customDay !== null + ? getExpiryDate(customDay) + : typeof selectedDay === "number" + ? getExpiryDate(selectedDay) + : ""; + + createGiftCard({ + amount: amountString, + text: selectedValue || "", + service: selectedService || "", + expire_date, + card_type: cardType, + }); + }, [ + selected, + customAmount, + selectedDay, + customDay, + selectedValue, + selectedService, + cardType, + createGiftCard, + ]); + + return ( +
+
+
+ کارت هدیه +
+
+ + + +
+ + + + + +
+ +
+
+
+ خدمات +
+ +
+ + navigate("/")} + onSubmit={submit} + disabled={!btnTrigger} + isPending={isPending} + /> +
+ ); }; export default React.memo(CreateGiftCard); diff --git a/src/apps/new-ui/pages/invite-friends/index.tsx b/src/apps/new-ui/pages/invite-friends/index.tsx index ae977199..a58dc9de 100644 --- a/src/apps/new-ui/pages/invite-friends/index.tsx +++ b/src/apps/new-ui/pages/invite-friends/index.tsx @@ -88,7 +88,7 @@ const InviteFriends = () => { > rose { > rose; +} + +const AddReservedCustomer: React.FC = ({ + prefillName, + prefillPhone, +}) => { + const [searchParams] = useSearchParams(); + const queryName = searchParams.get("name") || ""; + const queryPhone = searchParams.get("phone") || ""; + const [name, setName] = useState(prefillName || queryName); + const [phoneNumber, setPhoneNumber] = useState(prefillPhone || queryPhone); + const [statusTitle, setStatusTitle] = useState(""); + const [birthDate, setBirthDate] = useState(""); + const [sendAskPhotoSMS, setSendAskPhotoSMS] = useState(false); + const [isSelectPhotoOpen, setIsSelectPhotoOpen] = useState(false); + const [profilePicture, setProfilePicture] = useState(addPhoto); + const fileInputRef = useRef(null); + const [isAddStatusOpen, setIsAddStatusOpen] = useState(false); + const [photoFile, setPhotoFile] = useState(null); + + const navigate = useNavigate(); + + const btnEnable = name !== "" && phoneNumber !== ""; + + const addCustomerMutation = useAddSingleCustomer(); + + const statusList = [ + { id: 1, title: "VIP", icon: general }, + { id: 2, title: "عمومی", icon: family }, + { id: 3, title: "آشنایان", icon: VIP }, + ]; + + const handleChangePhoto = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + const base64 = reader.result as string; + setProfilePicture(base64); + setPhotoFile(file); + }; + reader.readAsDataURL(file); // تبدیل به base64 + } + }; + + const handleDeletePhoto = () => { + setProfilePicture(addPhoto); + }; + + const handleSaveCustomer = async () => { + if (!btnEnable) return; + + const formData = new FormData(); + formData.append("name", name); + formData.append("phone_number", phoneNumber); + formData.append("customer_type", statusTitle || "عمومی"); + formData.append("description", sendAskPhotoSMS ? "true" : "false"); + formData.append("date_of_birth", birthDate || ""); + + if (photoFile) { + formData.append("profile_picture", photoFile); + } + + try { + await addCustomerMutation.mutateAsync(formData); + + navigate( + `${appointmentRoutes.advanceAppointment}?header=${encodeURIComponent( + "تعیین ساعت رزرو نوبت" + )}` + ); + } catch (err) { + console.error("خطا در ذخیره مشتری:", err); + } + }; + + useEffect(() => { + return () => { + localStorage.removeItem("birthDate"); + }; + }, []); + + const handlePickContact = async () => { + try { + const navigatorWithContacts = navigator as typeof navigator & { + contacts?: ContactsManager; + }; + + if (navigatorWithContacts.contacts) { + const props = ["name", "tel"]; + const opts = { multiple: false }; + const contacts = await navigatorWithContacts.contacts.select( + props, + opts + ); + + if (contacts.length > 0) { + const contact = contacts[0]; + setName(contact.name?.[0] || ""); + + const rawNumber = contact.tel?.[0] || ""; + const cleanedNumber = rawNumber.replace(/\D/g, ""); + setPhoneNumber(cleanedNumber); + } + } else { + showErrorAlert("این مرورگر از انتخاب مخاطب پشتیبانی نمی‌کند."); + } + } catch (err) { + console.error(err); + } + }; + return ( +
+
+

+ افزودن مشتری جدید +

+ + {/* =======profile pic========= */} +
setIsSelectPhotoOpen(true)} + className="mt-4 w-24 h-24 rounded-full cursor-pointer" + > + add +
+ + {/* =================== */} +
+ add customer +

+ افزودن مشتری از مخاطبین +

+
+ + {/* ==========name=========== */} +
+ setName(e.target.value)} + type="text" + required + maxLength={24} + className="w-full rounded-full border-2 h-10 border-gray-300 p-4 text-sm" + placeholder="نام و نام خانوادگی" + /> +
+ + {/* =================phone number=========== */} +
+ setPhoneNumber(e.target.value)} + digitsOnly={true} + maxLength={14} + dir="ltr" + required + className="w-full rounded-full border-2 border-gray-300 h-10 p-4 text-sm text-right" + placeholder="شماره موبایل" + /> +
+
+ + {/* =============status============== */} +
+

دسته‌بندی

+
setIsAddStatusOpen(true)} + className="flex items-center justify-center gap-2 border-2 w-[90px] border-[#A780C3] rounded-full py-2 cursor-pointer" + > + add customer +

افزودن

+
+
+ +
+ {statusList.map((status) => ( +
setStatusTitle(status.title)} + > + status +

{status.title}

+
+ ))} +
+ + {/* ===========birth date=========== */} +
+ setBirthDate(newDate)} + /> +
+ + {/* ==========send sms============ */} +
+ setSendAskPhotoSMS(!sendAskPhotoSMS)} + label="ارسال پیامک درخواست تصویر کاربری از مشتری" + /> +
+ + {/* ============buttons=========== */} +
+ + +
+ + {/* ==========select pic============= */} + {isSelectPhotoOpen && ( +
+
+
setIsSelectPhotoOpen(false)} + className="absolute cursor-pointer bg-[#dedee3] text-[#999999] text-2xl rounded-full p-2 top-2 right-2" + > + +
+ +
+

مدیریت تصویر مشتری

+ customer photo + +
+ + + + + +
+ + +
+
+
+ )} + + {isAddStatusOpen && ( +
+
+
setIsAddStatusOpen(false)} + className="absolute cursor-pointer bg-[#dedee3] text-[#999999] text-2xl rounded-full p-2 top-2 right-2 " + > + +
+ setIsAddStatusOpen(false)} /> +
+
+ )} +
+ ); +}; + +export default AddReservedCustomer; diff --git a/src/apps/new-ui/pages/reserved-appointments/+components/AppointmentDetail.tsx b/src/apps/new-ui/pages/reserved-appointments/+components/AppointmentDetail.tsx index 4deeace5..4700563a 100644 --- a/src/apps/new-ui/pages/reserved-appointments/+components/AppointmentDetail.tsx +++ b/src/apps/new-ui/pages/reserved-appointments/+components/AppointmentDetail.tsx @@ -7,6 +7,7 @@ import { appointmentRoutes } from "@appointment/routes"; import { formatPersianDate } from "@/utils/date-time"; import { IReservedAppointment } from "@/apps/appointment/utils/types"; import { useGetAllCustomers } from "@/apps/new-ui/services/Customer"; +import { showWarningAlert } from "@/apps/new-ui/components/CustomAlert"; interface AppointmentDetailProps { apt: IReservedAppointment; @@ -27,6 +28,7 @@ const AppointmentDetail: React.FC = ({ const foundCustomer = data?.customers.find( (customer) => customer.phone_number === apt.phone_number ); + return ( {isOpen && ( @@ -151,20 +153,37 @@ const AppointmentDetail: React.FC = ({ ) : ( <> -
+
diff --git a/src/apps/new-ui/pages/reserved-appointments/+components/AppointmentRequest.tsx b/src/apps/new-ui/pages/reserved-appointments/+components/AppointmentRequest.tsx index de65f0d3..d72f5d08 100644 --- a/src/apps/new-ui/pages/reserved-appointments/+components/AppointmentRequest.tsx +++ b/src/apps/new-ui/pages/reserved-appointments/+components/AppointmentRequest.tsx @@ -3,13 +3,12 @@ import AppointmentDetail from "./AppointmentDetail"; import { useState } from "react"; import avatar from "../../../assets/profilePhoto.svg"; import status from "../../../assets/appointment/status.svg"; -import { Check, Copy, XIcon } from "lucide-react"; +import { Check, XIcon } from "lucide-react"; import { IReservedAppointment } from "@/apps/appointment/utils/types"; import { appointmentRoutes } from "@/apps/appointment/routes"; import { showWarningAlert } from "@/apps/new-ui/components/CustomAlert"; import { useGetAllCustomers } from "@/apps/new-ui/services/Customer"; import { useNavigate } from "react-router-dom"; -import { useClipboard } from "@/apps/new-ui/Utils/copyToClipboard"; const AppointmentRequest = ({ selectedDateAppointment, @@ -17,7 +16,6 @@ const AppointmentRequest = ({ selectedDateAppointment: IReservedAppointment[]; }) => { const { data } = useGetAllCustomers(); - const { copied, copy } = useClipboard(2000); const navigate = useNavigate(); const [isOpen, setIsOpen] = useState(false); const [selectedApt, setSelectedApt] = useState( @@ -33,18 +31,6 @@ const AppointmentRequest = ({ }); }; - const onCopyPhone = async (text?: string) => { - if (!text) return; - try { - const ok = await copy(text); - if (!ok) { - console.warn("Copy failed via utility for text:", text); - } - } catch (err) { - console.warn("Copy threw:", err); - } - }; - return (
{selectedDateAppointment.map((apt, index) => { @@ -91,24 +77,6 @@ const AppointmentRequest = ({

{apt.phone_number}

- - {!foundCustomer?.phone_number && ( - - )}
@@ -137,13 +105,30 @@ const AppointmentRequest = ({