diff --git a/src/composables/useOrdersPage.ts b/src/composables/useOrdersPage.ts index a642443..d0cf2ee 100644 --- a/src/composables/useOrdersPage.ts +++ b/src/composables/useOrdersPage.ts @@ -2,8 +2,8 @@ import { computed, inject, onMounted, provide, reactive, ref, watch, type Inject import { useRoute, useRouter } from 'vue-router' import { companiesApi, customersApi, mapAdminOrder, ordersApi } from '@/services/api' import { ApiError } from '@/services/http' -import type { Company, Customer, Order, RawOrderStatus, SettlementMethod } from '@/types' -import { rawStatusLabel } from '@/utils/orderStatus' +import type { Company, Customer, Order, RawOrderStatus } from '@/types' +import { ORDER_TABS, rawStatusLabel } from '@/utils/orderStatus' import { useToast } from '@/composables/useToast' @@ -14,8 +14,15 @@ export function useOrdersPage() { const companies = ref([]) const customers = ref([]) + const TAB_KEYS = new Set(ORDER_TABS.map((tab) => tab.key)) + + function tabFromQuery() { + const tab = typeof route.query.tab === 'string' ? route.query.tab : '' + return TAB_KEYS.has(tab) ? tab : 'all' + } + const orderList = ref([]) - const activeTab = ref('all') + const activeTab = ref(tabFromQuery()) const selectedOrderId = ref('') const searchQuery = ref('') const showAddModal = ref(false) @@ -27,16 +34,19 @@ export function useOrdersPage() { const emptyForm = () => ({ customerId: '', customerName: '', - companyId: companies.value[0]?.id ?? '', - settlementMethod: 'check' as SettlementMethod, - meterage: 1000, customerPhone: '', + items: [] as Array<{ + productId: string + productName: string + catalogCode: string + meterage: number + }>, }) const form = reactive(emptyForm()) const isFormValid = computed( - () => form.customerId.length > 0 && form.companyId.length > 0, + () => form.customerId.length > 0 && form.items.length > 0, ) const activeCustomers = computed(() => { @@ -120,9 +130,6 @@ export function useOrdersPage() { orderList.value = ordersData.map((o) => mapAdminOrder(o) as Order) companies.value = companiesData customers.value = customersData - if (!form.companyId && companiesData[0]) { - form.companyId = companiesData[0].id - } await applyOrderDeepLink() } catch (err) { toast.error(err instanceof ApiError ? err.message : 'بارگذاری سفارش‌ها انجام نشد.') @@ -140,6 +147,22 @@ export function useOrdersPage() { }, ) + watch( + () => route.query.tab, + () => { + const next = tabFromQuery() + if (activeTab.value !== next) activeTab.value = next + }, + ) + + watch(activeTab, (tab) => { + const current = typeof route.query.tab === 'string' ? route.query.tab : 'all' + const next = tab === 'all' ? undefined : tab + const currentNorm = current === 'all' ? undefined : current + if (currentNorm === next) return + void router.replace({ query: { ...route.query, tab: next } }) + }) + function selectOrder(order: Order, openDetail = false) { selectedOrderId.value = order.id if (openDetail) { @@ -220,22 +243,42 @@ export function useOrdersPage() { formError.value = '' } + function addFormItem(item: { + productId: string + productName: string + catalogCode: string + meterage: number + }) { + if (form.items.some((row) => row.productId === item.productId)) { + formError.value = 'این محصول قبلاً به سفارش اضافه شده است.' + return false + } + form.items.push(item) + formError.value = '' + return true + } + + function removeFormItem(productId: string) { + form.items = form.items.filter((row) => row.productId !== productId) + } + async function submitAddOrder() { if (!isFormValid.value) { - formError.value = 'انتخاب مشتری و شرکت الزامی است.' + formError.value = 'انتخاب مشتری و حداقل یک محصول الزامی است.' return } saving.value = true try { const created = await ordersApi.create({ - companyId: form.companyId, customerId: form.customerId || undefined, userId: form.customerId || undefined, customerName: form.customerName.trim(), customerPhone: form.customerPhone.trim() || undefined, - settlementMethod: form.settlementMethod, - meterage: form.meterage, + items: form.items.map((item) => ({ + productId: item.productId, + meterage: item.meterage, + })), }) const mapped = mapAdminOrder(created) as Order orderList.value.unshift(mapped) @@ -264,6 +307,8 @@ export function useOrdersPage() { showDetailModal, formError, form, + addFormItem, + removeFormItem, isFormValid, filteredOrders, selectedOrder, diff --git a/src/services/api.ts b/src/services/api.ts index 956ff8d..6395b22 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -228,7 +228,7 @@ export const customersApi = { /* ── Orders ────────────────────────────────────────────────── */ export type AdminCreateOrderPayload = { - companyId: string + companyId?: string customerId?: string customerName?: string customerPhone?: string @@ -237,10 +237,13 @@ export type AdminCreateOrderPayload = { settlementType?: SettlementMethod meterage?: number notes?: string + items?: Array<{ productId: string; meterage: number; notes?: string }> } export type OrderItemPricingPayload = { - id: string + id?: string + productId?: string + requestedMeterage?: number confirmedMeterage?: number unitPrice?: number supplyPrice?: number @@ -250,6 +253,7 @@ export type UpdateOrderPricingPayload = { items: OrderItemPricingPayload[] shippingCost?: number note?: string + deletedItemIds?: string[] } export type ApiOrder = Order & { diff --git a/src/types/index.ts b/src/types/index.ts index 5c9457d..b7e1872 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -69,6 +69,7 @@ export interface BankAccount { export interface OrderItem { id: string + productId?: string catalogCode: string productName: string color: string diff --git a/src/utils/orderStatus.ts b/src/utils/orderStatus.ts index 1da7c83..e810043 100644 --- a/src/utils/orderStatus.ts +++ b/src/utils/orderStatus.ts @@ -74,6 +74,20 @@ export function canPriceOrder(order: { return PRE_INVOICE_ADMIN_STATUSES.includes(order.status) } +export function canFinalApproveOrder(order: { rawStatus?: RawOrderStatus }): boolean { + return order.rawStatus === 'checks_approved' || order.rawStatus === 'cash_confirmed' +} + +export const SHIPPABLE_RAW_STATUSES: RawOrderStatus[] = [ + 'final_approved', + 'preparing', + 'ready_to_ship', +] + +export function canCreateShipment(order: { rawStatus?: RawOrderStatus }): boolean { + return !!order.rawStatus && SHIPPABLE_RAW_STATUSES.includes(order.rawStatus) +} + export const ORDER_TABS = [ { key: 'all', label: 'همه' }, { key: 'pending_review', label: 'در انتظار بررسی' }, diff --git a/src/views/cash-payments/CashPaymentsView.vue b/src/views/cash-payments/CashPaymentsView.vue index 4da7dc1..7deecec 100644 --- a/src/views/cash-payments/CashPaymentsView.vue +++ b/src/views/cash-payments/CashPaymentsView.vue @@ -4,7 +4,7 @@ import { useRoute } from 'vue-router' import AdminIcon from '@/components/icons/AdminIcon.vue' import AdminPageSkeleton from '@/components/ui/AdminPageSkeleton.vue' import AdminSelect from '@/components/ui/AdminSelect.vue' -import { cashPaymentsApi } from '@/services/api' +import { cashPaymentsApi, ordersApi } from '@/services/api' import { ApiError } from '@/services/http' import { CASH_PAYMENT_STATUS_BADGE, @@ -73,6 +73,21 @@ async function approvePayment(payment: CashPayment) { } } +async function finalApproveOrder(payment: CashPayment) { + busyId.value = payment.id + try { + await ordersApi.updateStatus(payment.orderId, { + status: 'final_approved', + note: 'تأیید نهایی پس از تأیید رسید نقدی', + }) + toast.success(`سفارش ${payment.orderNumber} تأیید نهایی شد.`) + } catch (err) { + toast.error(err instanceof ApiError ? err.message : 'تأیید نهایی انجام نشد.') + } finally { + busyId.value = null + } +} + async function rejectPayment(payment: CashPayment) { busyId.value = payment.id try { @@ -165,6 +180,16 @@ async function rejectPayment(payment: CashPayment) { رد +
+ +
— @@ -187,8 +212,12 @@ async function rejectPayment(payment: CashPayment) { پرداختی: {{ formatCurrency(payment.paidAmount) }} {{ payment.trackingCode }} -
+
+
diff --git a/src/views/checks/CheckManagementView.vue b/src/views/checks/CheckManagementView.vue index bf02294..f3d6997 100644 --- a/src/views/checks/CheckManagementView.vue +++ b/src/views/checks/CheckManagementView.vue @@ -42,7 +42,7 @@ const allChecksApproved = computed( const canFinalApprove = computed(() => { if (!allChecksApproved.value || !order.value) return false const status = (order.value.rawStatus ?? order.value.status) as RawOrderStatus - return status === 'checks_approved' || status === 'awaiting_check_review' + return status === 'checks_approved' }) const finalizing = ref(false) diff --git a/src/views/orders/OrderPricingView.vue b/src/views/orders/OrderPricingView.vue index c989d3f..6e0d23f 100644 --- a/src/views/orders/OrderPricingView.vue +++ b/src/views/orders/OrderPricingView.vue @@ -1,18 +1,19 @@ diff --git a/src/views/orders/components/DesktopOrdersPage.vue b/src/views/orders/components/DesktopOrdersPage.vue index 5552c20..3545dde 100644 --- a/src/views/orders/components/DesktopOrdersPage.vue +++ b/src/views/orders/components/DesktopOrdersPage.vue @@ -6,7 +6,7 @@ import StatusBadge from '@/components/ui/StatusBadge.vue' import BaseModal from '@/components/ui/BaseModal.vue' import OrderDetailContent from '@/views/orders/components/OrderDetailContent.vue' import { useOrdersPageContext } from '@/composables/useOrdersPage' -import { ORDER_TABS, canPriceOrder } from '@/utils/orderStatus' +import { ORDER_TABS, canFinalApproveOrder, canPriceOrder } from '@/utils/orderStatus' import { formatCurrency, formatDateTime, formatMeterage } from '@/utils/format' import type { Order } from '@/types' @@ -95,6 +95,15 @@ function handleCashReceipt() { goCashReceipt(selectedOrder.value) } +function handleFinalApprove() { + if (!selectedOrder.value) return + void changeStatus({ + orderId: selectedOrder.value.id, + status: 'final_approved', + note: 'تأیید نهایی سفارش', + }) +} + function printOrderDetail() { if (!selectedOrder.value) return document.documentElement.classList.add('printing-order-detail') @@ -107,7 +116,7 @@ function printOrderDetail() { window.print() } -function runMenuAction(action: 'detail' | 'pricing' | 'checks' | 'cash') { +function runMenuAction(action: 'detail' | 'pricing' | 'checks' | 'cash' | 'final') { const order = menuOrder.value if (!order) return closeMenu() @@ -123,6 +132,14 @@ function runMenuAction(action: 'detail' | 'pricing' | 'checks' | 'cash') { goChecks(order) return } + if (action === 'final') { + void changeStatus({ + orderId: order.id, + status: 'final_approved', + note: 'تأیید نهایی سفارش', + }) + return + } goCashReceipt(order) } @@ -261,7 +278,7 @@ function runMenuAction(action: 'detail' | 'pricing' | 'checks' | 'cash') { مدیریت چک‌ها + @@ -287,6 +313,7 @@ function runMenuAction(action: 'detail' | 'pricing' | 'checks' | 'cash') { @pricing="handlePricing" @checks="handleChecks" @cash-receipt="handleCashReceipt" + @final-approve="handleFinalApprove" @status="changeStatus" /> diff --git a/src/views/orders/components/OrderAddModal.vue b/src/views/orders/components/OrderAddModal.vue index 1382be5..0297e01 100644 --- a/src/views/orders/components/OrderAddModal.vue +++ b/src/views/orders/components/OrderAddModal.vue @@ -1,24 +1,33 @@