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 */}
- +
+ +
افزودن حساب
diff --git a/src/apps/new-ui/pages/Profile/+components/PillSelect.tsx b/src/apps/new-ui/pages/Profile/+components/PillSelect.tsx
index 842dd7ab..bf8450f1 100644
--- a/src/apps/new-ui/pages/Profile/+components/PillSelect.tsx
+++ b/src/apps/new-ui/pages/Profile/+components/PillSelect.tsx
@@ -14,17 +14,9 @@ type PillSelectProps = {
defaultValue?: string;
onChange?: (value: string) => void;
className?: string;
- minWidth?: string;
};
-export default function PillSelect({
- options,
- value,
- defaultValue,
- onChange,
- className = "",
- minWidth = "min-w-[160px]",
-}: PillSelectProps) {
+export default function PillSelect({ options, value, defaultValue, onChange, className = "" }: PillSelectProps) {
const [open, setOpen] = useState(false);
const [selected, setSelected] = useState(value ?? defaultValue ?? options[0]?.value);
@@ -53,27 +45,27 @@ export default function PillSelect({
const selOption = options.find((o) => o.value === selected) ?? options[0];
return (
-
-
setOpen((s) => !s)}
- className={`w-full mt-4 text-right inline-flex border-2 border-[#0000001A] shadow-md items-center justify-between gap-3 rounded-2xl bg-white border-gray-200 px-4 py-4 ${
- open ? "ring-2 ring-purple-200" : ""
- }`}
- >
-
-

+
setOpen((s) => !s)}
+ aria-haspopup="listbox"
+ aria-expanded={open}
+ className={`relative w-full inline-block ${className} mt-4 text-right inline-flex border-2 border-[#0000001A] shadow-md items-center justify-between gap-2 rounded-2xl bg-white p-4 ${
+ open ? "ring-2 ring-purple-200" : ""
+ }`}
+ >
+ {/*
+
- {selOption.label}
-
+
{selOption.label}
+
- {/* left small arrow */}
-
-
-
-
+ {/* left small arrow */}
+
+
+
+ {/* */}
{/* 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 (
-
-
-
-

-
-
-
-
-
- لیست ماژولها
-
-
- {userInfo?.user_abilities_list.map((ab) => (
-
-
+ return (
+
+
+

-
-
-
- {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",