feat: require payment method when submitting a preorder
Price depends on cash or check terms, so the customer chooses them before admin pricing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import type { SettlementType } from '@/types'
|
||||
|
||||
defineProps<{
|
||||
settlementType: SettlementType | null
|
||||
checkTermMonths: number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:settlementType': [value: SettlementType]
|
||||
'update:checkTermMonths': [value: number]
|
||||
}>()
|
||||
|
||||
const methods: { value: SettlementType; title: string; hint: string }[] = [
|
||||
{ value: 'cash', title: 'نقدی', hint: 'کل مبلغ بهصورت نقدی' },
|
||||
{ value: 'cash_check', title: 'نصف نقد، نصف چکی', hint: 'نیمی نقد و نیمی چک' },
|
||||
{ value: 'check', title: 'چکی', hint: 'کل مبلغ با چک' },
|
||||
]
|
||||
|
||||
const terms = [1, 2, 3, 4]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset class="pay-plan">
|
||||
<legend class="pay-plan__legend">روش پرداخت</legend>
|
||||
<p class="pay-plan__note">قیمت پیشسفارش به روش پرداخت بستگی دارد. قبل از اعلام قیمت باید مشخص شود.</p>
|
||||
<div class="pay-plan__methods">
|
||||
<button
|
||||
v-for="method in methods"
|
||||
:key="method.value"
|
||||
type="button"
|
||||
class="pay-plan__method"
|
||||
:class="{ 'pay-plan__method--active': settlementType === method.value }"
|
||||
@click="emit('update:settlementType', method.value)"
|
||||
>
|
||||
<strong>{{ method.title }}</strong>
|
||||
<span>{{ method.hint }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="settlementType === 'check' || settlementType === 'cash_check'" class="pay-plan__terms">
|
||||
<span class="pay-plan__terms-label">مدت چک</span>
|
||||
<div class="pay-plan__terms-row">
|
||||
<button
|
||||
v-for="month in terms"
|
||||
:key="month"
|
||||
type="button"
|
||||
class="pay-plan__term"
|
||||
:class="{ 'pay-plan__term--active': checkTermMonths === month }"
|
||||
@click="emit('update:checkTermMonths', month)"
|
||||
>
|
||||
{{ month }} ماهه
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pay-plan {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pay-plan__legend {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pay-plan__note {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary, #667085);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.pay-plan__methods,
|
||||
.pay-plan__terms-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pay-plan__method,
|
||||
.pay-plan__term {
|
||||
text-align: right;
|
||||
border: 1px solid var(--color-border, #e4e7ec);
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pay-plan__method strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pay-plan__method span {
|
||||
color: var(--color-text-secondary, #667085);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pay-plan__method--active,
|
||||
.pay-plan__term--active {
|
||||
border-color: var(--color-primary, #1570ef);
|
||||
background: color-mix(in srgb, var(--color-primary, #1570ef) 8%, white);
|
||||
}
|
||||
|
||||
.pay-plan__terms-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pay-plan__terms-row {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pay-plan__term {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
min-width: 72px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import { useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { confirm } from '@/composables/useConfirm'
|
||||
import type { SettlementType } from '@/types'
|
||||
|
||||
export function useCartPage() {
|
||||
const router = useRouter()
|
||||
@@ -11,6 +12,8 @@ export function useCartPage() {
|
||||
const showFullDialog = ref(false)
|
||||
const submitting = ref(false)
|
||||
const submitError = ref('')
|
||||
const settlementType = ref<SettlementType | null>(null)
|
||||
const checkTermMonths = ref<number | null>(null)
|
||||
|
||||
const items = computed(() => cartStore.activeCart?.items ?? [])
|
||||
const isEmpty = computed(() => items.value.length === 0)
|
||||
@@ -33,11 +36,22 @@ export function useCartPage() {
|
||||
}
|
||||
|
||||
/**
|
||||
* ثبت سفارش بدون آدرس و بدون انتخاب تسویه.
|
||||
* سفارش هنوز نهایی نیست؛ قیمتگذاری و روش تسویه بعد از بررسی مدیریت است.
|
||||
* پیشسفارش با روش پرداخت (و مدت چک در صورت نیاز) ثبت میشود.
|
||||
* قیمتگذاری مدیریت بعد از این انتخاب انجام میشود.
|
||||
*/
|
||||
async function submitOrder() {
|
||||
if (!cartStore.activeCart || items.value.length === 0 || submitting.value) return
|
||||
if (!settlementType.value) {
|
||||
submitError.value = 'قبل از ثبت پیشسفارش روش پرداخت را انتخاب کنید.'
|
||||
return
|
||||
}
|
||||
if (
|
||||
(settlementType.value === 'check' || settlementType.value === 'cash_check') &&
|
||||
!checkTermMonths.value
|
||||
) {
|
||||
submitError.value = 'مدت چک را از ۱ تا ۴ ماه انتخاب کنید.'
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
submitError.value = ''
|
||||
try {
|
||||
@@ -46,7 +60,10 @@ export function useCartPage() {
|
||||
submitError.value = 'سبد خرید خالی است یا با سرور همگام نشد.'
|
||||
return
|
||||
}
|
||||
const order = await orderStore.submitOrder(cartStore.activeCart)
|
||||
const order = await orderStore.submitOrder(cartStore.activeCart, {
|
||||
settlementType: settlementType.value,
|
||||
checkTermMonths: checkTermMonths.value,
|
||||
})
|
||||
await cartStore.hydrate()
|
||||
router.replace(`/orders/${order.id}/success`)
|
||||
} catch (e) {
|
||||
@@ -90,6 +107,8 @@ export function useCartPage() {
|
||||
showFullDialog,
|
||||
submitting,
|
||||
submitError,
|
||||
settlementType,
|
||||
checkTermMonths,
|
||||
removeItem,
|
||||
proceedToCheckout,
|
||||
proceedAnyway,
|
||||
|
||||
+31
-4
@@ -117,11 +117,13 @@ export const useOrderStore = defineStore('order', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* سفارش بدون روش تسویه ثبت میشود؛ مشتری بعد از قیمتگذاری مدیریت
|
||||
* روش تسویه را انتخاب میکند.
|
||||
* پیشسفارش همراه روش پرداخت ثبت میشود تا قیمتگذاری بر همان اساس انجام شود.
|
||||
* اقلام همه شرکتها در یک فاکتور واحد ثبت میشوند.
|
||||
*/
|
||||
async function submitOrder(cart: Cart): Promise<Order> {
|
||||
async function submitOrder(
|
||||
cart: Cart,
|
||||
payment: { settlementType: SettlementType; checkTermMonths?: number | null },
|
||||
): Promise<Order> {
|
||||
if (!cart.items.length) {
|
||||
throw new Error('سبد خرید خالی است.')
|
||||
}
|
||||
@@ -143,6 +145,9 @@ export const useOrderStore = defineStore('order', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
companyId: companyIds[0],
|
||||
settlementType: payment.settlementType,
|
||||
checkTermMonths:
|
||||
payment.settlementType === 'cash' ? undefined : payment.checkTermMonths ?? undefined,
|
||||
notes: notes || undefined,
|
||||
items: cart.items.map((item) => ({
|
||||
productId: item.productId,
|
||||
@@ -159,7 +164,28 @@ export const useOrderStore = defineStore('order', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** انتخاب روش تسویه پس از صدور فاکتور. */
|
||||
async function setPaymentPlan(
|
||||
orderId: string,
|
||||
settlementType: SettlementType,
|
||||
checkTermMonths?: number | null,
|
||||
): Promise<Order> {
|
||||
try {
|
||||
const order = await apiRequest<Order>(`/orders/${orderId}/payment-plan`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
settlementType,
|
||||
checkTermMonths:
|
||||
settlementType === 'cash' ? undefined : checkTermMonths ?? undefined,
|
||||
},
|
||||
})
|
||||
upsertOrder(order)
|
||||
return order
|
||||
} catch (e) {
|
||||
throw new Error(e instanceof ApiError ? e.message : 'ثبت روش پرداخت ناموفق بود.')
|
||||
}
|
||||
}
|
||||
|
||||
/** انتخاب روش تسویه پس از صدور فاکتور؛ فقط برای سفارشهایی که روش پرداخت ندارند. */
|
||||
async function chooseSettlement(
|
||||
orderId: string,
|
||||
settlementType: SettlementType,
|
||||
@@ -273,6 +299,7 @@ export const useOrderStore = defineStore('order', () => {
|
||||
removeDraftCheck,
|
||||
getDraftRass,
|
||||
submitOrder,
|
||||
setPaymentPlan,
|
||||
chooseSettlement,
|
||||
submitCashReceipt,
|
||||
updateOrderChecks,
|
||||
|
||||
+3
-1
@@ -187,8 +187,10 @@ export interface Order {
|
||||
companyName: string
|
||||
items: OrderItem[]
|
||||
totalMeterage: number
|
||||
/** تا وقتی مشتری بعد از قیمتگذاری روش تسویه را انتخاب نکرده، خالی است. */
|
||||
/** هنگام ثبت پیشسفارش انتخاب میشود. سفارشهای قدیمی یا ثبتشده توسط ادمین ممکن است خالی باشند. */
|
||||
settlementType: SettlementType | null
|
||||
/** مدت چک به ماه (۱ تا ۴) برای روشهای چکی */
|
||||
checkTermMonths?: number | null
|
||||
checkMode?: CheckRegistrationMode
|
||||
checks?: CheckData[]
|
||||
checkRassDays?: number
|
||||
|
||||
@@ -48,13 +48,13 @@ const STATUS_HERO: Partial<Record<OrderStatus, StatusHeroConfig>> = {
|
||||
variant: 'waiting',
|
||||
title: 'پیشسفارش ثبت شد',
|
||||
description:
|
||||
'سفارش شما برای بررسی و قیمتگذاری به مدیریت ارسال شد. پس از صدور فاکتور میتوانید روش تسویه (نقد یا چک) را انتخاب کنید.',
|
||||
'سفارش شما برای بررسی و قیمتگذاری به مدیریت ارسال شد. روش پرداخت هنگام ثبت پیشسفارش مشخص شده است.',
|
||||
},
|
||||
awaiting_review: {
|
||||
variant: 'waiting',
|
||||
title: 'در انتظار بررسی مدیریت',
|
||||
description:
|
||||
'پیشسفارش شما در صف بررسی است. پس از قیمتگذاری و صدور فاکتور، از طریق همین صفحه برای انتخاب تسویه مطلع میشوید.',
|
||||
'پیشسفارش شما در صف بررسی است. قیمت بر اساس روش پرداختی که انتخاب کردهاید اعلام میشود.',
|
||||
},
|
||||
pricing: {
|
||||
variant: 'waiting',
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { OrderStatus, SettlementType } from '@/types'
|
||||
|
||||
export const SETTLEMENT_TYPE_LABELS: Record<SettlementType, string> = {
|
||||
cash: 'تسویه نقدی',
|
||||
check: 'تسویه چکی',
|
||||
cash_check: 'تسویه نقدی-چکی',
|
||||
cash: 'نقدی',
|
||||
check: 'چکی',
|
||||
cash_check: 'نصف نقد، نصف چکی',
|
||||
}
|
||||
|
||||
export function settlementTypeLabel(
|
||||
type: SettlementType | null | undefined,
|
||||
): string {
|
||||
if (!type) return 'در انتظار انتخاب مشتری'
|
||||
if (!type) return 'هنوز انتخاب نشده'
|
||||
return SETTLEMENT_TYPE_LABELS[type] ?? type
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ const {
|
||||
showFullDialog,
|
||||
submitting,
|
||||
submitError,
|
||||
settlementType,
|
||||
checkTermMonths,
|
||||
removeItem,
|
||||
proceedToCheckout,
|
||||
proceedAnyway,
|
||||
@@ -43,6 +45,10 @@ function goToOrdering() {
|
||||
:remaining-capacity="cartStore.remainingCapacity"
|
||||
:is-empty="isEmpty"
|
||||
:submitting="submitting"
|
||||
:settlement-type="settlementType"
|
||||
:check-term-months="checkTermMonths"
|
||||
@update:settlement-type="settlementType = $event"
|
||||
@update:check-term-months="checkTermMonths = $event"
|
||||
@back="router.push('/')"
|
||||
@remove="removeItem"
|
||||
@checkout="proceedToCheckout"
|
||||
@@ -59,6 +65,10 @@ function goToOrdering() {
|
||||
:remaining-capacity="cartStore.remainingCapacity"
|
||||
:is-empty="isEmpty"
|
||||
:submitting="submitting"
|
||||
:settlement-type="settlementType"
|
||||
:check-term-months="checkTermMonths"
|
||||
@update:settlement-type="settlementType = $event"
|
||||
@update:check-term-months="checkTermMonths = $event"
|
||||
@remove="removeItem"
|
||||
@checkout="proceedToCheckout"
|
||||
@select-company="goToOrdering()"
|
||||
|
||||
@@ -10,7 +10,8 @@ import IconLightbulb from '@/components/icons/IconLightbulb.vue'
|
||||
import IconLayoutGrid from '@/components/icons/IconLayoutGrid.vue'
|
||||
import IconDiamond from '@/components/icons/IconDiamond.vue'
|
||||
import { formatMeterage, formatNumber } from '@/utils/format'
|
||||
import type { CartItem } from '@/types'
|
||||
import type { CartItem, SettlementType } from '@/types'
|
||||
import PreorderPaymentFields from '@/components/orders/PreorderPaymentFields.vue'
|
||||
|
||||
defineProps<{
|
||||
items: CartItem[]
|
||||
@@ -27,6 +28,8 @@ defineProps<{
|
||||
remainingCapacity: number
|
||||
isEmpty: boolean
|
||||
submitting?: boolean
|
||||
settlementType: SettlementType | null
|
||||
checkTermMonths: number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -34,6 +37,8 @@ const emit = defineEmits<{
|
||||
checkout: []
|
||||
selectCompany: []
|
||||
back: []
|
||||
'update:settlementType': [value: SettlementType]
|
||||
'update:checkTermMonths': [value: number]
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
@@ -180,6 +185,12 @@ const quickLinks = [
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<PreorderPaymentFields
|
||||
:settlement-type="settlementType"
|
||||
:check-term-months="checkTermMonths"
|
||||
@update:settlement-type="emit('update:settlementType', $event)"
|
||||
@update:check-term-months="emit('update:checkTermMonths', $event)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="cart-summary-card__cta"
|
||||
@@ -190,7 +201,7 @@ const quickLinks = [
|
||||
{{ submitting ? 'در حال ثبت…' : 'ثبت پیشسفارش' }}
|
||||
</button>
|
||||
<p class="cart-summary-card__note">
|
||||
پس از ثبت سفارش، مدیریت قیمتگذاری میکند و فاکتور صادر میشود؛ سپس روش تسویه را انتخاب میکنید.
|
||||
روش پرداخت الان ثبت میشود. مدیریت بعد از آن قیمت و متراژ را اعلام میکند.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import EmptyState from '@/components/ui/EmptyState.vue'
|
||||
import IconTrash from '@/components/icons/IconTrash.vue'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import { formatMeterage, formatNumber } from '@/utils/format'
|
||||
import type { CartItem } from '@/types'
|
||||
import type { CartItem, SettlementType } from '@/types'
|
||||
import PreorderPaymentFields from '@/components/orders/PreorderPaymentFields.vue'
|
||||
|
||||
defineProps<{
|
||||
items: CartItem[]
|
||||
@@ -21,6 +22,8 @@ defineProps<{
|
||||
remainingCapacity: number
|
||||
isEmpty: boolean
|
||||
submitting?: boolean
|
||||
settlementType: SettlementType | null
|
||||
checkTermMonths: number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -28,6 +31,8 @@ const emit = defineEmits<{
|
||||
remove: [id: string]
|
||||
checkout: []
|
||||
selectCompany: []
|
||||
'update:settlementType': [value: SettlementType]
|
||||
'update:checkTermMonths': [value: number]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -98,8 +103,14 @@ const emit = defineEmits<{
|
||||
<strong>{{ formatMeterage(remainingCapacity) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<PreorderPaymentFields
|
||||
:settlement-type="settlementType"
|
||||
:check-term-months="checkTermMonths"
|
||||
@update:settlement-type="emit('update:settlementType', $event)"
|
||||
@update:check-term-months="emit('update:checkTermMonths', $event)"
|
||||
/>
|
||||
<p class="cart-mobile-page__hint">
|
||||
پس از ثبت سفارش، کارشناسان قیمتگذاری میکنند و فاکتور برای شما صادر میشود.
|
||||
پس از ثبت، مدیریت بر اساس همین روش پرداخت قیمت و متراژ را اعلام میکند.
|
||||
</p>
|
||||
<BaseButton block :loading="submitting" @click="emit('checkout')">ثبت پیشسفارش</BaseButton>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,8 @@ import { ref, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import type { Order } from '@/types'
|
||||
import type { Order, SettlementType } from '@/types'
|
||||
import PreorderPaymentFields from '@/components/orders/PreorderPaymentFields.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import CurrencyInput from '@/components/ui/CurrencyInput.vue'
|
||||
import PersianDateInput from '@/components/ui/PersianDateInput.vue'
|
||||
@@ -155,7 +156,37 @@ const trackingFooterLabel = computed(() =>
|
||||
)
|
||||
|
||||
function settlementLabel(type: Order['settlementType']) {
|
||||
return settlementTypeLabel(type)
|
||||
const base = settlementTypeLabel(type)
|
||||
const months = order.value?.checkTermMonths
|
||||
if ((type === 'check' || type === 'cash_check') && months) {
|
||||
return `${base} · چک ${months} ماهه`
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
const planType = ref<SettlementType | null>(null)
|
||||
const planMonths = ref<number | null>(null)
|
||||
const planSaving = ref(false)
|
||||
|
||||
async function savePaymentPlan() {
|
||||
const current = order.value
|
||||
if (!current || !planType.value) {
|
||||
formError.value = 'روش پرداخت را انتخاب کنید.'
|
||||
return
|
||||
}
|
||||
if ((planType.value === 'check' || planType.value === 'cash_check') && !planMonths.value) {
|
||||
formError.value = 'مدت چک را از ۱ تا ۴ ماه انتخاب کنید.'
|
||||
return
|
||||
}
|
||||
planSaving.value = true
|
||||
formError.value = ''
|
||||
try {
|
||||
await orderStore.setPaymentPlan(current.id, planType.value, planMonths.value)
|
||||
} catch (e) {
|
||||
formError.value = e instanceof Error ? e.message : 'ثبت روش پرداخت ناموفق بود.'
|
||||
} finally {
|
||||
planSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function showCashTracking(order: Order) {
|
||||
@@ -314,6 +345,19 @@ async function submitReceipt() {
|
||||
<p class="desktop-page__subtitle">{{ order.orderNumber }} · {{ order.companyName }}</p>
|
||||
</header>
|
||||
|
||||
<div
|
||||
v-if="order.status === 'awaiting_review' && !order.settlementType"
|
||||
class="order-info-card desktop-only"
|
||||
>
|
||||
<PreorderPaymentFields
|
||||
:settlement-type="planType"
|
||||
:check-term-months="planMonths"
|
||||
@update:settlement-type="planType = $event"
|
||||
@update:check-term-months="planMonths = $event"
|
||||
/>
|
||||
<BaseButton :loading="planSaving" @click="savePaymentPlan">ثبت روش پرداخت</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="order-detail-page__body">
|
||||
<div v-if="!showTracking" class="order-info-card mobile-only">
|
||||
<div class="order-info-card__head">
|
||||
@@ -340,6 +384,19 @@ async function submitReceipt() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="order.status === 'awaiting_review' && !order.settlementType"
|
||||
class="order-info-card__plan"
|
||||
>
|
||||
<PreorderPaymentFields
|
||||
:settlement-type="planType"
|
||||
:check-term-months="planMonths"
|
||||
@update:settlement-type="planType = $event"
|
||||
@update:check-term-months="planMonths = $event"
|
||||
/>
|
||||
<BaseButton block :loading="planSaving" @click="savePaymentPlan">ثبت روش پرداخت</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="order-info-card__row order-info-card__row--last">
|
||||
<span class="order-info-card__key">مبلغ فاکتور</span>
|
||||
<strong class="order-info-card__amount">
|
||||
|
||||
Reference in New Issue
Block a user