diff --git a/package.json b/package.json index 0f360430..9fc0f721 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "app.salona", "private": true, - "version": "2.74.4", + "version": "2.74.6", "type": "module", "scripts": { "dev": "vite --host", diff --git a/src/apps/new-ui/pages/OpinionSMS/+components/CreatedSurveyCard.tsx b/src/apps/new-ui/pages/OpinionSMS/+components/CreatedSurveyCard.tsx index 57a1d57f..70cf15ca 100644 --- a/src/apps/new-ui/pages/OpinionSMS/+components/CreatedSurveyCard.tsx +++ b/src/apps/new-ui/pages/OpinionSMS/+components/CreatedSurveyCard.tsx @@ -4,6 +4,7 @@ import FormDoneIcon from "@new-ui/assets/waiting-img-done.svg"; import moment from "jalali-moment"; import { Eye, Share2, X } from "lucide-react"; import { CreatedSurvey } from "../types"; +import status from "@new-ui/assets/appointment/status.svg"; type Props = { survey: CreatedSurvey; @@ -45,11 +46,15 @@ const CreatedSurveyCard: React.FC = ({ survey, isPreview, onDelete, onSha {/* user card */}
- {survey?.customer_name +
+ {survey?.customer_name + status +
+

{survey.customer?.name}

{survey.customer?.phone_number}

diff --git a/src/apps/new-ui/pages/OpinionSMS/+components/UserCard.tsx b/src/apps/new-ui/pages/OpinionSMS/+components/UserCard.tsx index 66c0bfb9..9170e516 100644 --- a/src/apps/new-ui/pages/OpinionSMS/+components/UserCard.tsx +++ b/src/apps/new-ui/pages/OpinionSMS/+components/UserCard.tsx @@ -2,6 +2,7 @@ import tick from "@new-ui/assets/appointment/tick.svg"; import emptyTick from "@new-ui/assets/appointment/emptyTick.svg"; import waitingIcon from "@new-ui/assets/waiting-img.svg"; import { Eye } from "lucide-react"; +import status from "@new-ui/assets/appointment/status.svg"; interface Props { img?: string | undefined; @@ -13,7 +14,6 @@ interface Props { } const UserCard: React.FC = ({ img, title, checked, description, onClick, onView }) => { - return (
= ({ img, title, checked, description, onClick, }} >
- {title +
+ {img ? ( + <> + {title + status + + ) : ( + {title + )} +
+

{title}

= ({ img, title, checked, description, onClick, )} {checked ? tick : emptyTick} -

+
); }; -export default UserCard; \ No newline at end of file +export default UserCard; diff --git a/src/apps/new-ui/pages/ReportAll/Accounting/index.tsx b/src/apps/new-ui/pages/ReportAll/Accounting/index.tsx index ed5df28e..86f913fa 100644 --- a/src/apps/new-ui/pages/ReportAll/Accounting/index.tsx +++ b/src/apps/new-ui/pages/ReportAll/Accounting/index.tsx @@ -317,6 +317,7 @@ const Accounting: React.FC = () => { { const val = e.target.value.replace(/[^0-9]/g, ""); @@ -326,7 +327,6 @@ const Accounting: React.FC = () => { placeholder="مثال: 1402" className="w-full p-3 rounded-lg border border-[#E5E7EB] text-right" /> -

عدد سال را به صورت شمسی وارد کنید (مثال: 1402). این مقدار هنگام محاسبه به URL اضافه می‌شود.

)}
diff --git a/src/apps/new-ui/pages/ReportAll/ChartReport/index.tsx b/src/apps/new-ui/pages/ReportAll/ChartReport/index.tsx index 1ac03303..39a32588 100644 --- a/src/apps/new-ui/pages/ReportAll/ChartReport/index.tsx +++ b/src/apps/new-ui/pages/ReportAll/ChartReport/index.tsx @@ -5,249 +5,248 @@ import CostPieChart, { AccountingEntry as PieEntry } from "./+components/CostPie import MonthlyChart from "./+components/MonthlyChart"; import { CalendarDays } from "lucide-react"; import moment from "jalali-moment"; - import { useGetTotalProfitBetweenDates } from "@/apps/new-ui/services/ChartReport"; -import { useGetYearlyReport } from "@/apps/new-ui/services/ChartReport"; // فرض بر این که این هوک در همین فایل سرویس موجوده +import { useGetYearlyReport } from "@/apps/new-ui/services/ChartReport"; export type AccountingEntry = { - id: number; - type: "income" | "cost"; - amount: number; - date: string; // e.g. "1404-07-01" (jYYYY-jMM-jDD) یا هر فرمتی که کامپوننت‌های شما می‌خوان - category?: string; - note?: string; + id: number; + type: "income" | "cost"; + amount: number; + date: string; + category?: string; + note?: string; }; function filterByRange(entries: AccountingEntry[], start: string, end: string) { - if (!start && !end) return entries; - return entries.filter((e) => { - if (start && end) return e.date >= start && e.date <= end; - if (start) return e.date >= start; - if (end) return e.date <= end; - return true; - }); + if (!start && !end) return entries; + return entries.filter((e) => { + if (start && end) return e.date >= start && e.date <= end; + if (start) return e.date >= start; + if (end) return e.date <= end; + return true; + }); } export default function ChartReport() { - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); - const startParam = searchParams.get("start") ?? ""; - const endParam = searchParams.get("end") ?? ""; - const rangeParam = searchParams.get("range") ?? ""; - const isShowMonthly = rangeParam !== "mounth" && rangeParam !== "range"; + const startParam = searchParams.get("start") ?? ""; + const endParam = searchParams.get("end") ?? ""; + const rangeParam = searchParams.get("range") ?? ""; + const isShowMonthly = rangeParam !== "month" && rangeParam !== "range"; - // نمایش تاریخ در header (local state فقط برای نمایش) - const [startInput] = useState(startParam); - const [endInput] = useState(endParam); + const [startInput] = useState(startParam); + const [endInput] = useState(endParam); - const [tab, setTab] = useState<0 | 1>(0); + const [tab, setTab] = useState<0 | 1>(0); - // totals API (همان هوک اصلی شما) - const totalsQuery = useGetTotalProfitBetweenDates( - startParam && endParam ? { startDate: startParam, endDate: endParam } : undefined - ) as any; - const totalsData = totalsQuery?.data; + // === totals API hook === + const totalsQuery = useGetTotalProfitBetweenDates( + startParam && endParam ? { startDate: startParam, endDate: endParam } : undefined + ) as any; + const totalsData = totalsQuery?.data; + const totalsLoading = totalsQuery?.isLoading || totalsQuery?.isFetching; + const totalsError = totalsQuery?.isError; - // fallback mock (همان mock شما) - const mockTotals = useMemo( - () => ({ - net_profit: 2620097, - total_expence: 3950000, - total_income: 6570097, - }), - [] - ); + // parse numbers safely + const parseNum = (v: any): number | null => { + if (v === null || v === undefined) return null; + const n = Number(v); + return Number.isFinite(n) ? n : null; + }; - // parse numbers safely - const parseNum = (v: any): number | null => { - if (v === null || v === undefined) return null; - const n = Number(v); - return Number.isFinite(n) ? n : null; - }; + const parsedIncome = parseNum(totalsData?.total_income); + const parsedExpense = parseNum(totalsData?.total_expence); + const parsedProfit = parseNum(totalsData?.net_profit); - const parsedIncome = parseNum(totalsData?.total_income); - const parsedExpense = parseNum(totalsData?.total_expence); - const parsedProfit = parseNum(totalsData?.net_profit); + // === IMPORTANT: remove mock fallback. + // if totalsData is falsy, we won't create serverTxs (چیزی برای نمایش نیست). + const hasServerTotals = !!totalsData && (parsedIncome !== null || parsedExpense !== null || parsedProfit !== null); - const hasAnyNonZero = - (parsedIncome !== null && parsedIncome !== 0) || - (parsedExpense !== null && parsedExpense !== 0) || - (parsedProfit !== null && parsedProfit !== 0); + // serverTxs only when we have real totals from server + const serverTxs: AccountingEntry[] = useMemo(() => { + if (!hasServerTotals) return []; - const effectiveTotals = hasAnyNonZero ? totalsData : mockTotals; + const baseDate = startParam || endParam || moment().format("jYYYY-jMM-01"); + const incomeAmount = Number(totalsData.total_income ?? 0); + const costAmount = Number(totalsData.total_expence ?? 0); - // serverTxs همان داده‌ای است که برای PieChart استفاده می‌کنیم - const serverTxs: AccountingEntry[] = useMemo(() => { - const baseDate = startParam || endParam || moment().format("jYYYY-jMM-01"); - const incomeAmount = Number(effectiveTotals?.total_income ?? 0); - const costAmount = Number(effectiveTotals?.total_expence ?? 0); + return [ + { + id: 1, + type: "income", + amount: incomeAmount, + date: baseDate, + category: "سود/درآمد", + note: "مجموع درآمد (از totals API)", + }, + { + id: 2, + type: "cost", + amount: costAmount, + date: baseDate, + category: "هزینه‌ها", + note: "مجموع هزینه (از totals API)", + }, + ]; + }, [totalsData, startParam, endParam, hasServerTotals]); - return [ - { - id: 1, - type: "income", - amount: incomeAmount, - date: baseDate, - category: "سود/درآمد", - note: hasAnyNonZero ? "مجموع درآمد (از totals API)" : "مجموع درآمد (mock)", - }, - { - id: 2, - type: "cost", - amount: costAmount, - date: baseDate, - category: "هزینه‌ها", - note: hasAnyNonZero ? "مجموع هزینه (از totals API)" : "مجموع هزینه (mock)", - }, - ]; - }, [effectiveTotals, startParam, endParam, hasAnyNonZero]); + const entriesInRange = useMemo( + () => filterByRange(serverTxs, startParam, endParam), + [serverTxs, startParam, endParam] + ); - const entriesInRange = useMemo( - () => filterByRange(serverTxs, startParam, endParam), - [serverTxs, startParam, endParam] - ); + const incomeTotal = + parseNum(totalsData?.total_income) ?? + entriesInRange.filter((e) => e.type === "income").reduce((s, e) => s + e.amount, 0); - const incomeTotal = - parseNum(effectiveTotals?.total_income) ?? - entriesInRange.filter((e) => e.type === "income").reduce((s, e) => s + e.amount, 0); + const costTotal = + parseNum(totalsData?.total_expence) ?? + entriesInRange.filter((e) => e.type === "cost").reduce((s, e) => s + e.amount, 0); - const costTotal = - parseNum(effectiveTotals?.total_expence) ?? - entriesInRange.filter((e) => e.type === "cost").reduce((s, e) => s + e.amount, 0); + const profitTotal = parseNum(totalsData?.net_profit) ?? incomeTotal - costTotal; - const profitTotal = parseNum(effectiveTotals?.net_profit) ?? incomeTotal - costTotal; + console.log(profitTotal); + // === yearly report (tab 1) === + const shamsiYearFromParam = startParam ? startParam.slice(0, 4) : ""; + const currentJalaliYear = moment().format("jYYYY"); + const shamsiYear = shamsiYearFromParam || currentJalaliYear; - console.log(profitTotal); + const yearlyQuery = useGetYearlyReport(tab === 1 ? shamsiYear : undefined) as any; + const yearlyData = yearlyQuery?.data; + const yearlyLoading = yearlyQuery?.isLoading || yearlyQuery?.isFetching; + const yearlyError = yearlyQuery?.isError; - // === NEW: yearly report fetch when tab === 1 === - // shamsiYear: اگر startParam شامل تاریخ بود (jYYYY-jMM-jDD) از چهار رقم اول استفاده می‌کنیم - const shamsiYearFromParam = startParam ? startParam.slice(0, 4) : ""; - const currentJalaliYear = moment().format("jYYYY"); - const shamsiYear = shamsiYearFromParam || currentJalaliYear; + const monthlyEntries: AccountingEntry[] = useMemo(() => { + if (!yearlyData?.monthly_data || !Array.isArray(yearlyData.monthly_data)) return []; + const yearStr = String(yearlyData.year ?? shamsiYear); + return yearlyData.monthly_data.flatMap((m: any) => { + const monthStr = String(m.month).padStart(2, "0"); + const date = `${yearStr}-${monthStr}-01`; + return [ + { + id: Number(`${yearStr}${monthStr}1`), + type: "income", + amount: Number(m.total_income ?? 0), + date, + category: "درآمد ماهیانه", + note: `ماه ${m.month}`, + } as AccountingEntry, + { + id: Number(`${yearStr}${monthStr}2`), + type: "cost", + amount: Number(m.total_expense ?? 0), + date, + category: "هزینه ماهیانه", + note: `ماه ${m.month}`, + } as AccountingEntry, + ]; + }); + }, [yearlyData, shamsiYear]); - // فقط زمانی که tab === 1 هوک فعال باشه (hook خودتون هم enabled بررسی می‌کنه) - const yearlyQuery = useGetYearlyReport(tab === 1 ? shamsiYear : undefined) as any; - const yearlyData = yearlyQuery?.data; - const yearlyLoading = yearlyQuery?.isLoading || yearlyQuery?.isFetching; - const yearlyError = yearlyQuery?.isError; + useEffect(() => { + const t = setTimeout(() => { + window.dispatchEvent(new Event("resize")); + }, 120); + return () => clearTimeout(t); + }, [tab, startParam, endParam]); - // map monthly_data -> AccountingEntry[] مناسب MonthlyChart - const monthlyEntries: AccountingEntry[] = useMemo(() => { - if (!yearlyData?.monthly_data || !Array.isArray(yearlyData.monthly_data)) return []; + const renderKey = `${tab}-${startParam}-${endParam}`; - // هر ماه دو ورودی income و cost می‌سازیم (ممکنه بخواهی net_profit هم جدا ارسال بشه) - const yearStr = String(yearlyData.year ?? shamsiYear); - return yearlyData.monthly_data.flatMap((m: any) => { - const monthStr = String(m.month).padStart(2, "0"); - // date به صورت jYYYY-jMM-01 قرار می‌گیرد تا در صورت نیاز به نمایش محور ماه‌ها قابل استفاده باشد - const date = `${yearStr}-${monthStr}-01`; - return [ - { - id: Number(`${yearStr}${monthStr}1`), - type: "income", - amount: Number(m.total_income ?? 0), - date, - category: "درآمد ماهیانه", - note: `ماه ${m.month}`, - } as AccountingEntry, - { - id: Number(`${yearStr}${monthStr}2`), - type: "cost", - amount: Number(m.total_expense ?? 0), - date, - category: "هزینه ماهیانه", - note: `ماه ${m.month}`, - } as AccountingEntry, - ]; - }); - }, [yearlyData, shamsiYear]); + return ( + <> +
- // trigger resize after tab change to help charts redraw correctly - useEffect(() => { - const t = setTimeout(() => { - window.dispatchEvent(new Event("resize")); - }, 120); - return () => clearTimeout(t); - }, [tab, startParam, endParam]); +
+
+ + {tab === 0 ? ( + <> + از + {startInput || "-"} + - + تا + {endInput || "-"} + + ) : ( +

سال انتخاب شده : {shamsiYear}

+ )} +
- const renderKey = `${tab}-${startParam}-${endParam}`; +
+ - return ( - <> -
+ {isShowMonthly && ( + + )} +
-
- {/* header & date selector */} -
- - {tab === 0 ? ( - <> - از - {startInput || "-"} - - - تا - {endInput || "-"} - - ) : ( -

سال انتخاب شده : {shamsiYear}

- )} -
+
+ {tab === 0 ? ( +
+ {totalsLoading ? ( +
+ در حال بارگذاری داده‌های کلی... +
+ ) : totalsError ? ( +
خطا در دریافت داده‌های کلی.
+ ) : !hasServerTotals ? ( +
+ داده‌ای برای نمایش موجود نیست. +
+ ) : ( + + )} +
+ ) : ( +
+ {yearlyLoading ? ( +
+ در حال بارگذاری داده‌های ماهیانه... +
+ ) : yearlyError ? ( +
+ خطا در دریافت گزارش سالیانه. +
+ ) : ( + + )} +
+ )} +
- {/* tabs */} -
- - - {isShowMonthly && ( - - )} -
- - {/* content area */} -
- {tab === 0 ? ( -
- {/* Pie */} - +
+ +
- ) : ( -
- {/* Monthly */} - {yearlyLoading ? ( -
در حال بارگذاری داده‌های ماهیانه...
- ) : yearlyError ? ( -
خطا در دریافت گزارش سالیانه.
- ) : ( - - )} -
- )} -
- - {/* fixed close button */} -
- -
-
- - ); + + ); } diff --git a/src/apps/new-ui/pages/ReportAll/Income&Cost/Create/+components/AmountInput.tsx b/src/apps/new-ui/pages/ReportAll/Income&Cost/Create/+components/AmountInput.tsx index bc6e7a0d..8b99f26d 100644 --- a/src/apps/new-ui/pages/ReportAll/Income&Cost/Create/+components/AmountInput.tsx +++ b/src/apps/new-ui/pages/ReportAll/Income&Cost/Create/+components/AmountInput.tsx @@ -1,153 +1,136 @@ import { useEffect, useMemo, useState } from "react"; type Props = { - label?: string; - onChange: (value: number) => void; - value?: number | null; - locale?: string; // پیش‌فرض 'fa-IR' (اعداد فارسی). برای اعداد لاتین از 'en-US' استفاده کنید. + label?: string; + onChange: (value: number) => void; + value?: number | null; + locale?: string; }; const persianNums: Record = { - "۰": "0", - "۱": "1", - "۲": "2", - "۳": "3", - "۴": "4", - "۵": "5", - "۶": "6", - "۷": "7", - "۸": "8", - "۹": "9", + "۰": "0", + "۱": "1", + "۲": "2", + "۳": "3", + "۴": "4", + "۵": "5", + "۶": "6", + "۷": "7", + "۸": "8", + "۹": "9", }; const arabicNums: Record = { - "٠": "0", - "١": "1", - "٢": "2", - "٣": "3", - "٤": "4", - "٥": "5", - "٦": "6", - "٧": "7", - "٨": "8", - "٩": "9", + "٠": "0", + "١": "1", + "٢": "2", + "٣": "3", + "٤": "4", + "٥": "5", + "٦": "6", + "٧": "7", + "٨": "8", + "٩": "9", }; const toLatinDigits = (s: string) => - s.replace(/[۰-۹]/g, (d) => persianNums[d] ?? d).replace(/[٠-٩]/g, (d) => arabicNums[d] ?? d); + s.replace(/[۰-۹]/g, (d) => persianNums[d] ?? d).replace(/[٠-٩]/g, (d) => arabicNums[d] ?? d); const onlyDigits = (s: string) => { - if (!s) return ""; - const latin = toLatinDigits(s); - return latin.replace(/\D/g, ""); + if (!s) return ""; + const latin = toLatinDigits(s); + return latin.replace(/\D/g, ""); }; const formatWithLocale = (digits: string, locale: string) => { - if (!digits) return ""; - // جلوگیری از صفرهای پیشرو (مگر اینکه فقط "0" باشد) - const trimmed = digits.replace(/^0+(?=\d)/, ""); - const n = Number(trimmed || "0"); - try { - return new Intl.NumberFormat(locale).format(n); - } catch { - // fallback به فرمت با کاما (لاتین) - return trimmed.replace(/\B(?=(\d{3})+(?!\d))/g, ","); - } + if (!digits) return ""; + const trimmed = digits.replace(/^0+(?=\d)/, ""); + const n = Number(trimmed || "0"); + try { + return new Intl.NumberFormat(locale).format(n); + } catch { + return trimmed.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + } }; -const AmountInput: React.FC = ({ - label = "مبلغ", - onChange, - value = null, - locale = "fa-IR", -}) => { - // state نگهدارندهٔ RAW digits (فقط اعداد، بدون کاما) - const [raw, setRaw] = useState(value !== null ? String(Math.trunc(value)) : ""); - const [focused, setFocused] = useState(false); +const AmountInput: React.FC = ({ label = "مبلغ", onChange, value = null, locale = "fa-IR" }) => { + const [raw, setRaw] = useState(value !== null ? String(Math.trunc(value)) : ""); + const [focused, setFocused] = useState(false); - // وقتی prop value تغییر کنه، state رو هم بروز کن - useEffect(() => { - if (value !== null && !isNaN(value)) { - setRaw(String(Math.trunc(value))); - } - }, [value]); + useEffect(() => { + if (value !== null && !isNaN(value)) { + setRaw(String(Math.trunc(value))); + } + }, [value]); - // اعلام عدد به والد (همیشه به صورت عدد خام، بدون فرمت) - const emitNumber = (s: string) => { - if (!s) { - onChange(0); - return; - } - const n = Number(s); - if (!isNaN(n)) onChange(n); - else onChange(0); - }; + const emitNumber = (s: string) => { + if (!s) { + onChange(0); + return; + } + const n = Number(s); + if (!isNaN(n)) onChange(n); + else onChange(0); + }; - // نمایش با فرمت (گروه‌بندی هزارگان) — اکنون همیشه نمایش فرمت‌شده است - const displayValue = useMemo(() => formatWithLocale(raw, locale), [raw, locale]); + const displayValue = useMemo(() => formatWithLocale(raw, locale), [raw, locale]); - // هندلر تغییر ورودی (تایپ/حذف) - const handleChange = (e: React.ChangeEvent) => { - const v = e.target.value; - // حذف کاماها قبل از پردازش - const withoutCommas = v.replace(/[,٬\s\u200C]/g, ""); - // تبدیل ارقام فارسی/عربی و حذف هرچیز غیر رقمی - const digits = onlyDigits(withoutCommas); - // می‌تونی محدودیت طول بذاری اگر خواستی: e.g. digits.slice(0, 12) - setRaw(digits); - emitNumber(digits); - }; + const handleChange = (e: React.ChangeEvent) => { + const v = e.target.value; + const withoutCommas = v.replace(/[,٬\s\u200C]/g, ""); + const digits = onlyDigits(withoutCommas); + setRaw(digits); + emitNumber(digits); + }; - // کنترل پیست (paste) - const handlePaste = (e: React.ClipboardEvent) => { - const text = e.clipboardData.getData("text"); - const digits = onlyDigits(text); - if (!digits) { - e.preventDefault(); - return; - } - e.preventDefault(); - setRaw(digits); - emitNumber(digits); - }; + // کنترل پیست (paste) + const handlePaste = (e: React.ClipboardEvent) => { + const text = e.clipboardData.getData("text"); + const digits = onlyDigits(text); + if (!digits) { + e.preventDefault(); + return; + } + e.preventDefault(); + setRaw(digits); + emitNumber(digits); + }; - const hasValue = raw.trim() !== ""; + const hasValue = raw.trim() !== ""; - return ( -
-
- setFocused(true)} - onBlur={() => setFocused(false)} - onPaste={handlePaste} - className={`w-full text-right bg-white outline-none transition-shadow duration-150 border-2 rounded-full py-2 px-6 border-[#6a3f7f] font-medium`} - /> -
- تومان -
+ return ( +
+
+ setFocused(true)} + onBlur={() => setFocused(false)} + onPaste={handlePaste} + className={`w-full text-right bg-white outline-none transition-shadow duration-150 border-2 rounded-full py-2 px-6 border-[#6a3f7f] font-medium`} + /> +
+ تومان +
- {/* فلوٹنگ لیبل — وقتی فوکس یا مقدار وجود داشته باشه کوچک و بالاتر میره (سمت راست) */} - -
-
- ); + > + {label} + +
+
+ ); }; export default AmountInput; diff --git a/src/apps/new-ui/pages/ReportAll/Income&Cost/Create/index.tsx b/src/apps/new-ui/pages/ReportAll/Income&Cost/Create/index.tsx index 8f866242..b775ede9 100644 --- a/src/apps/new-ui/pages/ReportAll/Income&Cost/Create/index.tsx +++ b/src/apps/new-ui/pages/ReportAll/Income&Cost/Create/index.tsx @@ -41,7 +41,12 @@ const CreateIncomeAndCost: React.FC = () => { const navigate = useNavigate(); - const [reportActionsType, setReportActionsType] = useState(null); + // ------------ states -------------- + // selectedActionId => برای نمایش استایل انتخاب‌شده در Card (عدد) + const [selectedActionId, setSelectedActionId] = useState(null); + // reportActionsTypeTitle => عنوان مربوط به آیتم انتخاب شده (string|null) + const [reportActionsTypeTitle, setReportActionsTypeTitle] = useState(null); + const [amount, setAmount] = useState(null); const [date, setDate] = useState(""); const [description, setDescription] = useState(""); @@ -65,6 +70,68 @@ const CreateIncomeAndCost: React.FC = () => { const upsertIncome = upsertIncomeHook.mutate; const upsertExpense = upsertExpenseHook.mutate; + // ---------- helpers ---------- + const findItemById = (idVal: number | string | null) => + pageMode.find((p) => String(p.id) === String(idVal)) ?? null; + const findItemByTitle = (title: string | null) => + pageMode.find((p) => p.title === (title ?? "").trim()) ?? null; + + // ---------- handler for Card fallback ---------- + // Card ممکنه عدد (id)، عنوان (string)، یا شیء ارسال کنه — همه حالت‌ها رو هندل می‌کنیم. + const handleSelectReportAction = (val: any) => { + if (val === null || val === undefined) { + setSelectedActionId(null); + setReportActionsTypeTitle(null); + // اگر خواستی textarea پاک بشه: + setDescription(""); + return; + } + + let resolvedId: number | null = null; + let resolvedTitle: string | null = null; + + if (typeof val === "number") { + resolvedId = val; + const found = findItemById(val); + resolvedTitle = found ? found.title : String(val); + } else if (typeof val === "string") { + // ممکنه string عنوان باشه یا id به صورت رشته + const byId = findItemById(val); + if (byId) { + resolvedId = byId.id; + resolvedTitle = byId.title; + } else { + const byTitle = findItemByTitle(val); + if (byTitle) { + resolvedId = byTitle.id; + resolvedTitle = byTitle.title; + } else { + // fallback: treat as title + resolvedTitle = val; + } + } + } else if (typeof val === "object") { + // اگر آبجکت شامل id یا title باشه + if (val.id !== undefined && val.id !== null) { + resolvedId = Number(val.id); + } + if (val.title) resolvedTitle = String(val.title); + if (!resolvedTitle && resolvedId !== null) { + const f = findItemById(resolvedId); + if (f) resolvedTitle = f.title; + } + } else { + // fallback stringify + resolvedTitle = String(val); + } + + setSelectedActionId(resolvedId); + setReportActionsTypeTitle(resolvedTitle); + // وقتی کاربر روی کارت زد، عنوان بیاد توی textarea (طبق خواست شما) + if (resolvedTitle) setDescription(resolvedTitle); + }; + + // ---------- populate edit values from server ---------- useEffect(() => { const source = mode === "cost" ? expenseData : mode === "income" ? incomeData : undefined; if (!id || !source) return; @@ -81,7 +148,36 @@ const CreateIncomeAndCost: React.FC = () => { setAmount(parsedAmount); setDate(srcDate); setDescription(srcDescription); - setReportActionsType(srcReportActionsType); + + // srcReportActionsType ممکنه id یا title باشه: + let resolvedId: number | null = null; + let resolvedTitle: string | null = null; + + // try id + const byId = findItemById(srcReportActionsType); + if (byId) { + resolvedId = byId.id; + resolvedTitle = byId.title; + } else { + // try title + const byTitle = findItemByTitle(srcReportActionsType); + if (byTitle) { + resolvedId = byTitle.id; + resolvedTitle = byTitle.title; + } + } + + // اگر سرور reportActionsType نداشت، ولی description دقیقا یک title هست، اون رو انتخاب کن + if (!resolvedTitle) { + const matchFromDesc = findItemByTitle(srcDescription); + if (matchFromDesc) { + resolvedId = matchFromDesc.id; + resolvedTitle = matchFromDesc.title; + } + } + + setSelectedActionId(resolvedId); + setReportActionsTypeTitle(resolvedTitle); setInitialValues({ amount: parsedAmount, @@ -89,7 +185,25 @@ const CreateIncomeAndCost: React.FC = () => { description: srcDescription, reportActionsType: srcReportActionsType, }); - }, [id, mode, expenseData, incomeData]); + }, [id, mode, expenseData, incomeData, pageMode]); + + // ---------- وقتی کاربر تایپ کرد و متن دقیقا با یک title برابر شد، id را ست کن ---------- + useEffect(() => { + const trimmed = description.trim(); + if (trimmed === "") { + setSelectedActionId(null); + setReportActionsTypeTitle(null); + return; + } + const match = findItemByTitle(trimmed); + if (match) { + setSelectedActionId(match.id); + setReportActionsTypeTitle(match.title); + } else { + setSelectedActionId(null); + setReportActionsTypeTitle(null); + } + }, [description, pageMode]); const trimmedDescription = description.trim(); @@ -97,10 +211,10 @@ const CreateIncomeAndCost: React.FC = () => { amount !== initialValues.amount || date !== initialValues.date || trimmedDescription !== initialValues.description || - reportActionsType !== initialValues.reportActionsType; + reportActionsTypeTitle !== initialValues.reportActionsType; const canSubmit = - Boolean(amount && date && trimmedDescription !== "" && reportActionsType) && (id ? isChanged : true); + Boolean(amount && date && trimmedDescription !== "") && (id ? isChanged : true); const submitHandler = () => { if (!canSubmit) return; @@ -137,11 +251,12 @@ const CreateIncomeAndCost: React.FC = () => {
+ {/* به Card عدد id رو می‌دیم تا استایل انتخابی نمایش داده بشه */}
diff --git a/vite.config.ts.timestamp-1760862718890-42cf24eefc95d.mjs b/vite.config.ts.timestamp-1760862718890-42cf24eefc95d.mjs new file mode 100644 index 00000000..91b01586 --- /dev/null +++ b/vite.config.ts.timestamp-1760862718890-42cf24eefc95d.mjs @@ -0,0 +1,211 @@ +// vite.config.ts +import { VitePWA } from "file:///D:/Programming/Web/react/Salona-new-version/node_modules/.pnpm/vite-plugin-pwa@0.21.0_@vit_7b4554c40dbd6718eec754f594d7dff5/node_modules/vite-plugin-pwa/dist/index.js"; +import react from "file:///D:/Programming/Web/react/Salona-new-version/node_modules/.pnpm/@vitejs+plugin-react-swc@3._4951458e7c610861b13cb5e14fda6531/node_modules/@vitejs/plugin-react-swc/index.mjs"; +import { defineConfig } from "file:///D:/Programming/Web/react/Salona-new-version/node_modules/.pnpm/vite@5.4.10_@types+node@22._af8e64f0dfb3fff02f0a6040c7895f8b/node_modules/vite/dist/node/index.js"; +import path from "path"; + +// package.json +var package_default = { + name: "app.salona", + private: true, + version: "2.74.6", + type: "module", + scripts: { + dev: "vite --host", + build: "tsc -b && vite build", + lint: "eslint .", + preview: "vite preview", + "generate-pwa-icons": "pwa-assets-generator" + }, + dependencies: { + "@emoji-mart/data": "^1.2.1", + "@emoji-mart/react": "^1.1.1", + "@heroicons/react": "^2.2.0", + "@lottiefiles/dotlottie-react": "^0.13.5", + "@radix-ui/react-accordion": "1.2.1", + "@radix-ui/react-avatar": "1.1.1", + "@radix-ui/react-dialog": "1.1.2", + "@radix-ui/react-label": "2.1.0", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-select": "2.1.2", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-switch": "1.1.1", + "@radix-ui/react-tabs": "1.1.2", + "@radix-ui/react-tooltip": "1.1.3", + "@tabler/icons-react": "^3.35.0", + "@tailwindcss/vite": "^4.1.14", + "@tanstack/react-query": "5.59.20", + "@zxing/browser": "^0.1.5", + aos: "2.3.4", + axios: "1.7.7", + "class-variance-authority": "0.7.0", + clsx: "2.1.1", + "date-fns": "^4.1.0", + dayjs: "^1.11.18", + "emoji-mart": "^5.6.0", + "emoji-picker-react": "^4.14.0", + firebase: "^11.10.0", + "html-to-image": "^1.11.13", + "input-otp": "1.4.1", + "jalaali-js": "^1.2.8", + "jalali-moment": "3.3.11", + jalaliday: "^3.1.1", + leaflet: "1.9.4", + "lucide-react": "0.456.0", + motion: "^12.23.24", + "qr-scanner": "^1.4.2", + "qrcode.react": "^4.2.0", + react: "18.3.1", + "react-date-object": "2.1.9", + "react-datetime-picker": "^7.0.1", + "react-day-picker": "^9.11.1", + "react-device-detect": "^2.2.3", + "react-dom": "18.3.1", + "react-element-to-jsx-string": "^17.0.1", + "react-error-boundary": "4.1.2", + "react-fast-marquee": "^1.6.5", + "react-h5-audio-player": "^3.10.1", + "react-hook-form": "7.53.2", + "react-hot-toast": "2.4.1", + "react-html-parser": "^2.0.2", + "react-icons": "^5.5.0", + "react-image-crop": "11.0.7", + "react-leaflet": "4.2.1", + "react-multi-date-picker": "4.5.2", + "react-player": "^3.3.3", + "react-qr-reader": "3.0.0-beta-1", + "react-qr-scanner": "1.0.0-alpha.11", + "react-router-dom": "6.28.0", + "react-select": "^5.10.2", + "react-simple-captcha": "^9.3.1", + "react-spinners": "0.14.1", + "react-time-picker": "^8.0.2", + "react-type-animation": "^3.2.0", + "react-video-player": "^1.0.0", + recharts: "^2.15.4", + sweetalert2: "11.14.5", + swiper: "11.2.2", + "tailwind-merge": "2.5.4", + "tailwindcss-animate": "1.0.7", + "use-local-storage": "3.0.0", + vaul: "1.1.1", + "video-react": "^0.16.0", + "workbox-precaching": "^7.3.0", + "workbox-window": "^7.3.0" + }, + devDependencies: { + "@eslint/js": "9.13.0", + "@svgr/webpack": "^8.1.0", + "@types/aos": "3.0.7", + "@types/leaflet": "1.9.14", + "@types/node": "22.9.0", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", + "@types/react-html-parser": "^2.0.7", + "@types/swiper": "^6.0.0", + "@vite-pwa/assets-generator": "0.2.6", + "@vitejs/plugin-react-swc": "3.5.0", + autoprefixer: "10.4.20", + eslint: "9.13.0", + "eslint-plugin-react-hooks": "5.0.0", + "eslint-plugin-react-refresh": "0.4.14", + globals: "15.11.0", + postcss: "8.4.47", + tailwindcss: "3.4.14", + typescript: "~5.6.3", + "typescript-eslint": "8.11.0", + vite: "5.4.10", + "vite-plugin-pwa": "0.21.0" + }, + resolutions: { + sharp: "0.32.6", + "sharp-ico": "0.1.5" + } +}; + +// vite.config.ts +var __vite_injected_original_dirname = "D:\\Programming\\Web\\react\\Salona-new-version"; +var vite_config_default = defineConfig({ + plugins: [ + react(), + VitePWA({ + strategies: "injectManifest", + srcDir: "src", + filename: "sw.js", + registerType: "autoUpdate", + injectRegister: "auto", + // injectRegister: false, + manifest: { + name: "Salona", + short_name: "Salona", + theme_color: "rgb(171,37,192)", + background_color: "rgb(171,37,192)", + dir: "rtl", + lang: "fa", + description: "Salona is a platform to provide innovative tools to hairdressers and beauty salons.", + display_override: ["standalone", "fullscreen", "window-controls-overlay", "browser"], + icons: [ + { + src: "pwa-64x64.png", + sizes: "64x64", + type: "image/png" + }, + { + src: "pwa-192x192.png", + sizes: "192x192", + type: "image/png" + }, + { + src: "pwa-512x512.png", + sizes: "512x512", + type: "image/png" + }, + { + src: "maskable-icon-512x512.png", + sizes: "512x512", + type: "image/png", + purpose: "maskable" + } + ] + }, + injectManifest: { + globPatterns: ["**/*.{js,css,html,svg,png,ico}"], + maximumFileSizeToCacheInBytes: 10 * 1024 * 1024 + }, + devOptions: { + enabled: false, + navigateFallback: "index.html", + suppressWarnings: true, + type: "module" + } + }) + ], + resolve: { + alias: { + "@": path.resolve(__vite_injected_original_dirname, "./src"), + "@package.json": path.resolve(__vite_injected_original_dirname, "./package.json"), + "@appointment": path.resolve(__vite_injected_original_dirname, "./src/apps/appointment"), + "@card": path.resolve(__vite_injected_original_dirname, "./src/apps/card"), + "@new-ui": path.resolve(__vite_injected_original_dirname, "./src/apps/new-ui") + } + }, + // مهم: server settings برای dev/tunnel + server: { + host: true, + // bind به 0.0.0.0 (مهم برای تونل) + port: 5173 + // مطمئن شو با پورتی که تونل بهش متصل میشه یکیه + }, + build: { + rollupOptions: { + external: ["workbox-window"] + } + }, + define: { + "import.meta.env.VITE_APP_VERSION": JSON.stringify(package_default.version) + } +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiLCAicGFja2FnZS5qc29uIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiRDpcXFxcUHJvZ3JhbW1pbmdcXFxcV2ViXFxcXHJlYWN0XFxcXFNhbG9uYS1uZXctdmVyc2lvblwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiRDpcXFxcUHJvZ3JhbW1pbmdcXFxcV2ViXFxcXHJlYWN0XFxcXFNhbG9uYS1uZXctdmVyc2lvblxcXFx2aXRlLmNvbmZpZy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vRDovUHJvZ3JhbW1pbmcvV2ViL3JlYWN0L1NhbG9uYS1uZXctdmVyc2lvbi92aXRlLmNvbmZpZy50c1wiO2ltcG9ydCB7IFZpdGVQV0EgfSBmcm9tIFwidml0ZS1wbHVnaW4tcHdhXCI7XHJcbmltcG9ydCByZWFjdCBmcm9tIFwiQHZpdGVqcy9wbHVnaW4tcmVhY3Qtc3djXCI7XHJcbmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gXCJ2aXRlXCI7XHJcbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XHJcbmltcG9ydCBwa2cgZnJvbSBcIi4vcGFja2FnZS5qc29uXCIgYXNzZXJ0IHsgdHlwZTogXCJqc29uXCIgfTtcclxuXHJcbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXHJcbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XHJcbiAgICBwbHVnaW5zOiBbXHJcbiAgICAgICAgcmVhY3QoKSxcclxuICAgICAgICBWaXRlUFdBKHtcclxuICAgICAgICAgICAgc3RyYXRlZ2llczogXCJpbmplY3RNYW5pZmVzdFwiLFxyXG4gICAgICAgICAgICBzcmNEaXI6IFwic3JjXCIsXHJcbiAgICAgICAgICAgIGZpbGVuYW1lOiBcInN3LmpzXCIsXHJcbiAgICAgICAgICAgIHJlZ2lzdGVyVHlwZTogXCJhdXRvVXBkYXRlXCIsXHJcbiAgICAgICAgICAgIGluamVjdFJlZ2lzdGVyOiBcImF1dG9cIixcclxuICAgICAgICAgICAgLy8gaW5qZWN0UmVnaXN0ZXI6IGZhbHNlLFxyXG4gICAgICAgICAgICBtYW5pZmVzdDoge1xyXG4gICAgICAgICAgICAgICAgbmFtZTogXCJTYWxvbmFcIixcclxuICAgICAgICAgICAgICAgIHNob3J0X25hbWU6IFwiU2Fsb25hXCIsXHJcbiAgICAgICAgICAgICAgICB0aGVtZV9jb2xvcjogXCJyZ2IoMTcxLDM3LDE5MilcIixcclxuICAgICAgICAgICAgICAgIGJhY2tncm91bmRfY29sb3I6IFwicmdiKDE3MSwzNywxOTIpXCIsXHJcbiAgICAgICAgICAgICAgICBkaXI6IFwicnRsXCIsXHJcbiAgICAgICAgICAgICAgICBsYW5nOiBcImZhXCIsXHJcbiAgICAgICAgICAgICAgICBkZXNjcmlwdGlvbjogXCJTYWxvbmEgaXMgYSBwbGF0Zm9ybSB0byBwcm92aWRlIGlubm92YXRpdmUgdG9vbHMgdG8gaGFpcmRyZXNzZXJzIGFuZCBiZWF1dHkgc2Fsb25zLlwiLFxyXG4gICAgICAgICAgICAgICAgZGlzcGxheV9vdmVycmlkZTogW1wic3RhbmRhbG9uZVwiLCBcImZ1bGxzY3JlZW5cIiwgXCJ3aW5kb3ctY29udHJvbHMtb3ZlcmxheVwiLCBcImJyb3dzZXJcIl0sXHJcbiAgICAgICAgICAgICAgICBpY29uczogW1xyXG4gICAgICAgICAgICAgICAgICAgIHtcclxuICAgICAgICAgICAgICAgICAgICAgICAgc3JjOiBcInB3YS02NHg2NC5wbmdcIixcclxuICAgICAgICAgICAgICAgICAgICAgICAgc2l6ZXM6IFwiNjR4NjRcIixcclxuICAgICAgICAgICAgICAgICAgICAgICAgdHlwZTogXCJpbWFnZS9wbmdcIixcclxuICAgICAgICAgICAgICAgICAgICB9LFxyXG4gICAgICAgICAgICAgICAgICAgIHtcclxuICAgICAgICAgICAgICAgICAgICAgICAgc3JjOiBcInB3YS0xOTJ4MTkyLnBuZ1wiLFxyXG4gICAgICAgICAgICAgICAgICAgICAgICBzaXplczogXCIxOTJ4MTkyXCIsXHJcbiAgICAgICAgICAgICAgICAgICAgICAgIHR5cGU6IFwiaW1hZ2UvcG5nXCIsXHJcbiAgICAgICAgICAgICAgICAgICAgfSxcclxuICAgICAgICAgICAgICAgICAgICB7XHJcbiAgICAgICAgICAgICAgICAgICAgICAgIHNyYzogXCJwd2EtNTEyeDUxMi5wbmdcIixcclxuICAgICAgICAgICAgICAgICAgICAgICAgc2l6ZXM6IFwiNTEyeDUxMlwiLFxyXG4gICAgICAgICAgICAgICAgICAgICAgICB0eXBlOiBcImltYWdlL3BuZ1wiLFxyXG4gICAgICAgICAgICAgICAgICAgIH0sXHJcbiAgICAgICAgICAgICAgICAgICAge1xyXG4gICAgICAgICAgICAgICAgICAgICAgICBzcmM6IFwibWFza2FibGUtaWNvbi01MTJ4NTEyLnBuZ1wiLFxyXG4gICAgICAgICAgICAgICAgICAgICAgICBzaXplczogXCI1MTJ4NTEyXCIsXHJcbiAgICAgICAgICAgICAgICAgICAgICAgIHR5cGU6IFwiaW1hZ2UvcG5nXCIsXHJcbiAgICAgICAgICAgICAgICAgICAgICAgIHB1cnBvc2U6IFwibWFza2FibGVcIixcclxuICAgICAgICAgICAgICAgICAgICB9LFxyXG4gICAgICAgICAgICAgICAgXSxcclxuICAgICAgICAgICAgfSxcclxuXHJcbiAgICAgICAgICAgIGluamVjdE1hbmlmZXN0OiB7XHJcbiAgICAgICAgICAgICAgICBnbG9iUGF0dGVybnM6IFtcIioqLyoue2pzLGNzcyxodG1sLHN2ZyxwbmcsaWNvfVwiXSxcclxuICAgICAgICAgICAgICAgIG1heGltdW1GaWxlU2l6ZVRvQ2FjaGVJbkJ5dGVzOiAxMCAqIDEwMjQgKiAxMDI0LFxyXG4gICAgICAgICAgICB9LFxyXG5cclxuICAgICAgICAgICAgZGV2T3B0aW9uczoge1xyXG4gICAgICAgICAgICAgICAgZW5hYmxlZDogZmFsc2UsXHJcbiAgICAgICAgICAgICAgICBuYXZpZ2F0ZUZhbGxiYWNrOiBcImluZGV4Lmh0bWxcIixcclxuICAgICAgICAgICAgICAgIHN1cHByZXNzV2FybmluZ3M6IHRydWUsXHJcbiAgICAgICAgICAgICAgICB0eXBlOiBcIm1vZHVsZVwiLFxyXG4gICAgICAgICAgICB9LFxyXG4gICAgICAgIH0pLFxyXG4gICAgXSxcclxuICAgIHJlc29sdmU6IHtcclxuICAgICAgICBhbGlhczoge1xyXG4gICAgICAgICAgICBcIkBcIjogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgXCIuL3NyY1wiKSxcclxuICAgICAgICAgICAgXCJAcGFja2FnZS5qc29uXCI6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsIFwiLi9wYWNrYWdlLmpzb25cIiksXHJcbiAgICAgICAgICAgIFwiQGFwcG9pbnRtZW50XCI6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsIFwiLi9zcmMvYXBwcy9hcHBvaW50bWVudFwiKSxcclxuICAgICAgICAgICAgXCJAY2FyZFwiOiBwYXRoLnJlc29sdmUoX19kaXJuYW1lLCBcIi4vc3JjL2FwcHMvY2FyZFwiKSxcclxuICAgICAgICAgICAgXCJAbmV3LXVpXCI6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsIFwiLi9zcmMvYXBwcy9uZXctdWlcIiksXHJcbiAgICAgICAgfSxcclxuICAgIH0sXHJcbiAgICAvLyBcdTA2NDVcdTA2NDdcdTA2NDU6IHNlcnZlciBzZXR0aW5ncyBcdTA2MjhcdTA2MzFcdTA2MjdcdTA2Q0MgZGV2L3R1bm5lbFxyXG4gICAgc2VydmVyOiB7XHJcbiAgICAgICAgaG9zdDogdHJ1ZSwgLy8gYmluZCBcdTA2MjhcdTA2NDcgMC4wLjAuMCAoXHUwNjQ1XHUwNjQ3XHUwNjQ1IFx1MDYyOFx1MDYzMVx1MDYyN1x1MDZDQyBcdTA2MkFcdTA2NDhcdTA2NDZcdTA2NDQpXHJcbiAgICAgICAgcG9ydDogNTE3MywgLy8gXHUwNjQ1XHUwNjM3XHUwNjQ1XHUwNjI2XHUwNjQ2IFx1MDYzNFx1MDY0OCBcdTA2MjhcdTA2MjcgXHUwNjdFXHUwNjQ4XHUwNjMxXHUwNjJBXHUwNkNDIFx1MDZBOVx1MDY0NyBcdTA2MkFcdTA2NDhcdTA2NDZcdTA2NDQgXHUwNjI4XHUwNjQ3XHUwNjM0IFx1MDY0NVx1MDYyQVx1MDYzNVx1MDY0NCBcdTA2NDVcdTA2Q0NcdTA2MzRcdTA2NDcgXHUwNkNDXHUwNkE5XHUwNkNDXHUwNjQ3XHJcbiAgICB9LFxyXG4gICAgYnVpbGQ6IHtcclxuICAgICAgICByb2xsdXBPcHRpb25zOiB7XHJcbiAgICAgICAgICAgIGV4dGVybmFsOiBbXCJ3b3JrYm94LXdpbmRvd1wiXSxcclxuICAgICAgICB9LFxyXG4gICAgfSxcclxuICAgIGRlZmluZToge1xyXG4gICAgICAgIFwiaW1wb3J0Lm1ldGEuZW52LlZJVEVfQVBQX1ZFUlNJT05cIjogSlNPTi5zdHJpbmdpZnkocGtnLnZlcnNpb24pLFxyXG4gICAgfSxcclxufSk7XHJcbiIsICJ7XHJcbiAgXCJuYW1lXCI6IFwiYXBwLnNhbG9uYVwiLFxyXG4gIFwicHJpdmF0ZVwiOiB0cnVlLFxyXG4gIFwidmVyc2lvblwiOiBcIjIuNzQuNlwiLFxyXG4gIFwidHlwZVwiOiBcIm1vZHVsZVwiLFxyXG4gIFwic2NyaXB0c1wiOiB7XHJcbiAgICBcImRldlwiOiBcInZpdGUgLS1ob3N0XCIsXHJcbiAgICBcImJ1aWxkXCI6IFwidHNjIC1iICYmIHZpdGUgYnVpbGRcIixcclxuICAgIFwibGludFwiOiBcImVzbGludCAuXCIsXHJcbiAgICBcInByZXZpZXdcIjogXCJ2aXRlIHByZXZpZXdcIixcclxuICAgIFwiZ2VuZXJhdGUtcHdhLWljb25zXCI6IFwicHdhLWFzc2V0cy1nZW5lcmF0b3JcIlxyXG4gIH0sXHJcbiAgXCJkZXBlbmRlbmNpZXNcIjoge1xyXG4gICAgXCJAZW1vamktbWFydC9kYXRhXCI6IFwiXjEuMi4xXCIsXHJcbiAgICBcIkBlbW9qaS1tYXJ0L3JlYWN0XCI6IFwiXjEuMS4xXCIsXHJcbiAgICBcIkBoZXJvaWNvbnMvcmVhY3RcIjogXCJeMi4yLjBcIixcclxuICAgIFwiQGxvdHRpZWZpbGVzL2RvdGxvdHRpZS1yZWFjdFwiOiBcIl4wLjEzLjVcIixcclxuICAgIFwiQHJhZGl4LXVpL3JlYWN0LWFjY29yZGlvblwiOiBcIjEuMi4xXCIsXHJcbiAgICBcIkByYWRpeC11aS9yZWFjdC1hdmF0YXJcIjogXCIxLjEuMVwiLFxyXG4gICAgXCJAcmFkaXgtdWkvcmVhY3QtZGlhbG9nXCI6IFwiMS4xLjJcIixcclxuICAgIFwiQHJhZGl4LXVpL3JlYWN0LWxhYmVsXCI6IFwiMi4xLjBcIixcclxuICAgIFwiQHJhZGl4LXVpL3JlYWN0LXBvcG92ZXJcIjogXCJeMS4xLjE1XCIsXHJcbiAgICBcIkByYWRpeC11aS9yZWFjdC1zZWxlY3RcIjogXCIyLjEuMlwiLFxyXG4gICAgXCJAcmFkaXgtdWkvcmVhY3Qtc2xvdFwiOiBcIjEuMS4wXCIsXHJcbiAgICBcIkByYWRpeC11aS9yZWFjdC1zd2l0Y2hcIjogXCIxLjEuMVwiLFxyXG4gICAgXCJAcmFkaXgtdWkvcmVhY3QtdGFic1wiOiBcIjEuMS4yXCIsXHJcbiAgICBcIkByYWRpeC11aS9yZWFjdC10b29sdGlwXCI6IFwiMS4xLjNcIixcclxuICAgIFwiQHRhYmxlci9pY29ucy1yZWFjdFwiOiBcIl4zLjM1LjBcIixcclxuICAgIFwiQHRhaWx3aW5kY3NzL3ZpdGVcIjogXCJeNC4xLjE0XCIsXHJcbiAgICBcIkB0YW5zdGFjay9yZWFjdC1xdWVyeVwiOiBcIjUuNTkuMjBcIixcclxuICAgIFwiQHp4aW5nL2Jyb3dzZXJcIjogXCJeMC4xLjVcIixcclxuICAgIFwiYW9zXCI6IFwiMi4zLjRcIixcclxuICAgIFwiYXhpb3NcIjogXCIxLjcuN1wiLFxyXG4gICAgXCJjbGFzcy12YXJpYW5jZS1hdXRob3JpdHlcIjogXCIwLjcuMFwiLFxyXG4gICAgXCJjbHN4XCI6IFwiMi4xLjFcIixcclxuICAgIFwiZGF0ZS1mbnNcIjogXCJeNC4xLjBcIixcclxuICAgIFwiZGF5anNcIjogXCJeMS4xMS4xOFwiLFxyXG4gICAgXCJlbW9qaS1tYXJ0XCI6IFwiXjUuNi4wXCIsXHJcbiAgICBcImVtb2ppLXBpY2tlci1yZWFjdFwiOiBcIl40LjE0LjBcIixcclxuICAgIFwiZmlyZWJhc2VcIjogXCJeMTEuMTAuMFwiLFxyXG4gICAgXCJodG1sLXRvLWltYWdlXCI6IFwiXjEuMTEuMTNcIixcclxuICAgIFwiaW5wdXQtb3RwXCI6IFwiMS40LjFcIixcclxuICAgIFwiamFsYWFsaS1qc1wiOiBcIl4xLjIuOFwiLFxyXG4gICAgXCJqYWxhbGktbW9tZW50XCI6IFwiMy4zLjExXCIsXHJcbiAgICBcImphbGFsaWRheVwiOiBcIl4zLjEuMVwiLFxyXG4gICAgXCJsZWFmbGV0XCI6IFwiMS45LjRcIixcclxuICAgIFwibHVjaWRlLXJlYWN0XCI6IFwiMC40NTYuMFwiLFxyXG4gICAgXCJtb3Rpb25cIjogXCJeMTIuMjMuMjRcIixcclxuICAgIFwicXItc2Nhbm5lclwiOiBcIl4xLjQuMlwiLFxyXG4gICAgXCJxcmNvZGUucmVhY3RcIjogXCJeNC4yLjBcIixcclxuICAgIFwicmVhY3RcIjogXCIxOC4zLjFcIixcclxuICAgIFwicmVhY3QtZGF0ZS1vYmplY3RcIjogXCIyLjEuOVwiLFxyXG4gICAgXCJyZWFjdC1kYXRldGltZS1waWNrZXJcIjogXCJeNy4wLjFcIixcclxuICAgIFwicmVhY3QtZGF5LXBpY2tlclwiOiBcIl45LjExLjFcIixcclxuICAgIFwicmVhY3QtZGV2aWNlLWRldGVjdFwiOiBcIl4yLjIuM1wiLFxyXG4gICAgXCJyZWFjdC1kb21cIjogXCIxOC4zLjFcIixcclxuICAgIFwicmVhY3QtZWxlbWVudC10by1qc3gtc3RyaW5nXCI6IFwiXjE3LjAuMVwiLFxyXG4gICAgXCJyZWFjdC1lcnJvci1ib3VuZGFyeVwiOiBcIjQuMS4yXCIsXHJcbiAgICBcInJlYWN0LWZhc3QtbWFycXVlZVwiOiBcIl4xLjYuNVwiLFxyXG4gICAgXCJyZWFjdC1oNS1hdWRpby1wbGF5ZXJcIjogXCJeMy4xMC4xXCIsXHJcbiAgICBcInJlYWN0LWhvb2stZm9ybVwiOiBcIjcuNTMuMlwiLFxyXG4gICAgXCJyZWFjdC1ob3QtdG9hc3RcIjogXCIyLjQuMVwiLFxyXG4gICAgXCJyZWFjdC1odG1sLXBhcnNlclwiOiBcIl4yLjAuMlwiLFxyXG4gICAgXCJyZWFjdC1pY29uc1wiOiBcIl41LjUuMFwiLFxyXG4gICAgXCJyZWFjdC1pbWFnZS1jcm9wXCI6IFwiMTEuMC43XCIsXHJcbiAgICBcInJlYWN0LWxlYWZsZXRcIjogXCI0LjIuMVwiLFxyXG4gICAgXCJyZWFjdC1tdWx0aS1kYXRlLXBpY2tlclwiOiBcIjQuNS4yXCIsXHJcbiAgICBcInJlYWN0LXBsYXllclwiOiBcIl4zLjMuM1wiLFxyXG4gICAgXCJyZWFjdC1xci1yZWFkZXJcIjogXCIzLjAuMC1iZXRhLTFcIixcclxuICAgIFwicmVhY3QtcXItc2Nhbm5lclwiOiBcIjEuMC4wLWFscGhhLjExXCIsXHJcbiAgICBcInJlYWN0LXJvdXRlci1kb21cIjogXCI2LjI4LjBcIixcclxuICAgIFwicmVhY3Qtc2VsZWN0XCI6IFwiXjUuMTAuMlwiLFxyXG4gICAgXCJyZWFjdC1zaW1wbGUtY2FwdGNoYVwiOiBcIl45LjMuMVwiLFxyXG4gICAgXCJyZWFjdC1zcGlubmVyc1wiOiBcIjAuMTQuMVwiLFxyXG4gICAgXCJyZWFjdC10aW1lLXBpY2tlclwiOiBcIl44LjAuMlwiLFxyXG4gICAgXCJyZWFjdC10eXBlLWFuaW1hdGlvblwiOiBcIl4zLjIuMFwiLFxyXG4gICAgXCJyZWFjdC12aWRlby1wbGF5ZXJcIjogXCJeMS4wLjBcIixcclxuICAgIFwicmVjaGFydHNcIjogXCJeMi4xNS40XCIsXHJcbiAgICBcInN3ZWV0YWxlcnQyXCI6IFwiMTEuMTQuNVwiLFxyXG4gICAgXCJzd2lwZXJcIjogXCIxMS4yLjJcIixcclxuICAgIFwidGFpbHdpbmQtbWVyZ2VcIjogXCIyLjUuNFwiLFxyXG4gICAgXCJ0YWlsd2luZGNzcy1hbmltYXRlXCI6IFwiMS4wLjdcIixcclxuICAgIFwidXNlLWxvY2FsLXN0b3JhZ2VcIjogXCIzLjAuMFwiLFxyXG4gICAgXCJ2YXVsXCI6IFwiMS4xLjFcIixcclxuICAgIFwidmlkZW8tcmVhY3RcIjogXCJeMC4xNi4wXCIsXHJcbiAgICBcIndvcmtib3gtcHJlY2FjaGluZ1wiOiBcIl43LjMuMFwiLFxyXG4gICAgXCJ3b3JrYm94LXdpbmRvd1wiOiBcIl43LjMuMFwiXHJcbiAgfSxcclxuICBcImRldkRlcGVuZGVuY2llc1wiOiB7XHJcbiAgICBcIkBlc2xpbnQvanNcIjogXCI5LjEzLjBcIixcclxuICAgIFwiQHN2Z3Ivd2VicGFja1wiOiBcIl44LjEuMFwiLFxyXG4gICAgXCJAdHlwZXMvYW9zXCI6IFwiMy4wLjdcIixcclxuICAgIFwiQHR5cGVzL2xlYWZsZXRcIjogXCIxLjkuMTRcIixcclxuICAgIFwiQHR5cGVzL25vZGVcIjogXCIyMi45LjBcIixcclxuICAgIFwiQHR5cGVzL3JlYWN0XCI6IFwiMTguMy4xMlwiLFxyXG4gICAgXCJAdHlwZXMvcmVhY3QtZG9tXCI6IFwiMTguMy4xXCIsXHJcbiAgICBcIkB0eXBlcy9yZWFjdC1odG1sLXBhcnNlclwiOiBcIl4yLjAuN1wiLFxyXG4gICAgXCJAdHlwZXMvc3dpcGVyXCI6IFwiXjYuMC4wXCIsXHJcbiAgICBcIkB2aXRlLXB3YS9hc3NldHMtZ2VuZXJhdG9yXCI6IFwiMC4yLjZcIixcclxuICAgIFwiQHZpdGVqcy9wbHVnaW4tcmVhY3Qtc3djXCI6IFwiMy41LjBcIixcclxuICAgIFwiYXV0b3ByZWZpeGVyXCI6IFwiMTAuNC4yMFwiLFxyXG4gICAgXCJlc2xpbnRcIjogXCI5LjEzLjBcIixcclxuICAgIFwiZXNsaW50LXBsdWdpbi1yZWFjdC1ob29rc1wiOiBcIjUuMC4wXCIsXHJcbiAgICBcImVzbGludC1wbHVnaW4tcmVhY3QtcmVmcmVzaFwiOiBcIjAuNC4xNFwiLFxyXG4gICAgXCJnbG9iYWxzXCI6IFwiMTUuMTEuMFwiLFxyXG4gICAgXCJwb3N0Y3NzXCI6IFwiOC40LjQ3XCIsXHJcbiAgICBcInRhaWx3aW5kY3NzXCI6IFwiMy40LjE0XCIsXHJcbiAgICBcInR5cGVzY3JpcHRcIjogXCJ+NS42LjNcIixcclxuICAgIFwidHlwZXNjcmlwdC1lc2xpbnRcIjogXCI4LjExLjBcIixcclxuICAgIFwidml0ZVwiOiBcIjUuNC4xMFwiLFxyXG4gICAgXCJ2aXRlLXBsdWdpbi1wd2FcIjogXCIwLjIxLjBcIlxyXG4gIH0sXHJcbiAgXCJyZXNvbHV0aW9uc1wiOiB7XHJcbiAgICBcInNoYXJwXCI6IFwiMC4zMi42XCIsXHJcbiAgICBcInNoYXJwLWljb1wiOiBcIjAuMS41XCJcclxuICB9XHJcbn1cclxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUE2VCxTQUFTLGVBQWU7QUFDclYsT0FBTyxXQUFXO0FBQ2xCLFNBQVMsb0JBQW9CO0FBQzdCLE9BQU8sVUFBVTs7O0FDSGpCO0FBQUEsRUFDRSxNQUFRO0FBQUEsRUFDUixTQUFXO0FBQUEsRUFDWCxTQUFXO0FBQUEsRUFDWCxNQUFRO0FBQUEsRUFDUixTQUFXO0FBQUEsSUFDVCxLQUFPO0FBQUEsSUFDUCxPQUFTO0FBQUEsSUFDVCxNQUFRO0FBQUEsSUFDUixTQUFXO0FBQUEsSUFDWCxzQkFBc0I7QUFBQSxFQUN4QjtBQUFBLEVBQ0EsY0FBZ0I7QUFBQSxJQUNkLG9CQUFvQjtBQUFBLElBQ3BCLHFCQUFxQjtBQUFBLElBQ3JCLG9CQUFvQjtBQUFBLElBQ3BCLGdDQUFnQztBQUFBLElBQ2hDLDZCQUE2QjtBQUFBLElBQzdCLDBCQUEwQjtBQUFBLElBQzFCLDBCQUEwQjtBQUFBLElBQzFCLHlCQUF5QjtBQUFBLElBQ3pCLDJCQUEyQjtBQUFBLElBQzNCLDBCQUEwQjtBQUFBLElBQzFCLHdCQUF3QjtBQUFBLElBQ3hCLDBCQUEwQjtBQUFBLElBQzFCLHdCQUF3QjtBQUFBLElBQ3hCLDJCQUEyQjtBQUFBLElBQzNCLHVCQUF1QjtBQUFBLElBQ3ZCLHFCQUFxQjtBQUFBLElBQ3JCLHlCQUF5QjtBQUFBLElBQ3pC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