Improve auth, checkout, and API client for real backend.
Wire OTP and cart/catalog to the API with Vite proxy, fix resend/register name persistence, harden order submit errors, and polish OTP and cart footer UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+58
-26
@@ -6,22 +6,29 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import AuthThemeToggle from '@/components/ui/AuthThemeToggle.vue'
|
||||
import { toPersianDigits, extractNumericString } from '@/utils/format'
|
||||
import IconEditPen from '@/components/icons/IconEditPen.vue'
|
||||
import { toPersianDigits, extractNumericString, normalizePhone } from '@/utils/format'
|
||||
import {
|
||||
FORM_VALIDATION_SUMMARY,
|
||||
clearFieldError,
|
||||
hasFieldErrors,
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
import { getPendingRegister } from '@/utils/pendingRegister'
|
||||
|
||||
const OTP_LENGTH = 6
|
||||
const OTP_LENGTH = 4
|
||||
const RESEND_SECONDS = 60
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const phone = computed(() => (route.query.phone as string) ?? '')
|
||||
const phone = computed(() => {
|
||||
const raw = route.query.phone
|
||||
const value = Array.isArray(raw) ? raw[0] : raw
|
||||
return normalizePhone(typeof value === 'string' ? value : '')
|
||||
})
|
||||
const isRegister = computed(() => route.query.mode === 'register')
|
||||
const digits = ref<string[]>(Array.from({ length: OTP_LENGTH }, () => ''))
|
||||
const inputRefs = ref<(HTMLInputElement | null)[]>([])
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
@@ -171,7 +178,7 @@ async function submit() {
|
||||
return
|
||||
}
|
||||
|
||||
const result = await auth.confirmOtp(phone.value, code.value)
|
||||
const result = await auth.confirmOtp(phone.value, extractNumericString(code.value))
|
||||
if (result.success) {
|
||||
router.replace('/')
|
||||
} else {
|
||||
@@ -184,12 +191,23 @@ async function submit() {
|
||||
async function resend() {
|
||||
if (!canResend.value) return
|
||||
|
||||
const isRegister = route.query.mode === 'register'
|
||||
const result = await auth.requestOtp(phone.value, { isRegister })
|
||||
formError.value = ''
|
||||
fieldErrors.value = {}
|
||||
|
||||
const pending = isRegister.value ? getPendingRegister() : null
|
||||
|
||||
if (isRegister.value && !pending) {
|
||||
formError.value = 'برای ارسال مجدد کد، نام و نام خانوادگی لازم است. لطفاً دوباره ثبتنام را شروع کنید.'
|
||||
return
|
||||
}
|
||||
|
||||
const result = await auth.requestOtp(phone.value, {
|
||||
isRegister: isRegister.value,
|
||||
firstName: pending?.firstName,
|
||||
lastName: pending?.lastName,
|
||||
})
|
||||
if (result.success) {
|
||||
digits.value = Array.from({ length: OTP_LENGTH }, () => '')
|
||||
formError.value = ''
|
||||
fieldErrors.value = {}
|
||||
startTimer()
|
||||
focusInput(0)
|
||||
} else {
|
||||
@@ -198,11 +216,14 @@ async function resend() {
|
||||
}
|
||||
|
||||
function changePhone() {
|
||||
const isRegister = route.query.mode === 'register'
|
||||
router.replace({ name: isRegister ? 'register' : 'login' })
|
||||
router.replace({ name: isRegister.value ? 'register' : 'login' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!phone.value) {
|
||||
router.replace({ name: 'login' })
|
||||
return
|
||||
}
|
||||
startTimer()
|
||||
focusInput(0)
|
||||
})
|
||||
@@ -222,8 +243,12 @@ onMounted(() => {
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="otp-0">کد تأیید</label>
|
||||
<div class="flex justify-center gap-2.5 ltr" dir="ltr" role="group" aria-label="کد تأیید">
|
||||
<div
|
||||
class="otp-row flex w-full max-w-full justify-center ltr"
|
||||
dir="ltr"
|
||||
role="group"
|
||||
aria-label="کد تأیید"
|
||||
>
|
||||
<input
|
||||
v-for="(digit, index) in digits"
|
||||
:id="`otp-${index}`"
|
||||
@@ -257,24 +282,39 @@ onMounted(() => {
|
||||
<template v-if="canResend">ارسال مجدد کد</template>
|
||||
<template v-else>ارسال مجدد کد ({{ timerLabel }})</template>
|
||||
</BaseButton>
|
||||
<BaseButton block variant="ghost" type="button" class="mt-2" @click="changePhone">
|
||||
تغییر شماره موبایل
|
||||
</BaseButton>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 mx-auto flex items-center justify-center gap-1.5 border-0 bg-transparent p-0 text-sm text-muted cursor-pointer transition-colors hover:text-text"
|
||||
@click="changePhone"
|
||||
>
|
||||
<span class="ltr tabular-nums" dir="ltr">{{ toPersianDigits(phone) }}</span>
|
||||
<span>ویرایش شماره</span>
|
||||
<IconEditPen :size="18" :stroke-width="1.5" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.otp-row {
|
||||
gap: clamp(0.25rem, 1.8vw, 0.625rem);
|
||||
}
|
||||
|
||||
.otp-box {
|
||||
width: 48px;
|
||||
height: 52px;
|
||||
flex: 1 1 0;
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
max-width: 48px;
|
||||
aspect-ratio: 48 / 52;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-size: 1.25rem;
|
||||
font-size: clamp(0.95rem, 4.2vw, 1.25rem);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
caret-color: var(--color-primary);
|
||||
@@ -295,12 +335,4 @@ onMounted(() => {
|
||||
font-size: 0.8125rem;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.otp-box {
|
||||
width: 42px;
|
||||
height: 48px;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
hasFieldErrors,
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
import { setPendingRegister } from '@/utils/pendingRegister'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
@@ -52,10 +53,15 @@ async function submit() {
|
||||
return
|
||||
}
|
||||
|
||||
const result = await auth.requestOtp(normalized, {
|
||||
isRegister: true,
|
||||
const names = {
|
||||
firstName: firstName.value.trim(),
|
||||
lastName: lastName.value.trim(),
|
||||
}
|
||||
setPendingRegister(names)
|
||||
|
||||
const result = await auth.requestOtp(normalized, {
|
||||
isRegister: true,
|
||||
...names,
|
||||
})
|
||||
if (result.success) {
|
||||
router.push({ name: 'otp', query: { phone: normalized, mode: 'register' } })
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { useCatalogStore } from '@/stores/catalog'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import IconSuccessCircle from '@/components/icons/IconSuccessCircle.vue'
|
||||
import { toPersianDigits } from '@/utils/format'
|
||||
import type { BankAccount, Order } from '@/types'
|
||||
@@ -18,6 +19,8 @@ const authStore = useAuthStore()
|
||||
const catalogStore = useCatalogStore()
|
||||
const bankAccounts = ref<BankAccount[]>([])
|
||||
const submitting = ref(false)
|
||||
const formError = ref('')
|
||||
const createdOrder = ref<Order | null>(null)
|
||||
|
||||
const companyId = computed(() => {
|
||||
const firstItem = cartStore.activeCart?.items[0]
|
||||
@@ -39,7 +42,7 @@ onMounted(async () => {
|
||||
const expiryMessage = computed(() => {
|
||||
const hour = catalogStore.getCompany(companyId.value)?.cashInvoiceValidUntilHour ?? 17
|
||||
const time = toPersianDigits(String(hour).padStart(2, '0'))
|
||||
return `این فاکتور تا ساعت ${time}:۰۰ امروز معتبر است.`
|
||||
return `پس از ثبت، فاکتور تا ساعت ${time}:۰۰ امروز معتبر است.`
|
||||
})
|
||||
|
||||
const accountsSectionTitle = computed(() => {
|
||||
@@ -59,14 +62,17 @@ function formatIban(value: string) {
|
||||
}
|
||||
|
||||
async function createOrder(): Promise<Order | null> {
|
||||
if (createdOrder.value) return createdOrder.value
|
||||
if (!authStore.user || !cartStore.activeCart || submitting.value) return null
|
||||
formError.value = ''
|
||||
submitting.value = true
|
||||
try {
|
||||
const order = await orderStore.submitOrder(authStore.user.id, cartStore.activeCart, 'cash')
|
||||
createdOrder.value = order
|
||||
await cartStore.hydrate()
|
||||
return order
|
||||
} catch (e) {
|
||||
alert(e instanceof Error ? e.message : 'ثبت سفارش ناموفق بود.')
|
||||
formError.value = e instanceof Error ? e.message : 'ثبت سفارش ناموفق بود.'
|
||||
return null
|
||||
} finally {
|
||||
submitting.value = false
|
||||
@@ -97,9 +103,13 @@ async function registerReceipt() {
|
||||
/>
|
||||
|
||||
<div class="cash-page__scroll page__scroll">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div class="cash-page__success">
|
||||
<IconSuccessCircle :size="56" />
|
||||
<p class="cash-page__success-title">فاکتور شما ثبت شد.</p>
|
||||
<p class="cash-page__success-title">
|
||||
{{ createdOrder ? 'فاکتور شما ثبت شد.' : 'آماده ثبت فاکتور نقدی' }}
|
||||
</p>
|
||||
<p class="cash-page__success-subtitle">{{ expiryMessage }}</p>
|
||||
</div>
|
||||
|
||||
@@ -128,8 +138,12 @@ async function registerReceipt() {
|
||||
</div>
|
||||
|
||||
<div class="cash-page__footer">
|
||||
<BaseButton variant="outline" block @click="viewOrder">مشاهده سفارش</BaseButton>
|
||||
<BaseButton block class="cash-page__submit" @click="registerReceipt">ثبت رسید پرداخت</BaseButton>
|
||||
<BaseButton variant="outline" block :loading="submitting" @click="viewOrder">
|
||||
ثبت و مشاهده سفارش
|
||||
</BaseButton>
|
||||
<BaseButton block class="cash-page__submit" :loading="submitting" @click="registerReceipt">
|
||||
ثبت سفارش و رسید پرداخت
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -142,9 +156,13 @@ async function registerReceipt() {
|
||||
|
||||
<div class="cash-page__success">
|
||||
<IconSuccessCircle :size="56" />
|
||||
<p class="cash-page__success-title">فاکتور شما ثبت شد.</p>
|
||||
<p class="cash-page__success-title">
|
||||
{{ createdOrder ? 'فاکتور شما ثبت شد.' : 'آماده ثبت فاکتور نقدی' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<section class="accounts-panel">
|
||||
<h2 class="accounts-panel__title">{{ accountsSectionTitle }}</h2>
|
||||
<div class="desktop-grid-2">
|
||||
@@ -166,8 +184,12 @@ async function registerReceipt() {
|
||||
</section>
|
||||
|
||||
<div class="cash-desktop-page__actions">
|
||||
<BaseButton variant="outline" class="desktop-btn-compact" @click="viewOrder">مشاهده سفارش</BaseButton>
|
||||
<BaseButton class="desktop-btn-compact" @click="registerReceipt">ثبت رسید پرداخت</BaseButton>
|
||||
<BaseButton variant="outline" class="desktop-btn-compact" :loading="submitting" @click="viewOrder">
|
||||
ثبت و مشاهده سفارش
|
||||
</BaseButton>
|
||||
<BaseButton class="desktop-btn-compact" :loading="submitting" @click="registerReceipt">
|
||||
ثبت سفارش و رسید پرداخت
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import EmptyState from '@/components/ui/EmptyState.vue'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import { formatCurrency, formatDate, formatNumber, toPersianDigits } from '@/utils/format'
|
||||
@@ -17,6 +18,7 @@ const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
const orderStore = useOrderStore()
|
||||
const authStore = useAuthStore()
|
||||
const formError = ref('')
|
||||
|
||||
const isCheckout = computed(() => route.name === 'check-list')
|
||||
|
||||
@@ -88,6 +90,7 @@ function toggleMenu(id: string) {
|
||||
|
||||
async function submitOrder() {
|
||||
if (!authStore.user || !cartStore.activeCart || checks.value.length === 0) return
|
||||
formError.value = ''
|
||||
|
||||
try {
|
||||
const order = await orderStore.submitOrder(
|
||||
@@ -100,7 +103,7 @@ async function submitOrder() {
|
||||
await cartStore.hydrate()
|
||||
router.replace(`/orders/${order.id}/success`)
|
||||
} catch (e) {
|
||||
alert(e instanceof Error ? e.message : 'ثبت سفارش ناموفق بود.')
|
||||
formError.value = e instanceof Error ? e.message : 'ثبت سفارش ناموفق بود.'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -127,6 +130,8 @@ async function submitOrder() {
|
||||
|
||||
<template v-else>
|
||||
<div class="check-list-page__scroll page__scroll">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div class="summary-cards">
|
||||
<div class="summary-card">
|
||||
<span class="summary-card__label">مجموع مبلغ چکها</span>
|
||||
|
||||
@@ -126,7 +126,7 @@ const emit = defineEmits<{
|
||||
}
|
||||
|
||||
.product-mobile-page__scroll {
|
||||
padding-bottom: 76px;
|
||||
padding-bottom: 96px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
Reference in New Issue
Block a user