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:
2026-08-08 11:50:18 +02:00
co-authored by Cursor
parent 38864704f1
commit 72cc6000f1
20 changed files with 1023 additions and 171 deletions
+58 -26
View File
@@ -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>
+8 -2
View File
@@ -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' } })
+30 -8
View File
@@ -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>
+7 -2
View File
@@ -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;