import { toast } from "react-toastify";
import Cookies from "js-cookie";
import moment from "moment-jalaali";
import "moment-timezone";
// Data
import specialties from "@/data/specialties.json";
import states from "@/data/state.json";
import cities from "@/data/city.json";
export const numberToArStyle = (num) => num?.toLocaleString("ar-AE") || "";
// عناوین احترامی که در نام ذخیرهشدهٔ پزشک وجود ندارند و باید پیش از جستجو حذف شوند
const NAME_HONORIFICS = [
"دکتر",
"دكتر",
"پروفسور",
"خانم",
"آقای",
"اقای",
"آقا",
"اقا",
"جناب",
"سرکار",
];
// «دکتر مهدیه» → «مهدیه» تا جستجوی نام در API نتیجه بدهد.
// اگر بعد از حذف چیزی نماند، مقدار اصلی برگردانده میشود.
export const normalizeDoctorNameQuery = (value) => {
if (typeof value !== "string") return value;
let result = value.trim();
let changed = true;
while (changed) {
changed = false;
for (const honorific of NAME_HONORIFICS) {
if (result === honorific || result.startsWith(`${honorific} `)) {
result = result.slice(honorific.length).trim();
changed = true;
}
}
}
return result || value.trim();
};
export const isActiveURL = (link, name) => link === name;
// نگاشت پاسخ API بلاگ (image_url/tags/created_at/body/author-object) به شکلی که
// کامپوننتهای بلاگ انتظار دارند (images[{url}]/tag[{name}]/created/body.value/author-string).
export const normalizeBlog = (blog) => {
if (!blog) return null;
return {
...blog,
images: blog.image_url ? [{ url: blog.image_url }] : [],
tag: Array.isArray(blog.tags)
? blog.tags.map((t) => (typeof t === "string" ? { name: t } : t))
: [],
created: blog.created_at ?? blog.created ?? null,
body:
blog.body && typeof blog.body === "object"
? blog.body
: { value: blog.body ?? "" },
author:
blog.author && typeof blog.author === "object"
? blog.author.name
: blog.author ?? null,
};
};
// تبدیل مسیر نسبی تصویر (/uploads/...) به URL کامل backend.
// مسیرهای absolute (http...) و asset های محلی (/assets، /default) دستنخورده میمانند.
// تصویر پیشفرض همهٔ مقالهها — لیست، کارت، جزئیات، OG و توییتر همگی همین را
// میگیرند تا هیچ مقالهای بدون تصویر (یا با آواتار کاربر) نمایش داده نشود.
export const BLOG_FALLBACK_IMG = "/assets/images/blog-default-cover.png";
/**
* کاور یک مقاله با ترتیب تقدم og_image → image_url → images[0].
* جدا از imageUrl() است چون پیشفرض آن آواتار کاربر است و برای بلاگ غلط.
*/
export const blogCover = (blog) =>
imageUrl(
blog?.og_image || blog?.image_url || blog?.images?.[0]?.url,
BLOG_FALLBACK_IMG
);
export const imageUrl = (url, fallback = "/assets/images/user.png") => {
if (!url) return fallback;
if (/^https?:\/\//.test(url)) return url;
if (url.startsWith("/uploads")) {
const base = process.env.NEXT_PUBLIC_API_URL || "";
return `${base}${url}`;
}
return url;
};
// نام پزشک با پیشوند «دکتر»، بدون تکرار. بعضی رکوردها خودشان با «دکتر»/«دکتر.»
// ذخیره شدهاند؛ بدون این نرمالسازی خروجی «دکتر دکتر ...» میشود.
export const doctorTitle = (name) => {
const clean = (name || "").trim().replace(/^(دکتر|دكتر)[.،:]?\s+/, "");
return clean ? `دکتر ${clean}` : "";
};
export const convertToJalali = (date) => {
const currentDate = date;
const jalaliDate = moment(currentDate).format("jYYYY/jM/jD");
const jalaliYear = moment(jalaliDate, "jYYYY/jM/jD").jYear();
const jalaliMonth = moment(jalaliDate, "jYYYY/jM/jD").jMonth() + 1;
const jalaliDay = moment(jalaliDate, "jYYYY/jM/jD").jDate();
return `${jalaliYear}/${jalaliMonth}/${jalaliDay}`;
};
export const convertTimestampToJalali = (timestamp) => {
if (!timestamp) return "--";
const date = moment.unix(timestamp);
return date.format("jYYYY/jMM/jDD");
};
export const convertTimestampToTime = (timestamp) => {
if (!timestamp) return "--";
const date = moment.unix(timestamp);
return date.format("HH:mm");
};
export const handleTwoLength = (value) =>
String(value).length === 1 ? `0${value}` : value;
export const checkAllValues = (data) => {
for (let key in data) {
const item = data[key];
if (typeof item === "object" && item !== null) {
if ("value" in item) {
if (!item.value) {
return false;
}
} else {
if (!checkAllValues(item)) {
return false;
}
}
}
}
return true;
};
export const checkTimes = (arr) => {
return arr.map((day) => {
const hasMorningTime = day.morning_time.some((time) => time !== "");
const hasEveningTime = day.evening_time.some((time) => time !== "");
return hasMorningTime && hasEveningTime;
});
};
export const uploadImage = async (event) => {
let obj = {
file: "",
blob: "",
};
const file = event.target.files[0];
if (file) {
obj.file = file;
const readFileAsDataURL = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
reader.readAsDataURL(file);
});
};
try {
obj.blob = await readFileAsDataURL(file);
return obj;
} catch (error) {
console.error("Error reading file: ", error);
throw error;
}
} else {
throw new Error("No file selected");
}
};
export const alert = (type, text, prop) => {
toast[type](
پیغام{" "}
{type === "error"
? "خطا"
: type === "warning"
? "هشدار"
: type === "info"
? "اعلام"
: "موفقیت"}
{text}
,
{ prop }
);
};
export const formatPhoneNumber = (input) => {
if (typeof input !== "string") input = String(input);
const normalized = input
.replace(/[\u06F0-\u06F9]/g, (d) => String(d.charCodeAt(0) - 0x06f0))
.replace(/[\u0660-\u0669]/g, (d) => String(d.charCodeAt(0) - 0x0660));
const digits = normalized.replace(/\D/g, "");
const trimmed = digits.substring(0, 9);
const match = trimmed.match(/^(\d{0,2})(\d{0,3})(\d{0,4})$/);
if (!match) return trimmed;
return [match[1], match[2], match[3]].filter(Boolean).join(" ");
};
export function validatePhoneNumber(input) {
if (!input) {
return { valid: false, text: "شماره موبایل نمیتواند خالی باشد." };
}
// چک کردن اینکه فقط اعداد انگلیسی باشد
const hasNonEnglishDigits = /[^0-9]/.test(input);
if (hasNonEnglishDigits) {
return { valid: false, text: "فقط از اعداد انگلیسی استفاده کنید." };
}
if (input.length !== 11) {
return { valid: false, text: "شماره موبایل باید دقیقا 11 رقم باشد." };
}
const isValid = /^09\d{9}$/.test(input);
if (!isValid) {
return { valid: false, text: "شماره موبایل نامعتبر است." };
}
return { valid: true, text: null };
}
export const searchOnList = (list, value, name) => {
let newList = list;
newList = list.filter((item) => item[name]?.toLowerCase()?.includes(value));
return newList;
};
export function clearFilterParams(router, listRemove) {
const currentParams = new URLSearchParams(window.location.search);
const keysToRemove = listRemove;
keysToRemove.forEach((key) => currentParams.delete(key));
const queryString = currentParams.toString();
const newUrl = queryString ? `/doctors?${queryString}` : "/doctors";
router.push(newUrl);
}
export function clearDoctorClinicParams(router, slug) {
const clinicslug = slug;
const newUrl = `/clinic/${clinicslug}`;
router.push(newUrl)
}
/** گزینهٔ «کل گروه» در فهرست زیرتخصصها؛ با id تهی، چون حالتش نبودِ زیرتخصص است. */
export const allOfCategoryOption = (category) => ({
id: null,
name: `همه تخصصهای ${category.name}`,
isAll: true,
});
/**
* فیلتر بعدی وقتی کاربر گروه یا زیرتخصص را عوض میکند.
*
* تابع خالص است چون همین قاعده بود که شکسته بود: انتخاب یک گروه بیصدا اولین
* زیرتخصصش را مینشاند و جستجوی کاربر را تنگ میکرد. داخل کامپوننت، تستپذیر نبود.
*/
export const nextSpecialtyFilter = (filter, name, value) => {
const next = { ...filter, [name]: value };
// گروه یعنی کل گروه، نه اولین فرزندش.
if (name === "category") next.specialty = null;
// «همه تخصصهای X» زیرتخصص واقعی نیست؛ حالتش همان نبودِ زیرتخصص است. اگر خودش
// در فیلتر مینشست، نامش در URL میرفت و چون در specialties.json نیست، با رفرش
// یا اشتراک لینک بازسازی نمیشد.
if (name === "specialty" && value?.isAll) next.specialty = null;
return next;
};
export const filterList = (data) => {
const parentList = specialties.filter((item) => !item.parent_id);
const childrenList = specialties.filter((item) => item.parent_id);
if (!data?.category) {
return { parentList, childrenList: [] };
}
const ofCategory = childrenList.filter(
(item) => String(item.parent_id) === String(data.category.id)
);
// «همه» اول فهرست مینشیند تا برگشت از یک زیرتخصص به کل گروه ممکن باشد.
// خودش وارد فیلتر نمیشود؛ انتخابش یعنی specialty تهی، و آنوقت QueryForDoctorsReq
// به category.id برمیگردد که بکاند به همهٔ زیرشاخهها گسترشش میدهد.
return {
parentList,
childrenList: ofCategory.length
? [allOfCategoryOption(data.category), ...ofCategory]
: [],
};
};
export function getCleanNumberValue(value, defaultValue = 0) {
if (typeof value !== "string") value = String(value);
const persianArabicToEnglish = value
.replace(/[\u06F0-\u06F9]/g, (d) => String(d.charCodeAt(0) - 0x06f0))
.replace(/[\u0660-\u0669]/g, (d) => String(d.charCodeAt(0) - 0x0660));
const cleaned = persianArabicToEnglish.replace(/[^0-9.]/g, "");
const number = parseFloat(cleaned);
return isNaN(number) ? defaultValue : number;
}
export function hasDataChanged(previousData, newData) {
const oldDataString = JSON.stringify(
previousData,
Object.keys(previousData).sort()
);
const newDataString = JSON.stringify(newData, Object.keys(newData).sort());
return oldDataString !== newDataString;
}
export const getParsedUserInfo = () => {
const user = Cookies.get("userInfo");
if (!user) return null;
try {
return JSON.parse(user);
} catch {
return null;
}
};
export const handleTimeExpiresToken = (expires_in) => {
const accessTokenExpires = new Date();
accessTokenExpires.setSeconds(accessTokenExpires.getSeconds() + expires_in);
const refreshTokenExpires = new Date();
refreshTokenExpires.setDate(refreshTokenExpires.getDate() + 30);
return {
accessTokenExpires,
refreshTokenExpires,
};
};
export const removeAdditionalKeysDashboard = (information) => {
// فقط متادیتای سمت فرانت حذف میشود؛ national_code باید ارسال شود تا قابل ثبت/آپدیت باشد.
const {
changed,
created,
id,
status,
prev_data,
uuid,
...usedKays
} = information;
return usedKays;
};
export const changeDateType = (data, toTimeStamp) => {
const newData = { ...data };
const birthday = newData.birthday;
if (toTimeStamp) {
const m = moment(birthday, "jYYYY/jMM/jDD");
newData.birthday = m.unix();
} else {
newData.birthday = moment.unix(birthday).format("jYYYY/jMM/jDD");
}
return newData;
};
export const removeTokenHead = {
headers: {
Authorization: "",
},
};
export const dateToTimestamp = (date) => {
// تنظیم timezone به تهران
const tehranMoment = date.clone().tz("Asia/Tehran").startOf("day");
// Get the Unix timestamp in seconds
const inSeconde = tehranMoment.unix();
return inSeconde;
};
export const degreeVal = [
{ name: "پزشک عمومی", val: "general" },
{ name: "پزشک متخصص", val: "specialist" },
{ name: "پزشک فوق تخصص", val: "subspecialistplus" },
];
export const addLabelToList = (list) =>
list.map((item) => {
return {
...item,
label: item.name,
};
});
export const QueryForDoctorsFilter = (newFilter) => {
const query = {};
// 🔹 active => only when value === 1
if (newFilter.active === 1) {
query.active = 1;
}
// 🔹 specialty or category
if (newFilter.specialty) {
query.specialty = newFilter.specialty.name;
} else if (newFilter.category) {
query.specialty = newFilter.category.name;
}
// 🔹 city
if (newFilter.city) {
query.city = newFilter.city.name;
}
// 🔹 state
if (newFilter.state) {
query.state = newFilter.state.name;
}
// 🔹 degree (only if not "همه موارد")
if (newFilter.degree && newFilter.degree !== "همه موارد") {
query.degree = newFilter.degree;
}
// 🔹 gender (only if not "هر دو")
if (newFilter.gender && newFilter.gender !== "هر دو") {
query.gender = newFilter.gender;
}
// 🔹 name (only if has value)
if (newFilter.name && newFilter.name.trim() !== "") {
query.name = newFilter.name;
}
// 🔹 sort => only if value is 3,4,5
if ([3, 4, 5].includes(Number(newFilter.sort))) {
// query.sort = newFilter.sort === 3 ? "ASC" : "DESC";
query.sort = newFilter.sort;
}
return query;
};
export const QueryForClinicDoctorFilter = (newFilter) => {
const query = {};
// 🔹 active => only when value === 1
if (newFilter.active === 1) {
query.active = 1;
}
// 🔹 specialty or category
if (newFilter.specialty) {
query.specialty = newFilter.specialty.name;
} else if (newFilter.category) {
query.specialty = newFilter.category.name;
}
// 🔹 degree (only if not "همه موارد")
if (newFilter.degree && newFilter.degree !== "همه موارد") {
query.degree = newFilter.degree;
}
// 🔹 gender (only if not "هر دو")
if (newFilter.gender && newFilter.gender !== "هر دو") {
query.gender = newFilter.gender;
}
// 🔹 name (only if has value)
if (newFilter.name && newFilter.name.trim() !== "") {
query.name = newFilter.name;
}
// 🔹 sort => only if value is 3,4,5
if ([3, 4, 5].includes(Number(newFilter.sort))) {
// query.sort = newFilter.sort === 3 ? "ASC" : "DESC";
query.sort = newFilter.sort;
}
return query;
}
export const QueryForDoctorsReq = (newFilter) => {
const params = {};
if (newFilter.active === 1) params.active = 1;
if (newFilter.specialty?.id) {
params.specialty_id = newFilter.specialty.id;
} else if (newFilter.category?.id) {
params.specialty_id = newFilter.category.id;
}
if (newFilter.city?.id) params.city_id = newFilter.city.id;
if (newFilter.state?.id) params.state_id = newFilter.state.id;
if (newFilter.degree && newFilter.degree !== "همه موارد")
switch (newFilter.degree) {
case "کارشناس":
params.degree = "expert";
break;
case "پزشک عمومی":
params.degree = "general";
break;
case "پزشک متخصص":
params.degree = "specialist";
break;
case "پزشک فوق تخصص":
params.degree = "subspecialistplus";
break;
default:
break;
}
if (newFilter.gender && newFilter.gender !== "هر دو")
params.gender = newFilter.gender === "مرد" ? "man" : "woman";
if (newFilter.name) params.name = normalizeDoctorNameQuery(newFilter.name);
if (newFilter.sort) {
if (newFilter.sort === 3) params.sort = "ASC";
else if (newFilter.sort === 4 || newFilter.sort === 5) params.sort = "DESC";
}
return params;
};
export const QueryForDoctorsClinicReq = (newFilter) => {
const params = {};
if (newFilter.active === 1) params.active = 1;
if (newFilter.specialty?.id) {
params.specialty = newFilter.specialty.id;
} else if (newFilter.category?.id) {
params.specialty = newFilter.category.id;
}
if (newFilter.degree && newFilter.degree !== "همه موارد")
switch (newFilter.degree) {
case "کارشناس":
params.degree = "expert";
break;
case "پزشک عمومی":
params.degree = "general";
break;
case "پزشک متخصص":
params.degree = "specialist";
break;
case "پزشک فوق تخصص":
params.degree = "subspecialistplus";
break;
default:
break;
}
if (newFilter.gender && newFilter.gender !== "هر دو")
params.gender = newFilter.gender === "مرد" ? "man" : "woman";
if (newFilter.name) params.name = normalizeDoctorNameQuery(newFilter.name);
if (newFilter.sort) {
if (newFilter.sort === 3) params.sort = "ASC";
else if (newFilter.sort === 4 || newFilter.sort === 5) params.sort = "DESC";
}
return params;
}
export const buildDoctorParams = (searchParams) => {
const params = {};
// 🔹 active
if (searchParams.active && Number(searchParams.active) === 1) {
params.active = 1;
}
// 🔹 specialty (find by name in specialties.json)
if (searchParams.specialty) {
const spec = specialties.find((s) => s.name === searchParams.specialty);
if (spec) {
params.specialty_id = spec.id;
}
}
// 🔹 city (find by name in city.json)
if (searchParams.city) {
const cityObj = cities.find((c) => c.name === searchParams.city);
if (cityObj) {
params.city_id = cityObj.id;
}
}
// 🔹 state (find by name in state.json)
if (searchParams.state) {
const stateObj = states.find((s) => s.name === searchParams.state);
if (stateObj) {
params.state_id = stateObj.id;
}
}
// 🔹 degree → convert fa → en
if (searchParams.degree) {
switch (searchParams.degree) {
case "کارشناس":
params.degree = "expert";
break;
case "پزشک عمومی":
params.degree = "general";
break;
case "پزشک متخصص":
params.degree = "specialist";
break;
case "پزشک فوق تخصص":
params.degree = "subspecialistplus";
break;
default:
break;
}
}
// 🔹 gender
if (searchParams.gender) {
if (searchParams.gender === "مرد") params.gender = "man";
if (searchParams.gender === "زن") params.gender = "woman";
}
// 🔹 name
if (searchParams.name) {
params.name = normalizeDoctorNameQuery(searchParams.name);
}
// 🔹 sort (3 = ASC, 4|5 = DESC)
if (searchParams.sort) {
const sortNum = Number(searchParams.sort);
if (sortNum === 3) params.sort = "ASC";
else if ([4, 5].includes(sortNum)) params.sort = "DESC";
}
// 🔹 page
params.page = searchParams.page || 1;
// 🔹 limit
params.limit = 12;
return params;
};
export const QueryForClinicsFilter = (searchParams) => {
const params = {};
// 🔹 specialty (find by name in specialties.json)
if (searchParams.specialty) {
const spec = specialties.find((s) => s.name === searchParams.specialty);
if (spec) {
params.specialty = spec.id;
}
}
// 🔹 city (find by name in city.json)
if (searchParams.city) {
const cityObj = cities.find((c) => c.name === searchParams.city.name);
if (cityObj) {
params.city = cityObj.id;
}
}
// 🔹 state (find by name in state.json)
if (searchParams.province) {
const stateObj = states.find((s) => s.name === searchParams.province.name);
if (stateObj) {
params.state = stateObj.id;
}
}
// 🔹 page
params.page = searchParams.page || 1;
// 🔹 limit
params.limit = 12;
return params;
};
export const buildClinicParams = (searchParams) => {
const params = {};
// 🔹 city (find by name in city.json)
if (searchParams.city) {
const cityObj = cities.find((c) => c.name === searchParams.city);
if (cityObj) {
params.city = cityObj.id;
}
}
// 🔹 state (find by name in state.json)
if (searchParams.state) {
const stateObj = states.find((s) => s.name === searchParams.state);
if (stateObj) {
params.state = stateObj.id;
}
}
// 🔹 specialty (find by name in specialties.json)
if (searchParams.specialty) {
const spec = specialties.find((s) => s.name === searchParams.specialty);
if (spec) {
params.specialty = spec.id;
}
}
// 🔹 page
params.page = searchParams.page || 1;
// 🔹 limit
params.limit = 12;
return params;
};
export function timeAgo(timestamp) {
const now = moment();
const then = moment.unix(timestamp);
const diffSeconds = now.diff(then, "seconds");
if (diffSeconds < 60) {
return diffSeconds === 0 ? "همین الان" : `${diffSeconds} ثانیه پیش`;
} else if (diffSeconds < 3600) {
const minutes = Math.floor(diffSeconds / 60);
return `${minutes} دقیقه پیش`;
} else if (diffSeconds < 86400) {
const hours = Math.floor(diffSeconds / 3600);
return `${hours} ساعت پیش`;
} else if (diffSeconds < 2592000) {
const days = Math.floor(diffSeconds / 86400);
return `${days} روز پیش`;
} else if (diffSeconds < 31104000) {
const months = Math.floor(diffSeconds / 2592000);
return `${months} ماه پیش`;
} else {
const years = Math.floor(diffSeconds / 31104000);
return `${years} سال پیش`;
}
}
export function isUserLoggedIn() {
// access_token در memory است نه کوکی؛ userInfo کوکیِ non-httpOnly است که هنگام لاگین ست میشود.
return !!Cookies.get("userInfo");
}
export function convertTimestampToPersianDateSimple(timestamp) {
const date = new Date(timestamp * 1000);
const options = {
timeZone: "Asia/Tehran",
calendar: "persian",
year: "numeric",
month: "2-digit",
day: "2-digit",
weekday: "long",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: true,
};
return date.toLocaleString("fa-IR", options);
}
export function changeNumToDefault(str) {
if (!str) return "";
return str
.toString()
.replace(/[۰-۹]/g, d => "۰۱۲۳۴۵۶۷۸۹".indexOf(d))
.replace(/[٠-٩]/g, d => "٠١٢٣٤٥٦٧٨٩".indexOf(d));
}
// اعتبارسنجی کد ملی ایران: ۱۰ رقم + الگوریتم رقم کنترلی.
export const isValidIranNationalCode = (input) => {
if (!input) return false;
const code = String(input).replace(/\D/g, "");
if (!/^\d{10}$/.test(code)) return false;
// کدهای با ارقام یکسان (مثل 0000000000) نامعتبرند
if (/^(\d)\1{9}$/.test(code)) return false;
const check = +code[9];
let sum = 0;
for (let i = 0; i < 9; i++) sum += +code[i] * (10 - i);
const r = sum % 11;
return r < 2 ? check === r : check === 11 - r;
};