في نهاية هذا البرنامج التعليمي، سنمتلك تطبيق إدارة الموارد البشرية الذي يتضمن:
صفحة تسجيل الدخول: تتيح للمستخدمين تسجيل الدخول كمدير أو موظف. يمكن للمديرين الوصول إلى صفحات الإجازات الشخصية و الطلبات، بينما يمكن للموظفين الوصول إلى صفحة الإجازات الشخصية فقط.
صفحات الإجازات الشخصية: تتيح للموظفين طلب، عرض، وإلغاء إجازاتهم. ويمكن للمديرين أيضًا تعيين إجازات جديدة.
صفحة الطلبات: يمكن لمديري الموارد البشرية فقط الوصول إليها للموافقة على الطلبات أو رفضها.
ملحوظة: يمكنك الحصول على الشيفرة البرمجية الكاملة للتطبيق الذي سنبنيه في هذا البرنامج التعليمي من هذا مستودع GitHub
أثناء القيام بذلك، سنستخدم:
واجهة برمجة التطبيقات REST: لجلب وتحديث البيانات. يحتوي Refine على حزم مزود بيانات وواجهات برمجة التطبيقات REST مدمجة، ولكن يمكنك أيضًا بناء واجهتك الخاصة لتناسب متطلباتك المحددة. في هذا الدليل، سنستخدم NestJs CRUD كخدمة خلفية لنا وحزمة @refinedev/nestjsx-crud كمزود بيانات.
مكونات واجهة المستخدم Material UI: سنستخدمها لمكونات واجهة المستخدم وسنقوم بتخصيصها بالكامل وفقًا لتصميمنا الخاص. يحتوي Refine على دعم مدمج لمكونات واجهة المستخدم Material UI، ولكن يمكنك استخدام أي مكتبة واجهة مستخدم تريدها.
بمجرد أن نبني التطبيق، سنقوم بنشره على الإنترنت باستخدام منصة تطبيقات DigitalOcean التي تسهل إعداد وإطلاق وتطوير التطبيقات والمواقع الثابتة. يمكنك نشر الكود ببساطة عن طريق الإشارة إلى مستودع GitHub وترك منصة التطبيقات تقوم بالعمل الشاق لإدارة البنية التحتية، وأوقات تشغيل التطبيقات، والاعتمادات.
Refine هو إطار عمل React مفتوح المصدر لبناء تطبيقات الويب B2B المعقدة، متمحور حول إدارة البيانات بشكل رئيسي مثل الأدوات الداخلية ولوحات التحكم ولوحات البيانات. تم تصميمه من خلال توفير مجموعة من الخطافات والمكونات لتحسين عملية التطوير بتحسين سير العمل للمطور.
يوفر ميزات كاملة وجاهزة للإنتاج لتطبيقات المستوى الشركي لتبسيط المهام المدفوعة مثل إدارة الحالة والبيانات والمصادقة ومراقبة الوصول. يمكن للمطورين بذلك التركيز على جوهر تطبيقهم بطريقة تجريدية عن العديد من تفاصيل التنفيذ المرهقة.
سنستخدم الأمر npm create refine-app لتهيئة المشروع بشكل تفاعلي.
npm create refine-app@latest
اختر الخيارات التالية عند الطلب:
✔ Choose a project template · Vite
✔ What would you like to name your project?: · hr-app
✔ Choose your backend service to connect: · nestjsx-crud
✔ Do you want to use a UI Framework?: · Material UI
✔ Do you want to add example pages?: · No
✔ Do you need any Authentication logic?: · None
✔ Choose a package manager: · npm
بمجرد الانتهاء من الإعداد، انتقل إلى مجلد المشروع وابدأ تطبيقك باستخدام:
npm run dev
افتح http://localhost:5173 في متصفحك لرؤية التطبيق.
الآن بعد أن قمنا بإعداد مشروعنا، دعنا نجري بعض التغييرات على هيكل المشروع ونزيل الملفات غير الضرورية.
أولاً، قم بتثبيت التبعيات الخارجية:
@mui/x-date-pickers، @mui/x-date-pickers-pro: هذه مكونات لاختيار التاريخ لـ Material UI. سنستخدمها لاختيار نطاق التاريخ لطلبات الإجازة.
react-hot-toast: مكتبة تنبيه بسيطة لـ React. سنستخدمها لعرض رسائل النجاح والخطأ.
react-infinite-scroll-component: مكون React لتسهيل التمرير اللانهائي. سنستخدمه لتحميل المزيد من طلبات الإجازة كلما قام المستخدم بالتمرير لأسفل الصفحة لرؤية المزيد من الطلبات.
dayjs: مكتبة تاريخ خفيفة لتحليل وتحقق ومعالجة وتنسيق التواريخ.
vite-tsconfig-paths: ملحق Vite يتيح لك استخدام أسماء مسارات TypeScript في مشروع Vite الخاص بك.
بعد تثبيت الاعتماديات، قم بتحديث vite.config.ts و tsconfig.json لاستخدام ملحق vite-tsconfig-paths. هذا يتيح استخدام اختصارات المسارات في مشاريع Vite، مما يسمح بالاستيرادات باستخدام اختصار @.
بعد ذلك، دعنا نزيل الملفات والمجلدات غير الضرورية:
src/contexts: يحتوي هذا المجلد على ملف واحد وهو ColorModeContext. يتعامل مع وضع الظلام/الضوء للتطبيق. لن نستخدمه في هذا الدرس.
src/components: يحتوي هذا المجلد على مكون <Header />. سنستخدم مكون رأس مخصص في هذا الدرس.
rm-rf src/contexts src/components
بعد إزالة الملفات والمجلدات، يعطي App.tsx خطأً سنقوم بإصلاحه في الخطوات التالية.
خلال الدرس، سنقوم بتغطية برمجة الصفحات والمكونات الأساسية. لذا، احصل على الملفات والمجلدات الضرورية من مستودع GitHub. مع هذه الملفات، سيكون لدينا هيكل أساسي لتطبيق إدارة الموارد البشرية الخاص بنا.
الأيقونات: مجلد الأيقونات يحتوي على جميع أيقونات التطبيق.
الموارد: مصفوفة تحدد كيانات البيانات (موظف و مدير) التي ستقوم Refine بجلبها. نستخدم الموارد الأبوية والطفلية لتنظيم البيانات وإدارة الأذونات. تحتوي كل مورد على نطاق يحدد دور المستخدم، الذي يتحكم في الوصول إلى أجزاء مختلفة من التطبيق.
عميل الاستعلام: عميل استعلام مخصص للتحكم الكامل وتخصيص جلب البيانات.
المزامنة مع الموقع: يتيح مزامنة حالة التطبيق (الفلاتر، الترتيبات، الصفحات وما إلى ذلك) مع عنوان URL.
انظر عن كثب إلى موفر-السمة. لقد قمنا بتخصيص سمة Material UI بشكل كبير لتتناسب مع تصميم تطبيق إدارة الموارد البشرية، وقمنا بإنشاء سمتين واحدة للمديرين والأخرى للموظفين لتمييزهم بألوان مختلفة.
أيضًا، قمنا بإضافة Inter كخط مخصص للتطبيق. للتثبيت، تحتاج إلى إضافة السطر التالي إلى ملف index.html:
<head><metacharset="utf-8"/><linkrel="icon"href="/favicon.ico"/><^><link<^/><^>
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
rel="stylesheet" /> <^><metaname="viewport"content="width=device-width, initial-scale=1"/><metaname="theme-color"content="#000000"/><metaname="description"content="refine | Build your React-based CRUD applications, without constraints."/><metadata-rh="true"property="og:image"content="https://refine.dev/img/refine_social.png"/><metadata-rh="true"name="twitter:image"content="https://refine.dev/img/refine_social.png"/><title>
Refine - Build your React-based CRUD applications, without constraints.
</title></head>
في الخطوة السابقة، أضفنا مكون تخطيط مخصص إلى التطبيق. عادةً، يمكننا استخدام التخطيط الافتراضي لإطار واجهة المستخدم، ولكننا نريد أن نوضح كيف يمكنك إجراء التخصيص.
يتضمن مكون التخطيط الرأس، الشريط الجانبي، ومنطقة المحتوى الرئيسية. يستخدم <ThemedLayoutV2 /> كأساس وقمنا بتخصيصه ليتناسب مع تصميم تطبيق إدارة الموارد البشرية.
يحتوي الشريط الجانبي على شعار التطبيق وروابط التنقل. على الأجهزة المحمولة، هو شريط جانبي قابل للطي يفتح عندما ينقر المستخدم على أيقونة القائمة. تم إعداد روابط التنقل باستخدام useMenu من Refine وتم عرضها بناءً على دور المستخدم بمساعدة <CanAccess /> المكون.
المركب على الشريط الجانبي، يعرض صورة المستخدم المسجل الدخول واسمه. عند النقر، يفتح نافذة منبثقة تحتوي على تفاصيل المستخدم وزر تسجيل الخروج. يمكن للمستخدمين التبديل بين أدوار مختلفة عن طريق الاختيار من القائمة المنسدلة. يتيح هذا المكون الاختبار من خلال التبديل بين المستخدمين بمختلف الأدوار.
لا يعرض أي شيء على أجهزة الكمبيوتر المكتبية. على الأجهزة المحمولة، يعرض شعار التطبيق وأيقونة قائمة لفتح الشريط الجانبي. الرأس ثابت ودائم الظهور في أعلى الصفحة.
يعرض عنوان الصفحة وأزرار التنقل. يتم توليد عنوان الصفحة تلقائيًا باستخدام useResource، الذي يجلب اسم المورد من سياق Refine. يسمح لنا بمشاركة نفس التصميم والتنسيق عبر التطبيق.
في هذه الخطوة، سنقوم بتنفيذ منطق المصادقة والتفويض لتطبيق إدارة الموارد البشرية الخاص بنا. سيكون هذا مثالًا رائعًا على التحكم في الوصول في التطبيقات المؤسسية.
عندما يقوم المستخدمون بتسجيل الدخول كمدير، سيكون بإمكانهم رؤية صفحات الإجازة والطلبات. إذا قاموا بتسجيل الدخول كموظف، سيرون فقط صفحة الإجازة. يمكن للمديرين الموافقة أو رفض طلبات الإجازة في صفحة الطلبات.
في Refine، تتم معالجة المصادقة بواسطة authProvider. يسمح لك بتعريف منطق المصادقة لتطبيقك. في الخطوة السابقة، قمنا بالفعل بنسخ authProvider من مستودع GitHub ومنحناه إلى مكون <Refine /> كخاصية. سنستخدم الحلقات والمكونات التالية للتحكم في سلوك تطبيقنا بناءً على ما إذا كان المستخدم مسجلاً الدخول أم لا.
useLogin: حلقة توفر دالة mutate لتسجيل دخول المستخدم.
useLogout: حلقة توفر دالة mutate لتسجيل خروج المستخدم.
useIsAuthenticated: هو هوك يُرجع قيمة بوليانية تشير إلى ما إذا كان المستخدم مصدقًا.
<Authenticated />: هو مكون يقوم بعرض أطفاله فقط إذا كان المستخدم مصدقًا.
Authorization
في Refine، يتم التعامل مع التفويض بواسطة accessControlProvider. وهو يتيح لك تعريف أدوار المستخدمين والأذونات، والتحكم في الوصول إلى أجزاء مختلفة من التطبيق بناءً على دور المستخدم. في الخطوة السابقة، قمنا بالفعل بنسخ accessControlProvider من مستودع GitHub وأعطيناه لمكون <Refine /> كخاصية. دعنا نلقي نظرة فاحصة على accessControlProvider لنرى كيف يعمل.
src/providers/access-control/index.ts
import type {AccessControlBindings}from"@refinedev/core";import{Role}from"@/types";exportconstaccessControlProvider:AccessControlBindings={options:{queryOptions:{keepPreviousData:true,},buttons:{hideIfUnauthorized:true,},},can:async({ params, action })=>{const user =JSON.parse(localStorage.getItem("user")||"{}");if(!user)return{can:false};const scope = params?.resource?.meta?.scope;// إذا لم يكن للمورد نطاق، فلا يمكن الوصول إليهif(!scope)return{can:false};if(user.role===Role.MANAGER){return{can:true,};}if(action ==="manager"){return{can: user.role===Role.MANAGER,};}if(action ==="employee"){return{can: user.role===Role.EMPLOYEE,};}// يمكن للمستخدمين الوصول إلى الموارد فقط إذا كان دورهم مطابقًا لنطاق الموردreturn{can: user.role=== scope,};},};
في تطبيقنا، لدينا دورين: MANAGER و EMPLOYEE.
لدى المديرين إمكانية الوصول إلى صفحة Requests، بينما لا يمكن للموظفين الوصول إلا إلى صفحة Time Off. يقوم accessControlProvider بفحص دور المستخدم ونطاق المورد لتحديد ما إذا كان بإمكان المستخدم الوصول إلى المورد. إذا تطابق دور المستخدم مع نطاق المورد، يمكنه الوصول إلى المورد. خلاف ذلك، يتم رفض الوصول. سنستخدم useCan هوك و<CanAccess /> مكون للتحكم في سلوك تطبيقنا بناءً على دور المستخدم.
في الخطوة السابقة، أضفنا authProvider إلى مكون <Refine />. authProvider مسؤول عن معالجة المصادقة.
أولاً، نحتاج إلى الحصول على الصور. سنستخدم هذه الصور كصور خلفية لصفحة تسجيل الدخول. أنشئ مجلدًا جديدًا يسمى images في مجلد public واحصل على الصور من مستودع GitHub.
بعد الحصول على الصور، دعنا ننشئ ملفًا جديدًا يسمى index.tsx في مجلد src/pages/login ونضيف الكود التالي:
لتبسيط عملية المصادقة، قمنا بإنشاء كائن mockUsers يحتوي على مصفوفتين: managers و employees. تحتوي كل مصفوفة على كائنات مستخدمين محددة مسبقًا. عندما يختار المستخدم بريدًا إلكترونيًا من القائمة المنسدلة وينقر على زر تسجيل الدخول، يتم استدعاء دالة login مع البريد الإلكتروني المحدد. دالة login هي دالة تعديل مقدمة من هوك useLogin من Refine. تستدعي authProvider.login مع البريد الإلكتروني المحدد.
بعد ذلك، دعنا نستورد مكون <PageLogin /> ونقوم بتحديث ملف App.tsx بالتغييرات المميزة.
في ملف App.tsx المحدث، قمنا بإضافة المكون <Authenticated /> من Refine. يُستخدم هذا المكون لحماية المسارات التي تتطلب المصادقة. يأخذ خاصية key لتحديد المكون بشكل فريد، وخاصية fallback للرسم عندما لا يكون المستخدم مصادقًا، وخاصية redirectOnFail لإعادة توجيه المستخدم إلى المسار المحدد عندما تفشل المصادقة. تحت الغطاء، تستدعي طريقة authProvider.check للتحقق مما إذا كان المستخدم مصادقًا.
دعنا نلقي نظرة أقرب على ما لدينا في key="auth-pages"
<Authenticated /> المكون يلتف حول مسار path="*" للتحقق من حالة مصادقة المستخدم. هذا المسار هو مسار شامل يعرض <ErrorComponent /> عندما يكون المستخدم مصدقًا. يسمح لنا بإظهار صفحة 404 عندما يحاول المستخدم الوصول إلى مسار غير موجود.
الآن، عندما تقوم بتشغيل التطبيق والتنقل إلى http://localhost:5173/login، يجب أن ترى صفحة تسجيل الدخول مع القائمة المنسدلة لاختيار المستخدم.
في الوقت الحالي، صفحة “/” لا تفعل شيئًا. في الخطوات التالية سنقوم بتنفيذ صفحات Time Off و Requests.
في هذه الخطوة، سنقوم ببناء صفحة الإجازات. يمكن للموظفين طلب إجازة ورؤية تاريخ إجازاتهم. يمكن للمديرين أيضًا عرض تاريخهم، ولكن بدلاً من طلب الإجازة، يمكنهم تعيينها لأنفسهم مباشرة. سنجعل هذا يعمل باستخدام accessControlProvider من Refine، ومكون <CanAccess />، وخطاف useCan.
<PageEmployeeTimeOffsList />
قبل أن نبدأ في بناء صفحة الإجازات، نحتاج إلى إنشاء بعض المكونات لعرض تاريخ الإجازات، وطلبات الإجازات القادمة، وإحصائيات الإجازات المستخدمة. في نهاية هذه الخطوة، سنستخدم هذه المكونات لبناء صفحة الإجازات.
بناء مكون <TimeOffList /> لعرض تاريخ الإجازات
قم بإنشاء مجلد جديد يسمى time-offs في مجلد src/components. داخل مجلد time-offs، أنشئ ملفًا جديدًا يسمى list.tsx وأضف الكود التالي:
<DateField />: يقوم بتنسيق وعرض التواريخ بطريقة سهلة الاستخدام.
value: التاريخ الذي سيتم عرضه.
format: يحدد تنسيق التاريخ (مثل “5 يناير”).
5. إنشاء مرشحات وعوامل تصنيف بناءً على type
المرشحات:
constfilters:Record<Props["type"],CrudFilters>={history:[{field:"status",operator:"eq",value:TimeOffStatus.APPROVED,},{field:"endsAt",operator:"lt",value: today,},],// ... أنواع أخرى};
يحدد المعايير لاسترجاع التواريخ المسموح بها بناءً على الحالة والتواريخ.
history: يسترجع التواريخ المسموح بها التي انتهت بالفعل.
upcoming: يسترجع التواريخ المسموح بها التي ستحدث قريبًا.
عوامل التصنيف:
constsorters:Record<Props["type"],CrudSort[]>={history:[{field:"startsAt",order:"desc"}],// ... أنواع أخرى};
يحدد ترتيب البيانات المسترجعة.
history: يقوم بالترتيب حسب تاريخ البدء بترتيب تنازلي.
بناء مكون <TimeOffLeaveCards /> لعرض إحصائيات الإجازات المستخدمة
أنشئ ملفًا جديدًا يسمى leave-cards.tsx في مجلد src/components/time-offs وأضف الكود التالي:
src/components/time-offs/leave-cards.tsx
import{ useGetIdentity, useList }from"@refinedev/core";import{Box,Grid,Skeleton,Typography}from"@mui/material";import{AnnualLeaveIcon,CasualLeaveIcon,SickLeaveIcon}from"@/icons";import{
type Employee,TimeOffStatus,TimeOffType,
type TimeOff,}from"@/types";
type Props={
employeeId?: number;};exportconstTimeOffLeaveCards=(props:Props)=>{const{data: employee,isLoading: isLoadingEmployee }=
useGetIdentity<Employee>({queryOptions:{enabled:!props.employeeId,},});
const {data: timeOffsSick,isLoading: isLoadingTimeOffsSick } =
useList<TimeOff>({resource:"time-offs",// نحن بحاجة فقط إلى العدد الإجمالي لأيام الإجازة المرضية، لذا يمكننا تعيين pageSize إلى 1 لتقليل الحملpagination:{pageSize:1},filters:[{field:"status",operator:"eq",value:TimeOffStatus.APPROVED,},{field:"timeOffType",operator:"eq",value:TimeOffType.SICK,},{field:"employeeId",operator:"eq",value: employee?.id,},],queryOptions:{enabled:!!employee?.id,},});
const {data: timeOffsCasual,isLoading: isLoadingTimeOffsCasual } =
useList<TimeOff>({resource:"time-offs",// نحن بحاجة فقط إلى العدد الإجمالي لأيام الإجازة المرضية، لذا يمكننا تعيين pageSize إلى 1 لتقليل الحملpagination:{pageSize:1},filters:[{field:"status",operator:"eq",value:TimeOffStatus.APPROVED,},{field:"timeOffType",operator:"eq",value:TimeOffType.CASUAL,},{field:"employeeId",operator:"eq",value: employee?.id,},],queryOptions:{enabled:!!employee?.id,},});
const loading =
isLoadingEmployee || isLoadingTimeOffsSick || isLoadingTimeOffsCasual;
return (
<Gridcontainerspacing="24px"><Griditemxs={12}sm={4}><Cardloading={loading}type="annual"value={employee?.availableAnnualLeaveDays ||0}/></Grid><Griditemxs={12}sm={4}><Cardloading={loading}type="sick"value={timeOffsSick?.total ||0}/></Grid><Griditemxs={12}sm={4}><Cardloading={loading}type="casual"value={timeOffsCasual?.total ||0}/></Grid></Grid>
);
};
const variantMap = {annual:{label:"Annual Leave",description:"Days available",bgColor:"primary.50",titleColor:"primary.900",descriptionColor:"primary.700",iconColor:"primary.700",icon:<AnnualLeaveIcon/>,},sick:{label:"Sick Leave",description:"Days used",bgColor:"#FFF7ED",titleColor:"#7C2D12",descriptionColor:"#C2410C",iconColor:"#C2410C",icon:<SickLeaveIcon/>,},casual:{label:"Casual Leave",description:"Days used",bgColor:"grey.50",titleColor:"grey.900",descriptionColor:"grey.700",iconColor:"grey.700",icon:<CasualLeaveIcon/>,},};
const Card = (props: {type:"annual"|"sick"|"casual";value: number;
loading?: boolean;}) => {return(<Boxsx={{backgroundColor: variantMap[props.type].bgColor,padding:"24px",borderRadius:"12px",}}><Boxsx={{display:"flex",alignItems:"center",justifyContent:"space-between",}}><Typographyvariant="h6"sx={{color: variantMap[props.type].titleColor,fontSize:"16px",fontWeight:500,lineHeight:"24px",}}>{variantMap[props.type].label}</Typography><Boxsx={{color: variantMap[props.type].iconColor,}}>{variantMap[props.type].icon}</Box></Box><Boxsx={{marginTop:"8px",display:"flex",flexDirection:"column"}}>{props.loading?(<Boxsx={{width:"40%",height:"32px",display:"flex",alignItems:"center",justifyContent:"center",}}><Skeletonvariant="rounded"sx={{width:"100%",height:"20px",}}/></Box>):(<Typographyvariant="caption"sx={{color: variantMap[props.type].descriptionColor,fontSize:"24px",lineHeight:"32px",fontWeight:600,}}>{props.value}</Typography>)}<Typographyvariant="body1"sx={{color: variantMap[props.type].descriptionColor,fontSize:"12px",lineHeight:"16px",}}>{variantMap[props.type].description}</Typography></Box></Box>);};
<TimeOffLeaveCards />
مكون <TimeOffLeaveCards /> يعرض إحصائيات حول إجازات الموظف. يظهر ثلاث بطاقات للإجازة السنوية، والإجازة المرضية، والإجازة العادية، تشير إلى عدد الأيام المتاحة أو المستخدمة.
لنقم بتفصيل الأجزاء الرئيسية من المكون:
1. جلب البيانات
بيانات الموظف: يستخدم useGetIdentity للحصول على معلومات الموظف الحالي، مثل عدد أيام الإجازة السنوية المتاحة.
عدد الإجازات: يستخدم useList لجلب العدد الإجمالي لأيام الإجازة المرضية والعادية المستخدمة من قبل الموظف. يتم تعيين pageSize إلى 1 لأننا بحاجة فقط إلى العدد الإجمالي، وليس جميع التفاصيل.
2. عرض البطاقات
يقوم المكون بتقديم ثلاث بطاقات، واحدة لكل نوع إجازة.
تظهر كل بطاقة:
نوع الإجازة (مثل الإجازة السنوية).
عدد الأيام المتاحة أو المستخدمة.
أيقونة تمثل نوع الإجازة.
3. التعامل مع حالات التحميل
إذا كانت البيانات لا تزال قيد التحميل، يتم عرض عنصر هيكلي بدلاً من الأرقام الفعلية.
يتم تمرير خاصية loading إلى البطاقات لإدارة هذه الحالة.
4. مكون البطاقة
يستقبل type، value، و loading كخصائص.
يستخدم variantMap للحصول على التسميات والألوان والأيقونات الصحيحة بناءً على نوع الإجازة.
يعرض معلومات الإجازة بتنسيق مناسب.
بناء <PageEmployeeTimeOffsList />
الآن بعد أن لدينا المكونات لقائمة الإجازات وعرض بطاقات الإجازة، دعنا ننشئ الملف الجديد في مجلد src/pages/employee/time-offs/ باسم list.tsx ونضيف الكود التالي:
src/pages/time-off.tsx
import{CanAccess, useCan }from"@refinedev/core";import{CreateButton}from"@refinedev/mui";import{Box,Grid}from"@mui/material";import{PageHeader}from"@/components/layout/page-header";import{TimeOffList}from"@/components/time-offs/list";import{TimeOffLeaveCards}from"@/components/time-offs/leave-cards";import{TimeOffIcon}from"@/icons";import{ThemeProvider}from"@/providers/theme-provider";import{Role}from"@/types";exportconstPageEmployeeTimeOffsList=()=>{const{data: useCanData }=useCan({action:"manager",params:{resource:{name:"time-offs",meta:{scope:"manager",},},},});const isManager = useCanData?.can;return(<ThemeProviderrole={isManager ?Role.MANAGER:Role.EMPLOYEE}><Box><PageHeadertitle="Time Off"rightSlot={<CreateButtonsize="large"variant="contained"startIcon={<TimeOffIcon/>}><CanAccessaction="manager"fallback="Request Time Off">
Assign Time Off
</CanAccess></CreateButton>}/><TimeOffLeaveCards/><Gridcontainerspacing="24px"sx={{marginTop:"24px",}}><Griditemxs={12}md={6}><Boxsx={{display:"flex",flexDirection:"column",gap:"24px",}}><TimeOffListtype="inReview"/><TimeOffListtype="upcoming"/></Box></Grid><Griditemxs={12}md={6}><TimeOffListtype="history"/></Grid></Grid></Box></ThemeProvider>);};
<PageEmployeeTimeOffsList /> هو المكون الرئيسي لصفحة الإجازات، سنستخدم هذا المكون لعرض قوائم الإجازات وبطاقات الإجازة عندما يتنقل المستخدمون إلى مسار /employee/time-offs.
<PageEmployeeTimeOffsList />
دعنا نفصل الأجزاء الرئيسية من المكون:
1. التحقق من أدوار المستخدم
يستخدم useCan لتحديد ما إذا كان المستخدم الحالي مديرًا.
يحدد isManager إلى true إذا كان لدى المستخدم أذونات المدير.
2. تطبيق السمة بناءً على الدور
يحيط المحتوى داخل <ThemeProvider />.
يتغير الثيم بناءً على ما إذا كان المستخدم مديرًا أو موظفًا.
3. رأس الصفحة مع زر شرطي
يعرض <PageHeader /> بعنوان “وقت الإجازة”.
يتضمن <CreateButton /> الذي يتغير بناءً على دور المستخدم:
إذا كان المستخدم مديرًا، فإن الزر يقول “تعيين وقت الإجازة”.
إذا لم يكن المستخدم مديرًا، فإنه يقول “طلب وقت الإجازة”.
يتم التعامل مع هذا باستخدام مكون <CanAccess />، الذي يتحقق من الأذونات.
لقد أضفنا موردًا جديدًا للإجازات كابنًا لمورد employee. يشير هذا إلى أن الإجازات ذات علاقة بالموظفين ويمكن الوصول إليها من قبل الموظفين.
name: 'time-offs': هذا هو المعرف للمورد، المستخدم داخليًا بواسطة Refine.
list: '/employee/time-offs': يحدد الRoute الذي يعرض عرض القائمة للمورد.
الميتا: كائن يحتوي على بيانات ذاتية إضافية عن المورد.
parent: 'employee': يقوم بتجميع هذا المورد تحت نطاق employee، والذي يمكن استخدامه لتنظيم الموارد في واجهة المستخدم (مثل القائمة الجانبية) أو لمراقبة الوصول.
scope: Role.EMPLOYEE: يشير إلى أن هذا المورد يمكن الوصول إليه من قبل المستخدمين الذين يحملون دور EMPLOYEE. نحن نستخدم هذا في accessControlProvider لإدارة الأذونات.
label: 'Time Off': الاسم الذي سيتم عرضه للمورد في واجهة المستخدم.
icon: <TimeOffIcon />: يرتبط رمز TimeOffIcon بهذا المورد للتعرف البصري.
2. إعادة توجيه المستخدمين إلى مورد “time-offs” عندما يقومون بالانتقال إلى المسار /
نستخدم مكون <NavigateToResource /> لتوجيه المستخدمين إلى مورد time-offs عندما يتصفحون المسار /. يضمن هذا أن يرون المستخدمون قائمة الإجازات بشكل افتراضي.
3. إعادة التوجيه إلى مورد “time-offs” عندما يكون المستخدمون مصادقين
ننظم صفحات الموظفين باستخدام مسارات متداخلة. أولاً، ننشئ مسارًا رئيسيًا بـ path='employee' يلف المحتوى بتصميم وتخطيط مخصصين للموظفين. داخل هذا المسار، نضيف path='time-offs'، الذي يعرض مكون PageEmployeeTimeOffsList. يقوم هذا الهيكل بتجميع جميع ميزات الموظف تحت مسار واحد ويحافظ على تنسيق النمط.
بعد إضافة هذه التغييرات، يمكنك التنقل إلى مسار /employee/time-offs لرؤية صفحة قائمة الإجازات في العمل.
/employee/time-offs
في الوقت الحالي، تعمل صفحة قائمة الإجازات، ولكنها تفتقر إلى القدرة على إنشاء طلبات إجازة جديدة. دعنا نضيف القدرة على إنشاء طلبات إجازة جديدة.
سنقوم بإنشاء صفحة جديدة لطلب أو تخصيص إجازة. ستتضمن هذه الصفحة نموذجًا حيث يمكن للمستخدمين تحديد نوع الإجازة، وتواريخ البدء والانتهاء، وأي ملاحظات إضافية.
قبل أن نبدأ، نحتاج إلى إنشاء مكونات جديدة لاستخدامها في النموذج:
بناء مكون <TimeOffFormSummary />
قم بإنشاء ملف جديد يسمى form-summary.tsx في مجلد src/components/time-offs/ وأضف الكود التالي:
src/components/time-offs/form-summary.tsx
import{Box,Divider,Typography}from"@mui/material";
type Props={availableAnnualDays: number;requestedDays: number;};exportconstTimeOffFormSummary=(props:Props)=>{const remainingDays = props.availableAnnualDays- props.requestedDays;return(<Boxsx={{display:"flex",flexDirection:"column",alignItems:"flex-end",gap:"16px",whiteSpace:"nowrap",}}><Boxsx={{display:"flex",gap:"16px",}}><Typographyvariant="body2"color="text.secondary">
Available Annual Leave Days:
</Typography><Typographyvariant="body2">{props.availableAnnualDays}</Typography></Box><Boxsx={{display:"flex",gap:"16px",}}><Typographyvariant="body2"color="text.secondary">
Requested Days:
</Typography><Typographyvariant="body2">{props.requestedDays}</Typography></Box><Dividersx={{width:"100%",}}/><Boxsx={{display:"flex",gap:"16px",height:"40px",}}><Typographyvariant="body2"color="text.secondary">
Remaining Days:
</Typography><Typographyvariant="body2"fontWeight={500}>{remainingDays}</Typography></Box></Box>);};
<TimeOffFormSummary />
مكون <TimeOffFormSummary /> يعرض ملخص طلب الإجازة. يظهر عدد أيام الإجازة السنوية المتاحة، وعدد الأيام المطلوبة، والأيام المتبقية. سنستخدم هذا المكون في نموذج الإجازة لتزويد المستخدمين بنظرة عامة واضحة عن طلبهم.
بناء مكون <PageEmployeeTimeOffsCreate />
قم بإنشاء ملف جديد يسمى create.tsx في مجلد src/pages/employee/time-offs/ وأضف الكود التالي:
src/pages/time-offs/create.tsx
import{ useCan, useGetIdentity, type HttpError}from"@refinedev/core";import{ useForm }from"@refinedev/react-hook-form";import{Controller}from"react-hook-form";import type {DateRange}from"@mui/x-date-pickers-pro/models";import{Box,Button,MenuItem,Select,Typography}from"@mui/material";importdayjsfrom"dayjs";import{PageHeader}from"@/components/layout/page-header";import{InputText}from"@/components/input/text";import{LoadingOverlay}from"@/components/loading-overlay";import{InputDateStartsEnds}from"@/components/input/date-starts-ends";import{TimeOffFormSummary}from"@/components/time-offs/form-summary";import{ThemeProvider}from"@/providers/theme-provider";import{
type Employee,
type TimeOff,TimeOffType,TimeOffStatus,Role,}from"@/types";import{CheckRectangleIcon}from"@/icons";
type FormValues=Omit<TimeOff,"id"|"notes">&{notes: string;dates:DateRange<dayjs.Dayjs>;
};
export const PageEmployeeTimeOffsCreate = () => {const{data: useCanData }=useCan({action:"manager",params:{resource:{name:"time-offs",meta:{scope:"manager",},},},});const isManager = useCanData?.can;const{data: employee }=
useGetIdentity<Employee>();
const {refineCore:{ formLoading, onFinish },...formMethods
} = useForm<TimeOff, HttpError, FormValues>({defaultValues:{timeOffType:TimeOffType.ANNUAL,notes:"",dates:[null,null],},refineCoreProps:{successNotification:()=>{return{message: isManager
?"Time off assigned":"Your time off request is submitted for review.",type:"success",};},},});
const { control, handleSubmit, formState, watch } = formMethods;
const onFinishHandler = async (values: FormValues) => {constpayload:FormValues={...values,startsAt:dayjs(values.dates[0]).format("YYYY-MM-DD"),endsAt:dayjs(values.dates[1]).format("YYYY-MM-DD"),...(isManager &&{status:TimeOffStatus.APPROVED,}),};awaitonFinish(payload);};
const timeOffType = watch("timeOffType");
const selectedDays = watch("dates");
const startsAt = selectedDays[0];
const endsAt = selectedDays[1];
const availableAnnualDays = employee?.availableAnnualLeaveDays ?? 0;
const requestedDays =
startsAt && endsAt ? endsAt.diff(startsAt, "day") + 1 : 0;
return (
<ThemeProviderrole={isManager ?Role.MANAGER:Role.EMPLOYEE}><LoadingOverlayloading={formLoading}><Box><PageHeadertitle={isManager ?"Assign Time Off":"Request Time Off"}showListButtonshowDivider/><Boxcomponent="form"onSubmit={handleSubmit(onFinishHandler)}sx={{display:"flex",flexDirection:"column",gap:"24px",marginTop:"24px",}}><Box><Typographyvariant="body2"sx={{mb:"8px",}}>
Time Off Type
</Typography>
<Controller
name="timeOffType"
control={control}
render={({ field })=>(<Select{...field}size="small"sx={{minWidth:"240px",height:"40px","& .MuiSelect-select":{paddingBlock:"10px",},}}><MenuItemvalue={TimeOffType.ANNUAL}>Annual Leave</MenuItem><MenuItemvalue={TimeOffType.CASUAL}>Casual Leave</MenuItem><MenuItemvalue={TimeOffType.SICK}>Sick Leave</MenuItem></Select>)}
/>
</Box><Box><Typographyvariant="body2"sx={{mb:"16px",}}>
Requested Dates
</Typography>
<Controller
name="dates"
control={control}
rules={{validate:(value)=>{if(!value[0]||!value[1]){return"Please select both start and end dates";}returntrue;},}}
render={({ field })=>{return(<Box
sx={{display:"grid",gridTemplateColumns:()=>{return{sm:"1fr",lg:"628px 1fr",};},gap:"40px",}}><InputDateStartsEnds{...field}error={formState.errors.dates?.message}availableAnnualDays={availableAnnualDays}requestedDays={requestedDays}/>{timeOffType ===TimeOffType.ANNUAL&&(<Box
sx={{display:"flex",maxWidth:"628px",alignItems:()=>{return{lg:"flex-end",};},justifyContent:()=>{return{xs:"flex-end",lg:"flex-start",};},}}><TimeOffFormSummaryavailableAnnualDays={availableAnnualDays}requestedDays={requestedDays}/></Box>
)}
</Box>
);
}}
/>
</Box><Boxsx={{maxWidth:"628px",}}><Controllername="notes"control={control}render={({ field, fieldState })=>{return(<InputText{...field}label="Notes"error={fieldState.error?.message}placeholder="Place enter your notes"multilinerows={3}/>);}}/></Box><Buttonvariant="contained"size="large"type="submit"startIcon={isManager ?<CheckRectangleIcon/>:undefined}>{isManager ?"Assign":"Send Request"}</Button></Box></Box></LoadingOverlay></ThemeProvider>
);
};
<PageEmployeeTimeOffsCreate />
مكون <PageEmployeeTimeOffsCreate /> يعرض نموذجًا لإنشاء طلبات إجازة جديدة في تطبيق إدارة الموارد البشرية. يمكن لكل من الموظفين والمديرين استخدامه لطلب أو تخصيص إجازة. يتضمن النموذج خيارات لتحديد نوع الإجازة، واختيار تواريخ البدء والانتهاء، وإضافة ملاحظات، ويظهر ملخصًا للإجازة المطلوبة.
با استخدام خطاف useCan، نتحقق مما إذا كان المستخدم الحالي لديه صلاحيات مدير. يحدد هذا ما إذا كان بإمكان المستخدم تعيين وقت الراحة أم فقط طلبه. سنعالج تقديم النموذج بشكل مختلف على onFinishHandler استنادًا إلى دور المستخدم.
2. حالة النموذج وإرساله
const{refineCore:{ formLoading, onFinish },...formMethods
}= useForm<TimeOff,HttpError,FormValues>({defaultValues:{timeOffType:TimeOffType.ANNUAL,notes:"",dates:[null,null],},refineCoreProps:{successNotification:()=>{return{message: isManager
?"Time off assigned":"Your time off request is submitted for review.",type:"success",};},},});const{ control, handleSubmit, formState, watch }= formMethods;constonFinishHandler=async(values:FormValues)=>{constpayload:FormValues={...values,startsAt:dayjs(values.dates[0]).format("YYYY-MM-DD"),endsAt:dayjs(values.dates[1]).format("YYYY-MM-DD"),...(isManager &&{status:TimeOffStatus.APPROVED,}),};awaitonFinish(payload);};
useForm يهيء النموذج بالقيم الافتراضية ويضبط إشعارات النجاح استنادًا إلى دور المستخدم. تقوم وظيفة onFinishHandler بمعالجة بيانات النموذج قبل إرسالها. بالنسبة للمديرين، يتم تعيين الحالة إلى APPROVED على الفور، بينما تُرسل طلبات الموظفين للمراجعة.
في تصميمنا، تتغير اللون الرئيسي استنادًا إلى دور المستخدم. نحن نستخدم <ThemeProvider /> لتطبيق السمة الصحيحة وفقًا لذلك. نص زر الإرسال والرمز يتغير أيضًا اعتمادًا على ما إذا كان المستخدم مديرًا أو موظفًا.
4. إضافة مسار “employee/time-offs/create”
نحتاج إلى إضافة المسار الجديد لصفحة إنشاء وقت الراحة. دعنا نحدث ملف App.tsx لتضمين هذا المسار:
بعد إضافة هذه التغييرات، يمكنك التنقل إلى المسار /employee/time-offs/create أو النقر على زر “Assign Time Off” في صفحة قائمة أوقات الراحة للوصول إلى نموذج إنشاء وقت الراحة.
في هذه الخطوة، سنقوم بإنشاء صفحة جديدة لإدارة طلبات الإجازات. ستتيح هذه الصفحة للمديرين مراجعة وتقديم الموافقة أو الرفض على طلبات الإجازة المقدمة من الموظفين.
سنقوم بإنشاء صفحة جديدة لإدارة طلبات الإجازات. ستتضمن هذه الصفحة قائمة بطلبات الإجازات، تعرض تفاصيل مثل اسم الموظف، نوع الإجازة، التواريخ المطلوبة، والحالة الحالية.
قبل أن نبدأ، نحتاج إلى إنشاء مكونات جديدة لاستخدامها في القائمة:
إنشاء مكون <RequestsList />
قم بإنشاء ملف جديد يسمى list.tsx في مجلد src/components/requests/ وأضف الكود التالي:
يعرض مكون <RequestsList /> قائمة بطلبات الإجازات مع تمرير غير محدود. يتضمن مؤشر تحميل، أماكن شاغرة هيكلية، ورسالة عند عدم وجود بيانات. تم تصميم هذا المكون للتعامل مع مجموعات بيانات كبيرة بكفاءة وتوفير تجربة مستخدم سلسة.
بناء مكون <RequestsListItem />
قم بإنشاء ملف جديد يسمى list-item.tsx في مجلد src/components/requests/ وأضف الكود التالي:
يعرض مكون <RequestsListItem /> طلب إجازة واحد في القائمة. يتضمن صورة الموظف، الاسم، الوصف، وزر لعرض تفاصيل الطلب. هذا المكون قابل لإعادة الاستخدام ويمكن استخدامه لعرض كل عنصر في قائمة طلبات الإجازة.
بناء مكون <PageManagerRequestsList />
قم بإنشاء ملف جديد يسمى list.tsx في مجلد src/pages/manager/requests/ وأضف الكود التالي:
import type {PropsWithChildren}from"react";import{ useGo, useInfiniteList }from"@refinedev/core";import{Box,Typography}from"@mui/material";importdayjsfrom"dayjs";import{Frame}from"@/components/frame";import{PageHeader}from"@/components/layout/page-header";import{RequestsListItem}from"@/components/requests/list-item";import{RequestsList}from"@/components/requests/list";import{ indigo }from"@/providers/theme-provider/colors";import{TimeOffIcon,RequestTypeIcon,NoTimeOffIcon}from"@/icons";import{TimeOffStatus, type Employee, type TimeOff}from"@/types";exportconstPageManagerRequestsList=({ children }:PropsWithChildren)=>{return(<><Box><PageHeadertitle="Awaiting Requests"/><TimeOffsList/></Box>{children}</>);};constTimeOffsList=()=>{const go =useGo();const{data: timeOffsData,isLoading: timeOffsLoading,fetchNextPage: timeOffsFetchNextPage,hasNextPage: timeOffsHasNextPage,}= useInfiniteList<TimeOff&{employee:Employee;}>({resource:"time-offs",filters:[{field:"status",operator:"eq",value:TimeOffStatus.PENDING},],sorters:[{field:"createdAt",order:"desc"}],meta:{join:["employee"],},});const timeOffs = timeOffsData?.pages.flatMap((page)=> page.data)||[];const totalCount = timeOffsData?.pages[0].total;return(<Frametitle="Time off Requests"titleSuffix={!!totalCount &&
totalCount >0&&(<Boxsx={{padding:"4px",display:"flex",alignItems:"center",justifyContent:"center",minWidth:"24px",height:"24px",borderRadius:"4px",backgroundColor: indigo[100],}}><Typographyvariant="caption"sx={{color: indigo[500],fontSize:"12px",lineHeight:"16px",}}>{totalCount}</Typography></Box>)}icon={<TimeOffIconwidth={24}height={24}/>}sx={{flex:1,paddingBottom:"0px",}}sxChildren={{padding:0,}}><RequestsListloading={timeOffsLoading}dataLength={timeOffs.length}hasMore={timeOffsHasNextPage ||false}next={timeOffsFetchNextPage}scrollableTarget="scrollableDiv-timeOffs"noDataText="No time off requests right now."noDataIcon={<NoTimeOffIcon/>}>{timeOffs.map((timeOff)=>{const date =dayjs(timeOff.createdAt).fromNow();const fullName =`${timeOff.employee.firstName}${timeOff.employee.lastName}`;const avatarURL = timeOff.employee.avatarUrl;const requestedDay =dayjs(timeOff.endsAt).diff(dayjs(timeOff.startsAt),"day")+1;const description =`Requested ${requestedDay}${
requestedDay >1?"days":"day"} of time ${timeOff.timeOffType.toLowerCase()} leave.`;return(<RequestsListItem
key={timeOff.id}
date={date}
avatarURL={avatarURL}
title={fullName}
showTimeSince
descriptionIcon={<RequestTypeIcontype={timeOff.timeOffType}/>}
description={description}
onClick={()=>{go({type:"replace",to:{resource:"requests",id: timeOff.id,action:"edit",},});}}/>);})}</RequestsList></Frame>);};
يعرض مكون <PageManagerRequestsList /> طلبات الإجازة المعلقة التي يحتاج المديرون إلى الموافقة عليها. يظهر تفاصيل مثل اسم الموظف، نوع الإجازة، التواريخ المطلوبة، ومدة الزمن التي مضت منذ تقديم الطلب. يمكن للمديرين النقر على طلب لرؤية المزيد من التفاصيل. يستخدم <RequestsList /> و <RequestsListItem /> لعرض القائمة.
هذا المكون يقبل أيضًا children كخاصية. بعد ذلك، سنقوم بتنفيذ مسار نافذة منبثقة باستخدام <Outlet /> لعرض تفاصيل الطلب، مما يجعل مسار /manager/requests/:id داخل المكون.
إضافة مسار “/manager/requests”
نحتاج إلى إضافة المسار الجديد لصفحة إدارة طلبات الإجازة. دعونا نحدث ملف App.tsx ليشمل هذا المسار:
في هذه الخطوة، سنقوم بإنشاء صفحة جديدة لعرض تفاصيل طلب الإجازة. ستظهر هذه الصفحة اسم الموظف، نوع الإجازة، التواريخ المطلوبة، والحالة الحالية. يمكن للمديرين الموافقة على الطلب أو رفضه من هذه الصفحة.
بناء مكون <TimeOffRequestModal />
أولاً، قم بإنشاء ملف يسمى use-get-employee-time-off-usage في مجلد src/hooks/ وأضف الكود التالي:
سنستخدم هوك useGetEmployeeTimeOffUsage لحساب العدد الإجمالي للأيام التي أخذها الموظف لكل نوع من أنواع الإجازات. ستعرض هذه المعلومات في صفحة تفاصيل طلب الإجازة.
بعد ذلك، قم بإنشاء ملف جديد يسمى time-off-request-modal.tsx في مجلد src/components/requests/ وأضف الكود التالي:
يتم استخدام useGetEmployeeTimeOffUsage لجلب استخدام الموظف لأيام الإجازة. يقوم هذا الـ hook بحساب الأيام المتبقية من الإجازة السنوية وأيام الإجازة المرضية والعادية المستخدمة سابقًا استنادًا إلى تاريخ إجازات الموظف.
يستخدم useList مع الفلاتر المذكورة أعلاه لجلب جميع الإجازات المعتمدة التي تتداخل مع طلب الإجازة الحالي. يتم استخدام هذه القائمة لعرض الموظفين الذين هم في إجازة بين التواريخ المطلوبة.
3. معالجة الموافقة/الرفض لطلب الإجازة
يتم استدعاء دالة handleSubmit عندما يوافق المدير أو يرفض طلب الإجازة.
يقوم Refine تلقائيًا بإبطال ذاكرة التخزين المؤقت للموارد بعد تغيير المورد (في هذه الحالة time-offs). نظرًا لأن استخدام الموظف للإجازة يتم حسابه بناءً على تاريخ الإجازات، فإننا نقوم أيضًا بإبطال ذاكرة التخزين المؤقت لمورد employees لتحديث استخدام الموظف للإجازة.
إضافة مسار “/manager/requests/:id”
في هذه الخطوة، سنقوم بإنشاء مسار جديد لعرض صفحة تفاصيل طلب الإجازة، حيث يمكن للمديرين الموافقة أو رفض الطلبات.
دعنا ننشئ ملفًا جديدًا يسمى edit.tsx في مجلد src/pages/manager/requests/time-offs/ ونضيف الكود التالي:
src/pages/manager/requests/time-offs/edit.tsx
import{ useGo, useShow }from"@refinedev/core";import{TimeOffRequestModal}from"@/components/requests/time-off-request-modal";import type {Employee,TimeOff}from"@/types";exportconstPageManagerRequestsTimeOffsEdit=()=>{const go =useGo();const{query: timeOffRequestQuery }= useShow<TimeOff&{employee:Employee}>({meta:{join:["employee"],},});const loading = timeOffRequestQuery.isLoading;return(<TimeOffRequestModal
open
loading={loading}
timeOff={timeOffRequestQuery?.data?.data}
onClose={()=>go({to:{resource:"requests",action:"list",},})}
onSuccess={()=>{go({to:{resource:"requests",action:"list",},});}}/>);};
الآن نحتاج إلى إضافة المسار الجديد لعرض صفحة تفاصيل طلب الإجازة. دعنا نقوم بتحديث ملف App.tsx لتضمين هذا المسار:
الكود أعلاه يقوم بإعداد هيكل مسارات متداخلة حيث يتم عرض نافذة منبثقة عند التنقل إلى مسار فرعي محدد. مكون <PageManagerRequestsTimeOffsEdit /> هو نافذة منبثقة ويُعرض كطفل لمكون <PageManagerRequestsList />. تتيح لنا هذه البنية عرض النافذة المنبثقة فوق صفحة القائمة مع الحفاظ على ظهور صفحة القائمة في الخلفية.
عند التنقل إلى مسار /manager/requests/:id/edit أو النقر على طلب إجازة في القائمة، ستظهر صفحة تفاصيل طلب الإجازة كنافذة منبثقة فوق صفحة القائمة.
التفويض هو عنصر حاسم في تطبيقات المؤسسات، حيث يلعب دورًا رئيسيًا في كل من الأمن وكفاءة التشغيل. يضمن أن المستخدمين المخولين فقط يمكنهم الوصول إلى موارد محددة، مما يحمي البيانات الحساسة والوظائف. نظام التفويض الخاص بـ Refine يوفر البنية التحتية اللازمة لحماية مواردك وضمان تفاعل المستخدمين مع تطبيقك بطريقة آمنة ومراقبة. في هذه الخطوة، سنقوم بتنفيذ التفويض والتحكم في الوصول لميزة إدارة طلبات الإجازات. سنقوم بتقييد الوصول إلى المسارات /manager/requests و/manager/requests/:id/edit للمديرين فقط بمساعدة <CanAccess /> المكون.
حاليًا، عندما تسجل الدخول كموظف، لا يمكنك رؤية رابط صفحة Requests في الشريط الجانبي، لكن يمكنك الوصول إلى مسار /manager/requests عن طريق كتابة عنوان URL في المتصفح. سنضيف حاجزًا لمنع الوصول غير المصرح به إلى هذه المسارات.
في الكود أعلاه، أضفنا مكون <CanAccess /> إلى مسار “/manager”. يتحقق هذا المكون مما إذا كان لدى المستخدم دور “مدير” قبل عرض المسارات الفرعية. إذا لم يكن لدى المستخدم دور “مدير”، سيتم إعادة توجيههم إلى صفحة قائمة الإجازات للموظفين.
الآن، عند تسجيل الدخول كموظف ومحاولة الوصول إلى الطريق /manager/requests، سيتم توجيهك إلى صفحة قائمة الإجازات للموظفين.
ثم، حدد أنك تريد رفع الشفرة الخاصة بك إلى فرع main بهذا الأمر:
git branch -M main
أخيرًا، ادفع الكود إلى مستودع GitHub باستخدام هذا الأمر:
git push -u origin main
عند المطالبة، أدخل بيانات اعتماد GitHub الخاصة بك لدفع الكود.
ستتلقى رسالة نجاح بعد دفع الكود إلى مستودع GitHub.
في هذا القسم، قمت بدفع مشروعك إلى GitHub حتى تتمكن من الوصول إليه باستخدام تطبيقات DigitalOcean. الخطوة التالية هي إنشاء تطبيق DigitalOcean جديد باستخدام مشروعك وإعداد النشر التلقائي.
خلال ذلك، ستقوم بأخذ تطبيق React وتحضيره للنشر عبر منصة تطبيقات DigitalOcean. ستربط مستودع GitHub الخاص بك بـ DigitalOcean، وتقوم بتكوين كيفية بناء التطبيق، ثم تنشئ نشرًا أوليًا لمشروع. بعد نشر المشروع، سيتم إعادة بناء التغييرات الإضافية التي تجريها تلقائيًا وتحديثها.
بنهاية هذه الخطوة، سيكون لديك تطبيقك منشورًا على DigitalOcean مع توفير تسليم مستمر.
قم بتسجيل الدخول إلى حسابك في DigitalOcean وانتقل إلى صفحة التطبيقات. انقر على زر إنشاء تطبيق:
إذا لم تكن قد قمت بربط حسابك على GitHub بـ DigitalOcean، سيُطلب منك القيام بذلك. انقر على زر Connect to GitHub. ستنفتح نافذة جديدة، تطلب منك تخويل DigitalOcean للوصول إلى حسابك على GitHub.
بعد أن تخوّل DigitalOcean، سيتم توجيهك مرة أخرى إلى صفحة تطبيقات DigitalOcean. الخطوة التالية هي اختيار مستودع GitHub الخاص بك. بعد اختيار المستودع، سيُطلب منك اختيار فرع للنشر. حدد الفرع main وانقر على زر Next.
بعد ذلك، سترى خطوات التكوين لتطبيقك. في هذا البرنامج التعليمي، يمكنك النقر على زر Next لتخطي خطوات التكوين. ومع ذلك، يمكنك أيضًا تكوين تطبيقك كما تريد.
انتظر حتى يكتمل البناء. بعد اكتمال البناء، اضغط على Live App للوصول إلى مشروعك في المتصفح. سيكون نفس المشروع الذي اختبرته محليًا، ولكن سيكون على الويب برابط URL آمن. كما يمكنك متابعة هذا البرنامج التعليمي المتوفر على موقع DigitalOcean لتعلم كيفية نشر تطبيقات React على منصة التطبيقات.
ملاحظة: في حال فشل بناءك في النشر بنجاح، يمكنك تكوين أمر البناء الخاص بك على DigitalOcean لاستخدام npm install --production=false && npm run build && npm prune --production بدلاً من npm run build