diff --git a/src/api/dashboard.ts b/src/api/dashboard.ts index 8cc8bfb..bd71746 100644 --- a/src/api/dashboard.ts +++ b/src/api/dashboard.ts @@ -4,13 +4,17 @@ import type { DashboardFlowPoint, DashboardFinance, DashboardMaster, + DashboardOrderPeakHours, PriceChartPoint, } from '@/model/dashboard' import type { Order } from '@/model/order' import type { Balance } from '@/model/product' import type { TableInput } from '@core/type/baseTable' -export const getDashboard = async () => await useAxios.get('/admin/dashboard') +export const getDashboard = async (params?: { + startDateProduct?: string + endDateProduct?: string +}) => await useAxios.get('/admin/dashboard', { params }) export const updateMyInformation = async (val: User) => await useAxios.post('/admin/me/update', val) @@ -24,6 +28,9 @@ export const getDashboardBalance = async (startDate: string, endDate: string) => export const getDashboardFlow = async (type: 'week' | 'month') => await useAxios.post('/admin/dashboard/customer/flow', { type }) +export const getDashboardOrderPeakHours = async (days: 7 | 30 = 30) => + await useAxios.post('/admin/dashboard/customer/order-peak-hours', { days }) + export const getPriceChartData = async () => { const res = await useAxios.post<{ code?: number diff --git a/src/api/product.ts b/src/api/product.ts index 7d81c40..d4297e7 100644 --- a/src/api/product.ts +++ b/src/api/product.ts @@ -57,6 +57,34 @@ export const setActiveTime = async (open: string, close: string) =>{ await useAxios.put(`/admin/products`, {auto_open_at: open, auto_close_at: close}) } +export type ProductActivationSnapshot = { + has_snapshot: boolean + saved_at?: string | null + count?: number +} + +export const getProductActivationSnapshot = async () => + await useAxios.get('/admin/products/quick/activation-snapshot') + +export const deactivateAllProducts = async () => + await useAxios.post<{ message: string; snapshot_count: number; has_snapshot: boolean }>( + '/admin/products/quick/deactivate-all', + ) + +export const restoreProductActivation = async () => + await useAxios.post<{ message: string; restored: number }>( + '/admin/products/quick/restore-activation', + ) + +export const closeAllProductBuy = async () => + await useAxios.post<{ message: string }>('/admin/products/quick/close-buy') + +export const closeAllProductSell = async () => + await useAxios.post<{ message: string }>('/admin/products/quick/close-sell') + +export const syncProductShow = async () => + await useAxios.post<{ message: string }>('/admin/products/quick/sync-show') + export const getBalance = async (startDate: string, endDate: string) => { return await useAxios.post('/admin/getBalance', {startDate, endDate}) } \ No newline at end of file diff --git a/src/model/dashboard.ts b/src/model/dashboard.ts index 8e81273..61d8a1f 100644 --- a/src/model/dashboard.ts +++ b/src/model/dashboard.ts @@ -51,6 +51,21 @@ export interface DashboardFlowPoint { 2?: number } +export interface DashboardOrderPeakHour { + hour: number + buy: number + sell: number + total: number +} + +export interface DashboardOrderPeakHours { + hours: DashboardOrderPeakHour[] + peak_hour: number | null + peak_count: number + days: number + total_orders: number +} + export interface PriceChartPoint { timestamp: number value: number diff --git a/src/pages/dashboard/components/DashboardLabelWithHelp.vue b/src/pages/dashboard/components/DashboardLabelWithHelp.vue index 835c418..80fc0b8 100644 --- a/src/pages/dashboard/components/DashboardLabelWithHelp.vue +++ b/src/pages/dashboard/components/DashboardLabelWithHelp.vue @@ -1,9 +1,9 @@ diff --git a/src/pages/dashboard/components/DashboardQuickActionBtn.vue b/src/pages/dashboard/components/DashboardQuickActionBtn.vue new file mode 100644 index 0000000..f98ab93 --- /dev/null +++ b/src/pages/dashboard/components/DashboardQuickActionBtn.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/src/pages/dashboard/components/DashboardQuickActionsBar.vue b/src/pages/dashboard/components/DashboardQuickActionsBar.vue new file mode 100644 index 0000000..84606cd --- /dev/null +++ b/src/pages/dashboard/components/DashboardQuickActionsBar.vue @@ -0,0 +1,471 @@ + + + + + diff --git a/src/pages/dashboard/components/DashboardQuickWalletModal.vue b/src/pages/dashboard/components/DashboardQuickWalletModal.vue new file mode 100644 index 0000000..b5f1c76 --- /dev/null +++ b/src/pages/dashboard/components/DashboardQuickWalletModal.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/src/pages/dashboard/components/DashboardSectionHeader.vue b/src/pages/dashboard/components/DashboardSectionHeader.vue index ff9dc0d..1cfb55b 100644 --- a/src/pages/dashboard/components/DashboardSectionHeader.vue +++ b/src/pages/dashboard/components/DashboardSectionHeader.vue @@ -1,10 +1,11 @@ diff --git a/src/pages/dashboard/components/IosInfoBadge.vue b/src/pages/dashboard/components/IosInfoBadge.vue index d33420f..920e136 100644 --- a/src/pages/dashboard/components/IosInfoBadge.vue +++ b/src/pages/dashboard/components/IosInfoBadge.vue @@ -31,6 +31,7 @@ const sizeClass = computed(() => { diff --git a/src/pages/dashboard/composables/useDashboardPage.ts b/src/pages/dashboard/composables/useDashboardPage.ts index 17d2935..937e152 100644 --- a/src/pages/dashboard/composables/useDashboardPage.ts +++ b/src/pages/dashboard/composables/useDashboardPage.ts @@ -1,11 +1,12 @@ //@ts-ignore import moment from 'moment-jalaali' -import { ref, computed, onMounted } from 'vue' +import { ref, computed, onMounted, watch } from 'vue' import { getDashboard, getDashboardBalance, getDashboardFinance, getDashboardFlow, + getDashboardOrderPeakHours, getPriceChartData, getRecentOrders, toggleGateway, @@ -15,21 +16,63 @@ import type { DashboardFlowPoint, DashboardFinance, DashboardMaster, + DashboardOrderPeakHours, PriceChartPoint, } from '@/model/dashboard' import type { Order } from '@/model/order' import type { Balance } from '@/model/product' import { usePrice } from '@core/utils/usePrice' import { toJalali } from '@core/utils/useDataTime' +import { isMobile } from '@core/utils/is' import { isStoreApp } from '@/utils/config' +function truncateChartLabel(label: string, maxLen: number) { + const text = String(label ?? '').trim() + if (text.length <= maxLen) return text + return `${text.slice(0, Math.max(1, maxLen - 1))}…` +} + +/** عرض تقریبی ستون برچسب محصول در نمودار افقی */ +function productChartLabelColumnWidth(labels: string[], mobile: boolean) { + const longest = labels.reduce((max, label) => Math.max(max, [...String(label)].length), 0) + const perChar = mobile ? 8.5 : 10 + const min = mobile ? 80 : 112 + const cap = mobile ? 118 : 240 + return Math.min(cap, Math.max(min, Math.ceil(longest * perChar + 12))) +} + +function formatPeakHourLabel(hour: number) { + return `${hour.toLocaleString('fa-IR', { useGrouping: false })}:۰۰` +} + const MESGHAL = 4.331802 +export type ProductActivityRange = 'day' | 'week' | 'month' | 'year' + +function productActivityDateRange(range: ProductActivityRange) { + const end = moment().endOf('day') + const startByRange = { + day: moment().startOf('day'), + week: moment().subtract(7, 'days').startOf('day'), + month: moment().subtract(1, 'month').startOf('day'), + year: moment().subtract(1, 'year').startOf('day'), + } + const start = startByRange[range] + + return { + startDateProduct: start.format('YYYY-MM-DD HH:mm:ss'), + endDateProduct: end.format('YYYY-MM-DD HH:mm:ss'), + } +} + export function useDashboardPage() { const loading = ref(true) const flowLoading = ref(false) + const productActivityLoading = ref(false) const priceRange = ref<'DAILY' | 'WEEKLY' | 'MONTHLY'>('DAILY') const flowRange = ref<'week' | 'month'>('week') + const peakHoursRange = ref<7 | 30>(30) + const productActivityRange = ref('month') const startDate = ref(moment().subtract(30, 'days').startOf('day').format('YYYY-MM-DD HH:mm:ss')) const endDate = ref(moment().endOf('day').format('YYYY-MM-DD HH:mm:ss')) @@ -40,12 +83,13 @@ export function useDashboardPage() { const walletSums = ref<{ gold?: number; cash?: number; goldToCash?: number; cashToCash?: number }>() const recentOrders = ref([]) const flowData = ref([]) + const peakHoursData = ref(null) + const peakHoursLoading = ref(false) + const priceChartByRange = ref>>({}) const priceSeries = ref([]) const isStore = computed(() => isStoreApp()) - const pendingOrders = computed(() => master.value?.await_order ?? []) - const customerStats = computed(() => ({ newCount: isStore.value ? master.value?.new_customer_major ?? 0 @@ -60,11 +104,11 @@ export function useDashboardPage() { async function loadCore() { const [dash, fin, bal, wallet, orders, prices] = await Promise.allSettled([ - getDashboard(), + getDashboard(productActivityDateRange(productActivityRange.value)), getDashboardFinance(startDate.value, endDate.value), getDashboardBalance(startDate.value, endDate.value), getWalletTable({ limit: 1, page: 1 } as never), - getRecentOrders(12), + getRecentOrders(14), getPriceChartData(), ]) @@ -74,10 +118,21 @@ export function useDashboardPage() { if (wallet.status === 'fulfilled') walletSums.value = wallet.value?.sums if (orders.status === 'fulfilled') recentOrders.value = orders.value if (prices.status === 'fulfilled') { - priceSeries.value = prices.value?.DAILY ?? [] + priceChartByRange.value = prices.value ?? {} + applyPriceRangeSeries(priceRange.value) } } + function chartPointRial(value?: number) { + const scaled = Number(value ?? 0) + if (scaled <= 0) return 0 + return (scaled - 1) * 1000 + } + + function applyPriceRangeSeries(range: 'DAILY' | 'WEEKLY' | 'MONTHLY') { + priceSeries.value = priceChartByRange.value[range] ?? [] + } + async function loadFlow() { flowLoading.value = true try { @@ -90,10 +145,41 @@ export function useDashboardPage() { } } + async function loadPeakHours() { + peakHoursLoading.value = true + try { + peakHoursData.value = (await getDashboardOrderPeakHours(peakHoursRange.value)) ?? null + } catch (e) { + console.error(e) + peakHoursData.value = null + } finally { + peakHoursLoading.value = false + } + } + + async function loadProductActivity() { + productActivityLoading.value = true + try { + const dash = await getDashboard(productActivityDateRange(productActivityRange.value)) + if (master.value) { + master.value = { + ...master.value, + GT_products_report: dash?.GT_products_report ?? [], + } + } else if (dash) { + master.value = dash + } + } catch (e) { + console.error(e) + } finally { + productActivityLoading.value = false + } + } + async function refreshAll() { loading.value = true try { - await Promise.all([loadCore(), loadFlow()]) + await Promise.all([loadCore(), loadFlow(), loadPeakHours()]) } finally { loading.value = false } @@ -113,13 +199,29 @@ export function useDashboardPage() { } } - async function switchPriceRange(range?: 'DAILY' | 'WEEKLY' | 'MONTHLY') { - const key = range ?? priceRange.value - priceRange.value = key - const prices = await getPriceChartData() - priceSeries.value = prices?.[key] ?? [] + function switchPriceRange(range?: 'DAILY' | 'WEEKLY' | 'MONTHLY') { + if (range) { + priceRange.value = range + } + applyPriceRangeSeries(priceRange.value) } + watch(priceRange, (key) => { + applyPriceRangeSeries(key) + }) + + watch(productActivityRange, () => { + void loadProductActivity() + }) + + watch(flowRange, () => { + void loadFlow() + }) + + watch(peakHoursRange, () => { + void loadPeakHours() + }) + async function onToggleGateway() { await toggleGateway() master.value = await getDashboard() @@ -160,10 +262,16 @@ export function useDashboardPage() { const gifts = finance.value?.GiftsPrise ?? 0 return { - chart: { type: 'donut', fontFamily: 'inherit' }, + chart: { type: 'donut', fontFamily: 'inherit', offsetY: 4 }, labels: ['سود معامله', 'کارمزد طلایی', 'هدایای طلا'], colors: ['#10b981', '#f59e0b', '#8b5cf6'], - legend: { position: 'bottom', fontSize: '12px' }, + legend: { + position: 'bottom', + fontSize: '12px', + horizontalAlign: 'center', + offsetY: 6, + itemMargin: { horizontal: 12, vertical: 4 }, + }, dataLabels: { enabled: false }, plotOptions: { pie: { @@ -188,6 +296,7 @@ export function useDashboardPage() { }) const flowChartOptions = computed(() => { + const mobile = isMobile.value const points = [...flowData.value].sort((a, b) => String(a.date ?? '').localeCompare(String(b.date ?? '')), ) @@ -197,16 +306,39 @@ export function useDashboardPage() { }) return { - chart: { type: 'bar', stacked: true, toolbar: { show: false }, fontFamily: 'inherit' }, - plotOptions: { bar: { borderRadius: 4, columnWidth: '55%' } }, + chart: { + type: 'bar', + stacked: true, + toolbar: { show: false }, + fontFamily: 'inherit', + redrawOnParentResize: true, + }, + plotOptions: { bar: { borderRadius: 4, columnWidth: mobile ? '48%' : '55%' } }, colors: ['#10b981', '#f43f5e'], - xaxis: { categories, labels: { rotate: -35, style: { fontSize: '11px' } } }, - yaxis: { + grid: { + padding: { left: mobile ? 0 : 4, right: mobile ? 4 : 8 }, + }, + xaxis: { + categories, labels: { - formatter: (v: number) => `${Number(v).toLocaleString('fa-IR')} گرم`, + rotate: mobile ? -45 : -35, + style: { fontSize: mobile ? '10px' : '11px' }, }, }, - legend: { position: 'top' }, + yaxis: { + tickAmount: mobile ? 4 : 6, + labels: { + formatter: (v: number) => + mobile ? Number(v).toLocaleString('fa-IR') : `${Number(v).toLocaleString('fa-IR')} گرم`, + style: { fontSize: mobile ? '10px' : '11px' }, + maxWidth: mobile ? 40 : 64, + offsetX: 0, + }, + }, + tooltip: { + y: { formatter: (v: number) => `${Number(v).toLocaleString('fa-IR')} گرم` }, + }, + legend: { position: 'top', fontSize: mobile ? '10px' : '11px' }, dataLabels: { enabled: false }, series: [ { name: 'خرید مشتری', data: points.map((p) => Number(p[1] ?? p['1'] ?? 0)) }, @@ -215,48 +347,197 @@ export function useDashboardPage() { } }) - const priceChartOptions = computed(() => ({ - chart: { type: 'area', toolbar: { show: false }, fontFamily: 'inherit', zoom: { enabled: false } }, - stroke: { curve: 'smooth', width: 2 }, - fill: { - type: 'gradient', - gradient: { shadeIntensity: 1, opacityFrom: 0.45, opacityTo: 0.05 }, - }, - colors: ['#2563eb'], - dataLabels: { enabled: false }, - xaxis: { - type: 'datetime', - labels: { datetimeUTC: false, style: { fontSize: '11px' } }, - }, - yaxis: { - labels: { - formatter: (v: number) => `${Math.round(v / 1_000_000).toLocaleString('fa-IR')}M`, - }, - }, - tooltip: { - x: { format: 'yyyy/MM/dd HH:mm' }, - y: { formatter: (v: number) => usePrice(v, true, '') }, - }, - series: [ - { - name: 'مظنه', - data: priceSeries.value.map((p) => [p.timestamp, p.value]), - }, - ], - })) + const priceChartOptions = computed(() => { + const tooltipDateFormat = + priceRange.value === 'DAILY' ? 'yyyy/MM/dd HH:mm' : 'yyyy/MM/dd' - const productChartOptions = computed(() => { - const items = (master.value?.GT_products_report ?? []).slice(0, 8) return { - chart: { type: 'bar', toolbar: { show: false }, fontFamily: 'inherit' }, - plotOptions: { bar: { horizontal: true, borderRadius: 4 } }, - colors: ['#0ea5e9', '#f97316'], + chart: { type: 'area', toolbar: { show: false }, fontFamily: 'inherit', zoom: { enabled: false } }, + stroke: { curve: 'smooth', width: 2 }, + fill: { + type: 'gradient', + gradient: { shadeIntensity: 1, opacityFrom: 0.45, opacityTo: 0.05 }, + }, + colors: ['#2563eb'], dataLabels: { enabled: false }, xaxis: { - categories: items.map((p) => p.title ?? `#${p.id}`), - labels: { style: { fontSize: '11px' } }, + type: 'datetime', + labels: { + datetimeUTC: false, + style: { fontSize: '11px' }, + datetimeFormatter: { + year: 'yyyy', + month: 'MMM', + day: priceRange.value === 'DAILY' ? 'dd HH:mm' : 'dd MMM', + hour: 'HH:mm', + }, + }, + }, + yaxis: { + labels: { + formatter: (v: number) => `${Math.round(v / 1_000_000).toLocaleString('fa-IR')}M`, + }, + }, + tooltip: { + x: { format: tooltipDateFormat }, + y: { formatter: (v: number) => usePrice(v, true, '') }, + }, + series: [ + { + name: 'مظنه', + data: priceSeries.value.map((p) => [p.timestamp, chartPointRial(p.value)]), + }, + ], + } + }) + + const flowChartHeight = computed(() => (isMobile.value ? 240 : 220)) + + const peakHourSlots = computed(() => peakHoursData.value?.hours ?? []) + + const topPeakHours = computed(() => + [...peakHourSlots.value] + .filter((slot) => slot.total > 0) + .sort((a, b) => b.total - a.total) + .slice(0, 3), + ) + + const peakHoursChartOptions = computed(() => { + const mobile = isMobile.value + const slots = peakHourSlots.value + const categories = slots.map((slot) => formatPeakHourLabel(slot.hour)) + + return { + chart: { + type: 'bar', + stacked: true, + toolbar: { show: false }, + fontFamily: 'inherit', + redrawOnParentResize: true, + }, + plotOptions: { + bar: { + borderRadius: 3, + columnWidth: mobile ? '72%' : '62%', + }, + }, + colors: ['#10b981', '#f43f5e'], + grid: { + padding: { left: mobile ? 0 : 4, right: mobile ? 4 : 8 }, + }, + xaxis: { + categories, + tickAmount: mobile ? 8 : 12, + labels: { + rotate: mobile ? -55 : -35, + style: { fontSize: mobile ? '9px' : '10px' }, + }, + }, + yaxis: { + tickAmount: mobile ? 4 : 5, + labels: { + formatter: (v: number) => Number(v).toLocaleString('fa-IR'), + style: { fontSize: mobile ? '10px' : '11px' }, + }, + }, + tooltip: { + y: { formatter: (v: number) => `${Number(v).toLocaleString('fa-IR')} سفارش` }, + }, + legend: { position: 'top', fontSize: mobile ? '10px' : '11px' }, + dataLabels: { enabled: false }, + series: [ + { name: 'خرید', data: slots.map((slot) => slot.buy) }, + { name: 'فروش', data: slots.map((slot) => slot.sell) }, + ], + } + }) + + const peakHoursChartHeight = computed(() => (isMobile.value ? 250 : 230)) + + const productReportItems = computed(() => master.value?.GT_products_report ?? []) + + const productChartHeight = computed(() => { + const count = Math.max(productReportItems.value.length, 1) + const rowHeight = isMobile.value ? 74 : 58 + return Math.max(isMobile.value ? 210 : 230, count * rowHeight + 52) + }) + + const productChartLabelWidth = computed(() => { + const items = productReportItems.value.slice(0, 8) + const mobile = isMobile.value + const categories = items.map((p) => p.title ?? `#${p.id}`) + const displayLabels = categories.map((label) => + mobile ? truncateChartLabel(label, 11) : label, + ) + return productChartLabelColumnWidth(displayLabels, mobile) + }) + + const productChartOptions = computed(() => { + const items = productReportItems.value.slice(0, 8) + const mobile = isMobile.value + const categories = items.map((p) => p.title ?? `#${p.id}`) + const labelWidth = productChartLabelWidth.value + const displayLabel = (val: string) => + mobile ? truncateChartLabel(String(val), 11) : String(val) + + return { + chart: { + type: 'bar', + toolbar: { show: false }, + fontFamily: 'inherit', + redrawOnParentResize: true, + }, + plotOptions: { + bar: { + horizontal: true, + borderRadius: 4, + barHeight: mobile ? '58%' : '68%', + }, + }, + colors: ['#0ea5e9', '#f97316'], + dataLabels: { enabled: false }, + grid: { + padding: { + left: 0, + right: 10, + top: 6, + }, + }, + xaxis: { + categories, + labels: { style: { fontSize: mobile ? '10px' : '11px' } }, + axisBorder: { show: false }, + axisTicks: { show: false }, + }, + yaxis: { + labels: { + minWidth: labelWidth, + maxWidth: labelWidth, + align: 'left', + offsetX: mobile ? -4 : -6, + style: { + fontSize: mobile ? '10px' : '11px', + fontWeight: 500, + }, + formatter: displayLabel, + }, + }, + tooltip: { + shared: true, + intersect: false, + y: { + formatter: (val: number) => `${Number(val).toLocaleString('fa-IR')} گرم`, + }, + x: { + formatter: (_val: number, opts?: { dataPointIndex?: number }) => + categories[opts?.dataPointIndex ?? 0] ?? '', + }, + }, + legend: { + position: 'top', + horizontalAlign: 'center', + fontSize: mobile ? '10px' : '11px', }, - legend: { position: 'top' }, series: [ { name: 'خرید (گرم)', data: items.map((p) => p.transaction_sum_gold_buy ?? 0) }, { @@ -281,18 +562,28 @@ export function useDashboardPage() { balance, walletSums, recentOrders, - pendingOrders, customerStats, priceRange, flowRange, + peakHoursRange, + peakHoursData, + peakHoursLoading, + topPeakHours, flowData, priceSeries, isStore, MESGHAL, + productActivityRange, + productActivityLoading, + productReportItems, + productChartHeight, + productChartLabelWidth, refreshAll, applyDateRange, switchPriceRange, loadFlow, + loadPeakHours, + loadProductActivity, onToggleGateway, formatGram, directionLabel, @@ -301,6 +592,10 @@ export function useDashboardPage() { stsLabel, financeChartOptions, flowChartOptions, + flowChartHeight, + peakHoursChartOptions, + peakHoursChartHeight, + formatPeakHourLabel, priceChartOptions, productChartOptions, usePrice, diff --git a/src/pages/dashboard/composables/useDashboardQuickActions.ts b/src/pages/dashboard/composables/useDashboardQuickActions.ts new file mode 100644 index 0000000..8583625 --- /dev/null +++ b/src/pages/dashboard/composables/useDashboardQuickActions.ts @@ -0,0 +1,115 @@ +import { ref } from 'vue' +import { Modal, message } from 'ant-design-vue' +import { + closeAllProductBuy, + closeAllProductSell, + deactivateAllProducts, + getProductActivationSnapshot, + restoreProductActivation, + syncProductShow, +} from '@/api/product' +import { runPriceSync } from '@/api/price' + +export function useDashboardQuickActions(onAfterAction?: () => Promise) { + const quickActionLoading = ref(false) + const hasProductSnapshot = ref(false) + const productSnapshotCount = ref(0) + + async function loadSnapshotStatus() { + try { + const res = await getProductActivationSnapshot() + hasProductSnapshot.value = !!res?.has_snapshot + productSnapshotCount.value = res?.count ?? 0 + } catch { + hasProductSnapshot.value = false + productSnapshotCount.value = 0 + } + } + + async function runQuickAction(action: () => Promise<{ message?: string } | void>) { + quickActionLoading.value = true + try { + const res = await action() + message.success(res?.message ?? 'انجام شد') + await loadSnapshotStatus() + await onAfterAction?.() + } catch (e) { + console.error(e) + message.error('خطا در انجام عملیات') + } finally { + quickActionLoading.value = false + } + } + + function confirmQuickAction(title: string, content: string, action: () => Promise<{ message?: string }>) { + Modal.confirm({ + title, + content, + okText: 'بله', + cancelText: 'انصراف', + centered: true, + okButtonProps: { danger: title.includes('بستن') && title.includes('محصول') }, + onOk: () => runQuickAction(action), + }) + } + + function deactivateAll() { + confirmQuickAction( + 'بستن همه محصولات', + 'همه محصولات (خرید، فروش و نمایش) بسته می‌شوند. وضعیت فعلی ذخیره می‌شود تا بتوانید بعداً بازگردانی کنید.', + deactivateAllProducts, + ) + } + + function restoreProducts() { + confirmQuickAction( + 'بازگردانی محصولات', + `وضعیت ${productSnapshotCount.value || ''} محصول ذخیره‌شده بازگردانی می‌شود.`, + restoreProductActivation, + ) + } + + function closeAllBuy() { + confirmQuickAction( + 'بستن خرید همه محصولات', + 'امکان خرید برای همه محصولات غیرفعال می‌شود.', + closeAllProductBuy, + ) + } + + function closeAllSell() { + confirmQuickAction( + 'بستن فروش همه محصولات', + 'امکان فروش برای همه محصولات غیرفعال می‌شود.', + closeAllProductSell, + ) + } + + function syncShow() { + void runQuickAction(syncProductShow) + } + + function syncPrices() { + confirmQuickAction( + 'همگام‌سازی قیمت', + 'قیمت محصولات از منبع تنظیم‌شده به‌روزرسانی می‌شود.', + async () => { + const res = await runPriceSync() + return { message: res?.message ?? (res?.data ? 'قیمت به‌روز شد' : 'قیمت تغییری نکرد') } + }, + ) + } + + return { + quickActionLoading, + hasProductSnapshot, + productSnapshotCount, + loadSnapshotStatus, + deactivateAll, + restoreProducts, + closeAllBuy, + closeAllSell, + syncShow, + syncPrices, + } +} diff --git a/src/pages/dashboard/dashboardSectionCopy.ts b/src/pages/dashboard/dashboardSectionCopy.ts index b6adac7..a1cab0b 100644 --- a/src/pages/dashboard/dashboardSectionCopy.ts +++ b/src/pages/dashboard/dashboardSectionCopy.ts @@ -78,18 +78,18 @@ export const dashboardCharts = { formula: 'نمودار = قیمت پایه در هر لحظه', }, productActivity: { - description: 'مقایسه حجم خرید و فروش هر محصول.', - formula: 'خرید = جمع گرم مثبت | فروش = جمع گرم منفی', + description: 'مقایسه حجم خرید و فروش هر محصول در بازه انتخابی.', + formula: 'خرید = جمع گرم مثبت | فروش = جمع گرم منفی (معاملات همان بازه)', }, } as const satisfies Record export const dashboardOrders = { - pending: { - description: 'سفارش‌هایی که هنوز تأیید نشده‌اند.', - formula: 'سود هر گرم × مقدار = سود کل سفارش', + peakHours: { + description: 'توزیع سفارش‌های موفق مشتریان خانگی در ۲۴ ساعت شبانه‌روز؛ برای شناخت ساعات شلوغ.', + formula: 'هر ستون = تعداد سفارش در آن ساعت (جمع چند روز اخیر)', }, recent: { - description: '۱۲ سفارش آخر با جزئیات مظنه و سود.', + description: '۱۴ سفارش آخر با جزئیات مظنه و سود.', formula: 'مظنه = قیمت پایه | قیمت مشتری = مظنه ± اسپرد | سود/گرم = تفاوت اسپرد', }, } as const satisfies Record diff --git a/src/pages/dashboard/index.vue b/src/pages/dashboard/index.vue index d4ef7b4..7c6ba5a 100644 --- a/src/pages/dashboard/index.vue +++ b/src/pages/dashboard/index.vue @@ -3,47 +3,48 @@
-
-
- - /گرم - {{ usePrice(master?.price ?? 0, true, '') }} - · - مثقال - {{ usePrice(master?.pricem ?? 0, true, '') }} - - · {{ toJalali(master.update_date) }} - -
- -
-
- +
+
+
- {{ master?.is_gateway_open ? 'فعال' : 'غیرفعال' }} - + /گرم + {{ usePrice(master?.price ?? 0, true, '') }} + · + مثقال + {{ usePrice(master?.pricem ?? 0, true, '') }}
- - سفارشات - +

+ {{ toJalali(master.update_date) }} +

+
+ +
+
+ +
{{ metric.value }}

-
- - تا - - اعمال - بروزرسانی +
+ + تا + +
+ اعمال + بروزرسانی +
-
+
+
در این بازه داده مالی ثبت نشده
+
-
+
- + class="shrink-0" + > + ۷ روز + ۳۰ روز +
@@ -176,29 +190,27 @@
-
-
+
+
- + class="dashboard-price-range shrink-0" + > + 24 ساعت اخیر + هفته گذشته + ماه گذشته +
- +
-
-
- - -
-
-
-
- #{{ order.id }} - - {{ directionLabel(order.direction) }} - -
-

- {{ order.customer?.name ?? '—' }} -

-
- مقدار: {{ formatGram(order.amount) }} - مظنه: {{ usePrice(order.price_original ?? 0, true, '') }} - قیمت مشتری: {{ usePrice(order.price ?? 0, true, '') }} - - سود: {{ usePrice(order.total_fee ?? 0, true, '') }} - -
+
+
+
+ + + ۷ روز + ۳۰ روز +
+ +
+ + شلوغ‌ترین: {{ formatPeakHourLabel(peakHoursData.peak_hour) }} + ({{ peakHoursData.peak_count }} سفارش) + + + {{ formatPeakHourLabel(slot.hour) }} · {{ slot.total }} + +
+ + + +
+ در این بازه سفارشی ثبت نشده +
+
-
- سفارش بازی نیست + +
+
+ + + روزانه + هفتگی + ماهانه + سالانه + +
+ +
+ +
+
+ در این بازه معامله‌ای برای محصولات ثبت نشده +
+
-
+
-
-
+
+
-
+
-

{{ item.value }}

+

{{ item.value }}

-
+
-
+
- {{ item.value }} +

{{ item.value }}

- -
- - -
diff --git a/src/pages/menuPages/customers/base/utils/walletChargeForm.ts b/src/pages/menuPages/customers/base/utils/walletChargeForm.ts index beaaf20..7de50b3 100644 --- a/src/pages/menuPages/customers/base/utils/walletChargeForm.ts +++ b/src/pages/menuPages/customers/base/utils/walletChargeForm.ts @@ -1,6 +1,6 @@ import type { Charge, WalletChargeDirection } from '@/model/wallet' -export function createEmptyChargeForm(customerId: number): Charge { +export function createEmptyChargeForm(customerId?: number): Charge { return { customer_id: customerId, value: undefined,