feat(auth): referrer code at signup and agent code in profile
Optional referrer agent code in registration with field validation UX. Show read-only agent referral code in profile edit and desktop overview. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,6 +10,7 @@ type PendingOtp = {
|
|||||||
isRegister: boolean
|
isRegister: boolean
|
||||||
firstName?: string
|
firstName?: string
|
||||||
lastName?: string
|
lastName?: string
|
||||||
|
referrerAgentCode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
let memoryPending: PendingOtp | null = null
|
let memoryPending: PendingOtp | null = null
|
||||||
@@ -50,7 +51,12 @@ function setPendingOtp(value: PendingOtp | null) {
|
|||||||
|
|
||||||
export async function sendOtp(
|
export async function sendOtp(
|
||||||
phone: string,
|
phone: string,
|
||||||
options: { firstName?: string; lastName?: string; isRegister: boolean },
|
options: {
|
||||||
|
firstName?: string
|
||||||
|
lastName?: string
|
||||||
|
isRegister: boolean
|
||||||
|
referrerAgentCode?: string
|
||||||
|
},
|
||||||
): Promise<{ success: boolean; message: string }> {
|
): Promise<{ success: boolean; message: string }> {
|
||||||
const normalized = normalizePhone(phone)
|
const normalized = normalizePhone(phone)
|
||||||
if (!/^09\d{9}$/.test(normalized)) {
|
if (!/^09\d{9}$/.test(normalized)) {
|
||||||
@@ -62,11 +68,13 @@ export async function sendOtp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const referrerAgentCode = options.referrerAgentCode?.trim() || undefined
|
||||||
setPendingOtp({
|
setPendingOtp({
|
||||||
phone: normalized,
|
phone: normalized,
|
||||||
isRegister: options.isRegister,
|
isRegister: options.isRegister,
|
||||||
firstName: options.firstName,
|
firstName: options.firstName,
|
||||||
lastName: options.lastName,
|
lastName: options.lastName,
|
||||||
|
referrerAgentCode,
|
||||||
})
|
})
|
||||||
const res = await apiRequest<{ success: boolean; message: string; debugCode?: string }>(
|
const res = await apiRequest<{ success: boolean; message: string; debugCode?: string }>(
|
||||||
'/auth/otp/request',
|
'/auth/otp/request',
|
||||||
@@ -78,6 +86,7 @@ export async function sendOtp(
|
|||||||
firstName: options.firstName,
|
firstName: options.firstName,
|
||||||
lastName: options.lastName,
|
lastName: options.lastName,
|
||||||
isRegister: options.isRegister,
|
isRegister: options.isRegister,
|
||||||
|
referrerAgentCode,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
+15
-2
@@ -50,7 +50,12 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
|
|
||||||
async function requestOtp(
|
async function requestOtp(
|
||||||
phone: string,
|
phone: string,
|
||||||
options: { firstName?: string; lastName?: string; isRegister: boolean },
|
options: {
|
||||||
|
firstName?: string
|
||||||
|
lastName?: string
|
||||||
|
isRegister: boolean
|
||||||
|
referrerAgentCode?: string
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
@@ -62,9 +67,16 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
|
|
||||||
const firstName = options.firstName?.trim() || stored?.firstName
|
const firstName = options.firstName?.trim() || stored?.firstName
|
||||||
const lastName = options.lastName?.trim() || stored?.lastName
|
const lastName = options.lastName?.trim() || stored?.lastName
|
||||||
|
const referrerAgentCode =
|
||||||
|
options.referrerAgentCode?.trim() || stored?.referrerAgentCode
|
||||||
|
|
||||||
if (options.isRegister && firstName && lastName) {
|
if (options.isRegister && firstName && lastName) {
|
||||||
pendingRegister.value = { firstName, lastName }
|
pendingRegister.value = {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
jobCategory: stored?.jobCategory,
|
||||||
|
referrerAgentCode,
|
||||||
|
}
|
||||||
setPendingRegister(pendingRegister.value)
|
setPendingRegister(pendingRegister.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +84,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
isRegister: options.isRegister,
|
isRegister: options.isRegister,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
|
referrerAgentCode,
|
||||||
})
|
})
|
||||||
if (!result.success) error.value = result.message
|
if (!result.success) error.value = result.message
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type PendingRegister = {
|
|||||||
firstName: string
|
firstName: string
|
||||||
lastName: string
|
lastName: string
|
||||||
jobCategory?: JobCategory
|
jobCategory?: JobCategory
|
||||||
|
referrerAgentCode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JOB_CATEGORY_OPTIONS: Array<{ id: JobCategory; label: string }> = [
|
export const JOB_CATEGORY_OPTIONS: Array<{ id: JobCategory; label: string }> = [
|
||||||
@@ -24,6 +25,7 @@ export function getPendingRegister(): PendingRegister | null {
|
|||||||
firstName: parsed.firstName.trim(),
|
firstName: parsed.firstName.trim(),
|
||||||
lastName: parsed.lastName.trim(),
|
lastName: parsed.lastName.trim(),
|
||||||
jobCategory: parsed.jobCategory,
|
jobCategory: parsed.jobCategory,
|
||||||
|
referrerAgentCode: parsed.referrerAgentCode?.trim() || undefined,
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
@@ -38,6 +40,7 @@ export function setPendingRegister(value: PendingRegister | null) {
|
|||||||
firstName: value.firstName.trim(),
|
firstName: value.firstName.trim(),
|
||||||
lastName: value.lastName.trim(),
|
lastName: value.lastName.trim(),
|
||||||
jobCategory: value.jobCategory,
|
jobCategory: value.jobCategory,
|
||||||
|
referrerAgentCode: value.referrerAgentCode?.trim() || undefined,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ async function resend() {
|
|||||||
isRegister: isRegister.value,
|
isRegister: isRegister.value,
|
||||||
firstName: pending?.firstName,
|
firstName: pending?.firstName,
|
||||||
lastName: pending?.lastName,
|
lastName: pending?.lastName,
|
||||||
|
referrerAgentCode: pending?.referrerAgentCode,
|
||||||
})
|
})
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
digits.value = Array.from({ length: OTP_LENGTH }, () => '')
|
digits.value = Array.from({ length: OTP_LENGTH }, () => '')
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const firstName = ref('')
|
|||||||
const lastName = ref('')
|
const lastName = ref('')
|
||||||
const phone = ref('')
|
const phone = ref('')
|
||||||
const jobCategory = ref<JobCategory | ''>('garment_producer')
|
const jobCategory = ref<JobCategory | ''>('garment_producer')
|
||||||
|
const referrerAgentCode = ref('')
|
||||||
const acceptedTerms = ref(true)
|
const acceptedTerms = ref(true)
|
||||||
const fieldErrors = ref<FieldErrors>({})
|
const fieldErrors = ref<FieldErrors>({})
|
||||||
const formError = ref('')
|
const formError = ref('')
|
||||||
@@ -80,6 +81,7 @@ async function submit() {
|
|||||||
firstName: firstName.value.trim(),
|
firstName: firstName.value.trim(),
|
||||||
lastName: lastName.value.trim(),
|
lastName: lastName.value.trim(),
|
||||||
jobCategory: jobCategory.value as JobCategory,
|
jobCategory: jobCategory.value as JobCategory,
|
||||||
|
referrerAgentCode: referrerAgentCode.value.trim() || undefined,
|
||||||
}
|
}
|
||||||
setPendingRegister(names)
|
setPendingRegister(names)
|
||||||
|
|
||||||
@@ -87,11 +89,18 @@ async function submit() {
|
|||||||
isRegister: true,
|
isRegister: true,
|
||||||
firstName: names.firstName,
|
firstName: names.firstName,
|
||||||
lastName: names.lastName,
|
lastName: names.lastName,
|
||||||
|
referrerAgentCode: names.referrerAgentCode,
|
||||||
})
|
})
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
router.push({ name: 'otp', query: { phone: normalized, mode: 'register' } })
|
router.push({ name: 'otp', query: { phone: normalized, mode: 'register' } })
|
||||||
} else {
|
} else {
|
||||||
formError.value = result.message ?? 'خطا در ارسال کد تأیید.'
|
const msg = result.message ?? 'خطا در ارسال کد تأیید.'
|
||||||
|
if (msg.includes('کد واسطه معتبر نیست')) {
|
||||||
|
fieldErrors.value = { ...fieldErrors.value, referrerAgentCode: 'کد واسطه معتبر نیست' }
|
||||||
|
formError.value = ''
|
||||||
|
} else {
|
||||||
|
formError.value = msg
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -143,6 +152,29 @@ async function submit() {
|
|||||||
</label>
|
</label>
|
||||||
<p v-if="fieldErrors.phone" class="auth-error">{{ fieldErrors.phone }}</p>
|
<p v-if="fieldErrors.phone" class="auth-error">{{ fieldErrors.phone }}</p>
|
||||||
|
|
||||||
|
<label
|
||||||
|
class="auth-field auth-field--stacked"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.referrerAgentCode }"
|
||||||
|
>
|
||||||
|
<span class="auth-field__caption">کد واسطه (اختیاری)</span>
|
||||||
|
<input
|
||||||
|
v-model="referrerAgentCode"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
dir="ltr"
|
||||||
|
class="auth-field__input-ltr"
|
||||||
|
placeholder="مثلاً W-1247"
|
||||||
|
@input="clearField('referrerAgentCode')"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p class="auth-hint">
|
||||||
|
اگر از طریق واسطه معرفی شدهاید، کد واسطه را وارد کنید؛ در غیر این صورت این قسمت را خالی
|
||||||
|
بگذارید.
|
||||||
|
</p>
|
||||||
|
<p v-if="fieldErrors.referrerAgentCode" class="auth-error">
|
||||||
|
{{ fieldErrors.referrerAgentCode }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<p class="job-label">دسته شغلی شما</p>
|
<p class="job-label">دسته شغلی شما</p>
|
||||||
<div class="job-grid" :class="{ 'is-invalid': fieldErrors.jobCategory }">
|
<div class="job-grid" :class="{ 'is-invalid': fieldErrors.jobCategory }">
|
||||||
<button
|
<button
|
||||||
@@ -242,6 +274,49 @@ async function submit() {
|
|||||||
color: #98a2b3;
|
color: #98a2b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-field--stacked {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
height: auto;
|
||||||
|
min-height: 52px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-field__caption {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #475569;
|
||||||
|
text-align: right;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-field__input-ltr {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
color: #1e293b;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-field__input-ltr::placeholder {
|
||||||
|
color: #98a2b3;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-hint {
|
||||||
|
margin: -4px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: #667085;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-error {
|
.auth-error {
|
||||||
margin: -4px 0 0;
|
margin: -4px 0 0;
|
||||||
color: #b42318;
|
color: #b42318;
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ const initials = computed(() => {
|
|||||||
return `${first}${last}` || '؟'
|
return `${first}${last}` || '؟'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const agentReferralCode = computed(() => auth.user?.agentCode?.trim() || '—')
|
||||||
|
|
||||||
function clearField(key: string) {
|
function clearField(key: string) {
|
||||||
fieldErrors.value = clearFieldError(fieldErrors.value, key)
|
fieldErrors.value = clearFieldError(fieldErrors.value, key)
|
||||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||||
@@ -229,6 +231,15 @@ async function save() {
|
|||||||
:error="fieldErrors.nationalId"
|
:error="fieldErrors.nationalId"
|
||||||
@update:model-value="clearField('nationalId')"
|
@update:model-value="clearField('nationalId')"
|
||||||
/>
|
/>
|
||||||
|
<BaseInput
|
||||||
|
v-if="auth.isAgent"
|
||||||
|
class="profile-edit__full"
|
||||||
|
:model-value="agentReferralCode"
|
||||||
|
label="کد واسطه"
|
||||||
|
dir="ltr"
|
||||||
|
disabled
|
||||||
|
hint="این کد را به مشتریان معرفیشده بدهید تا هنگام ثبتنام در اپ وارد کنند."
|
||||||
|
/>
|
||||||
<div v-if="auth.isAgent" class="form-group profile-edit__full">
|
<div v-if="auth.isAgent" class="form-group profile-edit__full">
|
||||||
<label class="form-label" for="profile-address">آدرس (واسطه)</label>
|
<label class="form-label" for="profile-address">آدرس (واسطه)</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ const checksCount = computed(() => {
|
|||||||
const customerCode = computed(() => {
|
const customerCode = computed(() => {
|
||||||
const u = auth.user
|
const u = auth.user
|
||||||
if (!u) return toPersianDigits('—')
|
if (!u) return toPersianDigits('—')
|
||||||
if (auth.isAgent && u.agentCode) return toPersianDigits(u.agentCode)
|
if (auth.isAgent) {
|
||||||
|
return u.agentCode ? toPersianDigits(u.agentCode) : toPersianDigits('—')
|
||||||
|
}
|
||||||
if (u.nationalId) return toPersianDigits(u.nationalId.slice(-6).padStart(6, '0'))
|
if (u.nationalId) return toPersianDigits(u.nationalId.slice(-6).padStart(6, '0'))
|
||||||
return toPersianDigits(u.id?.slice(-6) ?? '—')
|
return toPersianDigits(u.id?.slice(-6) ?? '—')
|
||||||
})
|
})
|
||||||
@@ -73,7 +75,8 @@ const joinDate = computed(() =>
|
|||||||
|
|
||||||
const creditBalance = computed(() => 0)
|
const creditBalance = computed(() => 0)
|
||||||
|
|
||||||
const companyRows = computed(() => [
|
const companyRows = computed(() => {
|
||||||
|
const rows: Array<{ label: string; value: string; badge?: boolean }> = [
|
||||||
{ label: 'نام شرکت', value: auth.fullName || '—' },
|
{ label: 'نام شرکت', value: auth.fullName || '—' },
|
||||||
{ label: 'شناسه ملی', value: auth.user?.nationalId ? toPersianDigits(auth.user.nationalId) : '—' },
|
{ label: 'شناسه ملی', value: auth.user?.nationalId ? toPersianDigits(auth.user.nationalId) : '—' },
|
||||||
{
|
{
|
||||||
@@ -87,7 +90,17 @@ const companyRows = computed(() => [
|
|||||||
badge: true,
|
badge: true,
|
||||||
},
|
},
|
||||||
{ label: 'وضعیت حساب', value: 'فعال', badge: true },
|
{ label: 'وضعیت حساب', value: 'فعال', badge: true },
|
||||||
])
|
]
|
||||||
|
if (auth.isAgent) {
|
||||||
|
rows.splice(1, 0, {
|
||||||
|
label: 'کد واسطه',
|
||||||
|
value: auth.user?.agentCode
|
||||||
|
? toPersianDigits(auth.user.agentCode)
|
||||||
|
: '—',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
})
|
||||||
|
|
||||||
const quickAccess = [
|
const quickAccess = [
|
||||||
{ label: 'حسابهای بانکی', icon: IconCashSettlement },
|
{ label: 'حسابهای بانکی', icon: IconCashSettlement },
|
||||||
|
|||||||
Reference in New Issue
Block a user