fix some bug

This commit is contained in:
2026-07-01 22:28:09 +03:30
parent d2c5c20729
commit 3bf249377f
38 changed files with 1872 additions and 1610 deletions
+2
View File
@@ -8,6 +8,8 @@ export const createProduct = async (obj: Customer) =>
await useAxios.post<AddId>(`/admin/product`, obj) await useAxios.post<AddId>(`/admin/product`, obj)
export const deleteCustomer = async (id: number) => await useAxios.delete(`/admin/product/${id}`) export const deleteCustomer = async (id: number) => await useAxios.delete(`/admin/product/${id}`)
export const archiveProduct = async (id: number) => await useAxios.delete(`/admin/product/${id}`)
export const updateCustomer = async (id: number, obj: Customer) => export const updateCustomer = async (id: number, obj: Customer) =>
await useAxios.put<AddId>(`/admin/product/${id}`, obj) await useAxios.put<AddId>(`/admin/product/${id}`, obj)
+2 -2
View File
@@ -19,7 +19,7 @@
> >
<AButton <AButton
v-if="showCancel" v-if="showCancel"
type="primary" danger
:class="[{ 'col-span-4': showOk }]" :class="[{ 'col-span-4': showOk }]"
size="large" size="large"
ghost ghost
@@ -70,7 +70,7 @@
> >
<AButton <AButton
v-if="showCancel" v-if="showCancel"
type="primary" danger
:class="[{ 'col-span-4': showOk }]" :class="[{ 'col-span-4': showOk }]"
size="large" size="large"
ghost ghost
+80 -69
View File
@@ -1,13 +1,9 @@
(function() { (function() {
'use strict'; 'use strict';
//debugger
// Store config in window object
window.__APP_CONFIG__ = window.__APP_CONFIG__ || {}; window.__APP_CONFIG__ = window.__APP_CONFIG__ || {};
window.__CONFIG_READY__ = false; window.__CONFIG_READY__ = false;
// Create a promise that will be resolved when config is loaded
let configResolve; let configResolve;
let configReject; let configReject;
window.__CONFIG_PROMISE__ = new Promise((resolve, reject) => { window.__CONFIG_PROMISE__ = new Promise((resolve, reject) => {
@@ -15,14 +11,11 @@
configReject = reject; configReject = reject;
}); });
const VERSION_URL = 'https://abshode-service.montgit.ir/server/get_version'; const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL';
const CONFIG_URLS = [
'https://nerkh.gold/get_user',
'https://abshode-service.montgit.ir/server/get_user'
];
const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL'; const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL';
const VERSION_RETRY_ROUNDS = 5;
const CONFIG_RETRY_ROUNDS = 5;
// Timeout to ensure main.ts loads even if config fails
const CONFIG_TIMEOUT = 12000; const CONFIG_TIMEOUT = 12000;
let configTimeoutId; let configTimeoutId;
let mainScriptLoaded = false; let mainScriptLoaded = false;
@@ -31,6 +24,40 @@
return window.location.protocol === 'https:' ? 'https:' : 'http:'; return window.location.protocol === 'https:' ? 'https:' : 'http:';
} }
function getVersionUrls() {
const protocol = getProtocol();
return [
'https://nerkh.gold/get_version',
'https://tablo.talasevin.ir/get_version',
'https://abshode-service.montgit.ir/server/get_version',
protocol + '//212.23.201.173:8381/get_version',
protocol + '//91.212.174.137:4040/server/get_version'
];
}
function getConfigUrls() {
const protocol = getProtocol();
return [
protocol + '//nerkh.gold/get_user',
protocol + '//tablo.talasevin.ir/get_user',
protocol + '//abshode-service.montgit.ir/server/get_user',
protocol + '//212.23.201.173:8381/get_user',
protocol + '//91.212.174.137:4040/server/get_user'
];
}
function sortVersionUrls(urls) {
const lastSuccessUrl = localStorage.getItem(VERSION_SUCCESS_URL_KEY);
if (!lastSuccessUrl) {
return urls.slice();
}
return [lastSuccessUrl].concat(urls.filter((url) => url !== lastSuccessUrl));
}
function sortConfigUrls(urls) { function sortConfigUrls(urls) {
const lastSuccessUrl = localStorage.getItem(CONFIG_SUCCESS_URL_KEY); const lastSuccessUrl = localStorage.getItem(CONFIG_SUCCESS_URL_KEY);
@@ -41,6 +68,12 @@
}); });
} }
function clearStaleSuccessUrl(key, url) {
if (localStorage.getItem(key) === url) {
localStorage.removeItem(key);
}
}
async function fetchJson(url, timeout = 3500) { async function fetchJson(url, timeout = 3500) {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), timeout); const timeoutId = window.setTimeout(() => controller.abort(), timeout);
@@ -72,36 +105,50 @@
} }
} }
async function loadVersion() { async function loadVersion(rounds = VERSION_RETRY_ROUNDS) {
try { const urls = sortVersionUrls(getVersionUrls());
await fetchJson(VERSION_URL, 3500);
console.log('Version loaded from:', VERSION_URL); for (let round = 1; round <= rounds; round++) {
} catch (error) { for (let index = 0; index < urls.length; index++) {
console.warn('Version check failed from', VERSION_URL, error); const url = urls[index];
const timeout = index === urls.length - 1 ? 6000 : 3500;
try {
await fetchJson(url, timeout);
localStorage.setItem(VERSION_SUCCESS_URL_KEY, url);
console.log('Version loaded from:', url);
return;
} catch (error) {
clearStaleSuccessUrl(VERSION_SUCCESS_URL_KEY, url);
console.warn('Version failed from', url, 'round:', round, error);
}
}
} }
console.warn('Version check failed from all endpoints');
} }
async function fetchConfig() { async function fetchConfig(rounds = CONFIG_RETRY_ROUNDS) {
const protocol = getProtocol(); const urls = sortConfigUrls(getConfigUrls());
const urls = sortConfigUrls(
CONFIG_URLS.map((url) => url.replace(/^https?:/, protocol))
);
for (let index = 0; index < urls.length; index++) { for (let round = 1; round <= rounds; round++) {
const url = urls[index]; for (let index = 0; index < urls.length; index++) {
const timeout = index === urls.length - 1 ? 5000 : 3500; const url = urls[index];
const timeout = index === urls.length - 1 ? 7000 : 4000;
try { try {
const config = await fetchJson(url, timeout); const config = await fetchJson(url, timeout);
if (!config || typeof config !== 'object') { if (!config || typeof config !== 'object') {
throw new Error('Config response is invalid'); throw new Error('Config response is invalid');
}
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url);
return { config, url };
} catch (error) {
clearStaleSuccessUrl(CONFIG_SUCCESS_URL_KEY, url);
console.warn('Config failed from', url, 'round:', round, error);
} }
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url);
return { config, url };
} catch (error) {
console.warn('Config failed from', url, error);
} }
} }
@@ -148,7 +195,6 @@
loadMainScript(); loadMainScript();
} }
// Fetch config from server
async function loadConfig() { async function loadConfig() {
try { try {
await loadVersion(); await loadVersion();
@@ -169,57 +215,24 @@
} }
} }
// Function to dynamically load main.ts script
function loadMainScript() { function loadMainScript() {
//const config= window.__APP_CONFIG__
// Check if main.ts is already loaded
if (mainScriptLoaded || document.querySelector('script[src="/src/main.ts"]')) { if (mainScriptLoaded || document.querySelector('script[src="/src/main.ts"]')) {
return; return;
} }
mainScriptLoaded = true; mainScriptLoaded = true;
/*const manifestLink = document.createElement("link")
manifestLink.rel = "manifest"
manifestLink.id= "manifestsdfsdfsdfsdfsdfsdf"
manifestLink.href = `https://nerkh.gold/get_user/${config.user.id}/monifist.json`
document.head.appendChild(manifestLink)*/
import('/src/main.ts').then(() => { import('/src/main.ts').then(() => {
console.log('Main script loaded and executed'); console.log('Main script loaded and executed');
}); });
/*const script = document.createElement('script');
script.type = 'module';
script.src = '/src/main.ts';
script.async = true;*/
// Handle script load errors
/*script.onerror = function() {
console.error('Failed to load main.ts script');
mainScriptLoaded = false;
};
script.onload = function() {
console.log('Main script loaded successfully');
};
document.body.appendChild(script);*/
//console.log('Main script loading started...');
} }
// Set timeout to ensure main.ts loads even if config fails
configTimeoutId = setTimeout(function() { configTimeoutId = setTimeout(function() {
if (!mainScriptLoaded) { if (!mainScriptLoaded) {
console.warn('Config loader: Timeout reached. Loading main.ts with empty config.'); console.warn('Config loader: Timeout reached. Loading main.ts with empty config.');
// Set default empty config
window.__APP_CONFIG__ = window.__APP_CONFIG__ || {}; window.__APP_CONFIG__ = window.__APP_CONFIG__ || {};
window.__CONFIG_READY__ = true; window.__CONFIG_READY__ = true;
// Resolve promise with empty config
if (configResolve) { if (configResolve) {
configResolve({}); configResolve({});
} }
@@ -228,7 +241,5 @@
} }
}, CONFIG_TIMEOUT); }, CONFIG_TIMEOUT);
// Start loading config immediately
loadConfig(); loadConfig();
})(); })();
+3
View File
@@ -6,3 +6,6 @@ export const MODAL_ABOVE_ACTION_SHEET_Z_INDEX = 1100
/** z-index برای dropdown سلکت‌ها داخل مودال (باید بالاتر از مودال باشد) */ /** z-index برای dropdown سلکت‌ها داخل مودال (باید بالاتر از مودال باشد) */
export const SELECT_DROPDOWN_Z_INDEX = MODAL_ABOVE_ACTION_SHEET_Z_INDEX + 100 export const SELECT_DROPDOWN_Z_INDEX = MODAL_ABOVE_ACTION_SHEET_Z_INDEX + 100
/** z-index برای tooltip راهنما داخل مودال (باید از override جدول‌ها و خود مودال بالاتر باشد) */
export const TOOLTIP_ABOVE_MODAL_Z_INDEX = SELECT_DROPDOWN_Z_INDEX + 50
+2
View File
@@ -37,6 +37,8 @@ export interface DashboardFinance {
fullPriceSood?: number fullPriceSood?: number
fullPriceKarmozd?: number fullPriceKarmozd?: number
fullPriceKaermozd?: number fullPriceKaermozd?: number
/** سود اسپرد + کارمزد */
totalTradingProfit?: number
orderCount?: number orderCount?: number
Gifts?: string Gifts?: string
GiftsPrise?: number GiftsPrise?: number
+34
View File
@@ -1,5 +1,7 @@
import type { User } from '@/model/user' import type { User } from '@/model/user'
export type WalletKind = 'main' | 'product' | 'fund'
export interface WalletMetalBalance { export interface WalletMetalBalance {
metal_id: number metal_id: number
label: string label: string
@@ -8,6 +10,20 @@ export interface WalletMetalBalance {
display_unit?: string display_unit?: string
} }
export interface WalletProductSummary {
id?: number
title?: string
group_type?: string
unit?: string
amount?: number | string
metal_id?: number
}
export interface WalletFundSummary {
id?: number
title?: string
}
export interface Wallet { export interface Wallet {
owner: number owner: number
id: number id: number
@@ -17,6 +33,22 @@ export interface Wallet {
product_id?: number | null product_id?: number | null
metal_gold?: Record<string, number> metal_gold?: Record<string, number>
customer: User customer: User
product?: WalletProductSummary | null
fund?: WalletFundSummary | null
wallet_kind?: WalletKind
wallet_kind_label?: string
wallet_subject?: string
product_title?: string | null
product_group_type?: string | null
product_unit?: string | null
product_amount?: number | string | null
fund_title?: string | null
wallet_metal_id?: number
wallet_metal_label?: string
gold_to_cash?: number
gold_grams?: number
total_rial_value?: number
created_at?: string
} }
export interface WalletTableSums { export interface WalletTableSums {
@@ -43,6 +75,8 @@ export interface Withdraw {
tracked_number?: string tracked_number?: string
date_at?: string date_at?: string
sts?: number sts?: number
type?: number
bank_account_id?: number | null
created_at?: string created_at?: string
updated_at?: string updated_at?: string
sts_str?: string sts_str?: string
@@ -1,18 +1,17 @@
<template> <template>
<a-tooltip placement="top" :overlay-inner-style="{ maxWidth: '300px', direction: 'rtl' }"> <a-tooltip
<template #title> placement="top"
<div class="space-y-1.5 text-xs leading-5"> :title="tooltipText"
<p>{{ help.description }}</p> overlay-class-name="dashboard-help-tooltip"
<p v-if="help.formula" class="border-t border-white/20 pt-1.5 text-white/90"> :overlay-style="{ zIndex: TOOLTIP_ABOVE_MODAL_Z_INDEX }"
<span class="font-semibold">فرمول:</span> {{ help.formula }} :overlay-inner-style="{ maxWidth: '26rem', direction: 'rtl', textAlign: 'right', whiteSpace: 'pre-line' }"
</p> :mouse-enter-delay="0.15"
</div> >
</template>
<span <span
class="dashboard-help-trigger inline-flex shrink-0 items-center justify-center" class="dashboard-help-trigger inline-flex shrink-0 items-center justify-center"
role="button" role="img"
tabindex="-1" :aria-label="tooltipText"
@click.stop tabindex="0"
> >
<IosInfoBadge :size="badgeSize" /> <IosInfoBadge :size="badgeSize" />
</span> </span>
@@ -22,6 +21,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy' import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
import IosInfoBadge from '@/pages/dashboard/components/IosInfoBadge.vue' import IosInfoBadge from '@/pages/dashboard/components/IosInfoBadge.vue'
import { TOOLTIP_ABOVE_MODAL_Z_INDEX } from '@/constants/overlayZIndex'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -34,6 +34,14 @@ const props = withDefaults(
) )
const badgeSize = computed(() => props.size) const badgeSize = computed(() => props.size)
const tooltipText = computed(() => {
const blocks = [props.help.description?.trim()].filter(Boolean)
if (props.help.formula?.trim()) {
blocks.push(props.help.formula.trim())
}
return blocks.join('\n\n')
})
</script> </script>
<style scoped> <style scoped>
@@ -42,3 +50,10 @@ const badgeSize = computed(() => props.size)
cursor: help; cursor: help;
} }
</style> </style>
<style>
.ant-tooltip.dashboard-help-tooltip {
z-index: 1250 !important;
pointer-events: none;
}
</style>
@@ -1,10 +1,10 @@
<template> <template>
<div class="dashboard-stat-card flex h-full min-h-[4.25rem] items-center gap-2 rounded-xl px-2.5 py-2"> <div class="dashboard-stat-card flex h-full min-h-0 items-start gap-1.5 rounded-lg px-2 py-1.5">
<div <div
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md"
:class="iconBgClass" :class="iconBgClass"
> >
<BaseIcon :icon="icon" size="17" :class="iconTextClass" /> <BaseIcon :icon="icon" size="15" :class="iconTextClass" />
</div> </div>
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<DashboardLabelWithHelp <DashboardLabelWithHelp
@@ -13,8 +13,10 @@
size="xs" size="xs"
label-class="dashboard-text-muted font-medium" label-class="dashboard-text-muted font-medium"
/> />
<p class="dashboard-text truncate text-[15px] font-bold leading-snug"> <p v-if="subtitle" class="dashboard-stat-card__subtitle">{{ subtitle }}</p>
{{ value }} <p class="dashboard-stat-card__value dashboard-text leading-snug">
<span class="font-bold">{{ value }}</span>
<span v-if="unit" class="dashboard-stat-card__unit">{{ unit }}</span>
</p> </p>
</div> </div>
</div> </div>
@@ -30,6 +32,8 @@ const props = withDefaults(
defineProps<{ defineProps<{
title: string title: string
value: string value: string
subtitle?: string
unit?: string
help?: DashboardHelp help?: DashboardHelp
icon?: string icon?: string
tone?: Tone tone?: Tone
@@ -37,6 +41,7 @@ const props = withDefaults(
{ {
icon: 'vuesax-linear:chart-2', icon: 'vuesax-linear:chart-2',
tone: 'blue', tone: 'blue',
unit: '',
}, },
) )
@@ -109,6 +114,26 @@ const iconTextClass = computed(() => toneMap[props.tone].iconText)
.dashboard-stat-card__icon--orange { .dashboard-stat-card__icon--orange {
background: rgba(249, 115, 22, 0.14); background: rgba(249, 115, 22, 0.14);
} }
.dashboard-stat-card__subtitle {
margin-top: 2px;
font-size: 10px;
line-height: 1.35;
color: var(--admin-text-secondary);
}
.dashboard-stat-card__value {
margin-top: 2px;
font-size: 13px;
word-break: break-word;
}
.dashboard-stat-card__unit {
margin-right: 4px;
font-size: 11px;
font-weight: 600;
color: var(--admin-text-secondary);
}
</style> </style>
<style lang="less"> <style lang="less">
@@ -0,0 +1,183 @@
<template>
<section class="dashboard-income-summary">
<DashboardSectionHeader title="خلاصه درآمد معاملات" :help="help" />
<p class="dashboard-income-summary__intro">
سود اسپرد (اختلاف قیمت) و کارمزد جدا محاسبه می‌شوند. جمع = هر دو. همه مبالغ
<strong>ریال</strong> هستند (نه تومان).
</p>
<div class="dashboard-income-summary__grid">
<article
v-for="row in rows"
:key="row.key"
class="dashboard-income-summary__card"
:class="{ 'dashboard-income-summary__card--total': row.isTotal }"
>
<DashboardLabelWithHelp
v-if="row.help"
:label="row.label"
:help="row.help"
size="xs"
label-class="dashboard-income-summary__card-title"
/>
<span v-else class="dashboard-income-summary__card-title">{{ row.label }}</span>
<p class="dashboard-income-summary__hint">{{ row.hint }}</p>
<p class="dashboard-income-summary__value">
<span class="dashboard-income-summary__amount">{{ row.amount }}</span>
<span class="dashboard-income-summary__unit">{{ unit }}</span>
</p>
</article>
</div>
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import DashboardSectionHeader from '@/pages/dashboard/components/DashboardSectionHeader.vue'
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
import {
dashboardTradingIncomeSummary,
dashboardKpi,
} from '@/pages/dashboard/dashboardSectionCopy'
import { DASHBOARD_RIAL_UNIT, formatDashboardAmount } from '@/pages/dashboard/dashboardFormat'
import type { DashboardFinance } from '@/model/dashboard'
const props = defineProps<{
finance?: DashboardFinance | null
}>()
const help = dashboardTradingIncomeSummary
const unit = DASHBOARD_RIAL_UNIT
const spreadRial = computed(() => Number(props.finance?.fullPriceSood ?? 0))
const commissionRial = computed(() =>
Number(props.finance?.fullPriceKarmozd ?? props.finance?.fullPriceKaermozd ?? 0)
)
const totalRial = computed(() =>
Number(props.finance?.totalTradingProfit ?? spreadRial.value + commissionRial.value)
)
const rows = computed(() => [
{
key: 'spread',
label: 'سود اسپرد',
hint: 'اختلاف قیمت پایه و قیمت مشتری',
amount: formatDashboardAmount(spreadRial.value),
help: dashboardKpi.spreadProfit,
isTotal: false,
},
{
key: 'commission',
label: 'کارمزد',
hint: 'درصد یا وزن کارمزد، به ریال',
amount: formatDashboardAmount(commissionRial.value),
help: dashboardKpi.commission,
isTotal: false,
},
{
key: 'total',
label: 'جمع سود',
hint: 'اسپرد + کارمزد',
amount: formatDashboardAmount(totalRial.value),
help: dashboardKpi.totalTradingProfit,
isTotal: true,
},
])
</script>
<style scoped lang="less">
.dashboard-income-summary {
margin-top: 0.5rem;
border-radius: 0.75rem;
border: 1px solid var(--admin-border);
background: var(--admin-bg-elevated);
padding: 0.65rem 0.75rem 0.75rem;
color: var(--admin-text);
}
.dashboard-income-summary__intro {
margin: 0 0 0.5rem;
font-size: 0.6875rem;
line-height: 1.55;
color: var(--admin-text-secondary);
}
.dashboard-income-summary__grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.5rem;
}
@media (max-width: 640px) {
.dashboard-income-summary__grid {
grid-template-columns: 1fr;
}
}
.dashboard-income-summary__card {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-height: 4.75rem;
padding: 0.5rem 0.6rem;
border-radius: 0.5rem;
border: 1px solid var(--admin-border);
background: var(--admin-bg-muted);
}
.dashboard-income-summary__card--total {
border-color: rgba(245, 158, 11, 0.4);
background: rgba(245, 158, 11, 0.1);
}
.dashboard-income-summary__card-title {
font-size: 0.75rem;
font-weight: 700;
color: var(--admin-text);
line-height: 1.35;
}
.dashboard-income-summary__hint {
margin: 0;
flex: 1;
font-size: 0.625rem;
line-height: 1.45;
color: var(--admin-text-secondary);
}
.dashboard-income-summary__value {
margin: 0.15rem 0 0;
direction: ltr;
text-align: right;
unicode-bidi: isolate;
}
.dashboard-income-summary__amount {
font-size: 0.9375rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
}
.dashboard-income-summary__card--total .dashboard-income-summary__amount {
font-size: 1rem;
color: #b45309;
}
.dashboard-income-summary__unit {
margin-right: 3px;
font-size: 0.625rem;
font-weight: 600;
color: var(--admin-text-secondary);
}
html.is-dark {
.dashboard-income-summary__card--total {
border-color: rgba(245, 158, 11, 0.45);
background: rgba(245, 158, 11, 0.14);
}
.dashboard-income-summary__card--total .dashboard-income-summary__amount {
color: #fcd34d;
}
}
</style>
@@ -23,6 +23,7 @@ import type {
import type { Order } from '@/model/order' import type { Order } from '@/model/order'
import type { Balance } from '@/model/product' import type { Balance } from '@/model/product'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { DASHBOARD_RIAL_UNIT, formatDashboardAmount } from '@/pages/dashboard/dashboardFormat'
import { toJalali } from '@core/utils/useDataTime' import { toJalali } from '@core/utils/useDataTime'
import { metalLabel } from '@/utils/adminMetalLabels' import { metalLabel } from '@/utils/adminMetalLabels'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
@@ -278,11 +279,12 @@ export function useDashboardPage() {
const sood = finance.value?.fullPriceSood ?? 0 const sood = finance.value?.fullPriceSood ?? 0
const karmozd = finance.value?.fullPriceKarmozd ?? finance.value?.fullPriceKaermozd ?? 0 const karmozd = finance.value?.fullPriceKarmozd ?? finance.value?.fullPriceKaermozd ?? 0
const gifts = finance.value?.GiftsPrise ?? 0 const gifts = finance.value?.GiftsPrise ?? 0
const total = finance.value?.totalTradingProfit ?? sood + karmozd
return { return {
...chartSurfaceOptions(), ...chartSurfaceOptions(),
chart: { type: 'donut', fontFamily: 'inherit', offsetY: 4, foreColor: chartSurfaceOptions().chart.foreColor }, chart: { type: 'donut', fontFamily: 'inherit', offsetY: 4, foreColor: chartSurfaceOptions().chart.foreColor },
labels: ['سود معامله', metalLabel.goldCommission(), metalLabel.goldGifts()], labels: ['سود اسپرد', 'کارمزد معامله', 'ارزش هدایا (گرم)'],
colors: ['#10b981', '#f59e0b', '#8b5cf6'], colors: ['#10b981', '#f59e0b', '#8b5cf6'],
legend: { legend: {
position: 'bottom', position: 'bottom',
@@ -300,15 +302,15 @@ export function useDashboardPage() {
show: true, show: true,
total: { total: {
show: true, show: true,
label: 'درآمد خالص', label: 'جمع سود معاملات',
formatter: () => usePrice(finance.value?.sumAll ?? 0, true, ''), formatter: () => `${formatDashboardAmount(total)} ${DASHBOARD_RIAL_UNIT}`,
}, },
}, },
}, },
}, },
}, },
tooltip: { tooltip: {
y: { formatter: (v: number) => usePrice(v, true, '') }, y: { formatter: (v: number) => `${formatDashboardAmount(v)} ${DASHBOARD_RIAL_UNIT}` },
}, },
series: [sood, karmozd, gifts], series: [sood, karmozd, gifts],
} }
@@ -409,7 +411,7 @@ export function useDashboardPage() {
}, },
tooltip: { tooltip: {
x: { format: tooltipDateFormat }, x: { format: tooltipDateFormat },
y: { formatter: (v: number) => usePrice(v, true, '') }, y: { formatter: (v: number) => `${formatDashboardAmount(v)} ${DASHBOARD_RIAL_UNIT}` },
}, },
series: [ series: [
{ {
+10
View File
@@ -0,0 +1,10 @@
/** مبلغ بدون واحد — برای نمایش جدا از «ریال» در کارت‌های داشبورد */
export function formatDashboardAmount(value: number | string | null | undefined): string {
const n = Number(value ?? 0)
if (Number.isNaN(n)) return '0'
return Math.round(n)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
export const DASHBOARD_RIAL_UNIT = 'ریال'
+22 -9
View File
@@ -27,13 +27,17 @@ export const dashboardGateway = help(
) )
export const dashboardKpi = { export const dashboardKpi = {
profit: help( spreadProfit: help(
'مجموع سود اسپرد (تفاوت قیمت خرید/فروش با مشتری) روی همه سفارش‌های ثبت‌شده در بازه.', 'مجموع سود اسپرد (اختلاف قیمت پایه و قیمت مشتری) — فقط از تفاوت buy/sell price. کارمزد جداگانه در کارت «کارمزد معامله» است.',
'سود کل = جمع «سود هر سفارش»' 'سود اسپرد = جمع «سود کل» (total_fee) هر سفارش در بازه'
), ),
commission: help( commission: help(
'مجموع کارمزد طلایی محصول که روی همان سفارش‌ها محاسبه شده.', 'مجموع کارمزد ریالی معاملات (درصد/وزن کارمزد) — جدا از سود اسپرد.',
'کارمزد کل = جمع «کارمزد هر سفارش»' 'کارمزد = جمع service_charge_price هر سفارش در بازه'
),
totalTradingProfit: help(
'مجموع سود اسپرد و کارمزد — همان ردیف «جمع» در خلاصه درآمد.',
'جمع = سود اسپرد + کارمزد معاملات'
), ),
orderCount: help( orderCount: help(
'تعداد همه سفارش‌هایی که در بازه انتخابی ثبت شده‌اند (هر وضعیتی).', 'تعداد همه سفارش‌هایی که در بازه انتخابی ثبت شده‌اند (هر وضعیتی).',
@@ -53,6 +57,11 @@ export const dashboardKpi = {
), ),
} as const satisfies Record<string, DashboardHelp> } as const satisfies Record<string, DashboardHelp>
export const dashboardTradingIncomeSummary = help(
'تفکیک درآمد از معاملات در بازه انتخابی: سود اسپرد (اختلاف قیمت) و کارمزد ریالی جدا، و جمع هر دو.',
'جمع سود معاملات = سود اسپرد + کارمزد معاملات'
)
export const dashboardDateRange = help( export const dashboardDateRange = help(
'تاریخ شروع و پایان برای محاسبه کارت‌ها، تراز و نمودارها.', 'تاریخ شروع و پایان برای محاسبه کارت‌ها، تراز و نمودارها.',
'گزارش = از تاریخ … تا تاریخ …' 'گزارش = از تاریخ … تا تاریخ …'
@@ -64,8 +73,12 @@ export const dashboardDateMetrics = {
'جمع مبلغ واریزهای تأییدشده از درگاه' 'جمع مبلغ واریزهای تأییدشده از درگاه'
), ),
netIncome: help( netIncome: help(
'درآمد پس از کسر ارزش طلای هدیه‌داده‌شده.', 'شاخص قدیمی: اختلاف قیمت اصلی و پرداختی منهای ارزش هدایا — با «جمع سود معاملات» یکی نیست.',
'درآمد خالص = (اختلاف قیمت اصلی و پرداختی) − (گرم هدیه × مظنه)' 'تراز قیمت−پرداخت = جمع |قیمت اصلی − پرداختی| − (گرم هدیه × مظنه)'
),
totalTradingProfit: help(
'همان «جمع سود معاملات» در خلاصه بالا — اسپرد + کارمزد.',
'جمع = fullPriceSood + fullPriceKarmozd'
), ),
gifts: help( gifts: help(
'طلایی که ادمین به مشتریان هدیه داده در همین بازه.', 'طلایی که ادمین به مشتریان هدیه داده در همین بازه.',
@@ -75,8 +88,8 @@ export const dashboardDateMetrics = {
export const dashboardCharts = { export const dashboardCharts = {
incomeMix: help( incomeMix: help(
'نشان می‌دهد سود معامله، کارمزد و هدایا چقدر از درآمد کل را تشکیل می‌دهند.', 'تفکیک درآمد: سود اسپرد (اختلاف قیمت)، کارمزد ریالی، و ارزش هدایا.',
'درآمد = سود معامله + کارمزد + (گرم هدیه × مظنه)' 'درآمد = سود اسپرد + کارمزد + (گرم هدیه × مظنه)'
), ),
buySellVolume: help( buySellVolume: help(
'مقدار طلای خرید و فروش مشتری در هر روز (فقط سفارش موفق).', 'مقدار طلای خرید و فروش مشتری در هر روز (فقط سفارش موفق).',
+77 -46
View File
@@ -16,10 +16,10 @@
label-class="text-white/80" label-class="text-white/80"
/> />
<span class="text-[11px] text-white/80">/گرم</span> <span class="text-[11px] text-white/80">/گرم</span>
<span class="text-lg font-bold leading-none">{{ usePrice(master?.price ?? 0, true, '') }}</span> <span class="text-lg font-bold leading-none">{{ usePrice(master?.price ?? 0, true, 'ریال') }}</span>
<span class="text-white/35">·</span> <span class="text-white/35">·</span>
<span class="text-[11px] text-white/80">مثقال</span> <span class="text-[11px] text-white/80">مثقال</span>
<span class="text-sm font-semibold leading-none">{{ usePrice(master?.pricem ?? 0, true, '') }}</span> <span class="text-sm font-semibold leading-none">{{ usePrice(master?.pricem ?? 0, true, 'ریال') }}</span>
</div> </div>
<p v-if="master?.update_date" class="mt-1 text-[10px] leading-none text-white/55"> <p v-if="master?.update_date" class="mt-1 text-[10px] leading-none text-white/55">
{{ toJalali(master.update_date) }} {{ toJalali(master.update_date) }}
@@ -45,45 +45,33 @@
<DashboardQuickWalletModal v-model:visible="showQuickWallet" /> <DashboardQuickWalletModal v-model:visible="showQuickWallet" />
<!-- KPI --> <DashboardTradingIncomeSummary :finance="finance" />
<section class="mt-2 grid grid-cols-2 items-stretch gap-2 md:grid-cols-3 xl:grid-cols-6">
<DashboardStatCard <!-- تراز و آمار — ۴ کارت در یک ردیف -->
tone="gold" <section class="dashboard-metrics-row mt-2">
icon="vuesax-linear:money-receive"
title="سود معاملات"
:value="usePrice(finance?.fullPriceSood ?? 0, true, '')"
:help="copy.kpi.profit"
/>
<DashboardStatCard
tone="amber"
icon="vuesax-linear:coin"
:title="metalLabel.goldCommission()"
:value="usePrice(finance?.fullPriceKarmozd ?? finance?.fullPriceKaermozd ?? 0, true, '')"
:help="copy.kpi.commission"
/>
<DashboardStatCard <DashboardStatCard
tone="green" tone="green"
icon="vuesax-linear:receipt-2" icon="vuesax-linear:receipt-2"
title="تعداد سفارش" title="تعداد سفارش"
subtitle="ثبت‌شده در بازه"
:value="String(finance?.orderCount ?? 0)" :value="String(finance?.orderCount ?? 0)"
:help="copy.kpi.orderCount" :help="copy.kpi.orderCount"
/> />
<template v-if="metalBalanceItems.length"> <DashboardStatCard
<DashboardStatCard v-if="metalBalanceItems.length === 1"
v-for="item in metalBalanceItems" tone="blue"
:key="item.metal_id" icon="vuesax-linear:empty-wallet"
tone="blue" :title="metalBalanceTitle(metalBalanceItems[0].metal_id, metalBalanceItems[0].label)"
icon="vuesax-linear:empty-wallet" subtitle="تراز وزن — سفارش موفق"
:title="metalBalanceTitle(item.metal_id, item.label)" :value="formatGram(metalBalanceItems[0].goldBalance, metalBalanceItems[0].metal_id)"
:value="formatGram(item.goldBalance, item.metal_id)" :help="copy.kpi.goldBalance"
:help="copy.kpi.goldBalance" />
/>
</template>
<DashboardStatCard <DashboardStatCard
v-else v-else
tone="blue" tone="blue"
icon="vuesax-linear:empty-wallet" icon="vuesax-linear:empty-wallet"
:title="metalLabel.goldBalance()" :title="metalLabel.goldBalance()"
subtitle="تراز وزن — سفارش موفق"
:value="formatGram(balance?.goldBalance)" :value="formatGram(balance?.goldBalance)"
:help="copy.kpi.goldBalance" :help="copy.kpi.goldBalance"
/> />
@@ -91,22 +79,26 @@
tone="rose" tone="rose"
icon="vuesax-linear:dollar-circle" icon="vuesax-linear:dollar-circle"
title="تراز ریالی" title="تراز ریالی"
:value="usePrice(balance?.cashBalance ?? 0, true, '')" subtitle="ورود/خروج نقد"
:value="formatDashboardAmount(balance?.cashBalance)"
:unit="DASHBOARD_RIAL_UNIT"
:help="copy.kpi.cashBalance" :help="copy.kpi.cashBalance"
/> />
<DashboardStatCard <DashboardStatCard
tone="violet" tone="violet"
icon="vuesax-linear:chart-2" icon="vuesax-linear:chart-2"
title="تراز مظنه" title="تراز مظنه"
:value="usePrice(balance?.mazanehBalance ?? 0, true, '')" subtitle="میانگین موزون"
:value="formatDashboardAmount(balance?.mazanehBalance)"
:unit="DASHBOARD_RIAL_UNIT"
:help="copy.kpi.mazanehBalance" :help="copy.kpi.mazanehBalance"
/> />
</section> </section>
<!-- Date filter --> <!-- بازه گزارش -->
<section class="dashboard-panel mt-2"> <section class="dashboard-panel mt-2">
<DashboardSectionHeader title="بازه گزارش" :help="copy.dateRange" /> <DashboardSectionHeader title="بازه گزارش" :help="copy.dateRange" />
<div class="mb-2 grid grid-cols-1 gap-2 sm:grid-cols-3"> <div class="dashboard-metrics-row mb-2">
<div <div
v-for="metric in dateRangeMetrics" v-for="metric in dateRangeMetrics"
:key="metric.label" :key="metric.label"
@@ -149,7 +141,7 @@
<DashboardSectionHeader title="ترکیب درآمد" :help="copy.charts.incomeMix" /> <DashboardSectionHeader title="ترکیب درآمد" :help="copy.charts.incomeMix" />
<div class="dashboard-chart-wrap"> <div class="dashboard-chart-wrap">
<apexchart <apexchart
v-if="(finance?.fullPriceSood ?? 0) + (finance?.fullPriceKarmozd ?? 0) > 0" v-if="(finance?.fullPriceSood ?? 0) + (finance?.fullPriceKarmozd ?? 0) + (finance?.GiftsPrise ?? 0) > 0"
type="donut" type="donut"
height="220" height="220"
:options="financeChartOptions" :options="financeChartOptions"
@@ -349,8 +341,9 @@
<th class="px-2 py-2 text-right">مقدار</th> <th class="px-2 py-2 text-right">مقدار</th>
<th class="px-2 py-2 text-right">مظنه/گرم</th> <th class="px-2 py-2 text-right">مظنه/گرم</th>
<th class="px-2 py-2 text-right">قیمت مشتری</th> <th class="px-2 py-2 text-right">قیمت مشتری</th>
<th class="px-2 py-2 text-right">سود/گرم</th> <th class="px-2 py-2 text-right">سود اسپرد/گرم</th>
<th class="px-2 py-2 text-right">سود کل</th> <th class="px-2 py-2 text-right">سود اسپرد کل</th>
<th class="px-2 py-2 text-right">کارمزد (ریال)</th>
<th class="px-2 py-2 text-right">وضعیت</th> <th class="px-2 py-2 text-right">وضعیت</th>
<th class="px-2 py-2 text-right">تاریخ</th> <th class="px-2 py-2 text-right">تاریخ</th>
</tr> </tr>
@@ -369,13 +362,16 @@
</td> </td>
<td class="max-w-[8rem] truncate px-2 py-2.5">{{ order.customer?.name ?? '—' }}</td> <td class="max-w-[8rem] truncate px-2 py-2.5">{{ order.customer?.name ?? '—' }}</td>
<td class="px-2 py-2.5">{{ formatGram(order.amount) }}</td> <td class="px-2 py-2.5">{{ formatGram(order.amount) }}</td>
<td class="px-2 py-2.5">{{ usePrice(order.price_original ?? 0, true, '') }}</td> <td class="px-2 py-2.5">{{ usePrice(order.price_original ?? 0, true, 'ریال') }}</td>
<td class="px-2 py-2.5">{{ usePrice(order.price ?? 0, true, '') }}</td> <td class="px-2 py-2.5">{{ usePrice(order.price ?? 0, true, 'ریال') }}</td>
<td class="dashboard-profit px-2 py-2.5"> <td class="dashboard-profit px-2 py-2.5">
{{ usePrice(order.fee ?? 0, true, '') }} {{ usePrice(order.fee ?? 0, true, 'ریال') }}
</td> </td>
<td class="dashboard-profit px-2 py-2.5 font-semibold"> <td class="dashboard-profit px-2 py-2.5 font-semibold">
{{ usePrice(order.total_fee ?? 0, true, '') }} {{ usePrice(order.total_fee ?? 0, true, 'ریال') }}
</td>
<td class="px-2 py-2.5">
{{ usePrice(order.service_charge_price ?? 0, true, 'ریال') }}
</td> </td>
<td class="px-2 py-2.5"> <td class="px-2 py-2.5">
<span class="rounded-md px-2 py-0.5 text-xs" :class="stsClass(order.sts)"> <span class="rounded-md px-2 py-0.5 text-xs" :class="stsClass(order.sts)">
@@ -449,6 +445,7 @@ import DashboardQuickActionsBar from '@/pages/dashboard/components/DashboardQuic
import DashboardQuickWalletModal from '@/pages/dashboard/components/DashboardQuickWalletModal.vue' import DashboardQuickWalletModal from '@/pages/dashboard/components/DashboardQuickWalletModal.vue'
import DashboardSectionHeader from '@/pages/dashboard/components/DashboardSectionHeader.vue' import DashboardSectionHeader from '@/pages/dashboard/components/DashboardSectionHeader.vue'
import DashboardStatCard from '@/pages/dashboard/components/DashboardStatCard.vue' import DashboardStatCard from '@/pages/dashboard/components/DashboardStatCard.vue'
import DashboardTradingIncomeSummary from '@/pages/dashboard/components/DashboardTradingIncomeSummary.vue'
import { import {
dashboardCharts, dashboardCharts,
dashboardCustomerMetrics, dashboardCustomerMetrics,
@@ -462,6 +459,7 @@ import {
dashboardWallet, dashboardWallet,
dashboardWalletMetrics, dashboardWalletMetrics,
} from '@/pages/dashboard/dashboardSectionCopy' } from '@/pages/dashboard/dashboardSectionCopy'
import { DASHBOARD_RIAL_UNIT, formatDashboardAmount } from '@/pages/dashboard/dashboardFormat'
import { useDashboardPage } from '@/pages/dashboard/composables/useDashboardPage' import { useDashboardPage } from '@/pages/dashboard/composables/useDashboardPage'
import { useDashboardQuickActions } from '@/pages/dashboard/composables/useDashboardQuickActions' import { useDashboardQuickActions } from '@/pages/dashboard/composables/useDashboardQuickActions'
import { metalBalanceTitle, metalLabel } from '@/utils/adminMetalLabels' import { metalBalanceTitle, metalLabel } from '@/utils/adminMetalLabels'
@@ -523,6 +521,12 @@ const {
customerStats, customerStats,
} = useDashboardPage() } = useDashboardPage()
const tradingProfitTotal = computed(() => {
const spread = Number(finance.value?.fullPriceSood ?? 0)
const commission = Number(finance.value?.fullPriceKarmozd ?? finance.value?.fullPriceKaermozd ?? 0)
return Number(finance.value?.totalTradingProfit ?? spread + commission)
})
const { const {
quickActionLoading, quickActionLoading,
hasProductSnapshot, hasProductSnapshot,
@@ -543,16 +547,21 @@ void loadSnapshotStatus()
const dateRangeMetrics = computed(() => [ const dateRangeMetrics = computed(() => [
{ {
label: 'واریز بانکی', label: 'واریز بانکی',
value: usePrice(balance.value?.income_money ?? 0), value: `${formatDashboardAmount(balance.value?.income_money)} ${DASHBOARD_RIAL_UNIT}`,
help: dashboardDateMetrics.bankDeposit, help: dashboardDateMetrics.bankDeposit,
}, },
{ {
label: 'درآمد خالص', label: 'جمع سود معاملات',
value: usePrice(finance.value?.sumAll ?? 0), value: `${formatDashboardAmount(tradingProfitTotal.value)} ${DASHBOARD_RIAL_UNIT}`,
help: dashboardDateMetrics.totalTradingProfit,
},
{
label: 'تراز قیمت−پرداخت (پس از هدیه)',
value: `${formatDashboardAmount(finance.value?.sumAll)} ${DASHBOARD_RIAL_UNIT}`,
help: dashboardDateMetrics.netIncome, help: dashboardDateMetrics.netIncome,
}, },
{ {
label: 'هدایا', label: 'هدایا (گرم)',
value: `${finance.value?.Gifts ?? '0'} گرم`, value: `${finance.value?.Gifts ?? '0'} گرم`,
help: dashboardDateMetrics.gifts, help: dashboardDateMetrics.gifts,
}, },
@@ -594,13 +603,13 @@ const walletMetricItems = computed(() => [
}, },
{ {
label: 'ریال مشتریان', label: 'ریال مشتریان',
value: usePrice(walletSums.value?.cash ?? 0, true, ''), value: `${formatDashboardAmount(walletSums.value?.cash)} ${DASHBOARD_RIAL_UNIT}`,
help: dashboardWalletMetrics.cash, help: dashboardWalletMetrics.cash,
variant: 'sky', variant: 'sky',
}, },
{ {
label: metalLabel.goldValueInRial(), label: metalLabel.goldValueInRial(),
value: usePrice(walletSums.value?.goldToCash ?? 0, true, ''), value: `${formatDashboardAmount(walletSums.value?.goldToCash)} ${DASHBOARD_RIAL_UNIT}`,
help: dashboardWalletMetrics.goldToCash, help: dashboardWalletMetrics.goldToCash,
variant: 'violet', variant: 'violet',
}, },
@@ -616,6 +625,24 @@ const walletMetricItems = computed(() => [
padding: 1rem; padding: 1rem;
} }
.dashboard-metrics-row {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.5rem;
}
@media (max-width: 1024px) {
.dashboard-metrics-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 480px) {
.dashboard-metrics-row {
grid-template-columns: 1fr;
}
}
.dashboard-text { .dashboard-text {
color: var(--admin-text); color: var(--admin-text);
} }
@@ -630,6 +657,10 @@ const walletMetricItems = computed(() => [
.dashboard-inline-metric { .dashboard-inline-metric {
background: var(--admin-bg-muted); background: var(--admin-bg-muted);
min-height: 3.75rem;
display: flex;
flex-direction: column;
justify-content: space-between;
} }
.dashboard-profit { .dashboard-profit {
@@ -90,6 +90,7 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
text_rule: 'longtext', text_rule: 'longtext',
text_about: 'longtext', text_about: 'longtext',
fee_service_charge_percentage: 'number', fee_service_charge_percentage: 'number',
fee_deduct_from_gold: 'switch',
fee_single: 'price', fee_single: 'price',
buy_fee: 'price', buy_fee: 'price',
sell_fee: 'price', sell_fee: 'price',
@@ -285,6 +286,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
'مبلغ کسر (ریال) از قیمت پایه برای سمت فروش. اگر خالی بگذارید، از «اختلاف پایه» استفاده می‌شود.', 'مبلغ کسر (ریال) از قیمت پایه برای سمت فروش. اگر خالی بگذارید، از «اختلاف پایه» استفاده می‌شود.',
fee_service_charge_percentage: fee_service_charge_percentage:
'درصد کارمزد هر معامله از مبلغ سفارش (اعشاری؛ مثلاً ۰٫۰۰۵ یعنی نیم در هزار).', 'درصد کارمزد هر معامله از مبلغ سفارش (اعشاری؛ مثلاً ۰٫۰۰۵ یعنی نیم در هزار).',
fee_deduct_from_gold:
'روشن: کارمزد از مقدار طلا کسر می‌شود — در فروش مبلغ ریالی دقیق (مثلاً ۱۰۰ میلیون) واریز می‌شود و کارمزد از طلای کیف کم می‌شود. خاموش (پیش‌فرض): کارمزد از ریال فروش کم می‌شود — رفتار قبلی.',
fee_free: fee_free:
'تعداد اولین سفارش‌های هر کاربر که از کارمزد معاف می‌شوند. صفر یعنی غیرفعال.', 'تعداد اولین سفارش‌های هر کاربر که از کارمزد معاف می‌شوند. صفر یعنی غیرفعال.',
min_withdraw: min_withdraw:
@@ -309,6 +312,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
'روشن: پس از ثبت‌نام کاربر جدید، پیامک اطلاع‌رسانی برای معرف ارسال می‌شود.', 'روشن: پس از ثبت‌نام کاربر جدید، پیامک اطلاع‌رسانی برای معرف ارسال می‌شود.',
register_referral_bonus_message: register_referral_bonus_message:
'روشن: متن پاداش معرف در پیامک خوش‌آمد ثبت‌نام گنجانده می‌شود.', 'روشن: متن پاداش معرف در پیامک خوش‌آمد ثبت‌نام گنجانده می‌شود.',
referral_gift_on_first_order:
'روشن: پاداش معرف و معرف‌شونده پس از اولین سفارش واریز می‌شود. خاموش: واریز هنگام ثبت‌نام/تأیید احراز (رفتار قبلی).',
referral_limit: referral_limit:
'حداکثر تعداد دعوت مجاز برای هر کاربر. خالی = بدون محدودیت.', 'حداکثر تعداد دعوت مجاز برای هر کاربر. خالی = بدون محدودیت.',
referral_bonus_amount: referral_bonus_amount:
@@ -423,11 +428,13 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
buy_fee: 'اضافه قیمت خرید', buy_fee: 'اضافه قیمت خرید',
sell_fee: 'کسر قیمت فروش', sell_fee: 'کسر قیمت فروش',
fee_service_charge_percentage: 'درصد کارمزد معامله', fee_service_charge_percentage: 'درصد کارمزد معامله',
fee_deduct_from_gold: 'کارمزد از مقدار طلا',
fee_free: 'معافیت کارمزد سفارش اول', fee_free: 'معافیت کارمزد سفارش اول',
min_withdraw: 'حداقل مبلغ برداشت', min_withdraw: 'حداقل مبلغ برداشت',
min_first_buy: 'حداقل اولین خرید', min_first_buy: 'حداقل اولین خرید',
minimum_sell: 'حداقل مقدار فروش', minimum_sell: 'حداقل مقدار فروش',
referral_id: 'کد معرف اجباری', referral_id: 'کد معرف اجباری',
referral_gift_on_first_order: 'پاداش معرف پس از اولین سفارش',
open_register: 'ثبت‌نام باز', open_register: 'ثبت‌نام باز',
auto_active_customer: 'فعال‌سازی خودکار پس از ثبت‌نام', auto_active_customer: 'فعال‌سازی خودکار پس از ثبت‌نام',
balance_report_exclude_customer_id: 'حذف مشتری از گزارش تراز', balance_report_exclude_customer_id: 'حذف مشتری از گزارش تراز',
+103 -22
View File
@@ -17,7 +17,7 @@
<BasicActionSheet <BasicActionSheet
v-model:visible="addUserVisible" v-model:visible="addUserVisible"
:title="`${editId ? 'ویرایش' : 'افزودن'} کاربر`" :title="`${editId ? 'ویرایش' : 'افزودن'} کاربر`"
modal-width="40rem" modal-width="55rem"
:loading="loading" :loading="loading"
:showButton="true" :showButton="true"
:showCancel="true" :showCancel="true"
@@ -29,47 +29,77 @@
> >
<div v-if="isMobile" class="mb-3 text-base font-700">{{ `${editId ? 'ویرایش' : 'افزودن'} کاربر` }}</div> <div v-if="isMobile" class="mb-3 text-base font-700">{{ `${editId ? 'ویرایش' : 'افزودن'} کاربر` }}</div>
<a-form :model="form" ref="formRef" autocomplete="off"> <a-form :model="form" ref="formRef" autocomplete="off">
<div class="grid md:grid-cols-2 gap-4"> <div class="grid gap-4 md:grid-cols-3">
<a-form-item label="نام" name="fname" :rules="inputRules()"> <a-form-item name="fname" :rules="inputRules()">
<template #label>
<DashboardLabelWithHelp label="نام" :help="fnameHelp" size="sm" />
</template>
<BasicInput v-model:value="form.fname" /> <BasicInput v-model:value="form.fname" />
</a-form-item> </a-form-item>
<a-form-item label="نام خانوادگی" name="lname" :rules="inputRules()"> <a-form-item name="lname" :rules="inputRules()">
<template #label>
<DashboardLabelWithHelp label="نام خانوادگی" :help="lnameHelp" size="sm" />
</template>
<BasicInput v-model:value="form.lname" /> <BasicInput v-model:value="form.lname" />
</a-form-item> </a-form-item>
<a-form-item label="شماره همراه" name="mobile" :rules="inputLenRules(11)"> <a-form-item name="mobile" :rules="inputLenRules(11)">
<template #label>
<DashboardLabelWithHelp label="شماره همراه" :help="mobileHelp" size="sm" />
</template>
<BasicInput v-model:value="form.mobile" type="number" /> <BasicInput v-model:value="form.mobile" type="number" />
</a-form-item> </a-form-item>
<a-form-item label="پسورد" name="password"> <a-form-item name="password">
<template #label>
<DashboardLabelWithHelp label="پسورد" :help="passwordHelp" size="sm" />
</template>
<BasicInput is-password v-model:value="form.password" /> <BasicInput is-password v-model:value="form.password" />
</a-form-item> </a-form-item>
<a-form-item label="تاریخ تولد" name="birth_date"> <a-form-item name="birth_date">
<template #label>
<DashboardLabelWithHelp label="تاریخ تولد" :help="birthDateHelp" size="sm" />
</template>
<BaseDatePicker v-model:value="form.birth_date" /> <BaseDatePicker v-model:value="form.birth_date" />
</a-form-item> </a-form-item>
<!-- <a-form-item label="ایمیل" name="email" :rules="inputRules()"> <a-form-item name="phone_number">
<BasicInput v-model:value="form.email" /> <template #label>
</a-form-item>--> <DashboardLabelWithHelp label="شماره تلفن" :help="phoneNumberHelp" size="sm" />
<!-- <a-form-item label="نام" name="shop_name" :rules="inputRules()"> </template>
<BasicInput v-model:value="form.shop_name" />
</a-form-item>-->
<a-form-item label="شماره تلفن" name="phone_number">
<BasicInput v-model:value="form.phone_number" type="number" /> <BasicInput v-model:value="form.phone_number" type="number" />
</a-form-item> </a-form-item>
<a-form-item label="نام استان" name="province"> <a-form-item name="province">
<template #label>
<DashboardLabelWithHelp label="نام استان" :help="provinceHelp" size="sm" />
</template>
<BasicInput v-model:value="form.province" /> <BasicInput v-model:value="form.province" />
</a-form-item> </a-form-item>
<a-form-item label="نام شهر" name="city"> <a-form-item name="city">
<template #label>
<DashboardLabelWithHelp label="نام شهر" :help="cityHelp" size="sm" />
</template>
<BasicInput v-model:value="form.city" /> <BasicInput v-model:value="form.city" />
</a-form-item> </a-form-item>
<a-form-item label="شماره ملی" name="n_code"> <a-form-item name="n_code">
<template #label>
<DashboardLabelWithHelp label="شماره ملی" :help="nCodeHelp" size="sm" />
</template>
<BasicInput v-model:value="form.n_code" /> <BasicInput v-model:value="form.n_code" />
</a-form-item> </a-form-item>
<a-form-item label="میزان پاداش" name="profit_amount"> <a-form-item name="profit_amount">
<template #label>
<DashboardLabelWithHelp label="میزان پاداش" :help="profitAmountHelp" size="sm" />
</template>
<BasicInput v-model:value="form.profit_amount" /> <BasicInput v-model:value="form.profit_amount" />
</a-form-item> </a-form-item>
<a-form-item label="حداکثر معرفی" name="add_user_limit"> <a-form-item name="add_user_limit">
<template #label>
<DashboardLabelWithHelp label="حداکثر معرفی" :help="addUserLimitHelp" size="sm" />
</template>
<BasicInput v-model:value="form.add_user_limit" /> <BasicInput v-model:value="form.add_user_limit" />
</a-form-item> </a-form-item>
<a-form-item label="نوع مشتری" name="n_code"> <a-form-item name="type">
<template #label>
<DashboardLabelWithHelp label="نوع مشتری" :help="customerTypeHelp" size="sm" />
</template>
<BasicSelect <BasicSelect
v-model:value="form.type" v-model:value="form.type"
:items="customerItems" :items="customerItems"
@@ -77,10 +107,16 @@
keyName="key" keyName="key"
/> />
</a-form-item> </a-form-item>
<a-form-item label="کد معرف" name="n_code"> <a-form-item name="referral_to_id">
<template #label>
<DashboardLabelWithHelp label="کد معرف" :help="referralToIdHelp" size="sm" />
</template>
<BasicInput v-model:value="form.referral_to_id" :disabled="editId" /> <BasicInput v-model:value="form.referral_to_id" :disabled="editId" />
</a-form-item> </a-form-item>
<a-form-item v-if="editId" label="سقف اعتبار" name="gram" :rules="inputRules()"> <a-form-item v-if="editId" name="gram" :rules="inputRules()">
<template #label>
<DashboardLabelWithHelp label="سقف اعتبار" :help="gramHelp" size="sm" />
</template>
<BasicInput v-model:value="form.gram" /> <BasicInput v-model:value="form.gram" />
</a-form-item> </a-form-item>
<!-- <a-form-item label="نام چاپی" name="print_name" :rules="inputRules()"> <!-- <a-form-item label="نام چاپی" name="print_name" :rules="inputRules()">
@@ -314,6 +350,51 @@ import {
walletChargeTitle walletChargeTitle
} from './utils/walletChargeForm' } from './utils/walletChargeForm'
import { metalLabel } from '@/utils/adminMetalLabels' import { metalLabel } from '@/utils/adminMetalLabels'
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
const fnameHelp: DashboardHelp = {
description: 'نام کوچک مشتری که در اپ و پنل نمایش داده می‌شود.',
}
const lnameHelp: DashboardHelp = {
description: 'نام خانوادگی مشتری در پروفایل و گزارش‌ها.',
}
const mobileHelp: DashboardHelp = {
description: 'شماره همراه ۱۱ رقمی برای ورود به اپ و ارسال پیامک.',
}
const passwordHelp: DashboardHelp = {
description: 'رمز عبور ورود به اپ. در ویرایش خالی بگذارید اگر نمی‌خواهید تغییر کند.',
}
const birthDateHelp: DashboardHelp = {
description: 'تاریخ تولد مشتری برای احراز هویت و پرونده کاربری.',
}
const phoneNumberHelp: DashboardHelp = {
description: 'شماره تلفن ثابت (اختیاری) برای تماس.',
}
const provinceHelp: DashboardHelp = {
description: 'نام استان محل سکونت مشتری.',
}
const cityHelp: DashboardHelp = {
description: 'نام شهر محل سکونت مشتری.',
}
const nCodeHelp: DashboardHelp = {
description: 'کد ملی ۱۰ رقمی برای احراز هویت.',
}
const profitAmountHelp: DashboardHelp = {
description: 'میزان پاداش معرفی این کاربر به گرم طلا.',
}
const addUserLimitHelp: DashboardHelp = {
description: 'حداکثر تعداد نفری که این مشتری می‌تواند با کد معرف خود دعوت کند.',
}
const customerTypeHelp: DashboardHelp = {
description: 'نوع مشتری: همکار، خانگی یا سرمایه‌گذار — تعیین‌کننده دسترسی و قوانین معامله.',
}
const referralToIdHelp: DashboardHelp = {
description: 'کد معرف فردی که این مشتری را دعوت کرده. پس از ثبت قابل تغییر نیست.',
}
const gramHelp: DashboardHelp = {
description: 'حداکثر وزن طلایی که این مشتری می‌تواند خرید کند (به گرم).',
}
//tabs //tabs
const activeKey = ref('اطلاعات جامع') const activeKey = ref('اطلاعات جامع')
@@ -0,0 +1,90 @@
<template>
<BasicActionSheet
v-model:visible="visibleProxy"
title="مشخصات محصول"
modal-width="36rem"
:show-button="false"
@close="visibleProxy = false"
>
<template v-if="product">
<div v-if="isMobile" class="mb-3 text-base font-700">مشخصات محصول</div>
<div class="rounded-lg bg-gray-100 p-3 mb-4 flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div>
<div class="font-bold text-base">{{ product.title || '—' }}</div>
<div class="mt-1 text-sm text-gray-600">کد محصول: {{ product.id }}</div>
</div>
<CustomTag textColor="#92400e" bgColor="#fef3c7" class="font-bold px-4 self-start md:self-auto">
بایگانی‌شده
</CustomTag>
</div>
<div class="grid gap-3 md:grid-cols-2">
<BasicInfoItem title="دسته‌بندی" :value="product.group_type || '—'" />
<BasicInfoItem title="واحد" :value="product.unit || '—'" />
<BasicInfoItem title="وزن" :value="product.amount ?? '—'" />
<BasicInfoItem title="قیمت" :value="usePrice(product.price || 0)" />
<BasicInfoItem title="هزینه نقدی خرید" :value="usePrice(product.cash_fee_buy || 0)" />
<BasicInfoItem title="هزینه نقدی فروش" :value="usePrice(product.cash_fee_sell || 0)" />
<BasicInfoItem title="معامله مقداری" :value="product.trade_by_quantity ? 'بله' : 'خیر'" />
<BasicInfoItem title="تاریخ ثبت" :value="product.created_at ? toJalali(product.created_at) : '—'" />
</div>
<div class="mt-4 grid gap-2 md:grid-cols-3">
<div class="rounded-lg border border-gray-200 bg-white p-3 text-center">
<div class="mb-1 text-xs text-gray-500">نمایش</div>
<CustomTag
:textColor="product.show ? '#25855A' : '#C53030'"
:bgColor="product.show ? '#E3F4E9' : '#FECACA'"
class="font-bold px-4"
>
{{ product.show ? 'فعال' : 'غیرفعال' }}
</CustomTag>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-3 text-center">
<div class="mb-1 text-xs text-gray-500">خرید</div>
<CustomTag
:textColor="product.open_buy ? '#25855A' : '#C53030'"
:bgColor="product.open_buy ? '#E3F4E9' : '#FECACA'"
class="font-bold px-4"
>
{{ product.open_buy ? 'باز' : 'بسته' }}
</CustomTag>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-3 text-center">
<div class="mb-1 text-xs text-gray-500">فروش</div>
<CustomTag
:textColor="product.open_sell ? '#25855A' : '#C53030'"
:bgColor="product.open_sell ? '#E3F4E9' : '#FECACA'"
class="font-bold px-4"
>
{{ product.open_sell ? 'باز' : 'بسته' }}
</CustomTag>
</div>
</div>
</template>
</BasicActionSheet>
</template>
<script setup lang="ts">
import type { Product } from '@/model/product'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import CustomTag from '@/components/CustomTag.vue'
import { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice'
const props = defineProps<{
visible: boolean
product: Product | null
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
}>()
const visibleProxy = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value),
})
</script>
@@ -1,148 +1,101 @@
<template> <template>
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden"> <div class="flex h-full min-h-0 min-w-0 max-w-full flex-col overflow-hidden py-2 pr-3">
<BasicShowContext title="لیست محصولات بایگانی شده"> <DashboardLabelWithHelp
<div class="flex h-full min-h-0 flex-col"> label="لیست محصولات بایگانی‌شده"
:help="productArchiveHelp"
size="sm"
text-class="font-bold text-[var(--admin-text)]"
root-class="mb-3 shrink-0"
/>
<div class="flex min-h-0 flex-1 flex-col overflow-hidden">
<BaseTable v-if="!isMobile" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl"> <BaseTable v-if="!isMobile" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
<!-- <template #extraHeader> <template #action="{ record }">
<BaseButton type="primary" size="small" class="flex" @click="emits('add')"> <div class="gap-2 flex justify-center">
<div>افزودن مشتری عمده</div> <a-tooltip placement="bottom" class="cursor-pointer">
<BaseIcon icon="vuesax-linear:user-add" Size="16px" /> <template #title>
</BaseButton> <span>بیرون بردن از بایگانی</span>
</template>--> </template>
<template #action="{ column, record }"> <a-popconfirm
<div class="gap-2 flex justify-center"> title="آیا این محصول از بایگانی خارج شود؟"
<a-tooltip :key="record.is_archive" placement="bottom" class="cursor-pointer"> ok-text="تایید"
<template #title> cancel-text="رد"
<span>{{ record.is_archive ? 'بیرون بردن از بایگانی' : 'بایگانی کردن' }}</span> placement="topLeft"
</template> @confirm="archiveFunction(record.id)"
<a-popconfirm >
title="آیا مشتری را از بایگانی خارج می کنید؟" <BaseIcon class="cursor-pointer" icon="vuesax-linear:archive-slash" color="#e3bf00" />
ok-text="تایید" </a-popconfirm>
cancel-text="رد" </a-tooltip>
placement="topLeft"
@confirm="archiveFunction(record.id)"
@cancel=""
>
<!-- <template #icon><question-circle-outlined style="color: red" /></template>-->
<Icon class="cursor-pointer" icon="vuesax-linear:archive-slash" color="#e3bf00" />
</a-popconfirm>
</a-tooltip>
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)"> <a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)">
<template #title> <template #title>
<span>مشخصات</span> <span>مشخصات</span>
</template> </template>
<Icon icon="vuesax-linear:stickynote" color="#10913A" /> <BaseIcon icon="vuesax-linear:stickynote" color="#10913A" />
</a-tooltip> </a-tooltip>
<!-- <a-tooltip </div>
placement="bottom" </template>
class="cursor-pointer" </BaseTable>
@click="emits('delete', record.id)" <ArchiveMobileCardList
> v-else
<template #title> ref="mobileListRef"
<span>حذف</span> class="min-h-0 flex-1"
</template> @info="emits('info', $event)"
<Icon icon="vuesax-linear:trash" color="#EB3535" /> @unarchive="archiveFunction($event.id)"
</a-tooltip>--> />
</div> </div>
</template>
</BaseTable>
<ArchiveMobileCardList
v-else
ref="mobileListRef"
class="min-h-0 flex-1"
@info="emits('info', $event)"
@unarchive="archiveFunction($event.id)"
/>
</div>
</BasicShowContext>
</div> </div>
</template> </template>
<script lang="tsx" setup> <script setup lang="ts">
import type { TableInput } from '@core/type/baseTable' import type { TableInput } from '@core/type/baseTable'
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable' import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { changeProductArchive } from '@/api/product'
import { cols } from '../hooks/archiveColumns'
import { useUserStore } from '@/store/userStore'
import { usePrefix } from '@core/composable/usePrefix'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import ArchiveMobileCardList from './ArchiveMobileCardList.vue' import ArchiveMobileCardList from './ArchiveMobileCardList.vue'
import { getProductArchiveTable } from '@/api/product' import { changeProductArchive, getProductArchiveTable } from '@/api/product'
import { getProductArchiveColumns } from '../hooks/archiveColumns'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
const [submit, reload] = useBaseTable() const productArchiveHelp: DashboardHelp = {
/*const select = (val) => { description:
val.sts === 1 ? return ['1'] : ['2'] 'محصولات بایگانی‌شده از لیست اصلی حذف می‌شوند. هنگام بایگانی، نمایش (show)، خرید (open_buy) و فروش (open_sell) غیرفعال می‌شود.\n\n' +
}*/ 'در این صفحه فقط می‌توانید مشخصات را ببینید یا محصول را از بایگانی خارج کنید. تغییر نمایش/خرید/فروش از اینجا ممکن نیست.',
//emits formula:
const emits = defineEmits(['delete', 'edit', 'add', 'info', 'charge']) 'بعد از «خروج از بایگانی» محصول به لیست اصلی برمی‌گردد؛ وضعیت خرید/فروش/نمایش همان‌طور که موقع بایگانی بسته شده بود می‌ماند تا خودتان در لیست محصولات دوباره روشن کنید.',
const { prefixCls } = usePrefix('pophoverstatus')
//permission
function checkPermission(temp: string) {
const store = useUserStore()
return store.canAccess(temp)
} }
//define table const [submit, reload] = useBaseTable()
const table = ref(null) const emits = defineEmits<{ (e: 'info', record: unknown): void }>()
const tblProps = ref<any>({ const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
columns: cols,
api: (params: TableInput) => { const tblProps = ref({
return getProductArchiveTable(params) columns: getProductArchiveColumns(),
} api: (params: TableInput) => getProductArchiveTable(params),
//bordered: true
}) })
const loading = ref<boolean>(false)
async function archiveFunction(id: number) { async function archiveFunction(id: number) {
try { try {
loading.value = true
await changeProductArchive(id) await changeProductArchive(id)
await Promise.allSettled([reload(), mobileListRef.value?.fetch?.() as Promise<void>]) await Promise.allSettled([reload(), mobileListRef.value?.fetch?.() as Promise<void>])
message.success('محصول با موفقیت از بایگانی خارج شد.') message.success('محصول با موفقیت از بایگانی خارج شد.')
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} finally { message.error('خروج از بایگانی انجام نشد.')
loading.value = false
} }
} }
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
const fetch = async () => { const fetch = async () => {
await Promise.allSettled([reload(), mobileListRef.value?.fetch?.() as Promise<void>]) await Promise.allSettled([reload(), mobileListRef.value?.fetch?.() as Promise<void>])
} }
// const spinTable = (val: boolean) => {
// // console.log('sss') defineExpose({ fetch })
// table?.value?.loading.value = val
// }
defineExpose({
fetch
// spinTable
})
</script> </script>
<style lang="less"> <style lang="less">
@prefix: ~'@{prefixCls}-pophoverstatus'; .ant-popover,
.ant-tooltip {
.@{prefix} { z-index: 1001 !important;
.ant-menu-rtl.ant-menu-inline,
.ant-menu-rtl.ant-menu-vertical {
border-left: none;
}
.ant-menu-item-only-child {
border-radius: @border-radius-base;
}
//bor
/* .ant-input-group-addon:first-child {
!* border: #d9d9d9 1px solid;
border-radius: 0 6px 6px 0;*!
}*/
} }
</style> </style>
@@ -1,163 +1,127 @@
import type { BaseTableColumn } from '@core/type/baseTable' import type { BaseTableColumn } from '@core/type/baseTable'
import { toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue' import CustomTag from '@/components/CustomTag.vue'
/*import BudgetSelect from '@/components/BudgetSelect.vue' import { isStoreApp } from '@/utils/config'
import { useStaticApiListsStore } from '@/store/staticApisStore' import { usePrice } from '@core/utils/usePrice'
import type { FilterSelect } from '@/model/filterSelect'
import type { DocItem } from '@/model/docType'*/
//define store function formatMoney(value: number | null | undefined) {
//const store = useStaticApiListsStore() return usePrice(value ?? 0, true, '').trim()
//document types }
/*let docTypes: DocItem[]*/
/*onMounted(async () => {
docTypes = await store.readyDocTypeList()
})
const filteredDocTypes: FilterSelect = docTypes?.map((x: DocItem) => {
return {
label: x.title,
value: x.id
}
})*/
export const cols: BaseTableColumn[] = [ function boolTag(value: boolean | number | undefined, onLabel: string, offLabel: string) {
{ const on = Boolean(value)
title: 'ردیف', return (
dataIndex: 'id', <CustomTag
key: 'id', class="px-3 font-bold py-1"
filterable: false, textColor={on ? '#65a30d' : '#dc2626'}
sortable: false, bgColor={on ? '#ecfccb' : '#fee2e2'}
customRender: (record: any) => { >
return record.index + 1 {on ? onLabel : offLabel}
} </CustomTag>
}, )
{ }
title: 'کد کاربری',
dataIndex: 'id', export function getProductArchiveColumns(): BaseTableColumn[] {
key: 'id', const columns: BaseTableColumn[] = [
type: 'Number' {
// className: 'column-money' title: 'ردیف',
}, dataIndex: 'id',
{ key: 'row_index',
title: 'نام و نام خانوادگی',
dataIndex: 'name',
key: 'name',
type: 'Text'
},
{
title: 'شماره موبایل',
dataIndex: 'mobile',
key: 'mobile',
type: 'Number'
},
/* {
title: 'نام',
dataIndex: 'shop_name',
key: 'shop_name',
type: 'Text'
},*/
{
title: 'وضعیت کاربر',
dataIndex: 'active',
key: 'active',
type: 'Boolean',
customRender: ({ record }) => {
return record.active ? (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
فعال
</CustomTag>
</div>
) : (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA" class={'font-bold px-6'}>
غیر فعال
</CustomTag>
</div>
)
}
},
{
title: 'وضعیت سفارش دهی',
dataIndex: 'trade_active',
key: 'trade_active',
type: 'Boolean',
customRender: ({ record }) => {
return record.trade_active ? (
<div class="flex justify-center">
<CustomTag textColor="#25855A" bgColor="#E3F4E9" class={'font-bold px-6'}>
مجاز
</CustomTag>
</div>
) : (
<div class="flex justify-center">
<CustomTag textColor="#C53030" bgColor="#FECACA" class={'font-bold px-6'}>
غیر مجاز
</CustomTag>
</div>
)
}
},
/*{
title: 'نقش',
dataIndex: 'user_type_record',
key: 'user_type_record',
type: 'Select',
filterable: false, filterable: false,
sortable: false, sortable: false,
filterComponentProps: { customRender: (record: { index: number }) => record.index + 1,
multiple: true
},
filterField: 'user_type_obj.id',
filterComponent: ReadyUserTypeSelect,
/!* customRender: (record: any) => {
return record.record.user_type_record?.value
}*!/
customRender: ({ record }) => {
const items = record.is_admin ? (
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
ادمین
</CustomTag>
) : (
record.user_type_obj.map((item: { title: any }, index: any) => {
if (item) {
return (
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
{item?.title}
</CustomTag>
)
}
})
)
return <div class="flex justify-center items-center gap-2">{items}</div>
}
}, },
{ {
title: 'حوزه', title: 'کد محصول',
dataIndex: 'section', dataIndex: 'id',
key: 'section' key: 'id',
},
{
title: 'شماره موبایل',
dataIndex: 'mobile',
type: 'Number', type: 'Number',
key: 'mobile'
}, },
{ {
title: 'تاریخ ثبت', title: 'نام',
dataIndex: 'created_at', dataIndex: 'title',
key: 'created_at', key: 'title',
type: 'DateTime', type: 'Text',
customRender: (record: any) => { },
return toJalaliNoTime(record.record.created_at) {
} title: 'دسته‌بندی',
},*/ dataIndex: 'group_type',
{ key: 'group_type',
type: 'Text',
},
{
title: 'وزن',
dataIndex: 'amount',
key: 'amount',
type: 'Number',
},
{
title: 'واحد',
dataIndex: 'unit',
key: 'unit',
type: 'Text',
},
{
title: 'قیمت',
dataIndex: 'price',
key: 'price',
type: 'Text',
customRender: ({ record }) => formatMoney(record.price),
},
{
title: 'نمایش',
dataIndex: 'show',
key: 'show',
type: 'Boolean',
filterable: false,
sortable: false,
customRender: ({ record }) => (
<div class="flex justify-center">{boolTag(record.show, 'فعال', 'غیرفعال')}</div>
),
},
{
title: 'خرید',
dataIndex: 'open_buy',
key: 'open_buy',
type: 'Boolean',
filterable: false,
sortable: false,
customRender: ({ record }) => (
<div class="flex justify-center">{boolTag(record.open_buy, 'باز', 'بسته')}</div>
),
},
{
title: 'فروش',
dataIndex: 'open_sell',
key: 'open_sell',
type: 'Boolean',
filterable: false,
sortable: false,
customRender: ({ record }) => (
<div class="flex justify-center">{boolTag(record.open_sell, 'باز', 'بسته')}</div>
),
},
]
if (isStoreApp()) {
columns.splice(6, 0, {
title: 'موجودی',
dataIndex: 'inventory',
key: 'inventory',
type: 'Number',
})
}
columns.push({
title: 'عملیات', title: 'عملیات',
dataIndex: 'action', dataIndex: 'action',
key: 'action', key: 'action',
slot: 'action', slot: 'action',
fixed: 'right', fixed: 'right',
filterable: false filterable: false,
} })
]
return columns
}
/** @deprecated از getProductArchiveColumns استفاده کنید */
export const cols = getProductArchiveColumns()
+12 -297
View File
@@ -1,308 +1,23 @@
<template> <template>
<div> <div class="h-full min-h-0 min-w-0 overflow-hidden">
<!-- <BasicMainCard>--> <tbl ref="table" class="h-full w-full" @info="openInfo" />
<div class="flex w-full h-full"> <ProductArchiveInfoSheet v-model:visible="infoVisible" :product="selectedProduct" />
<tbl ref="table" class="w-full h-full" @info="info" @edit="edit" @charge="walletCharge" />
<!-- <BasicMenu :menu="menu" width="13rem" @onChange="tabSelected" />
<component :is="selectedComponent" class="mr-5" />-->
<!-- <BasicShowContext title="پروژه ها"> <Search /> </BasicShowContext>-->
</div>
<!-- </BasicMainCard>-->
<BasicModal
v-model:visible="addUserVisible"
width="40rem"
:title="`${editId ? 'ویرایش' : 'افزودن'} کاربر`"
@ok="ok"
:loading="loading"
@close="addUserVisible = false"
>
<a-form :model="form" ref="formRef" autocomplete="off">
<div class="grid md:grid-cols-2 gap-4">
<a-form-item label="نام" name="fname" :rules="inputRules()">
<BasicInput v-model:value="form.fname" />
</a-form-item>
<a-form-item label="نام خانوادگی" name="lname" :rules="inputRules()">
<BasicInput v-model:value="form.lname" />
</a-form-item>
<a-form-item label="شماره همراه" name="mobile" :rules="inputLenRules(11)">
<BasicInput v-model:value="form.mobile" type="number" />
</a-form-item>
<a-form-item label="تاریخ تولد" name="birth_date" :rules="inputRules()">
<BaseDatePicker v-model:value="form.birth_date" />
</a-form-item>
<!-- <a-form-item label="ایمیل" name="email" :rules="inputRules()">
<BasicInput v-model:value="form.email" />
</a-form-item>-->
<!-- <a-form-item label="نام فروشگاه" name="shop_name" :rules="inputRules()">
<BasicInput v-model:value="form.shop_name" />
</a-form-item>-->
<a-form-item label="شماره تلفن" name="phone_number" :rules="inputRules()">
<BasicInput v-model:value="form.phone_number" type="number" />
</a-form-item>
<a-form-item label="نام استان" name="province" :rules="inputRules()">
<BasicInput v-model:value="form.province" />
</a-form-item>
<a-form-item label="نام شهر" name="city" :rules="inputRules()">
<BasicInput v-model:value="form.city" />
</a-form-item>
<a-form-item label="شماره ملی" name="n_code" :rules="inputRules()">
<BasicInput v-model:value="form.n_code" />
</a-form-item>
<!-- <a-form-item label="نام چاپی" name="print_name" :rules="inputRules()">
<BasicInput v-model:value="form.print_name" />
</a-form-item>-->
</div>
</a-form>
</BasicModal>
<BasicModal
v-model:visible="userInfoVisible"
width="40vw"
title="اطلاعات کاربر"
custom-footer
@close="userInfoVisible = false"
>
<div class="grid grid-cols-4 gap-2">
<div class="col-span-4 bg-gray-100 w-full rounded-lg flex p-3 justify-between">
<div>
<div class="font-bold">{{ userInfo.name }}</div>
<div class="mt-1">{{ userInfo?.mobile }}</div>
</div>
<div>
<div>{{ toJalaliNoTime(userInfo?.created_at) }}</div>
<div class="mt-2">
<CustomTag
:textColor="userInfo?.active ? '#25855A' : '#C53030'"
:bgColor="userInfo?.active ? '#E3F4E9' : '#FECACA'"
class="font-bold px-6"
>
{{ userInfo?.active ? 'فعال' : 'غیرفعال' }}
</CustomTag>
</div>
</div>
</div>
<BasicInfoItem title="کد کاربری" :value="userInfo?.id" />
<BasicInfoItem title="نوع کاربر" :value="userInfo?.type_str" />
<BasicInfoItem
title="تاریخ تولد"
:value="userInfo?.birth_date ? toJalaliNoTime(userInfo?.birth_date) : '_'"
/>
<BasicInfoItem title="شماره ملی" :value="userInfo?.n_code ?? '_'" />
<BasicInfoItem title="نام استان" :value="userInfo?.province ?? '_'" />
<BasicInfoItem title="نام شهر" :value="userInfo?.city ?? '_'" />
<BasicInfoItem title="سقف اعتبار" :value="userInfo?.gram ? `${userInfo?.gram} گرم` : '_'" />
<BasicInfoItem title="کد معرف" :value="userInfo?.referral_id" />
<BasicInfoItem
title="کد معرف کاربر"
:value="userInfo?.referral ? userInfo?.referral?.id : '_'"
/>
<BasicInfoItem
title="نام معرف کاربر"
:value="userInfo?.referral ? userInfo?.referral?.name : '_'"
/>
<BasicInfoItem title="وضعیت سفارش دهی">
<CustomTag
:textColor="userInfo?.trade_active === 1 ? '#25855A' : '#C53030'"
:bgColor="userInfo?.trade_active === 1 ? '#E3F4E9' : '#FECACA'"
class="font-bold px-6"
>
{{ userInfo?.trade_active === 1 ? 'مجاز' : 'غیر مجاز' }}
</CustomTag>
</BasicInfoItem>
<div class="col-span-4 grid grid-cols-2 gap-3">
<div
v-for="(item, index) in userInfo.wallet"
:key="index"
class="grid grid-cols-2 bg-orange-50 gap-2 rounded-lg p-2"
>
<BasicInfoItem
title="نام قلک"
:value="item?.fund ? item?.fund?.title : metalLabel.goldWallet()"
/>
<BasicInfoItem :title="metalLabel.goldAmount()" :value="`${item?.gold} گرم`" />
</div>
</div>
</div>
</BasicModal>
<BasicModal
v-model:visible="showWalletCharge"
width="30rem"
title="شارژ کیف پول"
@ok="charge"
:loading="loading"
@close="showWalletCharge = false"
>
<a-form :model="formCharge" ref="formChargeRef" autocomplete="off">
<a-form-item label="مقدار شارژ (ریال)" name="value" :rules="inputRules()">
<BasicInput v-model:value="formCharge.value" type="number" />
</a-form-item>
</a-form>
</BasicModal>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// import BasicInfoModal from '@/components/BasicInfoModal'
import { inputLenRules, inputRules } from '@/utils/inputRules'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import { ref } from 'vue' import { ref } from 'vue'
import tbl from './components/table.vue'
import type { FormInstance } from 'ant-design-vue'
import BasicInfoModal from '@/components/BasicInfoModal/index.vue'
import { message } from 'ant-design-vue/es'
import type { Info } from '@/components/BasicInfoModal/model/Info'
import { toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import { confirmDeleteItem } from '@/utils/confirmModal'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import type { Item } from '@/model/Item' import type { Product } from '@/model/product'
import type { Customer } from '@/model/customer' import tbl from './components/table.vue'
import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer' import ProductArchiveInfoSheet from './components/ProductArchiveInfoSheet.vue'
import type { Charge } from '@/model/wallet'
import customers from '@/router/routes/customers'
import { chargeWallet } from '@/api/wallet'
import { metalLabel } from '@/utils/adminMetalLabels'
//loading const table = ref<null | { fetch: () => void }>(null)
const loading = ref<boolean>(false) const infoVisible = ref(false)
const selectedProduct = ref<Product | null>(null)
//define store function openInfo(product: Product) {
const store = useStaticApiListsStore() selectedProduct.value = cloneDeep(product)
infoVisible.value = true
//define tbl
const table = ref(null)
const refreshTable = () => table?.value?.fetch()
let updateKey = ref<number>(0)
//role confirm items
const confirmItems: Array<Item> = [
{
value: 'بله',
key: true
},
{
value: 'خیر',
key: false
}
]
//wallet
const showWalletCharge = ref<boolean>(false)
//form
const form = ref<Customer>({})
const formCharge = ref<Charge>({})
const formRef = ref<FormInstance>()
const formChargeRef = ref<FormInstance>()
const userInfo = ref<Customer>()
//info
let infoParams = reactive<Info>({})
function info(val: Customer) {
userInfoVisible.value = true
userInfo.value = cloneDeep(val)
}
//edit
let editId: number | undefined = undefined
async function edit(record: Customer) {
addUserVisible.value = true
editId = record.id
form.value = cloneDeep(record)
}
//add
async function add() {
loading.value = true
editId = undefined
// await store.getUsersList()
addUserVisible.value = true
updateKey.value++
resetData()
loading.value = false
}
async function walletCharge(val: Customer) {
showWalletCharge.value = true
formCharge.value.customer_id = val.id
}
async function charge() {
formChargeRef.value?.validateFields().then(async () => {
try {
loading.value = true
await chargeMoneyWallet(formCharge.value)
message.success('افزایش اعتبار با موفقیت انجام شد')
showWalletCharge.value = false
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
})
}
//reset data
const resetData = () => {
formRef.value?.resetFields()
form.value = {
fname: '',
lname: '',
active: false,
avatar_id: undefined,
sts: undefined,
id: undefined,
mobile: undefined
}
}
//delete
const deleteItem = (id: number) => confirmDeleteItem('کاربر', id, deleteCustomer, refreshTable)
//define selects
onMounted(async () => {
loading.value = true
// await getResponsible()
// await getQC()
// await getManager()
loading.value = false
})
//add project modal
const addUserVisible = ref<boolean>(false)
const userInfoVisible = ref<boolean>(false)
//submit user
async function ok() {
formRef.value?.validateFields().then(async () => {
// console.log('salam', form.value)
// return
loading.value = true
try {
if (editId) {
await updateCustomer(editId, form.value)
} else {
await createCustomer(form.value)
}
addUserVisible.value = false
refreshTable()
resetData()
message.success('عملیات با موفقیت انجام شد')
// await getResponsible()
// await getQC()
// await getManager()
} catch (e) {
console.error(e)
// message.error('خطا در بارگذاری اطلاعات')
} finally {
loading.value = false
}
})
} }
</script> </script>
<style></style>
@@ -1,112 +1,39 @@
<template> <template>
<Collapse class="grid grid-cols-1 gap-2" :activeKey="activeKey" @update:activeKey="$emit('update:activeKey', $event)"> <a-form
<BaseCollapse title="اطلاعات محصول" active-key="0" :actives="activeKey"> :model="form"
<template #content> ref="innerFormRef"
<a-form autocomplete="off"
:model="form" scroll-to-first-error
ref="innerFormRef" class="product-editor-form"
autocomplete="off" >
scroll-to-first-error <Collapse class="grid grid-cols-1 gap-2" :activeKey="activeKey" @update:activeKey="$emit('update:activeKey', $event)">
class="product-editor-form" <BaseCollapse title="قیمت‌گذاری" active-key="0" :actives="activeKey">
> <template #content>
<div <div
v-if="isMeltedCategory" v-if="isMeltedCategory"
class="md:flex flex-col gap-3 border-1 border-solid rounded-lg border-gray-200 p-3" class="product-editor-panel flex flex-col gap-3"
> >
<div class="items-center"> <div class="flex flex-wrap items-center gap-3">
<span class="ml-4">مرجع قیمت :</span> <DashboardLabelWithHelp label="مرجع قیمت" :help="meltedPriceSourceHelp" size="sm" />
<a-radio-group :value="meltedPriceOption" name="meltedPriceGroup" @update:value="$emit('update:meltedPriceOption', $event)"> <a-radio-group :value="meltedPriceOption" name="meltedPriceGroup" @update:value="$emit('update:meltedPriceOption', $event)">
<a-radio value="app">قیمت اپ (صفحه اصلی)</a-radio> <a-radio value="app">قیمت اپ</a-radio>
<a-radio value="fixed">ثابت</a-radio> <a-radio value="fixed">ثابت</a-radio>
<a-radio value="reference">مرجع متفاوت</a-radio> <a-radio value="reference">مرجع متفاوت</a-radio>
</a-radio-group> </a-radio-group>
</div>
<div v-if="meltedPriceOption === 'fixed'" class="md:flex items-center gap-3">
<span>قیمت ثابت :</span>
<BasicInput class="md:w-[20rem]" addon-before="ریال" type="price" v-model:value="form.price" />
</div>
<div v-if="meltedPriceOption === 'reference'" class="flex flex-col gap-2 md:min-w-[28rem]">
<div class="flex flex-col gap-1">
<div>مرجع قیمت :</div>
<BasicSelect
class="price-reference-select"
allowClear
:hasTooltip="false"
popup-class-name="price-reference-select-dropdown"
:dropdown-match-select-width="false"
:dropdown-style="{ minWidth: '28rem' }"
:items="pricesList"
valueName="name"
keyName="id"
v-model:value="form.reading_method"
/>
</div> </div>
<div class="md:flex justify-between items-center"> <div v-if="meltedPriceOption === 'fixed'" class="grid gap-4 md:grid-cols-3">
<div class="ml-4">تعدیل نسبت به قیمت مرجع (٪) :</div> <a-form-item name="price" class="!mb-0">
<BasicInput <template #label>
class="w-11rem" <DashboardLabelWithHelp label="قیمت ثابت" :help="fixedPriceHelp" size="sm" />
type="number" </template>
v-model:value="form.price_offset_percent" <BasicInput addon-before="ریال" type="price" v-model:value="form.price" />
placeholder="مثلاً 5 یا -3" </a-form-item>
/>
</div> </div>
<div class="text-xs text-gray-500"> <div v-if="meltedPriceOption === 'reference'" class="grid gap-4 md:grid-cols-3">
مثبت = گران‌تر، منفی = ارزان‌تر نسبت به قیمت مرجع. <a-form-item name="reading_method" class="!mb-0 md:col-span-2">
</div> <template #label>
<a-form-item label="قیمت صفر از مرجع" name="use_last_price_if_zero" class="!mb-0"> <DashboardLabelWithHelp label="مرجع قیمت" :help="priceReferenceHelp" size="sm" />
<a-switch v-model:checked="form.use_last_price_if_zero" /> </template>
<span class="mr-2 text-xs text-gray-500">
اگر مرجع قیمت نداد (صفر)، با آخرین قیمت ذخیره‌شده معامله شود
</span>
</a-form-item>
</div>
<div v-if="meltedPriceOption === 'app'" class="flex flex-col gap-2">
<div class="md:flex justify-between items-center">
<div class="ml-4">تعدیل نسبت به قیمت اپ (٪) :</div>
<BasicInput
class="w-11rem"
type="number"
v-model:value="form.price_offset_percent"
placeholder="مثلاً 5 یا -3"
/>
</div>
<div class="text-xs text-gray-500">
قیمت خرید/فروش از قیمت صفحه اصلی اپ محاسبه می‌شود. مثبت = گران‌تر، منفی = ارزان‌تر.
</div>
<a-form-item label="قیمت صفر از مرجع" name="use_last_price_if_zero" class="!mb-0">
<a-switch v-model:checked="form.use_last_price_if_zero" />
<span class="mr-2 text-xs text-gray-500">
اگر مرجع قیمت نداد (صفر)، با آخرین قیمت ذخیره‌شده معامله شود
</span>
</a-form-item>
</div>
</div>
<div
v-if="!isMeltedCategory"
class="md:flex justify-between border-1 border-solid rounded-lg border-gray-200 p-2"
>
<div class="items-center">
<span class="ml-4">دریافت قیمت از :</span>
<a-radio-group :value="methodOption" name="radioGroup" @update:value="$emit('update:methodOption', $event)">
<a-radio class="items-center" value="1">دستی</a-radio>
<a-radio value="2">اتوماتیک</a-radio>
</a-radio-group>
</div>
<div>
<div v-if="methodOption === '1'" class="md:flex items-center justify-between md:w-[25rem]">
<div>قیمت وسط :</div>
<BasicInput class="mt-1 md:mt-unset" addon-before="ریال" type="price" v-model:value="form.price" />
<AButton class="border-green text-green mt-1 md:mt-unset rounded-full aspect-square" size="small" shape="circle" @click="$emit('addPrice')">
<template #icon><Icon icon="vuesax-linear:add" size="20" /></template>
</AButton>
<AButton class="border-red text-red mr-1 md:mr-unset rounded-full" size="small" shape="circle" @click="$emit('minusPrice')">
<template #icon><Icon icon="vuesax-linear:minus" size="20" /></template>
</AButton>
</div>
<div v-if="methodOption === '2'" class="flex flex-col gap-2 md:min-w-[28rem]">
<div class="flex flex-col gap-1">
<div>قیمت :</div>
<BasicSelect <BasicSelect
class="price-reference-select" class="price-reference-select"
allowClear allowClear
@@ -119,192 +46,313 @@
keyName="id" keyName="id"
v-model:value="form.reading_method" v-model:value="form.reading_method"
/> />
</div> </a-form-item>
<div class="md:flex justify-between items-center"> <a-form-item name="price_offset_percent" class="!mb-0">
<div class="ml-4">تعدیل نسبت به قیمت مرجع (٪) :</div> <template #label>
<BasicInput <DashboardLabelWithHelp label="تعدیل مرجع (٪)" :help="referencePriceOffsetHelp" size="sm" />
class="w-11rem" </template>
type="number" <BasicInput type="number" v-model:value="form.price_offset_percent" placeholder="مثلاً 5 یا -3" />
v-model:value="form.price_offset_percent" </a-form-item>
placeholder="مثلاً 5 یا -3" <a-form-item name="use_last_price_if_zero" class="!mb-0">
/> <template #label>
</div> <DashboardLabelWithHelp label="قیمت صفر از مرجع" :help="useLastPriceIfZeroHelp" size="sm" />
<div class="text-xs text-gray-500"> </template>
مثبت = گران‌تر، منفی = ارزان‌تر نسبت به قیمت اتوماتیک. مثلاً ۵ یعنی ۵٪ بالاتر از مرجع. <a-switch v-model:checked="form.use_last_price_if_zero" />
</div> </a-form-item>
<a-form-item label="قیمت صفر از مرجع" name="use_last_price_if_zero" class="!mb-0"> </div>
<div v-if="meltedPriceOption === 'app'" class="grid gap-4 md:grid-cols-3">
<a-form-item name="price_offset_percent" class="!mb-0">
<template #label>
<DashboardLabelWithHelp label="تعدیل قیمت اپ (٪)" :help="appPriceOffsetHelp" size="sm" />
</template>
<BasicInput type="number" v-model:value="form.price_offset_percent" placeholder="مثلاً 5 یا -3" />
</a-form-item>
<a-form-item name="use_last_price_if_zero" class="!mb-0">
<template #label>
<DashboardLabelWithHelp label="قیمت صفر از مرجع" :help="useLastPriceIfZeroHelp" size="sm" />
</template>
<a-switch v-model:checked="form.use_last_price_if_zero" /> <a-switch v-model:checked="form.use_last_price_if_zero" />
<span class="mr-2 text-xs text-gray-500">
اگر مرجع قیمت نداد (صفر)، با آخرین قیمت ذخیره‌شده معامله شود
</span>
</a-form-item> </a-form-item>
</div> </div>
</div> </div>
</div>
<div class="mt-2 rounded-lg border border-solid border-blue-200 bg-blue-50 px-3 py-2"> <div v-if="!isMeltedCategory" class="product-editor-panel flex flex-col gap-4">
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between md:gap-3"> <div class="flex flex-wrap items-center gap-3">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 md:min-w-0 md:flex-1"> <DashboardLabelWithHelp label="دریافت قیمت از" :help="priceSourceHelp" size="sm" />
<span class="shrink-0 text-sm font-600 text-gray-700">قیمت لحظه‌ای:</span> <a-radio-group :value="methodOption" name="radioGroup" @update:value="$emit('update:methodOption', $event)">
<span v-if="livePricePreview.status === 'ready'" class="text-base font-700 text-blue-700"> <a-radio value="1">دستی</a-radio>
{{ usePrice(livePricePreview.amount) }} <a-radio value="2">اتوماتیک</a-radio>
</span> </a-radio-group>
<span v-else-if="livePricePreview.message" class="text-sm text-gray-500"> </div>
{{ livePricePreview.message }} <div v-if="methodOption === '1'" class="grid gap-4 md:grid-cols-3">
</span> <a-form-item name="price" class="!mb-0 md:col-span-2">
<span <template #label>
v-if="livePricePreview.status === 'ready'" <DashboardLabelWithHelp label="قیمت وسط" :help="manualPriceHelp" size="sm" />
class="truncate text-xs text-gray-500" </template>
> <div class="flex items-center gap-2">
مبنا: {{ livePricePreview.source }} <BasicInput class="flex-1" addon-before="ریال" type="price" v-model:value="form.price" />
<span v-if="Number(form.price_offset_percent) !== 0"> <AButton class="border-green text-green shrink-0 rounded-full aspect-square" size="small" shape="circle" @click="$emit('addPrice')">
— تعدیل {{ form.price_offset_percent }}٪ <template #icon><Icon icon="vuesax-linear:add" size="20" /></template>
</span> </AButton>
</span> <AButton class="border-red text-red shrink-0 rounded-full" size="small" shape="circle" @click="$emit('minusPrice')">
<template #icon><Icon icon="vuesax-linear:minus" size="20" /></template>
</AButton>
</div>
</a-form-item>
</div>
<div v-if="methodOption === '2'" class="grid gap-4 md:grid-cols-3">
<a-form-item name="reading_method" class="!mb-0 md:col-span-2">
<template #label>
<DashboardLabelWithHelp label="مرجع قیمت" :help="priceReferenceHelp" size="sm" />
</template>
<BasicSelect
class="price-reference-select"
allowClear
:hasTooltip="false"
popup-class-name="price-reference-select-dropdown"
:dropdown-match-select-width="false"
:dropdown-style="{ minWidth: '28rem' }"
:items="pricesList"
valueName="name"
keyName="id"
v-model:value="form.reading_method"
/>
</a-form-item>
<a-form-item name="price_offset_percent" class="!mb-0">
<template #label>
<DashboardLabelWithHelp label="تعدیل مرجع (٪)" :help="referencePriceOffsetHelp" size="sm" />
</template>
<BasicInput type="number" v-model:value="form.price_offset_percent" placeholder="مثلاً 5 یا -3" />
</a-form-item>
<a-form-item name="use_last_price_if_zero" class="!mb-0">
<template #label>
<DashboardLabelWithHelp label="قیمت صفر از مرجع" :help="useLastPriceIfZeroHelp" size="sm" />
</template>
<a-switch v-model:checked="form.use_last_price_if_zero" />
</a-form-item>
</div> </div>
<AButton size="small" class="shrink-0 self-end md:self-auto" @click="$emit('refreshPrices')">
بروزرسانی
</AButton>
</div> </div>
</div>
<div <div class="rounded-lg border border-solid border-blue-200 bg-blue-50 px-3 py-2">
v-if="!isMeltedCategory" <div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between md:gap-3">
class="md:flex justify-between p-2 border-1 border-solid rounded-lg border-gray-200 mt-2" <div class="flex flex-wrap items-center gap-x-3 gap-y-1 md:min-w-0 md:flex-1">
> <DashboardLabelWithHelp label="قیمت لحظه‌ای" :help="livePriceHelp" size="sm" />
<div> <span v-if="livePricePreview.status === 'ready'" class="text-base font-700 text-blue-700">
<span class="ml-4">وزن :</span> {{ usePrice(livePricePreview.amount) }}
<a-radio-group :value="weightOption" name="radioGroup" @update:value="$emit('update:weightOption', $event)"> </span>
<a-radio value="1">دارد</a-radio> <span v-else-if="livePricePreview.message" class="text-sm text-gray-500">
<a-radio value="2">ندارد</a-radio> {{ livePricePreview.message }}
</a-radio-group> </span>
<span
v-if="livePricePreview.status === 'ready'"
class="truncate text-xs text-gray-500"
>
مبنا: {{ livePricePreview.source }}
<span v-if="Number(form.price_offset_percent) !== 0">
— تعدیل {{ form.price_offset_percent }}٪
</span>
</span>
</div>
<AButton size="small" class="shrink-0 self-end md:self-auto" @click="$emit('refreshPrices')">
بروزرسانی
</AButton>
</div>
</div> </div>
<div v-if="weightOption === '1'" class="flex items-center md:col-span-2"> </template>
<a-form-item </BaseCollapse>
class="!mb-0 w-full"
:label="weightFieldLabel" <BaseCollapse title="مشخصات محصول" active-key="1" :actives="activeKey">
name="amount" <template #content>
:rules="inputRulesNumber('وزن')" <div class="grid gap-4 md:grid-cols-3">
> <a-form-item name="title" :rules="inputRules()">
<BasicInput v-model:value="form.amount" type="number" /> <template #label>
<DashboardLabelWithHelp label="عنوان" :help="titleHelp" size="sm" />
</template>
<BasicInput v-model:value="form.title" />
</a-form-item> </a-form-item>
<div class="mt-1 mr-4 shrink-0" v-if="form.amount && form.amount < 1 && 0 < form.amount && tradeWeightUnit === 'گرم'">معادل {{ form.amount * 1000 }} سوت</div> <a-form-item name="group_type" :rules="inputRules()">
</div> <template #label>
</div> <DashboardLabelWithHelp label="دسته‌بندی" :help="categoryHelp" size="sm" />
</template>
<div class="grid md:grid-cols-2 gap-4 mt-2">
<a-form-item label="عنوان" name="title" :rules="inputRules()"><BasicInput v-model:value="form.title" /></a-form-item>
<a-form-item label="دسته بندی" name="group_type" :rules="inputRules()">
<BasicSelect v-model:value="form.group_type" :items="categoryItems" valueName="value" keyName="key" /> <BasicSelect v-model:value="form.group_type" :items="categoryItems" valueName="value" keyName="key" />
</a-form-item> </a-form-item>
<a-form-item label="جنس فلز" name="metal_id" :rules="inputRules()"> <a-form-item name="metal_id" :rules="inputRules()">
<template #label>
<DashboardLabelWithHelp label="جنس فلز" :help="metalHelp" size="sm" />
</template>
<BasicSelect v-model:value="form.metal_id" :items="metalItems" valueName="value" keyName="key" /> <BasicSelect v-model:value="form.metal_id" :items="metalItems" valueName="value" keyName="key" />
</a-form-item> </a-form-item>
<a-form-item v-if="!isMeltedCategory" label="نوع قیمت" name="direct_price" :rules="inputRules()">
<BasicSelect v-model:value="form.direct_price" :items="priceItems" valueName="value" keyName="key" /> <template v-if="!isMeltedCategory">
</a-form-item> <div class="md:col-span-3 product-editor-panel">
<div class="flex flex-wrap items-center gap-3 mb-3">
<DashboardLabelWithHelp label="وزن محصول" :help="weightHelp" size="sm" />
<a-radio-group :value="weightOption" name="weightGroup" @update:value="$emit('update:weightOption', $event)">
<a-radio value="1">دارد</a-radio>
<a-radio value="2">ندارد</a-radio>
</a-radio-group>
</div>
<div v-if="weightOption === '1'" class="grid gap-4 md:grid-cols-3">
<a-form-item name="amount" :rules="inputRulesNumber('وزن')" class="!mb-0">
<template #label>
<DashboardLabelWithHelp :label="weightFieldLabel" :help="weightAmountHelp" size="sm" />
</template>
<BasicInput v-model:value="form.amount" type="number" />
</a-form-item>
<div
v-if="form.amount && form.amount < 1 && 0 < form.amount && tradeWeightUnit === 'گرم'"
class="flex items-end pb-1 text-sm text-gray-500"
>
معادل {{ form.amount * 1000 }} سوت
</div>
</div>
</div>
</template>
<a-form-item <a-form-item
v-if="isMeltedCategory || weightOption === '1'" v-if="isMeltedCategory || weightOption === '1'"
label="واحد"
name="unit" name="unit"
:rules="showUnitField ? inputRules() : []" :rules="showUnitField ? inputRules() : []"
> >
<template #label>
<DashboardLabelWithHelp label="واحد" :help="unitHelp" size="sm" />
</template>
<BasicSelect v-model:value="form.unit" :items="unitItems" valueName="value" keyName="value" /> <BasicSelect v-model:value="form.unit" :items="unitItems" valueName="value" keyName="value" />
</a-form-item> </a-form-item>
<a-form-item
v-if="showDirectPriceField"
name="direct_price"
:rules="inputRules()"
>
<template #label>
<DashboardLabelWithHelp label="نوع قیمت" :help="directPriceHelp" size="sm" />
</template>
<BasicSelect v-model:value="form.direct_price" :items="priceItems" valueName="value" keyName="key" />
</a-form-item>
<a-form-item <a-form-item
v-if="isStore && !isMeltedCategory" v-if="isStore && !isMeltedCategory"
label="تعداد موجودی"
name="inventory" name="inventory"
:rules="inputRules()" :rules="inputRules()"
> >
<template #label>
<DashboardLabelWithHelp label="تعداد موجودی" :help="inventoryHelp" size="sm" />
</template>
<BasicInput v-model:value="form.inventory" type="number" /> <BasicInput v-model:value="form.inventory" type="number" />
</a-form-item> </a-form-item>
<a-form-item label="هزینه نقدی موقع خرید" name="cash_fee_buy" :rules="requiredPriceRules('هزینه نقدی موقع خرید')"> </div>
</template>
</BaseCollapse>
<BaseCollapse title="کارمزد و محدودیت‌ها" active-key="2" :actives="activeKey">
<template #content>
<div class="grid gap-4 md:grid-cols-3">
<a-form-item name="cash_fee_buy" :rules="requiredPriceRules('هزینه نقدی موقع خرید')">
<template #label>
<DashboardLabelWithHelp label="هزینه نقدی خرید" :help="cashFeeBuyHelp" size="sm" />
</template>
<BasicInput v-model:value="form.cash_fee_buy" addon-before="ریال" type="price" /> <BasicInput v-model:value="form.cash_fee_buy" addon-before="ریال" type="price" />
</a-form-item> </a-form-item>
<a-form-item label="هزینه نقدی موقع فروش" name="cash_fee_sell" :rules="requiredPriceRules('هزینه نقدی موقع فروش')"> <a-form-item name="cash_fee_sell" :rules="requiredPriceRules('هزینه نقدی موقع فروش')">
<template #label>
<DashboardLabelWithHelp label="هزینه نقدی فروش" :help="cashFeeSellHelp" size="sm" />
</template>
<BasicInput v-model:value="form.cash_fee_sell" addon-before="ریال" type="price" /> <BasicInput v-model:value="form.cash_fee_sell" addon-before="ریال" type="price" />
</a-form-item> </a-form-item>
<a-form-item label="ساعت کاری اختصاصی این محصول" name="override_trading_hours"> <a-form-item name="show_in_assets">
<a-switch v-model:checked="form.override_trading_hours" /> <template #label>
<div class="text-xs text-gray-500 mt-1 leading-6"> <DashboardLabelWithHelp label="نمایش در دارایی‌ها" :help="showInAssetsHelp" size="sm" />
روشن: این محصول ساعت شروع و پایان مخصوص خودش را دارد و ساعت کاری سراسری فروشگاه را نادیده می‌گیرد (override). </template>
خاموش: همان ساعت کاری سراسری فروشگاه برای این محصول اعمال می‌شود و زمان‌های زیر نادیده گرفته می‌شوند. <a-switch v-model:checked="form.show_in_assets" />
</div>
</a-form-item> </a-form-item>
<a-form-item label="زمان شروع فعال سازی" name="auto_open_at">
<a-form-item name="override_trading_hours">
<template #label>
<DashboardLabelWithHelp label="ساعت کاری اختصاصی" :help="overrideTradingHoursHelp" size="sm" />
</template>
<a-switch v-model:checked="form.override_trading_hours" />
</a-form-item>
<a-form-item name="auto_open_at">
<template #label>
<DashboardLabelWithHelp label="زمان شروع" :help="autoOpenHelp" size="sm" />
</template>
<TimePicker <TimePicker
v-model:value="form.auto_open_at" v-model:value="form.auto_open_at"
:locale="customLocale" :locale="customLocale"
placeholder="انتخاب زمان" placeholder="انتخاب زمان"
:disabled="!form.override_trading_hours" :disabled="!form.override_trading_hours"
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')" :defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
class="w-full"
/> />
</a-form-item> </a-form-item>
<a-form-item label="زمان پایان فعال سازی" name="auto_close_at"> <a-form-item name="auto_close_at">
<template #label>
<DashboardLabelWithHelp label="زمان پایان" :help="autoCloseHelp" size="sm" />
</template>
<TimePicker <TimePicker
v-model:value="form.auto_close_at" v-model:value="form.auto_close_at"
:locale="customLocale" :locale="customLocale"
placeholder="انتخاب زمان" placeholder="انتخاب زمان"
:disabled="!form.override_trading_hours" :disabled="!form.override_trading_hours"
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')" :defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
class="w-full"
/> />
</a-form-item> </a-form-item>
<a-form-item label="محدودیت مبلغ سفارش" name="order_limit_enabled">
<a-form-item name="order_limit_enabled">
<template #label>
<DashboardLabelWithHelp label="محدودیت مبلغ سفارش" :help="orderLimitHelp" size="sm" />
</template>
<a-switch v-model:checked="form.order_limit_enabled" @change="onOrderLimitChange" /> <a-switch v-model:checked="form.order_limit_enabled" @change="onOrderLimitChange" />
</a-form-item> </a-form-item>
<a-form-item label="نمایش در صفحه دارایی‌ها" name="show_in_assets">
<a-switch v-model:checked="form.show_in_assets" />
</a-form-item>
<a-form-item <a-form-item
v-if="!isMeltedCategory && !editId" v-if="!isMeltedCategory && !editId"
label="معامله مقداری"
name="trade_by_quantity" name="trade_by_quantity"
> >
<template #label>
<DashboardLabelWithHelp label="معامله مقداری" :help="tradeByQuantityHelp" size="sm" />
</template>
<a-switch v-model:checked="form.trade_by_quantity" /> <a-switch v-model:checked="form.trade_by_quantity" />
<div class="text-xs text-gray-500 mt-1 leading-6">
روشن: فرم خرید/فروش با «ارزش کل» و «مقدار» (مستقل از دسته‌بندی). فقط هنگام ساخت محصول قابل تعیین است.
</div>
</a-form-item> </a-form-item>
<a-form-item <a-form-item v-else-if="!isMeltedCategory && editId">
v-else-if="!isMeltedCategory && editId" <template #label>
label="نوع معامله" <DashboardLabelWithHelp label="نوع معامله" :help="tradeByQuantityHelp" size="sm" />
> </template>
<div class="text-sm leading-6"> <div class="text-sm leading-6 pt-1">
{{ form.trade_by_quantity ? 'معامله مقداری (ارزش کل + مقدار)' : 'معامله عادی' }} {{ form.trade_by_quantity ? 'معامله مقداری' : 'معامله عادی' }}
</div>
<div class="text-xs text-gray-500 mt-1 leading-6">
نوع معامله پس از ساخت محصول قابل تغییر نیست.
</div> </div>
</a-form-item> </a-form-item>
<template v-if="form.order_limit_enabled"> <template v-if="form.order_limit_enabled">
<div class="col-span-2 text-xs text-gray-500 -mt-2 mb-1"> <a-form-item name="min_order_cash">
حداقل یکی از «حداقل» یا «حداکثر» الزامی است؛ می‌توانید فقط یکی را پر کنید. <template #label>
</div> <DashboardLabelWithHelp label="حداقل مبلغ سفارش" :help="minOrderHelp" size="sm" />
<a-form-item label="حداقل مبلغ سفارش (ریال)" name="min_order_cash"> </template>
<BasicInput v-model:value="form.min_order_cash" addon-before="ریال" type="price" /> <BasicInput v-model:value="form.min_order_cash" addon-before="ریال" type="price" />
</a-form-item> </a-form-item>
<a-form-item label="حداکثر مبلغ سفارش (ریال)" name="max_order_cash"> <a-form-item name="max_order_cash">
<template #label>
<DashboardLabelWithHelp label="حداکثر مبلغ سفارش" :help="maxOrderHelp" size="sm" />
</template>
<BasicInput v-model:value="form.max_order_cash" addon-before="ریال" type="price" /> <BasicInput v-model:value="form.max_order_cash" addon-before="ریال" type="price" />
</a-form-item> </a-form-item>
</template> </template>
</div> </div>
</a-form> </template>
</template> </BaseCollapse>
</BaseCollapse>
<BaseCollapse title="تصاویر محصول" active-key="1" :actives="activeKey"> <BaseCollapse title="تصاویر محصول" active-key="3" :actives="activeKey">
<template #content> <template #content>
<IndexImageUploader <IndexImageUploader
:key="editId ?? 'new-product'" :key="editId ?? 'new-product'"
:file-list="productIndexImage" :file-list="productIndexImage"
@update:file-list="$emit('update:productIndexImage', $event)" @update:file-list="$emit('update:productIndexImage', $event)"
@ok="(id, res) => $emit('uploadedIndexFile', id, res)" @ok="(id, res) => $emit('uploadedIndexFile', id, res)"
@delete="$emit('deleteIndexFile')" @delete="$emit('deleteIndexFile')"
/> />
</template> </template>
</BaseCollapse> </BaseCollapse>
</Collapse> </Collapse>
</a-form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -322,9 +370,124 @@ import { usePrice } from '@core/utils/usePrice'
import { usesMeltedPriceEditor } from '@/utils/productCategories' import { usesMeltedPriceEditor } from '@/utils/productCategories'
import { computeProductEditorPricePreview } from '@/utils/productEditorPrice' import { computeProductEditorPricePreview } from '@/utils/productEditorPrice'
import { resolveTradeWeightUnit, weightFieldLabelForUnit } from '@/utils/weightUnit' import { resolveTradeWeightUnit, weightFieldLabelForUnit } from '@/utils/weightUnit'
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
const priceSourceHelp: DashboardHelp = {
description: 'دستی: خودتان قیمت را وارد می‌کنید. اتوماتیک: قیمت از مرجع بیرونی (zaarbox) خوانده می‌شود.',
}
const meltedPriceSourceHelp: DashboardHelp = {
description: 'قیمت اپ: از صفحه اصلی اپ. ثابت: عدد دستی. مرجع متفاوت: انتخاب منبع دیگر.',
}
const manualPriceHelp: DashboardHelp = {
description: 'قیمت پایه محصول برای حالت دستی. دکمه +/- برای تنظیم سریع.',
}
const fixedPriceHelp: DashboardHelp = {
description: 'مبلغ ثابت خرید/فروش این محصول آبشده‌ای.',
}
const priceReferenceHelp: DashboardHelp = {
description: 'منبع قیمت زنده از فید همکار — قیمت لحظه‌ای از این مرجع محاسبه می‌شود.',
}
const livePriceHelp: DashboardHelp = {
description: 'پیش‌نمایش قیمتی که مشتری در اپ می‌بیند، بر اساس تنظیمات بالا.',
}
const titleHelp: DashboardHelp = {
description: 'نام نمایشی محصول در اپ کاربر و پنل.',
}
const categoryHelp: DashboardHelp = {
description: 'دسته‌بندی محصول در تب‌های اپ (سکه، شمش، آبشده و …).',
}
const metalHelp: DashboardHelp = {
description: 'نوع فلز محصول برای دارایی و کیف پول.',
}
const weightHelp: DashboardHelp = {
description: 'اگر محصول وزن مشخص دارد (مثلاً شمش ۲ گرمی)، «دارد» را بزنید.',
}
const weightAmountHelp: DashboardHelp = {
description: 'وزن هر واحد محصول — در حالت «متغیر» در قیمت ضرب می‌شود.',
}
const unitHelp: DashboardHelp = {
description: 'واحد نمایش و معامله: گرم، کیلوگرم یا عدد.',
}
const inventoryHelp: DashboardHelp = {
description: 'تعداد موجودی فروشگاه (فقط حالت فروشگاه).',
}
const cashFeeBuyHelp: DashboardHelp = {
description: 'کارمزد ریالی اضافه‌شده به قیمت خرید برای مشتری.',
}
const cashFeeSellHelp: DashboardHelp = {
description: 'کارمزد ریالی کسرشده از قیمت فروش برای مشتری.',
}
const showInAssetsHelp: DashboardHelp = {
description: 'روشن: این محصول در صفحه دارایی‌های مشتری نمایش داده می‌شود.',
}
const autoOpenHelp: DashboardHelp = {
description: 'ساعت شروع معامله این محصول — فقط وقتی ساعت اختصاصی روشن باشد.',
}
const autoCloseHelp: DashboardHelp = {
description: 'ساعت پایان معامله این محصول — فقط وقتی ساعت اختصاصی روشن باشد.',
}
const minOrderHelp: DashboardHelp = {
description: 'کمترین مبلغ ریالی مجاز برای هر سفارش این محصول.',
}
const maxOrderHelp: DashboardHelp = {
description: 'بیشترین مبلغ ریالی مجاز برای هر سفارش این محصول.',
}
const directPriceHelp: DashboardHelp = {
description: 'فقط وقتی «دریافت قیمت از: دستی» باشد معنا دارد.',
formula: 'متغیر = قیمت هر گرم × وزن | ثابت = قیمت کل هر عدد',
}
const referencePriceOffsetHelp: DashboardHelp = {
description: 'تعدیل درصدی نسبت به قیمت مرجع.',
formula: 'مثبت = گران‌تر، منفی = ارزان‌تر (مثلاً ۵ یعنی ۵٪ بالاتر)',
}
const appPriceOffsetHelp: DashboardHelp = {
description: 'قیمت خرید/فروش از قیمت صفحه اصلی اپ محاسبه می‌شود.',
formula: 'مثبت = گران‌تر، منفی = ارزان‌تر نسبت به قیمت اپ',
}
const useLastPriceIfZeroHelp: DashboardHelp = {
description: 'اگر مرجع قیمت نداد (صفر)، با آخرین قیمت ذخیره‌شده معامله شود.',
}
const overrideTradingHoursHelp: DashboardHelp = {
description:
'روشن: ساعت شروع و پایان مخصوص این محصول و نادیده گرفتن ساعت سراسری. خاموش: همان ساعت کاری سراسری فروشگاه.',
}
const orderLimitHelp: DashboardHelp = {
description: 'حداقل یکی از «حداقل» یا «حداکثر» الزامی است؛ می‌توانید فقط یکی را پر کنید.',
}
const tradeByQuantityHelp: DashboardHelp = {
description:
'فرم خرید/فروش با «ارزش کل» و «مقدار». فقط هنگام ساخت محصول قابل تعیین است و بعداً تغییر نمی‌کند.',
}
const innerFormRef = ref<FormInstance>() const innerFormRef = ref<FormInstance>()
const props = defineProps<{ const props = defineProps<{
@@ -351,6 +514,8 @@ const isStore = ref(Boolean(props.isStore))
const tradeWeightUnit = ref('گرم') const tradeWeightUnit = ref('گرم')
const isMeltedCategory = computed(() => usesMeltedPriceEditor(props.form)) const isMeltedCategory = computed(() => usesMeltedPriceEditor(props.form))
const showDirectPriceField = computed(() => !isMeltedCategory.value && props.methodOption === '1')
const showUnitField = computed(() => isMeltedCategory.value || props.weightOption === '1') const showUnitField = computed(() => isMeltedCategory.value || props.weightOption === '1')
const livePricePreview = computed(() => const livePricePreview = computed(() =>
@@ -441,6 +606,18 @@ defineExpose({
</script> </script>
<style scoped> <style scoped>
.product-editor-panel {
border: 1px solid var(--ant-color-border-secondary, rgba(0, 0, 0, 0.06));
border-radius: 0.5rem;
padding: 0.75rem 1rem;
margin-bottom: 0.75rem;
}
.product-editor-form :deep(.ant-form-item-label > label) {
height: auto;
min-height: 32px;
}
.price-reference-select { .price-reference-select {
width: 100%; width: 100%;
min-width: 22rem; min-width: 22rem;
@@ -25,6 +25,7 @@
@toggleSell="$emit('toggleSell', $event)" @toggleSell="$emit('toggleSell', $event)"
@toggleActive="$emit('toggleActive', $event)" @toggleActive="$emit('toggleActive', $event)"
@edit="$emit('edit', $event)" @edit="$emit('edit', $event)"
@archive="$emit('archive', $event)"
/> />
</template> </template>
@@ -42,6 +43,7 @@ defineEmits<{
(e: 'toggleBuy', item: Product): void (e: 'toggleBuy', item: Product): void
(e: 'toggleSell', item: Product): void (e: 'toggleSell', item: Product): void
(e: 'toggleActive', item: Product): void (e: 'toggleActive', item: Product): void
(e: 'archive', item: Product): void
}>() }>()
const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<Product>() const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<Product>()
@@ -66,6 +66,16 @@
<MobileDetailRow label="وزن" :value="row.amount ?? '-'" /> <MobileDetailRow label="وزن" :value="row.amount ?? '-'" />
<MobileDetailRow label="معامله مقداری" :value="row.trade_by_quantity ? 'بله' : 'خیر'" /> <MobileDetailRow label="معامله مقداری" :value="row.trade_by_quantity ? 'بله' : 'خیر'" />
</MobileInfoGrid> </MobileInfoGrid>
<BaseButton
class="w-full"
size="middle"
type="default"
icon="vuesax-linear:archive-tick"
@click="$emit('archive', row)"
>
بایگانی
</BaseButton>
</template> </template>
</MobileDetailSheet> </MobileDetailSheet>
</template> </template>
@@ -93,6 +103,7 @@ const emit = defineEmits<{
(e: 'toggleSell', item: Product): void (e: 'toggleSell', item: Product): void
(e: 'toggleActive', item: Product): void (e: 'toggleActive', item: Product): void
(e: 'edit', item: Product): void (e: 'edit', item: Product): void
(e: 'archive', item: Product): void
}>() }>()
const visibleProxy = computed({ const visibleProxy = computed({
@@ -1,37 +1,38 @@
<template> <template>
<div class="flex h-full min-h-0 min-w-0 max-w-full flex-col overflow-hidden"> <div class="flex h-full min-h-0 min-w-0 max-w-full flex-col overflow-hidden">
<div class="shrink-0"> <div class="shrink-0 py-2 pr-3">
<Collapse <DashboardLabelWithHelp
class="product-activation-collapse" label="فعال‌سازی محصولات"
:activeKey="activationActiveKey" :help="productActivationHelp"
@update:activeKey="activationActiveKey = $event as (string | number)[]" size="sm"
> text-class="font-bold text-[var(--admin-text)]"
<BaseCollapse title="فعال سازی محصولات" active-key="activation" :actives="activationActiveKey"> root-class="mb-3"
<template #content> />
<div class="flex flex-col gap-2 md:flex-row md:flex-wrap md:items-center"> <div class="flex flex-col gap-2 md:flex-row md:flex-wrap md:items-center">
<div class="content-center md:mr-4">زمان شروع</div> <div class="flex items-center gap-1.5 md:mr-4">
<div> <DashboardLabelWithHelp label="زمان شروع" :help="activationStartHelp" size="sm" />
<TimePicker </div>
v-model:value="startTime" <div>
:locale="customLocale" <TimePicker
placeholder="انتخاب زمان" v-model:value="startTime"
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')" :locale="customLocale"
/> placeholder="انتخاب زمان"
</div> :defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
<div class="content-center md:mr-4">زمان پایان</div> />
<div> </div>
<TimePicker <div class="flex items-center gap-1.5 md:mr-4">
v-model:value="endTime" <DashboardLabelWithHelp label="زمان پایان" :help="activationEndHelp" size="sm" />
:locale="customLocale" </div>
placeholder="انتخاب زمان" <div>
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')" <TimePicker
/> v-model:value="endTime"
</div> :locale="customLocale"
<BaseButton class="md:mr-2" @click="setTimeActive(startTime, endTime)">ثبت</BaseButton> placeholder="انتخاب زمان"
</div> :defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
</template> />
</BaseCollapse> </div>
</Collapse> <BaseButton class="md:mr-2" @click="setTimeActive(startTime, endTime)">ثبت</BaseButton>
</div>
</div> </div>
<BasicShowContext title="لیست محصولات" class="min-h-0 flex-1"> <BasicShowContext title="لیست محصولات" class="min-h-0 flex-1">
@@ -93,6 +94,18 @@
<template #title><span>ویرایش</span></template> <template #title><span>ویرایش</span></template>
<BaseIcon icon="vuesax-linear:edit" color="#4572E9" /> <BaseIcon icon="vuesax-linear:edit" color="#4572E9" />
</a-tooltip> </a-tooltip>
<a-tooltip placement="bottom" class="cursor-pointer">
<template #title><span>بایگانی کردن</span></template>
<a-popconfirm
title="آیا این محصول بایگانی شود؟ نمایش، خرید و فروش آن غیرفعال می‌شود."
ok-text="تایید"
cancel-text="رد"
placement="topLeft"
@confirm="archiveProductById(record.id)"
>
<BaseIcon icon="vuesax-linear:archive-tick" color="#e3bf00" class="cursor-pointer" />
</a-popconfirm>
</a-tooltip>
</div> </div>
</template> </template>
</BaseTable> </BaseTable>
@@ -105,6 +118,7 @@
@toggleBuy="activeBuy" @toggleBuy="activeBuy"
@toggleSell="activeSell" @toggleSell="activeSell"
@toggleActive="activeProduct" @toggleActive="activeProduct"
@archive="handleArchive"
/> />
</div> </div>
</BasicShowContext> </BasicShowContext>
@@ -112,13 +126,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Collapse, message, Modal, TimePicker } from 'ant-design-vue' import { message, Modal, TimePicker } from 'ant-design-vue'
import BaseCollapse from '@/components/BaseCollapse.vue'
import locale from 'ant-design-vue/es/date-picker/locale/fa_IR' import locale from 'ant-design-vue/es/date-picker/locale/fa_IR'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat' import customParseFormat from 'dayjs/plugin/customParseFormat'
import { ref } from 'vue' import { ref } from 'vue'
import { import {
archiveProduct,
changeActiveProduct, changeActiveProduct,
changeBuyActiveProduct, changeBuyActiveProduct,
changeSellActiveProduct, changeSellActiveProduct,
@@ -127,6 +141,8 @@ import {
} from '@/api/product' } from '@/api/product'
import { getProductColumns } from '@/pages/menuPages/products/base/hooks/productColumns' import { getProductColumns } from '@/pages/menuPages/products/base/hooks/productColumns'
import ProductMobileCardList from '@/pages/menuPages/products/base/components/ProductMobileCardList.vue' import ProductMobileCardList from '@/pages/menuPages/products/base/components/ProductMobileCardList.vue'
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable' import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
import type { TableInput } from '@core/type/baseTable' import type { TableInput } from '@core/type/baseTable'
import type { Product } from '@/model/product' import type { Product } from '@/model/product'
@@ -134,11 +150,36 @@ import { isMobile } from '@core/utils/is'
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
const productActivationHelp: DashboardHelp = {
description:
'ساعت شروع و پایان را یک‌جا روی همهٔ محصولات ثبت می‌کند (فیلدهای auto_open_at و auto_close_at در دیتابیس).\n\n' +
'بعد از «ثبت» چه می‌شود؟\n' +
'• خارج از این بازه، نمایش محصول در لیست اپ (فیلد show) به‌روز می‌شود — مشتری آن محصول را در فهرست نمی‌بیند یا غیرفعال می‌ماند.\n' +
'• مظنه و قیمت صفحهٔ اصلی برای مشتری همچنان نمایش داده می‌شود.\n' +
'• روشن/خاموش بودن خرید و فروش هر محصول جدا است و فقط با آیکون‌های جدول پایین تغییر می‌کند؛ این ساعت به‌تنهایی open_buy و open_sell را عوض نمی‌کند.\n' +
'• اگر در ویرایش محصول «ساعت کاری اختصاصی» روشن باشد، بازهٔ همان محصول بر این ساعت گروهی اولویت دارد.',
formula:
'تفاوت با «بستن خودکار خارج از ساعت کاری» (تنظیمات → ساعت کاری): آن سوییچ کل فروشگاه را می‌بندد، ثبت سفارش را مسدود می‌کند و قیمت صفحهٔ اصلی را قطع می‌کند («فروشگاه بسته است»). این بخش فقط پنجرهٔ نمایش محصولات است و مظنه باز می‌ماند.\n\n' +
'تفاوت با «بستن همه محصولات» (داشبورد): آن عمل فوری است و همه را یک‌جا غیرفعال می‌کند؛ اینجا زمان‌بندی روزانه است.\n\n' +
'تفاوت با «بایگانی»: بایگانی محصول را از لیست حذف می‌کند و نمایش/خرید/فروش را می‌بندد؛ این بخش فقط ساعت نمایش است و محصول از لیست اصلی حذف نمی‌شود.',
}
const activationStartHelp: DashboardHelp = {
description:
'ساعت شروع بازهٔ نمایش/فعال بودن محصولات برای همه — هم‌زمان روی تمام ردیف‌های جدول محصول اعمال می‌شود.',
formula: 'مثال: ۰۹:۰۰:۰۰ یعنی از ساعت ۹ صبح محصولات (طبق منطق show) در اپ قابل نمایش‌اند.',
}
const activationEndHelp: DashboardHelp = {
description:
'ساعت پایان همان بازه. بعد از این ساعت، نمایش محصول در اپ به‌روز می‌شود (معمولاً بسته می‌شود).',
formula: 'اگر شروع و پایان یکی باشند، بازه نادیده گرفته می‌شود. مثال پایان: ۲۲:۰۰:۰۰',
}
const [submit, reload] = useBaseTable() const [submit, reload] = useBaseTable()
const emits = defineEmits(['edit', 'add']) const emits = defineEmits(['edit', 'add'])
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null) const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
const activationActiveKey = ref<(string | number)[]>(isMobile.value ? [] : ['activation'])
const startTime = ref(dayjs('00:00:00', 'HH:mm:ss')) const startTime = ref(dayjs('00:00:00', 'HH:mm:ss'))
const endTime = ref(dayjs('00:00:00', 'HH:mm:ss')) const endTime = ref(dayjs('00:00:00', 'HH:mm:ss'))
@@ -195,6 +236,26 @@ const activeProduct = (product: Product) =>
await fetch() await fetch()
}) })
function handleArchive(product: Product) {
confirmToggle(
'آیا این محصول بایگانی شود؟ نمایش، خرید و فروش آن غیرفعال می‌شود.',
async () => {
await archiveProductById(product.id)
},
)
}
async function archiveProductById(id: number) {
try {
await archiveProduct(id)
message.success('محصول با موفقیت بایگانی شد.')
await fetch()
} catch (e) {
console.error(e)
message.error('بایگانی محصول انجام نشد.')
}
}
async function setTimeActive(start: dayjs.Dayjs | string, end: dayjs.Dayjs | string) { async function setTimeActive(start: dayjs.Dayjs | string, end: dayjs.Dayjs | string) {
try { try {
const startTimeFormatted = dayjs(start).format('HH:mm:ss') const startTimeFormatted = dayjs(start).format('HH:mm:ss')
@@ -212,18 +273,4 @@ async function setTimeActive(start: dayjs.Dayjs | string, end: dayjs.Dayjs | str
.ant-tooltip { .ant-tooltip {
z-index: 1001 !important; z-index: 1001 !important;
} }
.product-activation-collapse.ant-collapse {
:deep(> .ant-collapse-item > .ant-collapse-header) {
padding: 0 !important;
}
:deep(.ant-collapse-content-box) {
padding: 0 !important;
}
:deep(.ant-collapse-header .mb-3) {
margin-bottom: 0 !important;
}
}
</style> </style>
+4 -2
View File
@@ -409,8 +409,10 @@ async function submitProduct() {
} }
async function saveProduct() { async function saveProduct() {
if (!activeKey_.value.includes('0')) { const formSectionKeys = ['0', '1', '2']
activeKey_.value = ['0', ...activeKey_.value] const nextKeys = [...new Set([...formSectionKeys, ...activeKey_.value])]
if (nextKeys.length !== activeKey_.value.length) {
activeKey_.value = nextKeys
await nextTick() await nextTick()
} }
@@ -1,199 +0,0 @@
<template>
<div>
<div
:style="{ backgroundImage: `url(${bg1})` }"
class="flex min-h-[50vh] w-full items-center justify-center bg-cover px-4 py-6 md:h-full md:w-[80vw] md:px-0 md:py-0"
>
<div class="w-full max-w-md rounded-lg bg-white p-4 shadow-2xl md:h-[25vh] md:w-1/3 md:p-10">
<a-form :model="form" ref="formRef" autocomplete="off" @finish="ok">
<div class="gap-4">
<a-form-item label="سقف واریز اولیه (ریال)" name="value" :rules="inputRules()">
<BasicInput v-model:value="form.value" type="number" />
</a-form-item>
<div v-show="form.value > 10">معادل {{ usePrice(form.value / 10, true, 'تومان') }}</div>
<!-- <a-form-item label="نام چاپی" name="print_name" :rules="inputRules()">
<BasicInput v-model:value="form.print_name" />
</a-form-item>-->
<a-form-item>
<BaseButton
class="mt-4 w-full md:mt-10"
:disabled="loading"
type="primary"
size="middle"
html-type="submit"
>
<div>
<a-spin v-if="loading" size="small" />
<div v-else>ثبت</div>
</div>
</BaseButton>
</a-form-item>
</div>
</a-form>
</div>
</div>
<!-- <BasicShowContent>-->
<!-- &lt;!&ndash; <BasicTags-->
<!-- :items="items"-->
<!-- color="green"-->
<!-- @add="add"-->
<!-- @edit="edit"-->
<!-- :key="refresh"-->
<!-- :loading="loading"-->
<!-- @delete="deleteTag"-->
<!-- />&ndash;&gt;-->
<!-- </BasicShowContent>-->
<!-- <a-modal
v-model:visible="addNatureVisible"
title="Vertically centered modal dialog"
centered
@ok="addNatureVisible = false"
>
<BaseInput title="عنوان" size="large" />
</a-modal>-->
<!-- <BasicModal
v-model:visible="addNatureVisible"
:title="editId ? `ویرایش` : 'افزودن'"
@close="addNatureVisible = false"
@ok="ok"
:loading="loading"
>
<a-form ref="formRef" :model="form" autocomplete="off">
<a-form-item name="value" label="حروف اختصاری" :rules="valueValidation">
<BasicInput v-model:value="form.value" placeHolder="حروف اختصاری" />
</a-form-item>
<a-form-item name="title_fa" label="عنوان">
<BasicInput v-model:value="form.title_fa" placeHolder="نام" />
</a-form-item>
</a-form>
</BasicModal>-->
</div>
</template>
<script setup lang="ts">
import BasicShowContent from '@/components/BasicShowContext.vue'
import { inputRules, valueValidation } from '@/utils/inputRules'
import type { Const } from '../../../../model/const'
import type { TagItem } from '@/components/BasicTags/model'
import { message } from 'ant-design-vue'
import type { FormInstance } from 'ant-design-vue'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import { createAddConst, deleteConst, updateEditConst } from '@/api/consts'
import bg1 from '@/pages/login/assets/img/loginBack.svg'
import { usePrice } from '../../../../core/utils/usePrice'
//data Arrays
const items = ref<Array<TagItem>>()
const baseData = ref<Array<Const>>()
//limitedOrder modal
const addNatureVisible = ref<boolean>()
const loading = ref<boolean>()
//refresh
let refresh = 0
const form = ref<Const>({
title_fa: '',
value: ''
})
const formRef = ref<FormInstance>()
//store
const store = useStaticApiListsStore()
//get limitedOrder
/*onMounted(() => {
setTypes()
})
const getTypes = async () =>
(baseData.value = store.natureListData ?? (await store.getNatureConst()))*/
/*async function setTypes() {
try {
loading.value = true
await getTypes()
// console.log('set', baseData.value)
items.value = baseData.value?.map((x: Const) => {
return {
id: x.id,
name: `${x.title_fa ?? ''}(${x.value})`
}
})
refresh++
} catch (e) {
console.error(e)
// message.error('خطا در دریافت اطلاعات')
} finally {
loading.value = false
}
// console.log(items.value)
}*/
//delete limitedOrder item
/*
async function deleteTag(id: number) {
// console.log(id)
try {
await deleteConst(id)
} catch (e) {
console.error(e)
// message.error('خطا در حذف مورد')
await resetData()
}
}
*/
//edit limitedOrder
// let editId: number | undefined = undefined
// function edit(id: number) {
// const obj: Const = baseData.value?.find((x: Const) => x.id === id) ?? ({} as Const)
// addNatureVisible.value = true
// editId = id
// form.value = obj
// }
//resetData
// async function resetData() {
// await store.getNatureConst()
// formRef.value?.resetFields()
// form.value = {
// title_fa: '',
// value: ''
// }
// await setTypes()
// }
//add
// function add() {
// editId = undefined
// addNatureVisible.value = true
// resetData()
// }
//submit
async function ok() {
let obj: Const = {
key: 'limited_price',
// title_fa: form.value.title_fa,
value: form.value.value
// id: editId
}
formRef.value?.validateFields().then(async () => {
try {
loading.value = true
await updateEditConst(obj)
message.success('عملیات با موفقیت انجام شد')
} catch (e) {
console.error(e)
// message.error('خطا در ثبت اطلاعات')
} finally {
loading.value = false
// addNatureVisible.value = false
}
})
}
</script>
<style></style>
@@ -0,0 +1,132 @@
<template>
<BasicActionSheet
v-model:visible="visibleProxy"
title="مشخصات کیف پول"
modal-width="40rem"
:show-button="false"
@close="visibleProxy = false"
>
<template v-if="wallet">
<div v-if="isMobile" class="mb-3 text-base font-700">مشخصات کیف پول</div>
<div class="mb-4 flex flex-col gap-2 rounded-lg bg-gray-100 p-3 md:flex-row md:items-center md:justify-between">
<div class="min-w-0">
<div class="text-base font-bold">{{ wallet.wallet_subject || '—' }}</div>
<div class="mt-1 text-sm text-gray-600">کد کیف پول: {{ wallet.id ?? '—' }}</div>
</div>
<CustomTag
v-bind="kindStyle"
class="self-start px-4 font-bold md:self-auto"
>
{{ wallet.wallet_kind_label || '—' }}
</CustomTag>
</div>
<div class="mb-4 grid gap-3 md:grid-cols-2">
<BasicInfoItem title="کد مشتری" :value="wallet.customer?.id ?? '—'" />
<BasicInfoItem title="نام مشتری" :value="wallet.customer?.name || '—'" />
<BasicInfoItem title="موبایل" :value="wallet.customer?.mobile || '—'" />
<BasicInfoItem title="فلز مرتبط" :value="wallet.wallet_metal_label || '—'" />
</div>
<div v-if="wallet.wallet_kind === 'product'" class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3">
<div class="mb-2 text-sm font-700 text-green-900">اطلاعات محصول</div>
<div class="grid gap-3 md:grid-cols-2">
<BasicInfoItem title="کد محصول" :value="wallet.product_id ?? '—'" />
<BasicInfoItem title="نام محصول" :value="wallet.product_title || wallet.product?.title || '—'" />
<BasicInfoItem title="دسته" :value="wallet.product_group_type || '—'" />
<BasicInfoItem title="وزن / مقدار" :value="productWeightLabel" />
</div>
</div>
<div v-else-if="wallet.wallet_kind === 'fund'" class="mb-4 rounded-lg border border-purple-200 bg-purple-50 p-3">
<div class="mb-2 text-sm font-700 text-purple-900">اطلاعات قلک</div>
<div class="grid gap-3 md:grid-cols-2">
<BasicInfoItem title="کد قلک" :value="wallet.fund_id ?? '—'" />
<BasicInfoItem title="عنوان قلک" :value="wallet.fund_title || wallet.fund?.title || '—'" />
</div>
</div>
<div v-else class="mb-4 rounded-lg border border-blue-200 bg-blue-50 p-3 text-sm text-blue-900">
کیف اصلی مشتری — موجودی ریالی و طلای آبشده (و محصولات تعریف‌شده در تنظیمات کیف اصلی) در همین کیف نگهداری می‌شود.
</div>
<div class="mb-2 text-sm font-700">موجودی</div>
<div class="mb-4 grid gap-2 md:grid-cols-3">
<div class="rounded-lg border border-gray-200 bg-white p-3 text-center">
<div class="mb-1 text-xs text-gray-500">ریال</div>
<div class="font-bold break-all">{{ usePrice(wallet.cash ?? 0) }}</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-3 text-center">
<div class="mb-1 text-xs text-gray-500">ارزش ریالی فلز</div>
<div class="font-bold break-all">{{ usePrice(wallet.gold_to_cash ?? 0) }}</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-3 text-center">
<div class="mb-1 text-xs text-gray-500">جمع ارزش</div>
<div class="font-bold break-all">{{ usePrice(wallet.total_rial_value ?? 0) }}</div>
</div>
</div>
<div v-if="metalRows.length" class="grid gap-2 md:grid-cols-2">
<div
v-for="row in metalRows"
:key="row.metal_id"
class="rounded-lg border border-amber-200 bg-amber-50 p-3"
>
<div class="text-xs text-amber-800">{{ row.label }}</div>
<div class="mt-1 font-bold text-amber-950">{{ row.display }}</div>
</div>
</div>
</template>
</BasicActionSheet>
</template>
<script setup lang="ts">
import type { Wallet } from '@/model/wallet'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import BasicInfoItem from '@/components/BasicInfoItem.vue'
import CustomTag from '@/components/CustomTag.vue'
import { isMobile } from '@core/utils/is'
import { usePrice } from '@core/utils/usePrice'
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
import { walletKindTagStyle, walletProductWeightLabel } from '@/utils/walletDisplay'
import type { WalletMetalColumn } from '../hooks/walletColumns'
const props = defineProps<{
visible: boolean
wallet: Wallet | null
metals?: WalletMetalColumn[]
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
}>()
const visibleProxy = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value)
})
const kindStyle = computed(() => walletKindTagStyle(props.wallet?.wallet_kind))
const productWeightLabel = computed(() => walletProductWeightLabel(props.wallet))
const metalRows = computed(() => {
const wallet = props.wallet
if (!wallet) return []
const metals = props.metals?.length ? props.metals : [{ metal_id: 1, label: 'طلا' }]
return metals
.map((metal) => {
const value = metalCellValue(wallet as Record<string, unknown>, metal.metal_id)
return {
metal_id: metal.metal_id,
label: metal.label,
display: formatMetalDisplayValue(value, metal.metal_id),
raw: value
}
})
.filter((row) => row.raw !== 0)
})
</script>
@@ -3,6 +3,16 @@
<template #default="{ item: row }"> <template #default="{ item: row }">
<MobileSheetHeader :title="sheetTitle" :show-title="isMobile" /> <MobileSheetHeader :title="sheetTitle" :show-title="isMobile" />
<div class="mb-3 flex items-center justify-between gap-2">
<span class="text-sm text-gray-600">نوع کیف</span>
<span
class="rounded px-2 py-0.5 text-xs font-700"
:style="{ color: kindStyle.textColor, backgroundColor: kindStyle.bgColor }"
>
{{ row.wallet_kind_label || '—' }}
</span>
</div>
<div class="space-y-2"> <div class="space-y-2">
<MobileStatCard bordered variant="success" label="موجودی ریالی" :value="usePrice(row.cash ?? 0, true, '')"> <MobileStatCard bordered variant="success" label="موجودی ریالی" :value="usePrice(row.cash ?? 0, true, '')">
<template #prefix> <template #prefix>
@@ -10,11 +20,34 @@
</template> </template>
</MobileStatCard> </MobileStatCard>
<MobileStatCard
v-if="(row.gold_to_cash ?? 0) > 0"
bordered
variant="neutral"
label="ارزش ریالی فلز"
:value="usePrice(row.gold_to_cash ?? 0, true, '')"
>
<template #prefix>
<BaseIcon icon="vuesax-linear:coin" size="18" color="#b45309" />
</template>
</MobileStatCard>
<MobileStatCard
bordered
variant="neutral"
label="جمع ارزش"
:value="usePrice(row.total_rial_value ?? 0, true, '')"
>
<template #prefix>
<BaseIcon icon="vuesax-linear:money-recive" size="18" color="#1d4ed8" />
</template>
</MobileStatCard>
<MobileStatCard <MobileStatCard
v-for="entry in metalEntries(row)" v-for="entry in metalEntries(row)"
:key="entry.metal_id" :key="entry.metal_id"
bordered bordered
variant="warning" variant="neutral"
:label="walletMetalBalanceTitle(entry.metal_id, entry.label)" :label="walletMetalBalanceTitle(entry.metal_id, entry.label)"
:value="entry.display" :value="entry.display"
> >
@@ -26,8 +59,26 @@
<MobileInfoGrid> <MobileInfoGrid>
<MobileDetailRow label="کد کیف پول" :value="row.id ?? '-'" /> <MobileDetailRow label="کد کیف پول" :value="row.id ?? '-'" />
<MobileDetailRow label="موضوع" :value="row.wallet_subject || '-'" />
<MobileDetailRow label="کد مشتری" :value="row.customer?.id ?? '-'" />
<MobileDetailRow label="نام مشتری" :value="row.customer?.name || '-'" /> <MobileDetailRow label="نام مشتری" :value="row.customer?.name || '-'" />
<MobileDetailRow label="شماره موبایل" :value="row.customer?.mobile ?? '-'" /> <MobileDetailRow label="شماره موبایل" :value="row.customer?.mobile ?? '-'" />
<MobileDetailRow
v-if="row.wallet_kind === 'product'"
label="کد محصول"
:value="row.product_id ?? '-'"
/>
<MobileDetailRow
v-if="row.wallet_kind === 'product'"
label="دسته محصول"
:value="row.product_group_type || '-'"
/>
<MobileDetailRow
v-if="row.wallet_kind === 'fund'"
label="عنوان قلک"
:value="row.fund_title || row.fund?.title || '-'"
/>
<MobileDetailRow label="فلز مرتبط" :value="row.wallet_metal_label || '-'" />
</MobileInfoGrid> </MobileInfoGrid>
</template> </template>
</MobileDetailSheet> </MobileDetailSheet>
@@ -44,6 +95,7 @@ import { isMobile } from '@core/utils/is'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { walletMetalBalanceTitle } from '@/utils/adminMetalLabels' import { walletMetalBalanceTitle } from '@/utils/adminMetalLabels'
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight' import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
import { walletKindTagStyle, walletSheetTitle } from '@/utils/walletDisplay'
import type { WalletMetalColumn } from '../hooks/walletColumns' import type { WalletMetalColumn } from '../hooks/walletColumns'
const props = defineProps<{ const props = defineProps<{
@@ -61,7 +113,8 @@ const visibleProxy = computed({
set: (value: boolean) => emit('update:visible', value) set: (value: boolean) => emit('update:visible', value)
}) })
const sheetTitle = computed(() => props.item?.customer?.name || `کیف پول #${props.item?.id ?? '-'}`) const sheetTitle = computed(() => walletSheetTitle(props.item))
const kindStyle = computed(() => walletKindTagStyle(props.item?.wallet_kind))
function metalEntries(row: Wallet) { function metalEntries(row: Wallet) {
const metals = props.metals?.length const metals = props.metals?.length
@@ -1,24 +1,32 @@
<template> <template>
<MobileCard clickable @click="$emit('open', item)"> <MobileCard clickable @click="$emit('open', item)">
<div class="space-y-1"> <div class="space-y-1.5">
<div class="flex items-center justify-between gap-2"> <div class="flex items-start justify-between gap-2">
<span class="min-w-0 truncate text-sm font-700 text-gray-900">{{ customerName }}</span> <div class="min-w-0">
<span class="shrink-0 text-xs text-gray-400">#{{ item.id ?? '-' }}</span> <div class="truncate text-sm font-700 text-gray-900">{{ subjectLine }}</div>
<div class="mt-0.5 truncate text-xs text-gray-500">{{ customerName }}</div>
</div>
<span
class="shrink-0 rounded px-1.5 py-0.5 text-[10px] font-700"
:style="{ color: kindStyle.textColor, backgroundColor: kindStyle.bgColor }"
>
{{ kindLabel }}
</span>
</div> </div>
<div class="flex items-center justify-between gap-2 text-xs text-gray-600"> <div class="flex items-center justify-between gap-2 text-xs text-gray-600">
<span>
<span class="text-gray-500">کد کیف:</span>
<span class="font-600 text-gray-900">#{{ item.id ?? '-' }}</span>
</span>
<span> <span>
<span class="text-gray-500">ریال:</span> <span class="text-gray-500">ریال:</span>
<span class="font-600 text-gray-900">{{ cashLabel }}</span> <span class="font-600 text-gray-900">{{ cashLabel }}</span>
</span> </span>
<span v-if="metalEntries.length === 1">
<span class="text-gray-500">{{ metalEntries[0].label }}:</span>
<span class="font-700 text-gray-900">{{ metalEntries[0].display }}</span>
</span>
</div> </div>
<div <div
v-if="metalEntries.length > 1" v-if="metalEntries.length"
class="flex flex-wrap gap-x-3 gap-y-1 text-xs text-gray-600" class="flex flex-wrap gap-x-3 gap-y-1 text-xs text-gray-600"
> >
<span v-for="entry in metalEntries" :key="entry.metal_id"> <span v-for="entry in metalEntries" :key="entry.metal_id">
@@ -35,6 +43,7 @@ import type { Wallet } from '@/model/wallet'
import MobileCard from '@/components/mobile/MobileCard.vue' import MobileCard from '@/components/mobile/MobileCard.vue'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight' import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
import { walletKindTagStyle } from '@/utils/walletDisplay'
import type { WalletMetalColumn } from '../hooks/walletColumns' import type { WalletMetalColumn } from '../hooks/walletColumns'
const props = defineProps<{ const props = defineProps<{
@@ -47,6 +56,9 @@ defineEmits<{
}>() }>()
const customerName = computed(() => props.item.customer?.name || '-') const customerName = computed(() => props.item.customer?.name || '-')
const subjectLine = computed(() => props.item.wallet_subject || customerName.value)
const kindLabel = computed(() => props.item.wallet_kind_label || 'کیف پول')
const kindStyle = computed(() => walletKindTagStyle(props.item.wallet_kind))
const cashLabel = computed(() => usePrice(props.item.cash ?? 0, true, '')) const cashLabel = computed(() => usePrice(props.item.cash ?? 0, true, ''))
function metalValue(metalId: number): number { function metalValue(metalId: number): number {
@@ -45,20 +45,14 @@
</div> </div>
<BaseTable v-if="!isMobile" class="min-h-0 min-w-0 flex-1" v-bind="tblProps" @register="submit" ref="tbl"> <BaseTable v-if="!isMobile" class="min-h-0 min-w-0 flex-1" v-bind="tblProps" @register="submit" ref="tbl">
<template #action="{ column, record }"> <template #action="{ record }">
<div class="gap-2 flex justify-center"> <div class="flex justify-center gap-2">
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)"> <a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)">
<template #title> <template #title>
<span>مشخصات</span> <span>مشخصات کیف پول</span>
</template> </template>
<Icon icon="vuesax-linear:stickynote" color="#10913A" /> <Icon icon="vuesax-linear:stickynote" color="#10913A" />
</a-tooltip> </a-tooltip>
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('edit', record)">
<template #title>
<span>ویرایش</span>
</template>
<Icon icon="vuesax-linear:edit" color="#4572E9" />
</a-tooltip>
</div> </div>
</template> </template>
</BaseTable> </BaseTable>
@@ -78,10 +72,8 @@
import type { TableInput } from '@core/type/baseTable' import type { TableInput } from '@core/type/baseTable'
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable' import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue' import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { changeCustomerActivation } from '@/api/customer'
import { buildWalletColumns } from '../hooks/walletColumns' import { buildWalletColumns } from '../hooks/walletColumns'
import { usePrefix } from '@core/composable/usePrefix' import { usePrefix } from '@core/composable/usePrefix'
import { message } from 'ant-design-vue'
import { getWalletTable } from '@/api/wallet' import { getWalletTable } from '@/api/wallet'
import { usePrice } from '../../../../core/utils/usePrice' import { usePrice } from '../../../../core/utils/usePrice'
import WalletMobileCardList from './WalletMobileCardList.vue' import WalletMobileCardList from './WalletMobileCardList.vue'
@@ -92,7 +84,7 @@ import { formatMetalDisplayValue, metalDisplayUnit } from '@/utils/metalWeight'
const [submit, reload] = useBaseTable() const [submit, reload] = useBaseTable()
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null) const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
const emits = defineEmits(['delete', 'edit', 'add', 'info', 'sums']) const emits = defineEmits(['info', 'sums'])
const { prefixCls } = usePrefix('pophoverstatus') const { prefixCls } = usePrefix('pophoverstatus')
@@ -133,7 +125,6 @@ const tblProps = ref<any>({
return data return data
} }
}) })
const loading = ref<boolean>(false)
function walletSumTone(value?: number) { function walletSumTone(value?: number) {
if (value == null || value >= 0) { if (value == null || value >= 0) {
@@ -147,19 +138,6 @@ function onMobileSums(payload: unknown) {
emits('sums', payload) emits('sums', payload)
} }
async function changeCustomerActivationFunction(id: number) {
try {
loading.value = true
await changeCustomerActivation(id)
reload()
message.success('تغییر وضعیت با موفقیت انجام شد')
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
async function fetch() { async function fetch() {
await Promise.allSettled([reload(), mobileListRef.value?.fetch?.() as Promise<void>]) await Promise.allSettled([reload(), mobileListRef.value?.fetch?.() as Promise<void>])
} }
@@ -2,20 +2,32 @@ import type { BaseTableColumn } from '@core/type/baseTable'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { metalLabel, walletMetalBalanceTitle } from '@/utils/adminMetalLabels' import { metalLabel, walletMetalBalanceTitle } from '@/utils/adminMetalLabels'
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight' import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
import { walletKindTagStyle } from '@/utils/walletDisplay'
export type WalletMetalColumn = { metal_id: number; label: string; display_unit?: string } export type WalletMetalColumn = { metal_id: number; label: string; display_unit?: string }
function kindTag(record: Record<string, unknown>) {
const label = String(record.wallet_kind_label ?? '—')
const style = walletKindTagStyle(record.wallet_kind as never)
return (
<span
class="inline-block rounded px-2 py-0.5 text-xs font-600"
style={{ color: style.textColor, backgroundColor: style.bgColor }}
>
{label}
</span>
)
}
export function buildWalletColumns(metals: WalletMetalColumn[] = []): BaseTableColumn[] { export function buildWalletColumns(metals: WalletMetalColumn[] = []): BaseTableColumn[] {
const baseColumns: BaseTableColumn[] = [ const baseColumns: BaseTableColumn[] = [
{ {
title: 'ردیف', title: 'ردیف',
dataIndex: 'id', dataIndex: 'id',
key: 'id', key: 'row_index',
filterable: false, filterable: false,
sortable: false, sortable: false,
customRender: (record: any) => { customRender: (record: { index: number }) => record.index + 1
return record.index + 1
}
}, },
{ {
title: 'کد کیف پول', title: 'کد کیف پول',
@@ -23,6 +35,58 @@ export function buildWalletColumns(metals: WalletMetalColumn[] = []): BaseTableC
key: 'id', key: 'id',
type: 'Number' type: 'Number'
}, },
{
title: 'نوع کیف',
dataIndex: 'wallet_kind_label',
key: 'wallet_kind_label',
type: 'Text',
customRender(data) {
return kindTag(data.record as Record<string, unknown>)
}
},
{
title: 'موضوع / محصول',
dataIndex: 'wallet_subject',
key: 'wallet_subject',
type: 'Text',
customRender(data) {
const record = data.record
const subject = record.wallet_subject || '—'
const productId = record.product_id
if (record.wallet_kind === 'product' && productId) {
return (
<div>
<div>{subject}</div>
<div class="text-xs text-gray-500">کد محصول: {productId}</div>
</div>
)
}
if (record.wallet_kind === 'fund' && record.fund_id) {
return (
<div>
<div>{subject}</div>
<div class="text-xs text-gray-500">کد قلک: {record.fund_id}</div>
</div>
)
}
return subject
}
},
{
title: 'دسته محصول',
dataIndex: 'product_group_type',
key: 'product_group_type',
type: 'Text',
customRender(data) {
return data.record.product_group_type || '—'
}
},
{
title: 'کد مشتری',
dataIndex: 'customer.id',
key: 'customer.id',
type: 'Number'
},
{ {
title: 'نام مشتری', title: 'نام مشتری',
dataIndex: 'customer.name', dataIndex: 'customer.name',
@@ -30,7 +94,16 @@ export function buildWalletColumns(metals: WalletMetalColumn[] = []): BaseTableC
type: 'Text' type: 'Text'
}, },
{ {
title: 'موجودی کیف پول (ریال)', title: 'موبایل',
dataIndex: 'customer.mobile',
key: 'customer.mobile',
type: 'Text',
customRender(data) {
return data.record.customer?.mobile || '—'
}
},
{
title: 'موجودی ریالی',
dataIndex: 'cash', dataIndex: 'cash',
key: 'cash', key: 'cash',
type: 'Text', type: 'Text',
@@ -40,34 +113,53 @@ export function buildWalletColumns(metals: WalletMetalColumn[] = []): BaseTableC
} }
] ]
if (metals.length === 0) { const metalColumns: BaseTableColumn[] =
return [ metals.length === 0
...baseColumns, ? [
{ {
title: metalLabel.goldWalletBalanceGram(), title: metalLabel.goldWalletBalanceGram(),
dataIndex: 'gold', dataIndex: 'gold',
key: 'gold', key: 'gold',
type: 'Text', type: 'Text',
customRender(data) { customRender(data) {
const value = Number(data.record?.gold ?? 0) const value = Number(data.record?.gold ?? 0)
return formatMetalDisplayValue(value, 1) return formatMetalDisplayValue(value, 1)
} }
}
]
: metals.map((metal) => ({
title: walletMetalBalanceTitle(metal.metal_id, metal.label),
dataIndex: `metal_gold_${metal.metal_id}`,
key: `metal_${metal.metal_id}`,
type: 'Text',
customRender(data) {
const value = metalCellValue(data.record, metal.metal_id)
return formatMetalDisplayValue(value, metal.metal_id)
}
}))
const tailColumns: BaseTableColumn[] = [
{
title: 'ارزش ریالی فلز',
dataIndex: 'gold_to_cash',
key: 'gold_to_cash',
type: 'Text',
customRender(data) {
return usePrice(data.record.gold_to_cash ?? 0, true, '')
}
},
{
title: 'جمع ارزش (ریال)',
dataIndex: 'total_rial_value',
key: 'total_rial_value',
type: 'Text',
customRender(data) {
return usePrice(data.record.total_rial_value ?? 0, true, '')
} }
]
}
const metalColumns: BaseTableColumn[] = metals.map((metal) => ({
title: walletMetalBalanceTitle(metal.metal_id, metal.label),
dataIndex: `metal_gold_${metal.metal_id}`,
key: `metal_${metal.metal_id}`,
type: 'Text',
customRender(data) {
const value = metalCellValue(data.record, metal.metal_id)
return formatMetalDisplayValue(value, metal.metal_id)
} }
})) ]
return [...baseColumns, ...metalColumns] return [...baseColumns, ...metalColumns, ...tailColumns]
} }
export function getWalletColumns(): BaseTableColumn[] { export function getWalletColumns(): BaseTableColumn[] {
+45 -292
View File
@@ -2,167 +2,60 @@
<div class="h-full min-h-0"> <div class="h-full min-h-0">
<BasicMainCard> <BasicMainCard>
<div class="flex h-full min-h-0 flex-col"> <div class="flex h-full min-h-0 flex-col">
<div class="shrink-0 md:hidden"> <div class="shrink-0 md:hidden">
<!-- <div class="bg-gray-100 grid grid-cols-2 gap-3 w-full py-2 px-5"> <div class="grid grid-cols-3 gap-1 text-xs">
<div class="gap-5 text-xs"> <div class="rounded bg-blue-600 p-1 text-white">مجموع</div>
<div>مجموع موجودی کیف پول:</div> <div class="rounded bg-blue-600 p-1 text-white">ریال</div>
<div class="font-bold">{{ usePrice(sums?.cash ?? 0) }}</div> <div class="rounded bg-blue-600 p-1 text-white">وزن</div>
</div>
<div class="gap-5 text-xs">
<div>مجموع موجودی کیف پول:</div>
<div class="font-bold">
{{ usePrice(sums?.cashToGold.toFixed(2) ?? 0, true, 'گرم') }}
</div>
</div>
<div class="gap-5 text-xs">
<div>{{ metalLabel.totalGoldWallet() }}:</div>
<div class="font-bold">{{ usePrice(sums?.gold ?? 0, true, 'گرم') }}</div>
</div>
<div class="gap-5 text-xs">
<div>{{ metalLabel.totalGoldWallet() }}:</div>
<div class="font-bold">{{ usePrice(sums?.goldToCash ?? 0) }}</div>
</div>
</div>-->
<div class="grid grid-cols-3 gap-1 text-xs">
<div class="bg-blue-600 text-white rounded p-1">مجموع</div>
<div class="bg-blue-600 text-white rounded p-1">ریال</div>
<div class="bg-blue-600 text-white rounded p-1">وزن</div>
<template v-for="metal in mobileMetalRows" :key="`metal-${metal.metal_id}`"> <template v-for="metal in mobileMetalRows" :key="`metal-${metal.metal_id}`">
<div class="bg-blue-600 text-white rounded p-1">{{ metal.label }}</div> <div class="rounded bg-blue-600 p-1 text-white">{{ metal.label }}</div>
<div <div class="rounded p-1 text-white" :class="sumToneClass(metal.goldToCash)">
class="text-white rounded p-1" {{ usePrice(metal.goldToCash ?? 0, true, '') }}
:class="sumToneClass(metal.goldToCash)" </div>
> <div class="rounded p-1 text-white" :class="sumToneClass(metal.gold)">
{{ usePrice(metal.goldToCash ?? 0, true, '') }} {{ formatMetalDisplayValue(metal.gold ?? 0, metal.metal_id) }}
</div> </div>
<div </template>
class="text-white rounded p-1"
:class="sumToneClass(metal.gold)"
>
{{ formatMetalDisplayValue(metal.gold ?? 0, metal.metal_id) }}
</div>
</template>
<div class="bg-blue-600 text-white rounded p-1">کیف پول</div> <div class="rounded bg-blue-600 p-1 text-white">کیف پول</div>
<div <div class="rounded p-1 text-white" :class="sumToneClass(sums?.cash)">
class="text-white rounded p-1" {{ usePrice(sums?.cash ?? 0, true, '') }}
:class="sumToneClass(sums?.cash)" </div>
> <div class="rounded p-1 text-white" :class="sumToneClass(sums?.cashToGold)">
{{ usePrice(sums?.cash ?? 0, true, '') }} {{ usePrice(sums?.cashToGold ?? 0, true, '') }}
</div> </div>
<div
class="text-white rounded p-1"
:class="sumToneClass(sums?.cashToGold)"
>
{{ usePrice(sums?.cashToGold ?? 0, true, '') }}
</div> </div>
</div> </div>
</div>
<div class="flex min-h-0 flex-1 flex-col">
<tbl
ref="table"
class="h-full min-h-0 w-full"
@info="info"
@edit="edit"
@sums="setSums"
@delete="deleteItem"
@add="add"
/>
<!-- <BasicMenu :menu="menu" width="13rem" @onChange="tabSelected" /> <div class="flex min-h-0 flex-1 flex-col">
<component :is="selectedComponent" class="mr-5" />--> <tbl ref="table" class="h-full min-h-0 w-full" @info="openWalletInfo" @sums="setSums" />
<!-- <BasicShowContext title="پروژه ها"> <Search /> </BasicShowContext>--> </div>
</div>
</div> </div>
</BasicMainCard> </BasicMainCard>
<BasicModal
v-model:visible="addUserVisible" <WalletInfoSheet
width="40rem" v-model:visible="infoVisible"
:title="`${editId ? 'ویرایش' : 'افزودن'} کاربر`" :wallet="selectedWallet"
@ok="ok" :metals="catalogMetals"
:loading="loading"
@close="addUserVisible = false"
>
<a-form :model="form" ref="formRef" autocomplete="off">
<div class="grid md:grid-cols-2 gap-4">
<a-form-item label="نام" name="fname" :rules="inputRules()">
<BasicInput v-model:value="form.fname" />
</a-form-item>
<a-form-item label="نام خانوادگی" name="lname" :rules="inputRules()">
<BasicInput v-model:value="form.lname" />
</a-form-item>
<a-form-item label="شماره همراه" name="mobile" :rules="inputLenRules(11)">
<BasicInput v-model:value="form.mobile" type="number" />
</a-form-item>
<a-form-item label="تاریخ تولد" name="birth_date" :rules="inputRules()">
<BaseDatePicker v-model:value="form.birth_date" />
</a-form-item>
<!-- <a-form-item label="ایمیل" name="email" :rules="inputRules()">
<BasicInput v-model:value="form.email" />
</a-form-item>-->
<!-- <a-form-item label="نام فروشگاه" name="shop_name" :rules="inputRules()">
<BasicInput v-model:value="form.shop_name" />
</a-form-item>-->
<a-form-item label="شماره تلفن" name="phone_number" :rules="inputRules()">
<BasicInput v-model:value="form.phone_number" type="number" />
</a-form-item>
<a-form-item label="نام استان" name="province" :rules="inputRules()">
<BasicInput v-model:value="form.province" />
</a-form-item>
<a-form-item label="نام شهر" name="city" :rules="inputRules()">
<BasicInput v-model:value="form.city" />
</a-form-item>
<a-form-item label="شماره ملی" name="n_code" :rules="inputRules()">
<BasicInput v-model:value="form.n_code" />
</a-form-item>
<!-- <a-form-item label="نام چاپی" name="print_name" :rules="inputRules()">
<BasicInput v-model:value="form.print_name" />
</a-form-item>-->
</div>
</a-form>
</BasicModal>
<BasicInfoModal
v-model:visible="userInfoVisible"
@close="userInfoVisible = false"
:title="infoParams.title"
:date="infoParams.date"
:time="infoParams.time"
:type="infoParams.type"
itemsTitle="اطلاعات فردی"
:name="infoParams.name"
:items="infoParams.items"
/> />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// import BasicInfoModal from '@/components/BasicInfoModal'
import { inputLenRules, inputRules } from '@/utils/inputRules'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import { ref } from 'vue' import { ref } from 'vue'
import tbl from './components/table.vue' import tbl from './components/table.vue'
import type { FormInstance } from 'ant-design-vue' import WalletInfoSheet from './components/WalletInfoSheet.vue'
import BasicInfoModal from '@/components/BasicInfoModal/index.vue' import type { Wallet, WalletTableSums } from '@/model/wallet'
import { message } from 'ant-design-vue/es'
import type { Info } from '@/components/BasicInfoModal/model/Info'
import { toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import { confirmDeleteItem } from '@/utils/confirmModal'
import { cloneDeep, sum } from 'lodash'
import type { Item } from '@/model/Item'
import type { Customer } from '@/model/customer'
import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import type { WalletTableSums } from '@/model/wallet'
import { formatMetalDisplayValue } from '@/utils/metalWeight' import { formatMetalDisplayValue } from '@/utils/metalWeight'
import type { WalletMetalColumn } from './hooks/walletColumns'
//loading const table = ref<null | { fetch: () => Promise<void> }>(null)
const loading = ref<boolean>(false)
//define store
const store = useStaticApiListsStore()
const sums = ref<WalletTableSums>() const sums = ref<WalletTableSums>()
const infoVisible = ref(false)
const selectedWallet = ref<Wallet | null>(null)
const catalogMetals = ref<WalletMetalColumn[]>([])
const mobileMetalRows = computed(() => { const mobileMetalRows = computed(() => {
if (sums.value?.metalBalances?.length) { if (sums.value?.metalBalances?.length) {
@@ -195,158 +88,18 @@ function sumToneClass(value?: number) {
return 'bg-red-600' return 'bg-red-600'
} }
//define tbl function setSums(val: WalletTableSums) {
const table = ref(null)
const refreshTable = () => table?.value?.fetch()
let updateKey = ref<number>(0)
//role confirm items
const confirmItems: Array<Item> = [
{
value: 'بله',
key: true
},
{
value: 'خیر',
key: false
}
]
//form
const form = ref<Customer>({})
const formRef = ref<FormInstance>()
//info
let infoParams = reactive<Info>({})
function setSums(val) {
sums.value = val sums.value = val
catalogMetals.value =
val?.metalBalances?.map((metal) => ({
metal_id: metal.metal_id,
label: metal.label
})) ?? []
} }
function info(val: Customer) { function openWalletInfo(wallet: Wallet) {
infoParams.name = val.name selectedWallet.value = wallet
infoParams.type = val.mobile infoVisible.value = true
infoParams.date = toJalaliNoTime(val.created_at)
infoParams.time = toTime(val.created_at)
infoParams.itemsTitle = 'اطلاعات کاربری'
infoParams.items = [
{
key: 'کد کاربری',
value: val.id ?? '-'
},
{
key: 'شماره ملی',
value: val.n_code ?? '-'
},
{
key: 'تاریخ تولد',
value: toJalaliNoTime(val.birth_date) ?? '-'
},
/* {
key: 'ایمیل',
value: val.email ?? '-'
},*/
/* {
key: 'نام فروشگاه',
value: val.shop_name ?? '-'
},*/
{
key: 'شماره تلفن',
value: val.phone_number ?? '-'
},
{
key: 'نام استان',
value: val.province ?? '-'
},
{
key: 'نام شهر',
value: val.city ?? '-'
}
/* {
key: 'نام چاپی',
value: val.print_name ?? '-'
}*/
]
userInfoVisible.value = true
}
//edit
let editId: number | undefined = undefined
async function edit(record: Customer) {
addUserVisible.value = true
editId = record.id
form.value = cloneDeep(record)
}
//add
async function add() {
loading.value = true
editId = undefined
// await store.getUsersList()
addUserVisible.value = true
updateKey.value++
resetData()
loading.value = false
}
//reset data
const resetData = () => {
formRef.value?.resetFields()
form.value = {
fname: '',
lname: '',
active: false,
avatar_id: undefined,
sts: undefined,
id: undefined,
mobile: undefined
}
}
//delete
const deleteItem = (id: number) => confirmDeleteItem('کاربر', id, deleteCustomer, refreshTable)
//define selects
onMounted(async () => {
loading.value = true
// await getResponsible()
// await getQC()
// await getManager()
loading.value = false
})
//add project modal
const addUserVisible = ref<boolean>(false)
const userInfoVisible = ref<boolean>(false)
//submit user
async function ok() {
formRef.value?.validateFields().then(async () => {
// console.log('salam', form.value)
// return
loading.value = true
try {
if (editId) {
await updateCustomer(editId, form.value)
} else {
await createCustomer(form.value)
}
addUserVisible.value = false
refreshTable()
resetData()
message.success('عملیات با موفقیت انجام شد')
// await getResponsible()
// await getQC()
// await getManager()
} catch (e) {
console.error(e)
// message.error('خطا در بارگذاری اطلاعات')
} finally {
loading.value = false
}
})
} }
</script> </script>
@@ -7,7 +7,7 @@
<span class="min-w-0 truncate">{{ sheetTitle }}</span> <span class="min-w-0 truncate">{{ sheetTitle }}</span>
<div class="flex shrink-0 items-center gap-2"> <div class="flex shrink-0 items-center gap-2">
<MobileIconButton <MobileIconButton
v-if="!row.tracked_number" v-if="needsTrackingConfirmation(row)"
plain plain
icon="vuesax-linear:tick-circle" icon="vuesax-linear:tick-circle"
color="#16A34A" color="#16A34A"
@@ -120,6 +120,7 @@ import { status } from '@/pages/menuPages/withdraw/information/hooks/status'
import { import {
formatWithdrawAmountDisplay, formatWithdrawAmountDisplay,
isMoneyWithdraw, isMoneyWithdraw,
needsTrackingConfirmation,
withdrawAmountValue, withdrawAmountValue,
withdrawTypeLabel withdrawTypeLabel
} from '@/pages/menuPages/withdraw/utils/withdrawDisplay' } from '@/pages/menuPages/withdraw/utils/withdrawDisplay'
@@ -11,9 +11,12 @@
</template> </template>
<template #tracked="{ column, record }"> <template #tracked="{ column, record }">
<div v-if="record.tracked_number">{{ record.tracked_number }}</div> <div v-if="record.tracked_number">{{ record.tracked_number }}</div>
<div v-else> <div v-else-if="needsTrackingConfirmation(record)">
<BaseButton @click="emits('withdraw', record)">{{ confirmButtonLabel }}</BaseButton> <BaseButton @click="emits('withdraw', record)">
{{ confirmLabelForRecord(record, props.flowType) }}
</BaseButton>
</div> </div>
<div v-else>-</div>
</template> </template>
<template #action="{ column, record }"> <template #action="{ column, record }">
<div class="gap-2 flex justify-center"> <div class="gap-2 flex justify-center">
@@ -114,6 +117,10 @@ import {getWithdrawTable, updateSts} from '@/api/wallet'
import WithdrawMobileCardList from './WithdrawMobileCardList.vue' import WithdrawMobileCardList from './WithdrawMobileCardList.vue'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import type { WithdrawFlowType } from '../types' import type { WithdrawFlowType } from '../types'
import {
confirmLabelForRecord,
needsTrackingConfirmation
} from '@/pages/menuPages/withdraw/utils/withdrawDisplay'
import TahesabRetryModal from '@/components/TahesabRetryModal.vue' import TahesabRetryModal from '@/components/TahesabRetryModal.vue'
import { initTahesabSettingFromApi, tahesabEnabledSetting } from '@/utils/config' import { initTahesabSettingFromApi, tahesabEnabledSetting } from '@/utils/config'
@@ -149,10 +156,6 @@ function checkPermission(temp: string) {
const table = ref(null) const table = ref(null)
const lastTableParams = ref<TableInput & { flow_type?: string }>({}) const lastTableParams = ref<TableInput & { flow_type?: string }>({})
const confirmButtonLabel = computed(() =>
props.flowType === 'deposit' ? 'تایید واریز' : 'تایید برداشت'
)
const tblProps = computed(() => ({ const tblProps = computed(() => ({
columns: getWithdrawColumns(), columns: getWithdrawColumns(),
api: async (params: TableInput) => { api: async (params: TableInput) => {
+7 -1
View File
@@ -34,7 +34,7 @@
<BasicModal <BasicModal
v-model:visible="confirmWithdrawVisible" v-model:visible="confirmWithdrawVisible"
width="15rem" width="15rem"
title="تایید برداشت" :title="confirmModalTitle"
wrap-class-name="withdraw-stack-modal" wrap-class-name="withdraw-stack-modal"
@ok="okWithdraw" @ok="okWithdraw"
:loading="loading" :loading="loading"
@@ -117,6 +117,7 @@ import type { Wallet, Withdraw } from '@/model/wallet'
import {completeWithdraw, exportWithdrawReport, getWithdrawById, sendWeights} from '@/api/wallet' import {completeWithdraw, exportWithdrawReport, getWithdrawById, sendWeights} from '@/api/wallet'
import Button from "@core/components/BaseButton"; import Button from "@core/components/BaseButton";
import type { WithdrawFlowType } from '@/pages/menuPages/withdraw/types' import type { WithdrawFlowType } from '@/pages/menuPages/withdraw/types'
import { confirmLabelForRecord } from '@/pages/menuPages/withdraw/utils/withdrawDisplay'
import { openHtmlPrintWindow } from '@/pages/menuPages/withdraw/utils/openHtmlPrintWindow' import { openHtmlPrintWindow } from '@/pages/menuPages/withdraw/utils/openHtmlPrintWindow'
//import DatePicker from '@core/components/_sample/components/datePicker.vue' //import DatePicker from '@core/components/_sample/components/datePicker.vue'
@@ -283,6 +284,11 @@ const confirmWithdrawVisible = ref<boolean>(false)
const confirmWeightVisible = ref<boolean>(false) const confirmWeightVisible = ref<boolean>(false)
const confirmWithdraw = ref<Withdraw | null>(null) const confirmWithdraw = ref<Withdraw | null>(null)
const confirmModalTitle = computed(() => {
if (!confirmWithdraw.value) return 'تایید تراکنش'
return confirmLabelForRecord(confirmWithdraw.value, flowType.value)
})
const weightTransactions = computed(() => { const weightTransactions = computed(() => {
const tx = confirmWithdraw.value?.transaction const tx = confirmWithdraw.value?.transaction
return Array.isArray(tx) ? tx : [] return Array.isArray(tx) ? tx : []
@@ -1,4 +1,5 @@
import type { Withdraw } from '@/model/wallet' import type { Withdraw } from '@/model/wallet'
import type { WithdrawFlowType } from '@/pages/menuPages/withdraw/types'
import { metalLabel } from '@/utils/adminMetalLabels' import { metalLabel } from '@/utils/adminMetalLabels'
import { getMainMetal } from '@/utils/metalLabels' import { getMainMetal } from '@/utils/metalLabels'
import { import {
@@ -61,3 +62,50 @@ export function withdrawAmountColorClass(record: Pick<Withdraw, 'cash' | 'gold'>
if (!value) return '' if (!value) return ''
return value > 0 ? 'text-green' : 'text-red' return value > 0 ? 'text-green' : 'text-red'
} }
type WithdrawFlowRecord = Pick<
Withdraw,
'sts' | 'note' | 'cash' | 'gold' | 'bank_account_id' | 'type'
>
/** همان منطق scopeWithdrawals / scopeDeposits در بک‌اند — برای برچسب دکمه در تب «همه» */
export function resolveRecordFlowType(record: WithdrawFlowRecord): Exclude<WithdrawFlowType, 'all'> {
const sts = Number(record.sts ?? 0)
const note = String(record.note ?? '')
const cash = Number(record.cash ?? 0)
const gold = Number(record.gold ?? 0)
if (
note.includes('برداشت') ||
record.type === 2 ||
(sts >= 50 && sts <= 52) ||
(sts === 1 && record.bank_account_id != null) ||
(sts === 2 && note.includes('برداشت')) ||
cash < 0 ||
gold < 0
) {
return 'withdraw'
}
return 'deposit'
}
export function confirmLabelForRecord(
record: WithdrawFlowRecord,
tabFlowType: WithdrawFlowType = 'all'
): string {
const flowType = tabFlowType === 'all' ? resolveRecordFlowType(record) : tabFlowType
return flowType === 'deposit' ? 'تایید واریز' : 'تایید برداشت'
}
/** فقط تراکنش‌های در انتظار پیگیری — نه واریز/کسر ادمین (sts=4) */
export function needsTrackingConfirmation(record: Pick<Withdraw, 'sts' | 'tracked_number'>): boolean {
if (record.tracked_number) return false
const sts = Number(record.sts ?? 0)
if ([2, 4, 5, 9].includes(sts)) return false
if (sts === 1) return true
if (sts >= 50 && sts <= 52) return true
return false
}
+5 -27
View File
@@ -1,35 +1,13 @@
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
const Orders: RouteRecordRaw = { /** ریدایرکت آدرس قدیمی — منو زیر پیکربندی است */
const Pricing: RouteRecordRaw = {
name: 'pricing', name: 'pricing',
path: '/pricing', path: '/pricing',
component: () => import('@core/layouts/BasicLayout.vue'),
redirect: { name: 'pricingMain' }, redirect: { name: 'pricingMain' },
meta: { meta: {
icon: 'vuesax-linear:dollar-circle', hideMenu: true
permissionsKey: 'pricing', }
title: 'قیمت گذاری',
disabled: false,
hideMenu: false,
hideChildren: true,
order: 9
},
children: [
{
name: 'pricingMain',
path: '',
component: () => import('@/pages/menuPages/pricing/index.vue'),
meta: {
permissionsKey: 'pricing',
icon: 'vuesax-linear:setting',
order: 3,
title: 'قیمت گذاری',
disabled: false,
hideMenu: false
//hideChildren: false
}
}
]
} }
export default Orders export default Pricing
+13 -13
View File
@@ -42,6 +42,17 @@ const Setting: RouteRecordRaw = {
order: 1 order: 1
} }
}, },
{
name: 'pricingMain',
path: 'pricing',
component: () => import('@/pages/menuPages/pricing/index.vue'),
meta: {
permissionsKey: 'pricing',
icon: 'vuesax-linear:dollar-circle',
title: 'تاریخچه قیمت',
order: 2
}
},
{ {
name: 'advancedConfiguration', name: 'advancedConfiguration',
path: 'advanced-configuration', path: 'advanced-configuration',
@@ -51,7 +62,7 @@ const Setting: RouteRecordRaw = {
deployAdminOnly: true, deployAdminOnly: true,
icon: 'vuesax-linear:code-circle', icon: 'vuesax-linear:code-circle',
title: 'تنظیمات پیشرفته', title: 'تنظیمات پیشرفته',
order: 2 order: 3
}, },
beforeEnter() { beforeEnter() {
const userStore = useUserStore() const userStore = useUserStore()
@@ -67,7 +78,7 @@ const Setting: RouteRecordRaw = {
meta: { meta: {
icon: 'vuesax-linear:image', icon: 'vuesax-linear:image',
title: 'بنرها', title: 'بنرها',
order: 3 order: 4
}, },
beforeEnter() { beforeEnter() {
if (!bannerEnabledSetting.value) { if (!bannerEnabledSetting.value) {
@@ -75,17 +86,6 @@ const Setting: RouteRecordRaw = {
} }
} }
}, },
{
name: 'settingMain_limitedOrder',
path: 'limitedOrder',
component: () => import('@/pages/menuPages/setting/limitedOrder/index.vue'),
meta: {
permissionsKey: 'config.limitedOrder',
icon: 'vuesax-linear:document-text',
title: 'محدودیت سفارش',
order: 4
}
},
{ {
name: 'priceTriggers', name: 'priceTriggers',
path: 'priceTriggers', path: 'priceTriggers',
+25
View File
@@ -0,0 +1,25 @@
import type { Wallet, WalletKind } from '@/model/wallet'
export function walletKindTagStyle(kind?: WalletKind) {
switch (kind) {
case 'main':
return { textColor: '#1d4ed8', bgColor: '#dbeafe' }
case 'product':
return { textColor: '#15803d', bgColor: '#dcfce7' }
case 'fund':
return { textColor: '#7e22ce', bgColor: '#f3e8ff' }
default:
return { textColor: '#374151', bgColor: '#f3f4f6' }
}
}
export function walletSheetTitle(wallet: Wallet | null | undefined): string {
if (!wallet) return 'مشخصات کیف پول'
return wallet.wallet_subject || wallet.customer?.name || `کیف پول #${wallet.id ?? '-'}`
}
export function walletProductWeightLabel(wallet: Wallet | null | undefined): string {
if (!wallet?.product_amount && wallet?.product_amount !== 0) return '—'
const unit = wallet.product_unit ? ` ${wallet.product_unit}` : ''
return `${wallet.product_amount}${unit}`
}