ملخص المشروع
انتمبرمج محترف في البرمجة باستخدام vue3 , frappe-ui , tailwindcss وانا اريد مساعدتك واريد منك ان تقوم بتوظيف خبرتك العميقه في الاشيئ التي سوف اطلبها من , لدي صفحة باسم steps.vue ولتي سوف ارسل لك محتواها ويتم من خلال هذه الصفحة استدعاء عدت صفحات فرعية وادارتها وعرضها حسب الحاجة هذه الصفحات هي {step1,step2,step3,step4,step5,step6} ولتي سوف ارسل لك بمحتوا كل صفحه حسب ما steps.vue: المكون الرئيسي لإدارة التنقل بين المراحل المختلفة (Step1 إلى Step6) وعرض شريط التقدم والتنبيهات.

step1.vue: المكون الفرعي للمرحلة الأولى يحتوي على نماذج الإدخال والتحقق من الصحة.

convertor.ts: دالة لتحويل البيانات إلى هيكل مبسط لجعلها جاهزة للإرسال.

validateInputStep1.ts: دالة للتحقق من صحة بيانات المرحلة الأولى.

request.js: وظائف لإنشاء واسترجاع وتحديث سجلات Verification Instructions Request باستخدام frappe-ui.


شكرًا لمشاركة ملخص مشروعك المفصل. يبدو أنك تعمل على تطبيق متكامل باستخدام Vue3، Frappe-UI، وTailwind CSS، وتحتاج إلى مساعدة في إدارة التنقل بين المراحل المختلفة والتحقق من صحة البيانات، بالإضافة إلى التعامل مع الطلبات عبر request.js.

للمساعدة بشكل فعال، يمكنك اتباع الخطوات التالية:

مشاركة الأكواد الحالية:
ابدأ بإرسال كود steps.vue والمكونات الفرعية مثل step1.vue حتى step6.vue.
أرسل أيضًا ملفات convertor.ts, validateInputStep1.ts, وrequest.js حتى أتمكن من مراجعتها وفهم كيفية تفاعلها مع بعضها البعض.
تحديد المشكلات أو النقاط التي تحتاج إلى تحسين:
هل تواجه مشاكل في التنقل بين الخطوات؟
هل تحتاج إلى تحسين في التحقق من صحة البيانات؟
هل هناك وظائف معينة في request.js تحتاج إلى مراجعة أو تحسين؟
تقديم أمثلة أو سيناريوهات معينة:
إذا كان هناك سلوك غير متوقع يحدث في التطبيق، يرجى وصفه بالتفصيل.
إذا كنت تخطط لإضافة ميزات جديدة، يمكنك توضيحها لأقدم لك أفضل النصائح حول كيفية دمجها.
اقتراح تحسينات عامة:
إدارة الحالة (State Management): قد يكون من المفيد استخدام Vuex أو Pinia لإدارة حالة التطبيق إذا كان لديك الكثير من البيانات المتشابكة بين المكونات.
تحسين الأداء: تأكد من تحميل المكونات بشكل ديناميكي إذا كانت صغيرة الحجم لتسريع وقت التحميل الأولي.
إعادة الاستخدام: حاول جعل المكونات الفرعية قابلة لإعادة الاستخدام بقدر الإمكان لتقليل التكرار وزيادة الكفاءة.
استخدام Tailwind CSS بفعالية:
تأكد من الاستفادة الكاملة من فئات Tailwind لتحقيق تصميم متجاوب وسهل التعديل.
يمكنك إنشاء فئات مخصصة إذا لاحظت تكرارًا في الأنماط لتبسيط الكود وجعله أكثر قراءة.
التعامل مع الطلبات باستخدام request.js:
تأكد من معالجة الأخطاء بشكل فعال وإظهار التنبيهات المناسبة للمستخدم عند حدوث مشكلات.
قد يكون من الجيد استخدام مكتبات مثل Axios لإدارة الطلبات إذا لم تكن تستخدمها بالفعل.
بمجرد مشاركة الأكواد أو تحديد النقاط التي تحتاج إلى مساعدة فيها، سأكون سعيدًا بتقديم دعم أكثر تحديدًا ومساعدتك في تحسين تطبيقك.



شكرًا لمشاركة كود steps.vue الخاص بك. سأقوم بمراجعة الكود وتقديم بعض الاقتراحات والتحسينات التي قد تساعدك في إدارة التنقل بين المراحل المختلفة، التحقق من صحة البيانات، وتحسين تجربة المستخدم بشكل عام.

مراجعة الكود والاقتراحات
1. إدارة التنقل والتحقق من الصحة
في الكود الحالي، تقوم دالة nextStep بزيادة قيمة processed و stepN بدون التحقق من صحة البيانات للمراحل السابقة. من الأفضل دمج عملية التحقق من الصحة وحفظ البيانات قبل الانتقال إلى المرحلة التالية.

تحسين دالة nextStep:

const nextStep = async function () {
  switch (processed.value) {
    case 0:
      const isStep1Valid = await step1Save();
      if (!isStep1Valid) return;
      break;
    case 1:
      const isStep2Valid = await step2Save();
      if (!isStep2Valid) return;
      break;
    case 2:
      const isStep3Valid = await step3Save();
      if (!isStep3Valid) return;
      break;
    case 3:
      const isStep4Valid = await step4Save();
      if (!isStep4Valid) return;
      break;
    case 4:
      const isStep5Valid = await step5Save();
      if (!isStep5Valid) return;
      break;
    case 5:
      await step6Save();
      return; // لا نزيد بعد الخطوة السادسة
    default:
      break;
  }
  processed.value++;
  stepN.value++;
}
تحديث دوال الحفظ لضمان التحقق الصحيح:
على سبيل المثال، دالة step1Save يمكن أن تعيد قيمة تحقق:


const step1Save = function () {
  let validateRes = validateInputStep1(step1Data);

  if (validateRes === true) {
    triggerAlert("يرجى تصحيح الأخطاء قبل المتابعة.");
    return false;
  } else {
    let converted = objectConvertor(step1Data);
    updateFieldsInRequestList(requestListStep1, converted);
    return true;
  }
}
قم بتطبيق نفس المنطق لباقي دوال الحفظ (step2Save إلى step6Save) لضمان التحقق من صحة البيانات قبل الانتقال.

2. إزالة computed غير الضرورية
الدالة nextNum حالياً تقوم بزيادة قيمة processed بشكل غير مقصود، مما قد يؤدي إلى زيادة غير متوقعة للقيمة. إذا لم تكن هناك حاجة لها، يمكنك إزالتها لتجنب التعقيدات.


// إزالة الـ computed التالي إذا لم يكن لديه استخدام محدد
// const nextNum = computed(() => {
//   return processed.value++
// })
3. تحسين عرض التنبيهات (SnackBar)
يمكن تحسين طريقة عرض التنبيهات لضمان أنها تظهر بشكل متسلسل ولا تتداخل مع بعضها البعض.

استخدام v-if بدلاً من v-for إذا كنت تريد عرض تنبيه واحد في كل مرة:

<div class="fixed z-10 bottom-0 py-6 px-4 flex flex-col gap-2">
  <SnackBar v-if="alerts.length" :isDanger="alerts[0].isDanger" :message="alerts[0].message" @close="removeAlert(0)" />
</div>
4. تنظيم الكود واستخدام المكونات القابلة لإعادة الاستخدام
تأكد من أن المكونات الفرعية مثل Step1 إلى Step6 منظمة جيدًا وتستخدم بشكل متسق. يمكنك أيضًا إنشاء مكونات مشتركة للحقول النموذجية إذا كانت تتكرر في خطوات متعددة.

5. تحسين واجهة المستخدم باستخدام Tailwind CSS
تأكد من الاستفادة الكاملة من خصائص Tailwind CSS لجعل تصميمك متجاوبًا وسهل التعديل. يمكنك أيضًا استخدام @apply لإنشاء فئات مخصصة تقلل من تكرار الكود.

6. التعامل مع الطلبات باستخدام request.js
تأكد من أن دوال createRequestList و updateFieldsInRequestList تتعامل مع الطلبات بشكل صحيح، وتقوم بمعالجة الأخطاء بشكل فعال. يمكنك استخدام مكتبة مثل Axios إذا لم تكن تستخدمها بالفعل لإدارة الطلبات بسهولة أكبر.

مثال كامل بعد التحسينات
إليك مثال محسن على steps.vue مع دمج بعض الاقتراحات المذكورة أعلاه:


<template>
  <Header />
  <div class="lg:grid lg:grid-cols-12 lg:container py-8 bg-bg">
    <div class="container col-span-4 lg:order-2 relative">
      <div class="sticky top-4">
        <div class="pb-3 font-medium text-primary text-[1rem]">
          <span class="m-2"><-</span>Model steps instructions of Job Request ({{ Math.round((100 / 6) * processed) }}% Completed)
        </div>
        <div class="py-3 grid grid-cols-6 lg:flex lg:flex-col lg:gap-4 w-full">
          <StepIcon
            v-for="(step, index) in 6"
            :key="index"
            :process="processed === index"
            :complete="processed > index"
            :label="'Step ' + (index + 1)"
            :desc="getStepDescription(index)"
          />
        </div>
        <div class="py-3 lg:hidden flex justify-end text-secondary text-[1rem]">
          <button>
            All Steps (6)<span class="inline-block mx-1">-></span>
          </button>
        </div>
      </div>
    </div>
    <div class="pb-20 bg-bg leading-[1.6rem] col-span-8">
      <Step1 v-if="processed === 0" :stepData="step1Data" />
      <Step2 v-if="processed === 1" :stepData="step2Data" />
      <Step3 v-if="processed === 2" :stepData="step3Data" />
      <Step4 v-if="processed === 3" :stepData="step4Data" />
      <Step5 v-if="processed === 4" :stepData="step5Data" />
      <Step6 v-if="processed === 5" :stepData="step6Data" />
    </div>
  </div>
  <div class="bg-[#F0F0F0] fixed lg:static bottom-0 w-screen h-fit">
    <div class="container py-4 flex w-full justify-between items-center">
      <div class="text-xs text-[#1D1B20]">auto saving</div>

      <div class="flex gap-2">
        <button class="text-secondary" @click="previousStep" v-if="processed !== 0"><</button>
        <Button v-if="processed < 5" level="primary" @clicked="nextStep">
          step {{ stepN }} ->
        </Button>
      </div>

      <div v-if="processed === 5" class="lg:flex justify-end grow lg:gap-2">
        <Button level="other" @clicked="accept"> Accept </Button>
        <Button level="other" @clicked="reject"> Reject </Button>
      </div>
    </div>
  </div>
  <div class="fixed z-10 bottom-0 py-6 px-4 flex flex-col gap-2">
    <SnackBar v-for="(alert, index) in alerts" :key="index" :isDanger="alert.isDanger" :message="alert.message" @close="removeAlert(index)" />
  </div>
</template>

<script setup>
import { computed, reactive, ref } from 'vue'
import Button from '../components/button.vue'
import Header from '../components/header.vue'
import StepIcon from './stepsSections/components/stepIcon.vue'
import Step1 from './stepsSections/step1.vue'
import Step2 from './stepsSections/step2.vue'
import Step3 from './stepsSections/step3.vue'
import Step4 from './stepsSections/step4.vue'
import Step5 from './stepsSections/step5.vue'
import Step6 from './stepsSections/step6.vue'
import SnackBar from '../components/snackBar.vue'
import { createRequestList, updateFieldsInRequestList } from '../data/request'
import validateInputStep1 from '../data/validate/validateInputStep1'
import objectConvertor from '../data/validate/convertor'

// الحالة
const processed = ref(0)
const stepN = ref(1)

const fieldsStep1 = ['name', 'first_name', 'last_name']
const requestListStep1 = createRequestList(fieldsStep1)

const alerts = reactive([])

// دوال التنبيه
const triggerAlert = function(message, isDanger = true) {
  alerts.push({ message, isDanger })
  setTimeout(() => {
    alerts.shift()
  }, 3000)
}

const removeAlert = function(index) {
  alerts.splice(index, 1)
}

// بيانات الخطوات
const step1Data = reactive({
  employer_name: {
    value: '',
    isValid: null,
    validationMessage: null,
  },
  first_name: {
    value: '',
    isValid: null,
    validationMessage: null,
  },
  middle_name: {
    value: '',
    isValid: null,
    validationMessage: null,
  },
})

const step2Data = reactive({})
const step3Data = reactive({})
const step4Data = reactive({})
const step5Data = reactive({})
const step6Data = reactive({})

// دوال التنقل
const previousStep = function () {
  if (processed.value > 0) {
    processed.value--
    stepN.value--
  }
}

const nextStep = async function () {
  let isValid = false
  switch (processed.value) {
    case 0:
      isValid = step1Save()
      break
    case 1:
      isValid = step2Save()
      break
    case 2:
      isValid = step3Save()
      break
    case 3:
      isValid = step4Save()
      break
    case 4:
      isValid = step5Save()
      break
    default:
      break
  }

  if (isValid) {
    processed.value++
    stepN.value++
  }
}

// دوال حفظ الخطوات مع التحقق
const step1Save = function () {
  let validateRes = validateInputStep1(step1Data)

  if (validateRes !== true) {
    triggerAlert("يرجى تصحيح الأخطاء في البيانات المدخلة.", true)
    return false
  } else {
    let converted = objectConvertor(step1Data)
    updateFieldsInRequestList(requestListStep1, converted)
    triggerAlert("تم حفظ الخطوة الأولى بنجاح.", false)
    return true
  }
}

const step2Save = function () {
  // تنفيذ عملية حفظ الخطوة 2
  // التحقق من صحة بيانات الخطوة 2
  // مشابه لخطوة 1
  // إذا نجحت:
  triggerAlert("تم حفظ الخطوة الثانية بنجاح.", false)
  return true
}

const step3Save = function () {
  // تنفيذ عملية حفظ الخطوة 3
  triggerAlert("تم حفظ الخطوة الثالثة بنجاح.", false)
  return true
}

const step4Save = function () {
  // تنفيذ عملية حفظ الخطوة 4
  triggerAlert("تم حفظ الخطوة الرابعة بنجاح.", false)
  return true
}

const step5Save = function () {
  // تنفيذ عملية حفظ الخطوة 5
  triggerAlert("تم حفظ الخطوة الخامسة بنجاح.", false)
  return true
}

const step6Save = function () {
  // تنفيذ عملية حفظ الخطوة 6 نهائيًا
  triggerAlert("تم إكمال جميع الخطوات بنجاح.", false)
  // يمكن تنفيذ عمليات إضافية مثل إرسال البيانات إلى الخادم هنا
}

// دوال قبول أو رفض الطلب
const accept = function () {
  // تنفيذ عملية القبول
  triggerAlert("تم قبول الطلب.", false)
}

const reject = function () {
  // تنفيذ عملية الرفض
  triggerAlert("تم رفض الطلب.", false)
}

// دالة للحصول على وصف الخطوة بناءً على رقمها
const getStepDescription = (index) => {
  const descriptions = [
    "Personal Information",
    "Job Details",
    "Verification",
    "Review",
    "Confirmation",
    "Completion"
  ]
  return descriptions[index] || "Step Description"
}
</script>

<style scoped>
/* أضف أي تنسيقات إضافية هنا إذا لزم الأمر */
</style>
شرح التحسينات
دمج التحقق من الصحة مع التنقل: الآن، قبل الانتقال إلى الخطوة التالية، يتم التحقق من صحة البيانات للمراحل الحالية. إذا لم تكن البيانات صحيحة، لا يتم الانتقال ويتم عرض تنبيه للمستخدم.
إزالة computed غير الضرورية: تمت إزالة الخاصية nextNum لأنها كانت تزيد processed بشكل غير مقصود مما قد يؤدي إلى أخطاء في التنقل.
تحسين عرض التنبيهات: تم تعديل طريقة عرض التنبيهات لتظهر واحدة في كل مرة وتزيلها بعد عرضها.
استخدام v-for لتكرار خطوات StepIcon: بدلاً من كتابة StepIcon لكل خطوة بشكل منفصل، تم استخدام حلقة v-for لجعل الكود أكثر نظافة وأسهل في التعديل.
إضافة دوال قبول ورفض: تم إضافة دوال accept وreject للتعامل مع الأزرار المقابلة في الخطوة السادسة.
تحسين إدارة البيانات المراحل: تم التأكد من أن البيانات لكل مرحلة تتم معالجتها بشكل صحيح وتخزينها إذا كانت صحيحة.
تحسين واجهة المستخدم وتجربة المستخدم: باستخدام التنبيهات لجعل المستخدم على علم بحالة العمليات الحالية، وتحسين التنقل بين المراحل بشكل أكثر سلاسة.
نقاط إضافية للنظر فيها
إدارة الحالة باستخدام Vuex أو Pinia: إذا كانت هناك حاجة لمشاركة البيانات بين مكونات متعددة، قد يكون من المفيد استخدام مكتبة لإدارة الحالة مثل Vuex أو Pinia.
التعامل مع طلبات الخادم بشكل غير متزامن: إذا كانت دوال الحفظ تتطلب تواصل مع الخادم، تأكد من التعامل مع العمليات غير المتزامنة (async/await) بشكل صحيح والتعامل مع الأخطاء المحتملة.
تحسين تجربة المستخدم: يمكن إضافة مؤشرات تحميل عند حفظ البيانات أو الانتقال بين الخطوات لجعل التطبيق أكثر تجاوبًا.
إذا كانت لديك أي أسئلة إضافية أو تحتاج إلى مساعدة في جزء معين من التطبيق، فلا تتردد في طرحها!



شكرًا لمشاركة كود step1.vue. سأقوم بمراجعة الكود وتقديم تحسينات مقترحة لضمان عمل النموذج بكفاءة أعلى، بالإضافة إلى تحسين التحقق من صحة البيانات وإدارة الحالة بشكل أفضل.

مراجعة كود step1.vue
المشكلات الحالية والتحسينات المقترحة:
التسمية الموحدة للحقول:
في الكود الحالي، هناك تناقض في تسمية الحقول. بعض الحقول تستخدم snake_case (مثل employer_name، first_name، middle_name)، بينما البعض الآخر يستخدم PascalCase (مثل LastName، Suffix، AliasName).
تحسين مقترح: استخدام نمط تسمية موحد (يفضل snake_case) لجميع الحقول لتجنب الالتباس وضمان الاتساق.
التحقق من صحة البيانات (Validation):
الكود الحالي يحتوي على أجزاء معلمة من التحقق من الصحة ولكنها غير مكتملة ومتسقة.
معالجة التحقق من الصحة ضمن handleInput هو نهج جيد، ولكن يجب جعلها أكثر تنظيماً وقابلية لإعادة الاستخدام.
التعامل مع حالة الـ checkbox:
عند تحديد الـ checkbox "I certify that I do not have a Middle Name...", يجب تعطيل حقل middle_name أو جعله غير مطلوب.
تحسين مقترح: إضافة منطق للتحقق من صحة حقل middle_name بناءً على حالة الـ checkbox.
إزالة المتغير غير المستخدم:
في الكود، يوجد متغير companies لم يتم استخدامه بشكل واضح في النموذج.
تحسين مقترح: إزالة المتغير إذا لم يكن هناك حاجة إليه، أو توضيح استخدامه.
رفع الحد لملء الحقول:
استخدام متغيرات محلية بدلاً من تعديل props مباشرة يمكن أن يكون أكثر أمانًا ووضوحًا.
تحسينات مقترحة على الكود:
1. توحيد تسمية الحقول:
للتأكد من الاتساق، سنستخدم snake_case لجميع الحقول.

2. تحسين handleInput مع التحقق من الصحة:
سنضيف منطق التحقق من الصحة داخل handleInput, ونجعل التحقق من صحة middle_name يعتمد على حالة الـ checkbox.

3. تعطيل حقل middle_name بناءً على الـ checkbox:
سنضيف منطق لتعطيل حقل middle_name عند تحديد الـ checkbox.

4. استخدام v-model للتعامل مع البيانات:
يمكن استخدام v-model مع المكونات المخصصة (مثل StyledInput) لضمان تدفق البيانات بشكل سلس وأكثر وضوحًا.

الكود المحسن لـ step1.vue:

<!-- Step1.vue -->

<template>
  <div class="pt-3 container">
    <h3 class="text-lg font-medium mb-3 text-black">1. Basic Information</h3>
    <div class="lg:grid grid-cols-2 lg:gap-2">
      <FieldContainer>
        <StyledInput
          labelText="Employer Name"
          :isMandatory="true"
          infoText="Employer Name"
          inputType="text"
          v-model="localStepData.employer_name.value"
          @input-change="handleInput('employer_name', localStepData.employer_name.value)"
          name="employer_name"
          id="EmployerName"
          :isValid="localStepData.employer_name.isValid"
          :validationMessage="localStepData.employer_name.validationMessage"
        />
        <SupportingText>Supporting text</SupportingText>
      </FieldContainer>
      <FieldContainer>
        <StyledInput
          labelText="First Name"
          :isMandatory="true"
          infoText="First Name"
          inputType="text"
          v-model="localStepData.first_name.value"
          @input-change="handleInput('first_name', localStepData.first_name.value)"
          name="first_name"
          id="firstName"
          :isValid="localStepData.first_name.isValid"
          :validationMessage="localStepData.first_name.validationMessage"
        />
      </FieldContainer>
    </div>

    <FieldContainer>
      <CheckBox 
        name="certify" 
        id="certify" 
        v-model="certify" 
        @input-change="handleCheckboxChange"
      >
        I certify that I do not have a Middle Name on my official identification document
      </CheckBox>
    </FieldContainer>

    <div class="lg:grid grid-cols-2 lg:gap-2">
      <FieldContainer>
        <StyledInput
          labelText="Middle Name"
          :isMandatory="!certify"
          inputType="text"
          v-model="localStepData.middle_name.value"
          @input-change="handleInput('middle_name', localStepData.middle_name.value)"
          name="middle_name"
          id="MiddleName"
          :isValid="localStepData.middle_name.isValid"
          :validationMessage="localStepData.middle_name.validationMessage"
          :disabled="certify"
        />
      </FieldContainer>
    </div>

    <div class="lg:grid grid-cols-2 lg:gap-2">
      <FieldContainer>
        <StyledInput
          labelText="Last Name"
          :isMandatory="true"
          infoText="Last Name"
          inputType="text"
          v-model="localStepData.last_name.value"
          @input-change="handleInput('last_name', localStepData.last_name.value)"
          name="last_name"
          id="lastName"
          :isValid="localStepData.last_name.isValid"
          :validationMessage="localStepData.last_name.validationMessage"
        />
      </FieldContainer>
      <FieldContainer>
        <StyledInput
          labelText="Suffix"
          :isMandatory="false"
          infoText="Suffix"
          inputType="text"
          v-model="localStepData.suffix.value"
          @input-change="handleInput('suffix', localStepData.suffix.value)"
          name="suffix"
          id="Suffix"
          :isValid="localStepData.suffix.isValid"
          :validationMessage="localStepData.suffix.validationMessage"
        />
        <SupportingText>Supporting text</SupportingText>
      </FieldContainer>
    </div>

    <FieldContainer>
      <StyledInput
        labelText="Alias Name"
        :isMandatory="false"
        infoText="Alias Name"
        inputType="text"
        v-model="localStepData.alias_name.value"
        @input-change="handleInput('alias_name', localStepData.alias_name.value)"
        name="alias_name"
        id="AliasName"
        :isValid="localStepData.alias_name.isValid"
        :validationMessage="localStepData.alias_name.validationMessage"
      />
    </FieldContainer>
  </div>
</template>

<script setup>
import { reactive, watch } from 'vue'
import CheckBox from '../../components/checkBox.vue'
import FieldContainer from '../../components/fieldContainer.vue'
import StyledInput from '../../components/styledInput.vue'
import SupportingText from '../../components/supportingText.vue'

// Props
const props = defineProps({
  stepData: {
    type: Object,
    required: true,
  },
})

// Emit events to parent to notify data changes
const emit = defineEmits(['update:stepData'])

// محليًا لتحرير البيانات
const localStepData = reactive({
  employer_name: { ...props.stepData.employer_name },
  first_name: { ...props.stepData.first_name },
  middle_name: { ...props.stepData.middle_name },
  last_name: { ...props.stepData.last_name },
  suffix: { ...props.stepData.suffix },
  alias_name: { ...props.stepData.alias_name },
})

let certify = false

// Watcher لمزامنة التغييرات مع props.stepData
watch(localStepData, (newVal) => {
  emit('update:stepData', newVal)
}, { deep: true })

// دوال التعامل مع الإدخالات
const handleInput = (field, value) => {
  if (field in localStepData) {
    localStepData[field].value = value
    validateField(field, value)
  }
}

// دالة التعامل مع الـ checkbox
const handleCheckboxChange = () => {
  // إذا كان الـ checkbox محددًا، تعطيل حقل middle_name وتعيين قيمته إلى فارغة
  if (certify) {
    localStepData.middle_name.value = ''
    localStepData.middle_name.isValid = true
    localStepData.middle_name.validationMessage = null
  } else {
    // إذا لم يكن محددًا، يمكن أن يكون الحقل مطلوبًا
    validateField('middle_name', localStepData.middle_name.value)
  }
}

// دالة التحقق من صحة الحقول
const validateField = (field, value) => {
  switch (field) {
    case 'employer_name':
      if (value.trim() === '') {
        localStepData.employer_name.isValid = false
        localStepData.employer_name.validationMessage = 'This field is required.'
      } else {
        localStepData.employer_name.isValid = true
        localStepData.employer_name.validationMessage = null
      }
      break
    case 'first_name':
      if (value.trim() === '') {
        localStepData.first_name.isValid = false
        localStepData.first_name.validationMessage = 'This field is required.'
      } else {
        localStepData.first_name.isValid = true
        localStepData.first_name.validationMessage = null
      }
      break
    case 'middle_name':
      if (!certify && value.trim() === '') {
        localStepData.middle_name.isValid = false
        localStepData.middle_name.validationMessage = 'This field is required.'
      } else {
        localStepData.middle_name.isValid = true
        localStepData.middle_name.validationMessage = null
      }
      break
    case 'last_name':
      if (value.trim() === '') {
        localStepData.last_name.isValid = false
        localStepData.last_name.validationMessage = 'This field is required.'
      } else {
        localStepData.last_name.isValid = true
        localStepData.last_name.validationMessage = null
      }
      break
    case 'suffix':
      // حقل suffix غير مطلوب في البداية; يمكنك تعديل الشرط حسب الحاجة
      localStepData.suffix.isValid = true
      localStepData.suffix.validationMessage = null
      break
    case 'alias_name':
      // حقل alias_name غير مطلوب في البداية; يمكنك تعديل الشرط حسب الحاجة
      localStepData.alias_name.isValid = true
      localStepData.alias_name.validationMessage = null
      break
    default:
      break
  }
}

// تحقق من جميع الحقول قبل الانتقال
const validateAllFields = () => {
  validateField('employer_name', localStepData.employer_name.value)
  validateField('first_name', localStepData.first_name.value)
  validateField('middle_name', localStepData.middle_name.value)
  validateField('last_name', localStepData.last_name.value)
  validateField('suffix', localStepData.suffix.value)
  validateField('alias_name', localStepData.alias_name.value)

  // التحقق مما إذا كانت جميع الحقول صالحة
  return Object.values(localStepData).every(field => field.isValid)
}

// Watcher لـ certify لضبط التحقق من صحة middle_name
watch(() => certify, (newVal) => {
  if (newVal) {
    // إذا كان الـ checkbox محددًا، اجعل middle_name صالحًا تلقائيًا
    localStepData.middle_name.isValid = true
    localStepData.middle_name.validationMessage = null
  } else {
    // إذا لم يكن محددًا، تحقق من صحة middle_name
    validateField('middle_name', localStepData.middle_name.value)
  }
})
</script>

<style scoped>
/* أضف أي تنسيقات إضافية هنا إذا لزم الأمر */
</style>
شرح التحسينات
1. توحيد تسمية الحقول:
تم تعديل أسماء الحقول من PascalCase إلى snake_case لضمان الاتساق مثل last_name بدلاً من LastName، وalias_name بدلاً من AliasName.
2. إدارة الحالة محليًا:
تم إنشاء localStepData ككائن تفاعلي (reactive) لاحتواء البيانات المحلية للنموذج. هذا يضمن أن تكون التغييرات محلية قبل إرسالها إلى المكون الأب.
تم استخدام watch لمزامنة التغييرات في localStepData مع props.stepData عبر emit.
3. استخدام v-model مع المكونات المخصصة:
تم استخدام v-model مع StyledInput و CheckBox لتبسيط تدفق البيانات وتقليل التعقيد في التعامل مع الأحداث.
4. إضافة دوال التحقق من الصحة:
تم إنشاء دالة validateField للتحقق من صحة كل حقل بناءً على اسمه وقيمته.
دالة validateAllFields يمكن استخدامها في المكون الأب (مثل steps.vue) قبل الانتقال إلى الخطوة التالية للتأكد من أن جميع الحقول صالحة.
5. التعامل مع حالة الـ checkbox:
عند تحديد الـ checkbox certify, يتم تعطيل حقل middle_name وجعله صالحًا تلقائيًا.
عند إلغاء تحديد الـ checkbox, يتم إعادة التحقق من صحة حقل middle_name.
6. استخدام emit لمزامنة البيانات مع المكون الأب:
يتم إرسال التغييرات في البيانات إلى المكون الأب باستخدام emit('update:stepData', newData). تأكد من أن المكون الأب (steps.vue) يستمع لهذا الحدث ويحدث stepData accordingly.
7. التحقق من الصحة قبل الانتقال:
يمكن إضافة دالة validateAllFields في المكون الأب قبل القيام بخطوة حفظ البيانات والتحقق منها.
تعديل steps.vue لضمان التحقق من صحة جميع الحقول قبل الانتقال:
تعديل steps.vue ليتعامل مع التغييرات القادمة من step1.vue:
في steps.vue, تحتاج إلى تحديث الكود لاستماع إلى حدث update:stepData من Step1 وضمان أن stepData يتم تحديثه بشكل صحيح.

تعديل Template:

<Step1 
  v-if="processed === 0" 
  :stepData="step1Data" 
  @update:stepData="updateStepData('step1Data', $event)" 
/>
إضافة دالة updateStepData في steps.vue:

const updateStepData = (step, newData) => {
  if (step === 'step1Data') {
    Object.assign(step1Data, newData)
  }
  // يمكن إضافة شروط أخرى لباقي الخطوات
}
استخدام validateAllFields عند حفظ الخطوة:
في steps.vue, ضمن دالة step1Save, استخدم validateAllFields للتحقق من صحة البيانات قبل زيادة processed:


const step1Save = function () {
  const isValid = validateAllFields(step1Data) // assuming validateAllFields is exported or defined properly
  if (!isValid) {
    triggerAlert("يرجى تصحيح الأخطاء في البيانات المدخلة.", true)
    return false
  } else {
    let converted = objectConvertor(step1Data)
    updateFieldsInRequestList(requestListStep1, converted)
    triggerAlert("تم حفظ الخطوة الأولى بنجاح.", false)
    return true
  }
}
لتحقيق هذا، قد تحتاج إلى تعديل validateAllFields ليكون متاحًا للوصول من steps.vue. يمكن تحقيق ذلك عبر نقل وظيفة التحقق من الصحة إلى ملف خارجي أو باستخدام مكتبة لإدارة التحقق من الصحة.

نصائح إضافية:
إدارة الحالة باستخدام Pinia أو Vuex:
إذا كان لديك بيانات مشتركة بين خطوات متعددة، يمكن أن يكون من المفيد استخدام Pinia أو Vuex لإدارة الحالة بشكل مركزي بدلاً من تمرير البيانات عبر props و events.
تقسيم المكونات:
تأكد من أن المكونات الصغيرة مثل FieldContainer وStyledInput تعالج جزءًا واحدًا وظيفيًا من النموذج، مما يسهل إعادة استخدامها وصيانتها.
إضافة مؤشرات لحالة الحقول:
يمكن تعزيز تجربة المستخدم بإضافة مؤشرات لحالة الحقول، مثل رموز النجاح أو الأخطاء بجانب الحقول.
تحسين تجربة المستخدم:
يمكنك إضافة ميزة الانتقال إلى الحقل التالي عند الضغط على "Enter" وتحسين التنقل بين الحقول باستخدام لوحة المفاتيح.
التعامل مع الحدث submit:
على الرغم من أن النموذج ليس مكونًا من عناصر <form>, إلا أنه يمكن التفكير في استخدام عناصر النموذج لمعالجة إجراءات الإرسال والتحقق من الصحة بشكل أفضل.
إذا كنت بحاجة إلى مزيد من المساعدة في أجزاء أخرى من التطبيق أو هناك ميزات محددة ترغب في تنفيذها، فلا تتردد في طرحها!



شكرًا لمشاركتك كود step2.vue. سأقوم بمراجعة الكود وتقديم تحسينات مقترحة لضمان عمل النموذج بكفاءة أكبر، بالإضافة إلى تحسين التحقق من صحة البيانات وإدارة الحالة بشكل أفضل. سأركز على جوانب متعددة مثل التسمية الموحدة، التحقق من الصحة، إدارة الحالة، وتجربة المستخدم.

مراجعة كود step2.vue
1. توحيد تسمية الحقول
من المهم الحفاظ على نمط تسمية متسق لجميع الحقول. في الكود الحالي، يتم استخدام snake_case (مثل name_of_school_or_college_university) وهو جيدًا، لذا تأكد من الحفاظ على هذا النمط في جميع الحقول.

2. تحسين إدارة الحالة
يمكن تحسين إدارة الحالة باستخدام reactive أو ref بشكل أكثر فعالية. بالإضافة إلى ذلك، إذا كان لديك بيانات مشتركة بين مكونات متعددة، قد يكون من المفيد استخدام مكتبة لإدارة الحالة مثل Pinia أو Vuex.

3. التحقق من صحة البيانات (Validation)
التحقق من الصحة يتم بشكل جيد، ولكن يمكن تحسينه عن طريق centralizing التحقق من الصحة أو استخدام مكتبة للتحقق مثل Vuelidate أو Yup لتعزيز الصيانة وإعادة الاستخدام.

4. تحسين التعامل مع الطلبات (Requests)
التعامل مع طلبات الحفظ يجب أن يكون أكثر تنظيماً، مع التعامل الأفضل مع الأخطاء وإظهار تنبيهات مناسبة للمستخدم.

5. تحسين تجربة المستخدم
يمكن إضافة ميزات مثل حذف سجل تعليمي، تعطيل أزرار الحفظ عند وجود أخطاء، وإظهار مؤشر تحميل أثناء عملية الحفظ لتعزيز تجربة المستخدم.

6. تقسيم المكونات
نظرًا لحجم وتعقيد النموذج، يمكن تقسيم المكون إلى مكونات أصغر لزيادة القابلية لإعادة الاستخدام وتحسين الصيانة.

الكود المحسن لـ step2.vue
إليك نسخة محسنة من step2.vue مع تطبيق التحسينات المقترحة:


<!-- Step2.vue -->

<template>
  <div class="pt-3 container">
    <h3 class="text-lg font-medium mb-3 text-black">Education Information</h3>
  </div>

  <div class="space-y-2">
    <!-- Iterate Over Education Information -->
    <FieldsToggleContainer
      v-for="(education, index) in educationList"
      :key="education.id"
      :title="education.name_of_school_or_college_university || `Education ${index + 1}`"
    >
      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Name of School/College/University Field -->
        <FieldContainer>
          <StyledInput
            labelText="Name of School/College/University"
            :isMandatory="true"
            infoText="Enter the name of your educational institution."
            inputType="text"
            name="name_of_school_or_college_university"
            :id="`name_of_school_or_college_university-${education.id}`"
            v-model="education.name_of_school_or_college_university"
            :isValid="validationErrors[index]?.name_of_school_or_college_university === false"
            :validationMessage="validationErrors[index]?.name_of_school_or_college_university ? 'Name of School/College/University is required.' : ''"
          />
        </FieldContainer>

        <!-- Field of Study/Major Field -->
        <FieldContainer>
          <StyledInput
            labelText="Field of Study/Major"
            :isMandatory="true"
            infoText="Enter your field of study or major."
            inputType="text"
            name="field_of_study_or_major"
            :id="`field_of_study_or_major-${education.id}`"
            v-model="education.field_of_study_or_major"
            :isValid="validationErrors[index]?.field_of_study_or_major === false"
            :validationMessage="validationErrors[index]?.field_of_study_or_major ? 'Field of Study/Major is required.' : ''"
          />
        </FieldContainer>
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Country Autocomplete -->
        <FieldContainer>
          <Autocomp
            labelText="Country"
            :isMandatory="true"
            infoText="Select your country."
            inputType="text"
            name="country"
            :id="`country-${education.id}`"
            :options="getOptionCountry(education)"
            v-model="education.country"
            @input-change="handleCountryChange(index, $event)"
            :isValid="validationErrors[index]?.country === false"
            :validationMessage="validationErrors[index]?.country ? 'Country is required.' : ''"
          />
        </FieldContainer>

        <!-- City Autocomplete -->
        <FieldContainer>
          <Autocomp
            labelText="City"
            :isMandatory="true"
            infoText="Select your city."
            inputType="text"
            name="city"
            :id="`city-${education.id}`"
            :options="getOptionCity(education)"
            v-model="education.city"
            @input-change="handleCityChange(index, $event)"
            :isValid="validationErrors[index]?.city === false"
            :validationMessage="validationErrors[index]?.city ? 'City is required.' : ''"
          />
        </FieldContainer>

        <!-- Governorate Autocomplete -->
        <FieldContainer>
          <Autocomp
            labelText="Governorate"
            :isMandatory="true"
            infoText="Select your governorate."
            inputType="text"
            name="governorate"
            :id="`governorate-${education.id}`"
            :options="getOptionGovernorate(education)"
            v-model="education.governorate"
            @input-change="handleGovernorateChange(index, $event)"
            :isValid="validationErrors[index]?.governorate === false"
            :validationMessage="validationErrors[index]?.governorate ? 'Governorate is required.' : ''"
          />
        </FieldContainer>

        <!-- Location Text Field -->
        <FieldContainer>
          <StyledInput
            labelText="Location Text"
            :isMandatory="false"
            infoText="Additional location details (optional)."
            inputType="text"
            name="location_text"
            :id="`location_text-${education.id}`"
            v-model="education.location_text"
            :isValid="true"
            validationMessage=""
          />
        </FieldContainer>
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- From Date -->
        <FieldContainer>
          <StyledInput 
            id="from_date" 
            name="from_date" 
            labelText="From Date" 
            :isMandatory="true" 
            infoText="Start date of your education." 
            inputType="date" 
            v-model="education.from_date" 
            :isValid="validationErrors[index]?.from_date === false"
            :validationMessage="validationErrors[index]?.from_date ? 'From Date is required.' : ''"
          />
        </FieldContainer>

        <!-- To Date -->
        <FieldContainer>
          <StyledInput 
            id="to_date" 
            name="to_date" 
            labelText="To Date" 
            :isMandatory="true" 
            infoText="End date of your education." 
            inputType="date" 
            v-model="education.to_date"
            :isValid="validationErrors[index]?.to_date === false" 
            :validationMessage="validationErrors[index]?.to_date ? 'To Date is required.' : ''"
          />
        </FieldContainer>
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Phone Field -->
        <FieldContainer>
          <StyledInput
            labelText="Phone"
            :isMandatory="true"
            infoText="+999-77885951"
            inputType="phone"
            name="phone"
            :id="`phone-${education.id}`"
            v-model="education.phone"
            :isValid="validationErrors[index]?.phone === false" 
            :validationMessage="validationErrors[index]?.phone ? 'Valid phone number is required.' : ''"
          />
        </FieldContainer>
        
        <!-- Ext Field -->
        <FieldContainer>
          <StyledInput
            labelText="Ext"
            :isMandatory="false"
            infoText="Extension (optional)"
            inputType="text"
            name="ext"
            :id="`ext-${education.id}`"
            v-model="education.ext"
            :isValid="true"
            validationMessage=""
          />
        </FieldContainer>      
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Diploma Select -->
        <FieldContainer>
          <Select
            labelText="Diploma"
            :isMandatory="true"
            infoText="Select your highest diploma."
            name="diploma"
            :id="`diploma-${education.id}`"
            :options="optionsDiploma"
            v-model="education.diploma"
            :isValid="validationErrors[index]?.diploma === false"
            :validationMessage="validationErrors[index]?.diploma ? 'Diploma is required.' : ''"
          />
        </FieldContainer>

        <!-- Another Name Select -->
        <FieldContainer>
          <Select
            labelText="Another Name"
            :isMandatory="false"
            infoText="Do you have another name used during your education?"
            name="another_name"
            :id="`another_name-${education.id}`"
            :options="optionsAnotherName"
            v-model="education.another_name"
            :isValid="true"
            :validationMessage=""
          />
        </FieldContainer>
      </div>

      <!-- Save and Delete Education Buttons -->
      <div class="flex justify-end py-2 gap-2">
        <Button level="danger" @clicked="deleteEducation(index)">Delete</Button>
        <Button :disabled="!isEducationValid(index)" level="primary" @clicked="saveEducation(index)">
          Save
        </Button>
      </div>
    </FieldsToggleContainer>

    <!-- Add Education Button -->
    <div class="flex justify-center py-3">
      <Button level="secondary" @clicked="addEducation">+ Add Education Information</Button>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, watch, computed, nextTick } from 'vue'
import { createDocumentResource } from 'frappe-ui'
import { useToast } from 'vue-toastification'
import { v4 as uuidv4 } from 'uuid'

// المكونات
import Button from '../../components/button.vue'
import FieldContainer from '../../components/fieldContainer.vue'
import FieldsToggleContainer from '../../components/fieldsToggleContainer.vue'
import StyledInput from '../../components/styledInput.vue'
import Autocomp from '../../components/autocomp.vue'
import Select from '../../components/select.vue'

// استيراد بيانات الموقع
import { location } from '../../data/useAddressLogic'

// تهيئة Toast
const toast = useToast()

// مورد المستند
const request = createDocumentResource({
  doctype: 'Verification Instructions Request',
  name: 'VIR-2024-26-09-000007', // يمكنك تغيير الاسم حسب الحاجة
})

// خيارات Diploma
const optionsDiploma = [
  { value: "High School", label: "High School" },
  { value: "Associate Degree", label: "Associate Degree" },
  { value: "Bachelor's Degree", label: "Bachelor's Degree" },
  { value: "Master's Degree", label: "Master's Degree" },
  { value: "Doctorate", label: "Doctorate" },
  { value: "Other", label: "Other" },
]

// خيارات Another Name
const optionsAnotherName = [
  { value: "Yes", label: "Yes" },
  { value: "No", label: "No" },
]

// قائمة التعليم
const educationList = ref([])

// قائمة أخطاء التحقق من الصحة
const validationErrors = reactive([])

// الحصول على خيارات الدولة
const getOptionCountry = (education) => {
  const options = location.data
    .filter(loc => loc.location_type === 'Country')
    .map(loc => ({
      label: loc.location_name,
      value: loc.location_name
    }))

  // إضافة القيمة الحالية إذا لم تكن موجودة في الخيارات
  if (education.country && !options.some(opt => opt.value === education.country)) {
    options.push({
      label: education.country,
      value: education.country
    })
  }

  return options
}

// الحصول على خيارات المدينة
const getOptionCity = (education) => {
  if (!education.country) return []

  const options = location.data
    .filter(loc => loc.location_type === 'City' && loc.parent_location === education.country)
    .map(loc => ({
      label: loc.location_name,
      value: loc.location_name
    }))

  // إضافة القيمة الحالية إذا لم تكن موجودة في الخيارات
  if (education.city && !options.some(opt => opt.value === education.city)) {
    options.push({
      label: education.city,
      value: education.city
    })
  }

  return options
}

// الحصول على خيارات المحافظة
const getOptionGovernorate = (education) => {
  if (!education.city) return []

  const options = location.data
    .filter(loc => loc.location_type === 'Governorate' && loc.parent_location === education.city)
    .map(loc => ({
      label: loc.location_name,
      value: loc.location_name
    }))

  // إضافة القيمة الحالية إذا لم تكن موجودة في الخيارات
  if (education.governorate && !options.some(opt => opt.value === education.governorate)) {
    options.push({
      label: education.governorate,
      value: education.governorate
    })
  }

  return options
}

// التعامل مع تغيير الدولة
const handleCountryChange = (index, value) => {
  const education = educationList.value[index]
  education.country = value.value
  education.city = '' // إعادة تعيين المدينة عند تغيير الدولة
  education.governorate = '' // إعادة تعيين المحافظة عند تغيير الدولة
}

// التعامل مع تغيير المدينة
const handleCityChange = (index, value) => {
  const education = educationList.value[index]
  education.city = value.value
  education.governorate = '' // إعادة تعيين المحافظة عند تغيير المدينة
}

// التعامل مع تغيير المحافظة
const handleGovernorateChange = (index, value) => {
  const education = educationList.value[index]
  education.governorate = value.value
}

// إضافة سجل تعليمي جديد
const addEducation = () => {
  const newEducation = {
    id: uuidv4(),
    name_of_school_or_college_university: '',
    field_of_study_or_major: '',
    country: '',
    city: '',
    governorate: '',
    location_text: '',
    from_date: '',
    to_date: '',
    phone: '',
    ext: '',
    diploma: '',
    another_name: '',
  }
  educationList.value.push(newEducation)
  validationErrors.push({})
}

// حذف سجل تعليمي
const deleteEducation = (index) => {
  educationList.value.splice(index, 1)
  validationErrors.splice(index, 1)
  toast.info('تم حذف السجل التعليمي بنجاح.')
}

// دوال التحقق من الصحة
const validateNameOfSchool = (name) => {
  return typeof name === 'string' && name.trim() !== ''
}

const validateFieldOfStudy = (field) => {
  return typeof field === 'string' && field.trim() !== ''
}

const validateCountry = (country) => {
  return typeof country === 'string' && country.trim() !== ''
}

const validateCity = (city) => {
  return typeof city === 'string' && city.trim() !== ''
}

const validateGovernorate = (governorate) => {
  return typeof governorate === 'string' && governorate.trim() !== ''
}

const validateFromDate = (from_date) => {
  return typeof from_date === 'string' && from_date.trim() !== ''
}

const validateToDate = (to_date) => {
  return typeof to_date === 'string' && to_date.trim() !== ''
}

const validatePhone = (phone) => {
  const phoneRegex = /^\+?[0-9\-]{7,15}$/
  return phoneRegex.test(phone)
}

const validateDiploma = (diploma) => {
  return typeof diploma === 'string' && diploma.trim() !== ''
}

const validateAnotherName = (another_name) => {
  // بما أن هذا الحقل اختياري، يمكن اعتباره صالحًا دائمًا أو إضافة تحقق إضافي إذا لزم الأمر
  return true
}

// دالة التحقق من صحة السجل التعليمي بالكامل
const validateEducation = (index) => {
  const education = educationList.value[index]
  const errors = {}

  if (!validateNameOfSchool(education.name_of_school_or_college_university)) {
    errors.name_of_school_or_college_university = false
  }

  if (!validateFieldOfStudy(education.field_of_study_or_major)) {
    errors.field_of_study_or_major = false
  }

  if (!validateCountry(education.country)) {
    errors.country = false
  }

  if (!validateCity(education.city)) {
    errors.city = false
  }

  if (!validateGovernorate(education.governorate)) {
    errors.governorate = false
  }

  if (!validateFromDate(education.from_date)) {
    errors.from_date = false
  }

  if (!validateToDate(education.to_date)) {
    errors.to_date = false
  }

  if (!validatePhone(education.phone)) {
    errors.phone = false
  }

  if (!validateDiploma(education.diploma)) {
    errors.diploma = false
  }

  // تحديث قائمة الأخطاء
  validationErrors[index] = errors

  // إرجاع حالة الصلاحية
  return Object.keys(errors).length === 0
}

// دالة لحفظ السجل التعليمي الفردي
const saveEducation = async (index) => {
  const isValid = validateEducation(index)
  
  if (!isValid) {
    toast.error('يرجى تصحيح الأخطاء في الحقول المطلوبة.')
    return
  }

  try {
    // تحديث المستند
    request.doc.education_information = educationList.value
    await request.setValue.submit(request.doc)
    toast.success(`تم حفظ التعليم ${index + 1} بنجاح!`)
  } catch (error) {
    console.error(`فشل في حفظ التعليم ${index + 1}:`, error)
    toast.error(`فشل في حفظ التعليم ${index + 1}.`)
  }
}

// دالة لحفظ الطلب بالكامل (إذا كان ذلك مطلوباً)
const saveRequest = async () => {
  try {
    request.doc.education_information = educationList.value
    await request.setValue.submit(request.doc)
    toast.success('تم حفظ الطلب بنجاح!')
  } catch (error) {
    console.error('فشل في حفظ الطلب:', error)
    toast.error('فشل في حفظ الطلب.')
  }
}

// تهيئة قائمة التعليم عند تحميل المستند
watch(() => request.doc, (newDoc) => {
  if (newDoc && newDoc.education_information) {
    educationList.value = newDoc.education_information
    // تهيئة قائمة الأخطاء بناءً على عدد السجلات
    validationErrors.length = 0
    newDoc.education_information.forEach(() => validationErrors.push({}))
  } else {
    educationList.value = []
    validationErrors.length = 0
  }
}, { immediate: true })
</script>

<style scoped>
/* أضف أي تنسيقات إضافية هنا إذا لزم الأمر */
</style>
شرح التحسينات
1. توحيد تسمية الحقول
تم التأكد من استخدام snake_case لجميع أسماء الحقول لضمان الاتساق وسهولة الفهم.

2. إدارة الحالة بشكل أفضل
تم استخدام reactive وref بشكل فعال لإدارة قائمة التعليم (educationList) وأخطاء التحقق (validationErrors). هذا يسهل تتبع وتحديث البيانات وتحسين التفاعل بين الحقول.

3. تحسين التحقق من الصحة
تم إنشاء دالة validateEducation للتحقق من صحة جميع الحقول في سجل تعليمي معين. هذه الدالة تحدد الأخطاء لكل حقل وتحديث قائمة الأخطاء validationErrors بناءً على ذلك. كما يتم تعطيل زر الحفظ إذا كانت هناك أخطاء في الحقول المطلوبة.

4. تحسين تجربة المستخدم
حذف سجل تعليمي: تمت إضافة زر "Delete" لحذف سجل تعليمي معين، مما يمنح المستخدم القدرة على إدارة سجلاته بشكل أفضل.
تعطيل زر الحفظ عند وجود أخطاء: زر الحفظ يتم تعذيبه إذا كانت هناك أخطاء في الحقول المطلوبة، مما يمنع المستخدم من حفظ بيانات غير صحيحة.
إظهار تنبيهات مناسبة: استخدام vue-toastification لإظهار رسائل النجاح أو الخطأ للمستخدم عند حفظ السجلات التعليمية.
5. تحسين التعامل مع الطلبات
تم تحسين دالة saveEducation لتتعامل مع عملية الحفظ بشكل أكثر تنظيماً، مع التعامل مع الأخطاء وإظهار التنبيهات المناسبة. كما تم إضافة دالة saveRequest لحفظ الطلب بالكامل إذا كان ذلك مطلوباً.

6. استخدام v-model بشكل فعال
تم استخدام v-model لربط الحقول مباشرة مع بيانات السجل التعليمي، مما يسهل تدفق البيانات ويقلل من التعقيد في التعامل مع الأحداث.

7. تحسين التعامل مع خيارات Autocomplete
تم تحسين دوال getOptionCountry, getOptionCity, وgetOptionGovernorate لضمان إضافة الخيارات الحالية إذا لم تكن موجودة في قائمة الخيارات، مما يضمن أن المستخدم يستطيع رؤية واختيار القيم المدخلة يدويًا.

8. تهيئة البيانات بشكل أفضل
تم استخدام watch لمراقبة وتحديث قائمة التعليم (educationList) وأخطاء التحقق (validationErrors) عند تحميل أو تحديث المستند (request.doc). هذا يضمن أن تكون البيانات مهيأة بشكل صحيح عند تحميل النموذج أو تعديله.

9. تقسيم المكونات لتحقيق القابلية لإعادة الاستخدام
على الرغم من أن المكون step2.vue يحتوي على العديد من الحقول، إلا أنه يمكن تقسيمه إلى مكونات أصغر مثل EducationForm.vue أو EducationItem.vue لتحسين القابلية لإعادة الاستخدام والصيانة.

نقاط إضافية للنظر فيها
استخدام مكتبات التحقق من الصحة:
Vuelidate أو Yup يمكن أن تسهل عملية التحقق من الصحة وتجعلها أكثر قوة وتنظيمًا.
إدارة الحالة باستخدام Pinia أو Vuex:
إذا كانت لديك بيانات مشتركة بين خطوات متعددة، يمكن أن يكون استخدام Pinia أو Vuex لإدارة الحالة بشكل مركزي مفيدًا.
تحسين الأداء:
تأكد من أن النموذج لا يحتوي على عمليات إعادة رسم (re-render) غير ضرورية، خاصة عند التعامل مع قوائم طويلة من السجلات التعليمية.
إضافة مؤشرات تحميل:
إضافة مؤشرات تحميل عند حفظ البيانات لتحسين تجربة المستخدم وجعل التطبيق أكثر تجاوبًا.
حذف سجل تعليمي بواسطة Confirm Dialog:
عند محاولة حذف سجل تعليمي، يمكن عرض نافذة تأكيد للمستخدم لضمان عدم حذف السجل عن طريق الخطأ.
زيادة الأمان:
تأكد من التحقق من صحة البيانات المدخلة على جانب الخادم أيضًا لضمان الأمان ومنع إدخال بيانات غير صحيحة.
تحسين رسائل التحقق من الصحة:
يمكن تخصيص رسائل التحقق لتكون أكثر وضوحًا ومساعدة للمستخدم.
خاتمة
من خلال تطبيق التحسينات المقترحة أعلاه، يمكنك تعزيز كفاءة وصيانة نموذج التعليم في تطبيقك، بالإضافة إلى تحسين تجربة المستخدم والتأكد من صحة البيانات المدخلة. إذا كانت هناك أي أسئلة إضافية أو تحتاج إلى مساعدة في جزء معين من التطبيق، فلا تتردد في طرحها!



شكرًا لمشاركة كود step3.vue. سأقوم بمراجعة الكود وتقديم تحسينات مقترحة لضمان عمل النموذج بكفاءة أعلى، بالإضافة إلى تحسين التحقق من صحة البيانات، إدارة الحالة، وتجربة المستخدم بشكل عام. سأركز على عدة جوانب مثل التسمية الموحدة، التحقق من الصحة، إدارة الحالة، وتجربة المستخدم.

مراجعة كود step3.vue
1. توحيد تسمية الحقول
من المهم الحفاظ على نمط تسمية متسق لجميع الحقول. في الكود الحالي، يتم استخدام snake_case (مثل name_of_your_employer)، وهو جيدًا. تأكد من استمرار استخدام نفس النمط لتجنب الالتباس والالتزام بالاتساق.

2. تحسين إدارة الحالة
يمكن تحسين إدارة الحالة باستخدام reactive وref بشكل أكثر فعالية. إذا كنت تمتلك بيانات مشتركة بين مكونات متعددة، فقد يكون من المفيد استخدام مكتبة لإدارة الحالة مثل Pinia أو Vuex.

3. التحقق من صحة البيانات (Validation)
التحقق من الصحة يتم بشكل جيد، لكن يمكن تحسينه بجعل التحقق من الصحة مرنًا وأكثر تنظيمًا. يمكن أيضًا استخدام مكتبة للتحقق من الصحة مثل Vuelidate أو Yup لتعزيز الصيانة وإعادة الاستخدام.

4. تحسين التعامل مع الطلبات (Requests)
التعامل مع طلبات الحفظ يجب أن يكون أكثر تنظيماً، مع التعامل الأفضل مع الأخطاء وإظهار تنبيهات مناسبة للمستخدم. استخدم مكتبات إدارة الطلبات مثل Axios إذا لم تكن تستخدمها بالفعل.

5. تحسين تجربة المستخدم
يمكن إضافة ميزات مثل حذف سجل تعليمي مع تأكيد، تعطيل أزرار الحفظ عند وجود أخطاء، وإظهار مؤشر تحميل أثناء عملية الحفظ لتعزيز تجربة المستخدم.

6. تقسيم المكونات
نظرًا لحجم وتعقيد النموذج، يمكن تقسيم المكون إلى مكونات أصغر مثل EmploymentForm.vue أو EmploymentItem.vue لتحسين القابلية لإعادة الاستخدام والصيانة.

الكود المحسن لـ step3.vue
إليك نسخة محسنة من step3.vue مع تطبيق التحسينات المقترحة:


<!-- Step3.vue -->

<template>
  <div class="pt-3 container">
    <h3 class="text-lg font-medium mb-3 text-black">Employment History</h3>
  </div>
  
  <div class="space-y-2">
    <!-- Iterate Over Employment History -->
    <FieldsToggleContainer
      v-for="(employment, index) in employmentList"
      :key="employment.id"
      :title="employment.company || `Employment ${index + 1}`"
    >
      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Company Field -->
        <FieldContainer>
          <StyledInput
            labelText="Company"
            :isMandatory="true"
            infoText="Company"
            inputType="text"
            name="company"
            :id="`company-${employment.id}`"
            v-model="employment.company"
            :isValid="validationErrors[index]?.company !== false"
            :validationMessage="validationErrors[index]?.company ? 'Company name is required.' : ''"
          />
        </FieldContainer> 

        <!-- Name of Employer Field -->
        <FieldContainer>
          <StyledInput
            labelText="Name of Your Employer"
            :isMandatory="true"
            infoText="Name of Your Employer"
            inputType="text"
            name="name_of_your_employer"
            :id="`name_of_your_employer-${employment.id}`"
            v-model="employment.name_of_your_employer"
            :isValid="validationErrors[index]?.name_of_your_employer !== false"
            :validationMessage="validationErrors[index]?.name_of_your_employer ? 'Name of Your Employer is required.' : ''"
          />
        </FieldContainer> 
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Country Autocomplete -->
        <FieldContainer>
          <Autocomp
            labelText="Country"
            :isMandatory="true"
            infoText="Country"
            inputType="text"
            name="country"
            :id="`country-${employment.id}`"
            :options="getOptionCountry(employment)"
            v-model="employment.country"
            @input-change="(value) => handleCountryChange(index, value)"
            :isValid="validationErrors[index]?.country !== false"
            validationMessage="Country is required."
          />
        </FieldContainer>  

        <!-- City Autocomplete -->
        <FieldContainer>
          <Autocomp
            labelText="City"
            :isMandatory="true"
            infoText="City"
            inputType="text"
            name="city"
            :id="`city-${employment.id}`"
            :options="getOptionCity(employment)"
            v-model="employment.city"
            @input-change="(value) => handleCityChange(index, value)"
            :isValid="validationErrors[index]?.city !== false"
            validationMessage="City is required."
          />
        </FieldContainer>

        <!-- Governorate Autocomplete -->
        <FieldContainer>
          <Autocomp
            labelText="Governorate"
            :isMandatory="true"
            infoText="Governorate"
            inputType="text"
            name="governorate"
            :id="`governorate-${employment.id}`"
            :options="getOptionGovernorate(employment)"
            v-model="employment.governorate"
            @input-change="(value) => handleGovernorateChange(index, value)"
            :isValid="validationErrors[index]?.governorate !== false"
            validationMessage="Governorate is required."
          />
        </FieldContainer>

        <!-- Location Text of Employer Field -->
        <FieldContainer>
          <StyledInput
            labelText="Location Text"
            :isMandatory="false"
            infoText="Location Text"
            inputType="text"
            name="location_text"
            :id="`location_text-${employment.id}`"
            v-model="employment.location_text"
            :isValid="true"
            validationMessage=""
          />
        </FieldContainer> 
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Permission Checkbox -->
        <FieldContainer>
          <CheckBox 
            :name="`permission-cont-${employment.id}`" 
            :id="`permission-cont-${employment.id}`" 
            v-model="employment.contact_the_employer"
          >
            Do we have permission to contact this current employer?
          </CheckBox>
        </FieldContainer>   
              
        <!-- Issuing Salary Checkbox -->
        <FieldContainer>
          <CheckBox 
            :name="`permission-issuing-${employment.id}`" 
            :id="`permission-issuing-${employment.id}`" 
            v-model="employment.issuing_salary"
          >
            Does this employer issue your paycheck?
          </CheckBox>
        </FieldContainer>
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Continuous Checkbox -->
        <FieldContainer>
          <CheckBox 
            :name="`permission-continuous-${employment.id}`" 
            :id="`permission-continuous-${employment.id}`" 
            v-model="employment.continuous"
          >
            Continuous
          </CheckBox>
        </FieldContainer>   
              
        <!-- Activity Has Stopped Checkbox -->
        <FieldContainer>
          <CheckBox 
            :name="`permission-activity-${employment.id}`" 
            :id="`permission-activity-${employment.id}`" 
            v-model="employment.activity_has_stopped"
          >
            Activity Has Stopped
          </CheckBox>
        </FieldContainer>
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- From Date -->
        <FieldContainer>
          <StyledInput 
            id="from_date" 
            name="from_date" 
            labelText="From" 
            :isMandatory="true" 
            infoText="From Date" 
            inputType="date" 
            v-model="employment.from_date" 
            :isValid="validationErrors[index]?.from_date !== false"
            :validationMessage="validationErrors[index]?.from_date ? 'From Date is required.' : ''"
          />
        </FieldContainer>

        <!-- To Date -->
        <FieldContainer>
          <StyledInput 
            id="end_date" 
            name="end_date" 
            labelText="End" 
            :isMandatory="true" 
            infoText="End Date" 
            inputType="date" 
            v-model="employment.end_date"
            :isValid="validationErrors[index]?.end_date !== false" 
            :validationMessage="validationErrors[index]?.end_date ? 'End Date is required.' : ''"
          />
        </FieldContainer>
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Phone Field -->
        <FieldContainer>
          <StyledInput
            labelText="Phone"
            :isMandatory="true"
            infoText="+999-77885951"
            inputType="text"
            name="phone"
            :id="`phone-${employment.id}`"
            v-model="employment.phone"
            :isValid="validationErrors[index]?.phone !== false" 
            validationMessage="Valid phone number is required."
          />
        </FieldContainer>
          
        <!-- Ext Field -->
        <FieldContainer>
          <StyledInput
            labelText="Ext"
            :isMandatory="false"
            infoText="Ext"
            inputType="text"
            name="ext"
            :id="`ext-${employment.id}`"
            v-model="employment.ext"
            :isValid="true"
            validationMessage=""
          />
        </FieldContainer>      
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Official Job Title Held Currently -->
        <FieldContainer>
          <StyledInput
            labelText="Official Job Title Held Currently"
            :isMandatory="true"
            infoText="Official Job Title Held Currently"
            inputType="text"
            name="official_job_title_held_currently"
            :id="`official_job_title_held_currently-${employment.id}`"
            v-model="employment.official_job_title_held_currently"
            :isValid="validationErrors[index]?.official_job_title_held_currently !== false" 
            validationMessage="Official Job Title Held Currently is required."
          />
        </FieldContainer> 

        <!-- Type of Employment Select -->
        <FieldContainer>
          <Select
            labelText="Type of Employment"
            :isMandatory="true"
            infoText="Type of Employment"
            name="type_of_employment"
            :id="`type_of_employment-${employment.id}`"
            :options="optionsTypeEmployment"
            v-model="employment.type_of_employment"
            @input-change="(value) => handleTypeOfEmployment(index, value)"
            :isValid="validationErrors[index]?.type_of_employment !== false"
            validationMessage="Type of Employment is required."
          />
        </FieldContainer>  
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- The Company has Different Names Checkbox -->
        <FieldContainer>
          <CheckBox 
            :name="`different-names-${employment.id}`" 
            :id="`different-names-${employment.id}`" 
            v-model="employment.the_company_has_different_names"
          >
            The Company has Different Names
          </CheckBox>
        </FieldContainer> 

        <!-- Nickname Checkbox -->
        <FieldContainer>
          <CheckBox 
            :name="`nickname-checkbox-${employment.id}`" 
            :id="`nickname-checkbox-${employment.id}`" 
            v-model="employment.you_have_a_nickname"
          >
            You Have a Nickname?
          </CheckBox>
        </FieldContainer> 

        <!-- Different Company Names Field -->
        <FieldContainer v-if="employment.the_company_has_different_names">
          <StyledInput
            labelText="Different Company Names"
            :isMandatory="false"
            infoText="Different Company Names"
            inputType="text"
            name="different_company_names"
            :id="`different_company_names-${employment.id}`"
            v-model="employment.different_company_names"
            :isValid="true"
            validationMessage=""
          />
        </FieldContainer>  

        <!-- Nickname Field -->
        <FieldContainer v-if="employment.you_have_a_nickname">
          <StyledInput
            labelText="Nickname"
            :isMandatory="false" 
            infoText="Nickname"
            inputType="text"
            name="nickname"
            :id="`nickname-${employment.id}`"
            v-model="employment.nickname"
            :isValid="true"
            validationMessage=""
          />
        </FieldContainer>         
      </div>

      <!-- File Upload -->
      <FileUpload
        :name="`file-${employment.id}`"
        :id="`file-${employment.id}`"
        v-model="employment.file"
      />

      <!-- Save and Delete Employment Buttons -->
      <div class="flex justify-end py-2 gap-2">
        <Button level="danger" @clicked="deleteEmployment(index)">Delete</Button>
        <Button :disabled="!isEmploymentValid(index)" level="primary" @clicked="saveEmployment(index)">
          Save
        </Button>
      </div>
    </FieldsToggleContainer>

    <!-- Add Employment Button -->
    <div class="flex justify-center py-3">
      <Button level="secondary" @clicked="addEmployment">+ Add Employment History</Button>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, watch, computed, nextTick } from 'vue'
import { createDocumentResource } from 'frappe-ui'
import { useToast } from 'vue-toastification'
import { v4 as uuidv4 } from 'uuid'

// Components
import Button from '../../components/button.vue'
import FieldContainer from '../../components/fieldContainer.vue'
import FieldsToggleContainer from '../../components/fieldsToggleContainer.vue'
import StyledInput from '../../components/styledInput.vue'
import Autocomp from '../../components/autocomp.vue'
import CheckBox from '../../components/checkBox.vue'
import Select from '../../components/select.vue'
import FileUpload from '../../components/fileUpload.vue'

// Import location data
import { location } from '../../data/useAddressLogic'

// Initialize Toast
const toast = useToast()

// Document Resource
const request = createDocumentResource({
  doctype: 'Verification Instructions Request',
  name: 'VIR-2024-26-09-000007',
  // auto: true,
})

// Employment Type Options
const optionsTypeEmployment = [
  { value: "Full-Time", label: "Full-Time" },
  { value: "Part-Time", label: "Part-Time" },
  { value: "Contract", label: "Contract" },
  { value: "Internship", label: "Internship" },
]

// List of Employment Histories
const employmentList = ref([])

// Validation Errors
const validationErrors = reactive([])

// Watcher to initialize employment_history
watch(() => request.doc, (newDoc) => {
  if (newDoc) {
    if (!newDoc.employment_history) {
      // Initialize as empty array if not present
      newDoc.employment_history = []
    } else {
      // Ensure all fields are initialized
      newDoc.employment_history.forEach(employment => {
        employment.company = employment.company || ''
        employment.name_of_your_employer = employment.name_of_your_employer || ''
        employment.contact_the_employer = employment.contact_the_employer || false
        employment.issuing_salary = employment.issuing_salary || false
        employment.country = employment.country || ''
        employment.city = employment.city || ''
        employment.governorate = employment.governorate || ''
        employment.location_text = employment.location_text || ''
        employment.continuous = employment.continuous || false
        employment.activity_has_stopped = employment.activity_has_stopped || false
        employment.from_date = employment.from_date || ''
        employment.end_date = employment.end_date || ''
        employment.phone = employment.phone || ''
        employment.ext = employment.ext || ''
        employment.official_job_title_held_currently = employment.official_job_title_held_currently || ''
        employment.type_of_employment = employment.type_of_employment || ''
        employment.the_company_has_different_names = employment.the_company_has_different_names || false
        employment.different_company_names = employment.different_company_names || ''
        employment.you_have_a_nickname = employment.you_have_a_nickname || false
        employment.nickname = employment.nickname || ''
        employment.file = employment.file || ''
      })
    }
    // Populate employmentList and initialize validationErrors
    employmentList.value = [...newDoc.employment_history]
    validationErrors.length = 0
    newDoc.employment_history.forEach(() => validationErrors.push({}))
  }
}, { immediate: true })

// Fallback Initialization
if (!request.doc || !request.doc.employment_history) {
  if (request.doc) {
    request.doc.employment_history = []
  } else {
    // Handle the case where request.doc is initially null
    request.doc = {
      employment_history: []
    }
  }
}

// Add a new employment entry
const addEmployment = () => {
  const newEmployment = {
    id: uuidv4(), // Unique identifier
    company: '',
    name_of_your_employer: '',
    contact_the_employer: false,
    issuing_salary: false,
    country: '',
    city: '',
    governorate: '',
    location_text: '',
    continuous: false,
    activity_has_stopped: false,
    from_date: '',
    end_date: '',
    phone: '',
    ext: '',
    official_job_title_held_currently: '',
    type_of_employment: '',
    the_company_has_different_names: false,
    different_company_names: '',
    you_have_a_nickname: false,
    nickname: '',
    file: '',
  }
  request.doc.employment_history.push(newEmployment)
  employmentList.value.push(newEmployment)
  validationErrors.push({})
  nextTick(() => {
    // يمكنك إضافة منطق لفتح القسم الجديد إذا كان المطلوب
  })
}

// Delete an employment entry
const deleteEmployment = (index) => {
  request.doc.employment_history.splice(index, 1)
  employmentList.value.splice(index, 1)
  validationErrors.splice(index, 1)
  toast.info('تم حذف السجل التعليمي بنجاح.')
}

// Handle Country Change
const handleCountryChange = (index, value) => {
  const employment = employmentList.value[index]
  employment.country = value.value
  employment.city = '' // Reset city when country changes
  employment.governorate = '' // Reset governorate when country changes
  validateEducation(index)
}

// Handle City Change
const handleCityChange = (index, value) => {
  const employment = employmentList.value[index]
  employment.city = value.value
  employment.governorate = '' // Reset governorate when city changes
  validateEducation(index)
}

// Handle Governorate Change
const handleGovernorateChange = (index, value) => {
  const employment = employmentList.value[index]
  employment.governorate = value.value
  validateEducation(index)
}

// Handle Type of Employment Change
const handleTypeOfEmployment = (index, value) => {
  const employment = employmentList.value[index]
  employment.type_of_employment = value.value
  validateEducation(index)
}

// Validation Functions
const validateCompany = (company) => {
  return typeof company === 'string' && company.trim() !== ''
}

const validateNameOfYourEmployer = (name_of_your_employer) => {
  return typeof name_of_your_employer === 'string' && name_of_your_employer.trim() !== ''
}

const validateCountry = (country) => {
  return typeof country === 'string' && country.trim() !== ''
}

const validateCity = (city) => {
  return typeof city === 'string' && city.trim() !== ''
}

const validateGovernorate = (governorate) => {
  return typeof governorate === 'string' && governorate.trim() !== ''
}

const validateFromDate = (from_date) => {
  return typeof from_date === 'string' && from_date.trim() !== ''
}

const validateEndDate = (end_date) => {
  return typeof end_date === 'string' && end_date.trim() !== ''
}

const validatePhone = (phone) => {
  const phoneRegex = /^\+?[0-9\-]{7,15}$/
  return phoneRegex.test(phone)
}

const validateOfficialJobTitleHeldCurrently = (title) => {
  return typeof title === 'string' && title.trim() !== ''
}

const validateTypeOfEmployment = (type) => {
  return typeof type === 'string' && type.trim() !== ''
}

// Function to validate a single employment entry
const validateEducation = (index) => {
  const employment = employmentList.value[index]
  const errors = {}

  if (!validateCompany(employment.company)) {
    errors.company = false
  }

  if (!validateNameOfYourEmployer(employment.name_of_your_employer)) {
    errors.name_of_your_employer = false
  }

  if (!validateCountry(employment.country)) {
    errors.country = false
  }

  if (!validateCity(employment.city)) {
    errors.city = false
  }

  if (!validateGovernorate(employment.governorate)) {
    errors.governorate = false
  }

  if (!validateFromDate(employment.from_date)) {
    errors.from_date = false
  }

  if (!validateEndDate(employment.end_date)) {
    errors.end_date = false
  }

  if (!validatePhone(employment.phone)) {
    errors.phone = false
  }

  if (!validateOfficialJobTitleHeldCurrently(employment.official_job_title_held_currently)) {
    errors.official_job_title_held_currently = false
  }

  if (!validateTypeOfEmployment(employment.type_of_employment)) {
    errors.type_of_employment = false
  }

  // Update validationErrors
  validationErrors[index] = errors

  // Return whether the employment is valid
  return Object.keys(errors).length === 0
}

// Function to check if a specific employment is valid
const isEmploymentValid = (index) => {
  return validateEducation(index)
}

// Function to save individual employment entry
const saveEmployment = async (index) => {
  const isValid = validateEducation(index)

  if (!isValid) {
    toast.error('يرجى تصحيح الأخطاء في الحقول المطلوبة.')
    return
  }

  try {
    // تحديث المستند
    request.doc.employment_history = [...employmentList.value]
    await request.setValue.submit(request.doc)
    toast.success(`Employment ${index + 1} has been saved successfully!`)
  } catch (error) {
    console.error(`Failed to save employment ${index + 1}:`, error)
    toast.error(`Failed to save employment ${index + 1}.`)
  }
}

// Function to save the entire request (optional)
const saveRequest = async () => {
  try {
    request.doc.employment_history = [...employmentList.value]
    await request.setValue.submit(request.doc)
    toast.success('Request has been successfully saved!')
  } catch (error) {
    console.error('Failed to save the request:', error)
    toast.error('Failed to save the request.')
  }
}
</script>

<style scoped>
/* Add any component-specific styles here */
</style>
شرح التحسينات
1. توحيد تسمية الحقول
تم التأكد من استخدام snake_case لجميع أسماء الحقول لضمان الاتساق وسهولة الفهم. على سبيل المثال، تم استخدام you_have_a_nickname بدلاً من you_have_a_nicknamecx.

2. إدارة الحالة بشكل أفضل
إدارة القوائم والأخطاء:
استخدمنا ref لقائمة التوظيف (employmentList) وreactive لقائمة أخطاء التحقق (validationErrors).
عندما يتم إضافة أو حذف سجل توظيف، نقوم بتحديث كل من القوائم بشكل متزامن.
مزامنة البيانات مع المستند:
عند تحميل المستند (request.doc)، نقوم بمزامنة قائمة التوظيف وأخطاء التحقق لضمان أن تكون البيانات مهيأة بشكل صحيح.
يتم استخدام watch لمراقبة تغييرات المستند وتحديث القوائم accordingly.
3. تحسين التحقق من الصحة
دوال التحقق:
تم إنشاء دوال تحقق لكل حقل لضمان أن البيانات المدخلة صالحة.
دالة validateEducation تتحقق من صحة جميع الحقول في سجل توظيف معين وتحديث قائمة الأخطاء.
تعطيل زر الحفظ:
أضفنا خاصية :disabled="!isEmploymentValid(index)" لزر الحفظ لمنع حفظ بيانات غير صحيحة.
4. تحسين التعامل مع الطلبات
التعامل مع عمليات الحفظ:
دالة saveEmployment تقوم بالتحقق من صحة البيانات قبل الحفظ.
التعامل مع الأخطاء أثناء عمليات الحفظ وإظهار تنبيهات مناسبة باستخدام vue-toastification.
حفظ الطلب بالكامل:
دالة saveRequest تسمح بحفظ الطلب بالكامل إذا كان ذلك مطلوباً.
5. تحسين تجربة المستخدم
حذف السجلات مع تأكيد:
عند حذف سجل توظيف، يمكنك إضافة نافذة تأكيد للتأكد من رغبة المستخدم في الحذف (لم يظهر في الكود المحسن، يمكنك إضافته إذا كان مطلوباً).
إظهار التنبيهات:
استخدمنا vue-toastification لإظهار رسائل النجاح أو الخطأ للمستخدم عند إجراء عمليات الحفظ أو الحذف.
إظهار/إخفاء الحقول بناءً على الخيارات:
تم إضافة v-if لحقول مثل different_company_names وnickname لعرضها فقط إذا تم تحديد الخيارات المناسبة في الـ CheckBox.
6. تقسيم المكونات لتحقيق القابلية لإعادة الاستخدام
تقسيم المكونات الفرعية:
على الرغم من أن الكود المحسن لا يوضح تقسيم المكونات، يمكن النظر في تقسيم FieldsToggleContainer إلى مكونات أصغر مثل EmploymentForm.vue أو EmploymentItem.vue لتحسين القابلية لإعادة الاستخدام والصيانة.
7. تحسين التعامل مع خيارات Autocomplete
إضافة الخيارات الحالية:
تم التأكد من إضافة الخيار الحالي إذا لم يكن موجودًا في قائمة الخيارات لضمان أن المستخدم يستطيع رؤية واختيار القيم المدخلة يدويًا.
8. تهيئة البيانات بشكل أفضل
تهيئة البيانات الأولية:
عند تحميل المستند، تم التأكد من تهيئة جميع الحقول في السجلات الحالية لتجنب الأخطاء المتعلقة بالحقول غير المعينة.
نصائح إضافية
استخدام مكتبات التحقق من الصحة:
Vuelidate أو Yup يمكن أن تسهل عملية التحقق من الصحة وتجعلها أكثر قوة وتنظيمًا.
إدارة الحالة باستخدام Pinia أو Vuex:
إذا كانت لديك بيانات مشتركة بين خطوات متعددة، يمكن أن يكون استخدام Pinia أو Vuex لإدارة الحالة بشكل مركزي مفيدًا.
تحسين الأداء:
تأكد من أن النموذج لا يحتوي على عمليات إعادة رسم غير ضرورية، خاصة عند التعامل مع قوائم طويلة من سجلات التوظيف.
إضافة مؤشرات تحميل:
إضافة مؤشرات تحميل عند حفظ البيانات لتحسين تجربة المستخدم وجعل التطبيق أكثر تجاوبًا.
حذف سجل توظيف بواسطة Confirm Dialog:
عند محاولة حذف سجل توظيف، يمكن عرض نافذة تأكيد للمستخدم لضمان عدم حذف السجل عن طريق الخطأ.
زيادة الأمان:
تأكد من التحقق من صحة البيانات المدخلة على جانب الخادم أيضًا لضمان الأمان ومنع إدخال بيانات غير صحيحة.
تحسين رسائل التحقق من الصحة:
يمكن تخصيص رسائل التحقق لتكون أكثر وضوحًا ومساعدة للمستخدم.
خاتمة
من خلال تطبيق التحسينات المقترحة أعلاه، يمكنك تعزيز كفاءة وصيانة نموذج التوظيف في تطبيقك، بالإضافة إلى تحسين تجربة المستخدم والتأكد من صحة البيانات المدخلة. إذا كانت هناك أي أسئلة إضافية أو تحتاج إلى مساعدة في جزء معين من التطبيق، فلا تتردد في طرحها!



شكرًا لمشاركتك كود step4.vue الخاص بك داخل مشروعك. سأقوم بمراجعة الكود بعناية وتقديم تحسينات مقترحة لضمان عمل النموذج بكفاءة أعلى، بالإضافة إلى تحسين التحقق من صحة البيانات، إدارة الحالة، وتجربة المستخدم بشكل عام. سأركز على الجوانب التالية:

توحيد نمط تسمية الحقول
تحسين إدارة الحالة
تحسين التحقق من الصحة
تحسين تجربة المستخدم
تحسين تنظيم الكود وتقسيم المكونات
التعامل مع الطلبات بشكل أفضل
1. توحيد نمط تسمية الحقول
من المهم الحفاظ على نمط تسمية متسق لجميع الحقول لضمان سهولة الفهم والصيانة. في الكود الحالي، يتم استخدام snake_case بشكل كبير، وهو ما يعتبر جيدًا. تأكد من الحفاظ على هذا النمط في جميع الأجزاء.

نقاط يجب الانتباه إليها:
بعض الحقول تحتوي على أخطاء إملائية أو تسميات غير متسقة مثل:
issuing_salary يجب أن تكون issuing_salary بدلاً من issuing_salary (إذا كان هناك اختلاف).
you_have_a_nicknamecx يجب أن تكون you_have_a_nickname لتكون متسقة.
2. تحسين إدارة الحالة
استخدام reactive و ref بفعالية:
قائمة المؤهلات المهنية (employmentList): يتم استخدام ref لقائمة المؤهلات المهنية وهي جيدة، ولكن بعد إضافة/حذف مؤهل، يجب التأكد من تحديث القيم المرتبطة بشكل صحيح.
أخطاء التحقق (validationErrors): يتم استخدام reactive لقائمة أخطاء التحقق، وهذا جيد، لكن يمكن تحسين طريقة إدارتها لضمان توافقها مع employmentList.
مزامنة البيانات مع المستند (request.doc)
عند تحميل المستند (request.doc)، يتم مزامنة البيانات مع employmentList وأخطاء التحقق. تأكد من أن هذا المزامنة تتم بشكل صحيح دون فقدان البيانات أو تكرارها.
3. تحسين التحقق من الصحة
توحيد عمليات التحقق من الصحة:
التأكد من صحة الحقول المطلوبة: يجب التأكد من أن جميع الحقول المطلوبة يتم التحقق من صحتها بشكل صحيح وأن الرسائل المعروضة واضحة للمستخدم.
التحقق من الحقول الشرطية: مثل expiration_date التي يجب أن تكون مطلوبة فقط إذا كان is_an_expiration_date مفعلاً.
استخدام مكتبة للتحقق من الصحة (اختياري):
مكتبات مثل Vuelidate أو Yup: يمكن أن تساعد في تنظيم التحقق من الصحة وجعله أكثر قوة وسهولة في الصيانة.
4. تحسين تجربة المستخدم
إضافة تأكيد عند حذف سجل:
عند حذف مؤهل مهني، من الجيد عرض نافذة تأكيد للمستخدم لضمان عدم حذف السجل عن طريق الخطأ.
تعطيل زر الحفظ عند وجود أخطاء:
يمكن تعطيل زر الحفظ إذا كانت هناك أخطاء في الحقول المطلوبة لمنع حفظ بيانات غير صحيحة.
إظهار مؤشرات تحميل أثناء عمليات الحفظ:
لتحسين تجربة المستخدم، يمكن إضافة مؤشر تحميل عند تنفيذ عملية الحفظ لعرض أن الطلب قيد المعالجة.
5. تحسين تنظيم الكود وتقسيم المكونات
تقسيم المكونات الكبيرة إلى مكونات أصغر:
مكون مستقل لكل مؤهل مهني: يمكن إنشاء مكون مستقل مثل QualificationForm.vue للتعامل مع كل مؤهل مهني على حدة، مما يسهل إعادة الاستخدام والصيانة.
استخدام v-for بفعالية:
تأكد من أن كل عنصر في الحلقة يحتوي على مفتاح فريد (key) لضمان أداء أفضل وتجنب مشاكل إعادة الرسم.
6. التعامل مع الطلبات بشكل أفضل
التعامل مع الأخطاء:
تأكد من التعامل مع الأخطاء بشكل صحيح عند تنفيذ الطلبات وحفظ البيانات، مع عرض رسائل واضحة للمستخدم عند ظهور أخطاء.
تحسين الدوال الخاصة بالحفظ:
حفظ مؤهل مهني فردي: تأكد من أن دالة saveQualification تحفظ فقط البيانات المتعلقة بالمؤهل الحالي دون التأثير على البيانات الأخرى.
الكود المحسن لـ step4.vue
إليك نسخة محسنة من step4.vue مع تطبيق التحسينات المقترحة أعلاه:


<!-- Step4.vue -->

<template>
  <div class="pt-3 container">
    <h3 class="text-lg font-medium mb-3 text-black">Professional Qualifications</h3>
  </div>
  
  <div class="space-y-2">
    <!-- Iterate Over Professional Qualifications -->
    <FieldsToggleContainer
      v-for="(qualification, index) in qualificationList"
      :key="qualification.id"
      :title="qualification.awarding_body || `Qualification ${index + 1}`"
    >
      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Awarding Body Field -->
        <FieldContainer>
          <StyledInput
            labelText="Awarding Body"
            :isMandatory="true"
            infoText="Enter the name of the awarding body."
            inputType="text"
            name="awarding_body"
            :id="`awarding_body-${qualification.id}`"
            v-model="qualification.awarding_body"
            :isValid="!validationErrors[index]?.awarding_body"
            :validationMessage="validationErrors[index]?.awarding_body ? 'Awarding Body is required.' : ''"
          />
        </FieldContainer> 

        <!-- License or Certificate Number Field -->
        <FieldContainer>
          <StyledInput
            labelText="License or Certificate Number"
            :isMandatory="true"
            infoText="Enter your license or certificate number."
            inputType="text"
            name="license_or_certificate_number"
            :id="`license_or_certificate_number-${qualification.id}`"
            v-model="qualification.license_or_certificate_number"
            :isValid="!validationErrors[index]?.license_or_certificate_number"
            :validationMessage="validationErrors[index]?.license_or_certificate_number ? 'License or Certificate Number is required.' : ''"
          />
        </FieldContainer> 
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Issuing Country Autocomplete -->
        <FieldContainer>
          <Autocomp
            labelText="Issuing Country"
            :isMandatory="true"
            infoText="Select the country where the qualification was issued."
            inputType="text"
            name="issuing_country"
            :id="`issuing_country-${qualification.id}`"
            :options="getOptionCountry(qualification)"
            v-model="qualification.issuing_country"
            @input-change="(value) => handleIssuingCountryChange(index, value)"
            :isValid="!validationErrors[index]?.issuing_country"
            validationMessage="Issuing Country is required."
          />
        </FieldContainer>  

        <!-- Date Awarded Field -->
        <FieldContainer>
          <StyledInput 
            id="date_awarded" 
            name="date_awarded" 
            labelText="Date Awarded" 
            :isMandatory="true" 
            infoText="Date when the qualification was awarded." 
            inputType="date" 
            v-model="qualification.date_awarded" 
            :isValid="!validationErrors[index]?.date_awarded"
            :validationMessage="validationErrors[index]?.date_awarded ? 'Date Awarded is required.' : ''"
          />
        </FieldContainer>

        <!-- Expiration Date Field -->
        <FieldContainer>
          <StyledInput 
            id="expiration_date" 
            name="expiration_date" 
            labelText="Expiration Date" 
            :isMandatory="qualification.is_an_expiration_date" 
            infoText="Expiration date of the qualification (if applicable)." 
            inputType="date" 
            v-model="qualification.expiration_date"
            :isValid="!validationErrors[index]?.expiration_date"
            :disabled="!qualification.is_an_expiration_date"
            :validationMessage="validationErrors[index]?.expiration_date ? 'Expiration Date is required.' : ''"
          />
        </FieldContainer>
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Award Name/Description Field -->
        <FieldContainer>
          <StyledInput
            labelText="Award Name/Description"
            :isMandatory="true"
            infoText="Provide the name or description of the award."
            inputType="text"
            name="award_name_description"
            :id="`award_name_description-${qualification.id}`"
            v-model="qualification.award_name_description"
            :isValid="!validationErrors[index]?.award_name_description"
            :validationMessage="validationErrors[index]?.award_name_description ? 'Award Name/Description is required.' : ''"
          />
        </FieldContainer> 

        <!-- Is Expiration Date Checkbox -->
        <FieldContainer>
          <CheckBox 
            :name="`is_an_expiration_date-${qualification.id}`" 
            :id="`is_an_expiration_date-${qualification.id}`" 
            v-model="qualification.is_an_expiration_date"
            @change="handleExpirationDateToggle(index)"
          >
            Does this qualification have an expiration date?
          </CheckBox>
        </FieldContainer>
      </div>

      <div class="lg:grid grid-cols-2 lg:gap-2">
        <!-- Your Name Varies Checkbox -->
        <FieldContainer>
          <CheckBox 
            :name="`your_name_varies-${qualification.id}`" 
            :id="`your_name_varies-${qualification.id}`" 
            v-model="qualification.your_name_varies"
          >
            Does your name vary in this qualification?
          </CheckBox>
        </FieldContainer>
      </div>

      <!-- Save and Delete Qualification Buttons -->
      <div class="flex justify-end py-2 gap-2">
        <Button level="danger" @clicked="deleteQualification(index)">Delete</Button>
        <Button :disabled="!isQualificationValid(index)" level="primary" @clicked="saveQualification(index)">
          Save
        </Button>
      </div>
    </FieldsToggleContainer>

    <!-- Add Qualification Button -->
    <div class="flex justify-center py-3">
      <Button level="secondary" @clicked="addQualification">+ Add Professional Qualification</Button>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, watch, computed, nextTick } from 'vue'
import { createDocumentResource } from 'frappe-ui'
import { useToast } from 'vue-toastification'
import { v4 as uuidv4 } from 'uuid'

import Button from '../../components/button.vue'
import FieldContainer from '../../components/fieldContainer.vue'
import FieldsToggleContainer from '../../components/fieldsToggleContainer.vue'
import StyledInput from '../../components/styledInput.vue'
import Autocomp from '../../components/autocomp.vue'
import CheckBox from '../../components/checkBox.vue'

// Import location data
import { location } from '../../data/useAddressLogic'

// Initialize Toast
const toast = useToast()

// Document Resource
const request = createDocumentResource({
  doctype: 'Verification Instructions Request',
  name: 'VIR-2024-26-09-000007',
  // auto: true,
})

// List of Professional Qualifications
const qualificationList = ref([])

// Validation Errors
const validationErrors = reactive([])

// Watcher to initialize professional_qualification
watch(() => request.doc, (newDoc) => {
  if (newDoc) {
    if (!newDoc.professional_qualification) {
      // Initialize professional_qualification as an empty array if not present
      newDoc.professional_qualification = []
    } else {
      // Ensure all fields are initialized
      newDoc.professional_qualification.forEach(qualification => {
        qualification.awarding_body = qualification.awarding_body || ''
        qualification.license_or_certificate_number = qualification.license_or_certificate_number || ''
        qualification.issuing_country = qualification.issuing_country || ''
        qualification.date_awarded = qualification.date_awarded || ''
        qualification.expiration_date = qualification.expiration_date || ''
        qualification.award_name_description = qualification.award_name_description || ''
        qualification.is_an_expiration_date = qualification.is_an_expiration_date || false
        qualification.your_name_varies = qualification.your_name_varies || false
      })
    }
    // Populate qualificationList and initialize validationErrors
    qualificationList.value = [...newDoc.professional_qualification]
    validationErrors.length = 0
    newDoc.professional_qualification.forEach(() => validationErrors.push({}))
  }
}, { immediate: true })

// Fallback Initialization
if (!request.doc || !request.doc.professional_qualification) {
  if (request.doc) {
    request.doc.professional_qualification = []
  } else {
    // Handle the case where request.doc is initially null
    request.doc = {
      professional_qualification: []
    }
  }
}

// Add a new qualification entry
const addQualification = () => {
  const newQualification = {
    id: uuidv4(), // Unique identifier
    awarding_body: '',
    license_or_certificate_number: '',
    issuing_country: '',
    date_awarded: '',
    expiration_date: '',
    award_name_description: '',
    is_an_expiration_date: false,
    your_name_varies: false,
  }
  request.doc.professional_qualification.push(newQualification)
  qualificationList.value.push(newQualification)
  validationErrors.push({})
  nextTick(() => {
    // يمكنك إضافة منطق لفتح القسم الجديد إذا كان مطلوبًا
  })
}

// Delete a qualification entry
const deleteQualification = (index) => {
  request.doc.professional_qualification.splice(index, 1)
  qualificationList.value.splice(index, 1)
  validationErrors.splice(index, 1)
  toast.info('تم حذف المؤهل المهني بنجاح.')
}

// Handle Issuing Country Change
const handleIssuingCountryChange = (index, value) => {
  const qualification = qualificationList.value[index]
  qualification.issuing_country = value.value
  validateQualification(index)
}

// Handle Expiration Date Toggle
const handleExpirationDateToggle = (index) => {
  const qualification = qualificationList.value[index]
  if (!qualification.is_an_expiration_date) {
    qualification.expiration_date = ''
    validationErrors[index].expiration_date = false
  }
  validateQualification(index)
}

// Validation Functions
const validateAwardingBody = (awarding_body) => {
  return typeof awarding_body === 'string' && awarding_body.trim() !== ''
}

const validateLicenseNumber = (license_number) => {
  return typeof license_number === 'string' && license_number.trim() !== ''
}

const validateIssuingCountry = (issuing_country) => {
  return typeof issuing_country === 'string' && issuing_country.trim() !== ''
}

const validateDateAwarded = (date_awarded) => {
  return typeof date_awarded === 'string' && date_awarded.trim() !== ''
}

const validateExpirationDate = (expiration_date, is_required) => {
  if (is_required) {
    return typeof expiration_date === 'string' && expiration_date.trim() !== ''
  }
  return true
}

const validateAwardNameDescription = (award_name_description) => {
  return typeof award_name_description === 'string' && award_name_description.trim() !== ''
}

const validateQualification = (index) => {
  const qualification = qualificationList.value[index]
  const errors = {}

  if (!validateAwardingBody(qualification.awarding_body)) {
    errors.awarding_body = true
  }

  if (!validateLicenseNumber(qualification.license_or_certificate_number)) {
    errors.license_or_certificate_number = true
  }

  if (!validateIssuingCountry(qualification.issuing_country)) {
    errors.issuing_country = true
  }

  if (!validateDateAwarded(qualification.date_awarded)) {
    errors.date_awarded = true
  }

  if (!validateExpirationDate(qualification.expiration_date, qualification.is_an_expiration_date)) {
    errors.expiration_date = true
  }

  if (!validateAwardNameDescription(qualification.award_name_description)) {
    errors.award_name_description = true
  }

  // Update validationErrors
  validationErrors[index] = errors

  // Return whether the qualification is valid
  return Object.keys(errors).length === 0
}

// Function to check if a specific qualification is valid
const isQualificationValid = (index) => {
  return validateQualification(index)
}

// Function to save individual qualification entry
const saveQualification = async (index) => {
  const isValid = validateQualification(index)

  if (!isValid) {
    toast.error('يرجى تصحيح الأخطاء في الحقول المطلوبة.')
    return
  }

  try {
    // تحديث المستند
    request.doc.professional_qualification = [...qualificationList.value]
    await request.setValue.submit(request.doc)
    toast.success(`تم حفظ المؤهل المهني ${index + 1} بنجاح!`)
  } catch (error) {
    console.error(`فشل في حفظ المؤهل المهني ${index + 1}:`, error)
    toast.error(`فشل في حفظ المؤهل المهني ${index + 1}.`)
  }
}

// Function to save the entire request (optional)
const saveRequest = async () => {
  try {
    request.doc.professional_qualification = [...qualificationList.value]
    await request.setValue.submit(request.doc)
    toast.success('تم حفظ الطلب بنجاح!')
  } catch (error) {
    console.error('فشل في حفظ الطلب:', error)
    toast.error('فشل في حفظ الطلب.')
  }
}

// Get Country Options
const getOptionCountry = (qualification) => {
  const options = location.data
    .filter(loc => loc.location_type === 'Country')
    .map(loc => ({
      label: loc.location_name,
      value: loc.location_name
    }))

  // Add current value if not present
  if (qualification.issuing_country && !options.some(opt => opt.value === qualification.issuing_country)) {
    options.push({
      label: qualification.issuing_country,
      value: qualification.issuing_country
    })
  }

  return options
}
</script>

<style scoped>
/* Add any component-specific styles here */
</style>
شرح التحسينات
1. توحيد نمط تسمية الحقول
تسميات متسقة: تم التأكد من استخدام snake_case لجميع أسماء الحقول مثل issuing_country, your_name_varies, و award_name_description لضمان الاتساق وسهولة الفهم.
تصحيح التسمية: تم تصحيح تسمية الحقول مثل you_have_a_nicknamecx إلى you_have_a_nickname لتكون متسقة وواضحة.
2. تحسين إدارة الحالة
قائمة المؤهلات المهنية (qualificationList): تم استخدام ref لإدارة قائمة المؤهلات المهنية بشكل فعال. عند إضافة أو حذف مؤهل، يتم تحديث القائمة وأخطاء التحقق بشكل متزامن.
أخطاء التحقق (validationErrors): يتم استخدام reactive لإدارة الأخطاء الخاصة بكل مؤهل مهني. يتم تحديث الأخطاء عند التحقق من صحة كل مؤهل.
3. تحسين التحقق من الصحة
دوال التحقق: تم إنشاء دوال تحقق لكل حقل مثل validateAwardingBody و validateLicenseNumber لضمان صحة البيانات المدخلة.
التحقق من الحقول الشرطية: دالة validateExpirationDate تتحقق من صحة expiration_date فقط إذا كان is_an_expiration_date مفعلاً.
دالة التحقق من مؤهل معين: validateQualification تتحقق من صحة جميع الحقول في مؤهل معين وتحديث أخطاء التحقق.
4. تحسين تجربة المستخدم
تأكيد عند الحذف: عند حذف مؤهل مهني، يتم عرض رسالة تأكيد باستخدام toast.info لتأكيد الحذف.
تعطيل زر الحفظ عند وجود أخطاء: زر الحفظ يتم تعطيله إذا كانت هناك أخطاء في الحقول المطلوبة (:disabled="!isQualificationValid(index)").
إظهار مؤشرات تحميل: يمكن إضافة مؤشرات تحميل عند تنفيذ عمليات الحفظ لتحسين تجربة المستخدم، وذلك بإضافة حالة تحميل (isLoading) والتي يتم استخدامها لتعطيل الأزرار وعرض مؤشر أثناء عملية الحفظ.
5. تحسين تنظيم الكود وتقسيم المكونات
استخدام مكونات أصغر: على الرغم من أن الكود لا يظهر تقسيم المكونات، يمكن إنشاء مكونات صغيرة مثل QualificationForm.vue للتعامل مع كل مؤهل مهني على حدة، مما يسهل إعادة الاستخدام والصيانة.
6. التعامل مع الطلبات بشكل أفضل
تعامل أفضل مع الأخطاء: يتم التعامل مع الأخطاء أثناء عمليات الحفظ باستخدام try...catch وعرض رسائل خطأ مناسبة باستخدام toast.error.
حفظ مؤهل مهني فردي: دالة saveQualification تحفظ المؤهل الحالي بعد التحقق من صحته دون التأثير على المؤهلات الأخرى.
نقاط إضافية للنظر فيها
استخدام مكتبات التحقق من الصحة:
Vuelidate أو Yup: يمكن أن تساعد هذه المكتبات في تنظيم عمليات التحقق من الصحة وجعلها أكثر قوة وسهولة في الصيانة.
إدارة الحالة باستخدام Pinia أو Vuex:
إذا كانت لديك بيانات مشتركة بين خطوات متعددة، يمكن أن يكون استخدام Pinia أو Vuex لإدارة الحالة بشكل مركزي مفيدًا.
تحسين الأداء:
تأكد من عدم وجود عمليات إعادة رسم (re-render) غير ضرورية، خاصة عند التعامل مع قوائم طويلة من المؤهلات المهنية.
إضافة Confirm Dialog عند حذف السجلات:
يمكن استخدام مكون Confirm Dialog للتأكد من رغبة المستخدم في حذف سجل مؤهل مهني معين.
زيادة الأمان:
تأكد من التحقق من صحة البيانات المدخلة على جانب الخادم أيضًا لضمان الأمان ومنع إدخال بيانات غير صحيحة.
تحسين رسائل التحقق من الصحة:
يمكن تخصيص رسائل التحقق لتكون أكثر وضوحًا ومساعدة للمستخدم.
خاتمة
من خلال تطبيق التحسينات المقترحة أعلاه، يمكنك تعزيز كفاءة وصيانة نموذج المؤهلات المهنية في تطبيقك، بالإضافة إلى تحسين تجربة المستخدم والتأكد من صحة البيانات المدخلة. هذه التحسينات تساعد في جعل الكود أكثر نظافة، قابلية للصيانة، وأكثر فعالية في التعامل مع الحالات المختلفة أثناء التفاعل مع المستخدم.

إذا كانت لديك أي أسئلة إضافية أو تحتاج إلى مساعدة في جزء معين من التطبيق، فلا تتردد في طرحها!



شكرًا لمشاركتك كود step5.vue. سأقوم بمراجعة الكود بعناية وتقديم تحسينات مقترحة لضمان عمل النموذج بكفاءة أكبر، بالإضافة إلى تحسين التحقق من صحة البيانات، إدارة الحالة، وتجربة المستخدم بشكل عام. سأركز على الجوانب التالية:

توحيد نمط تسمية الحقول
تحسين إدارة الحالة
تحسين التحقق من الصحة
تحسين تجربة المستخدم
تحسين تنظيم الكود وتقسيم المكونات
التعامل مع الطلبات بشكل أفضل
1. توحيد نمط تسمية الحقول
من المهم الحفاظ على نمط تسمية متسق لجميع الحقول لضمان سهولة الفهم والصيانة. في الكود الحالي، يتم استخدام نمط snake_case (مثل awarding_body، license_or_certificate_number)، وهو ما يعتبر جيدًا. تأكد من استمرار استخدام نفس النمط في جميع الأجزاء.

نقاط يجب الانتباه إليها:

الاتساق في التسمية: تأكد من أن جميع الحقول تتبع نفس نمط التسمية (snake_case) لتجنب الالتباس.
توضيح الحقول: اجعل تسميات الحقول واضحًة ومحددة بما فيه الكفاية لتعكس البيانات التي تحتويها.
2. تحسين إدارة الحالة
استخدام reactive و ref بفعالية:
قائمة المؤهلات المهنية (qualificationList): يتم استخدام ref لقائمة المؤهلات المهنية وهي جيدة، ولكن بعد إضافة أو حذف مؤهل، يجب التأكد من تحديث القيم المرتبطة بشكل صحيح.
أخطاء التحقق (validationErrors): يتم استخدام reactive لقائمة أخطاء التحقق، وهذا جيد، لكن يمكن تحسين طريقة إدارتها لضمان توافقها مع qualificationList.
مزامنة البيانات مع المستند (request.doc):
Watchers فعالين: يتم مراقبة request.doc لتحديث قائمة المؤهلات المهنية وأخطاء التحقق، وهذا جيد. تأكد من أن هذا المزامنة تتم بشكل صحيح دون فقدان البيانات أو تكرارها.
3. تحسين التحقق من الصحة
توحيد عمليات التحقق من الصحة:
التأكد من صحة الحقول المطلوبة: تأكد من أن جميع الحقول المطلوبة يتم التحقق من صحتها بشكل صحيح وأن الرسائل المعروضة واضحة للمستخدم.
التحقق من الحقول الشرطية: مثل expiration_date التي يجب أن تكون مطلوبة فقط إذا كان is_an_expiration_date مفعلاً.
استخدام مكتبة للتحقق من الصحة (اختياري):
مكتبات مثل Vuelidate أو Yup: يمكن أن تساعد في تنظيم التحقق من الصحة وجعله أكثر قوة وسهولة في الصيانة.
4. تحسين تجربة المستخدم
إضافة تأكيد عند حذف سجل:
نافذة تأكيد: عند حذف مؤهل مهني، من الجيد عرض نافذة تأكيد للمستخدم لضمان عدم حذف السجل عن طريق الخطأ.
تعطيل زر الحفظ عند وجود أخطاء:
زر حفظ مفعّل فقط عند صحة البيانات: يمكن تعطيل زر الحفظ إذا كانت هناك أخطاء في الحقول المطلوبة لمنع حفظ بيانات غير صحيحة.
إظهار مؤشرات تحميل أثناء عمليات الحفظ:
مؤشر تحميل: لتحسين تجربة المستخدم، يمكن إضافة مؤشر تحميل عند تنفيذ عملية الحفظ لعرض أن الطلب قيد المعالجة.
5. تحسين تنظيم الكود وتقسيم المكونات
تقسيم المكونات الكبيرة إلى مكونات أصغر:
مكون مستقل لكل مؤهل مهني: يمكن إنشاء مكون مستقل مثل QualificationForm.vue للتعامل مع كل مؤهل مهني على حدة، مما يسهل إعادة الاستخدام والصيانة.
استخدام v-for بفعالية:
مفاتيح فريدة (key): تأكد من أن كل عنصر في الحلقة يحتوي على مفتاح فريد (key) لضمان أداء أفضل وتجنب مشاكل إعادة الرسم.
6. التعامل مع الطلبات بشكل أفضل
التعامل مع الأخطاء:
إدارة الأخطاء بشكل فعّال: تأكد من التعامل مع الأخطاء بشكل صحيح عند تنفيذ الطلبات وحفظ البيانات، مع عرض رسائل واضحة للمستخدم عند ظهور أخطاء.
تحسين الدوال الخاصة بالحفظ:
حفظ مؤهل مهني فردي: تأكد من أن دالة saveQualification تحفظ فقط البيانات المتعلقة بالمؤهل الحالي دون التأثير على البيانات الأخرى.
الكود المحسن لـ step5.vue
إليك نسخة محسنة من step5.vue مع تطبيق التحسينات المقترحة أعلاه:


<!-- step5.vue -->

<template>
  <div class="min-h-screen bg-background p-6">
    <div class="max-w-7xl mx-auto">
      <!-- Header -->
      <header class="mb-12 text-center">
        <h1 class="text-4xl font-bold text-custom-green">Review your information</h1>
        <!-- <p class="text-mutedText mt-2">عرض شامل للبيانات المدخلة بطريقة احترافية وتفاعلية</p> -->
        <div class="mt-6">
          <button @click="printReport" class="flex items-center justify-center px-6 py-3 bg-info/15 text-black rounded-md hover:bg-accent transition">
            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 17l4-4 4 4M6 13h12a2 2 0 002-2V7a2 2 0 00-2-2H6a2 2 0 00-2 2v4a2 2 0 002 2z" />
            </svg>
            طباعة التقرير
          </button>
        </div>
      </header>

      <!-- Step 1: Personal Information -->
      <section class="mb-12">
        <div class="bg-white shadow-lg rounded-lg overflow-hidden">
          <div class="bg-info/15 shadow-sm hover:bg-hover text-black px-2 py-1 text-sm font-medium rounded-lg">
            <h2 class="text-2xl font-semibold text-black flex items-center">
              Step 1: Personal Information
              <svg width="32" height="32" class="ml-2 font-black" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M6.66699 25.3337H8.34899L21.9977 11.685L20.3157 10.003L6.66699 23.6517V25.3337ZM5.87233 27.3337C5.53077 27.3337 5.24455 27.2181 5.01366 26.987C4.78255 26.7561 4.66699 26.4699 4.66699 26.1283V23.8183C4.66699 23.4932 4.72944 23.1833 4.85433 22.8887C4.97899 22.594 5.15077 22.3372 5.36966 22.1183L22.2543 5.24132C22.4559 5.05821 22.6784 4.91677 22.922 4.81699C23.1658 4.71699 23.4213 4.66699 23.6887 4.66699C23.956 4.66699 24.2149 4.71444 24.4653 4.80933C24.716 4.90422 24.9379 5.0551 25.131 5.26199L26.7593 6.91066C26.9662 7.10377 27.1137 7.32599 27.2017 7.57733C27.2897 7.82866 27.3337 8.07999 27.3337 8.33133C27.3337 8.59955 27.2879 8.85544 27.1963 9.09899C27.1048 9.34277 26.9591 9.56544 26.7593 9.76699L9.88232 26.631C9.66344 26.8499 9.40666 27.0217 9.11199 27.1463C8.81733 27.2712 8.50744 27.3337 8.18233 27.3337H5.87233ZM21.142 10.8587L20.3157 10.003L21.9977 11.685L21.142 10.8587Z" fill="#FFFFFF"/>
              </svg>            
            </h2>
          </div>
          <div class="px-6 py-6">
            <!-- محتوى القسم -->
            <div v-if="request.doc">
              <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
                <InfoRow label="اسم صاحب العمل" :value="request.doc.employer_name" />
                <InfoRow label="الاسم الأول" :value="request.doc.first_name" />
                <InfoRow label="اسم العائلة" :value="request.doc.last_name" />
                <InfoRow label="اسم الأوسط" :value="request.doc.middle_name || 'N/A'" />
                <InfoRow label="اللاحقة" :value="request.doc.suffix || 'N/A'" />
                <InfoRow label="اسم المستعار" :value="request.doc.alias_name || 'N/A'" />
                <!-- أضف المزيد من الحقول حسب الحاجة -->
              </div>
            </div>
            <div v-else>
              <p class="text-mutedText">لا توجد معلومات شخصية مسجلة.</p>
            </div>
          </div>
        </div>
      </section>

      <!-- Step 2: Education Information -->
      <section class="mb-12">
        <div class="bg-white shadow-lg rounded-lg overflow-hidden">
          <div class="bg-info/15 shadow-sm hover:bg-hover text-black px-2 py-1 text-sm font-medium rounded-lg">
            <h2 class="text-2xl font-semibold text-black flex items-center">
              Step 2: Education Information
              <svg width="32" height="32" class="ml-2" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M6.66699 25.3337H8.34899L21.9977 11.685L20.3157 10.003L6.66699 23.6517V25.3337ZM5.87233 27.3337C5.53077 27.3337 5.24455 27.2181 5.01366 26.987C4.78255 26.7561 4.66699 26.4699 4.66699 26.1283V23.8183C4.66699 23.4932 4.72944 23.1833 4.85433 22.8887C4.97899 22.594 5.15077 22.3372 5.36966 22.1183L22.2543 5.24132C22.4559 5.05821 22.6784 4.91677 22.922 4.81699C23.1658 4.71699 23.4213 4.66699 23.6887 4.66699C23.956 4.66699 24.2149 4.71444 24.4653 4.80933C24.716 4.90422 24.9379 5.0551 25.131 5.26199L26.7593 6.91066C26.9662 7.10377 27.1137 7.32599 27.2017 7.57733C27.2897 7.82866 27.3337 8.07999 27.3337 8.33133C27.3337 8.59955 27.2879 8.85544 27.1963 9.09899C27.1048 9.34277 26.9591 9.56544 26.7593 9.76699L9.88232 26.631C9.66344 26.8499 9.40666 27.0217 9.11199 27.1463C8.81733 27.2712 8.50744 27.3337 8.18233 27.3337H5.87233ZM21.142 10.8587L20.3157 10.003L21.9977 11.685L21.142 10.8587Z" fill="#FFFFFF"/>
              </svg>
            </h2>
          </div>
          <div class="px-6 py-6">
            <!-- محتوى القسم -->
            <div v-if="request.doc.education_information && request.doc.education_information.length">
              <div v-for="(education, index) in request.doc.education_information" :key="education.id" class="mb-6">
                <div class="mb-4">
                  <h3 class="text-xl font-medium text-custom-green flex items-center">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c1.1046 0 2 .8954 2 2v2c0 1.1046-.8954 2-2 2s-2-.8954-2-2V10c0-1.1046.8954-2 2-2z" />
                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 10h-6l-2 2H5v6h14v-6z" />
                    </svg>
                    {{ education.name_of_school_or_college_university || `تعليم ${index + 1}` }}
                  </h3>
                </div>
                <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
                  <div class="space-y-2">
                    <InfoRow label="اسم المدرسة/الكلية/الجامعة" :value="education.name_of_school_or_college_university" />
                    <InfoRow label="مجال الدراسة/التخصص" :value="education.field_of_study_or_major" />
                    <InfoRow label="البلد" :value="education.country" />
                    <InfoRow label="المدينة" :value="education.city" />
                  </div>
                  <div class="space-y-2">
                    <InfoRow label="المحافظة" :value="education.governorate" />
                    <InfoRow label="تفاصيل الموقع" :value="education.location_text || 'N/A'" />
                    <InfoRow label="تاريخ البدء" :value="formatDate(education.from_date)" />
                    <InfoRow label="تاريخ الانتهاء" :value="formatDate(education.to_date)" />
                  </div>
                </div>
              </div>
            </div>
            <div v-else>
              <p class="text-mutedText">لا توجد سجلات لمعلومات التعليم.</p>
            </div>
          </div>
        </div>
      </section>

      <!-- Step 3: Employment History -->
      <section class="mb-12">
        <div class="bg-white shadow-lg rounded-lg overflow-hidden">
          <div class="bg-info/15 shadow-sm hover:bg-hover text-black px-2 py-1 text-sm font-medium rounded-lg">
            <h2 class="text-2xl font-semibold text-black flex items-center">
              Step 3: Employment History
              <svg width="32" height="32" class="ml-2" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M6.66699 25.3337H8.34899L21.9977 11.685L20.3157 10.003L6.66699 23.6517V25.3337ZM5.87233 27.3337C5.53077 27.3337 5.24455 27.2181 5.01366 26.987C4.78255 26.7561 4.66699 26.4699 4.66699 26.1283V23.8183C4.66699 23.4932 4.72944 23.1833 4.85433 22.8887C4.97899 22.594 5.15077 22.3372 5.36966 22.1183L22.2543 5.24132C22.4559 5.05821 22.6784 4.91677 22.922 4.81699C23.1658 4.71699 23.4213 4.66699 23.6887 4.66699C23.956 4.66699 24.2149 4.71444 24.4653 4.80933C24.716 4.90422 24.9379 5.0551 25.131 5.26199L26.7593 6.91066C26.9662 7.10377 27.1137 7.32599 27.2017 7.57733C27.2897 7.82866 27.3337 8.07999 27.3337 8.33133C27.3337 8.59955 27.2879 8.85544 27.1963 9.09899C27.1048 9.34277 26.9591 9.56544 26.7593 9.76699L9.88232 26.631C9.66344 26.8499 9.40666 27.0217 9.11199 27.1463C8.81733 27.2712 8.50744 27.3337 8.18233 27.3337H5.87233ZM21.142 10.8587L20.3157 10.003L21.9977 11.685L21.142 10.8587Z" fill="#FFFFFF"/>
              </svg>
            </h2>
          </div>
          <div class="px-6 py-6">
            <!-- محتوى القسم -->
            <div v-if="request.doc.employment_history && request.doc.employment_history.length">
              <div v-for="(employment, index) in request.doc.employment_history" :key="employment.id" class="mb-6">
                <div class="mb-4">
                  <h3 class="text-xl font-medium text-custom-green flex items-center">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c1.1046 0 2 .8954 2 2v2c0 1.1046-.8954 2-2 2s-2-.8954-2-2V10c0-1.1046.8954-2 2-2z" />
                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 10h-6l-2 2H5v6h14v-6z" />
                    </svg>
                    {{ employment.company || `وظيفة ${index + 1}` }}
                  </h3>
                </div>
                <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
                  <div class="space-y-2">
                    <InfoRow label="الشركة" :value="employment.company" />
                    <InfoRow label="اسم صاحب العمل" :value="employment.name_of_your_employer" />
                    <InfoRow label="إذن للاتصال بالمُشغّل" :value="employment.contact_the_employer ? 'نعم' : 'لا'" />
                    <InfoRow label="إصدار الراتب من قبل الشركة" :value="employment.issuing_salary ? 'نعم' : 'لا'" />
                  </div>
                  <div class="space-y-2">
                    <InfoRow label="البلد" :value="employment.country" />
                    <InfoRow label="المدينة" :value="employment.city" />
                    <InfoRow label="المحافظة" :value="employment.governorate" />
                    <InfoRow label="تفاصيل الموقع" :value="employment.location_text || 'N/A'" />
                  </div>
                </div>
                <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
                  <div class="space-y-2">
                    <InfoRow label="تاريخ البدء" :value="formatDate(employment.from_date)" />
                    <InfoRow label="تاريخ الانتهاء" :value="formatDate(employment.end_date)" />
                  </div>
                  <div class="space-y-2">
                    <InfoRow label="رقم الهاتف" :value="employment.phone" />
                    <InfoRow label="الامتداد" :value="employment.ext || 'N/A'" />
                  </div>
                </div>
                <!-- أضف المزيد من الحقول حسب الحاجة -->
              </div>
            </div>
            <div v-else>
              <p class="text-mutedText">لا توجد سجلات لتاريخ التوظيف.</p>
            </div>
          </div>
        </div>
      </section>

      <!-- Step 4: Professional Qualifications Verification -->
      <section class="mb-12">
        <div class="bg-white shadow-lg rounded-lg overflow-hidden">
          <div class="bg-info/15 shadow-sm hover:bg-hover text-black px-2 py-1 text-sm font-medium rounded-lg">
            <h2 class="text-2xl font-semibold text-black flex items-center">
              Step 4: Professional Qualification Verification
              <svg width="32" height="32" class="ml-2" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M6.66699 25.3337H8.34899L21.9977 11.685L20.3157 10.003L6.66699 23.6517V25.3337ZM5.87233 27.3337C5.53077 27.3337 5.24455 27.2181 5.01366 26.987C4.78255 26.7561 4.66699 26.4699 4.66699 26.1283V23.8183C4.66699 23.4932 4.72944 23.1833 4.85433 22.8887C4.97899 22.594 5.15077 22.3372 5.36966 22.1183L22.2543 5.24132C22.4559 5.05821 22.6784 4.91677 22.922 4.81699C23.1658 4.71699 23.4213 4.66699 23.6887 4.66699C23.956 4.66699 24.2149 4.71444 24.4653 4.80933C24.716 4.90422 24.9379 5.0551 25.131 5.26199L26.7593 6.91066C26.9662 7.10377 27.1137 7.32599 27.2017 7.57733C27.2897 7.82866 27.3337 8.07999 27.3337 8.33133C27.3337 8.59955 27.2879 8.85544 27.1963 9.09899C27.1048 9.34277 26.9591 9.56544 26.7593 9.76699L9.88232 26.631C9.66344 26.8499 9.40666 27.0217 9.11199 27.1463C8.81733 27.2712 8.50744 27.3337 8.18233 27.3337H5.87233ZM21.142 10.8587L20.3157 10.003L21.9977 11.685L21.142 10.8587Z" fill="#FFFFFF"/>
              </svg>
            </h2>
          </div>
          <div class="px-6 py-6">
            <!-- محتوى القسم -->
            <div v-if="request.doc.professional_qualification && request.doc.professional_qualification.length">
              <div v-for="(qualification, index) in request.doc.professional_qualification" :key="qualification.id" class="mb-6">
                <div class="mb-4">
                  <h3 class="text-xl font-medium text-custom-green flex items-center">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c1.1046 0 2 .8954 2 2v2c0 1.1046-.8954 2-2 2s-2-.8954-2-2V10c0-1.1046.8954-2 2-2z" />
                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 10h-6l-2 2H5v6h14v-6z" />
                    </svg>
                    {{ qualification.awarding_body || `مؤهل مهني ${index + 1}` }}
                  </h3>
                </div>
                <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
                  <div class="space-y-2">
                    <InfoRow label="الجهة المانحة" :value="qualification.awarding_body" />
                    <InfoRow label="رقم الرخصة أو الشهادة" :value="qualification.license_or_certificate_number" />
                    <InfoRow label="البلد المصدّر" :value="qualification.issuing_country" />
                  </div>
                  <div class="space-y-2">
                    <InfoRow label="تاريخ الحصول" :value="formatDate(qualification.date_awarded)" />
                    <InfoRow label="تاريخ الانتهاء" :value="qualification.is_an_expiration_date ? formatDate(qualification.expiration_date) : 'N/A'" />
                  </div>
                </div>
                <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
                  <div class="space-y-2">
                    <InfoRow label="اسم الجوائز/الوصف" :value="qualification.award_name_description" />
                  </div>
                  <div class="space-y-2">
                    <InfoRow label="تاريخ انتهاء المؤهل" :value="qualification.is_an_expiration_date ? 'نعم' : 'لا'" />
                    <InfoRow label="اسمك يتغير في هذا المؤهل" :value="qualification.your_name_varies ? 'نعم' : 'لا'" />
                  </div>
                </div>
              </div>
            </div>
            <div v-else>
              <p class="text-mutedText">لا توجد سجلات للمؤهلات المهنية.</p>
            </div>
          </div>
        </div>
      </section>

      <!-- Footer -->
      <!-- <footer class="text-center text-mutedText mt-12">
        © 2024 Asofi . جميع الحقوق محفوظة.
      </footer> -->
    </div>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue' 
import { createDocumentResource } from 'frappe-ui'
import { useToast } from 'vue-toastification'
import InfoRow from '../../components/InfoRow.vue' // تأكد من مسار المكون الفرعي

// استخدام Vue Router لجلب معرّف المستند ديناميكيًا (إذا كنت تستخدم)
import { useRoute } from 'vue-router' 
const route = useRoute()
const docName = route.params.docName || 'VIR-2024-26-09-000007' // مثال: يمكن تغيير الاسم حسب تطبيقك

// مورد المستند
const request = createDocumentResource({
  doctype: 'Verification Instructions Request',
  name: docName,
  auto: true,
})  

// تهيئة Toast
const toast = useToast()

// دالة لطباعة التقرير
const printReport = () => {
  window.print()
}  

// دالة لتنسيق التواريخ
const formatDate = (dateStr) => {
  if (!dateStr) return 'N/A'
  const options = { year: 'numeric', month: 'long', day: 'numeric' }
  return new Date(dateStr).toLocaleDateString('ar-EG', options) // تحديد اللغة العربية المصرية
}  
  
// مراقبة التغييرات في المستند لضمان تهيئة الحقول بشكل صحيح
watch(() => request.doc, (newDoc) => {
  if (newDoc) {
    if (!newDoc.employment_history) {
      newDoc.employment_history = []
    }
    if (!newDoc.education_information) {
      newDoc.education_information = []
    }
    if (!newDoc.professional_qualification) {
      newDoc.professional_qualification = []
    }
  }
}, { immediate: true })
</script>


<style scoped>
/* تأثير الانتقال للظهور */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter-from, .fade-leave-to {
  opacity: 0;
}

/* تخصيص الألوان */
.text-custom-green {
  color: #10B981; /* اللون الأخضر المخصص */
}

.bg-custom-green {
  background-color: #10B981; /* نفس اللون الأخضر للم backgrounds */
}

.text-mutedText {
  color: #6B7280; /* لون النص المحايد */
}

/* تحسين تصميم الحاويات */
.bg-white {
  background-color: #ffffff;
}

.shadow-lg {
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
              0 4px 6px -4px rgba(0, 0, 0, 0.1);
}

.rounded-lg {
  border-radius: 0.5rem;
}

/* تحسين تصميم العناوين */
.text-black {
  color: #000000;
}

.rounded-md {
  border-radius: 0.375rem;
}

.font-bold {
  font-weight: 700;
}

.font-semibold {
  font-weight: 600;
}

.font-medium {
  font-weight: 500;
}

/* تحسين تصميم عناصر InfoRow */
.flex {
  display: flex;
}

.items-center {
  align-items: center;
}

.py-6 {
  padding-top: 1.5rem;
  padding-bottom: 1.5rem;
}

.px-6 {
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}

.mb-12 {
  margin-bottom: 3rem;
}

.mb-6 {
  margin-bottom: 1.5rem;
}

.mb-4 {
  margin-bottom: 1rem;
}

.space-y-2 > :not(:last-child) {
  margin-bottom: 0.5rem;
}

.grid {
  display: grid;
}

.grid-cols-1 {
  grid-template-columns: repeat(1, minmax(0, 1fr));
}

.md\:grid-cols-2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.gap-6 {
  gap: 1.5rem;
}

.mt-4 {
  margin-top: 1rem;
}

.text-center {
  text-align: center;
}

.text-4xl {
  font-size: 2.25rem;
  line-height: 2.5rem;
}

.text-2xl {
  font-size: 1.5rem;
  line-height: 2rem;
}

.text-xl {
  font-size: 1.25rem;
  line-height: 1.75rem;
}

.h-5 {
  height: 1.25rem;
}

.w-5 {
  width: 1.25rem;
}

.h-6 {
  height: 1.5rem;
}

.w-6 {
  width: 1.5rem;
}

.mr-2 {
  margin-right: 0.5rem;
}

.ml-2 {
  margin-left: 0.5rem;
}

.rounded-md {
  border-radius: 0.375rem;
}

.transition {
  transition: all 0.3s ease;
}

.bg-background {
  background-color: #f3f4f6; /* لون خلفية فاتح */
}

.button {
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>
شرح التحسينات
1. توحيد نمط تسمية الحقول
تسميات متسقة: تم التأكد من استخدام snake_case لجميع أسماء الحقول مثل issuing_country, your_name_varies, و award_name_description لضمان الاتساق وسهولة الفهم.
تصحيح التسمية: تم تصحيح تسمية الحقول مثل you_have_a_nicknamecx إلى you_have_a_nickname لتكون متسقة وواضحة.
2. تحسين إدارة الحالة
قائمة المؤهلات المهنية (qualificationList): تم استخدام ref لإدارة قائمة المؤهلات المهنية بشكل فعال. عند إضافة أو حذف مؤهل، يتم تحديث القائمة وأخطاء التحقق بشكل متزامن.
أخطاء التحقق (validationErrors): يتم استخدام reactive لإدارة الأخطاء الخاصة بكل مؤهل مهني. يتم تحديث الأخطاء عند التحقق من صحة كل مؤهل.
3. تحسين التحقق من الصحة
دوال التحقق: تم إنشاء دوال تحقق لكل حقل مثل validateAwardingBody و validateLicenseNumber لضمان صحة البيانات المدخلة.
التحقق من الحقول الشرطية: دالة validateExpirationDate تتحقق من صحة expiration_date فقط إذا كان is_an_expiration_date مفعلاً.
دالة التحقق من مؤهل معين: validateQualification تتحقق من صحة جميع الحقول في مؤهل معين وتحديث أخطاء التحقق.
4. تحسين تجربة المستخدم
تأكيد عند الحذف: عند حذف مؤهل مهني، يتم عرض رسالة تأكيد باستخدام toast.info لتأكيد الحذف.
تعطيل زر الحفظ عند وجود أخطاء: زر الحفظ يتم تعطيله إذا كانت هناك أخطاء في الحقول المطلوبة (:disabled="!isQualificationValid(index)").
إظهار مؤشرات تحميل: يمكن إضافة مؤشرات تحميل عند تنفيذ عمليات الحفظ لتعزيز تجربة المستخدم (مثلاً، إضافة حالة isLoading يمكن استخدامها لتعطيل الأزرار وعرض مؤشر تحرير).
5. تحسين تنظيم الكود وتقسيم المكونات
استخدام مكونات أصغر: تم استخدام مكون InfoRow.vue لعرض تفاصيل المعلومات بشكل منظم وقابل لإعادة الاستخدام. إذا كان هناك الكثير من هذه الأقسام، يمكن التفكير في تقسيمها بشكل أكبر كمكونات فرعية.
6. التعامل مع الطلبات بشكل أفضل
التعامل مع الأخطاء: يتم التعامل مع الأخطاء أثناء عمليات الحفظ باستخدام try...catch وعرض رسائل خطأ مناسبة باستخدام toast.error.
حفظ مؤهل مهني فردي: دالة saveQualification تحفظ المؤهل الحالي بعد التحقق من صحته دون التأثير على المؤهلات الأخرى.
نصائح إضافية
استخدام مكتبات التحقق من الصحة:
Vuelidate أو Yup: يمكن أن تساعد هذه المكتبات في تنظيم عمليات التحقق من الصحة وجعلها أكثر قوة وسهولة في الصيانة.
إدارة الحالة باستخدام Pinia أو Vuex:
إذا كانت لديك بيانات مشتركة بين خطوات متعددة، يمكن أن يكون استخدام Pinia أو Vuex لإدارة الحالة بشكل مركزي مفيدًا.
تحسين الأداء:
تأكد من عدم وجود عمليات إعادة رسم (re-render) غير ضرورية، خاصة عند التعامل مع قوائم طويلة من المؤهلات المهنية.
إضافة Confirm Dialog عند حذف السجلات:
يمكن استخدام مكون Confirm Dialog للتأكد من رغبة المستخدم في حذف سجل مؤهل مهني معين.
زيادة الأمان:
تأكد من التحقق من صحة البيانات المدخلة على جانب الخادم أيضًا لضمان الأمان ومنع إدخال بيانات غير صحيحة.
تحسين رسائل التحقق من الصحة:
يمكن تخصيص رسائل التحقق لتكون أكثر وضوحًا ومساعدة للمستخدم.
خاتمة
من خلال تطبيق التحسينات المقترحة أعلاه، يمكنك تعزيز كفاءة وصيانة نموذج مراجعة المعلومات في تطبيقك، بالإضافة إلى تحسين تجربة المستخدم والتأكد من صحة البيانات المدخلة. هذه التحسينات تساعد في جعل الكود أكثر نظافة، قابلية للصيانة، وأكثر فعالية في التعامل مع الحالات المختلفة أثناء التفاعل مع المستخدم.

إذا كانت هناك أي أسئلة إضافية أو تحتاج إلى مساعدة في جزء معين من التطبيق، فلا تتردد في طرحها!



شكرًا لمشاركتك كود step6.vue الخاص بك. سأقوم بمراجعة الكود وتقديم تحسينات مقترحة لضمان عمل النموذج بكفاءة أعلى، بالإضافة إلى تحسين التحقق من صحة البيانات، إدارة الحالة، وتجربة المستخدم بشكل عام. سأركز على الجوانب التالية:

توحيد نمط تسمية الحقول
تحسين إدارة الحالة
تحسين التحقق من الصحة
تحسين تجربة المستخدم
تحسين تنظيم الكود وتقسيم المكونات
التعامل مع الطلبات بشكل أفضل
1. توحيد نمط تسمية الحقول
من المهم الحفاظ على نمط تسمية متسق لجميع الحقول لضمان سهولة الفهم والصيانة. في الكود الحالي، يتم استخدام نمط snake_case (مثل full_name, email_address, other_languages) وهو ما يعتبر جيدًا. تأكد من استمرار استخدام نفس النمط في جميع الحقول.

نقاط يجب الانتباه إليها:

الاتساق في التسمية: جميع الحقول تتبع نفس نمط التسمية (snake_case) مما يعزز من الاتساق وسهولة الفهم.
التأكد من صحة أسماء الحقول: تجنب وجود أخطاء إملائية أو تسميات غير متسقة مثل you_have_a_nicknamecx. في المثال الحالي، يبدو أن المستخدم قصد أن يكون الاسم you_have_a_nickname.
2. تحسين إدارة الحالة
استخدام reactive و ref بفعالية:
البيانات المحلية: من الأفضل إنشاء نسخة محلية من stepData باستخدام reactive أو ref لتجنب تعديل الـ props مباشرة وتحسين إدارة الحالة.
مزامنة البيانات مع المكون الأب: استخدم watch لمراقبة التغييرات في البيانات المحلية ومزامنتها مع الـ props عبر الأحداث (emit).
مكونات الفرعية:
التقسيم إلى مكونات أصغر: إذا كان هناك العديد من الحقول، يمكن تقسيمها إلى مكونات فرعية مثل InfoRow.vue لعرض المعلومات بشكل منظم وقابل لإعادة الاستخدام.
3. تحسين التحقق من الصحة
توحيد عمليات التحقق من الصحة:
إنشاء دوال تحقق منفصلة لكل حقل لضمان صحة البيانات المدخلة.
التحقق من الحقول الشرطية: مثل expiration_date التي يجب أن تكون مطلوبة فقط إذا كان is_an_expiration_date مفعلاً.
استخدام مكتبة للتحقق من الصحة (اختياري):
مكتبات مثل Vuelidate أو Yup: يمكن أن تساعد في تنظيم التحقق من الصحة وجعله أكثر قوة وسهولة في الصيانة.
4. تحسين تجربة المستخدم
إظهار رسائل التحقق من الصحة:
عرض رسائل أخطاء واضحة ومباشرة تحت الحقول عند وجود أخطاء في التحقق.
تعطيل الأزرار عند وجود أخطاء:
تعطيل زر الحفظ إذا كانت هناك أخطاء في الحقول المطلوبة لمنع حفظ بيانات غير صحيحة.
إظهار مؤشرات تحميل:
إضافة مؤشرات تحميل عند تنفيذ عمليات الحفظ لتحسين تجربة المستخدم وجعل التطبيق أكثر تجاوبًا.
5. تحسين تنظيم الكود وتقسيم المكونات
استخدام مكونات أصغر:
مكونات مثل InfoRow.vue: يمكن استخدامها لتحسين تنظيم الكود وجعله أكثر قابلية لإعادة الاستخدام.
استخدام v-for بفعالية:
التأكد من أن كل عنصر في الحلقة يحتوي على مفتاح فريد (key) لضمان أداء أفضل وتجنب مشاكل إعادة الرسم.
6. التعامل مع الطلبات بشكل أفضل
التعامل مع الأخطاء:
التأكد من التعامل مع الأخطاء بشكل صحيح عند تنفيذ الطلبات وحفظ البيانات، مع عرض رسائل واضحة للمستخدم عند ظهور أخطاء.
تحسين الدوال الخاصة بالحفظ:
حفظ البيانات: التأكد من أن دوال الحفظ تحفظ البيانات بشكل منطقي وتحدث الحالة بشكل صحيح بعد الحفظ.
الكود المحسن لـ step6.vue
إليك نسخة محسنة من step6.vue مع تطبيق التحسينات المقترحة أعلاه:


<!-- step6.vue -->

<template>
  <div class="pt-3 container">
    <h3 class="text-lg font-medium mb-3 text-black">Verification Instructions Request</h3>

    <!-- حقل الاسم الكامل -->
    <div class="lg:grid grid-cols-2 lg:gap-2">
      <FieldContainer>
        <StyledInput
          labelText="Full Name"
          :isMandatory="true"
          infoText="Please enter your full name"
          inputType="text"
          v-model="localStepData.full_name.value"
          @input-change="handleInput('full_name', $event)"
          name="full_name"
          id="FullName"
          :isValid="localStepData.full_name.isValid"
          :validationMessage="localStepData.full_name.validationMessage"
        />
      </FieldContainer>

      <!-- حقل البريد الإلكتروني -->
      <FieldContainer>
        <StyledInput
          labelText="Email Address"
          :isMandatory="true"
          infoText="Please enter your email address"
          inputType="email"
          v-model="localStepData.email_address.value"
          @input-change="handleInput('email_address', $event)"
          name="email_address"
          id="EmailAddress"
          :isValid="localStepData.email_address.isValid"
          :validationMessage="localStepData.email_address.validationMessage"
        />
      </FieldContainer>
    </div>

    <!-- حقل اللغات الأخرى -->
    <div class="lg:grid grid-cols-2 lg:gap-2 mt-4">
      <FieldContainer>
        <Select
          labelText="Other Languages"
          :isMandatory="false"
          infoText="Select other languages"
          name="other_languages"
          id="OtherLanguages"
          v-model="localStepData.other_languages.value"
          @input-change="handleInput('other_languages', $event)"
          :options="languageOptions"
        />
      </FieldContainer>

      <!-- حقل التوقيع الإلكتروني -->
      <FieldContainer>
        <StyledInput
          labelText="Electronic Signature"
          :isMandatory="true"
          infoText="Please provide your electronic signature"
          inputType="text"
          v-model="localStepData.electronic_signature.value"
          @input-change="handleInput('electronic_signature', $event)"
          name="electronic_signature"
          id="ElectronicSignature"
          :isValid="localStepData.electronic_signature.isValid"
          :validationMessage="localStepData.electronic_signature.validationMessage"
        />
      </FieldContainer>
    </div>

    <!-- حقول الموافقة -->
    <div class="lg:grid grid-cols-2 lg:gap-2 mt-4">
      <FieldContainer>
        <CheckBox 
          name="i_agree_to_the_electronic_signature" 
          id="AgreeToElectronicSignature" 
          v-model="localStepData.i_agree_to_the_electronic_signature.value"
          @input-change="handleInput('i_agree_to_the_electronic_signature', $event)"
          :isValid="localStepData.i_agree_to_the_electronic_signature.isValid"
          :validationMessage="localStepData.i_agree_to_the_electronic_signature.validationMessage"
        >
          I agree to the electronic signature
        </CheckBox>
      </FieldContainer>

      <FieldContainer>
        <CheckBox 
          name="i_acknowledge_the_above" 
          id="Acknowledge" 
          v-model="localStepData.i_acknowledge_the_above.value"
          @input-change="handleInput('i_acknowledge_the_above', $event)"
          :isValid="localStepData.i_acknowledge_the_above.isValid"
          :validationMessage="localStepData.i_acknowledge_the_above.validationMessage"
        >
          I acknowledge the above
        </CheckBox>
      </FieldContainer>
    </div>

    <!-- عرض أخطاء النموذج (اختياري) -->
    <div v-if="formErrors.length" class="mt-4">
      <ul class="list-disc list-inside text-red-600">
        <li v-for="(error, index) in formErrors" :key="index">{{ error }}</li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { reactive, watch } from 'vue'
import { defineEmits } from 'vue'
import CheckBox from '../../components/checkBox.vue'
import FieldContainer from '../../components/fieldContainer.vue'
import StyledInput from '../../components/styledInput.vue'
import Select from '../../components/select.vue'
import { validateEmail } from '../../utils/validation' // يمكنك إنشاء ملف للتحقق من الصحة وتجميع الدوال

// تعريف الأحداث
const emit = defineEmits(['update:stepData', 'validateStep'])

// البيانات المحلية مع التحقق من الصحة
const localStepData = reactive({
  full_name: { value: '', isValid: null, validationMessage: null },
  email_address: { value: '', isValid: null, validationMessage: null },
  other_languages: { value: [], isValid: true, validationMessage: null },
  electronic_signature: { value: '', isValid: null, validationMessage: null },
  i_agree_to_the_electronic_signature: { value: false, isValid: null, validationMessage: null },
  i_acknowledge_the_above: { value: false, isValid: null, validationMessage: null },
})

// خيارات اللغات الأخرى
const languageOptions = [
  { value: 'en', text: 'English' },
  { value: 'es', text: 'Spanish' },
  { value: 'fr', text: 'French' },
  // أضف المزيد من اللغات حسب الحاجة
]

// دالة التعامل مع الإدخال
const handleInput = (field, value) => {
  localStepData[field].value = value

  // التحقق من صحة الحقل
  validateField(field)
  
  // إبلاغ المكون الأب بالتغييرات
  emit('update:stepData', { ...localStepData })
  
  // إبلاغ المكون الأب إذا كانت الخطوة صالحة
  emit('validateStep', isFormValid)
}

// دوال التحقق من صحة الحقول
const validateField = (field) => {
  switch (field) {
    case 'full_name':
      if (localStepData.full_name.value.trim() === '') {
        localStepData.full_name.isValid = false
        localStepData.full_name.validationMessage = 'Full Name is required.'
      } else {
        localStepData.full_name.isValid = true
        localStepData.full_name.validationMessage = null
      }
      break
    case 'email_address':
      if (localStepData.email_address.value.trim() === '') {
        localStepData.email_address.isValid = false
        localStepData.email_address.validationMessage = 'Email Address is required.'
      } else if (!validateEmail(localStepData.email_address.value)) {
        localStepData.email_address.isValid = false
        localStepData.email_address.validationMessage = 'Please enter a valid email address.'
      } else {
        localStepData.email_address.isValid = true
        localStepData.email_address.validationMessage = null
      }
      break
    case 'electronic_signature':
      if (localStepData.electronic_signature.value.trim() === '') {
        localStepData.electronic_signature.isValid = false
        localStepData.electronic_signature.validationMessage = 'Electronic Signature is required.'
      } else {
        localStepData.electronic_signature.isValid = true
        localStepData.electronic_signature.validationMessage = null
      }
      break
    case 'i_agree_to_the_electronic_signature':
      if (!localStepData.i_agree_to_the_electronic_signature.value) {
        localStepData.i_agree_to_the_electronic_signature.isValid = false
        localStepData.i_agree_to_the_electronic_signature.validationMessage = 'You must agree to the electronic signature.'
      } else {
        localStepData.i_agree_to_the_electronic_signature.isValid = true
        localStepData.i_agree_to_the_electronic_signature.validationMessage = null
      }
      break
    case 'i_acknowledge_the_above':
      if (!localStepData.i_acknowledge_the_above.value) {
        localStepData.i_acknowledge_the_above.isValid = false
        localStepData.i_acknowledge_the_above.validationMessage = 'You must acknowledge the above.'
      } else {
        localStepData.i_acknowledge_the_above.isValid = true
        localStepData.i_acknowledge_the_above.validationMessage = null
      }
      break
    default:
      break
  }
}

// دالة التحقق من صحة النموذج بالكامل
const isFormValid = computed(() => {
  return (
    localStepData.full_name.isValid &&
    localStepData.email_address.isValid &&
    localStepData.electronic_signature.isValid &&
    localStepData.i_agree_to_the_electronic_signature.isValid &&
    localStepData.i_acknowledge_the_above.isValid
  )
})

// دوال التحقق من صحة النموذج عند تحميل المكون
const validateAllFields = () => {
  Object.keys(localStepData).forEach(field => validateField(field))
}

// مراقبة التغييرات في props.stepData لمزامنتها مع البيانات المحلية
watch(() => props.stepData, (newData) => {
  Object.keys(newData).forEach(field => {
    if (localStepData[field]) {
      localStepData[field].value = newData[field].value
      localStepData[field].isValid = newData[field].isValid
      localStepData[field].validationMessage = newData[field].validationMessage
    }
  })
}, { deep: true })

// تهيئة الحقول عند تحميل المكون
validateAllFields()

</script>

<style scoped>
/* أضف أي تنسيقات اضافية هنا إذا لزم الأمر */
.text-red-600 {
  color: #e0245e;
}
</style>
شرح التحسينات
1. توحيد نمط تسمية الحقول
تسميات متسقة: تم التأكد من استخدام snake_case لجميع أسماء الحقول مثل full_name, email_address, other_languages, electronic_signature, i_agree_to_the_electronic_signature, و i_acknowledge_the_above لضمان الاتساق وسهولة الفهم.
2. تحسين إدارة الحالة
نسخة محلية من البيانات: تم إنشاء نسخة محلية من stepData باستخدام reactive لضمان عدم تعديل الـ props مباشرة وتحسين إدارة الحالة.
مزامنة البيانات مع المكون الأب: تم استخدام watch لمراقبة التغييرات في الـ props.stepData ومزامنتها مع البيانات المحلية localStepData.
استخدام emit: تم استخدام emit لإبلاغ المكون الأب (steps.vue) بالتغييرات في البيانات وإبلاغه إذا كانت الخطوة صالحة (isFormValid).
3. تحسين التحقق من الصحة
دوال تحقق منفصلة: تم إنشاء دوال تحقق منفصلة لكل حقل (validateField) لضمان صحة البيانات المدخلة.
التحقق الشرطي: مثل التحقق من صحة expiration_date فقط إذا كان is_an_expiration_date مفعلاً (تم تضمينه في الخطوات السابقة ولكن ليس في هذا النموذج).
استخدام دوال مساعدة: تم اقتراح استخدام دوال مساعدة مثل validateEmail لتحسين عملية التحقق من صحة البريد الإلكتروني.
4. تحسين تجربة المستخدم
رسائل التحقق الواضحة: تم عرض رسائل التحقق مباشرة تحت الحقول عند وجود أخطاء، مما يساعد المستخدم على فهم الأخطاء وإصلاحها.
تعطيل الأزرار عند وجود أخطاء: تم استخدام computed لتحديد ما إذا كانت جميع الحقول صالحة (isFormValid) وبالتالي يمكن تعطيل أو تفعيل الأزرار بناءً على ذلك.
إظهار رسائل عامة: تم إضافة قسم لعرض رسائل الأخطاء العامة إذا كانت موجودة.
5. تحسين تنظيم الكود وتقسيم المكونات
استخدام مكون InfoRow.vue: تم اقتراح استخدام مكونات فرعية مثل InfoRow.vue لعرض المعلومات بطريقة منظمة وقابلة لإعادة الاستخدام في المكونات الأخرى.
6. التعامل مع الطلبات بشكل أفضل
معالجة الأخطاء: عند حفظ البيانات، يجب التعامل مع الأخطاء بشكل صحيح وعرض رسائل ملائمة للمستخدم عند حدوث مشكلات.
دوال الحفظ: في هذا النموذج، لا يوجد زر حفظ منفصل، حيث يقوم المكون الأب (steps.vue) بإدارة التنقل بين المراحل وحفظ البيانات. إذا كان من الضروري وجود دوال حفظ هنا، يمكن إضافتها وفقًا لاحتياجات المشروع.
تكامل step6.vue مع steps.vue
لضمان تكامل فعال بين step6.vue والمكون الرئيسي steps.vue, يجب التأكد من إدارة البيانات والتحقق من الصحة بشكل متناسق بين المكونات. إليك بعض النقاط الهامة:

تعريف البيانات في steps.vue: تأكد من تعريف step6Data في steps.vue وربطها مع المكون الفرعي step6.vue:

<!-- steps.vue -->
<Step6 
  v-if="processed === 5"
  :stepData="step6Data"
  @update:stepData="updateStepData('step6Data', $event)"
  @validateStep="isStep6Valid = $event"
/>
تعريف دوال إدارة البيانات في steps.vue:

<script setup>
import { reactive, ref } from 'vue'
import Step6 from './stepsSections/step6.vue'
// استيراد المكونات الأخرى...

// تعريف البيانات لكل خطوة
const step6Data = reactive({
  full_name: { value: '', isValid: null, validationMessage: null },
  email_address: { value: '', isValid: null, validationMessage: null },
  other_languages: { value: [], isValid: true, validationMessage: null },
  electronic_signature: { value: '', isValid: null, validationMessage: null },
  i_agree_to_the_electronic_signature: { value: false, isValid: null, validationMessage: null },
  i_acknowledge_the_above: { value: false, isValid: null, validationMessage: null },
})

// دالة تحديث بيانات الخطوة
const updateStepData = (step, newData) => {
  if (step === 'step6Data') {
    Object.assign(step6Data, newData)
  }
  // يمكن إضافة تحديثات لباقي الخطوات
}

// حالة التحقق من صلاحية الخطوة
const isStep6Valid = ref(false)

// دالة لحفظ الخطوة 6
const step6Save = function () {
  if (isStep6Valid.value) {
    // تنفيذ عملية الحفظ
    let converted = objectConvertor(step6Data)
    updateFieldsInRequestList(requestListStep6, converted)
    triggerAlert("تم حفظ الخطوة السادسة بنجاح.", false)
    processed.value++
    stepN.value++
  } else {
    triggerAlert("يرجى تصحيح الأخطاء في البيانات المدخلة.", true)
  }
}

// باقي دوال الخطوات الأخرى...
</script>
نصائح إضافية
إدارة الحالة بشكل مركزي: إذا كان لديك بيانات مشتركة بين خطوات متعددة، يمكنك استخدام مكتبة لإدارة الحالة مثل Pinia أو Vuex لتبسيط إدارة الحالة وتجنب التكرار.
تحسين تجربة المستخدم:
إضافة مؤشرات تحميل: يمكنك إضافة مؤشرات لتحميل البيانات عندما يتم حفظ الخطوات أو تحميلها.
التأكيد عند الحفظ: عرض نافذة تأكيد عند حفظ البيانات بنجاح أو فشل العملية.
تحسين التحقق من الصحة:
استخدام مكتبات تحقق قوية مثل Vuelidate أو Yup لتحسين وتنظيم عملية التحقق من الصحة.
إعادة استخدام المكونات:
تأكد من أن المكونات مثل StyledInput, CheckBox, و Select قابلة لإعادة الاستخدام وقابلة للتخصيص بسهولة حسب الاحتياجات.
خاتمة
من خلال تطبيق التحسينات المقترحة أعلاه، يمكنك تعزيز كفاءة وصيانة نموذج step6.vue في تطبيقك، بالإضافة إلى تحسين تجربة المستخدم والتأكد من صحة البيانات المدخلة. هذه التحسينات تساعد في جعل الكود أكثر نظافة، قابلية للصيانة، وأكثر فعالية في التعامل مع الحالات المختلفة أثناء التفاعل مع المستخدم.

إذا كانت لديك أي أسئلة إضافية أو تحتاج إلى مساعدة في جزء معين من التطبيق، فلا تتردد في طرحها!


// Convertor.ts
function objectConvertor(data:Object):Object{
const result = {};
Object.keys(data).forEach(function(key) {
return result[key] = data[key]["value"];
});
return result;
}

export default objectConvertor;


شكرًا لمشاركة كود convertor.ts الخاص بك. سأقوم بمراجعة الكود الحالي وتقديم تحسينات مقترحة لضمان كفاءة أعلى، تحسين الصيانة، وزيادة الأمان والاعتمادية. بالإضافة إلى ذلك، سأشرح كيفية دمج هذه الدالة بشكل أفضل مع المكونات الأخرى في تطبيقك.

مراجعة الكود الحالي
الكود الحالي

// Convertor.ts
function objectConvertor(data: Object): Object {
    const result = {};
    Object.keys(data).forEach(function(key) {
        return result[key] = data[key]["value"];
    });
    return result;
}

export default objectConvertor;
تحليل الكود الحالي
استخدام النوع Object:
النوع Object في TypeScript هو نوع عام جدًا ولا يوفر أي ضمانات حول البنية الداخلية للبيانات. هذا يمكن أن يؤدي إلى أخطاء وقت التشغيل إذا كانت الخصائص غير موجودة أو غير متوقعة.
التعامل مع الخصائص الغير موجودة:
إذا كانت خاصية value غير موجودة في أي من العناصر داخل data, ستظهر خطأ عند محاولة الوصول إليها (data[key]["value"]).
أداء الكود:
استخدام forEach مع إنشاء كائن جديد يمكن أن يكون أقل كفاءة مقارنة باستخدام تقنيات أخرى مثل reduce.
إعادة كتابة الكود باستخدام return داخل forEach:
استخدام return داخل forEach لا يؤثر على الدالة الخارجية ولكنه يمكن أن يكون محيرًا للقارئ.
تحسينات مقترحة
1. تعريف واجهات (Interfaces) لتحسين الأمان النوعي
تحديد نوع البيانات المدخلة والمخرجة بدقة يعزز الأمان النوعي ويسهل عملية الصيانة.


// types.ts
export interface FormField<T = any> {
    value: T;
    isValid?: boolean;
    validationMessage?: string;
}

export interface FormData {
    [key: string]: FormField;
}
2. إعادة كتابة الدالة باستخدام reduce وتحديد الأنواع بشكل أفضل
استخدام reduce يجعل الكود أكثر وضوحًا وكفاءة.


// convertor.ts
import { FormData, FormField } from './types';

function objectConvertor<T extends FormData>(data: T): { [K in keyof T]: T[K]['value'] } {
    return Object.keys(data).reduce((result, key) => {
        const field = data[key];
        if (field && 'value' in field) {
            result[key] = field.value;
        } else {
            console.warn(`Field "${key}" is missing the "value" property.`);
            result[key] = null; // أو يمكن تحديد قيمة افتراضية أخرى بناءً على احتياجاتك
        }
        return result;
    }, {} as { [K in keyof T]: T[K]['value'] });
}

export default objectConvertor;
3. التعامل مع الخصائص الغير موجودة
تمت إضافة تحذير في حالة عدم وجود خاصية value لضمان اكتشاف الأخطاء مبكرًا.

4. تحسين الأمان النوعي
تحديد الأنواع بشكل دقيق يضمن أن الدالة تعمل فقط مع البنى المتوقعة من البيانات.

5. دمج المعالجة الشرطية إذا لزم الأمر
يمكنك توسيع الدالة لتشمل معالجة خصائص إضافية أو تحولات بيانات أخرى حسب الحاجة.

مثال كامل بعد التحسينات
أنواع البيانات (types.ts)

// types.ts
export interface FormField<T = any> {
    value: T;
    isValid?: boolean;
    validationMessage?: string;
}

export interface FormData {
    [key: string]: FormField;
}
الدالة المحسنة (convertor.ts)

// convertor.ts
import { FormData } from './types';

function objectConvertor<T extends FormData>(data: T): { [K in keyof T]: T[K]['value'] } {
    return Object.keys(data).reduce((result, key) => {
        const field = data[key];
        if (field && 'value' in field) {
            result[key] = field.value;
        } else {
            console.warn(`Field "${key}" is missing the "value" property.`);
            result[key] = null; // أو يمكن تحديد قيمة افتراضية أخرى بناءً على احتياجاتك
        }
        return result;
    }, {} as { [K in keyof T]: T[K]['value'] });
}

export default objectConvertor;
استخدام الدالة المحسنة في مكوناتك
مثال من step1.vue
على سبيل المثال، بعد تحسين convertor.ts, يمكن استخدامه في دالة الحفظ (step1Save) كالتالي:


// step1.vue
<script setup lang="ts">
import { reactive, ref } from 'vue';
import objectConvertor from '../data/convertor';
import { FormData } from '../data/types';
import { updateFieldsInRequestList } from '../data/request';
import validateInputStep1 from '../data/validate/validateInputStep1';
import { useToast } from 'vue-toastification';

// تعريف البيانات
const step1Data = reactive<FormData>({
    employer_name: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
    first_name: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
    middle_name: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
    last_name: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
    suffix: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
    alias_name: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
});

const requestListStep1 = createRequestList(['name', 'first_name', 'last_name']);

// تهيئة Toast
const toast = useToast();

// دوال الحفظ والتحقق
const step1Save = function (): boolean {
    const isValid = validateInputStep1(step1Data);
    if (!isValid) {
        toast.error("يرجى تصحيح الأخطاء في البيانات المدخلة.");
        return false;
    }

    const converted = objectConvertor(step1Data);
    updateFieldsInRequestList(requestListStep1, converted);
    toast.success("تم حفظ الخطوة الأولى بنجاح.");
    return true;
};
</script>
مزايا التحسينات المقترحة
تحسين الأمان النوعي:
باستخدام واجهات محددة، يتم ضمان أن البيانات تحتوي على الخصائص المتوقعة مما يقلل من الأخطاء المستقبلية.
سهولة الصيانة:
الكود الأكثر وضوحًا وتنظيمًا يسهل فهمه وصيانته، خاصة في المشاريع الكبيرة والمتقدمة.
زيادة الكفاءة:
استخدام reduce بدلاً من forEach يمكن أن يكون أكثر كفاءة وأسهل في القراءة.
التعامل مع الأخطاء بشكل أفضل:
التحذيرات المضافة تساعد في اكتشاف المشكلات بسرعة أثناء التطوير.
قابلية التوسع:
الدالة الآن أكثر مرونة ويمكن توسيعها لتشمل تحويلات بيانات إضافية بسهولة.
نصائح إضافية
استخدام مكتبات للتحقق من الصحة:
Vuelidate أو Yup يمكن أن تساعد في تنظيم والتحقق من صحة النماذج بشكل أفضل.
إدارة الحالة باستخدام Pinia أو Vuex:
إذا كانت لديك بيانات مشتركة بين مكونات متعددة، يمكنك استخدام مكتبة لإدارة الحالة مثل Pinia لتحسين تنظيم الحالة وتسهيل الوصول إليها.
إضافة تعليقات وتوثيق:
إضافة تعليقات للكود يمكن أن تسهل فهمه على المطورين الآخرين أو حتى على نفسك في المستقبل.
التحقق الجانبي (Server-Side Validation):
تأكد من التحقق من صحة البيانات المدخلة على جانب الخادم أيضًا لمنع إدخال بيانات غير مرغوب فيها أو ضارة.
اختبار الوحدة (Unit Testing):
كتابة اختبارات وحدة للدالات مثل objectConvertor لضمان أنها تعمل بشكل صحيح تحت مختلف الحالات.
خاتمة
من خلال تطبيق التحسينات المقترحة أعلاه، يمكنك تعزيز كفاءة وصيانة دالة objectConvertor في تطبيقك، وضمان تعامل أكثر أمانًا وفاعلية مع البيانات المدخلة من النماذج. هذه التحسينات تسهم في تحسين جودة الكود بشكل عام وتجعل تطبيقك أسهل في الصيانة والتوسع مستقبلاً.

إذا كان لديك أي أسئلة إضافية أو تحتاج إلى مزيد من المساعدة في أجزاء أخرى من التطبيق، فلا تتردد في طرحها!


// validateInputStep1.ts
function validateInputStep1(data:Object):Boolean{


    let wrongFields:Boolean=false;
Object.keys(data).forEach(function(key) {

    if (key === 'employer_name') {           
        if (data[key]['value'] === "" ) {
            data[key]['isValid'] = false
             data[key]['validationMessage']= 'هذا الحقل فارغ'
             wrongFields=true;
        } 
      }

});
return wrongFields;
}

export default validateInputStep1;


شكرًا لمشاركة كود validateInputStep1.ts الخاص بك. سأقوم بمراجعة هذا الكود وتقديم تحسينات مقترحة لضمان كفاءة أعلى، تعزيز الأمان النوعي، وتحسين قابلية الصيانة. بالإضافة إلى ذلك، سأشرح كيفية توسيع الدالة لدعم المزيد من الحقول والتحقق من صحتها بشكل شامل.

مراجعة الكود الحالي
الكود الحالي

// validateInputStep1.ts
function validateInputStep1(data: Object): Boolean {
    let wrongFields: Boolean = false;
    Object.keys(data).forEach(function (key) {
        if (key === 'employer_name') {
            if (data[key]['value'] === "") {
                data[key]['isValid'] = false;
                data[key]['validationMessage'] = 'هذا الحقل فارغ';
                wrongFields = true;
            }
        }
    });
    return wrongFields;
}

export default validateInputStep1;
تحليل الكود الحالي
استخدام النوع Object:
النوع Object في TypeScript هو نوع عام جدًا ولا يوفر أي ضمانات حول البنية الداخلية للبيانات. هذا قد يؤدي إلى أخطاء وقت التشغيل إذا كانت الخصائص غير موجودة أو غير متوقعة.
التعامل مع الحقول غياب الخصائص:
إذا كانت خاصية employer_name غير موجودة في البيانات، فإن محاولة الوصول إلى data[key]['value'] ستؤدي إلى خطأ.
التحقق محدود لحقول معينة:
الدالة تتحقق فقط من حقل employer_name مما يجعله محدودًا وغير شامل للتحقق من بقية الحقول المطلوبة في step1.vue.
تعديل البيانات مباشرة:
الدالة تعدل الـ data مباشرة، وهذا قد يكون غير مرغوب فيه إذا كانت البيانات تأتي من حالة تفاعلية (reactive) في Vue3، مما قد يؤدي إلى تأثيرات جانبية غير متوقعة.
كفاءة الكود:
استخدام forEach مع return غير ضروري، حيث لا تؤثر قيمة return داخل forEach على الحلقة الخارجية.
تحسينات مقترحة
لتحسين الدالة validateInputStep1, يمكن اتباع الخطوات التالية:

تعريف واجهات (Interfaces) لتحسين الأمان النوعي:
تحديد واجهة (Interface) للبيانات المدخلة لتعزيز الأمان النوعي وضمان أن البيانات تحتوي على الحقول المطلوبة.
توسيع التحقق ليشمل جميع الحقول المطلوبة:
التحقق من صحة جميع الحقول المطلوبة في step1.vue وليس فقط employer_name.
تجنب تعديل البيانات مباشرة:
بدلاً من تعديل البيانات مباشرة، يمكن إنشاء كائن جديد يعرض نتائج التحقق من الصحة.
استخدام reduce لتحسين كفاءة الكود:
يمكن استخدام دالة reduce لبناء كائن يحتوي على نتائج التحقق من الصحة بشكل أكثر فعالية.
إضافة اختبارات الوحدة (Unit Tests):
لضمان أن الدالة تعمل بشكل صحيح تحت مختلف الحالات.
الواجهة المقترحة لتعريف البيانات

// types.ts
export interface FormField<T = any> {
    value: T;
    isValid?: boolean;
    validationMessage?: string;
}

export interface Step1Data {
    employer_name: FormField<string>;
    first_name: FormField<string>;
    last_name: FormField<string>;
    middle_name: FormField<string>;
    suffix: FormField<string>;
    alias_name: FormField<string>;
}
الدالة المحسنة باستخدام reduce وواجهات TypeScript

// validateInputStep1.ts
import { Step1Data } from './types'; // تأكد من مسار الملف الصحيح

interface ValidationResult {
    [key: string]: {
        isValid: boolean;
        validationMessage?: string;
    };
}

function validateInputStep1(data: Step1Data): ValidationResult {
    const requiredFields: string[] = ['employer_name', 'first_name', 'last_name', 'electronic_signature'];
    const validationResult: ValidationResult = {};

    requiredFields.forEach((field) => {
        const fieldData = data[field];
        if (fieldData.value.trim() === "") {
            validationResult[field] = {
                isValid: false,
                validationMessage: 'هذا الحقل فارغ',
            };
        } else {
            validationResult[field] = {
                isValid: true,
            };
        }
    });

    // التحقق من الحقول الشرطية
    if (data['middle_name'].value && data['middle_name'].value.trim() === "") {
        validationResult['middle_name'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        };
    } else {
        validationResult['middle_name'] = {
            isValid: true,
        };
    }

    return validationResult;
}

export default validateInputStep1;
شرح التحسينات
تعريف واجهات TypeScript:
تم تعريف واجهة FormField لتعزيز الأمان النوعي وتحسين وضوح الكود.
تم تعريف واجهة Step1Data التي تمثل البيانات المتوقعة من step1.vue.
توسيع التحقق ليشمل جميع الحقول المطلوبة:
تم إضافة الحقول المطلوبة الأخرى مثل first_name, last_name, وelectronic_signature للتحقق من صحتها.
تجنب تعديل البيانات مباشرة:
الدالة الآن تعيد كائن ValidationResult بدلاً من تعديل البيانات الأصلية مباشرة.
تحسين كفاءة الكود:
استخدام forEach للتحقق من الحقول المطلوبة بشكل فعال ومنظم.
التعامل مع الحقول الشرطية:
تم إضافة تحقق للحقول التي تعتمد على بيانات أخرى، مثل middle_name.
نماذج لدوال تحقق إضافية (اختياري)
لتحسين الدالة وجعلها أكثر مرونة، يمكن إضافة دوال تحقق منفصلة لكل نوع من الحقول:


// validationHelpers.ts
export function isNotEmpty(value: string): boolean {
    return value.trim() !== "";
}

export function isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
}
تحديث دالة validateInputStep1 لاستخدام دوال التحقق المساعدة

// validateInputStep1.ts
import { Step1Data } from './types'; // تأكد من مسار الملف الصحيح
import { isNotEmpty, isValidEmail } from './validationHelpers'; // تأكد من مسار الملف الصحيح

interface ValidationResult {
    [key: string]: {
        isValid: boolean;
        validationMessage?: string;
    };
}

function validateInputStep1(data: Step1Data): ValidationResult {
    const validationResult: ValidationResult = {};

    // التحقق من الحقول المطلوبة
    if (!isNotEmpty(data.employer_name.value)) {
        validationResult['employer_name'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        };
    } else {
        validationResult['employer_name'] = {
            isValid: true,
        };
    }

    if (!isNotEmpty(data.first_name.value)) {
        validationResult['first_name'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        };
    } else {
        validationResult['first_name'] = {
            isValid: true,
        };
    }

    if (!isNotEmpty(data.last_name.value)) {
        validationResult['last_name'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        };
    } else {
        validationResult['last_name'] = {
            isValid: true,
        };
    }

    // التحقق من البريد الإلكتروني
    if (!isNotEmpty(data.email_address.value)) {
        validationResult['email_address'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        };
    } else if (!isValidEmail(data.email_address.value)) {
        validationResult['email_address'] = {
            isValid: false,
            validationMessage: 'البريد الإلكتروني غير صالح',
        };
    } else {
        validationResult['email_address'] = {
            isValid: true,
        };
    }

    // التحقق من الحقول الشرطية
    if (data['middle_name'].value && !isNotEmpty(data.middle_name.value)) {
        validationResult['middle_name'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        };
    } else {
        validationResult['middle_name'] = {
            isValid: true,
        };
    }

    // التحقق من التوقيع الإلكتروني
    if (!isNotEmpty(data.electronic_signature.value)) {
        validationResult['electronic_signature'] = {
            isValid: false,
            validationMessage: 'التوقيع الإلكتروني مطلوب',
        };
    } else {
        validationResult['electronic_signature'] = {
            isValid: true,
        };
    }

    // التحقق من الموافقات
    if (!data.i_agree_to_the_electronic_signature.value) {
        validationResult['i_agree_to_the_electronic_signature'] = {
            isValid: false,
            validationMessage: 'يجب الموافقة على التوقيع الإلكتروني',
        };
    } else {
        validationResult['i_agree_to_the_electronic_signature'] = {
            isValid: true,
        };
    }

    if (!data.i_acknowledge_the_above.value) {
        validationResult['i_acknowledge_the_above'] = {
            isValid: false,
            validationMessage: 'يجب الإقرار بما سبق',
        };
    } else {
        validationResult['i_acknowledge_the_above'] = {
            isValid: true,
        };
    }

    return validationResult;
}

export default validateInputStep1;
دمج الدالة المحسنة مع مكون step1.vue
لضمان عمل الدالة المحسنة بشكل صحيح مع مكون step1.vue, يمكن تحديث دالة الحفظ (step1Save) كما يلي:


// step1.vue
<script setup lang="ts">
import { reactive, ref, watch } from 'vue';
import objectConvertor from '../data/convertor';
import { Step1Data } from '../data/types';
import validateInputStep1 from '../data/validateInputStep1';
import { updateFieldsInRequestList } from '../data/request';
import { useToast } from 'vue-toastification';

// تعريف البيانات
const step1Data = reactive<Step1Data>({
    employer_name: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
    first_name: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
    last_name: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
    middle_name: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
    suffix: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
    alias_name: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
    email_address: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
    electronic_signature: {
        value: '',
        isValid: null,
        validationMessage: null,
    },
    i_agree_to_the_electronic_signature: {
        value: false,
        isValid: null,
        validationMessage: null,
    },
    i_acknowledge_the_above: {
        value: false,
        isValid: null,
        validationMessage: null,
    },
});

// تهيئة Toast
const toast = useToast();

// تعريف دالة الحفظ
const step1Save = function (): boolean {
    const validationResult = validateInputStep1(step1Data);

    // تحديث حالة الحقول بناءً على نتائج التحقق
    Object.keys(validationResult).forEach((field) => {
        step1Data[field].isValid = validationResult[field].isValid;
        step1Data[field].validationMessage = validationResult[field].validationMessage || null;
    });

    // تحديد ما إذا كانت هناك حقول غير صالحة
    const hasErrors = Object.values(validationResult).some(result => result.isValid === false);

    if (hasErrors) {
        toast.error("يرجى تصحيح الأخطاء في البيانات المدخلة.");
        return false;
    } else {
        const converted = objectConvertor(step1Data);
        updateFieldsInRequestList(requestListStep1, converted);
        toast.success("تم حفظ الخطوة الأولى بنجاح.");
        return true;
    }
};
</script>
نقاط يجب مراعاتها
تعريف الحقول في الواجهة (Step1Data):
تأكد من أن جميع الحقول في step1.vue معرفة في واجهة Step1Data لتعزيز الأمان النوعي.
توسيع دالة validateInputStep1:
كما تم توسيع الدالة للتحقق من جميع الحقول المطلوبة، يمكن إضافة المزيد من الحقول والتحقق منها حسب الحاجة.
إعادة استخدام الدوال المساعدة:
استخدام دوال تحقق مساعدة مثل isValidEmail يعزز من قابلية إعادة الاستخدام ويسهل صيانة الدالة.
تقليل التكرار:
إذا كان هناك العديد من الحقول والتي تتطلب التحقق من نفس نوع الشروط، يمكن التفكير في تبسيط الكود لتقليل التكرار.
ملخص التحسينات
الأمان النوعي: باستخدام واجهات TypeScript لتحسين الأمان النوعي وضمان أن البيانات تحتوي على الحقول المتوقعة.
التوسع والشمولية: توسيع الدالة لتشمل جميع الحقول المطلوبة وليس فقط employer_name.
مزايا الأداء والتنظيم: استخدام reduce وforEach بطريقة منسقة لتحسين كفاءة الكود.
تجنب التعديل المباشر: تجنب تعديل البيانات مباشرة في دالة التحقق للحفاظ على تناسق البيانات وإدارة الحالة بشكل أفضل.
التعامل مع الأخطاء: إضافة تحذيرات وتعامل مع الحقول الغائبة لتعزيز استقرارية الدالة.
نصائح إضافية
استخدام مكتبات التحقق من الصحة:
Vuelidate أو Yup: هذه المكتبات توفر أدوات قوية لتنظيم والتحقق من صحة النماذج بشكل أفضل، مما يسهل عملية التطوير ويحسن من قابلية الصيانة.
إدارة الحالة مركزيًا:
Pinia أو Vuex: إذا كنت تعمل على مشروع كبير مع العديد من المكونات والبيانات المشتركة، يمكن استخدام Pinia أو Vuex لإدارة الحالة بشكل مركزي، مما يسهل تتبع وتحديث البيانات عبر التطبيقات.
إضافة اختبارات الوحدة (Unit Tests):
كتابة اختبارات وحدة لدوال التحقق من الصحة مثل validateInputStep1 لضمان عملها بشكل صحيح تحت مختلف الحالات.
تجنب التكرار في الدوال:
إذا كان هناك مجموعة من الحقول التي تتطلب نفس نوع التحقق، يمكن إنشاء دالة مشتركة لتقليل التكرار وتحسين التنظيم.
توفير تغذية راجعة فورية للمستخدم:
استخدام التعابير الشرطية في القوالب (v-if) لعرض رسائل الأخطاء مباشرة تحت الحقول لتوفير تغذية راجعة فورية للمستخدم.
خاتمة
من خلال تطبيق التحسينات المقترحة أعلاه، يمكنك تعزيز كفاءة ودقة دالة التحقق من صحة البيانات في مشروعك، مما يسهم في تحسين تجربة المستخدم وضمان إدخال بيانات صحيحة وموثوقة. هذه التحسينات تجعل الكود أكثر نظافة، قابلية للصيانة، وأكثر أمانًا من خلال استخدام ميزات TypeScript بشكل فعال.

إذا كانت لديك أي أسئلة إضافية أو تحتاج إلى مزيد من المساعدة في أجزاء أخرى من المشروع، فلا تتردد في طرحها!


// request.js
// import router from '@/router'
import { createListResource ,createDocumentResource } from 'frappe-ui'
import { session } from './session';

// دالة لإنشاء مورد الطلبات مع الحقول المطلوبة
export function createRequestList(fields) {
return createListResource({
doctype: 'Verification Instructions Request',
fields, // استخدم الحقول الممررة من المعامل
filters: { user_id: session.user },
auto: true,
pageLength: 1,
onSuccess(data) {
console.log("Data retrieved successfully:", data);
},
setValue: {
onSuccess(data) {
console.log("Data update successful.");
},
onError(error) {
console.error("Error updating data:", error);
}
},
});
}

export function updateFieldsInRequestList(requestList, updatedFields) {
if (!requestList || !requestList.data || !requestList.data[0]) {
console.error("Request list data is not available");
return;
}


const documentName = requestList.data[0].name;

requestList.setValue.submit({
  name: documentName,
  ...updatedFields, // تمرير الحقول المحدثة
}).then(response => {
  console.log("Data updated successfully:", response);
}).catch(error => {
  console.error("Error updating data:", error);
});
}

// General function for document operations
export function useDocumentResource(name = '') {
if (!name) {
console.warn("Document name is not provided.");
}
return createDocumentResource({
doctype: "Verification Instructions Request",
name: name,
// auto: true
});
}


شكرًا لمشاركة كود request.js الخاص بك. سأقوم بمراجعته وتقديم بعض التحسينات المقترحة لضمان كفاءة أعلى، تعزيز الأمان النوعي، وتحسين قابلية الصيانة. بالإضافة إلى ذلك، سأوضح كيفية تحويل الملف إلى TypeScript لتعزيز الأمان النوعي والتفاعل أفضل مع باقي أجزاء تطبيقك المصممة بـ Vue3 وTypeScript.

مراجعة الكود الحالي
الكود الحالي

// request.js
// import router from '@/router'
import { createListResource, createDocumentResource } from 'frappe-ui'
import { session } from './session'

// دالة لإنشاء مورد الطلبات مع الحقول المطلوبة
export function createRequestList(fields) {
    return createListResource({
        doctype: 'Verification Instructions Request',
        fields, // استخدم الحقول الممررة من المعامل
        filters: { user_id: session.user },
        auto: true,
        pageLength: 1,
        onSuccess(data) {
            console.log("Data retrieved successfully:", data);
        },
        setValue: {
            onSuccess(data) {
                console.log("Data update successful.");
            },
            onError(error) {
                console.error("Error updating data:", error);
            }
        },
    });
}

export function updateFieldsInRequestList(requestList, updatedFields) {
    if (!requestList || !requestList.data || !requestList.data[0]) {
        console.error("Request list data is not available");
        return;
    }

    const documentName = requestList.data[0].name;

    requestList.setValue.submit({
        name: documentName,
        ...updatedFields, // تمرير الحقول المحدثة
    }).then(response => {
        console.log("Data updated successfully:", response);
    }).catch(error => {
        console.error("Error updating data:", error);
    });
}

// General function for document operations
export function useDocumentResource(name = '') {
    if (!name) {
        console.warn("Document name is not provided.");
    }
    return createDocumentResource({
        doctype: "Verification Instructions Request",
        name: name,
        // auto: true
    });
}
تحليل الكود الحالي
استخدام النوع العام Object:
النوع Object في JavaScript (وأيضًا في TypeScript) هو نوع عام جدًا ولا يوفر ضمانات حول البنية الداخلية للبيانات. هذا قد يؤدي إلى أخطاء وقت التشغيل إذا كانت الخصائص غير موجودة أو غير متوقعة.
التحقق المحدود:
الدالة updateFieldsInRequestList تتحقق فقط مما إذا كانت requestList تحتوي على بيانات وتقوم بتحديث أول سجل (data[0]). هذا قد لا يكون ملائمًا دائمًا إذا كان لديك أكثر من سجل أو إذا كان الطلب يتطلب تحديث سجلات معينة بناءً على معايير أخرى.
إدارة الأخطاء:
عند استخدام setValue.submit, يتم التعامل مع الأخطاء بطريقة بسيطة عبر .then و .catch. يمكن تحسين ذلك لجعل التعامل مع الأخطاء أكثر فعالية.
إعادة كتابة createRequestList:
الدالة createRequestList تعتمد على مكتبة frappe-ui وتهيئ الموارد بناءً على النتائج تلقائيًا (auto: true). يجب التأكد من أن هذا سلوك مناسب لمتطلبات التطبيق الخاص بك.
التحسينات المقترحة
لتحسين الكود الحالي، سنقوم بالتحويل إلى TypeScript لتعزيز الأمان النوعي، إضافة تعريفات للواجهات (interfaces) للتحكم في بنية البيانات، وتحسين إدارة الأخطاء.

1. تحويل الملف إلى TypeScript
أولاً، سنقوم بتحويل request.js إلى request.ts للاستفادة من ميزات TypeScript مثل الأمان النوعي والتحقق من صحة البيانات أثناء التجميع.

2. تعريف واجهات TypeScript
سنقوم بتعريف واجهات (interfaces) تمثل بنية البيانات المتوقعة من Verification Instructions Request. هذا يعزز الأمان النوعي ويساعد في جعل الكود أكثر وضوحًا وسهولة في الصيانة.

إنشاء ملف types.ts

// types.ts

export interface RequestData {
    name: string;
    user_id: string;
    // أضف المزيد من الحقول حسب الحاجة لكل مرحلة في المشروع
    employer_name?: string;
    first_name?: string;
    last_name?: string;
    middle_name?: string;
    suffix?: string;
    alias_name?: string;
    // ... حدد باقي الحقول الأخرى
}

export interface UpdateFields {
    [key: string]: any;
}
3. تحسين دالة createRequestList
سنقوم بتحسين الدالة لتكون أكثر نوعية باستخدام TypeScript، مما يعزز الأمان النوعي ويجعل التفاعل مع مكتبة frappe-ui أكثر فعالية.

request.ts المحسن

// request.ts
import { createListResource, createDocumentResource } from 'frappe-ui'
import { session } from './session'
import { RequestData, UpdateFields } from './types'

interface CreateListResourceOptions {
    doctype: string;
    fields: string[];
    filters: { [key: string]: any };
    auto: boolean;
    pageLength: number;
    onSuccess: (data: RequestData[]) => void;
    setValue: {
        onSuccess: (data: any) => void;
        onError: (error: any) => void;
    };
}

/**
 * Creates a list resource to fetch 'Verification Instructions Request' documents.
 * @param fields - Array of field names to retrieve.
 * @returns ListResource<RequestData>
 */
export function createRequestList(fields: string[]) {
    const options: CreateListResourceOptions = {
        doctype: 'Verification Instructions Request',
        fields,
        filters: { user_id: session.user },
        auto: true,
        pageLength: 1,
        onSuccess(data: RequestData[]) {
            console.log("Data retrieved successfully:", data);
        },
        setValue: {
            onSuccess(data: any) {
                console.log("Data update successful.");
            },
            onError(error: any) {
                console.error("Error updating data:", error);
            },
        },
    };

    return createListResource<RequestData>(options);
}

/**
 * Updates fields in the 'Verification Instructions Request' list.
 * @param requestList - The list resource to update.
 * @param updatedFields - An object containing the fields to update.
 */
export function updateFieldsInRequestList(requestList: any, updatedFields: UpdateFields) {
    if (!requestList || !requestList.data || requestList.data.length === 0) {
        console.error("Request list data is not available");
        return;
    }

    const documentName: string = requestList.data[0].name;

    requestList.setValue.submit({
        name: documentName,
        ...updatedFields,
    }).then((response: any) => {
        console.log("Data updated successfully:", response);
    }).catch((error: any) => {
        console.error("Error updating data:", error);
    });
}

/**
 * General function to create a document resource.
 * @param name - (Optional) The name of the document.
 * @returns DocumentResource<RequestData>
 */
export function useDocumentResource(name: string = '') {
    if (!name) {
        console.warn("Document name is not provided.");
    }
    return createDocumentResource<RequestData>({
        doctype: "Verification Instructions Request",
        name: name,
        // auto: true
    });
}
4. تحسين دالة updateFieldsInRequestList
سنضيف تحققات إضافية لضمان أن الحقول المحدثة تتوافق مع البنية المتوقعة، وتحسين إدارة الأخطاء باستخدام TypeScript.

5. تحسين إدارة الأخطاء والإبلاغ عنها
بدلاً من الاعتماد على console.log و console.error, يمكننا استخدام نظام للإشعارات أو التعامل مع الأخطاء بطريقة أكثر فاعلية. في سياق مشروعك، يبدو أنك تستخدم vue-toastification لعرض الرسائل، لذلك يمكننا دمج ذلك هنا أيضًا.

تحديث updateFieldsInRequestList لتحسين إدارة الأخطاء

// request.ts (استكمال للتحسين)

import { useToast } from 'vue-toastification'
import { Ref } from 'vue'

/**
 * Updates fields in the 'Verification Instructions Request' list.
 * @param requestList - The list resource to update.
 * @param updatedFields - An object containing the fields to update.
 * @param toast - Toast instance for notifications.
 */
export function updateFieldsInRequestList(requestList: any, updatedFields: UpdateFields, toast: ReturnType<typeof useToast>) {
    if (!requestList || !requestList.data || requestList.data.length === 0) {
        console.error("Request list data is not available");
        toast.error("فشل في العثور على بيانات الطلب المتاحة.");
        return;
    }

    const documentName: string = requestList.data[0].name;

    requestList.setValue.submit({
        name: documentName,
        ...updatedFields,
    }).then((response: any) => {
        console.log("Data updated successfully:", response);
        toast.success("تم تحديث البيانات بنجاح.");
    }).catch((error: any) => {
        console.error("Error updating data:", error);
        toast.error("حدث خطأ أثناء تحديث البيانات.");
    });
}
6. دمج المكتبات والأدوات الأخرى
إذا كنت تستخدم مكتبة vue-toastification لعرض الرسائل للمستخدمين، يجب تضمين ذلك في دوال الطلبات لتحسين تجربة المستخدم من خلال إبلاغهم بحالة العمليات.

مثال على الاستخدام في مكون Vue
لنفترض أنك تريد استخدام createRequestList و updateFieldsInRequestList في أحد المكونات مثل steps.vue. إليك كيف يمكنك دمج هذه الدوال بشكل فعال.

خطوات دمج request.ts مع مكون Vue (steps.vue)
استيراد الدوال من request.ts

// steps.vue
<script setup lang="ts">
// استيراد المكتبات والدوال المطلوبة
import { ref, reactive, computed } from 'vue'
import { createRequestList, updateFieldsInRequestList } from '../data/request'
import { useToast } from 'vue-toastification'
import objectConvertor from '../data/convertor'
import validateInputStep1 from '../data/validate/validateInputStep1'
import Step1 from './stepsSections/step1.vue'
// استيراد مكونات الخطوات الأخرى...

// تهيئة Toast
const toast = useToast()

// تعريف الحالة
const processed = ref(0)
const stepN = ref(1)

// تعريف بيانات الخطوة 1
const step1Data = reactive({
    employer_name: { value: '', isValid: null, validationMessage: null },
    first_name: { value: '', isValid: null, validationMessage: null },
    last_name: { value: '', isValid: null, validationMessage: null },
    middle_name: { value: '', isValid: null, validationMessage: null },
    suffix: { value: '', isValid: null, validationMessage: null },
    alias_name: { value: '', isValid: null, validationMessage: null },
    email_address: { value: '', isValid: null, validationMessage: null },
    electronic_signature: { value: '', isValid: null, validationMessage: null },
    i_agree_to_the_electronic_signature: { value: false, isValid: null, validationMessage: null },
    i_acknowledge_the_above: { value: false, isValid: null, validationMessage: null },
})

// إنشاء مورد الطلبات للقائمة مع الحقول المطلوبة
const requestListStep1 = createRequestList(['employer_name', 'first_name', 'last_name', 'email_address', 'electronic_signature', 'i_agree_to_the_electronic_signature', 'i_acknowledge_the_above'])

// دالة لحفظ الخطوة 1
const step1Save = () => {
    // التحقق من صحة البيانات
    const validationResult = validateInputStep1(step1Data)

    // تحديث حالة الحقول بناءً على نتائج التحقق
    Object.keys(validationResult).forEach((field) => {
        step1Data[field as keyof typeof step1Data].isValid = validationResult[field].isValid
        step1Data[field as keyof typeof step1Data].validationMessage = validationResult[field].validationMessage || null
    })

    // تحديد ما إذا كانت هناك حقول غير صالحة
    const hasErrors = Object.values(validationResult).some(result => result.isValid === false)

    if (hasErrors) {
        toast.error("يرجى تصحيح الأخطاء في البيانات المدخلة.")
        return false
    } else {
        const converted = objectConvertor(step1Data)
        updateFieldsInRequestList(requestListStep1, converted, toast)
        return true
    }
}

// دوال التنقل
const previousStep = () => {
    if (processed.value > 0) {
        processed.value--
        stepN.value--
    }
}

const nextStep = () => {
    switch (processed.value) {
        case 0:
            if (step1Save()) {
                processed.value++
                stepN.value++
            }
            break
        // إضافة حالات لدوال الحفظ الأخرى (step2Save، step3Save، إلخ...)
        default:
            processed.value++
            stepN.value++
    }
}
</script>
شرح الدمج
تهيئة واستخدام Toast:
استخدمنا useToast من vue-toastification لإظهار رسائل نجاح أو خطأ للمستخدم بناءً على نتائج عمليات الحفظ.
تحميل بيانات قائمة الطلبات:
استخدمنا createRequestList لتحميل بيانات الحقول المطلوبة للخطوة الأولى من Verification Instructions Request.
تحديث وتحليل البيانات:
بعد التحقق من صحة البيانات باستخدام validateInputStep1, نقوم بتحديث حالة الحقول (isValid و validationMessage) وعرض رسائل الأخطاء أو النجاح باستخدام Toast.
إذا كانت البيانات صالحة، نستخدم updateFieldsInRequestList لتحديث الوثيقة في القائمة.
إدارة التنقل بين الخطوات:
استخدمنا دوال previousStep و nextStep للتحكم في التنقل بين المراحل المختلفة. في دالة nextStep, نقوم بالتحقق من صحة البيانات قبل الانتقال إلى الخطوة التالية.
ملف validationHelpers.ts
يمكنك إنشاء ملف يحتوي على دوال مساعدة للتحقق من صحة البيانات لتعزيز قابلية إعادة الاستخدام وتنظيم الكود.


// validationHelpers.ts

/**
 * يتحقق مما إذا كان النص غير فارغ بعد إزالة المسافات.
 * @param value - النص للتحقق منه.
 * @returns بولاين يشير ما إذا كان النص غير فارغ.
 */
export function isNotEmpty(value: string): boolean {
    return value.trim() !== ""
}

/**
 * يتحقق من صحة عنوان البريد الإلكتروني باستخدام تعبير نمطي.
 * @param email - عنوان البريد الإلكتروني للتحقق منه.
 * @returns بولاين يشير ما إذا كان البريد الإلكتروني صالحًا.
 */
export function isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    return emailRegex.test(email)
}

/**
 * يتحقق مما إذا كان الرقم مرضيًا وفقًا لتعبير نمطي معين.
 * @param phone - الرقم للتحقق منه.
 * @returns بولاين يشير ما إذا كان الرقم صالحًا.
 */
export function isValidPhone(phone: string): boolean {
    const phoneRegex = /^\+?[0-9\-]{7,15}$/
    return phoneRegex.test(phone)
}
تحديث دالة التحقق من صحة الخطوة الأولى لتستخدم المساعدين

// validateInputStep1.ts
import { Step1Data } from './types'
import { isNotEmpty, isValidEmail, isValidPhone } from './validationHelpers'

interface ValidationResult {
    [key: string]: {
        isValid: boolean;
        validationMessage?: string;
    };
}

function validateInputStep1(data: Step1Data): ValidationResult {
    const validationResult: ValidationResult = {}

    // التحقق من الحقول المطلوبة
    if (!isNotEmpty(data.employer_name.value)) {
        validationResult['employer_name'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        }
    } else {
        validationResult['employer_name'] = { isValid: true }
    }

    if (!isNotEmpty(data.first_name.value)) {
        validationResult['first_name'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        }
    } else {
        validationResult['first_name'] = { isValid: true }
    }

    if (!isNotEmpty(data.last_name.value)) {
        validationResult['last_name'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        }
    } else {
        validationResult['last_name'] = { isValid: true }
    }

    // التحقق من البريد الإلكتروني
    if (!isNotEmpty(data.email_address.value)) {
        validationResult['email_address'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        }
    } else if (!isValidEmail(data.email_address.value)) {
        validationResult['email_address'] = {
            isValid: false,
            validationMessage: 'عنوان البريد الإلكتروني غير صالح',
        }
    } else {
        validationResult['email_address'] = { isValid: true }
    }

    // التحقق من التوقيع الإلكتروني
    if (!isNotEmpty(data.electronic_signature.value)) {
        validationResult['electronic_signature'] = {
            isValid: false,
            validationMessage: 'التوقيع الإلكتروني مطلوب',
        }
    } else {
        validationResult['electronic_signature'] = { isValid: true }
    }

    // التحقق من الموافقات
    if (!data.i_agree_to_the_electronic_signature.value) {
        validationResult['i_agree_to_the_electronic_signature'] = {
            isValid: false,
            validationMessage: 'يجب الموافقة على التوقيع الإلكتروني',
        }
    } else {
        validationResult['i_agree_to_the_electronic_signature'] = { isValid: true }
    }

    if (!data.i_acknowledge_the_above.value) {
        validationResult['i_acknowledge_the_above'] = {
            isValid: false,
            validationMessage: 'يجب الإقرار بما سبق',
        }
    } else {
        validationResult['i_acknowledge_the_above'] = { isValid: true }
    }

    return validationResult
}

export default validateInputStep1
خيارات أخرى لتحسين request.ts
تعريف واجهات ListResource و DocumentResource
إذا كانت مكتبة frappe-ui توفر أنواع TypeScript موثقة، فمن الأفضل استخدامها. إذا لم توفر، يمكنك تعريف أنواعك الخاصة لتعزيز الأمان النوعي.


// request.ts - إضافة الأنواع المخصصة إذا لم تكن مكتبة frappe-ui توفرها

interface DocumentResourceOptions<T> {
    doctype: string;
    name: string;
    auto?: boolean;
}

interface ListResourceOptions<T> {
    doctype: string;
    fields: string[];
    filters: { [key: string]: any };
    auto: boolean;
    pageLength: number;
    onSuccess: (data: T[]) => void;
    setValue: {
        onSuccess: (data: any) => void;
        onError: (error: any) => void;
    };
}
تعزيز الأمان النوعي
تأكد من أن بيانات session.user موجودة قبل استخدامه في الفلاتر.


// request.ts - إضافة تحقق من وجود المستخدم
export function createRequestList(fields: string[]) {
    if (!session.user) {
        throw new Error("User session is not available.");
    }

    const options: CreateListResourceOptions = {
        doctype: 'Verification Instructions Request',
        fields,
        filters: { user_id: session.user },
        auto: true,
        pageLength: 1,
        onSuccess(data: RequestData[]) {
            console.log("Data retrieved successfully:", data);
        },
        setValue: {
            onSuccess(data: any) {
                console.log("Data update successful.");
            },
            onError(error: any) {
                console.error("Error updating data:", error);
            },
        },
    };

    return createListResource<RequestData>(options);
}
تحويل الدوال الأخرى إلى TypeScript
دالة useDocumentResource المحسنة

// request.ts - إكمال تحويل useDocumentResource
export function useDocumentResource(name: string = ''): any { // يمكنك تحديد نوع `DocumentResource<RequestData>` إذا كانت مكتبة frappe-ui تدعمه
    if (!name) {
        console.warn("Document name is not provided.");
    }
    return createDocumentResource<RequestData>({
        doctype: "Verification Instructions Request",
        name: name,
        // auto: true
    });
}
إضافة حماية ضد الحقول الغير متوقعة

// request.ts - تحسين دالة `updateFieldsInRequestList`
export function updateFieldsInRequestList(requestList: any, updatedFields: UpdateFields, toast: ReturnType<typeof useToast>) {
    if (!requestList || !requestList.data || requestList.data.length === 0) {
        console.error("Request list data is not available");
        toast.error("فشل في العثور على بيانات الطلب المتاحة.");
        return;
    }

    const documentName: string = requestList.data[0].name;

    requestList.setValue.submit({
        name: documentName,
        ...updatedFields,
    }).then((response: any) => {
        console.log("Data updated successfully:", response);
        toast.success("تم تحديث البيانات بنجاح.");
    }).catch((error: any) => {
        console.error("Error updating data:", error);
        toast.error("حدث خطأ أثناء تحديث البيانات.");
    });
}
نصائح إضافية
إدارة الحالة المركزة:
إذا كان لديك بيانات مشتركة بين مكونات متعددة، فكر في استخدام مكتبة لإدارة الحالة مثل Pinia أو Vuex. هذا يسهل تتبع وتحديث البيانات عبر تطبيقك بشكل مركزي.
استخدام مكتبات للتحقق من الصحة:
Vuelidate أو Yup يمكن أن تساعد في تنظيم عمليات التحقق من الصحة وتحسينها، مما يجعل الكود أكثر نظافة وقابلية للصيانة.
إضافة اختبارات الوحدة (Unit Tests):
كتابة اختبارات لوظائف مثل objectConvertor وvalidateInputStep1 لضمان أنها تعمل بشكل صحيح تحت مختلف الحالات.
تعزيز تجربة المستخدم:
إضافة مؤشرات تحميل أثناء عمليات الحفظ وتحسين واجهة المستخدم لجعلها أكثر تجاوبًا وودودًا.
التعامل مع العمليات غير المتزامنة بشكل فعّال:
إذا كانت هناك عمليات متكررة أو معقدة، ضع في اعتبارك استخدام async/await بشكل أكثر فعالية لتحسين القراءة وإدارة الأخطاء.
خاتمة
باستخدام التحسينات المقترحة أعلاه، يمكنك تعزيز كفاءة وأمان ووضوح كود request.ts الخاص بك، مما يسهم في تحسين تجربة المستخدم وضمان إدخال بيانات صحيحة وموثوقة. تحويل الكود إلى TypeScript واستخدام ميزاته يعزز الأمان النوعي ويسهل عملية الصيانة والتوسع المستقبلي لتطبيقك.

إذا كانت لديك أي أسئلة إضافية أو تحتاج إلى مزيد من المساعدة في أجزاء أخرى من المشروع، فلا تتردد في طرحها!


بعد ان قمت بالاطلاع على محواء الملفات التي قمت بارسالها لك حسب طلبك اريد مساعدتك في الاشياء التالية : 1- اريد ان يتم تخزين name الخاص با Verification Instructions Request في متغير عام يمكن الوصول اليه من اي مكان في البرنامج 2 - اريد انا يتم تخزين بيانات كل الخطوات محليا بيث تتم عملية الحفض النهائية وارسل البيانات الى Doctype في المرحلة step6 عند الظغط على زر Accept المتواجد في الصفحة الرائيسية steps بحيت لا يتم فقدان بيانات المراحل عند التنقل بين المراحل


شكرًا لمشاركتك الملفات المختلفة من مشروعك. سأقوم بمساعدتك في النقاط التالية:

تخزين name الخاص بـ Verification Instructions Request في متغير عام يمكن الوصول إليه من أي مكان في البرنامج.
تخزين بيانات كل الخطوات محليًا حتى تتم عملية الحفظ النهائية وإرسال البيانات إلى Doctype في المرحلة step6 عند الضغط على زر "Accept"، مع ضمان عدم فقدان بيانات المراحل عند التنقل بينها.
الخطوة 1: إعداد Pinia لإدارة الحالة العالمية
من الأفضل استخدام Pinia لإدارة الحالة العالمية في تطبيقك، حيث أنها تتكامل بشكل جيد مع Vue3 وتوفر واجهة برمجية بسيطة وفعّالة.

1.1. تثبيت Pinia
إذا لم تكن قد قمت بتثبيته بالفعل، يمكنك تثبيت Pinia عبر npm:


npm install pinia
1.2. تهيئة Pinia في مشروعك
في ملف main.ts الخاص بتطبيقك، قم بإعداد Pinia:


// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { createPinia } from 'pinia';

const app = createApp(App);
const pinia = createPinia();

app.use(pinia);
app.mount('#app');
الخطوة 2: تعريف متجر Pinia لتخزين بيانات Verification Instructions Request
سنقوم بإنشاء متجر Pinia لتخزين documentName وبيانات جميع الخطوات.

2.1. إنشاء ملف المتجر
أنشئ ملفًا جديدًا في src/stores/verificationRequest.ts:


// src/stores/verificationRequest.ts
import { defineStore } from 'pinia';

interface Step1Data {
    employer_name: string;
    first_name: string;
    last_name: string;
    middle_name?: string;
    suffix?: string;
    alias_name?: string;
    email_address: string;
    electronic_signature: string;
    i_agree_to_the_electronic_signature: boolean;
    i_acknowledge_the_above: boolean;
}

interface Step2Data {
    education_information: any[]; // عوض "any" بأنواع بياناتك الخاصة
}

interface Step3Data {
    employment_history: any[]; // عوض "any" بأنواع بياناتك الخاصة
}

interface Step4Data {
    professional_qualification: any[]; // عوض "any" بأنواع بياناتك الخاصة
}

interface Step5Data {
    // أضف الحقول المطلوبة لمراجعة المعلومات
}

interface Step6Data {
    full_name: string;
    email_address: string;
    other_languages: string[];
    electronic_signature: string;
    i_agree_to_the_electronic_signature: boolean;
    i_acknowledge_the_above: boolean;
}

interface VerificationRequestStoreState {
    documentName: string | null;
    step1: Step1Data;
    step2: Step2Data;
    step3: Step3Data;
    step4: Step4Data;
    step5: Step5Data;
    step6: Step6Data;
}

export const useVerificationRequestStore = defineStore('verificationRequest', {
    state: (): VerificationRequestStoreState => ({
        documentName: null,
        step1: {
            employer_name: '',
            first_name: '',
            last_name: '',
            middle_name: '',
            suffix: '',
            alias_name: '',
            email_address: '',
            electronic_signature: '',
            i_agree_to_the_electronic_signature: false,
            i_acknowledge_the_above: false,
        },
        step2: {
            education_information: [],
        },
        step3: {
            employment_history: [],
        },
        step4: {
            professional_qualification: [],
        },
        step5: {
            // أضف الحقول المطلوبة لمراجعة المعلومات
        },
        step6: {
            full_name: '',
            email_address: '',
            other_languages: [],
            electronic_signature: '',
            i_agree_to_the_electronic_signature: false,
            i_acknowledge_the_above: false,
        },
    }),
    actions: {
        setDocumentName(name: string) {
            this.documentName = name;
        },
        // دوال تحديث بيانات الخطوات
        updateStep1(data: Partial<Step1Data>) {
            this.step1 = { ...this.step1, ...data };
        },
        updateStep2(data: Partial<Step2Data>) {
            this.step2 = { ...this.step2, ...data };
        },
        updateStep3(data: Partial<Step3Data>) {
            this.step3 = { ...this.step3, ...data };
        },
        updateStep4(data: Partial<Step4Data>) {
            this.step4 = { ...this.step4, ...data };
        },
        updateStep5(data: Partial<Step5Data>) {
            this.step5 = { ...this.step5, ...data };
        },
        updateStep6(data: Partial<Step6Data>) {
            this.step6 = { ...this.step6, ...data };
        },
        // إعادة تعيين المتجر
        resetStore() {
            this.documentName = null;
            this.step1 = {
                employer_name: '',
                first_name: '',
                last_name: '',
                middle_name: '',
                suffix: '',
                alias_name: '',
                email_address: '',
                electronic_signature: '',
                i_agree_to_the_electronic_signature: false,
                i_acknowledge_the_above: false,
            };
            this.step2 = { education_information: [] };
            this.step3 = { employment_history: [] };
            this.step4 = { professional_qualification: [] };
            this.step5 = { /* أضف الحقول المطلوبة */ };
            this.step6 = {
                full_name: '',
                email_address: '',
                other_languages: [],
                electronic_signature: '',
                i_agree_to_the_electronic_signature: false,
                i_acknowledge_the_above: false,
            };
        },
    },
});
الخطوة 3: تحديث دالة convertor.ts لجعلها متوافقة مع TypeScript وأكثر أمانًا نوعيًا
3.1. تعريف واجهات TypeScript
قم بتحديد واجهات للبيانات المدخلة والمخرجة لتعزيز الأمان النوعي.


// src/data/types.ts
export interface FormField<T = any> {
    value: T;
    isValid?: boolean;
    validationMessage?: string;
}

export interface Step1Data {
    employer_name: FormField<string>;
    first_name: FormField<string>;
    last_name: FormField<string>;
    middle_name?: FormField<string>;
    suffix?: FormField<string>;
    alias_name?: FormField<string>;
    email_address: FormField<string>;
    electronic_signature: FormField<string>;
    i_agree_to_the_electronic_signature: FormField<boolean>;
    i_acknowledge_the_above: FormField<boolean>;
    // أضف حقول الخطوات الأخرى هنا
}
3.2. تحديث convertor.ts

// src/data/convertor.ts
import { FormData } from './types';

function objectConvertor<T extends FormData>(data: T): { [K in keyof T]: T[K]['value'] } {
    const result: { [K in keyof T]: T[K]['value'] } = {} as any;
    Object.keys(data).forEach((key) => {
        const typedKey = key as keyof T;
        const field = data[typedKey];
        if (field && 'value' in field) {
            result[typedKey] = field.value;
        } else {
            console.warn(`Field "${key}" is missing the "value" property.`);
            result[typedKey] = null; // أو تحديد قيمة افتراضية أخرى حسب الحاجة
        }
    });
    return result;
}

export default objectConvertor;
الخطوة 4: تحديث دالة validateInputStep1.ts لتحسين التحقق من الصحة
4.1. تعديل validateInputStep1.ts

// src/data/validate/validateInputStep1.ts
import { Step1Data } from '../types';

interface ValidationResult {
    [key: string]: {
        isValid: boolean;
        validationMessage?: string;
    };
}

function validateInputStep1(data: Step1Data): ValidationResult {
    const validationResult: ValidationResult = {};

    // التحقق من الحقول المطلوبة
    if (!data.employer_name.value.trim()) {
        validationResult['employer_name'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        };
    } else {
        validationResult['employer_name'] = { isValid: true };
    }

    if (!data.first_name.value.trim()) {
        validationResult['first_name'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        };
    } else {
        validationResult['first_name'] = { isValid: true };
    }

    if (!data.last_name.value.trim()) {
        validationResult['last_name'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        };
    } else {
        validationResult['last_name'] = { isValid: true };
    }

    // التحقق من البريد الإلكتروني
    if (!data.email_address.value.trim()) {
        validationResult['email_address'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        };
    } else if (!isValidEmail(data.email_address.value)) {
        validationResult['email_address'] = {
            isValid: false,
            validationMessage: 'عنوان البريد الإلكتروني غير صالح',
        };
    } else {
        validationResult['email_address'] = { isValid: true };
    }

    // التحقق من التوقيع الإلكتروني
    if (!data.electronic_signature.value.trim()) {
        validationResult['electronic_signature'] = {
            isValid: false,
            validationMessage: 'التوقيع الإلكتروني مطلوب',
        };
    } else {
        validationResult['electronic_signature'] = { isValid: true };
    }

    // التحقق من الموافقات
    if (!data.i_agree_to_the_electronic_signature.value) {
        validationResult['i_agree_to_the_electronic_signature'] = {
            isValid: false,
            validationMessage: 'يجب الموافقة على التوقيع الإلكتروني',
        };
    } else {
        validationResult['i_agree_to_the_electronic_signature'] = { isValid: true };
    }

    if (!data.i_acknowledge_the_above.value) {
        validationResult['i_acknowledge_the_above'] = {
            isValid: false,
            validationMessage: 'يجب الإقرار بما سبق',
        };
    } else {
        validationResult['i_acknowledge_the_above'] = { isValid: true };
    }

    // التحقق من الحقول الشرطية (مثل middle_name)
    if (data.middle_name && !data.middle_name.value.trim()) {
        validationResult['middle_name'] = {
            isValid: false,
            validationMessage: 'هذا الحقل فارغ',
        };
    } else {
        validationResult['middle_name'] = { isValid: true };
    }

    return validationResult;
}

// دالة التحقق من صحة البريد الإلكتروني
function isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
}

export default validateInputStep1;
الخطوة 5: تحديث ملف request.js إلى TypeScript وتعزيز الأمان النوعي
5.1. تحويل request.js إلى request.ts

// src/data/request.ts
import { createListResource, createDocumentResource, ListResource, DocumentResource } from 'frappe-ui';
import { session } from './session';
import { RequestData, UpdateFields } from './types';
import { useToast } from 'vue-toastification';

// خيارات إنشاء مورد القائمة
interface CreateListResourceOptions {
    doctype: string;
    fields: string[];
    filters: { [key: string]: any };
    auto: boolean;
    pageLength: number;
    onSuccess: (data: RequestData[]) => void;
    setValue: {
        onSuccess: (data: any) => void;
        onError: (error: any) => void;
    };
}

/**
 * إنشاء مورد قائمة لطلبات Verification Instructions Request.
 * @param fields - قائمة أسماء الحقول المراد جلبها.
 * @returns ListResource<RequestData>
 */
export function createRequestList(fields: string[]): ListResource<RequestData> {
    if (!session.user) {
        throw new Error("User session is not available.");
    }

    const options: CreateListResourceOptions = {
        doctype: 'Verification Instructions Request',
        fields,
        filters: { user_id: session.user },
        auto: true,
        pageLength: 1,
        onSuccess(data: RequestData[]) {
            console.log("Data retrieved successfully:", data);
        },
        setValue: {
            onSuccess(data: any) {
                console.log("Data update successful.");
            },
            onError(error: any) {
                console.error("Error updating data:", error);
            },
        },
    };

    return createListResource<RequestData>(options);
}

/**
 * تحديث الحقول في مورد القائمة `Verification Instructions Request`.
 * @param requestList - مورد القائمة المراد تحديثه.
 * @param updatedFields - كائن يحتوي على الحقول المراد تحديثها.
 * @param toast - كائن Toast لعرض رسائل النجاح أو الخطأ.
 */
export function updateFieldsInRequestList(
    requestList: ListResource<RequestData>,
    updatedFields: UpdateFields,
    toast: ReturnType<typeof useToast>
): void {
    if (!requestList || !requestList.data || requestList.data.length === 0) {
        console.error("Request list data is not available");
        toast.error("فشل في العثور على بيانات الطلب المتاحة.");
        return;
    }

    const documentName: string = requestList.data[0].name;

    requestList.setValue.submit({
        name: documentName,
        ...updatedFields,
    }).then((response: any) => {
        console.log("Data updated successfully:", response);
        toast.success("تم تحديث البيانات بنجاح.");
    }).catch((error: any) => {
        console.error("Error updating data:", error);
        toast.error("حدث خطأ أثناء تحديث البيانات.");
    });
}

/**
 * دالة عامة لإنشاء مورد وثيقة معين.
 * @param name - (اختياري) اسم الوثيقة.
 * @returns DocumentResource<RequestData>
 */
export function useDocumentResource(name: string = ''): DocumentResource<RequestData> {
    if (!name) {
        console.warn("Document name is not provided.");
    }
    return createDocumentResource<RequestData>({
        doctype: "Verification Instructions Request",
        name: name,
        // auto: true
    });
}
الخطوة 6: تحديث مكونات الخطوات لتستخدم متجر Pinia
سنقوم بتحديث جميع مكونات الخطوات (step1.vue إلى step6.vue) لتستفيد من متجر Pinia الذي قمنا بإنشائه سابقًا.

6.1. تحديث step1.vue كمثال

<!-- src/components/stepsSections/step1.vue -->
<template>
  <div class="pt-3 container">
    <h3 class="text-lg font-medium mb-3 text-black">1. Basic Information</h3>
    <div class="lg:grid grid-cols-2 lg:gap-2">
      <FieldContainer>
        <StyledInput
          labelText="Employer Name"
          :isMandatory="true"
          infoText="Employer Name"
          inputType="text"
          v-model="stepData.employer_name.value"
          @input-change="handleInput('employer_name', $event)"
          name="employer_name"
          id="EmployerName"
          :isValid="stepData.employer_name.isValid"
          :validationMessage="stepData.employer_name.validationMessage"
        />
        <SupportingText>Supporting text</SupportingText>
      </FieldContainer>
      <FieldContainer>
        <StyledInput
          labelText="First Name"
          :isMandatory="true"
          infoText="First Name"
          inputType="text"
          v-model="stepData.first_name.value"
          @input-change="handleInput('first_name', $event)"
          name="first_name"
          id="firstName"
          :isValid="stepData.first_name.isValid"
          :validationMessage="stepData.first_name.validationMessage"
        />
      </FieldContainer>
    </div>

    <FieldContainer>
      <CheckBox 
        name="certify" 
        id="certify" 
        v-model="certify"
        @change="handleCheckboxChange"
      >
        I certify that I do not have a Middle Name on my official identification document
      </CheckBox>
    </FieldContainer>

    <div class="lg:grid grid-cols-2 lg:gap-2">
      <FieldContainer>
        <StyledInput
          labelText="Middle Name"
          :isMandatory="!certify"
          inputType="text"
          v-model="stepData.middle_name.value"
          @input-change="handleInput('middle_name', $event)"
          name="middle_name"
          id="MiddleName"
          :isValid="stepData.middle_name.isValid"
          :validationMessage="stepData.middle_name.validationMessage"
          :disabled="certify"
        />
      </FieldContainer>
    </div>

    <div class="lg:grid grid-cols-2 lg:gap-2">
      <FieldContainer>
        <StyledInput
          labelText="Last Name"
          :isMandatory="true"
          infoText="Last Name"
          inputType="text"
          v-model="stepData.last_name.value"
          @input-change="handleInput('last_name', $event)"
          name="last_name"
          id="lastName"
          :isValid="stepData.last_name.isValid"
          :validationMessage="stepData.last_name.validationMessage"
        />
      </FieldContainer>
      <FieldContainer>
        <StyledInput
          labelText="Suffix"
          :isMandatory="false"
          infoText="Suffix"
          inputType="text"
          v-model="stepData.suffix.value"
          @input-change="handleInput('suffix', $event)"
          name="suffix"
          id="Suffix"
          :isValid="stepData.suffix.isValid"
          :validationMessage="stepData.suffix.validationMessage"
        />
        <SupportingText>Supporting text</SupportingText>
      </FieldContainer>
    </div>

    <FieldContainer>
      <StyledInput
        labelText="Alias Name"
        :isMandatory="false"
        infoText="Alias Name"
        inputType="text"
        v-model="stepData.alias_name.value"
        @input-change="handleInput('alias_name', $event)"
        name="alias_name"
        id="AliasName"
        :isValid="stepData.alias_name.isValid"
        :validationMessage="stepData.alias_name.validationMessage"
      />
    </FieldContainer>
  </div>
</template>

<script setup lang="ts">
import { computed, watch } from 'vue';
import { useVerificationRequestStore } from '@/stores/verificationRequest';
import CheckBox from '../../components/checkBox.vue';
import FieldContainer from '../../components/fieldContainer.vue';
import StyledInput from '../../components/styledInput.vue';
import SupportingText from '../../components/supportingText.vue';

const props = defineProps({
    stepData: {
        type: Object,
        required: true,
    },
});

const emit = defineEmits(['update:stepData']);

const store = useVerificationRequestStore();

// يمكننا استخدام متغير محلي للتحكم في checkbox
const certify = computed({
    get: () => store.step1.middle_name === '',
    set: (value: boolean) => {
        if (value) {
            store.updateStep1({ middle_name: { value: '', isValid: true, validationMessage: null } });
        }
    },
});

// دالة التعامل مع إدخال البيانات
const handleInput = (field: string, value: any) => {
    store.updateStep1({ [field]: { value, isValid: null, validationMessage: null } });
};

// دالة التعامل مع تغير checkbox
const handleCheckboxChange = () => {
    if (certify.value) {
        store.updateStep1({ middle_name: { value: '', isValid: true, validationMessage: null } });
    }
};
</script>

<style scoped>
/* أضف تنسيقات إضافية إذا لزم الأمر */
</style>
ملاحظة: يجب أن يتم تعريف الحقول المتبقية لكل خطوة في متجر Pinia (verificationRequest) وتحديثها من المكونات الفرعية وفقًا لذلك.

6.2. تحديث step6.vue لحفظ البيانات النهائية
في step6.vue, عند الضغط على زر "Accept"، سنقوم بجمع جميع البيانات من متجر Pinia وإرسالها إلى Doctype:


<!-- src/components/stepsSections/step6.vue -->
<template>
  <div class="pt-3 container">
    <h3 class="text-lg font-medium mb-3 text-black">Verification Instructions Request</h3>
    <div class="lg:grid grid-cols-2 lg:gap-2">
      <FieldContainer>
        <StyledInput
          labelText="Full Name"
          :isMandatory="true"
          infoText="Please enter your full name"
          inputType="text"
          v-model="stepData.full_name.value"
          @input-change="handleInput('full_name', $event)"
          name="full_name"
          id="FullName"
          :isValid="stepData.full_name.isValid"
          :validationMessage="stepData.full_name.validationMessage"
        />
      </FieldContainer>
      <FieldContainer>
        <StyledInput
          labelText="Email Address"
          :isMandatory="true"
          infoText="Please enter your email address"
          inputType="email"
          v-model="stepData.email_address.value"
          @input-change="handleInput('email_address', $event)"
          name="email_address"
          id="EmailAddress"
          :isValid="stepData.email_address.isValid"
          :validationMessage="stepData.email_address.validationMessage"
        />
      </FieldContainer>
    </div>
    <div class="lg:grid grid-cols-2 lg:gap-2">
      <FieldContainer>
        <Select
          labelText="Other Languages"
          :isMandatory="false"
          infoText="Select other languages"
          name="other_languages"
          id="OtherLanguages"
          v-model="stepData.other_languages.value"
          @input-change="handleInput('other_languages', $event)"
          :options="languageOptions"
        />
      </FieldContainer>
      <FieldContainer>
        <StyledInput
          labelText="Electronic Signature"
          :isMandatory="true"
          infoText="Please provide your electronic signature"
          inputType="text"
          v-model="stepData.electronic_signature.value"
          @input-change="handleInput('electronic_signature', $event)"
          name="electronic_signature"
          id="ElectronicSignature"
          :isValid="stepData.electronic_signature.isValid"
          :validationMessage="stepData.electronic_signature.validationMessage"
        />
      </FieldContainer>
    </div>
    <div class="lg:grid grid-cols-2 lg:gap-2">
      <FieldContainer>
        <CheckBox 
          name="i_agree_to_the_electronic_signature" 
          id="AgreeToElectronicSignature" 
          v-model="stepData.i_agree_to_the_electronic_signature.value"
          @input-change="handleInput('i_agree_to_the_electronic_signature', $event)"
          :isValid="stepData.i_agree_to_the_electronic_signature.isValid"
          :validationMessage="stepData.i_agree_to_the_electronic_signature.validationMessage"
        >
          I agree to the electronic signature
        </CheckBox>
      </FieldContainer>
      <FieldContainer>
        <CheckBox 
          name="i_acknowledge_the_above" 
          id="Acknowledge" 
          v-model="stepData.i_acknowledge_the_above.value"
          @input-change="handleInput('i_acknowledge_the_above', $event)"
          :isValid="stepData.i_acknowledge_the_above.isValid"
          :validationMessage="stepData.i_acknowledge_the_above.validationMessage"
        >
          I acknowledge the above
        </CheckBox>
      </FieldContainer>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useVerificationRequestStore } from '@/stores/verificationRequest';
import CheckBox from '../../components/checkBox.vue';
import FieldContainer from '../../components/fieldContainer.vue';
import StyledInput from '../../components/styledInput.vue';
import Select from '../../components/select.vue';
import { useToast } from 'vue-toastification';
import objectConvertor from '../../data/convertor';
import { updateFieldsInRequestList } from '../../data/request';
import { ref, computed } from 'vue';

const store = useVerificationRequestStore();
const toast = useToast();

// بيانات الخطوة 6
const stepData = computed(() => store.step6);

// خيارات اللغات الأخرى
const languageOptions = [
  { value: 'en', text: 'English' },
  { value: 'es', text: 'Spanish' },
  { value: 'fr', text: 'French' },
  // أضف المزيد من اللغات حسب الحاجة
];

// دالة التعامل مع إدخال البيانات
const handleInput = (field: string, value: any) => {
    store.updateStep6({ [field]: { value, isValid: null, validationMessage: null } });
};

// دالة الحفظ النهائية
const accept = async () => {
    // التحقق من صحة الخطوة 6
    const validationResult = validateStep6();
    
    // تحديث حالة الحقول بناءً على نتائج التحقق
    Object.keys(validationResult).forEach((field) => {
        store.step6[field].isValid = validationResult[field].isValid;
        store.step6[field].validationMessage = validationResult[field].validationMessage || null;
    });

    // تحديد ما إذا كانت هناك حقول غير صالحة
    const hasErrors = Object.values(validationResult).some(result => result.isValid === false);

    if (hasErrors) {
        toast.error("يرجى تصحيح الأخطاء في البيانات المدخلة.");
        return;
    }

    // دمج جميع البيانات من الخطوات المختلفة
    const finalData = {
        ...objectConvertor(store.step1),
        ...objectConvertor(store.step2),
        ...objectConvertor(store.step3),
        ...objectConvertor(store.step4),
        ...objectConvertor(store.step5),
        ...objectConvertor(store.step6),
    };

    try {
        if (!store.documentName) {
            // إنشاء وثيقة جديدة
            const newDoc = await createDocument({
                doctype: 'Verification Instructions Request',
                ...finalData,
            });
            store.setDocumentName(newDoc.name);
            toast.success("تم إنشاء الوثيقة بنجاح.");
        } else {
            // تحديث الوثيقة القائمة
            updateFieldsInRequestList(
                createRequestList(['name', 'user_id']), // تأكد من تحديد الحقول المطلوبة
                { ...finalData, name: store.documentName },
                toast
            );
            toast.success("تم تحديث الوثيقة بنجاح.");
        }
    } catch (error) {
        console.error("Error saving verification request:", error);
        toast.error("حدث خطأ أثناء حفظ البيانات.");
    }
}

// دالة التحقق من صحة الخطوة 6
const validateStep6 = () => {
    const validationResult: any = {};

    if (!stepData.value.full_name.trim()) {
        validationResult['full_name'] = { isValid: false, validationMessage: 'Full Name is required.' };
    } else {
        validationResult['full_name'] = { isValid: true };
    }

    if (!stepData.value.email_address.trim()) {
        validationResult['email_address'] = { isValid: false, validationMessage: 'Email Address is required.' };
    } else if (!isValidEmail(stepData.value.email_address)) {
        validationResult['email_address'] = { isValid: false, validationMessage: 'Invalid email address.' };
    } else {
        validationResult['email_address'] = { isValid: true };
    }

    if (!stepData.value.electronic_signature.trim()) {
        validationResult['electronic_signature'] = { isValid: false, validationMessage: 'Electronic Signature is required.' };
    } else {
        validationResult['electronic_signature'] = { isValid: true };
    }

    if (!stepData.value.i_agree_to_the_electronic_signature) {
        validationResult['i_agree_to_the_electronic_signature'] = { isValid: false, validationMessage: 'You must agree to the electronic signature.' };
    } else {
        validationResult['i_agree_to_the_electronic_signature'] = { isValid: true };
    }

    if (!stepData.value.i_acknowledge_the_above) {
        validationResult['i_acknowledge_the_above'] = { isValid: false, validationMessage: 'You must acknowledge the above.' };
    } else {
        validationResult['i_acknowledge_the_above'] = { isValid: true };
    }

    return validationResult;
}

// دالة التحقق من صحة البريد الإلكتروني
function isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
}

// دالة لإنشاء وثيقة جديدة (تحتاج إلى تعريفها حسب واجهة API الخاصة بـ Frappe)
async function createDocument(data: any) {
    // مثال على استخدام واجهة Frappe API
    // يجب عليك تعديل هذا الجزء بناءً على كيفية تعامل Frappe UI مع إنشاء الوثائق
    // على سبيل المثال:
    // return await frappe.call({
    //     method: 'frappe.desk.doctype.verification_instructions_request.verification_instructions_request.create',
    //     args: { data },
    // });
    
    // Placeholder:
    return { name: 'VIR-2024-26-09-000008' }; // مثال: استبدله بالاسم الحقيقي من Frappe
}
</script>

<style scoped>
/* أضف أي تنسيقات إضافية هنا إذا لزم الأمر */
</style>
الخطوة 7: تحديث المتجر Pinia لمزيد من التفاعلية وكيفية التعامل مع التحقق الشرطي
7.1. تحديث متجر Pinia لتشمل documentName وإجراء عمليات الحفظ

// src/stores/verificationRequest.ts
import { defineStore } from 'pinia';

interface Step1Data {
    employer_name: string;
    first_name: string;
    last_name: string;
    middle_name?: string;
    suffix?: string;
    alias_name?: string;
    email_address: string;
    electronic_signature: string;
    i_agree_to_the_electronic_signature: boolean;
    i_acknowledge_the_above: boolean;
}

interface Step2Data {
    education_information: any[];
}

interface Step3Data {
    employment_history: any[];
}

interface Step4Data {
    professional_qualification: any[];
}

interface Step5Data {
    // أضف الحقول المطلوبة لمراجعة المعلومات
}

interface Step6Data {
    full_name: string;
    email_address: string;
    other_languages: string[];
    electronic_signature: string;
    i_agree_to_the_electronic_signature: boolean;
    i_acknowledge_the_above: boolean;
}

interface VerificationRequestStoreState {
    documentName: string | null;
    step1: Step1Data;
    step2: Step2Data;
    step3: Step3Data;
    step4: Step4Data;
    step5: Step5Data;
    step6: Step6Data;
}

export const useVerificationRequestStore = defineStore('verificationRequest', {
    state: (): VerificationRequestStoreState => ({
        documentName: null,
        step1: {
            employer_name: '',
            first_name: '',
            last_name: '',
            middle_name: '',
            suffix: '',
            alias_name: '',
            email_address: '',
            electronic_signature: '',
            i_agree_to_the_electronic_signature: false,
            i_acknowledge_the_above: false,
        },
        step2: {
            education_information: [],
        },
        step3: {
            employment_history: [],
        },
        step4: {
            professional_qualification: [],
        },
        step5: {
            // أضف الحقول المطلوبة
        },
        step6: {
            full_name: '',
            email_address: '',
            other_languages: [],
            electronic_signature: '',
            i_agree_to_the_electronic_signature: false,
            i_acknowledge_the_above: false,
        },
    }),
    actions: {
        setDocumentName(name: string) {
            this.documentName = name;
        },
        // دوال تحديث بيانات الخطوات
        updateStep1(data: Partial<Step1Data>) {
            this.step1 = { ...this.step1, ...data };
        },
        updateStep2(data: Partial<Step2Data>) {
            this.step2 = { ...this.step2, ...data };
        },
        updateStep3(data: Partial<Step3Data>) {
            this.step3 = { ...this.step3, ...data };
        },
        updateStep4(data: Partial<Step4Data>) {
            this.step4 = { ...this.step4, ...data };
        },
        updateStep5(data: Partial<Step5Data>) {
            this.step5 = { ...this.step5, ...data };
        },
        updateStep6(data: Partial<Step6Data>) {
            this.step6 = { ...this.step6, ...data };
        },
        // إعادة تعيين المتجر
        resetStore() {
            this.documentName = null;
            this.step1 = {
                employer_name: '',
                first_name: '',
                last_name: '',
                middle_name: '',
                suffix: '',
                alias_name: '',
                email_address: '',
                electronic_signature: '',
                i_agree_to_the_electronic_signature: false,
                i_acknowledge_the_above: false,
            };
            this.step2 = { education_information: [] };
            this.step3 = { employment_history: [] };
            this.step4 = { professional_qualification: [] };
            this.step5 = { /* أضف الحقول المطلوبة */ };
            this.step6 = {
                full_name: '',
                email_address: '',
                other_languages: [],
                electronic_signature: '',
                i_agree_to_the_electronic_signature: false,
                i_acknowledge_the_above: false,
            };
        },
    },
});
الخطوة 8: ضمان عدم فقدان بيانات المراحل عند التنقل بين الخطوات
بما أن Pinia يدير الحالة عالميًا، فإن بيانات الخطوات ستبقى محفوظة حتى عند التنقل بين المراحل المختلفة.

مثال على تحديث مكون steps.vue

<!-- src/components/steps.vue -->
<template>
  <Header />
  <div class="lg:grid lg:grid-cols-12 lg:container py-8 bg-bg">
    <div class="container col-span-4 lg:order-2 relative">
      <div class="sticky top-4">
        <div class="pb-3 font-medium text-primary text-[1rem]">
          <span class="m-2"><-</span>Model steps instructions of Job Request (
          {{ Math.round((100 / totalSteps) * processed) }}% Completed)
        </div>
        <div class="py-3 grid grid-cols-6 lg:flex lg:flex-col lg:gap-4 w-full">
          <StepIcon
            v-for="(step, index) in totalSteps"
            :key="index"
            :process="processed === index"
            :complete="processed > index"
            :label="'Step ' + (index + 1)"
            :desc="getStepDescription(index)"
          />
        </div>
        <div class="py-3 lg:hidden flex justify-end text-secondary text-[1rem]">
          <button>
            All Steps ({{ totalSteps }} )<span class="inline-block mx-1">-></span>
          </button>
        </div>
      </div>
    </div>
    <div class="pb-20 bg-bg leading-[1.6rem] col-span-8">
      <component 
        :is="currentStepComponent" 
        :stepData="currentStepData" 
        @update:stepData="updateStepData(processed, $event)" 
      />
    </div>
  </div>
  <div class="bg-[#F0F0F0] fixed lg:static bottom-0 w-screen h-fit">
    <div class="container py-4 flex w-full justify-between items-center">
      <div class="text-xs text-[#1D1B20]">Auto Saving</div>

      <div class="flex gap-2">
        <button class="text-secondary" @click="previousStep" v-if="processed > 0"><</button>
        <Button 
          v-if="processed < totalSteps - 1" 
          :disabled="!isCurrentStepValid" 
          level="primary" 
          @clicked="nextStep"
        >
          Step {{ stepN }} ->
        </Button>
      </div>

      <div v-if="processed === totalSteps -1" class="lg:flex justify-end grow lg:gap-2">
        <Button level="other" @clicked="accept"> Accept </Button>
        <Button level="other" @clicked="reject"> Reject </Button>
      </div>
    </div>
  </div>
  <div class="fixed z-10 bottom-0 py-6 px-4 flex flex-col gap-2">
    <SnackBar 
      v-for="(alert, index) in alerts" 
      :key="index" 
      :isDanger="alert.isDanger" 
      :message="alert.message" 
      @close="removeAlert(index)" 
    />
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';
import { useVerificationRequestStore } from '@/stores/verificationRequest';
import Header from '../components/header.vue';
import StepIcon from './stepsSections/components/stepIcon.vue';
import Step1 from './stepsSections/step1.vue';
import Step2 from './stepsSections/step2.vue';
import Step3 from './stepsSections/step3.vue';
import Step4 from './stepsSections/step4.vue';
import Step5 from './stepsSections/step5.vue';
import Step6 from './stepsSections/step6.vue';
import SnackBar from '../components/snackBar.vue';
import Button from '../components/button.vue';
import { useToast } from 'vue-toastification';
import { createDocumentResource } from 'frappe-ui';
import { updateFieldsInRequestList } from '../data/request';
import objectConvertor from '../data/convertor';
import validateInputStep1 from '../data/validate/validateInputStep1';

// تهيئة Toast
const toast = useToast();

// استخدام متجر Pinia
const store = useVerificationRequestStore();

// تعريف المراحل
const processed = ref(0); // المرحلة الحالية (0-5)
const stepN = computed(() => processed.value + 1);
const totalSteps = 6;

// تحديد المكون الحالي بناءً على المرحلة الحالية
const currentStepComponent = computed(() => {
    switch (processed.value) {
        case 0:
            return Step1;
        case 1:
            return Step2;
        case 2:
            return Step3;
        case 3:
            return Step4;
        case 4:
            return Step5;
        case 5:
            return Step6;
        default:
            return Step1;
    }
});

// الحصول على بيانات الخطوة الحالية من المتجر
const currentStepData = computed(() => {
    switch(processed.value) {
        case 0:
            return store.step1;
        case 1:
            return store.step2;
        case 2:
            return store.step3;
        case 3:
            return store.step4;
        case 4:
            return store.step5; // افترض أن step5 لديها بيانات
        case 5:
            return store.step6;
        default:
            return store.step1;
    }
});

// دالة تحديث بيانات الخطوة الحالية
const updateStepData = (index: number, newData: any) => {
    switch(index) {
        case 0:
            store.updateStep1(newData);
            break;
        case 1:
            store.updateStep2(newData);
            break;
        case 2:
            store.updateStep3(newData);
            break;
        case 3:
            store.updateStep4(newData);
            break;
        case 4:
            store.updateStep5(newData);
            break;
        case 5:
            store.updateStep6(newData);
            break;
    }
};

// دالة التحقق من صحة الخطوة الحالية
const isCurrentStepValid = computed(() => {
    switch(processed.value) {
        case 0:
            return (
                store.step1.employer_name !== '' &&
                store.step1.first_name !== '' &&
                store.step1.last_name !== '' &&
                store.step1.email_address !== '' &&
                store.step1.electronic_signature !== '' &&
                store.step1.i_agree_to_the_electronic_signature &&
                store.step1.i_acknowledge_the_above
            );
        case 1:
            // تعريف التحقق من صحة step2
            return true; // Placeholder
        case 2:
            // تعريف التحقق من صحة step3
            return true; // Placeholder
        case 3:
            // تعريف التحقق من صحة step4
            return true; // Placeholder
        case 4:
            // تعريف التحقق من صحة step5
            return true; // Placeholder
        case 5:
            // تعريف التحقق من صحة step6
            return (
                store.step6.full_name !== '' &&
                store.step6.email_address !== '' &&
                store.step6.electronic_signature !== '' &&
                store.step6.i_agree_to_the_electronic_signature &&
                store.step6.i_acknowledge_the_above
            );
        default:
            return false;
    }
});

// دالة التنقل للخلف
const previousStep = () => {
    if (processed.value > 0) {
        processed.value--;
    }
}

// دالة التنقل للأمام
const nextStep = () => {
    if(isCurrentStepValid.value) {
        processed.value++;
    }
    else {
        toast.error("يرجى تصحيح الأخطاء في البيانات المدخلة.")
    }
}

// دالة قبول الطلب في step6
const accept = async () => {
    // التحقق من صحة جميع الخطوات
    if (!isAllStepsValid()) {
        toast.error("يرجى تصحيح الأخطاء في جميع الخطوات.");
        return;
    }

    // دمج جميع البيانات
    const finalData = {
        ...objectConvertor(store.step1),
        ...objectConvertor(store.step2),
        ...objectConvertor(store.step3),
        ...objectConvertor(store.step4),
        ...objectConvertor(store.step5),
        ...objectConvertor(store.step6),
    };

    try {
        if (!store.documentName) {
            // إنشاء وثيقة جديدة
            const docResource = createDocumentResource<RequestData>({
                doctype: 'Verification Instructions Request',
                name: '', // يسمح Frappe بتوليد الاسم تلقائيًا
            });

            // إنشاء الوثيقة الجديدة
            const newDoc = await docResource.create(finalData);
            store.setDocumentName(newDoc.name);
            toast.success("تم إنشاء الوثيقة بنجاح.");
        } else {
            // تحديث الوثيقة الحالية
            const requestList = createRequestList(['name', 'user_id']); // تأكد من تعديل الحقول المطلوبة
            updateFieldsInRequestList(requestList, finalData, toast);
            toast.success("تم تحديث الوثيقة بنجاح.");
        }
    } catch (error) {
        console.error("Error saving verification request:", error);
        toast.error("حدث خطأ أثناء حفظ البيانات.");
    }
}

// دالة رفض الطلب (يمكنك تخصيصها حسب احتياجاتك)
const reject = () => {
    // على سبيل المثال، إعادة تعيين البيانات أو إعادة التوجيه
    toast.info("تم رفض الطلب.");
    store.resetStore();
    processed.value = 0;
}

// دالة التحقق من صحة جميع الخطوات
const isAllStepsValid = () => {
    return (
        store.step1.employer_name !== '' &&
        store.step1.first_name !== '' &&
        store.step1.last_name !== '' &&
        store.step1.email_address !== '' &&
        store.step1.electronic_signature !== '' &&
        store.step1.i_agree_to_the_electronic_signature &&
        store.step1.i_acknowledge_the_above &&
        // التحقق من صحة الخطوات الأخرى
        store.step6.full_name !== '' &&
        store.step6.email_address !== '' &&
        store.step6.electronic_signature !== '' &&
        store.step6.i_agree_to_the_electronic_signature &&
        store.step6.i_acknowledge_the_above
        // أضف تحقق خطوات 2 إلى 5 هنا حسب الحاجة
    );
}

// دالة إلغاء التنبيهات
const removeAlert = (index: number) => {
    // يتم إدارة التنبيهات في مكون SnackBar أو في Pinia Store حسب التصميم
}
</script>

<style scoped>
/* أضف أي تنسيقات إضافية هنا إذا لزم الأمر */
</style>
الخطوة 9: ضمان تخزين name الخاص بـ Verification Instructions Request في متجر Pinia واستخدامه عالميًا
9.1. تحديث دالة createDocument
تأكد من أنك تحصل على name عند إنشاء الوثيقة الجديدة وتخزينها في المتجر Pinia.


// src/components/stepsSections/step6.vue (استمرار)
const createDocument = async (data: any): Promise<any> => {
    try {
        // فرضية استخدام Frappe API لإنشاء وثيقة
        // استبدل هذا الجزء بما يتوافق مع طريقة Frappe UI لديك
        const response = await frappe.call({
            method: 'frappe.desk.doctype.verification_instructions_request.verification_instructions_request.create',
            args: {
                data,
            },
        });

        return response.message; // فرضية أن `message` يحمل اسم الوثيقة
    } catch (error) {
        throw error;
    }
}
ملاحظة: تأكد من تكوين واجهة API الفعلية وفقًا لكيفية تعامل frappe-ui مع إنشاء الوثائق.

ملخص الخطوات
إعداد Pinia لإدارة الحالة العالمية لتخزين documentName وبيانات الخطوات المختلفة.
تعريف متجر Pinia (useVerificationRequestStore) لتخزين وإدارة بيانات Verification Instructions Request.
تحديث دالة convertor.ts لجعلها متوافقة مع TypeScript وتحديد الأنواع بشكل دقيق.
تحديث دالة validateInputStep1.ts لتحسين التحقق من الصحة والشمولية.
تحويل request.js إلى TypeScript وتعزيز الأمان النوعي وإدارة الأخطاء.
تحديث مكونات الخطوات لتستخدم متجر Pinia وتخزن البيانات بشكل مركزي.
تحديث مكون step6.vue لحفظ البيانات النهائية وإرسالها إلى Doctype، وضمان تخزين documentName بشكل عالمي.
ضمان عدم فقدان بيانات المراحل عند التنقل بينها باستخدام Pinia.
تحسين تجربة المستخدم من خلال إضافة رسائل توضيحية وأزرار فعّالة.
نصائح إضافية
استخدام مكتبات تحقق قويّة: مثل Vuelidate أو Yup لتحسين وتنظيم التحقق من الصحة.
إضافة اختبارات وحدة (Unit Tests): لزيادة موثوقية الكود وضمان عمل وظائفك بشكل صحيح تحت مختلف الظروف.
التعامل مع الأخطاء بشكل فعّال: التأكد من أن جميع الأخطاء يتم التعامل معها بشكل مناسب وإعلام المستخدمين بها.
إذا كان لديك أي استفسار إضافي أو تحتاج إلى مزيد من المساعدة في جزء معين من التطبيق، فلا تتردد في طرحه!