Gate order actions by status, add final-approve after checks, and select customers in new orders.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,12 +4,13 @@ import AdminIcon from '@/components/icons/AdminIcon.vue'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
import BaseModal from '@/components/ui/BaseModal.vue'
|
||||
import OrderDetailContent from '@/views/orders/components/OrderDetailContent.vue'
|
||||
import OrderAddModal from '@/views/orders/components/OrderAddModal.vue'
|
||||
import { useOrdersPageContext } from '@/composables/useOrdersPage'
|
||||
import { ORDER_TABS } from '@/utils/orderStatus'
|
||||
import { formatCurrency, formatDateTime, formatMeterage } from '@/utils/format'
|
||||
|
||||
const isMobile = useMediaQuery('(max-width: 767px)')
|
||||
/** Matches `.compact-only` visible range (< 1280px) */
|
||||
const isCompactLayout = useMediaQuery('(max-width: 1279px)')
|
||||
|
||||
const {
|
||||
activeTab,
|
||||
@@ -141,7 +142,7 @@ function handleCashReceipt() {
|
||||
aria-label="بازگشت به لیست"
|
||||
@click="closeDetailPanel"
|
||||
>
|
||||
<AdminIcon name="chevron-left" :size="20" />
|
||||
<AdminIcon name="chevron-back" :size="20" />
|
||||
</button>
|
||||
<h2 class="admin-detail-panel__title">جزئیات سفارش</h2>
|
||||
</div>
|
||||
@@ -161,7 +162,7 @@ function handleCashReceipt() {
|
||||
|
||||
<!-- Tablet / small desktop: modal -->
|
||||
<BaseModal
|
||||
:open="!isMobile && showDetailModal && !!selectedOrder"
|
||||
:open="isCompactLayout && !isMobile && showDetailModal && !!selectedOrder"
|
||||
title="جزئیات سفارش"
|
||||
size="xl"
|
||||
@close="closeDetailModal"
|
||||
@@ -179,8 +180,6 @@ function handleCashReceipt() {
|
||||
/>
|
||||
</template>
|
||||
</BaseModal>
|
||||
|
||||
<OrderAddModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import AdminIcon from '@/components/icons/AdminIcon.vue'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
import BaseModal from '@/components/ui/BaseModal.vue'
|
||||
import OrderDetailContent from '@/views/orders/components/OrderDetailContent.vue'
|
||||
import OrderAddModal from '@/views/orders/components/OrderAddModal.vue'
|
||||
import { useOrdersPageContext } from '@/composables/useOrdersPage'
|
||||
import { ORDER_TABS } from '@/utils/orderStatus'
|
||||
import { ORDER_TABS, canPriceOrder } from '@/utils/orderStatus'
|
||||
import { formatCurrency, formatDateTime, formatMeterage } from '@/utils/format'
|
||||
import type { Order } from '@/types'
|
||||
|
||||
/** Matches `.desktop-only` in responsive.css (>= 1280px) */
|
||||
const isDesktopLayout = useMediaQuery('(min-width: 1280px)')
|
||||
|
||||
const {
|
||||
activeTab,
|
||||
selectedOrderId,
|
||||
@@ -241,6 +244,7 @@ function runMenuAction(action: 'detail' | 'pricing' | 'checks' | 'cash') {
|
||||
جزئیات سفارش
|
||||
</button>
|
||||
<button
|
||||
v-if="menuOrder && canPriceOrder(menuOrder)"
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="orders-page__menu-item"
|
||||
@@ -270,7 +274,7 @@ function runMenuAction(action: 'detail' | 'pricing' | 'checks' | 'cash') {
|
||||
</Teleport>
|
||||
|
||||
<BaseModal
|
||||
:open="showDetailModal && !!selectedOrder"
|
||||
:open="isDesktopLayout && showDetailModal && !!selectedOrder"
|
||||
title="جزئیات سفارش"
|
||||
size="xl"
|
||||
@close="closeDetailModal"
|
||||
@@ -306,8 +310,6 @@ function runMenuAction(action: 'detail' | 'pricing' | 'checks' | 'cash') {
|
||||
</button>
|
||||
</template>
|
||||
</BaseModal>
|
||||
|
||||
<OrderAddModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
import BaseModal from '@/components/ui/BaseModal.vue'
|
||||
import AdminSelect from '@/components/ui/AdminSelect.vue'
|
||||
import { useOrdersPageContext } from '@/composables/useOrdersPage'
|
||||
@@ -10,6 +10,8 @@ const {
|
||||
form,
|
||||
isFormValid,
|
||||
companies,
|
||||
activeCustomers,
|
||||
applyCustomer,
|
||||
closeAddModal,
|
||||
submitAddOrder,
|
||||
} = useOrdersPageContext()
|
||||
@@ -18,10 +20,31 @@ const companyOptions = computed(() =>
|
||||
companies.value.map((company) => ({ value: company.id, label: company.name })),
|
||||
)
|
||||
|
||||
const customerOptions = computed(() =>
|
||||
activeCustomers.value.map((customer) => ({
|
||||
value: customer.id,
|
||||
label: customer.phone
|
||||
? `${customer.name} — ${customer.phone}`
|
||||
: customer.name,
|
||||
})),
|
||||
)
|
||||
|
||||
const settlementOptions = [
|
||||
{ value: 'check', label: 'چکی' },
|
||||
{ value: 'cash', label: 'نقدی' },
|
||||
]
|
||||
|
||||
watch(
|
||||
() => form.customerId,
|
||||
(id) => {
|
||||
if (!id) {
|
||||
form.customerName = ''
|
||||
form.customerPhone = ''
|
||||
return
|
||||
}
|
||||
applyCustomer(id)
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -30,14 +53,17 @@ const settlementOptions = [
|
||||
<p v-if="formError" class="admin-form__error" role="alert">{{ formError }}</p>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="order-customer">نام مشتری <span class="admin-form__required">*</span></label>
|
||||
<input
|
||||
id="order-customer"
|
||||
v-model="form.customerName"
|
||||
type="text"
|
||||
class="admin-input"
|
||||
placeholder="مثال: پارچهسرای نیکو"
|
||||
<label>نام مشتری <span class="admin-form__required">*</span></label>
|
||||
<AdminSelect
|
||||
v-model="form.customerId"
|
||||
block
|
||||
aria-label="انتخاب مشتری"
|
||||
placeholder="انتخاب مشتری از لیست"
|
||||
:options="customerOptions"
|
||||
/>
|
||||
<p v-if="!customerOptions.length" class="admin-form__hint">
|
||||
مشتری فعالی یافت نشد. ابتدا از بخش مشتریان ثبت کنید.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
@@ -80,7 +106,10 @@ const settlementOptions = [
|
||||
v-model="form.customerPhone"
|
||||
type="tel"
|
||||
class="admin-input"
|
||||
placeholder="۰۹۱۲۱۲۳۴۵۶۷"
|
||||
placeholder="پس از انتخاب مشتری پر میشود"
|
||||
inputmode="tel"
|
||||
dir="ltr"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,3 +130,11 @@ const settlementOptions = [
|
||||
</template>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-form__hint {
|
||||
margin: 6px 0 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,8 +4,9 @@ import AdminIcon from '@/components/icons/AdminIcon.vue'
|
||||
import AdminSelect from '@/components/ui/AdminSelect.vue'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
import { formatCurrency, formatMeterage, formatNumber } from '@/utils/format'
|
||||
import { RAW_ORDER_STATUS_LABELS, rawStatusLabel } from '@/utils/orderStatus'
|
||||
import type { Order, RawOrderStatus } from '@/types'
|
||||
import { CUSTOMER_TYPE_LABELS } from '@/utils/adminLabels'
|
||||
import { ORDER_STATUS_META, RAW_ORDER_STATUS_LABELS, canPriceOrder, rawStatusLabel } from '@/utils/orderStatus'
|
||||
import type { CustomerType, Order, RawOrderStatus } from '@/types'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -61,6 +62,34 @@ const canSubmitStatus = computed(
|
||||
|
||||
const useTable = computed(() => props.layout === 'desktop')
|
||||
|
||||
const badgeStatusLabel = computed(
|
||||
() => ORDER_STATUS_META[props.order.status]?.label ?? props.order.status,
|
||||
)
|
||||
|
||||
/** Avoid duplicating the same Persian label under the badge. */
|
||||
const distinctRawStatusLabel = computed(() => {
|
||||
const raw = rawStatusLabel(props.order.rawStatus)
|
||||
if (!raw || raw === badgeStatusLabel.value) return ''
|
||||
return raw
|
||||
})
|
||||
|
||||
const customerTypeLabel = computed(() => {
|
||||
const type = props.order.customerType as CustomerType | undefined
|
||||
if (!type) return '—'
|
||||
return CUSTOMER_TYPE_LABELS[type] ?? props.order.customerType ?? '—'
|
||||
})
|
||||
|
||||
const showPricingAction = computed(() => canPriceOrder(props.order))
|
||||
const showChecksAction = computed(() => props.order.settlementMethod === 'check')
|
||||
const showCashAction = computed(() => props.order.settlementMethod === 'cash')
|
||||
/** Primary CTA follows the current workflow stage. */
|
||||
const primaryAction = computed<'pricing' | 'checks' | 'cash' | null>(() => {
|
||||
if (showPricingAction.value) return 'pricing'
|
||||
if (showChecksAction.value) return 'checks'
|
||||
if (showCashAction.value) return 'cash'
|
||||
return null
|
||||
})
|
||||
|
||||
watch([() => props.order.id, manualMode], () => {
|
||||
nextStatus.value = ''
|
||||
statusNote.value = ''
|
||||
@@ -89,25 +118,35 @@ function submitStatus() {
|
||||
<strong>{{ order.orderNumber }}</strong>
|
||||
<StatusBadge :status="order.status" />
|
||||
</div>
|
||||
<p class="order-detail__raw-status">{{ rawStatusLabel(order.rawStatus) }}</p>
|
||||
<p v-if="distinctRawStatusLabel" class="order-detail__raw-status">
|
||||
{{ distinctRawStatusLabel }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="order-detail__actions no-print">
|
||||
<button type="button" class="admin-btn admin-btn--primary admin-btn--sm" @click="emit('pricing')">
|
||||
<button
|
||||
v-if="showPricingAction"
|
||||
type="button"
|
||||
class="admin-btn admin-btn--sm"
|
||||
:class="primaryAction === 'pricing' ? 'admin-btn--primary' : 'admin-btn--outline'"
|
||||
@click="emit('pricing')"
|
||||
>
|
||||
قیمتگذاری سفارش
|
||||
</button>
|
||||
<button
|
||||
v-if="order.settlementMethod === 'check'"
|
||||
v-if="showChecksAction"
|
||||
type="button"
|
||||
class="admin-btn admin-btn--outline admin-btn--sm"
|
||||
class="admin-btn admin-btn--sm"
|
||||
:class="primaryAction === 'checks' ? 'admin-btn--primary' : 'admin-btn--outline'"
|
||||
@click="emit('checks')"
|
||||
>
|
||||
مدیریت چکها
|
||||
</button>
|
||||
<button
|
||||
v-if="order.settlementMethod === 'cash'"
|
||||
v-if="showCashAction"
|
||||
type="button"
|
||||
class="admin-btn admin-btn--outline admin-btn--sm"
|
||||
class="admin-btn admin-btn--sm"
|
||||
:class="primaryAction === 'cash' ? 'admin-btn--primary' : 'admin-btn--outline'"
|
||||
@click="emit('cashReceipt')"
|
||||
>
|
||||
رسید پرداخت
|
||||
@@ -127,7 +166,7 @@ function submitStatus() {
|
||||
<div class="order-detail__info-row">
|
||||
<AdminIcon name="phone" :size="16" />
|
||||
<span class="order-detail__info-label">تلفن</span>
|
||||
<span class="order-detail__info-value" dir="ltr">{{ order.customerPhone ?? '—' }}</span>
|
||||
<span class="order-detail__info-value order-detail__info-value--phone" dir="ltr">{{ order.customerPhone ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="order-detail__info-row">
|
||||
<AdminIcon name="id-card" :size="16" />
|
||||
@@ -137,7 +176,7 @@ function submitStatus() {
|
||||
<div class="order-detail__info-row">
|
||||
<AdminIcon name="user" :size="16" />
|
||||
<span class="order-detail__info-label">نوع مشتری</span>
|
||||
<span class="order-detail__info-value">{{ order.customerType ?? '—' }}</span>
|
||||
<span class="order-detail__info-value">{{ customerTypeLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -288,13 +327,17 @@ function submitStatus() {
|
||||
|
||||
.order-detail__id {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.order-detail__id strong {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.order-detail__raw-status {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
@@ -359,6 +402,11 @@ function submitStatus() {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.order-detail__info-value--phone {
|
||||
text-align: right;
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.order-detail__table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user