first commit

This commit is contained in:
2026-01-19 13:08:58 +03:30
commit 850b4a3f1e
293 changed files with 51775 additions and 0 deletions

57
src/utils/axios.ts Normal file
View File

@@ -0,0 +1,57 @@
import axios from "axios";
import { useUserStore } from "../context/zustand-store/userStore";
import { toast } from "react-toastify";
import { checkIsMobile } from "./checkIsMobile";
let hasShownUnauthorizedToast = false;
const getBaseURL = () => {
const hostname =
typeof window !== "undefined" ? window.location.hostname : "";
return hostname === "dam.rasadyar.com"
? "https://api.dam.rasadyar.com"
: "https://api.tdam.rasadyar.com";
};
const api = axios.create({
baseURL: getBaseURL(),
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
withCredentials: true,
});
api.interceptors.request.use(
(config) => {
const token = useUserStore.getState().auth;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401 && useUserStore.getState().auth) {
const logOut = useUserStore.getState().logOut;
if (!hasShownUnauthorizedToast) {
hasShownUnauthorizedToast = true;
toast.error("مجددا وارد شوید!", {
position: checkIsMobile() ? "bottom-center" : "top-center",
theme: "light",
rtl: true,
});
if (typeof logOut === "function") {
logOut();
}
}
}
return Promise.reject(error);
}
);
export default api;

24
src/utils/checkAccess.ts Normal file
View File

@@ -0,0 +1,24 @@
import { useUserProfileStore } from "../context/zustand-store/userStore";
type CheckAccessParams = {
page: string;
access: string;
};
export const checkAccess = ({ page, access }: CheckAccessParams): boolean => {
const profile = useUserProfileStore.getState()?.profile;
if (!access || !page) {
return true;
}
const finded = profile?.permissions?.find(
(item: any) => item.page_name === page
);
if (finded && finded.page_access.includes(access)) {
return true;
}
return false;
};

View File

@@ -0,0 +1,7 @@
export function checkIsMobile(): boolean {
if (typeof navigator === "undefined") return false;
const userAgent =
navigator.userAgent || navigator.vendor || (window as any).opera;
return /android|iphone|ipad|ipod|opera mini|iemobile|mobile/i.test(userAgent);
}

View File

@@ -0,0 +1,147 @@
/**
* Converts a number to Persian words
* @param num - The number to convert
* @returns Persian words representation of the number
*/
export const convertNumberToPersian = (num: number | null | undefined): string => {
if (num === null || num === undefined || isNaN(num)) {
return "صفر";
}
if (num === 0) {
return "صفر";
}
const ones = [
"",
"یک",
"دو",
"سه",
"چهار",
"پنج",
"شش",
"هفت",
"هشت",
"نه",
"ده",
"یازده",
"دوازده",
"سیزده",
"چهارده",
"پانزده",
"شانزده",
"هفده",
"هجده",
"نوزده",
];
const tens = [
"",
"",
"بیست",
"سی",
"چهل",
"پنجاه",
"شصت",
"هفتاد",
"هشتاد",
"نود",
];
const hundreds = [
"",
"صد",
"دویست",
"سیصد",
"چهارصد",
"پانصد",
"ششصد",
"هفتصد",
"هشتصد",
"نهصد",
];
const convertHundreds = (n: number): string => {
if (n === 0) return "";
if (n < 20) return ones[n];
if (n < 100) {
const ten = Math.floor(n / 10);
const one = n % 10;
if (one === 0) {
return tens[ten];
}
return `${tens[ten]} و ${ones[one]}`;
}
if (n < 1000) {
const hundred = Math.floor(n / 100);
const remainder = n % 100;
if (remainder === 0) {
return hundreds[hundred];
}
return `${hundreds[hundred]} و ${convertHundreds(remainder)}`;
}
return "";
};
const convertThousands = (n: number): string => {
if (n < 1000) {
return convertHundreds(n);
}
if (n < 1000000) {
const thousand = Math.floor(n / 1000);
const remainder = n % 1000;
let result = "";
if (thousand === 1) {
result = "هزار";
} else if (thousand < 1000) {
result = `${convertHundreds(thousand)} هزار`;
}
if (remainder > 0) {
result = result ? `${result} و ${convertHundreds(remainder)}` : convertHundreds(remainder);
}
return result;
}
if (n < 1000000000) {
const million = Math.floor(n / 1000000);
const remainder = n % 1000000;
let result = "";
if (million === 1) {
result = "یک میلیون";
} else if (million < 1000) {
result = `${convertHundreds(million)} میلیون`;
} else {
result = `${convertThousands(million)} میلیون`;
}
if (remainder > 0) {
result = `${result} و ${convertThousands(remainder)}`;
}
return result;
}
if (n < 1000000000000) {
const billion = Math.floor(n / 1000000000);
const remainder = n % 1000000000;
let result = "";
if (billion === 1) {
result = "یک میلیارد";
} else if (billion < 1000) {
result = `${convertHundreds(billion)} میلیارد`;
} else {
result = `${convertThousands(billion)} میلیارد`;
}
if (remainder > 0) {
result = `${result} و ${convertThousands(remainder)}`;
}
return result;
}
return "";
};
return convertThousands(Math.floor(num));
};

View File

@@ -0,0 +1,18 @@
type SubItems = {
name: string;
path: string;
component: () => React.ComponentType | any;
};
type Item = {
page_name: string;
[key: string]: any;
};
export function filterByAccess(
permissions: Item[] = [],
items: SubItems[]
): Item[] {
const nameSet = new Set(items.map((item) => item.name));
return permissions.filter((obj) => nameSet.has(obj.page_name));
}

106
src/utils/formatTime.ts Normal file
View File

@@ -0,0 +1,106 @@
import { format } from "date-fns-jalali";
export const formatTime = (time: any) => {
const date = new Date(time);
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
return format(new Date(time), "yyyy/MM/dd ") + `(${hours}:${minutes})`;
};
export const formatJustDate = (time: any) => {
if (time) {
try {
let normalizedTime = time;
if (typeof time === "string") {
const timezoneWithSecondsRegex = /([+-])(\d{2}):(\d{2}):(\d{2})$/;
const match = time.match(timezoneWithSecondsRegex);
if (match) {
normalizedTime = time.replace(timezoneWithSecondsRegex, `$1$2:$3`);
}
}
const date = new Date(normalizedTime);
if (isNaN(date.getTime())) {
console.warn(`Invalid date format: ${time}`);
return null;
}
return format(date, "yyyy/MM/dd");
} catch (error) {
console.warn(`Error formatting date: ${time}`, error);
return null;
}
} else {
return null;
}
};
export const formatJustTime = (time: any) => {
return format(new Date(time), "HH:mm");
};
export function formatStampDate(timestamp: number) {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}/${month}/${day}`;
}
export function formatStampDateTime(timestamp: number) {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
return `${year}/${month}/${day} ${hours}:${minutes}`;
}
export const formatAgeCalcuation = (dateString: string) => {
const birthDate = new Date(dateString);
const currentDate = new Date();
let years = currentDate.getFullYear() - birthDate.getFullYear();
let months = currentDate.getMonth() - birthDate.getMonth();
let days = currentDate.getDate() - birthDate.getDate();
if (days < 0) {
months--;
const lastMonth = new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
0
);
days += lastMonth.getDate();
}
if (months < 0) {
years--;
months += 12;
}
const parts: string[] = [];
if (years > 0) {
parts.push(`${years} سال`);
}
if (months > 0) {
parts.push(`${months} ماه`);
}
if (days > 0) {
parts.push(`${days} روز`);
}
if (parts.length === 0) {
return "0 روز";
}
return parts.join(" و ");
};
// export const formatJustDateGregorian = (time: any) => {
// var timePortion = time.getTime() % (3600 * 1000 * 24);
// return new Date(time - timePortion);
// };

19
src/utils/getAbleToSee.ts Normal file
View File

@@ -0,0 +1,19 @@
import { useUserProfileStore } from "../context/zustand-store/userStore";
export const getAbleToSee = (page: string, access: string) => {
let isAble;
if (!access || !page) {
isAble = "hidden";
} else {
const finded = useUserProfileStore
.getState()
?.profile?.permissions?.find((item: any) => item.page_name === page);
if (finded && finded.page_access.includes(access)) {
isAble = "";
} else {
isAble = "hidden";
}
}
return isAble;
};

View File

@@ -0,0 +1,6 @@
export function getBase64ImageSrc(
base64: string,
mimeType = "image/png"
): string {
return `data:${mimeType};base64,${base64}`;
}

View File

@@ -0,0 +1,205 @@
import Management from "../Pages/Management";
import * as R from "../routes/paths";
import Users from "../Pages/Users";
import Organizations from "../Pages/Organizations";
import Roles from "../Pages/Roles";
import Products from "../Pages/Products";
import { ProductsCategories } from "../Pages/ProductsCategories";
import Pricing from "../Pages/Pricing";
import IncentivePlans from "../Pages/IncentivePlans";
import Quota from "../Pages/Quota";
import Inventory from "../Pages/Inventory";
import Reporting from "../Pages/Reporting";
import LiveStockFarmers from "../Pages/LiveStockFarmers";
import LiveStocks from "../Pages/LiveStocks";
import Herds from "../Pages/Herds";
import Pos from "../Pages/Pos";
import PosCompanies from "../Pages/PosCompanies";
import PosAccounts from "../Pages/PosAccounts";
import RancherPlans from "../Pages/RancherPlans";
import Transactions from "../Pages/Transactions";
import Unions from "../Pages/Unions";
import Cooperatives from "../Pages/Cooperatives";
import CooperativeRanchers from "../Pages/CooperativeRanchers";
import SettingsOfUnits from "../Pages/SettingsOfUnits";
import Tagging from "../Pages/Tagging";
export const managementCategoryItems = [
{
name: "permission_control",
path: R.PERMISSION_ACCESS,
component: Management,
},
{
name: "users",
path: R.USERS,
component: Users,
},
{
name: "organizations",
path: R.ORGANIZATIONS,
component: Organizations,
},
{
name: "roles_management",
path: R.ROLES,
component: Roles,
},
];
export const unitCategoryItems = [
{
name: "unions",
path: R.UNION_LIST,
component: Unions,
},
{
name: "cooperatives",
path: R.COOPERATIVE_LIST,
component: Cooperatives,
},
{
name: "union_cooperatives",
path: R.UNION_COOPERATIVE_LIST,
component: Cooperatives,
},
{
name: "cooperative_ranchers",
path: R.COOPERATIVE_RANCHERS_LIST,
component: CooperativeRanchers,
},
{
name: "units_settings",
path: R.UNITS_SETTINGS,
component: SettingsOfUnits,
},
];
export const feedInputCategoryItems = [
{
name: "feed_input_products",
path: R.FEED_INPUT_PRODUCTS,
component: Products,
},
{
name: "product_categories",
path: R.PRODUCT_CATEGORIES,
component: ProductsCategories,
},
{
name: "pricing",
path: R.PRODUCT_PRICING,
component: Pricing,
},
];
export const quotaCategoryItems = [
{
name: "quota",
path: R.QUOTAS,
component: Quota,
},
{
name: "quota_distributions",
path: R.QUOTA_DISTRIBUTION,
component: Quota,
},
{
name: "inventory",
path: R.INVENTORY,
component: Inventory,
},
{
name: "inventory_entries",
path: R.INVENTORY_ENTRIES,
component: Inventory,
},
{
name: "reporting",
path: R.REPORTING,
component: Reporting,
},
{
name: "reporting_details",
path: R.REPORTING_DETAIL,
component: Reporting,
},
{
name: "incentive_plans",
path: R.QUOTA_INCENTIVE_PLANS,
component: IncentivePlans,
},
];
export const livestockCategoryItems = [
{
name: "livestock_farmers",
path: R.LIVESTOCK_FARMERS,
component: LiveStockFarmers,
},
{
name: "herds",
path: R.HERDS,
component: Herds,
},
{
name: "farmer_details",
path: R.LIVESTOCK_FARMER_DETAIL,
component: Herds,
},
{
name: "farmer_plans",
path: R.LIVESTOCK_FARMERS_INCENTIVE_PLANS,
component: RancherPlans,
},
{
name: "livestocks",
path: R.LIVESTOCKS,
component: LiveStocks,
},
{
name: "livestocks",
path: R.LIVESTOCKS_HERDS_DETAIL,
component: LiveStocks,
},
];
export const taggingCategoryItems = [
{
name: "tagging",
path: R.TAGGING,
component: Tagging,
},
];
export const posCategoryItems = [
{
name: "pos_companies",
path: R.POS_COMPANIES,
component: PosCompanies,
},
{
name: "pos_company_detail",
path: R.POS_COMPANY_DETAIL,
component: Pos,
},
{
name: "pos",
path: R.POS_POS_LIST,
component: Pos,
},
{
name: "pos_accounts",
path: R.POS_COMPANY_ACCOUNTS,
component: PosAccounts,
},
];
export const transactionCategoryItems = [
{
name: "transactions",
path: R.TRANSACTIONS,
component: Transactions,
},
];

View File

@@ -0,0 +1,105 @@
export function getFaPermissions(permission: string) {
let faPermission = "";
switch (permission) {
case "permission_control":
faPermission = "مدیریت دسترسی";
break;
case "users":
faPermission = "کاربران";
break;
case "organizations":
faPermission = "سازمان ها";
break;
case "roles_management":
faPermission = "مدیریت نقش";
break;
case "feed_input_products":
faPermission = "محصولات";
break;
case "product_categories":
faPermission = "دسته بندی محصولات";
break;
case "pricing":
faPermission = "قیمت گذاری";
break;
case "incentive_plans":
faPermission = "طرح های تشویقی";
break;
case "quota":
faPermission = "سهمیه ها (طرح فروش)";
break;
case "quota_distributions":
faPermission = "نمایش توزیع سهمیه";
break;
case "inventory":
faPermission = "انبار";
break;
case "reporting":
faPermission = "گزارش گیری";
break;
case "reporting_details":
faPermission = "جزئیات گزارش گیری";
break;
case "livestock_farmers":
faPermission = "دامداران";
break;
case "herds":
faPermission = "گله ها";
break;
case "livestocks":
faPermission = "دام ها";
break;
case "reporting_distribution_details":
faPermission = "گزارش توزیع سهمیه";
break;
case "farmer_details":
faPermission = "گله های دامدار";
break;
case "farmer_plans":
faPermission = "طرح های تشویقی دامدار";
break;
case "herd_livestocks":
faPermission = "دام های گله";
break;
case "pos_companies":
faPermission = "شرکت های پرداخت";
break;
case "pos_company_detail":
faPermission = "کارتخوان های شرکت پرداخت";
break;
case "pos":
faPermission = "لیست دستگاه";
break;
case "pos_accounts":
faPermission = "حساب های پوز";
break;
case "transactions":
faPermission = "تراکنش ها";
break;
case "inventory_entries":
faPermission = "لیست ورودی به انبار";
break;
case "unions":
faPermission = "اتحادیه ها";
break;
case "cooperatives":
faPermission = "تعاونی ها";
break;
case "union_cooperatives":
faPermission = "تعاونی های اتحادیه";
break;
case "cooperative_ranchers":
faPermission = "دامداران تعاونی";
break;
case "units_settings":
faPermission = "تنظیمات واحدها";
break;
case "tagging":
faPermission = "پلاک کوبی";
break;
default:
break;
}
return faPermission;
}

View File

@@ -0,0 +1,41 @@
type GetNestedValueOptions = {
disableLocaleString?: boolean;
};
export function getNestedValue(
obj: any,
keys: string[],
options?: GetNestedValueOptions
) {
const value = keys?.reduce((acc, key) => acc?.[key], obj);
if (options?.disableLocaleString) {
if (typeof value === "string" && !isNaN(Number(value))) {
return Number(value);
}
return value;
}
if (typeof value === "number") {
if (Number.isInteger(value)) {
return value.toLocaleString("en-US", { maximumFractionDigits: 0 });
}
return value.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
if (typeof value === "string" && !isNaN(Number(value))) {
const numValue = Number(value);
if (Number.isInteger(numValue)) {
return numValue.toLocaleString("en-US", { maximumFractionDigits: 0 });
}
return numValue.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
return value;
}

34
src/utils/getObjectId.ts Normal file
View File

@@ -0,0 +1,34 @@
interface Identifiable {
id?: string | number;
[key: string]: any;
}
export function getObjectId(
savedList: Identifiable[] = [],
newList: Identifiable[] = [],
key: string
): Identifiable[] {
if (!newList.length) {
return [];
}
if (!savedList.length) {
return newList;
}
const savedItemsMap = new Map();
savedList.forEach((item) => {
if (item[key] !== undefined) {
savedItemsMap.set(item[key], item.id);
}
});
return newList.map((item) => {
const id =
item[key] !== undefined ? savedItemsMap.get(item[key]) : undefined;
return {
...item,
...(id !== undefined ? { id } : {}),
};
});
}

View File

@@ -0,0 +1,24 @@
export function getPersianMonths(monthNumbers?: number[] | null): string[] {
const persianMonths: { [key: number]: string } = {
1: "فروردین",
2: "اردیبهشت",
3: "خرداد",
4: "تیر",
5: "مرداد",
6: "شهریور",
7: "مهر",
8: "آبان",
9: "آذر",
10: "دی",
11: "بهمن",
12: "اسفند",
};
if (!monthNumbers) {
return [];
}
return monthNumbers?.map((num) => {
return persianMonths[num];
});
}

View File

@@ -0,0 +1,125 @@
import { ItemWithSubItems } from "../types/userPermissions";
import { filterByAccess } from "./filterByAccess";
import {
feedInputCategoryItems,
livestockCategoryItems,
managementCategoryItems,
posCategoryItems,
quotaCategoryItems,
taggingCategoryItems,
transactionCategoryItems,
unitCategoryItems,
} from "./getCategoryParameters";
import { getUserAvalablePaths } from "./getUserAvalablePaths";
import LogoManagement from "../assets/images/svg/management.svg?react";
import LogoFeedInput from "../assets/images/svg/feed-input.svg?react";
import LogoWage from "../assets/images/svg/wage.svg?react";
import LogoLivestock from "../assets/images/svg/livestock.svg?react";
import LogoPos from "../assets/images/svg/pos.svg?react";
import LogoTransactions from "../assets/images/svg/transactions.svg?react";
import LogoUnits from "../assets/images/svg/units.svg?react";
import LogoTagging from "../assets/images/svg/tagging.svg?react";
type Item = {
page_name: string;
[key: string]: any;
};
export function getUserPermissions(
permissions: Item[] = []
): ItemWithSubItems[] {
if (!permissions || !permissions.length) {
return [];
}
const managementItems = filterByAccess(permissions, managementCategoryItems);
const feedInputItems = filterByAccess(permissions, feedInputCategoryItems);
const wageItems = filterByAccess(permissions, quotaCategoryItems);
const livestockItems = filterByAccess(permissions, livestockCategoryItems);
const posItems = filterByAccess(permissions, posCategoryItems);
const unitItems = filterByAccess(permissions, unitCategoryItems);
const taggingItems = filterByAccess(permissions, taggingCategoryItems);
const transactionItems = filterByAccess(
permissions,
transactionCategoryItems
);
const items: ItemWithSubItems[] = [];
if (managementItems.length) {
items.push({
en: "management",
fa: "مدیریت",
icon: LogoManagement,
subItems: getUserAvalablePaths("management", permissions),
});
}
if (unitItems.length) {
items.push({
en: "unit",
fa: "مدیریت واحد ها",
icon: LogoUnits,
subItems: getUserAvalablePaths("unit", permissions),
});
}
if (feedInputItems.length) {
items.push({
en: "feed-input",
fa: "نهاده",
icon: LogoFeedInput,
subItems: getUserAvalablePaths("feed-input", permissions),
});
}
if (wageItems.length) {
items.push({
en: "quota",
fa: "سهمیه",
icon: LogoWage,
subItems: getUserAvalablePaths("quota", permissions),
});
}
if (livestockItems.length) {
items.push({
en: "livestock",
fa: "امور دام",
icon: LogoLivestock,
subItems: getUserAvalablePaths("livestock", permissions),
});
}
if (taggingItems.length) {
items.push({
en: "livestock",
fa: "پلاک کوبی",
icon: LogoTagging,
subItems: getUserAvalablePaths("tagging", permissions),
});
}
if (transactionItems.length) {
items.push({
en: "transactions",
fa: "تراکنش ها",
icon: LogoTransactions,
subItems: getUserAvalablePaths("transactions", permissions),
});
}
if (posItems.length) {
items.push({
en: "pos",
fa: "شرکت پرداخت",
icon: LogoPos,
subItems: getUserAvalablePaths("pos", permissions),
});
}
return items;
}

View File

@@ -0,0 +1,73 @@
import {
feedInputCategoryItems,
livestockCategoryItems,
managementCategoryItems,
posCategoryItems,
quotaCategoryItems,
taggingCategoryItems,
transactionCategoryItems,
unitCategoryItems,
} from "./getCategoryParameters";
type SubItems = {
name: string;
path: string;
component: () => React.ComponentType | any;
};
type Item = {
page_name: string;
[key: string]: any;
};
export function getUserAvalablePaths(
category: string,
permissions: any[] = []
): SubItems[] {
if (!Array.isArray(permissions)) {
return [];
}
const filterByAccess = (items: SubItems[]): SubItems[] => {
if (!permissions?.length) return [];
const nameSet = new Set(
permissions.map((item: Item) => item?.page_name).filter(Boolean)
);
return items?.filter((item) => item?.name && nameSet.has(item.name)) || [];
};
let list: SubItems[] = [];
switch (category) {
case "management":
list = filterByAccess(managementCategoryItems);
break;
case "feed-input":
list = filterByAccess(feedInputCategoryItems);
break;
case "quota":
list = filterByAccess(quotaCategoryItems);
break;
case "livestock":
list = filterByAccess(livestockCategoryItems);
break;
case "transactions":
list = filterByAccess(transactionCategoryItems);
break;
case "pos":
list = filterByAccess(posCategoryItems);
break;
case "unit":
list = filterByAccess(unitCategoryItems);
break;
case "tagging":
list = filterByAccess(taggingCategoryItems);
break;
default:
break;
}
return list || [];
}

View File

@@ -0,0 +1,65 @@
import { useQuery, useMutation } from "@tanstack/react-query";
import { useBackdropStore } from "../context/zustand-store/appStore";
import api from "./axios";
import { AxiosError } from "axios";
type RequestParams = {
api: string;
method?: "get" | "post" | "put" | "delete" | "patch";
params?: any;
queryKey?: any[];
enabled?: boolean;
disableBackdrop?: boolean;
};
export function useApiRequest<TData = any>({
api: url,
params,
queryKey = [url],
enabled = true,
disableBackdrop = false,
}: RequestParams) {
const { openBackdrop, closeBackdrop } = useBackdropStore();
return useQuery<TData>({
queryKey,
queryFn: async () => {
if (!disableBackdrop && window.location.pathname !== "/") openBackdrop();
try {
const response = await api.get(url, { params });
return response.data;
} finally {
if (!disableBackdrop) closeBackdrop();
}
},
enabled,
retry: 3,
});
}
export function useApiMutation<TData = any>({
api: url,
method = "post",
disableBackdrop = false,
}: RequestParams) {
const { openBackdrop, closeBackdrop } = useBackdropStore();
return useMutation<TData, AxiosError, any>({
mutationFn: async (params) => {
if (!disableBackdrop) openBackdrop();
try {
const response = await api.request({
url,
method,
data: ["post", "put", "patch"].includes(method) ? params : undefined,
params: ["get", "delete"].includes(method) ? params : undefined,
});
return response.data;
} finally {
if (!disableBackdrop) closeBackdrop();
}
},
});
}