349 lines
9.9 KiB
JavaScript
349 lines
9.9 KiB
JavaScript
import { defineStore } from 'pinia'
|
|
import { computed, ref, watch } from 'vue'
|
|
import { STUDENT_REGISTRATION, STUDENT_REGISTRATION_ORDER } from '@/enums'
|
|
|
|
const STORAGE_KEY = 'student-registration'
|
|
|
|
const emptyState = () => ({
|
|
isProfileCompleted: false,
|
|
personal: {
|
|
avatar: null,
|
|
avatarId: '',
|
|
name: '',
|
|
birthday: '',
|
|
nationalCode: '',
|
|
maritalStatus: '',
|
|
gender: '',
|
|
phoneNumber: '',
|
|
provinceId: null,
|
|
cityId: null,
|
|
address: '',
|
|
postalCode: '',
|
|
virtualPhoneNumber: '',
|
|
seminaryCode: '',
|
|
seminaryServiceCenterCode: '',
|
|
childrenCount: '',
|
|
},
|
|
educational: {
|
|
educationStatus: '',
|
|
seminaryLevel: '',
|
|
universityLevel: '',
|
|
universityName: '',
|
|
fieldOfStudy: '',
|
|
activitySummary: '',
|
|
},
|
|
professional: {
|
|
propagationExperienceYears: 0,
|
|
propagationMethodDescription: '',
|
|
specializedTopics: '',
|
|
propagationPlatforms: [],
|
|
},
|
|
skill1: {
|
|
responseToLowSatisfaction: '',
|
|
responseToLowSatisfactionDescription: '',
|
|
},
|
|
skill2: {
|
|
responseToSessionCancellation: '',
|
|
responseToSessionCancellationDescription: '',
|
|
},
|
|
skill3: {
|
|
responseToAudienceConflict: '',
|
|
responseToAudienceConflictDescription: '',
|
|
},
|
|
skill4: {
|
|
responseToCompetingPropagator: '',
|
|
responseToCompetingPropagatorDescription: '',
|
|
},
|
|
skill5: {
|
|
responseToUnqualifiedAdvisors: '',
|
|
relevantCertificates: '',
|
|
hijabApproach: '',
|
|
},
|
|
skill6: {
|
|
faithProductionAudio: null,
|
|
faithProductionId: '',
|
|
},
|
|
skill7: {
|
|
leaderMessageVideo: null,
|
|
leaderMessageId: '',
|
|
},
|
|
currentStep: STUDENT_REGISTRATION.PERSONAL_INFORMATION,
|
|
completedSteps: [],
|
|
registrationMedia: {},
|
|
})
|
|
|
|
// Each field is owned by exactly one section. Used to route GET /me/register-data
|
|
// values back into the matching slice on hydration.
|
|
const SECTION_FIELDS = {
|
|
personal: [
|
|
'avatar',
|
|
'avatarId',
|
|
'name',
|
|
'birthday',
|
|
'nationalCode',
|
|
'maritalStatus',
|
|
'gender',
|
|
'phoneNumber',
|
|
'provinceId',
|
|
'cityId',
|
|
'address',
|
|
'postalCode',
|
|
'virtualPhoneNumber',
|
|
'seminaryCode',
|
|
'seminaryServiceCenterCode',
|
|
'childrenCount',
|
|
],
|
|
educational: [
|
|
'educationStatus',
|
|
'seminaryLevel',
|
|
'universityLevel',
|
|
'universityName',
|
|
'fieldOfStudy',
|
|
'activitySummary',
|
|
],
|
|
professional: [
|
|
'propagationExperienceYears',
|
|
'propagationMethodDescription',
|
|
'specializedTopics',
|
|
'propagationPlatforms',
|
|
],
|
|
skill1: ['responseToLowSatisfaction', 'responseToLowSatisfactionDescription'],
|
|
skill2: ['responseToSessionCancellation', 'responseToSessionCancellationDescription'],
|
|
skill3: ['responseToAudienceConflict', 'responseToAudienceConflictDescription'],
|
|
skill4: ['responseToCompetingPropagator', 'responseToCompetingPropagatorDescription'],
|
|
skill5: ['responseToUnqualifiedAdvisors', 'relevantCertificates', 'hijabApproach'],
|
|
skill6: ['faithProductionAudio', 'faithProductionId'],
|
|
skill7: ['leaderMessageVideo', 'leaderMessageId'],
|
|
}
|
|
|
|
const FIELD_TO_SECTION = Object.entries(SECTION_FIELDS).reduce((acc, [section, fields]) => {
|
|
fields.forEach((field) => {
|
|
acc[field] = section
|
|
})
|
|
return acc
|
|
}, {})
|
|
|
|
// Register-data travels as a flat `{ field: value }` object. Legacy payloads
|
|
// were a `[{ key, value }]` list — tolerate both when reading.
|
|
export const toRegisterDataObject = (input) => {
|
|
if (Array.isArray(input)) {
|
|
return input.reduce((acc, item) => {
|
|
if (item && typeof item === 'object' && 'key' in item) acc[item.key] = item.value
|
|
return acc
|
|
}, {})
|
|
}
|
|
return input && typeof input === 'object' ? input : {}
|
|
}
|
|
|
|
const readPersisted = () => {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY)
|
|
if (!raw) return null
|
|
return JSON.parse(raw)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
const normalizeCompletedSteps = (steps) =>
|
|
Array.isArray(steps) ? STUDENT_REGISTRATION_ORDER.filter((step) => steps.includes(step)) : []
|
|
|
|
const firstIncompleteIndex = (steps) => {
|
|
const index = STUDENT_REGISTRATION_ORDER.findIndex((step) => !steps.includes(step))
|
|
return index === -1 ? STUDENT_REGISTRATION_ORDER.length - 1 : index
|
|
}
|
|
|
|
const resolveCurrentStep = (candidate, completed) => {
|
|
const candidateIndex = STUDENT_REGISTRATION_ORDER.indexOf(candidate)
|
|
const reachableIndex = firstIncompleteIndex(completed)
|
|
if (candidateIndex >= 0 && candidateIndex <= reachableIndex) return candidate
|
|
return STUDENT_REGISTRATION_ORDER[reachableIndex]
|
|
}
|
|
|
|
export const useStudentRegistrationStore = defineStore('studentRegistration', () => {
|
|
const persisted = readPersisted() || {}
|
|
const initial = { ...emptyState(), ...persisted }
|
|
|
|
const isProfileCompleted = ref(initial.isProfileCompleted)
|
|
const personal = ref({ ...emptyState().personal, ...persisted.personal })
|
|
const educational = ref({ ...emptyState().educational, ...persisted.educational })
|
|
const professional = ref({ ...emptyState().professional, ...persisted.professional })
|
|
const skill1 = ref({ ...emptyState().skill1, ...persisted.skill1 })
|
|
const skill2 = ref({ ...emptyState().skill2, ...persisted.skill2 })
|
|
const skill3 = ref({ ...emptyState().skill3, ...persisted.skill3 })
|
|
const skill4 = ref({ ...emptyState().skill4, ...persisted.skill4 })
|
|
const skill5 = ref({ ...emptyState().skill5, ...persisted.skill5 })
|
|
const skill6 = ref({ ...emptyState().skill6, ...persisted.skill6 })
|
|
const skill7 = ref({ ...emptyState().skill7, ...persisted.skill7 })
|
|
const completedSteps = ref(normalizeCompletedSteps(initial.completedSteps))
|
|
const currentStep = ref(resolveCurrentStep(initial.currentStep, completedSteps.value))
|
|
const registrationMedia = ref(initial.registrationMedia || {})
|
|
|
|
const sectionRefs = {
|
|
personal,
|
|
educational,
|
|
professional,
|
|
skill1,
|
|
skill2,
|
|
skill3,
|
|
skill4,
|
|
skill5,
|
|
skill6,
|
|
skill7,
|
|
}
|
|
|
|
const snapshot = computed(() => ({
|
|
isProfileCompleted: isProfileCompleted.value,
|
|
personal: personal.value,
|
|
educational: educational.value,
|
|
professional: professional.value,
|
|
skill1: skill1.value,
|
|
skill2: skill2.value,
|
|
skill3: skill3.value,
|
|
skill4: skill4.value,
|
|
skill5: skill5.value,
|
|
skill6: skill6.value,
|
|
skill7: skill7.value,
|
|
currentStep: currentStep.value,
|
|
completedSteps: completedSteps.value,
|
|
registrationMedia: registrationMedia.value,
|
|
}))
|
|
|
|
watch(
|
|
snapshot,
|
|
(value) => {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(value))
|
|
} catch {
|
|
/* storage may be unavailable */
|
|
}
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
const markStepAsCompleted = (stepKey) => {
|
|
if (STUDENT_REGISTRATION_ORDER.includes(stepKey) && !completedSteps.value.includes(stepKey)) {
|
|
completedSteps.value = [...completedSteps.value, stepKey]
|
|
}
|
|
}
|
|
|
|
const updateCurrentStep = (stepKey) => {
|
|
if (canGoToStep(stepKey)) currentStep.value = stepKey
|
|
}
|
|
|
|
const updateSection = (section, value) => {
|
|
const target = sectionRefs[section]
|
|
if (!target || !value || typeof value !== 'object') return
|
|
target.value = { ...target.value, ...value }
|
|
}
|
|
|
|
const setRegistrationMedia = (value) => {
|
|
if (!value || typeof value !== 'object') return
|
|
registrationMedia.value = { ...registrationMedia.value, ...value }
|
|
}
|
|
|
|
const goToNextStep = () => {
|
|
const idx = STUDENT_REGISTRATION_ORDER.indexOf(currentStep.value)
|
|
if (
|
|
idx >= 0 &&
|
|
idx < STUDENT_REGISTRATION_ORDER.length - 1 &&
|
|
completedSteps.value.includes(currentStep.value)
|
|
) {
|
|
currentStep.value = STUDENT_REGISTRATION_ORDER[idx + 1]
|
|
}
|
|
}
|
|
|
|
const goToPrevStep = () => {
|
|
const idx = STUDENT_REGISTRATION_ORDER.indexOf(currentStep.value)
|
|
if (idx > 0) currentStep.value = STUDENT_REGISTRATION_ORDER[idx - 1]
|
|
}
|
|
|
|
const canGoToStep = (stepKey) => {
|
|
const targetIndex = STUDENT_REGISTRATION_ORDER.indexOf(stepKey)
|
|
return targetIndex >= 0 && targetIndex <= firstIncompleteIndex(completedSteps.value)
|
|
}
|
|
|
|
const buildCompletePayload = () => ({
|
|
...personal.value,
|
|
...educational.value,
|
|
...professional.value,
|
|
...skill1.value,
|
|
...skill2.value,
|
|
...skill3.value,
|
|
...skill4.value,
|
|
...skill5.value,
|
|
...skill6.value,
|
|
...skill7.value,
|
|
avatarId: personal.value.avatar?.uploadId
|
|
? Number.parseInt(personal.value.avatar.uploadId)
|
|
: personal.value.avatarId,
|
|
})
|
|
|
|
// Take the flat `{ field: value }` map from GET /me/register-data and slot
|
|
// each value into the right section ref, leaving anything we don't
|
|
// recognise alone.
|
|
const hydrateFromRegisterData = (input) => {
|
|
const flat = toRegisterDataObject(input)
|
|
const buckets = {}
|
|
Object.entries(flat).forEach(([key, value]) => {
|
|
const section = FIELD_TO_SECTION[key]
|
|
if (!section) return
|
|
buckets[section] = { ...buckets[section], [key]: value }
|
|
})
|
|
Object.entries(buckets).forEach(([section, patch]) => {
|
|
const target = sectionRefs[section]
|
|
if (target) target.value = { ...target.value, ...patch }
|
|
})
|
|
}
|
|
|
|
const reset = () => {
|
|
const fresh = emptyState()
|
|
isProfileCompleted.value = fresh.isProfileCompleted
|
|
personal.value = fresh.personal
|
|
educational.value = fresh.educational
|
|
professional.value = fresh.professional
|
|
skill1.value = fresh.skill1
|
|
skill2.value = fresh.skill2
|
|
skill3.value = fresh.skill3
|
|
skill4.value = fresh.skill4
|
|
skill5.value = fresh.skill5
|
|
skill6.value = fresh.skill6
|
|
skill7.value = fresh.skill7
|
|
currentStep.value = fresh.currentStep
|
|
completedSteps.value = fresh.completedSteps
|
|
registrationMedia.value = fresh.registrationMedia
|
|
try {
|
|
localStorage.removeItem(STORAGE_KEY)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
return {
|
|
isProfileCompleted,
|
|
personal,
|
|
educational,
|
|
professional,
|
|
skill1,
|
|
skill2,
|
|
skill3,
|
|
skill4,
|
|
skill5,
|
|
skill6,
|
|
skill7,
|
|
currentStep,
|
|
completedSteps,
|
|
registrationMedia,
|
|
markStepAsCompleted,
|
|
updateCurrentStep,
|
|
updateSection,
|
|
setRegistrationMedia,
|
|
goToNextStep,
|
|
goToPrevStep,
|
|
canGoToStep,
|
|
buildCompletePayload,
|
|
hydrateFromRegisterData,
|
|
reset,
|
|
}
|
|
})
|