diff --git a/package.json b/package.json index 9df96da4..ed90760d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "app.salona", "private": true, - "version": "2.86.4", + "version": "2.87.4", "type": "module", "scripts": { "dev": "vite --host", @@ -109,7 +109,8 @@ "typescript": "~5.6.3", "typescript-eslint": "8.11.0", "vite": "5.4.10", - "vite-plugin-pwa": "0.21.0" + "vite-plugin-pwa": "0.21.0", + "vite-tsconfig-paths": "^5.1.4" }, "resolutions": { "sharp": "0.32.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c315145d..fdcc5124 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -304,6 +304,9 @@ importers: vite-plugin-pwa: specifier: 0.21.0 version: 0.21.0(@vite-pwa/assets-generator@0.2.6)(vite@5.4.10(@types/node@22.9.0)(lightningcss@1.30.1)(terser@5.44.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.6.3)(vite@5.4.10(@types/node@22.9.0)(lightningcss@1.30.1)(terser@5.44.0)) packages: @@ -3731,6 +3734,9 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + goober@2.1.18: resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} peerDependencies: @@ -5274,6 +5280,16 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -5446,6 +5462,14 @@ packages: '@vite-pwa/assets-generator': optional: true + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + vite@5.4.10: resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -9347,6 +9371,8 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 + globrex@0.1.2: {} + goober@2.1.18(csstype@3.1.3): dependencies: csstype: 3.1.3 @@ -11015,6 +11041,10 @@ snapshots: ts-interface-checker@0.1.13: {} + tsconfck@3.1.6(typescript@5.6.3): + optionalDependencies: + typescript: 5.6.3 + tslib@2.8.1: {} tunnel-agent@0.6.0: @@ -11205,6 +11235,17 @@ snapshots: transitivePeerDependencies: - supports-color + vite-tsconfig-paths@5.1.4(typescript@5.6.3)(vite@5.4.10(@types/node@22.9.0)(lightningcss@1.30.1)(terser@5.44.0)): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.6.3) + optionalDependencies: + vite: 5.4.10(@types/node@22.9.0)(lightningcss@1.30.1)(terser@5.44.0) + transitivePeerDependencies: + - supports-color + - typescript + vite@5.4.10(@types/node@22.9.0)(lightningcss@1.30.1)(terser@5.44.0): dependencies: esbuild: 0.21.5 diff --git a/src/apps/appointment/routes/index.ts b/src/apps/appointment/routes/index.ts index 3603f16e..5e4d0fd9 100644 --- a/src/apps/appointment/routes/index.ts +++ b/src/apps/appointment/routes/index.ts @@ -16,7 +16,7 @@ export const appointmentRoutes = Object.freeze({ EditIncomeAndCost: "/income-and-cost/edit/:id", ChartReport: "/chart-report", Factor: "/factor", - + cashBack: '/cash-back-list', appointments: "/appointments", appointments_config: "/appointments/config", appointments_config_id: "/appointments/config/:id", diff --git a/src/apps/appointment/routes/structure.tsx b/src/apps/appointment/routes/structure.tsx index 2b3bf813..130b6b24 100644 --- a/src/apps/appointment/routes/structure.tsx +++ b/src/apps/appointment/routes/structure.tsx @@ -62,6 +62,7 @@ import AddCustomerPage from "@/apps/new-ui/pages/CustomerClub/+components/AddCus import ReserveAppointmentForm from "@/apps/new-ui/pages/reserved-appointments/reserve-form"; 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"; export const appointmentRoutesStructure = [ // { @@ -306,6 +307,10 @@ export const appointmentRoutesStructure = [ path: appointmentRoutes.inviteFriends, element: , }, + { + path: appointmentRoutes.cashBack, + element: , + }, { path: appointmentRoutes.smsSubscription, element: , diff --git a/src/apps/appointment/services/CashBack.tsx b/src/apps/appointment/services/CashBack.tsx new file mode 100644 index 00000000..d0b1bf1c --- /dev/null +++ b/src/apps/appointment/services/CashBack.tsx @@ -0,0 +1,92 @@ +import { toast } from "react-hot-toast"; +import { appointmentClient } from "@/utils/axios-interceptor"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { appointmentQueryKeys } from "../utils/queryKeys"; + +export type CashbackItem = { + id?: number; + code: string; + amount: number; + expire_date: string; // e.g. "1403-09-10" or ISO + used?: boolean; + creation_time?: string; + customer_phone_number?: string; +}; + +export const useGetUserCashbacks = () => { + return useQuery({ + queryKey: [appointmentQueryKeys.CASHBACKS], + queryFn: async (): Promise => { + const { data } = await appointmentClient.get("/get_user_cashbacks"); + return data; + }, + }); +}; + + +export const useCreateCashback = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (payload: { amount?: number; percentage?: number; expire_days?: number }) => + await appointmentClient.post("/create_cashback", payload), + onSuccess: (response) => { + if (response?.status >= 200 && response?.status < 300) { + toast.success(response.data?.message ?? "کش‌بک با موفقیت ایجاد شد"); + queryClient.invalidateQueries({ queryKey: [appointmentQueryKeys.CASHBACKS] }); + } + }, + }); +}; + +export const useGetCashbackByCode = (code?: string) => { + return useQuery({ + queryKey: [appointmentQueryKeys.CASHBACKS, "single", code], + queryFn: async (): Promise => { + const { data } = await appointmentClient.get(`/get_cashback/${encodeURIComponent(code ?? "")}`); + return data; + }, + enabled: !!code, + }); +}; + + +export const useDeleteCashback = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (code: string) => + await appointmentClient.delete(`/delete_cashback/${encodeURIComponent(code)}`), + onSuccess: (response) => { + if (response?.status >= 200 && response?.status < 300) { + toast.success(response.data?.message ?? "کش‌بک حذف شد"); + queryClient.invalidateQueries({ queryKey: [appointmentQueryKeys.CASHBACKS] }); + } + }, + }); +}; + +export const useUseCashback = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (code: string) => await appointmentClient.put(`/use_cashback/${encodeURIComponent(code)}`), + onSuccess: (response, code) => { + if (response?.status >= 200 && response?.status < 300) { + toast.success(response.data?.message ?? "کش‌بک استفاده شد"); + queryClient.invalidateQueries({ queryKey: [appointmentQueryKeys.CASHBACKS] }); + queryClient.invalidateQueries({ queryKey: [appointmentQueryKeys.CASHBACKS, "single", code] }); + } + }, + }); +}; + +export const useSearchCashbacksByPhone = (phone?: string) => { + return useQuery({ + queryKey: [appointmentQueryKeys.CASHBACKS, "search", phone], + queryFn: async (): Promise => { + const { data } = await appointmentClient.get( + `/search_cashbacks_by_phone/${encodeURIComponent(phone ?? "")}` + ); + return data; + }, + enabled: !!phone, + }); +}; diff --git a/src/apps/appointment/services/Notification.tsx b/src/apps/appointment/services/Notification.tsx index 29946083..7185cecd 100644 --- a/src/apps/appointment/services/Notification.tsx +++ b/src/apps/appointment/services/Notification.tsx @@ -1,46 +1,35 @@ -import axios from 'axios'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import { appointmentQueryKeys } from '../utils/queryKeys'; +import axios from "axios"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { appointmentQueryKeys } from "../utils/queryKeys"; export const useCheckSubscriptionUserNotification = ({ - endpoint, - phone_number, + endpoint, + phone_number, }: { - endpoint?: string; - phone_number?: string; + endpoint?: string; + phone_number?: string; }) => { - return useQuery({ - queryKey: [appointmentQueryKeys.SUBSCRIPTION_USER_NOTIFICATION], - queryFn: async (): Promise< - { message: string; subscribed: boolean } | undefined - > => { - const { data } = await axios.post( - 'https://webpush.mysalona.ir/check_subscription', - { - endpoint, - phone_number, + return useQuery({ + queryKey: [appointmentQueryKeys.SUBSCRIPTION_USER_NOTIFICATION], + queryFn: async (): Promise<{ message: string; subscribed: boolean } | undefined> => { + const { data } = await axios.post("https://webpush.mysalona.ir/check_subscription", { + endpoint, + phone_number, + }); + return data; }, - ); - return data; - }, - enabled: !!endpoint && !!phone_number, - }); + enabled: !!endpoint && !!phone_number, + }); }; export const useSubscribeUserNotification = () => { - return useMutation({ - mutationFn: async (requestBody: { - endpoint?: string; - phone_number?: string; - }) => - await axios.post('https://webpush.mysalona.ir/subscribe', requestBody), - }); + return useMutation({ + mutationFn: async (requestBody: { endpoint?: string; phone_number?: string }) => + await axios.post("https://webpush.mysalona.ir/subscribe", requestBody), + }); }; export const useUnSubscribeUserNotification = () => { - return useMutation({ - mutationFn: async (requestBody: { - endpoint?: string; - phone_number?: string; - }) => - await axios.post('https://webpush.mysalona.ir/unsubscribe', requestBody), - }); + return useMutation({ + mutationFn: async (requestBody: { endpoint?: string; phone_number?: string }) => + await axios.post("https://webpush.mysalona.ir/unsubscribe", requestBody), + }); }; diff --git a/src/apps/appointment/utils/queryKeys.ts b/src/apps/appointment/utils/queryKeys.ts index 18ea16d5..1a4a5ba3 100644 --- a/src/apps/appointment/utils/queryKeys.ts +++ b/src/apps/appointment/utils/queryKeys.ts @@ -1,38 +1,39 @@ export const appointmentQueryKeys = Object.freeze({ - ADS: "ADS", - APPOINTMENT_LIST: "APPOINTMENT_LIST", - USER_INFO: "USER_INFO", - SMS_TRANSACTIONS: "SMS_TRANSACTIONS", - CUSTOMERS: "CUSTOMERS", - ADD_SINGLE_CUSTOMERS: "ADD_SINGLE_CUSTOMERS", - CUSTOMERS_APPOINTMENT: "CUSTOMERS_APPOINTMENT", - APPOINTMENT_DETAIL: "APPOINTMENT_DETAIL", - PRODUCTS: "PRODUCTS", - TRANSACTIONS: "TRANSACTIONS", - VERIFY_PAYMENT: "VERIFY_PAYMENT", - REPAIRS: "REPAIRS", - TICKETS: "TICKETS", - USER_REPAIRS: "USER_REPAIRS", - RESERVE_APPOINTMENT_REQUESTS: "RESERVE_APPOINTMENT_REQUESTS", - SUBSCRIPTION_USER_NOTIFICATION: "SUBSCRIPTION_USER_NOTIFICATION", - LOYALTY_PATTERNS: "LOYALTY_PATTERNS", - LOYALTY_CARDS: "LOYALTY_CARDS", - SURVEYFROMS: "SURVEYFROMS", - SURVEY_QUESTION_FROMS: "SURVEY_QUESTION_FROMS", - ACCOUNTING: "ACCOUNTING", - SMS_PATTERNS: "SMS_PATTERNS", - PERSONEL: "PERSONEL", - PERSONEL_ACCESS: "PERSONEL_ACCESS", - PRIZES: "PRIZES", - ROSE_FULL_REPORT: "ROSE_FULL_REPORT", - MUSIC_LIST: "MUSIC_LIST", - ACADEMY_MOVIES: "ACADEMY_MOVIES", - ACADEMY_Test: " ACADEMY_Test", - SPECIALTY_LIST: " SPECIALTY_LIST", - USER_CONFIG: "USER_CONFIG", - EVENT_SAVED: "EVENT_SAVED", - EVENT_LIST: "EVENT_LIST", - EVENT_DETAIL:"EVENT_DETAIL", - REPORT_AT_A_GLANCE: "REPORT_AT_A_GLANCE", - SURVEY_AVERAGE: "SURVEY_AVERAGE", + ADS: "ADS", + APPOINTMENT_LIST: "APPOINTMENT_LIST", + USER_INFO: "USER_INFO", + SMS_TRANSACTIONS: "SMS_TRANSACTIONS", + CASHBACKS: "CASHBACKS", + CUSTOMERS: "CUSTOMERS", + ADD_SINGLE_CUSTOMERS: "ADD_SINGLE_CUSTOMERS", + CUSTOMERS_APPOINTMENT: "CUSTOMERS_APPOINTMENT", + APPOINTMENT_DETAIL: "APPOINTMENT_DETAIL", + PRODUCTS: "PRODUCTS", + TRANSACTIONS: "TRANSACTIONS", + VERIFY_PAYMENT: "VERIFY_PAYMENT", + REPAIRS: "REPAIRS", + TICKETS: "TICKETS", + USER_REPAIRS: "USER_REPAIRS", + RESERVE_APPOINTMENT_REQUESTS: "RESERVE_APPOINTMENT_REQUESTS", + SUBSCRIPTION_USER_NOTIFICATION: "SUBSCRIPTION_USER_NOTIFICATION", + LOYALTY_PATTERNS: "LOYALTY_PATTERNS", + LOYALTY_CARDS: "LOYALTY_CARDS", + SURVEYFROMS: "SURVEYFROMS", + SURVEY_QUESTION_FROMS: "SURVEY_QUESTION_FROMS", + ACCOUNTING: "ACCOUNTING", + SMS_PATTERNS: "SMS_PATTERNS", + PERSONEL: "PERSONEL", + PERSONEL_ACCESS: "PERSONEL_ACCESS", + PRIZES: "PRIZES", + ROSE_FULL_REPORT: "ROSE_FULL_REPORT", + MUSIC_LIST: "MUSIC_LIST", + ACADEMY_MOVIES: "ACADEMY_MOVIES", + ACADEMY_Test: " ACADEMY_Test", + SPECIALTY_LIST: " SPECIALTY_LIST", + USER_CONFIG: "USER_CONFIG", + EVENT_SAVED: "EVENT_SAVED", + EVENT_LIST: "EVENT_LIST", + EVENT_DETAIL: "EVENT_DETAIL", + REPORT_AT_A_GLANCE: "REPORT_AT_A_GLANCE", + SURVEY_AVERAGE: "SURVEY_AVERAGE", }); diff --git a/src/apps/appointment/utils/types.ts b/src/apps/appointment/utils/types.ts index bb8394b2..5000d7e0 100644 --- a/src/apps/appointment/utils/types.ts +++ b/src/apps/appointment/utils/types.ts @@ -41,6 +41,9 @@ export interface IUserAbility { } export interface IUserSettings { + default_cashback_expire_days : number; + default_cashback_percentage : number; + default_cashback_active: boolean; default_appointment_pattern: string | null; default_appointment_sms: boolean; default_reminder_pattern: string | null; diff --git a/src/apps/new-ui/components/AppointmentComponents/ToggleButton.tsx b/src/apps/new-ui/components/AppointmentComponents/ToggleButton.tsx index 722b4d32..998c6015 100644 --- a/src/apps/new-ui/components/AppointmentComponents/ToggleButton.tsx +++ b/src/apps/new-ui/components/AppointmentComponents/ToggleButton.tsx @@ -1,32 +1,28 @@ interface ToggleButtonType { - booleanState: boolean; - onclick: () => void; - label?: string; + booleanState: boolean; + onclick: () => void; + label?: string; } -const ToggleButton: React.FC = ({ - booleanState, - onclick, - label, -}) => { - return ( -
- +const ToggleButton: React.FC = ({ booleanState, onclick, label }) => { + return ( +
+ -

{label}

-
- ); +

{label}

+
+ ); }; export default ToggleButton; diff --git a/src/apps/new-ui/components/HeaderWithBubbles/index.tsx b/src/apps/new-ui/components/HeaderWithBubbles/index.tsx new file mode 100644 index 00000000..f8b5939e --- /dev/null +++ b/src/apps/new-ui/components/HeaderWithBubbles/index.tsx @@ -0,0 +1,105 @@ +import { useMemo } from "react"; + +type Props = { + text: string; + bubbleCount?: number; +}; + +export default function HeaderWithBubbles({ text, bubbleCount = 128 }: Props): JSX.Element { + const bubbles = useMemo(() => { + return Array.from({ length: bubbleCount }).map(() => { + const size = 2 + Math.random() * 2; + const distance = Math.random(); + const position = -5 + Math.random() * 110; + const time = 10 + Math.random() * 2; + const delay = -5 * (2 + Math.random() * 2); + return { + ["--size"]: `${size}rem`, + ["--distance"]: `${distance}rem`, + ["--position"]: `${position}%`, + ["--time"]: `${time}s`, + ["--delay"]: `${delay}s`, + } as React.CSSProperties; + }); + }, [bubbleCount]); + + return ( +
+
+ {bubbles.map((style, i) => ( +
+ ))} +
+ +
+

{text}

+
+ + + + + + + + + + + +
+ ); +} diff --git a/src/apps/new-ui/pages/AdvancedTurn/+components/Calendar/CalendarGrid.tsx b/src/apps/new-ui/pages/AdvancedTurn/+components/Calendar/CalendarGrid.tsx index 67b1a067..7f03545b 100644 --- a/src/apps/new-ui/pages/AdvancedTurn/+components/Calendar/CalendarGrid.tsx +++ b/src/apps/new-ui/pages/AdvancedTurn/+components/Calendar/CalendarGrid.tsx @@ -10,9 +10,18 @@ interface Props { closedWeekdaySet: Set; todayIso: string; onSelect: (iso: string, dateObj: DateObject) => void; + allowPastSelection?: boolean; } -export default function CalendarGrid({ cells, selectedIso, counts, closedWeekdaySet, todayIso, onSelect }: Props) { +export default function CalendarGrid({ + cells, + selectedIso, + counts, + closedWeekdaySet, + todayIso, + onSelect, + allowPastSelection = false, +}: Props) { return (
{cells.map((cell, idx) => { @@ -40,6 +49,7 @@ export default function CalendarGrid({ cells, selectedIso, counts, closedWeekday isToday={isToday} count={dayCount} onSelect={onSelect} + allowPastSelection={allowPastSelection} />
); diff --git a/src/apps/new-ui/pages/AdvancedTurn/+components/Calendar/DayCell.tsx b/src/apps/new-ui/pages/AdvancedTurn/+components/Calendar/DayCell.tsx index 9209cbf8..1a1deb5f 100644 --- a/src/apps/new-ui/pages/AdvancedTurn/+components/Calendar/DayCell.tsx +++ b/src/apps/new-ui/pages/AdvancedTurn/+components/Calendar/DayCell.tsx @@ -10,6 +10,7 @@ interface Props { isToday: boolean; count?: number; onSelect: (iso: string, dateObj: DateObject) => void; + allowPastSelection?: boolean; } export default function DayCell({ @@ -22,24 +23,37 @@ export default function DayCell({ isToday, count = 0, onSelect, + allowPastSelection = false, }: Props) { + const pastIsDisabled = isPast && !allowPastSelection; + return (
onSelect(iso, dateObj)} + onClick={() => { + // if (pastIsDisabled) return; + onSelect(iso, dateObj); + }} role="button" - tabIndex={0} + tabIndex={pastIsDisabled ? -1 : 0} onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") onSelect(iso, dateObj); + if ((e.key === "Enter" || e.key === " ") && !pastIsDisabled) { + onSelect(iso, dateObj); + } }} className={`relative rounded-lg h-14 flex items-center justify-center select-none ${ - isPast ? "opacity-40 pointer-events-none" : "" + pastIsDisabled ? "opacity-40" : "" }`} + aria-disabled={pastIsDisabled} >
diff --git a/src/apps/new-ui/pages/AdvancedTurn/+components/Calendar/MobileCalendarSingleMonth.tsx b/src/apps/new-ui/pages/AdvancedTurn/+components/Calendar/MobileCalendarSingleMonth.tsx index 21616c62..49083717 100644 --- a/src/apps/new-ui/pages/AdvancedTurn/+components/Calendar/MobileCalendarSingleMonth.tsx +++ b/src/apps/new-ui/pages/AdvancedTurn/+components/Calendar/MobileCalendarSingleMonth.tsx @@ -18,6 +18,7 @@ interface Props { onSelect?: (isoDateGregorian: string) => void; appointmentsMap?: AppMap; weekendIndices?: any; + allowPastSelection?: boolean; // <-- جدید، پیش‌فرض false } const DEFAULT_WEEKEND_INDICES = [6, 5] as const; @@ -29,6 +30,7 @@ export default function MobileCalendarSingleMonth({ onSelect, appointmentsMap, weekendIndices = DEFAULT_WEEKEND_INDICES, + allowPastSelection = false, }: Props) { const today = useMemo(() => new DateObject({ calendar: persian, locale: persian_fa }), []); const todayIso = useMemo(() => isoFromDateObj(today), [today]); @@ -57,28 +59,24 @@ export default function MobileCalendarSingleMonth({ else if (Array.isArray(parsed?.firstMonth)) arr = parsed.firstMonth; const normalized = arr.map((v) => Number(v)).filter((n) => Number.isFinite(n) && n >= 0 && n <= 6); setHolidays(normalized); - } catch { - // ignore - } + } catch {} }, [holidaysFromStorageKey]); - // ------------ FIXED buildCountsFromAppMap: iterate days of the PERSIAN month ------------ + // buildCountsFromAppMap const buildCountsFromAppMap = useCallback((appMap: AppMap | undefined, dMonth: DateObject) => { const m: CountsMap = {}; if (!appMap || !dMonth) return m; - // get first day of that displayMonth in persian and number of days in that persian month const first = new DateObject(dMonth).setDay(1); - // fallback for daysInMonth // @ts-ignore + // fallback for daysInMonth const daysInMonth = Number(first.daysInMonth || first.month.length || first.getDaysInMonth?.() || 30); for (let day = 1; day <= daysInMonth; day++) { - const dt = new DateObject(dMonth).setDay(day); // this respects persian calendar + const dt = new DateObject(dMonth).setDay(day); // respects persian calendar const iso = isoFromDateObj(dt); // YYYY-MM-DD (gregorian) as you store keys const arr = appMap[iso]; if (Array.isArray(arr) && arr.length > 0) { - // count unique non-empty entries const uniqueCount = Array.from(new Set(arr.filter(Boolean))).length; if (uniqueCount > 0) m[iso] = uniqueCount; } @@ -86,7 +84,6 @@ export default function MobileCalendarSingleMonth({ return m; }, []); - // load counts when displayMonth or appointmentsMap changes useEffect(() => { let cancelled = false; const ym = displayMonth; @@ -115,7 +112,6 @@ export default function MobileCalendarSingleMonth({ }; }, [displayMonth, getCountsForMonth, appointmentsMap, buildCountsFromAppMap]); - // stable weekend indices const weekendIndicesFinal = useMemo(() => weekendIndices ?? DEFAULT_WEEKEND_INDICES, [weekendIndices]); const holidaysSet = useMemo(() => new Set(holidays), [holidays]); @@ -158,7 +154,8 @@ export default function MobileCalendarSingleMonth({ const storedWeekIdx = storedDO.weekDay.index; const storedIsPast = isPastDateObject(storedDO); const storedIsClosed = closedWeekdaySet.has(storedWeekIdx); - if (!storedIsPast && !storedIsClosed) finalIso = stored; + // اگر تاریخ ذخیره شده گذشته باشه، فقط وقتی می‌پذیریم که allowPastSelection باشه + if ((!storedIsPast || allowPastSelection) && !storedIsClosed) finalIso = stored; } } catch {} } @@ -166,7 +163,8 @@ export default function MobileCalendarSingleMonth({ if (!finalIso) { let candidate = new DateObject({ calendar: persian, locale: persian_fa }); for (let i = 0; i < 365; i++) { - if (!closedWeekdaySet.has(candidate.weekDay.index) && !isPastDateObject(candidate)) { + const candidateIsPast = isPastDateObject(candidate); + if (!closedWeekdaySet.has(candidate.weekDay.index) && (allowPastSelection || !candidateIsPast)) { finalIso = isoFromDateObj(candidate); break; } @@ -198,7 +196,7 @@ export default function MobileCalendarSingleMonth({ return new DateObject({ date: selDate, calendar: persian, locale: persian_fa }).setDay(1); }); } catch {} - }, [closedWeekdaySet, todayIso, isPastDateObject]); + }, [closedWeekdaySet, todayIso, isPastDateObject, allowPastSelection]); const gotoPrevMonth = useCallback(() => { setDisplayMonth((m) => { @@ -216,8 +214,8 @@ export default function MobileCalendarSingleMonth({ // build cells for the grid const first = useMemo(() => new DateObject(displayMonth).setDay(1), [displayMonth]); - // fallback for days in month // @ts-ignore + // fallback for days in month const daysInMonth = Number(first.daysInMonth || first.month.length || first.getDaysInMonth?.() || 30); const startWeekIndex = first.weekDay.index; @@ -241,7 +239,11 @@ export default function MobileCalendarSingleMonth({ new Date(js.getFullYear(), js.getMonth(), js.getDate()) < new Date(now.getFullYear(), now.getMonth(), now.getDate()); const isClosed = closedWeekdaySet.has(dateObj.weekDay.index); - if (isPast) return; + console.log(isPast && !allowPastSelection); + if (isPast && !allowPastSelection) { + toast.error("انتخاب روزهای گذشته مجاز نیست."); + return; + } if (isClosed) { toast.error("این روز تعطیل است."); return; @@ -254,7 +256,7 @@ export default function MobileCalendarSingleMonth({ onSelectRef.current?.(iso); } catch {} }, - [closedWeekdaySet] + [closedWeekdaySet, allowPastSelection] ); return ( @@ -267,9 +269,10 @@ export default function MobileCalendarSingleMonth({ closedWeekdaySet={closedWeekdaySet} todayIso={todayIso} onSelect={handleSelect} + allowPastSelection={allowPastSelection} /> - {/* bottom controls (همان دکمه‌ها برای سازگاری با نسخه قبلی) */} + {/* bottom controls */}
+ + +
+
+ + {/* {item.customer_phone_number && ( +
+ مشتری: {item.customer_phone_number} +
+ )} */} +
+ + ); +}; + +export default CashBackCard; diff --git a/src/apps/new-ui/pages/CashBack/+components/CreateCashbackSheet.tsx b/src/apps/new-ui/pages/CashBack/+components/CreateCashbackSheet.tsx new file mode 100644 index 00000000..77438be5 --- /dev/null +++ b/src/apps/new-ui/pages/CashBack/+components/CreateCashbackSheet.tsx @@ -0,0 +1,425 @@ +import { useEffect, useRef, useState } from "react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { useForm, SubmitHandler, Controller } from "react-hook-form"; +import { useCreateCashback } from "@/apps/appointment/services/CashBack"; +import { toast } from "react-hot-toast"; + +type Props = { + isOpen: boolean; + onRequestClose: () => void; + onSuccess?: () => void; + animationDuration?: number; + dragThreshold?: number; +}; + +type Form = { + amount: number | ""; + percentage: number | ""; + expire_days: number | ""; +}; + +const initial: Form = { amount: "", percentage: "", expire_days: "" }; + +const formatThousands = (raw: string) => { + const digits = raw.replace(/\D/g, ""); + if (!digits) return ""; + return digits.replace(/\B(?=(\d{3})+(?!\d))/g, ","); +}; + +const CreateCashbackSheet: React.FC = ({ + isOpen, + onRequestClose, + animationDuration = 260, + dragThreshold = 120, +}) => { + const [internalVisible, setInternalVisible] = useState(false); + const openTimeout = useRef(null); + const closeTimeout = useRef(null); + + // focus & scroll restore + const prevActiveRef = useRef(null); + const scrollYRef = useRef(0); + + // drag + const sheetRef = useRef(null); + const startYRef = useRef(null); + const draggingRef = useRef(false); + const [translateY, setTranslateY] = useState(0); + const [isDragging, setIsDragging] = useState(false); + + const rafRef = useRef(null); + const lastYRef = useRef(null); + + const { + control, + register, + handleSubmit, + reset, + formState: { errors, isValid }, + } = useForm
({ defaultValues: initial, mode: "onChange" }); + const create = useCreateCashback(); + + // local formatted display for amount + const [amountDisplay, setAmountDisplay] = useState(""); + + const rafUpdate = (y: number) => { + if (rafRef.current) cancelAnimationFrame(rafRef.current); + rafRef.current = window.requestAnimationFrame(() => { + setTranslateY(y); + rafRef.current = null; + }); + }; + + useEffect(() => { + if (isOpen) { + prevActiveRef.current = (document.activeElement as HTMLElement) || null; + scrollYRef.current = window.scrollY || window.pageYOffset || 0; + + // lock page scroll + const body = document.body; + body.style.position = "fixed"; + body.style.top = `-${scrollYRef.current}px`; + body.style.left = "0"; + body.style.right = "0"; + body.style.width = "100%"; + body.style.overflow = "hidden"; + + if (openTimeout.current) window.clearTimeout(openTimeout.current); + openTimeout.current = window.setTimeout(() => setInternalVisible(true), 10); + } else { + // start hide animation immediately + setInternalVisible(false); + // ensure timeouts cleared + if (openTimeout.current) window.clearTimeout(openTimeout.current); + } + return () => { + // nothing here — final cleanup in separate effect + }; + }, [isOpen]); + + // pointer drag handlers + useEffect(() => { + if (!isOpen) return; + const sheetEl = sheetRef.current; + if (!sheetEl) return; + + const handleEl = sheetEl.querySelector(".drag-handle") as HTMLElement | null; + const pointerTarget: HTMLElement = handleEl || sheetEl; + + const prevTouch = pointerTarget.style.touchAction || ""; + if (!pointerTarget.style.touchAction) pointerTarget.style.touchAction = "none"; + + const onPointerDown = (ev: PointerEvent) => { + if (ev.button && ev.button !== 0) return; + + if (handleEl) { + const inside = (ev.target as HTMLElement).closest?.(".drag-handle"); + if (!inside) return; + } + + const isFormField = (el: Element | null) => { + if (!el) return false; + const field = (el as HTMLElement).closest("input, textarea, select, button, [contenteditable='true']"); + return Boolean(field); + }; + if (!handleEl && isFormField(ev.target as Element)) return; + + ev.preventDefault?.(); + startYRef.current = ev.clientY; + draggingRef.current = true; + setIsDragging(true); + try { + (ev.target as Element).setPointerCapture?.(ev.pointerId); + } catch {} + }; + + const onPointerMove = (ev: PointerEvent) => { + if (!draggingRef.current || startYRef.current === null) return; + const delta = ev.clientY - startYRef.current; + const clamped = Math.max(0, delta); + lastYRef.current = clamped; + rafUpdate(clamped); + }; + + const finishCloseViaDrag = () => { + setInternalVisible(false); + setTranslateY(window.innerHeight); + if (closeTimeout.current) window.clearTimeout(closeTimeout.current); + closeTimeout.current = window.setTimeout(() => { + onRequestClose(); + }, animationDuration); + }; + + const onPointerUp = (ev: PointerEvent) => { + if (!draggingRef.current) return; + draggingRef.current = false; + setIsDragging(false); + const delta = (ev.clientY ?? 0) - (startYRef.current ?? 0); + startYRef.current = null; + + if (delta > dragThreshold) { + finishCloseViaDrag(); + } else { + if (rafRef.current) cancelAnimationFrame(rafRef.current); + setTranslateY(0); + } + }; + + pointerTarget.addEventListener("pointerdown", onPointerDown as any, { passive: false }); + window.addEventListener("pointermove", onPointerMove as any); + window.addEventListener("pointerup", onPointerUp as any); + window.addEventListener("pointercancel", onPointerUp as any); + + return () => { + pointerTarget.removeEventListener("pointerdown", onPointerDown as any); + window.removeEventListener("pointermove", onPointerMove as any); + window.removeEventListener("pointerup", onPointerUp as any); + window.removeEventListener("pointercancel", onPointerUp as any); + pointerTarget.style.touchAction = prevTouch; + }; + }, [isOpen, dragThreshold, onRequestClose, animationDuration]); + + // cleanup on unmount / ensure body restored + useEffect(() => { + return () => { + if (openTimeout.current) window.clearTimeout(openTimeout.current); + if (closeTimeout.current) window.clearTimeout(closeTimeout.current); + if (rafRef.current) cancelAnimationFrame(rafRef.current); + + // restore body styles + const body = document.body; + body.style.position = ""; + body.style.top = ""; + body.style.left = ""; + body.style.right = ""; + body.style.width = ""; + body.style.overflow = ""; + + // restore scroll + window.scrollTo(0, scrollYRef.current); + + // restore focus + const prev = prevActiveRef.current; + if (prev) { + try { + prev.focus({ preventScroll: true }); + } catch { + try { + prev.focus(); + } catch {} + } + prevActiveRef.current = null; + } + }; + }, []); + + const transformStyle: React.CSSProperties = isDragging + ? { transform: `translate3d(0, ${translateY}px, 0)`, transition: "none", willChange: "transform" } + : { + transform: internalVisible ? "translate3d(0, 0, 0)" : `translate3d(0, 100%, 0)`, + transition: `transform ${animationDuration}ms cubic-bezier(0.22, 1, 0.36, 1)`, + willChange: "transform", + }; + + const onOverlayClick = () => { + if (isDragging) return; + setInternalVisible(false); + setTranslateY(window.innerHeight); + if (closeTimeout.current) window.clearTimeout(closeTimeout.current); + closeTimeout.current = window.setTimeout(() => { + onRequestClose(); + }, animationDuration); + }; + + const onSubmit: SubmitHandler = async (data) => { + try { + await create.mutateAsync({ + amount: data.amount ? Number(data.amount) : undefined, + percentage: data.percentage ? Number(data.percentage) : undefined, + expire_days: data.expire_days ? Number(data.expire_days) : undefined, + }); + // close with animation + setInternalVisible(false); + setTranslateY(window.innerHeight); + if (closeTimeout.current) window.clearTimeout(closeTimeout.current); + closeTimeout.current = window.setTimeout(() => { + onRequestClose(); + reset(initial); + setAmountDisplay(""); + }, animationDuration); + } catch (e) { + toast.error("خطا در ایجاد کش‌بک"); + } + }; + + if (!isOpen) return null; + + return ( +
+
+ +
e.stopPropagation()}> +
+
+
+ {/* hit area bigger for touch */} +
+
+
+
+

+ ایجاد کش‌بک جدید +

+

می‌توانید مقادیر مدنظر خود را وارد کنید

+
+
+
+ +
+ + + + + + + + +
+ + + +
+ +
+
+
+ ); +}; + +export default CreateCashbackSheet; diff --git a/src/apps/new-ui/pages/CashBack/index.tsx b/src/apps/new-ui/pages/CashBack/index.tsx new file mode 100644 index 00000000..14d33097 --- /dev/null +++ b/src/apps/new-ui/pages/CashBack/index.tsx @@ -0,0 +1,172 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import HeaderWithBubbles from "@/apps/new-ui/components/HeaderWithBubbles"; +import { DotLottieReact } from "@lottiefiles/dotlottie-react"; +import { Plus } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Pagination } from "@/components/ui/pagination"; +import { Skeleton } from "@/components/ui/skeleton"; + +import { CashbackItem, useDeleteCashback, useGetUserCashbacks } from "@/apps/appointment/services/CashBack"; +import { ErrorComponentFallback } from "@/components/ErrorComponentFallback"; +import CashBackCard from "./+components/CashBackCard"; +import CreateCashbackSheet from "./+components/CreateCashbackSheet"; + +const STAGGER_MS = 90; +const INITIAL_DELAY = 360; +const ITEMS_PER_PAGE = 8; + +const CashbacksPage: React.FC = () => { + const { data, isLoading, isError, error, refetch } = useGetUserCashbacks(); + const deleteMutation = useDeleteCashback(); + + const [isCreateOpen, setIsCreateOpen] = useState(false); + const [_isCreateVisible, setIsCreateVisible] = useState(false); + const openTimeoutRef = useRef(null); + const closeTimeoutRef = useRef(null); + + const openCreateSheet = () => { + setIsCreateOpen(true); + if (openTimeoutRef.current) window.clearTimeout(openTimeoutRef.current); + openTimeoutRef.current = window.setTimeout(() => setIsCreateVisible(true), 10); + }; + const closeCreateSheet = () => { + setIsCreateVisible(false); + if (closeTimeoutRef.current) window.clearTimeout(closeTimeoutRef.current); + closeTimeoutRef.current = window.setTimeout(() => setIsCreateOpen(false), 280); + }; + + // staggered cards + const [visibleArr, setVisibleArr] = useState([]); + const timersRef = useRef([]); + + // items extraction (be tolerant to shapes) + const items: CashbackItem[] = Array.isArray(data) ? data : []; + const totalCount = Array.isArray(data) ? items.length : 0; + const perPage = Array.isArray(data) ? ITEMS_PER_PAGE : 5; + + const [currentPage] = useState(1); + const pagedItems = Array.isArray(data) ? items.slice((currentPage - 1) * perPage, currentPage * perPage) : items; + + useEffect(() => { + // clear timers + timersRef.current.forEach((t) => window.clearTimeout(t)); + timersRef.current = []; + + if (!pagedItems.length) { + setVisibleArr([]); + return; + } + + setVisibleArr(Array(pagedItems.length).fill(false)); + pagedItems.forEach((_, i) => { + const t = window.setTimeout(() => { + setVisibleArr((prev) => { + const next = prev.slice(); + if (i < next.length) next[i] = true; + return next; + }); + }, INITIAL_DELAY + i * STAGGER_MS); + timersRef.current.push(t); + }); + + return () => { + timersRef.current.forEach((t) => window.clearTimeout(t)); + timersRef.current = []; + }; + }, [JSON.stringify(pagedItems.map((p) => p.id))]); + + useEffect(() => { + return () => { + timersRef.current.forEach((t) => window.clearTimeout(t)); + if (openTimeoutRef.current) window.clearTimeout(openTimeoutRef.current); + if (closeTimeoutRef.current) window.clearTimeout(closeTimeoutRef.current); + }; + }, []); + + const handleDelete = async (code?: string) => { + if (!code) return; + try { + await deleteMutation.mutateAsync(code); + } catch (e) { + console.error(e); + } + }; + + return ( +
+ + +
+ {items && items.length > 0 && ( +
+

لیست کش‌بک‌ ها

+ +
+ +
+
+ )} + + {isError ? ( + + ) : isLoading ? ( +
+ {new Array(4).fill("").map((_, idx) => ( +
+ + + +
+ ))} +
+ ) : items && items.length > 0 ? ( + <> +
+ {pagedItems.map((item, idx) => ( + + ))} +
+ +
+ +
+ + ) : ( +
+ +

+ هنوز کش‌بکی ثبت نکرده‌اید. برای ثبت کش‌بک جدید می‌توانید از دکمه زیر استفاده کنید. +

+
+ +
+
+ )} +
+ + +
+ ); +}; + +export default CashbacksPage; diff --git a/src/apps/new-ui/pages/Profile/+components/FooterActions.tsx b/src/apps/new-ui/pages/Profile/+components/FooterActions.tsx index 57df5a33..75c4d560 100644 --- a/src/apps/new-ui/pages/Profile/+components/FooterActions.tsx +++ b/src/apps/new-ui/pages/Profile/+components/FooterActions.tsx @@ -16,7 +16,7 @@ const FooterActions: React.FC = ({ exitIcon, onLogout, onAddAccount }) =>
+ {selOption.label} +
- {/* left small arrow */} - - arrowIcon - - + {/* left small arrow */} + + arrowIcon + + {/* */} {/* dropdown */} {open && ( @@ -81,7 +73,7 @@ export default function PillSelect({ role="listbox" aria-activedescendant={selected} tabIndex={-1} - className="absolute z-20 mt-2 w-full rounded-lg bg-white border border-gray-100 shadow-lg py-1" + className="absolute z-20 left-0 top-14 mt-2 w-full rounded-lg bg-white border border-gray-100 shadow-lg py-1" > {options.map((opt) => (
  • { const [appointmentSMS, setAppointmentSMS] = useState(false); const [repairSMS, setRepairSMS] = useState(false); const [surveySMS, setSurveySMS] = useState(false); + const [cashBack, setCashBack] = useState(false); useEffect(() => { if (userConfig) { - setSmsReminder(userConfig.default_reminder_sms ?? false); - setAppointmentSMS(userConfig.default_appointment_sms ?? false); - setRepairSMS(userConfig.default_repair_sms ?? false); - setSurveySMS(userConfig.default_survay_sms ?? false); + setSmsReminder(userConfig.default_reminder_sms); + setAppointmentSMS(userConfig.default_appointment_sms); + setRepairSMS(userConfig.default_repair_sms); + setSurveySMS(userConfig.default_survay_sms); + setCashBack(userConfig.default_cashback_active); + localStorage.setItem("cashBack", String(userConfig.default_cashback_active)); + window.dispatchEvent(new Event("cashBackChanged")); } + + return () => { + localStorage.removeItem("cashBack"); + }; }, [userConfig]); const handleToggle = async ( @@ -82,6 +90,11 @@ const SMSSetting = () => { booleanState={surveySMS} label={"پیامک نظرسنجی"} /> + handleToggle("default_cashback_active", cashBack, setCashBack, "بازگشت سرمایه")} + booleanState={cashBack} + label={"بازگشت سرمایه"} + />
  • ); diff --git a/src/apps/new-ui/pages/Profile/index.tsx b/src/apps/new-ui/pages/Profile/index.tsx index 1e9c7afb..f6062b25 100644 --- a/src/apps/new-ui/pages/Profile/index.tsx +++ b/src/apps/new-ui/pages/Profile/index.tsx @@ -1,5 +1,4 @@ -// Profile.tsx -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; @@ -68,6 +67,29 @@ const Profile: React.FC = () => { const [isSupportVisible, setIsSupportVisible] = useState(false); const supportAnimationDuration = 300; // ms - keep consistent with SupportBottomSheet prop + const [isCashBackEnabled, setIsCashBackEnabled] = useState(false); + + useEffect(() => { + const read = () => { + const raw = localStorage.getItem("cashBack"); + const val = raw !== null && (raw === "true" || raw === "1" || raw.toLowerCase() === "yes"); + setIsCashBackEnabled(Boolean(val)); + }; + + read(); + const onStorage = (e: StorageEvent) => { + if (e.key === "cashBack") read(); + }; + const onCustom = () => read(); + + window.addEventListener("storage", onStorage); + window.addEventListener("cashBackChanged", onCustom); + return () => { + window.removeEventListener("storage", onStorage); + window.removeEventListener("cashBackChanged", onCustom); + }; + }, []); + // services/hooks const { currectAccount, updateAccount, logOutAccount } = useMultiAccounts(); const { data: userInfo } = useGetUserInfo(); @@ -88,7 +110,6 @@ const Profile: React.FC = () => { name: current.name, phone_number: current.phone_number, businesscard_link: current.businesscard_link, - // @ts-ignore city: current.city, province: current.province, date_of_birth: current.date_of_birth, @@ -110,7 +131,6 @@ const Profile: React.FC = () => { } else { setProfilePhoto(photoDefault); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [userInfo, reset, currectAccount, updateAccount]); // handlers for user update & logout @@ -156,22 +176,17 @@ const Profile: React.FC = () => { const closeSupportTimeoutRef = useRef(null); const openSupport = useCallback(() => { - // save currently focused element to restore later without scrolling const active = (document.activeElement as HTMLElement) || null; prevActiveRef.current = active; setIsSupportOpen(true); - // small delay to allow mount, then show (enter animation) window.setTimeout(() => setIsSupportVisible(true), 10); }, []); const closeSupport = useCallback(() => { - // blur active element so browser won't try to scroll to it on unmount try { (document.activeElement as HTMLElement | null)?.blur?.(); - } catch { - // ignore - } + } catch {} // start exit animation setIsSupportVisible(false); @@ -276,7 +291,15 @@ const Profile: React.FC = () => { navigate("/transactions")} /> navigate("/modules-list")} /> -
    + {isCashBackEnabled && ( + navigate("/cash-back-list")} + /> + )} + +
    diff --git a/src/apps/new-ui/pages/modules-list/index.tsx b/src/apps/new-ui/pages/modules-list/index.tsx index 3de5fbf0..298e9697 100644 --- a/src/apps/new-ui/pages/modules-list/index.tsx +++ b/src/apps/new-ui/pages/modules-list/index.tsx @@ -9,96 +9,88 @@ import { useGetUserInfo } from "../../services/User"; import moduleRose from "../../assets/modules/moduleRose.webp"; import bgRose from "../../../../assets/images/bgRose.png"; -const modulesObject: Record< - AbilityActionEnum, - { title: string; image: string } -> = { - [AbilityActionEnum.REPAIR_REMINDER]: { - title: "ماژول ترمیم", - image: repairVector, - }, - [AbilityActionEnum.LOYALTY_SERVICE]: { - title: "ماژول وفاداری", - image: loyaltyVector, - }, - [AbilityActionEnum.SURVEY_SERVICE]: { - title: "ماژول نظرسنجی", - image: surveyVector, - }, - [AbilityActionEnum.ACCOUNTANT_SERVICE]: { - title: "ماژول حسابداری", - image: incomeVector, - }, +const modulesObject: Record = { + [AbilityActionEnum.REPAIR_REMINDER]: { + title: "ماژول ترمیم", + image: repairVector, + }, + [AbilityActionEnum.LOYALTY_SERVICE]: { + title: "ماژول وفاداری", + image: loyaltyVector, + }, + [AbilityActionEnum.SURVEY_SERVICE]: { + title: "ماژول نظرسنجی", + image: surveyVector, + }, + [AbilityActionEnum.ACCOUNTANT_SERVICE]: { + title: "ماژول حسابداری", + image: incomeVector, + }, }; const ModulesList = () => { - const { data: userInfo, isLoading: userInfoLoading } = useGetUserInfo(); + const { data: userInfo, isLoading: userInfoLoading } = useGetUserInfo(); - return ( -
    - -
    - rose -
    -
    - -
    -

    - لیست ماژول‌ها -

    -
    - {userInfo?.user_abilities_list.map((ab) => ( -
    -
    + return ( +
    + +
    rose -
    -
    -

    - {modulesObject[ab.ability_action].title} -

    - {ab.remaining_time === "bonus" ? ( -
    -
    - - -

    هدیه

    -
    -
    - ) : ab.remaining_time === "Expired" ? ( -
    - -

    غیر فعال

    -
    - ) : ( -
    - - -

    - - {ab.remaining_time} - - روز باقی‌مانده -

    -
    - )} -
    +
    +
    + +
    +

    + لیست ماژول‌ها +

    +
    + {userInfo?.user_abilities_list.map((ab) => ( +
    +
    + +
    +
    +

    + {modulesObject[ab.ability_action].title} +

    + {ab.remaining_time === "bonus" ? ( +
    +
    + + +

    هدیه

    +
    +
    + ) : ab.remaining_time === "Expired" ? ( +
    + +

    غیر فعال

    +
    + ) : ( +
    + + +

    + {ab.remaining_time} + روز باقی‌مانده +

    +
    + )} +
    +
    + ))} +
    - ))}
    -
    -
    - ); + ); }; export default ModulesList; diff --git a/src/apps/new-ui/pages/support/tickets/index.tsx b/src/apps/new-ui/pages/support/tickets/index.tsx index 43914340..f031c539 100644 --- a/src/apps/new-ui/pages/support/tickets/index.tsx +++ b/src/apps/new-ui/pages/support/tickets/index.tsx @@ -9,6 +9,7 @@ import { TicketPlusIcon } from "lucide-react"; import { DotLottieReact } from "@lottiefiles/dotlottie-react"; import TicketBottomSheet from "./TicketBottomSheet"; import TicketCard from "./TicketCard"; +import HeaderWithBubbles from "@/apps/new-ui/components/HeaderWithBubbles"; const STAGGER_MS = 100; // ms between each card const INITIAL_DELAY = 500; // ms before first card appears @@ -95,17 +96,16 @@ const Tickets = () => { return (
    -
    + {/*

    تیکت های من

    -
    +
    */} + -
    -
    -
    -

    لیست تیکت ها

    -
    +
    +
    +

    لیست تیکت ها

    {/* Open sheet instead of navigating */} diff --git a/src/apps/new-ui/routes/index.ts b/src/apps/new-ui/routes/index.ts index a73788c9..f0b38368 100644 --- a/src/apps/new-ui/routes/index.ts +++ b/src/apps/new-ui/routes/index.ts @@ -10,6 +10,7 @@ export const newAppointmentRoutes = Object.freeze({ sendLoyaltySMS: '/send-loyalty-sms', opinionSMS: "/opinion-sms", profile: '/profile', + cashBack: '/cash-back-list', modulesList: '/modules-list', report: '/report', customers: '/customers', diff --git a/tsconfig.app.json b/tsconfig.app.json index 5af35943..4d203ad4 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -1,35 +1,24 @@ { - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true, - - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"], - "@package.json": ["./package.json"], - "@appointment/*": ["./src/apps/appointment/*"], - "@card/*": ["./src/apps/card/*"], - "@new-ui/*": ["./src/apps/new-ui/*"] - } - }, - "include": ["src"] + "extends": "./tsconfig.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "ignoreDeprecations": "6.0" + }, + "include": ["src"] } diff --git a/tsconfig.json b/tsconfig.json index 5a1a0250..1ca8412f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "@card/*": ["./src/apps/card/*"], "@new-ui/*": ["./src/apps/new-ui/*"], "@new-ui-card/*": ["./src/apps/new-ui-card/*"], - } + }, + "ignoreDeprecations": "6.0" } } diff --git a/vite.config.ts b/vite.config.ts index 8e5e3116..7fa1beb8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,11 +3,13 @@ import react from "@vitejs/plugin-react-swc"; import { defineConfig } from "vite"; import path from "path"; import pkg from "./package.json" assert { type: "json" }; +import tsconfigPaths from "vite-tsconfig-paths"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ react(), + tsconfigPaths(), VitePWA({ strategies: "injectManifest", srcDir: "src",