end role and permission # 166
This commit is contained in:
parent
9a6e5bc53d
commit
3bf0308631
21
src/Pages/Admin/Roles/Permissions/FN/formatAbilityData.ts
Normal file
21
src/Pages/Admin/Roles/Permissions/FN/formatAbilityData.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
export const formatAbilityData = (Data: any[]) => {
|
||||||
|
const newArray: Array<{ name: any; [key: string]: boolean }> = [];
|
||||||
|
console.log(Data, "Data");
|
||||||
|
for (let i = 0; i < Data.length; i++) {
|
||||||
|
const currentObject = Data?.[i];
|
||||||
|
console.log(currentObject);
|
||||||
|
const newObjectShape = {
|
||||||
|
name: currentObject?.name,
|
||||||
|
delete: typeof currentObject?.delete === "boolean" ? false : "disabled",
|
||||||
|
index: typeof currentObject?.index === "boolean" ? false : "disabled",
|
||||||
|
show: typeof currentObject?.show === "boolean" ? false : "disabled",
|
||||||
|
store: typeof currentObject?.store === "boolean" ? false : "disabled",
|
||||||
|
update: typeof currentObject?.update === "boolean" ? false : "disabled",
|
||||||
|
} as any;
|
||||||
|
console.log(newObjectShape);
|
||||||
|
|
||||||
|
newArray.push(newObjectShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newArray;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const formatArrayToPermissions = ( newArray: Array<{ name: any; [key: string]: boolean }>): string[] => {
|
||||||
|
const Data: string[] = [];
|
||||||
|
|
||||||
|
newArray.forEach((obj) => {
|
||||||
|
const permission = obj.name;
|
||||||
|
Object.keys(obj).forEach((key) => {
|
||||||
|
if (key !== "name" && key !== "ALL" && obj[key] && obj[key] === true) {
|
||||||
|
Data.push(`${permission}::${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Data;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
export const mergePermissionsWithAbilities = (
|
||||||
|
newShapeArray: Record<any, any>[],
|
||||||
|
Ability: Record<any, boolean | "disabled" | "string">[]
|
||||||
|
) => {
|
||||||
|
const newShapeMap = new Map(newShapeArray.map((item) => [item.name, item]));
|
||||||
|
console.log(newShapeMap, "newShapeMap");
|
||||||
|
|
||||||
|
return Ability.map((abilityItem) => {
|
||||||
|
const correspondingNewShape = newShapeMap.get(abilityItem.name);
|
||||||
|
console.log(correspondingNewShape);
|
||||||
|
|
||||||
|
let ALL = false;
|
||||||
|
if (correspondingNewShape) {
|
||||||
|
if (
|
||||||
|
correspondingNewShape["index"] &&
|
||||||
|
correspondingNewShape["show"] &&
|
||||||
|
correspondingNewShape["store"] &&
|
||||||
|
correspondingNewShape["update"] &&
|
||||||
|
correspondingNewShape["delete"]
|
||||||
|
) {
|
||||||
|
ALL = true;
|
||||||
|
}
|
||||||
|
console.log(correspondingNewShape);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...abilityItem,
|
||||||
|
delete:
|
||||||
|
typeof correspondingNewShape.delete === "boolean"
|
||||||
|
? correspondingNewShape.delete
|
||||||
|
: "disabled",
|
||||||
|
index:
|
||||||
|
typeof correspondingNewShape.index === "boolean"
|
||||||
|
? correspondingNewShape.index
|
||||||
|
: "disabled",
|
||||||
|
show:
|
||||||
|
typeof correspondingNewShape.show === "boolean"
|
||||||
|
? correspondingNewShape.show
|
||||||
|
: "disabled",
|
||||||
|
store:
|
||||||
|
typeof correspondingNewShape.store === "boolean"
|
||||||
|
? correspondingNewShape.store
|
||||||
|
: "disabled",
|
||||||
|
update:
|
||||||
|
typeof correspondingNewShape.update === "boolean"
|
||||||
|
? correspondingNewShape.update
|
||||||
|
: "disabled",
|
||||||
|
ALL: ALL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return original ability item if no match found
|
||||||
|
return abilityItem;
|
||||||
|
});
|
||||||
|
};
|
||||||
31
src/Pages/Admin/Roles/Permissions/FN/transformPermissions.ts
Normal file
31
src/Pages/Admin/Roles/Permissions/FN/transformPermissions.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
export const transformPermissions = (Data: string[]) => {
|
||||||
|
const newArray: Array<{ name: any; [key: string]: boolean }> = [];
|
||||||
|
const hashMap = new Map<string, number>();
|
||||||
|
|
||||||
|
for (let i = 0; i < Data.length; i++) {
|
||||||
|
const [permission, value] = Data[i].split("::");
|
||||||
|
const existingIndex = hashMap.get(permission);
|
||||||
|
|
||||||
|
if (existingIndex !== undefined) {
|
||||||
|
if (value) {
|
||||||
|
newArray[existingIndex][value] = true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
newArray[existingIndex]["index"] &&
|
||||||
|
newArray[existingIndex]["show"] &&
|
||||||
|
newArray[existingIndex]["store"] &&
|
||||||
|
newArray[existingIndex]["update"] &&
|
||||||
|
newArray[existingIndex]["delete"]
|
||||||
|
) {
|
||||||
|
newArray[existingIndex]["ALL"] = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const newObject = value
|
||||||
|
? ({ name: permission, [value]: true } as any)
|
||||||
|
: { name: permission };
|
||||||
|
newArray.push(newObject);
|
||||||
|
hashMap.set(permission, newArray.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newArray;
|
||||||
|
};
|
||||||
|
|
@ -2,8 +2,13 @@ import React from 'react'
|
||||||
import DataTable from '../../../../Layout/Dashboard/Table/DataTable'
|
import DataTable from '../../../../Layout/Dashboard/Table/DataTable'
|
||||||
import { useColumns } from './useTableColumns'
|
import { useColumns } from './useTableColumns'
|
||||||
import { useFormikContext } from 'formik'
|
import { useFormikContext } from 'formik'
|
||||||
|
import { TableProps } from 'antd'
|
||||||
|
|
||||||
const FormTable = ({response}:{response:any}) => {
|
|
||||||
|
interface IFormTable extends TableProps {
|
||||||
|
response:any
|
||||||
|
}
|
||||||
|
const FormTable = ({response,...props}:IFormTable) => {
|
||||||
const {values} = useFormikContext<any>()
|
const {values} = useFormikContext<any>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -16,7 +21,7 @@ const FormTable = ({response}:{response:any}) => {
|
||||||
pagination={false}
|
pagination={false}
|
||||||
loading={false}
|
loading={false}
|
||||||
rowKey={"name"}
|
rowKey={"name"}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,11 @@ const TableHeader = () => {
|
||||||
pageTitle="role"
|
pageTitle="role"
|
||||||
addModal={false}
|
addModal={false}
|
||||||
/>
|
/>
|
||||||
<FilterLayout
|
{/* <FilterLayout
|
||||||
sub_children={""}
|
sub_children={""}
|
||||||
filterTitle="page_header.permissions"
|
filterTitle="page_header.permissions"
|
||||||
haveFilter={false}
|
haveFilter={false}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
<Table />
|
<Table />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React from "react";
|
import React, { Suspense } from "react";
|
||||||
import { useFilterState } from "../../../../Components/Utils/Filter/FilterState";
|
import { useFilterState } from "../../../../Components/Utils/Filter/FilterState";
|
||||||
import { useFilterStateState } from "../../../../zustand/Filter";
|
import { useFilterStateState } from "../../../../zustand/Filter";
|
||||||
import { useAddPermissions, useGetAllPermissions } from "../../../../api/Permissions";
|
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { ParamsEnum } from "../../../../enums/params";
|
import { ParamsEnum } from "../../../../enums/params";
|
||||||
import { Form, Formik } from "formik";
|
import { Form, Formik } from "formik";
|
||||||
|
|
@ -9,6 +8,12 @@ import FormTable from "./FormTable";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useGetAllRole, useUpdateRole } from "../../../../api/role";
|
import { useGetAllRole, useUpdateRole } from "../../../../api/role";
|
||||||
import { useGetAllAbility } from "../../../../api/Ability";
|
import { useGetAllAbility } from "../../../../api/Ability";
|
||||||
|
import LoadingLottie from "../../../../Components/Lottie/Loading/LoadingLottie";
|
||||||
|
import { Button } from "antd";
|
||||||
|
import { transformPermissions } from "./FN/transformPermissions";
|
||||||
|
import { formatAbilityData } from "./FN/formatAbilityData";
|
||||||
|
import { mergePermissionsWithAbilities } from "./FN/mergePermissionsWithAbilities";
|
||||||
|
import { formatArrayToPermissions } from "./FN/formatArrayToPermission";
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const { role_id } = useParams<ParamsEnum>();
|
const { role_id } = useParams<ParamsEnum>();
|
||||||
|
|
@ -24,107 +29,66 @@ const App: React.FC = () => {
|
||||||
sort_by,
|
sort_by,
|
||||||
...filterState,
|
...filterState,
|
||||||
});
|
});
|
||||||
const {data} = useGetAllAbility()
|
const { data, isLoading } = useGetAllAbility();
|
||||||
const AllAbilityData = data?.data ?? [];
|
const AllAbilityData = data?.data ?? [];
|
||||||
const currentData = response?.data?.data?.abilities ?? [];
|
const currentData = response?.data?.data?.abilities ?? [];
|
||||||
console.log(currentData,"currentData");
|
|
||||||
|
|
||||||
const GetAllAbility = (Data:string[])=>{
|
|
||||||
const newArray: Array<{ name: any; [key: string]: boolean }> = [];
|
|
||||||
const hashMap = new Map<string, number>();
|
|
||||||
|
|
||||||
for (let i = 0; i < Data.length; i++) {
|
const AllAbility = transformPermissions(AllAbilityData ?? []);
|
||||||
const [permission, value] = Data[i].split("::");
|
|
||||||
const existingIndex = hashMap.get(permission);
|
|
||||||
|
|
||||||
if (existingIndex !== undefined) {
|
|
||||||
|
|
||||||
} else {
|
const Ability = formatAbilityData(AllAbility) ?? [];
|
||||||
const newObject = permission as any;
|
const newShapeArray = transformPermissions([...currentData]);
|
||||||
newArray.push(newObject);
|
|
||||||
hashMap.set(permission, newArray.length - 1);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newArray
|
|
||||||
}
|
|
||||||
const Ability = GetAllAbility(AllAbilityData ?? []) ?? []
|
|
||||||
/// time complexity O(n) -_-
|
|
||||||
console.log(Ability);
|
|
||||||
|
|
||||||
const changePermissionShape = (Data:string[])=>{
|
const finalShape = mergePermissionsWithAbilities(newShapeArray, Ability);
|
||||||
const newArray: Array<{ name: any; [key: string]: boolean }> = [];
|
|
||||||
const hashMap = new Map<string, number>();
|
|
||||||
|
|
||||||
for (let i = 0; i < Data.length; i++) {
|
const [t] = useTranslation();
|
||||||
const [permission, value] = Data[i].split("::");
|
|
||||||
const existingIndex = hashMap.get(permission);
|
|
||||||
|
|
||||||
if (existingIndex !== undefined) {
|
const { mutate, isLoading: UpdateLoading } = useUpdateRole();
|
||||||
if(value){
|
|
||||||
console.log(1);
|
|
||||||
|
|
||||||
newArray[existingIndex][value] = true;
|
|
||||||
}
|
|
||||||
if(newArray[existingIndex]["index"] && newArray[existingIndex]["show"] && newArray[existingIndex]["store"] && newArray[existingIndex]["update"] && newArray[existingIndex]["delete"]){
|
|
||||||
|
|
||||||
newArray[existingIndex]["ALL"] = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const newObject = value ? { name: permission, [value]: true } as any : {name: permission} ;
|
|
||||||
newArray.push(newObject);
|
|
||||||
hashMap.set(permission, newArray.length - 1);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newArray
|
|
||||||
}
|
|
||||||
const newShapeArray = changePermissionShape([...currentData,...Ability])
|
|
||||||
|
|
||||||
const [t] = useTranslation()
|
|
||||||
const reverseChangePermissionShape = (
|
|
||||||
newArray: Array<{ name: any; [key: string]: boolean }>
|
|
||||||
): string[] => {
|
|
||||||
const Data: string[] = [];
|
|
||||||
|
|
||||||
newArray.forEach((obj) => {
|
|
||||||
const permission = obj.name;
|
|
||||||
Object.keys(obj).forEach((key) => {
|
|
||||||
if (key !== "name" && key !== "ALL" && obj[key]) {
|
|
||||||
Data.push(`${permission}::${key}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return Data;
|
|
||||||
};
|
|
||||||
const {mutate} = useUpdateRole()
|
|
||||||
const handelSubmit = (values: any) => {
|
const handelSubmit = (values: any) => {
|
||||||
console.log(values);
|
console.log(values);
|
||||||
|
|
||||||
const dataToSend = reverseChangePermissionShape(values);
|
const dataToSend = formatArrayToPermissions(values);
|
||||||
console.log(dataToSend);
|
console.log(dataToSend);
|
||||||
|
|
||||||
mutate({
|
mutate({
|
||||||
id: role_id,
|
id: role_id,
|
||||||
abilities:dataToSend
|
abilities: dataToSend?.length > 0 ? dataToSend : "",
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
const disabled =
|
||||||
|
isLoading || response.isLoading || response.isRefetching || UpdateLoading;
|
||||||
return (
|
return (
|
||||||
<Formik initialValues={newShapeArray} onSubmit={handelSubmit} enableReinitialize>
|
<Formik initialValues={finalShape} onSubmit={handelSubmit} enableReinitialize>
|
||||||
{({handleSubmit})=>{
|
{({ dirty }) => {
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
|
<div className="permissions_submit_button mt-4">
|
||||||
<FormTable response={response} />
|
<Button
|
||||||
<div className="permissions_submit_button">
|
className="button"
|
||||||
<button className="button" type="submit" >{t("practical.submit")}</button>
|
disabled={disabled || !dirty}
|
||||||
|
htmlType="submit"
|
||||||
|
>
|
||||||
|
{t("practical.submit")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
|
|
||||||
|
<FormTable
|
||||||
|
response={response}
|
||||||
|
loading={{
|
||||||
|
spinning: disabled,
|
||||||
|
indicator: (
|
||||||
|
<Suspense fallback={<></>}>
|
||||||
|
<LoadingLottie />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
|
size: "large",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ export const useColumns = () => {
|
||||||
cloneValue[index] = {};
|
cloneValue[index] = {};
|
||||||
}
|
}
|
||||||
cloneValue[index][type] = !cloneValue[index][type];
|
cloneValue[index][type] = !cloneValue[index][type];
|
||||||
|
|
||||||
|
if(!cloneValue[index][type]){
|
||||||
|
cloneValue[index]["ALL"] = false
|
||||||
|
}
|
||||||
setValues(cloneValue)
|
setValues(cloneValue)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -24,22 +28,22 @@ export const useColumns = () => {
|
||||||
if(cloneValue[index]["ALL"]){
|
if(cloneValue[index]["ALL"]){
|
||||||
cloneValue[index] = {
|
cloneValue[index] = {
|
||||||
name:cloneValue[index]?.name ,
|
name:cloneValue[index]?.name ,
|
||||||
delete: false,
|
delete: typeof cloneValue[index]?.delete === "boolean" ? false : "disabled" ,
|
||||||
index: false,
|
index: typeof cloneValue[index]?.index === "boolean" ? false : "disabled" ,
|
||||||
show: false,
|
show: typeof cloneValue[index]?.show === "boolean" ? false : "disabled" ,
|
||||||
store: false,
|
store: typeof cloneValue[index]?.store === "boolean" ? false : "disabled" ,
|
||||||
update: false,
|
update: typeof cloneValue[index]?.update === "boolean" ? false : "disabled" ,
|
||||||
ALL: false
|
ALL: false
|
||||||
}
|
}
|
||||||
|
|
||||||
}else{
|
}else{
|
||||||
cloneValue[index] = {
|
cloneValue[index] = {
|
||||||
name:cloneValue[index]?.name ,
|
name:cloneValue[index]?.name ,
|
||||||
delete: true,
|
delete: typeof cloneValue[index]?.delete === "boolean" ? true : "disabled" ,
|
||||||
index: true,
|
index: typeof cloneValue[index]?.index === "boolean" ? true : "disabled" ,
|
||||||
show: true,
|
show: typeof cloneValue[index]?.show === "boolean" ? true : "disabled" ,
|
||||||
store: true,
|
store: typeof cloneValue[index]?.store === "boolean" ? true : "disabled" ,
|
||||||
update: true,
|
update: typeof cloneValue[index]?.update === "boolean" ? true : "disabled" ,
|
||||||
ALL: true
|
ALL: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,10 +62,9 @@ setValues(cloneValue)
|
||||||
|
|
||||||
const CheckBoxField = ({record,type,index}:{record:any,type:string,index:number})=>{
|
const CheckBoxField = ({record,type,index}:{record:any,type:string,index:number})=>{
|
||||||
|
|
||||||
|
const isChecked = record?.[type] === true ;
|
||||||
const isChecked = record?.[type] ;
|
const isDisabled = record?.[type] === "disabled" ;
|
||||||
|
return <Checkbox onChange={()=>onChange(type,index)} checked={isChecked} disabled={isDisabled} />;
|
||||||
return <Checkbox onChange={()=>onChange(type,index)} checked={isChecked} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -71,11 +74,31 @@ setValues(cloneValue)
|
||||||
return !!item?.index && !!item?.show && !!item?.store && !!item?.update && !!item?.delete
|
return !!item?.index && !!item?.show && !!item?.store && !!item?.update && !!item?.delete
|
||||||
})
|
})
|
||||||
const onChangeAllPermissions = ()=>{
|
const onChangeAllPermissions = ()=>{
|
||||||
const newShape =cloneValue?.map((item:any)=>{
|
const newShape =cloneValue?.map((item:any,index:number)=>{
|
||||||
if(IsAllValuesTrue){
|
if(IsAllValuesTrue){
|
||||||
return {...item,delete: false,index: false,show: false,store: false,update: false,ALL: false}
|
console.log(item);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
delete: typeof item?.delete === "boolean" ? false : "disabled",
|
||||||
|
index: typeof item?.index === "boolean" ? false : "disabled",
|
||||||
|
show: typeof item?.show === "boolean" ? false : "disabled",
|
||||||
|
store: typeof item?.store === "boolean" ? false : "disabled",
|
||||||
|
update: typeof item?.update === "boolean" ? false : "disabled",
|
||||||
|
ALL: false
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
return {...item,delete: true,index: true,show: true,store: true,update: true,ALL: true}
|
console.log(item);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
delete: typeof item?.delete === "boolean" ? true : "disabled",
|
||||||
|
index: typeof item?.index === "boolean" ? true : "disabled",
|
||||||
|
show: typeof item?.show === "boolean" ? true : "disabled",
|
||||||
|
store: typeof item?.store === "boolean" ? true : "disabled",
|
||||||
|
update: typeof item?.update === "boolean" ? true : "disabled",
|
||||||
|
ALL: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
setValues(newShape)
|
setValues(newShape)
|
||||||
|
|
@ -83,8 +106,6 @@ setValues(cloneValue)
|
||||||
|
|
||||||
return <Checkbox onChange={()=>onChangeAllPermissions()} checked={IsAllValuesTrue} />;
|
return <Checkbox onChange={()=>onChangeAllPermissions()} checked={IsAllValuesTrue} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const columns: TableColumnsType<any> = [
|
const columns: TableColumnsType<any> = [
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -92,6 +113,9 @@ setValues(cloneValue)
|
||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
key: "name",
|
key: "name",
|
||||||
align: "center",
|
align: "center",
|
||||||
|
render: (_text,record,index) => {
|
||||||
|
return ( <> {t(`models.${record?.name}`)} </> );
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("columns.add"),
|
title: t("columns.add"),
|
||||||
|
|
|
||||||
|
|
@ -42,3 +42,7 @@
|
||||||
.PageTitleLastItem {
|
.PageTitleLastItem {
|
||||||
color: #202C4B !important;
|
color: #202C4B !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.permissions_submit_button{
|
||||||
|
transform: translateY(-40px);
|
||||||
|
}
|
||||||
|
|
@ -389,7 +389,14 @@
|
||||||
"sale":"عملية بيع",
|
"sale":"عملية بيع",
|
||||||
"collections": "التحصيلات",
|
"collections": "التحصيلات",
|
||||||
"phone_number":"رقم الهاتف",
|
"phone_number":"رقم الهاتف",
|
||||||
"email_address":"عنوان البريد الإلكتروني"
|
"email_address":"عنوان البريد الإلكتروني",
|
||||||
|
"ability": "القدرات",
|
||||||
|
"answer": "إجابة",
|
||||||
|
"area": "منطقة",
|
||||||
|
"city": "مدينة",
|
||||||
|
"coupon": "قسيمة",
|
||||||
|
"package": "حزمة",
|
||||||
|
"packageItem": "عنصر الحزمة"
|
||||||
},
|
},
|
||||||
"education_class_actions": {
|
"education_class_actions": {
|
||||||
"Student_Records": "سجلات الطلاب",
|
"Student_Records": "سجلات الطلاب",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user