diff --git a/android.keystore b/android.keystore deleted file mode 100644 index 76491275..00000000 Binary files a/android.keystore and /dev/null differ diff --git a/package.json b/package.json index dbf76e3c..4ae0f397 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "app.salona", "private": true, - "version": "3.122.486", + "version": "3.122.490", "type": "module", "scripts": { "dev": "vite --host", diff --git a/public/.well-known/assetlinks.json b/public/.well-known/assetlinks.json index cedf1f13..e7884e71 100644 --- a/public/.well-known/assetlinks.json +++ b/public/.well-known/assetlinks.json @@ -3,17 +3,10 @@ "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", - "package_name": "ir.mysalona.app.twa", + "package_name": "ir.mysalona.testapp.twa", "sha256_cert_fingerprints": [ - "2B:15:95:EC:CD:A0:FB:4F:E6:8B:4A:C1:98:7A:54:14:D8:BD:A9:3E:2D:F0:5A:E6:F7:31:CD:EE:EF:6E:16:6E" + "7B:32:AD:94:28:6B:BA:72:B6:56:34:13:19:B4:B0:8A:72:3A:EC:7E:31:89:9B:D4:16:64:74:7D:93:3A:F9:16" ] } - }, - { - "relation": ["check_validation"], - "target": { - "namespace": "cafebazaar_twa", - "package_name": "ir.mysalona.app.twa" - } } ] diff --git a/signing.keystore b/signing.keystore new file mode 100644 index 00000000..026eef34 Binary files /dev/null and b/signing.keystore differ diff --git a/src/apps/new-ui/Utils/removeLocalStorage.ts b/src/apps/new-ui/Utils/removeLocalStorage.ts new file mode 100644 index 00000000..aa6a76b1 --- /dev/null +++ b/src/apps/new-ui/Utils/removeLocalStorage.ts @@ -0,0 +1,56 @@ +export function clearExceptPredicate(predicate: any) { + for (let i = localStorage.length - 1; i >= 0; i--) { + const key: any = localStorage.key(i); + if (!predicate(key)) { + try { + localStorage.removeItem(key); + } catch (err) { + console.error("Failed to remove key", key, err); + } + } + } +} +type ClearResult = { + removed: string[]; + kept: string[]; + backupPreview: string[]; +}; + +export function clearExceptKeys(keysToKeep: string[] = []): ClearResult { + if (typeof window === "undefined" || !window.localStorage) { + return { removed: [], kept: [], backupPreview: [] }; + } + + const keepSet = new Set(keysToKeep.map(String)); + + // گرفتن snapshot از تمام کلیدها (با بررسی null) + const allKeys: string[] = []; + for (let i = 0; i < localStorage.length; i++) { + const k = localStorage.key(i); + if (k !== null) allKeys.push(k); + } + + // بکاپ (پیشنهاد می‌کنم فقط preview بگیریم یا در حافظه نگه داریم) + const backup: Record = {}; + allKeys.forEach((k) => { + backup[k] = localStorage.getItem(k); + }); + + const removed: string[] = []; + const kept: string[] = []; + + allKeys.forEach((key) => { + if (keepSet.has(key)) { + kept.push(key); + } else { + try { + localStorage.removeItem(key); + removed.push(key); + } catch (err) { + console.error("Failed to remove", key, err); + } + } + }); + + return { removed, kept, backupPreview: Object.keys(backup) }; +} diff --git a/src/apps/new-ui/Utils/selectedServiceStore.ts b/src/apps/new-ui/Utils/selectedServiceStore.ts new file mode 100644 index 00000000..d2f1838b --- /dev/null +++ b/src/apps/new-ui/Utils/selectedServiceStore.ts @@ -0,0 +1,60 @@ +// src/utils/selectedServiceStore.ts +const STORAGE_KEY = "serviceCost"; +const TITLE_KEY = "serviceTitle"; + +type Selected = { title: string; price: string }; + +const emitter = new EventTarget(); +let current: Selected | null = null; + +const toDigits = (s = "") => { + if (!s) return ""; + return s.replace(/,/g, "").replace(/[^\d]/g, ""); +}; + +export function setSelectedService(payload: Partial & { price?: any }) { + try { + const title = (payload.title ?? current?.title ?? "") as string; + const priceRaw = payload.price ?? current?.price ?? ""; + const price = String(priceRaw ?? ""); + current = { title, price }; + try { + localStorage.setItem(TITLE_KEY, title); + } catch {} + try { + localStorage.setItem(STORAGE_KEY, toDigits(price)); + } catch {} + // emit with detail in normalized form + emitter.dispatchEvent(new CustomEvent("selectedServiceChanged", { detail: { title, price: toDigits(price) } })); + // also emit legacy event for compatibility + setTimeout(() => { + try { + window.dispatchEvent(new CustomEvent("serviceSelected", { detail: { title, price: toDigits(price) } })); + } catch {} + }, 0); + } catch (err) { + console.error("selectedServiceStore.setSelectedService error:", err); + } +} + +export function getSelectedService(): Selected { + if (current) return current; + try { + const title = localStorage.getItem(TITLE_KEY) ?? ""; + const price = toDigits(localStorage.getItem(STORAGE_KEY) ?? ""); + current = { title, price }; + return current; + } catch { + return { title: "", price: "" }; + } +} + +export function onSelectedServiceChange(cb: (s: Selected) => void) { + const handler = (e: Event) => { + const ev = e as CustomEvent; + cb(ev.detail); + }; + emitter.addEventListener("selectedServiceChanged", handler); + // return unsubscribe + return () => emitter.removeEventListener("selectedServiceChanged", handler); +} diff --git a/src/apps/new-ui/components/AppointmentComponents/Services.tsx b/src/apps/new-ui/components/AppointmentComponents/Services.tsx index c7511f8b..fc430bf5 100644 --- a/src/apps/new-ui/components/AppointmentComponents/Services.tsx +++ b/src/apps/new-ui/components/AppointmentComponents/Services.tsx @@ -38,7 +38,7 @@ const Services = () => { }; }, [isAddServiceOpen]); - // When user selects a service via UI, dispatch 'serviceSelected' event (ServiceCost will apply it) + // When user selects a service via UI const handleServiceClick = (service: Service) => { const originalIndex = allServices.indexOf(service); if (service.name === selectedSlide?.name && originalIndex === selectedSlideIndex) { @@ -48,13 +48,13 @@ const Services = () => { setSelectedSlide(service); setSelectedSlideIndex(originalIndex >= 0 ? originalIndex : null); - // save title to localStorage (so tab/refresh retains selection) + // save title to localStorage const title = service.name ?? ""; try { localStorage.setItem(TITLE_KEY, title); } catch {} - // persist price to localStorage (so parents/components reading storage see the value) + // persist price to localStorage try { const priceStr = String(service.price ?? ""); localStorage.setItem(STORAGE_KEY, priceStr); @@ -64,7 +64,7 @@ const Services = () => { console.warn("writing price to storage failed:", err); } - // dispatch a dedicated event to signal that user explicitly selected a service + // dispatch event try { const ev = new CustomEvent("serviceSelected", { detail: { title, price: service.price ?? "" }, @@ -79,7 +79,7 @@ const Services = () => { return allServices.filter((service) => service.name.toLowerCase().includes(searchTerm.trim().toLowerCase())); }, [allServices, searchTerm]); - // keep selectedSlideIndex in sync with selectedSlide and allServices + // keep selectedSlideIndex in sync useEffect(() => { if (!allServices || allServices.length === 0) { setSelectedSlideIndex(null); @@ -90,82 +90,82 @@ const Services = () => { else setSelectedSlideIndex(null); }, [allServices, selectedSlide?.name]); - // initial selection when services list changes: - // priority: - // 1) editAppointmentService (if present) - // 2) saved serviceTitle in localStorage - // 3) default -> first service in the list - // - // Important: do NOT overwrite user's manual edited cost if EDITED_FLAG === "true" -useEffect(() => { - if (!allServices || allServices.length === 0) { - setSelectedSlide(null); - setSelectedSlideIndex(null); - return; - } - - const appointmentService = localStorage.getItem("editAppointmentService"); - if (appointmentService) { - const found = allServices.find((service) => service.name === appointmentService); - if (found) { - setSelectedSlide(found); - setSelectedSlideIndex(allServices.indexOf(found)); - try { localStorage.setItem(TITLE_KEY, found.name ?? ""); } catch {} - // persist price + dispatch on next tick so listeners are ready - if (localStorage.getItem(EDITED_FLAG) !== "true") { - try { - const priceStr = String(found.price ?? ""); - setTimeout(() => { - try { localStorage.setItem(STORAGE_KEY, priceStr); } catch {} - try { - window.dispatchEvent( - new CustomEvent("serviceSelected", { - detail: { title: found.name, price: found.price ?? "" }, - }), - ); - } catch {} - }, 0); - localStorage.removeItem(EDITED_FLAG); - } catch {} - } + // Initial selection logic + useEffect(() => { + if (!allServices || allServices.length === 0) { + setSelectedSlide(null); + setSelectedSlideIndex(null); return; } - } - const savedTitle = localStorage.getItem(TITLE_KEY); - if (savedTitle) { - const foundBySaved = allServices.find((service) => service.name === savedTitle); - if (foundBySaved) { - setSelectedSlide(foundBySaved); - setSelectedSlideIndex(allServices.indexOf(foundBySaved)); - try { localStorage.setItem(TITLE_KEY, foundBySaved.name ?? ""); } catch {} - if (localStorage.getItem(EDITED_FLAG) !== "true") { - try { - const priceStr = String(foundBySaved.price ?? ""); - setTimeout(() => { - try { localStorage.setItem(STORAGE_KEY, priceStr); } catch {} - try { - window.dispatchEvent( - new CustomEvent("serviceSelected", { - detail: { title: foundBySaved.name, price: foundBySaved.price ?? "" }, - }), - ); - } catch {} - }, 0); - localStorage.removeItem(EDITED_FLAG); - } catch {} + // 1. Check Edit Appointment Mode + const appointmentService = localStorage.getItem("editAppointmentService"); + if (appointmentService) { + const found = allServices.find((service) => service.name === appointmentService); + if (found) { + setSelectedSlide(found); + setSelectedSlideIndex(allServices.indexOf(found)); + try { localStorage.setItem(TITLE_KEY, found.name ?? ""); } catch {} + + // Only respect manual edits in Edit Mode + if (localStorage.getItem(EDITED_FLAG) !== "true") { + try { + const priceStr = String(found.price ?? ""); + setTimeout(() => { + try { localStorage.setItem(STORAGE_KEY, priceStr); } catch {} + try { + window.dispatchEvent( + new CustomEvent("serviceSelected", { + detail: { title: found.name, price: found.price ?? "" }, + }), + ); + } catch {} + }, 0); + localStorage.removeItem(EDITED_FLAG); + } catch {} + } + return; } - return; } - } - // default: first service - const first = allServices[0]; - if (first) { - setSelectedSlide(first); - setSelectedSlideIndex(0); - try { localStorage.setItem(TITLE_KEY, first.name ?? ""); } catch {} - if (localStorage.getItem(EDITED_FLAG) !== "true") { + // 2. Check Saved Title (Previous selection) + const savedTitle = localStorage.getItem(TITLE_KEY); + if (savedTitle) { + const foundBySaved = allServices.find((service) => service.name === savedTitle); + if (foundBySaved) { + setSelectedSlide(foundBySaved); + setSelectedSlideIndex(allServices.indexOf(foundBySaved)); + try { localStorage.setItem(TITLE_KEY, foundBySaved.name ?? ""); } catch {} + + if (localStorage.getItem(EDITED_FLAG) !== "true") { + try { + const priceStr = String(foundBySaved.price ?? ""); + setTimeout(() => { + try { localStorage.setItem(STORAGE_KEY, priceStr); } catch {} + try { + window.dispatchEvent( + new CustomEvent("serviceSelected", { + detail: { title: foundBySaved.name, price: foundBySaved.price ?? "" }, + }), + ); + } catch {} + }, 0); + localStorage.removeItem(EDITED_FLAG); + } catch {} + } + return; + } + } + + // 3. Default: First Service (Fix applied here) + const first = allServices[0]; + if (first) { + setSelectedSlide(first); + setSelectedSlideIndex(0); + try { localStorage.setItem(TITLE_KEY, first.name ?? ""); } catch {} + + // FIX: Always force price update for default fallback and CLEAR the edit flag. + // This ensures if there is only 1 service, the price is always set correctly. try { const priceStr = String(first.price ?? ""); setTimeout(() => { @@ -178,11 +178,11 @@ useEffect(() => { ); } catch {} }, 0); + // Important: We are falling back to default, so any "previous edit" is invalid. localStorage.removeItem(EDITED_FLAG); } catch {} } - } -}, [allServices]); + }, [allServices]); return (
@@ -194,7 +194,6 @@ useEffect(() => {
) : ( - // {/* Search Section */}
@@ -210,7 +209,6 @@ useEffect(() => {
)} - {/* Services Swiper */}
{ ); }; -export default Services; \ No newline at end of file +export default Services; diff --git a/src/apps/new-ui/pages/CheckTimeSlotCustomer/index.tsx b/src/apps/new-ui/pages/CheckTimeSlotCustomer/index.tsx index ba53ce40..421bd3d8 100644 --- a/src/apps/new-ui/pages/CheckTimeSlotCustomer/index.tsx +++ b/src/apps/new-ui/pages/CheckTimeSlotCustomer/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState, useMemo } from "react"; +import { useCallback, useEffect, useRef, useState, useMemo, useLayoutEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; import moment from "jalali-moment"; import toast from "react-hot-toast"; @@ -25,6 +25,7 @@ import GuidedOverlay from "@/apps/new-ui/components/GuidedOverlay/GuidedOverlay" import { useEndUserCreateReservation, useGetArtistFreeTimes } from "../../services/NewTest"; import AppointmentTimeClickable from "../../components/AppointmentComponents/AppointmentTimeClickable"; import { newAppointmentRoutes } from "../../routes"; +import { clearExceptKeys } from "../../Utils/removeLocalStorage"; type AppMap = Record; @@ -65,6 +66,10 @@ export default function CheckTimeSlotCustomer(): JSX.Element { const { data: customLink, isPending } = useGetCustomLink(customId); const navigate = useNavigate(); + useLayoutEffect(() => { + clearExceptKeys(["accounts", "userConfig"]); + }, []); + // customer const { data: customerData } = useGetCustomerForEndUser(+(customLink?.customer_id ?? 0), !!customLink?.customer_id); const [customerName, setCustomerName] = useState(undefined); diff --git a/src/apps/new-ui/pages/CheckTimeSlotGeneral/index.tsx b/src/apps/new-ui/pages/CheckTimeSlotGeneral/index.tsx index 61c78ccd..0142b6e5 100644 --- a/src/apps/new-ui/pages/CheckTimeSlotGeneral/index.tsx +++ b/src/apps/new-ui/pages/CheckTimeSlotGeneral/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useCallback, useState, useRef, useMemo } from "react"; +import { useEffect, useCallback, useState, useRef, useMemo, useLayoutEffect } from "react"; import toast from "react-hot-toast"; import { getJalaliDaysInMonth, @@ -25,6 +25,7 @@ import GuidedOverlay from "@/apps/new-ui/components/GuidedOverlay/GuidedOverlay" import SubmitPage from "./+components/SubmitPage"; import ServicesQA from "@/apps/new-ui/components/AppointmentComponents/ServicesQA"; import AppointmentTimeClickable from "../../components/AppointmentComponents/AppointmentTimeClickable"; +import { clearExceptKeys } from "../../Utils/removeLocalStorage"; type AppMap = Record; const STORAGE_KEY = "appointmentTimesMap"; @@ -74,6 +75,10 @@ const findFirstValidDay = ( export default function CheckTimeSlotGeneral() { const todayRef = useRef(getTodayJalali()); + useLayoutEffect(() => { + clearExceptKeys(["accounts","userConfig"]); + }, []); + const [selectedDateIso, setSelectedDateIso] = useState(() => { try { if (typeof window === "undefined") return null; diff --git a/src/apps/new-ui/pages/CreateTimeTable/+components/HolidayManagement.tsx b/src/apps/new-ui/pages/CreateTimeTable/+components/HolidayManagement.tsx index 72cc466f..5ed0b113 100644 --- a/src/apps/new-ui/pages/CreateTimeTable/+components/HolidayManagement.tsx +++ b/src/apps/new-ui/pages/CreateTimeTable/+components/HolidayManagement.tsx @@ -473,7 +473,6 @@ const HolidayManagement: React.FC = () => { if (currentStep === 2) { handleCustomerTimeChange(true); } - }; const [currentStepIndex, setCurrentStepIndex] = useState(0); @@ -536,7 +535,7 @@ const HolidayManagement: React.FC = () => { setIsGuideOpen(true); } }, []); - + useEffect(() => { if (isHolidaySelected) setStep([ @@ -577,7 +576,7 @@ const HolidayManagement: React.FC = () => { return (
{/* Guided overlay */} - {isGuideOpen && ( + {isGuideOpen && userInfo?.current_user.services && userInfo?.current_user.services.length !== 0 && ( { diff --git a/src/apps/new-ui/pages/PersonnelCheckTimeSlotCustomer/index.tsx b/src/apps/new-ui/pages/PersonnelCheckTimeSlotCustomer/index.tsx index 6ed62f83..9d67249a 100644 --- a/src/apps/new-ui/pages/PersonnelCheckTimeSlotCustomer/index.tsx +++ b/src/apps/new-ui/pages/PersonnelCheckTimeSlotCustomer/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import moment from "jalali-moment"; import toast from "react-hot-toast"; @@ -29,6 +29,7 @@ import { useCreateReserveEndUser, useGetMonthTimeslotsEndUser2 } from "../../ser import AppointmentTimeClickable from "../../components/AppointmentComponents/AppointmentTimeClickable"; import { newAppointmentRoutes } from "../../routes"; import GuidedOverlay from "../../components/GuidedOverlay/GuidedOverlay"; +import { clearExceptKeys } from "../../Utils/removeLocalStorage"; type AppMap = Record; @@ -69,6 +70,10 @@ export default function PersonnelCheckTimeSlotCustomer(): JSX.Element { const { data: customLink } = useSearchCustomLinkPersonel(customId); const navigate = useNavigate(); + useLayoutEffect(() => { + clearExceptKeys(["accounts", "userConfig"]); + }, []); + // customer const { data: customerData } = useGetCustomerForEndUser(+(customLink?.customer_id ?? 0), !!customLink?.customer_id); const [customerName, setCustomerName] = useState(undefined); diff --git a/src/apps/new-ui/pages/PersonnelCheckTimeSlotGeneral/index.tsx b/src/apps/new-ui/pages/PersonnelCheckTimeSlotGeneral/index.tsx index 69406734..2a9d8f3b 100644 --- a/src/apps/new-ui/pages/PersonnelCheckTimeSlotGeneral/index.tsx +++ b/src/apps/new-ui/pages/PersonnelCheckTimeSlotGeneral/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useCallback, useState, useRef, useMemo } from "react"; +import { useEffect, useCallback, useState, useRef, useMemo, useLayoutEffect } from "react"; import toast from "react-hot-toast"; import { getJalaliDaysInMonth, @@ -24,6 +24,7 @@ import "shepherd.js/dist/css/shepherd.css"; import { XIcon } from "lucide-react"; import AppointmentTimeClickable from "../../components/AppointmentComponents/AppointmentTimeClickable"; import GuidedOverlay from "@/apps/new-ui/components/GuidedOverlay/GuidedOverlay"; +import { clearExceptKeys } from "../../Utils/removeLocalStorage"; type AppMap = Record; const STORAGE_KEY = "appointmentTimesMap"; @@ -76,6 +77,10 @@ export default function PersonnelCheckTimeSlotGeneral() { const todayRef = useRef(getTodayJalali()); const { year: todayYear, month: todayMonth, day: todayDay } = todayRef.current; + useLayoutEffect(() => { + clearExceptKeys(["accounts", "userConfig"]); + }, []); + // ---- selected date (ISO gregorian) ---- const [selectedDateIso, setSelectedDateIso] = useState(() => { try { diff --git a/src/apps/new-ui/pages/ServicesMangement/+components/AddService.tsx b/src/apps/new-ui/pages/ServicesMangement/+components/AddService.tsx index e19b75d6..09fc0635 100644 --- a/src/apps/new-ui/pages/ServicesMangement/+components/AddService.tsx +++ b/src/apps/new-ui/pages/ServicesMangement/+components/AddService.tsx @@ -64,7 +64,7 @@ const AddService = () => { // قیمت اجباری تنها زمانی که user personnel نیست const priceIsValid = isUserAbilitiesPersonnel ? true : price > 0; - const btnEnable = addServiceTitle.trim() !== "" && !isPending && priceIsValid; + const btnEnable = addServiceTitle.trim() !== "" && !isPending && priceIsValid && time; // (اگر خواستی، می‌تونی isValidDuration(time) رو هم اضافه کنی) // ========= handlers =========