From 7fc9f98f8610faacf0874e3549a1036a9ba41ca0 Mon Sep 17 00:00:00 2001 From: safavimohadeseh78 Date: Sat, 8 Aug 2026 12:44:05 +0200 Subject: [PATCH] Polish admin pricing UI, currency inputs, and active switch confirm. EOF Co-authored-by: Cursor --- src/assets/styles/responsive.css | 9 ++- src/components/ui/ActiveSwitchCell.vue | 21 +++--- src/components/ui/AdminCurrencyInput.vue | 58 ++++++++++++++++ src/components/ui/BaseSwitch.vue | 21 +++--- src/data/mockData.ts | 5 +- src/services/api.ts | 3 - src/types/index.ts | 1 - src/utils/format.ts | 13 ++++ src/views/cash-payments/CashPaymentsView.vue | 1 + src/views/checks/CheckManagementView.vue | 6 +- src/views/customers/CustomersView.vue | 24 +++---- src/views/orders/OrderPricingView.vue | 69 ++++++++++--------- .../orders/components/OrderDetailContent.vue | 16 +++-- src/views/products/ProductsView.vue | 27 ++++---- 14 files changed, 181 insertions(+), 93 deletions(-) create mode 100644 src/components/ui/AdminCurrencyInput.vue diff --git a/src/assets/styles/responsive.css b/src/assets/styles/responsive.css index 4162232..e444aaa 100644 --- a/src/assets/styles/responsive.css +++ b/src/assets/styles/responsive.css @@ -122,12 +122,19 @@ .admin-card-item__header { display: flex; - align-items: flex-start; + align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; } +.admin-card-item__header-actions { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + .admin-card-item__title { margin: 0; font-size: 0.875rem; diff --git a/src/components/ui/ActiveSwitchCell.vue b/src/components/ui/ActiveSwitchCell.vue index ed9152c..171cf5e 100644 --- a/src/components/ui/ActiveSwitchCell.vue +++ b/src/components/ui/ActiveSwitchCell.vue @@ -26,18 +26,21 @@ const { toggle, isLoading, hasError } = useAsyncToggle({ ? `${props.entityType ?? 'مورد'} «${props.entityName}» فعال شد.` : `${props.entityType ?? 'مورد'} «${props.entityName}» غیرفعال شد.`, }) + async function onChange(next: boolean) { if (next === props.modelValue) return - if (!next) { - const ok = await confirm({ - title: `غیرفعال‌سازی ${props.entityType ?? 'مورد'}`, - message: `آیا از غیرفعال کردن «${props.entityName}» مطمئن هستید؟`, - tone: 'danger', - confirmLabel: 'غیرفعال کردن', - }) - if (!ok) return - } + const entity = props.entityType ?? 'مورد' + const ok = await confirm({ + title: next ? `فعال‌سازی ${entity}` : `غیرفعال‌سازی ${entity}`, + message: next + ? `آیا از فعال کردن «${props.entityName}» مطمئن هستید؟` + : `آیا از غیرفعال کردن «${props.entityName}» مطمئن هستید؟`, + tone: next ? 'primary' : 'danger', + confirmLabel: next ? 'فعال کردن' : 'غیرفعال کردن', + cancelLabel: 'انصراف', + }) + if (!ok) return const result = await toggle(props.id, props.modelValue) emit('update:modelValue', result) diff --git a/src/components/ui/AdminCurrencyInput.vue b/src/components/ui/AdminCurrencyInput.vue new file mode 100644 index 0000000..940c27f --- /dev/null +++ b/src/components/ui/AdminCurrencyInput.vue @@ -0,0 +1,58 @@ + + + diff --git a/src/components/ui/BaseSwitch.vue b/src/components/ui/BaseSwitch.vue index e35dbf1..a665aef 100644 --- a/src/components/ui/BaseSwitch.vue +++ b/src/components/ui/BaseSwitch.vue @@ -26,13 +26,22 @@ const inputId = computed(() => props.id ?? `switch-${crypto.randomUUID().slice(0 const stateText = computed(() => (props.modelValue ? props.activeText : props.inactiveText)) function onChange(event: Event) { + const input = event.target as HTMLInputElement + const next = input.checked + // Keep the visual state locked to modelValue until the parent accepts the change. + // Native click flips the DOM checkbox even when the parent cancels. + input.checked = props.modelValue if (props.disabled || props.loading) return - emit('update:modelValue', (event.target as HTMLInputElement).checked) + emit('update:modelValue', next) } @@ -82,7 +90,6 @@ function onChange(event: Event) { .base-switch__control { display: inline-flex; align-items: center; - gap: 10px; } .base-switch__input { @@ -131,12 +138,6 @@ function onChange(event: Event) { transform: translateX(-20px); } -.base-switch__state { - font-size: 0.8125rem; - color: var(--color-text-muted); - min-width: 3.5rem; -} - .base-switch__spinner { width: 12px; height: 12px; diff --git a/src/data/mockData.ts b/src/data/mockData.ts index a86516f..2f7d3ec 100644 --- a/src/data/mockData.ts +++ b/src/data/mockData.ts @@ -82,7 +82,6 @@ export const orders: Order[] = [ requestedMeterage: 2000, confirmedMeterage: 2000, supplyPrice: null, - unitDiscount: 0, totalAmount: null, }, { @@ -93,7 +92,6 @@ export const orders: Order[] = [ requestedMeterage: 1500, confirmedMeterage: 1000, supplyPrice: null, - unitDiscount: 0, totalAmount: null, }, { @@ -104,7 +102,6 @@ export const orders: Order[] = [ requestedMeterage: 750, confirmedMeterage: 750, supplyPrice: null, - unitDiscount: 0, totalAmount: null, }, ], @@ -250,7 +247,7 @@ export const internalMessages: InternalMessage[] = [ id: 'msg-2', senderName: 'رضا کریمی', subject: 'درخواست اصلاح قیمت', - preview: 'مشتری درخواست تخفیف ۵٪ روی متراژ بالای ۳۰۰۰ متر داده است.', + preview: 'مشتری درخواست اصلاح متراژ سفارش را داده است.', sentAt: '2024-10-25T08:40:00', isRead: false, }, diff --git a/src/services/api.ts b/src/services/api.ts index 8d5ae52..30ee5cd 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -203,13 +203,11 @@ export type OrderItemPricingPayload = { confirmedMeterage?: number unitPrice?: number supplyPrice?: number - unitDiscount?: number } export type UpdateOrderPricingPayload = { items: OrderItemPricingPayload[] shippingCost?: number - totalDiscount?: number note?: string } @@ -218,7 +216,6 @@ export type ApiOrder = Order & { settlementType?: SettlementMethod totalMeterage?: number shippingCost?: number | null - totalDiscount?: number | null finalAmount?: number | null checkRassDays?: number | null checkRassDate?: string diff --git a/src/types/index.ts b/src/types/index.ts index 0a1a49f..c8fe3d0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -40,7 +40,6 @@ export interface OrderItem { requestedMeterage: number confirmedMeterage: number | null supplyPrice: number | null - unitDiscount: number totalAmount: number | null } diff --git a/src/utils/format.ts b/src/utils/format.ts index 0670afc..6f6ab07 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -1,6 +1,19 @@ const persianDigits = '۰۱۲۳۴۵۶۷۸۹' const numberFormatter = new Intl.NumberFormat('fa-IR', { useGrouping: true }) +export function normalizeDigits(input: string): string { + return input.replace(/[۰-۹]/g, (digit) => String(persianDigits.indexOf(digit))) +} + +export function extractNumericString(input: string): string { + return normalizeDigits(input).replace(/[^\d]/g, '') +} + +export function parseFormattedNumber(value: string): number { + const numeric = extractNumericString(value) + return numeric ? Number(numeric) : 0 +} + export function formatNumber(value: number): string { if (!Number.isFinite(value)) return '—' return numberFormatter.format(value) diff --git a/src/views/cash-payments/CashPaymentsView.vue b/src/views/cash-payments/CashPaymentsView.vue index cd1cb68..dda9a34 100644 --- a/src/views/cash-payments/CashPaymentsView.vue +++ b/src/views/cash-payments/CashPaymentsView.vue @@ -168,6 +168,7 @@ async function rejectPayment(payment: CashPayment) {
{{ payment.customerName }} {{ formatCurrency(payment.amount) }} + پرداختی: {{ formatCurrency(payment.paidAmount) }} {{ payment.trackingCode }}
diff --git a/src/views/checks/CheckManagementView.vue b/src/views/checks/CheckManagementView.vue index 712c6e7..522db66 100644 --- a/src/views/checks/CheckManagementView.vue +++ b/src/views/checks/CheckManagementView.vue @@ -5,7 +5,7 @@ import AdminIcon from '@/components/icons/AdminIcon.vue' import StatusBadge from '@/components/ui/StatusBadge.vue' import { checksApi, mapAdminOrder, ordersApi, type ApiOrder } from '@/services/api' import { ApiError } from '@/services/http' -import { formatCurrency, formatNumber } from '@/utils/format' +import { formatCurrency } from '@/utils/format' import { useToast } from '@/composables/useToast' import type { Check } from '@/types' @@ -161,7 +161,7 @@ async function approveAll() { {{ check.row }} - {{ formatNumber(check.amount) }} + {{ formatCurrency(check.amount) }} {{ check.bankName }} {{ check.sayadId }} {{ check.dueDate.slice(0, 10) }} @@ -196,7 +196,7 @@ async function approveAll() {
-

چک {{ check.row }} — {{ formatNumber(check.amount) }} تومان

+

چک {{ check.row }} — {{ formatCurrency(check.amount) }}

diff --git a/src/views/customers/CustomersView.vue b/src/views/customers/CustomersView.vue index 7d44d6b..e8ede4d 100644 --- a/src/views/customers/CustomersView.vue +++ b/src/views/customers/CustomersView.vue @@ -220,13 +220,18 @@ function clearSearch() {

{{ customer.name }}

- +
+ + +
{{ customer.phone }} @@ -234,11 +239,6 @@ function clearSearch() { {{ customer.companyName }} {{ customer.orderCount }} سفارش · {{ formatMeterage(customer.totalMeterage) }}
-
- -
diff --git a/src/views/orders/OrderPricingView.vue b/src/views/orders/OrderPricingView.vue index 4e63424..e07b841 100644 --- a/src/views/orders/OrderPricingView.vue +++ b/src/views/orders/OrderPricingView.vue @@ -9,6 +9,7 @@ import { ApiError } from '@/services/http' import { formatCurrency, formatNumber } from '@/utils/format' import { confirm } from '@/composables/useConfirm' import { useToast } from '@/composables/useToast' +import AdminCurrencyInput from '@/components/ui/AdminCurrencyInput.vue' import type { OrderItem } from '@/types' const route = useRoute() @@ -50,7 +51,16 @@ async function loadOrder() { try { const data = await ordersApi.get(String(route.params.id)) order.value = data - items.value = (data.items ?? []).map((i) => ({ ...i })) + items.value = (data.items ?? []).map((item) => ({ + id: item.id, + catalogCode: item.catalogCode, + productName: item.productName, + color: item.color, + requestedMeterage: item.requestedMeterage, + confirmedMeterage: item.confirmedMeterage, + supplyPrice: item.supplyPrice, + totalAmount: item.totalAmount, + })) shippingCost.value = data.shippingCost ?? 0 } catch (err) { toast.error(err instanceof ApiError ? err.message : 'بارگذاری سفارش انجام نشد.') @@ -107,7 +117,6 @@ function submitAddProduct() { requestedMeterage: form.requestedMeterage, confirmedMeterage: form.confirmedMeterage || form.requestedMeterage, supplyPrice: null, - unitDiscount: 0, totalAmount: null, }) @@ -129,13 +138,21 @@ async function savePricing() { id: item.id, confirmedMeterage: item.confirmedMeterage ?? undefined, supplyPrice: item.supplyPrice ?? undefined, - unitDiscount: item.unitDiscount, })), shippingCost: shippingCost.value, note: customerNote.value.trim() || undefined, }) order.value = updated - items.value = (updated.items ?? []).map((i) => ({ ...i })) + items.value = (updated.items ?? []).map((item) => ({ + id: item.id, + catalogCode: item.catalogCode, + productName: item.productName, + color: item.color, + requestedMeterage: item.requestedMeterage, + confirmedMeterage: item.confirmedMeterage, + supplyPrice: item.supplyPrice, + totalAmount: item.totalAmount, + })) toast.success('قیمت‌گذاری ذخیره شد.') } catch (err) { toast.error(err instanceof ApiError ? err.message : 'ذخیره قیمت‌گذاری انجام نشد.') @@ -183,12 +200,6 @@ async function savePricing() { {{ displayOrder.totalAmount ? formatCurrency(displayOrder.totalAmount) : '—' }}

-
-

تخفیف کل

-

- {{ formatCurrency(order?.totalDiscount ?? 0) }} -

-

هزینه حمل

@@ -216,7 +227,6 @@ async function savePricing() { متراژ درخواستی متراژ تأیید شده قیمت تأمینی (واحد) - تخفیف (واحد) مبلغ کل (تومان) عملیات @@ -236,20 +246,12 @@ async function savePricing() { /> - - - - {{ item.totalAmount ? formatCurrency(item.totalAmount) : '—' }}

-
- - -
- +
@@ -473,4 +472,12 @@ async function savePricing() { font-size: 0.75rem; color: var(--color-text-muted); } + +.pricing-page__line-total { + grid-column: 1 / -1; + margin: 0; + font-size: 0.875rem; + font-weight: 700; + color: var(--color-text); +} diff --git a/src/views/orders/components/OrderDetailContent.vue b/src/views/orders/components/OrderDetailContent.vue index a459dcb..60a2250 100644 --- a/src/views/orders/components/OrderDetailContent.vue +++ b/src/views/orders/components/OrderDetailContent.vue @@ -1,7 +1,7 @@