Files
banu-front/src/features/auth/store/student-registration.js
T
Server Migration b64f528c0d
Deploy banu-front / deploy (push) Successful in 1m40s
fix: complete registration frontend flow
2026-08-15 17:28:40 +03:30

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,
}
})