fix some bug
This commit is contained in:
@@ -8,6 +8,8 @@ export const createProduct = async (obj: Customer) =>
|
||||
await useAxios.post<AddId>(`/admin/product`, obj)
|
||||
|
||||
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) =>
|
||||
await useAxios.put<AddId>(`/admin/product/${id}`, obj)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
>
|
||||
<AButton
|
||||
v-if="showCancel"
|
||||
type="primary"
|
||||
danger
|
||||
:class="[{ 'col-span-4': showOk }]"
|
||||
size="large"
|
||||
ghost
|
||||
@@ -70,7 +70,7 @@
|
||||
>
|
||||
<AButton
|
||||
v-if="showCancel"
|
||||
type="primary"
|
||||
danger
|
||||
:class="[{ 'col-span-4': showOk }]"
|
||||
size="large"
|
||||
ghost
|
||||
|
||||
+80
-69
@@ -1,13 +1,9 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
//debugger
|
||||
|
||||
|
||||
// Store config in window object
|
||||
window.__APP_CONFIG__ = window.__APP_CONFIG__ || {};
|
||||
window.__CONFIG_READY__ = false;
|
||||
|
||||
// Create a promise that will be resolved when config is loaded
|
||||
let configResolve;
|
||||
let configReject;
|
||||
window.__CONFIG_PROMISE__ = new Promise((resolve, reject) => {
|
||||
@@ -15,14 +11,11 @@
|
||||
configReject = reject;
|
||||
});
|
||||
|
||||
const VERSION_URL = 'https://abshode-service.montgit.ir/server/get_version';
|
||||
const CONFIG_URLS = [
|
||||
'https://nerkh.gold/get_user',
|
||||
'https://abshode-service.montgit.ir/server/get_user'
|
||||
];
|
||||
const VERSION_SUCCESS_URL_KEY = 'VERSION_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;
|
||||
let configTimeoutId;
|
||||
let mainScriptLoaded = false;
|
||||
@@ -31,6 +24,40 @@
|
||||
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) {
|
||||
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) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = window.setTimeout(() => controller.abort(), timeout);
|
||||
@@ -72,36 +105,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVersion() {
|
||||
try {
|
||||
await fetchJson(VERSION_URL, 3500);
|
||||
console.log('Version loaded from:', VERSION_URL);
|
||||
} catch (error) {
|
||||
console.warn('Version check failed from', VERSION_URL, error);
|
||||
async function loadVersion(rounds = VERSION_RETRY_ROUNDS) {
|
||||
const urls = sortVersionUrls(getVersionUrls());
|
||||
|
||||
for (let round = 1; round <= rounds; round++) {
|
||||
for (let index = 0; index < urls.length; index++) {
|
||||
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() {
|
||||
const protocol = getProtocol();
|
||||
const urls = sortConfigUrls(
|
||||
CONFIG_URLS.map((url) => url.replace(/^https?:/, protocol))
|
||||
);
|
||||
async function fetchConfig(rounds = CONFIG_RETRY_ROUNDS) {
|
||||
const urls = sortConfigUrls(getConfigUrls());
|
||||
|
||||
for (let index = 0; index < urls.length; index++) {
|
||||
const url = urls[index];
|
||||
const timeout = index === urls.length - 1 ? 5000 : 3500;
|
||||
for (let round = 1; round <= rounds; round++) {
|
||||
for (let index = 0; index < urls.length; index++) {
|
||||
const url = urls[index];
|
||||
const timeout = index === urls.length - 1 ? 7000 : 4000;
|
||||
|
||||
try {
|
||||
const config = await fetchJson(url, timeout);
|
||||
try {
|
||||
const config = await fetchJson(url, timeout);
|
||||
|
||||
if (!config || typeof config !== 'object') {
|
||||
throw new Error('Config response is invalid');
|
||||
if (!config || typeof config !== 'object') {
|
||||
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();
|
||||
}
|
||||
|
||||
// Fetch config from server
|
||||
async function loadConfig() {
|
||||
try {
|
||||
await loadVersion();
|
||||
@@ -169,57 +215,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Function to dynamically load main.ts script
|
||||
function loadMainScript() {
|
||||
//const config= window.__APP_CONFIG__
|
||||
// Check if main.ts is already loaded
|
||||
if (mainScriptLoaded || document.querySelector('script[src="/src/main.ts"]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
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(() => {
|
||||
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() {
|
||||
if (!mainScriptLoaded) {
|
||||
console.warn('Config loader: Timeout reached. Loading main.ts with empty config.');
|
||||
// Set default empty config
|
||||
window.__APP_CONFIG__ = window.__APP_CONFIG__ || {};
|
||||
window.__CONFIG_READY__ = true;
|
||||
|
||||
// Resolve promise with empty config
|
||||
if (configResolve) {
|
||||
configResolve({});
|
||||
}
|
||||
@@ -228,7 +241,5 @@
|
||||
}
|
||||
}, CONFIG_TIMEOUT);
|
||||
|
||||
// Start loading config immediately
|
||||
loadConfig();
|
||||
})();
|
||||
|
||||
|
||||
@@ -6,3 +6,6 @@ export const MODAL_ABOVE_ACTION_SHEET_Z_INDEX = 1100
|
||||
|
||||
/** z-index برای dropdown سلکتها داخل مودال (باید بالاتر از مودال باشد) */
|
||||
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
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface DashboardFinance {
|
||||
fullPriceSood?: number
|
||||
fullPriceKarmozd?: number
|
||||
fullPriceKaermozd?: number
|
||||
/** سود اسپرد + کارمزد */
|
||||
totalTradingProfit?: number
|
||||
orderCount?: number
|
||||
Gifts?: string
|
||||
GiftsPrise?: number
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { User } from '@/model/user'
|
||||
|
||||
export type WalletKind = 'main' | 'product' | 'fund'
|
||||
|
||||
export interface WalletMetalBalance {
|
||||
metal_id: number
|
||||
label: string
|
||||
@@ -8,6 +10,20 @@ export interface WalletMetalBalance {
|
||||
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 {
|
||||
owner: number
|
||||
id: number
|
||||
@@ -17,6 +33,22 @@ export interface Wallet {
|
||||
product_id?: number | null
|
||||
metal_gold?: Record<string, number>
|
||||
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 {
|
||||
@@ -43,6 +75,8 @@ export interface Withdraw {
|
||||
tracked_number?: string
|
||||
date_at?: string
|
||||
sts?: number
|
||||
type?: number
|
||||
bank_account_id?: number | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
sts_str?: string
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<template>
|
||||
<a-tooltip placement="top" :overlay-inner-style="{ maxWidth: '300px', direction: 'rtl' }">
|
||||
<template #title>
|
||||
<div class="space-y-1.5 text-xs leading-5">
|
||||
<p>{{ help.description }}</p>
|
||||
<p v-if="help.formula" class="border-t border-white/20 pt-1.5 text-white/90">
|
||||
<span class="font-semibold">فرمول:</span> {{ help.formula }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<a-tooltip
|
||||
placement="top"
|
||||
:title="tooltipText"
|
||||
overlay-class-name="dashboard-help-tooltip"
|
||||
:overlay-style="{ zIndex: TOOLTIP_ABOVE_MODAL_Z_INDEX }"
|
||||
:overlay-inner-style="{ maxWidth: '26rem', direction: 'rtl', textAlign: 'right', whiteSpace: 'pre-line' }"
|
||||
:mouse-enter-delay="0.15"
|
||||
>
|
||||
<span
|
||||
class="dashboard-help-trigger inline-flex shrink-0 items-center justify-center"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
@click.stop
|
||||
role="img"
|
||||
:aria-label="tooltipText"
|
||||
tabindex="0"
|
||||
>
|
||||
<IosInfoBadge :size="badgeSize" />
|
||||
</span>
|
||||
@@ -22,6 +21,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
|
||||
import IosInfoBadge from '@/pages/dashboard/components/IosInfoBadge.vue'
|
||||
import { TOOLTIP_ABOVE_MODAL_Z_INDEX } from '@/constants/overlayZIndex'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -34,6 +34,14 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
@@ -42,3 +50,10 @@ const badgeSize = computed(() => props.size)
|
||||
cursor: help;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.ant-tooltip.dashboard-help-tooltip {
|
||||
z-index: 1250 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<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
|
||||
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"
|
||||
>
|
||||
<BaseIcon :icon="icon" size="17" :class="iconTextClass" />
|
||||
<BaseIcon :icon="icon" size="15" :class="iconTextClass" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<DashboardLabelWithHelp
|
||||
@@ -13,8 +13,10 @@
|
||||
size="xs"
|
||||
label-class="dashboard-text-muted font-medium"
|
||||
/>
|
||||
<p class="dashboard-text truncate text-[15px] font-bold leading-snug">
|
||||
{{ value }}
|
||||
<p v-if="subtitle" class="dashboard-stat-card__subtitle">{{ subtitle }}</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,6 +32,8 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
value: string
|
||||
subtitle?: string
|
||||
unit?: string
|
||||
help?: DashboardHelp
|
||||
icon?: string
|
||||
tone?: Tone
|
||||
@@ -37,6 +41,7 @@ const props = withDefaults(
|
||||
{
|
||||
icon: 'vuesax-linear:chart-2',
|
||||
tone: 'blue',
|
||||
unit: '',
|
||||
},
|
||||
)
|
||||
|
||||
@@ -109,6 +114,26 @@ const iconTextClass = computed(() => toneMap[props.tone].iconText)
|
||||
.dashboard-stat-card__icon--orange {
|
||||
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 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 { Balance } from '@/model/product'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
import { DASHBOARD_RIAL_UNIT, formatDashboardAmount } from '@/pages/dashboard/dashboardFormat'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
import { metalLabel } from '@/utils/adminMetalLabels'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
@@ -278,11 +279,12 @@ export function useDashboardPage() {
|
||||
const sood = finance.value?.fullPriceSood ?? 0
|
||||
const karmozd = finance.value?.fullPriceKarmozd ?? finance.value?.fullPriceKaermozd ?? 0
|
||||
const gifts = finance.value?.GiftsPrise ?? 0
|
||||
const total = finance.value?.totalTradingProfit ?? sood + karmozd
|
||||
|
||||
return {
|
||||
...chartSurfaceOptions(),
|
||||
chart: { type: 'donut', fontFamily: 'inherit', offsetY: 4, foreColor: chartSurfaceOptions().chart.foreColor },
|
||||
labels: ['سود معامله', metalLabel.goldCommission(), metalLabel.goldGifts()],
|
||||
labels: ['سود اسپرد', 'کارمزد معامله', 'ارزش هدایا (گرم)'],
|
||||
colors: ['#10b981', '#f59e0b', '#8b5cf6'],
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
@@ -300,15 +302,15 @@ export function useDashboardPage() {
|
||||
show: true,
|
||||
total: {
|
||||
show: true,
|
||||
label: 'درآمد خالص',
|
||||
formatter: () => usePrice(finance.value?.sumAll ?? 0, true, ''),
|
||||
label: 'جمع سود معاملات',
|
||||
formatter: () => `${formatDashboardAmount(total)} ${DASHBOARD_RIAL_UNIT}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
y: { formatter: (v: number) => usePrice(v, true, '') },
|
||||
y: { formatter: (v: number) => `${formatDashboardAmount(v)} ${DASHBOARD_RIAL_UNIT}` },
|
||||
},
|
||||
series: [sood, karmozd, gifts],
|
||||
}
|
||||
@@ -409,7 +411,7 @@ export function useDashboardPage() {
|
||||
},
|
||||
tooltip: {
|
||||
x: { format: tooltipDateFormat },
|
||||
y: { formatter: (v: number) => usePrice(v, true, '') },
|
||||
y: { formatter: (v: number) => `${formatDashboardAmount(v)} ${DASHBOARD_RIAL_UNIT}` },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
|
||||
@@ -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 = 'ریال'
|
||||
@@ -27,13 +27,17 @@ export const dashboardGateway = help(
|
||||
)
|
||||
|
||||
export const dashboardKpi = {
|
||||
profit: help(
|
||||
'مجموع سود اسپرد (تفاوت قیمت خرید/فروش با مشتری) روی همه سفارشهای ثبتشده در بازه.',
|
||||
'سود کل = جمع «سود هر سفارش»'
|
||||
spreadProfit: help(
|
||||
'مجموع سود اسپرد (اختلاف قیمت پایه و قیمت مشتری) — فقط از تفاوت buy/sell price. کارمزد جداگانه در کارت «کارمزد معامله» است.',
|
||||
'سود اسپرد = جمع «سود کل» (total_fee) هر سفارش در بازه'
|
||||
),
|
||||
commission: help(
|
||||
'مجموع کارمزد طلایی محصول که روی همان سفارشها محاسبه شده.',
|
||||
'کارمزد کل = جمع «کارمزد هر سفارش»'
|
||||
'مجموع کارمزد ریالی معاملات (درصد/وزن کارمزد) — جدا از سود اسپرد.',
|
||||
'کارمزد = جمع service_charge_price هر سفارش در بازه'
|
||||
),
|
||||
totalTradingProfit: help(
|
||||
'مجموع سود اسپرد و کارمزد — همان ردیف «جمع» در خلاصه درآمد.',
|
||||
'جمع = سود اسپرد + کارمزد معاملات'
|
||||
),
|
||||
orderCount: help(
|
||||
'تعداد همه سفارشهایی که در بازه انتخابی ثبت شدهاند (هر وضعیتی).',
|
||||
@@ -53,6 +57,11 @@ export const dashboardKpi = {
|
||||
),
|
||||
} as const satisfies Record<string, DashboardHelp>
|
||||
|
||||
export const dashboardTradingIncomeSummary = help(
|
||||
'تفکیک درآمد از معاملات در بازه انتخابی: سود اسپرد (اختلاف قیمت) و کارمزد ریالی جدا، و جمع هر دو.',
|
||||
'جمع سود معاملات = سود اسپرد + کارمزد معاملات'
|
||||
)
|
||||
|
||||
export const dashboardDateRange = help(
|
||||
'تاریخ شروع و پایان برای محاسبه کارتها، تراز و نمودارها.',
|
||||
'گزارش = از تاریخ … تا تاریخ …'
|
||||
@@ -64,8 +73,12 @@ export const dashboardDateMetrics = {
|
||||
'جمع مبلغ واریزهای تأییدشده از درگاه'
|
||||
),
|
||||
netIncome: help(
|
||||
'درآمد پس از کسر ارزش طلای هدیهدادهشده.',
|
||||
'درآمد خالص = (اختلاف قیمت اصلی و پرداختی) − (گرم هدیه × مظنه)'
|
||||
'شاخص قدیمی: اختلاف قیمت اصلی و پرداختی منهای ارزش هدایا — با «جمع سود معاملات» یکی نیست.',
|
||||
'تراز قیمت−پرداخت = جمع |قیمت اصلی − پرداختی| − (گرم هدیه × مظنه)'
|
||||
),
|
||||
totalTradingProfit: help(
|
||||
'همان «جمع سود معاملات» در خلاصه بالا — اسپرد + کارمزد.',
|
||||
'جمع = fullPriceSood + fullPriceKarmozd'
|
||||
),
|
||||
gifts: help(
|
||||
'طلایی که ادمین به مشتریان هدیه داده در همین بازه.',
|
||||
@@ -75,8 +88,8 @@ export const dashboardDateMetrics = {
|
||||
|
||||
export const dashboardCharts = {
|
||||
incomeMix: help(
|
||||
'نشان میدهد سود معامله، کارمزد و هدایا چقدر از درآمد کل را تشکیل میدهند.',
|
||||
'درآمد = سود معامله + کارمزد + (گرم هدیه × مظنه)'
|
||||
'تفکیک درآمد: سود اسپرد (اختلاف قیمت)، کارمزد ریالی، و ارزش هدایا.',
|
||||
'درآمد = سود اسپرد + کارمزد + (گرم هدیه × مظنه)'
|
||||
),
|
||||
buySellVolume: help(
|
||||
'مقدار طلای خرید و فروش مشتری در هر روز (فقط سفارش موفق).',
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
label-class="text-white/80"
|
||||
/>
|
||||
<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-[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>
|
||||
<p v-if="master?.update_date" class="mt-1 text-[10px] leading-none text-white/55">
|
||||
{{ toJalali(master.update_date) }}
|
||||
@@ -45,45 +45,33 @@
|
||||
|
||||
<DashboardQuickWalletModal v-model:visible="showQuickWallet" />
|
||||
|
||||
<!-- KPI -->
|
||||
<section class="mt-2 grid grid-cols-2 items-stretch gap-2 md:grid-cols-3 xl:grid-cols-6">
|
||||
<DashboardStatCard
|
||||
tone="gold"
|
||||
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"
|
||||
/>
|
||||
<DashboardTradingIncomeSummary :finance="finance" />
|
||||
|
||||
<!-- تراز و آمار — ۴ کارت در یک ردیف -->
|
||||
<section class="dashboard-metrics-row mt-2">
|
||||
<DashboardStatCard
|
||||
tone="green"
|
||||
icon="vuesax-linear:receipt-2"
|
||||
title="تعداد سفارش"
|
||||
subtitle="ثبتشده در بازه"
|
||||
:value="String(finance?.orderCount ?? 0)"
|
||||
:help="copy.kpi.orderCount"
|
||||
/>
|
||||
<template v-if="metalBalanceItems.length">
|
||||
<DashboardStatCard
|
||||
v-for="item in metalBalanceItems"
|
||||
:key="item.metal_id"
|
||||
tone="blue"
|
||||
icon="vuesax-linear:empty-wallet"
|
||||
:title="metalBalanceTitle(item.metal_id, item.label)"
|
||||
:value="formatGram(item.goldBalance, item.metal_id)"
|
||||
:help="copy.kpi.goldBalance"
|
||||
/>
|
||||
</template>
|
||||
<DashboardStatCard
|
||||
v-if="metalBalanceItems.length === 1"
|
||||
tone="blue"
|
||||
icon="vuesax-linear:empty-wallet"
|
||||
:title="metalBalanceTitle(metalBalanceItems[0].metal_id, metalBalanceItems[0].label)"
|
||||
subtitle="تراز وزن — سفارش موفق"
|
||||
:value="formatGram(metalBalanceItems[0].goldBalance, metalBalanceItems[0].metal_id)"
|
||||
:help="copy.kpi.goldBalance"
|
||||
/>
|
||||
<DashboardStatCard
|
||||
v-else
|
||||
tone="blue"
|
||||
icon="vuesax-linear:empty-wallet"
|
||||
:title="metalLabel.goldBalance()"
|
||||
subtitle="تراز وزن — سفارش موفق"
|
||||
:value="formatGram(balance?.goldBalance)"
|
||||
:help="copy.kpi.goldBalance"
|
||||
/>
|
||||
@@ -91,22 +79,26 @@
|
||||
tone="rose"
|
||||
icon="vuesax-linear:dollar-circle"
|
||||
title="تراز ریالی"
|
||||
:value="usePrice(balance?.cashBalance ?? 0, true, '')"
|
||||
subtitle="ورود/خروج نقد"
|
||||
:value="formatDashboardAmount(balance?.cashBalance)"
|
||||
:unit="DASHBOARD_RIAL_UNIT"
|
||||
:help="copy.kpi.cashBalance"
|
||||
/>
|
||||
<DashboardStatCard
|
||||
tone="violet"
|
||||
icon="vuesax-linear:chart-2"
|
||||
title="تراز مظنه"
|
||||
:value="usePrice(balance?.mazanehBalance ?? 0, true, '')"
|
||||
subtitle="میانگین موزون"
|
||||
:value="formatDashboardAmount(balance?.mazanehBalance)"
|
||||
:unit="DASHBOARD_RIAL_UNIT"
|
||||
:help="copy.kpi.mazanehBalance"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<!-- Date filter -->
|
||||
<!-- بازه گزارش -->
|
||||
<section class="dashboard-panel mt-2">
|
||||
<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
|
||||
v-for="metric in dateRangeMetrics"
|
||||
:key="metric.label"
|
||||
@@ -149,7 +141,7 @@
|
||||
<DashboardSectionHeader title="ترکیب درآمد" :help="copy.charts.incomeMix" />
|
||||
<div class="dashboard-chart-wrap">
|
||||
<apexchart
|
||||
v-if="(finance?.fullPriceSood ?? 0) + (finance?.fullPriceKarmozd ?? 0) > 0"
|
||||
v-if="(finance?.fullPriceSood ?? 0) + (finance?.fullPriceKarmozd ?? 0) + (finance?.GiftsPrise ?? 0) > 0"
|
||||
type="donut"
|
||||
height="220"
|
||||
: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>
|
||||
</tr>
|
||||
@@ -369,13 +362,16 @@
|
||||
</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">{{ 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_original ?? 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">
|
||||
{{ usePrice(order.fee ?? 0, true, '') }}
|
||||
{{ usePrice(order.fee ?? 0, true, 'ریال') }}
|
||||
</td>
|
||||
<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 class="px-2 py-2.5">
|
||||
<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 DashboardSectionHeader from '@/pages/dashboard/components/DashboardSectionHeader.vue'
|
||||
import DashboardStatCard from '@/pages/dashboard/components/DashboardStatCard.vue'
|
||||
import DashboardTradingIncomeSummary from '@/pages/dashboard/components/DashboardTradingIncomeSummary.vue'
|
||||
import {
|
||||
dashboardCharts,
|
||||
dashboardCustomerMetrics,
|
||||
@@ -462,6 +459,7 @@ import {
|
||||
dashboardWallet,
|
||||
dashboardWalletMetrics,
|
||||
} from '@/pages/dashboard/dashboardSectionCopy'
|
||||
import { DASHBOARD_RIAL_UNIT, formatDashboardAmount } from '@/pages/dashboard/dashboardFormat'
|
||||
import { useDashboardPage } from '@/pages/dashboard/composables/useDashboardPage'
|
||||
import { useDashboardQuickActions } from '@/pages/dashboard/composables/useDashboardQuickActions'
|
||||
import { metalBalanceTitle, metalLabel } from '@/utils/adminMetalLabels'
|
||||
@@ -523,6 +521,12 @@ const {
|
||||
customerStats,
|
||||
} = 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 {
|
||||
quickActionLoading,
|
||||
hasProductSnapshot,
|
||||
@@ -543,16 +547,21 @@ void loadSnapshotStatus()
|
||||
const dateRangeMetrics = computed(() => [
|
||||
{
|
||||
label: 'واریز بانکی',
|
||||
value: usePrice(balance.value?.income_money ?? 0),
|
||||
value: `${formatDashboardAmount(balance.value?.income_money)} ${DASHBOARD_RIAL_UNIT}`,
|
||||
help: dashboardDateMetrics.bankDeposit,
|
||||
},
|
||||
{
|
||||
label: 'درآمد خالص',
|
||||
value: usePrice(finance.value?.sumAll ?? 0),
|
||||
label: 'جمع سود معاملات',
|
||||
value: `${formatDashboardAmount(tradingProfitTotal.value)} ${DASHBOARD_RIAL_UNIT}`,
|
||||
help: dashboardDateMetrics.totalTradingProfit,
|
||||
},
|
||||
{
|
||||
label: 'تراز قیمت−پرداخت (پس از هدیه)',
|
||||
value: `${formatDashboardAmount(finance.value?.sumAll)} ${DASHBOARD_RIAL_UNIT}`,
|
||||
help: dashboardDateMetrics.netIncome,
|
||||
},
|
||||
{
|
||||
label: 'هدایا',
|
||||
label: 'هدایا (گرم)',
|
||||
value: `${finance.value?.Gifts ?? '0'} گرم`,
|
||||
help: dashboardDateMetrics.gifts,
|
||||
},
|
||||
@@ -594,13 +603,13 @@ const walletMetricItems = computed(() => [
|
||||
},
|
||||
{
|
||||
label: 'ریال مشتریان',
|
||||
value: usePrice(walletSums.value?.cash ?? 0, true, ''),
|
||||
value: `${formatDashboardAmount(walletSums.value?.cash)} ${DASHBOARD_RIAL_UNIT}`,
|
||||
help: dashboardWalletMetrics.cash,
|
||||
variant: 'sky',
|
||||
},
|
||||
{
|
||||
label: metalLabel.goldValueInRial(),
|
||||
value: usePrice(walletSums.value?.goldToCash ?? 0, true, ''),
|
||||
value: `${formatDashboardAmount(walletSums.value?.goldToCash)} ${DASHBOARD_RIAL_UNIT}`,
|
||||
help: dashboardWalletMetrics.goldToCash,
|
||||
variant: 'violet',
|
||||
},
|
||||
@@ -616,6 +625,24 @@ const walletMetricItems = computed(() => [
|
||||
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 {
|
||||
color: var(--admin-text);
|
||||
}
|
||||
@@ -630,6 +657,10 @@ const walletMetricItems = computed(() => [
|
||||
|
||||
.dashboard-inline-metric {
|
||||
background: var(--admin-bg-muted);
|
||||
min-height: 3.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dashboard-profit {
|
||||
|
||||
@@ -90,6 +90,7 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
||||
text_rule: 'longtext',
|
||||
text_about: 'longtext',
|
||||
fee_service_charge_percentage: 'number',
|
||||
fee_deduct_from_gold: 'switch',
|
||||
fee_single: 'price',
|
||||
buy_fee: 'price',
|
||||
sell_fee: 'price',
|
||||
@@ -285,6 +286,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
'مبلغ کسر (ریال) از قیمت پایه برای سمت فروش. اگر خالی بگذارید، از «اختلاف پایه» استفاده میشود.',
|
||||
fee_service_charge_percentage:
|
||||
'درصد کارمزد هر معامله از مبلغ سفارش (اعشاری؛ مثلاً ۰٫۰۰۵ یعنی نیم در هزار).',
|
||||
fee_deduct_from_gold:
|
||||
'روشن: کارمزد از مقدار طلا کسر میشود — در فروش مبلغ ریالی دقیق (مثلاً ۱۰۰ میلیون) واریز میشود و کارمزد از طلای کیف کم میشود. خاموش (پیشفرض): کارمزد از ریال فروش کم میشود — رفتار قبلی.',
|
||||
fee_free:
|
||||
'تعداد اولین سفارشهای هر کاربر که از کارمزد معاف میشوند. صفر یعنی غیرفعال.',
|
||||
min_withdraw:
|
||||
@@ -309,6 +312,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
'روشن: پس از ثبتنام کاربر جدید، پیامک اطلاعرسانی برای معرف ارسال میشود.',
|
||||
register_referral_bonus_message:
|
||||
'روشن: متن پاداش معرف در پیامک خوشآمد ثبتنام گنجانده میشود.',
|
||||
referral_gift_on_first_order:
|
||||
'روشن: پاداش معرف و معرفشونده پس از اولین سفارش واریز میشود. خاموش: واریز هنگام ثبتنام/تأیید احراز (رفتار قبلی).',
|
||||
referral_limit:
|
||||
'حداکثر تعداد دعوت مجاز برای هر کاربر. خالی = بدون محدودیت.',
|
||||
referral_bonus_amount:
|
||||
@@ -423,11 +428,13 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
||||
buy_fee: 'اضافه قیمت خرید',
|
||||
sell_fee: 'کسر قیمت فروش',
|
||||
fee_service_charge_percentage: 'درصد کارمزد معامله',
|
||||
fee_deduct_from_gold: 'کارمزد از مقدار طلا',
|
||||
fee_free: 'معافیت کارمزد سفارش اول',
|
||||
min_withdraw: 'حداقل مبلغ برداشت',
|
||||
min_first_buy: 'حداقل اولین خرید',
|
||||
minimum_sell: 'حداقل مقدار فروش',
|
||||
referral_id: 'کد معرف اجباری',
|
||||
referral_gift_on_first_order: 'پاداش معرف پس از اولین سفارش',
|
||||
open_register: 'ثبتنام باز',
|
||||
auto_active_customer: 'فعالسازی خودکار پس از ثبتنام',
|
||||
balance_report_exclude_customer_id: 'حذف مشتری از گزارش تراز',
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<BasicActionSheet
|
||||
v-model:visible="addUserVisible"
|
||||
:title="`${editId ? 'ویرایش' : 'افزودن'} کاربر`"
|
||||
modal-width="40rem"
|
||||
modal-width="55rem"
|
||||
:loading="loading"
|
||||
:showButton="true"
|
||||
:showCancel="true"
|
||||
@@ -29,47 +29,77 @@
|
||||
>
|
||||
<div v-if="isMobile" class="mb-3 text-base font-700">{{ `${editId ? 'ویرایش' : 'افزودن'} کاربر` }}</div>
|
||||
<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()">
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<a-form-item name="fname" :rules="inputRules()">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="نام" :help="fnameHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.fname" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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">
|
||||
<a-form-item name="phone_number">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="شماره تلفن" :help="phoneNumberHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.phone_number" type="number" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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
|
||||
v-model:value="form.type"
|
||||
:items="customerItems"
|
||||
@@ -77,10 +107,16 @@
|
||||
keyName="key"
|
||||
/>
|
||||
</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" />
|
||||
</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" />
|
||||
</a-form-item>
|
||||
<!-- <a-form-item label="نام چاپی" name="print_name" :rules="inputRules()">
|
||||
@@ -314,6 +350,51 @@ import {
|
||||
walletChargeTitle
|
||||
} from './utils/walletChargeForm'
|
||||
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
|
||||
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>
|
||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||
<BasicShowContext title="لیست محصولات بایگانی شده">
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<div class="flex h-full min-h-0 min-w-0 max-w-full flex-col overflow-hidden py-2 pr-3">
|
||||
<DashboardLabelWithHelp
|
||||
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">
|
||||
<!-- <template #extraHeader>
|
||||
<BaseButton type="primary" size="small" class="flex" @click="emits('add')">
|
||||
<div>افزودن مشتری عمده</div>
|
||||
<BaseIcon icon="vuesax-linear:user-add" Size="16px" />
|
||||
</BaseButton>
|
||||
</template>-->
|
||||
<template #action="{ column, record }">
|
||||
<div class="gap-2 flex justify-center">
|
||||
<a-tooltip :key="record.is_archive" placement="bottom" class="cursor-pointer">
|
||||
<template #title>
|
||||
<span>{{ record.is_archive ? 'بیرون بردن از بایگانی' : 'بایگانی کردن' }}</span>
|
||||
</template>
|
||||
<a-popconfirm
|
||||
title="آیا مشتری را از بایگانی خارج می کنید؟"
|
||||
ok-text="تایید"
|
||||
cancel-text="رد"
|
||||
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>
|
||||
<template #action="{ record }">
|
||||
<div class="gap-2 flex justify-center">
|
||||
<a-tooltip placement="bottom" class="cursor-pointer">
|
||||
<template #title>
|
||||
<span>بیرون بردن از بایگانی</span>
|
||||
</template>
|
||||
<a-popconfirm
|
||||
title="آیا این محصول از بایگانی خارج شود؟"
|
||||
ok-text="تایید"
|
||||
cancel-text="رد"
|
||||
placement="topLeft"
|
||||
@confirm="archiveFunction(record.id)"
|
||||
>
|
||||
<BaseIcon 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)">
|
||||
<template #title>
|
||||
<span>مشخصات</span>
|
||||
</template>
|
||||
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
||||
</a-tooltip>
|
||||
<!-- <a-tooltip
|
||||
placement="bottom"
|
||||
class="cursor-pointer"
|
||||
@click="emits('delete', record.id)"
|
||||
>
|
||||
<template #title>
|
||||
<span>حذف</span>
|
||||
</template>
|
||||
<Icon icon="vuesax-linear:trash" color="#EB3535" />
|
||||
</a-tooltip>-->
|
||||
</div>
|
||||
</template>
|
||||
</BaseTable>
|
||||
<ArchiveMobileCardList
|
||||
v-else
|
||||
ref="mobileListRef"
|
||||
class="min-h-0 flex-1"
|
||||
@info="emits('info', $event)"
|
||||
@unarchive="archiveFunction($event.id)"
|
||||
/>
|
||||
</div>
|
||||
</BasicShowContext>
|
||||
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)">
|
||||
<template #title>
|
||||
<span>مشخصات</span>
|
||||
</template>
|
||||
<BaseIcon icon="vuesax-linear:stickynote" color="#10913A" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</BaseTable>
|
||||
<ArchiveMobileCardList
|
||||
v-else
|
||||
ref="mobileListRef"
|
||||
class="min-h-0 flex-1"
|
||||
@info="emits('info', $event)"
|
||||
@unarchive="archiveFunction($event.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="tsx" setup>
|
||||
<script setup lang="ts">
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
import useBaseTable from '@core/components/BaseTableOld/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 useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||
import { message } from 'ant-design-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 type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
|
||||
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
|
||||
|
||||
const [submit, reload] = useBaseTable()
|
||||
/*const select = (val) => {
|
||||
val.sts === 1 ? return ['1'] : ['2']
|
||||
}*/
|
||||
//emits
|
||||
const emits = defineEmits(['delete', 'edit', 'add', 'info', 'charge'])
|
||||
|
||||
const { prefixCls } = usePrefix('pophoverstatus')
|
||||
|
||||
//permission
|
||||
function checkPermission(temp: string) {
|
||||
const store = useUserStore()
|
||||
return store.canAccess(temp)
|
||||
const productArchiveHelp: DashboardHelp = {
|
||||
description:
|
||||
'محصولات بایگانیشده از لیست اصلی حذف میشوند. هنگام بایگانی، نمایش (show)، خرید (open_buy) و فروش (open_sell) غیرفعال میشود.\n\n' +
|
||||
'در این صفحه فقط میتوانید مشخصات را ببینید یا محصول را از بایگانی خارج کنید. تغییر نمایش/خرید/فروش از اینجا ممکن نیست.',
|
||||
formula:
|
||||
'بعد از «خروج از بایگانی» محصول به لیست اصلی برمیگردد؛ وضعیت خرید/فروش/نمایش همانطور که موقع بایگانی بسته شده بود میماند تا خودتان در لیست محصولات دوباره روشن کنید.',
|
||||
}
|
||||
|
||||
//define table
|
||||
const table = ref(null)
|
||||
const [submit, reload] = useBaseTable()
|
||||
const emits = defineEmits<{ (e: 'info', record: unknown): void }>()
|
||||
|
||||
const tblProps = ref<any>({
|
||||
columns: cols,
|
||||
api: (params: TableInput) => {
|
||||
return getProductArchiveTable(params)
|
||||
}
|
||||
//bordered: true
|
||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||
|
||||
const tblProps = ref({
|
||||
columns: getProductArchiveColumns(),
|
||||
api: (params: TableInput) => getProductArchiveTable(params),
|
||||
})
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
async function archiveFunction(id: number) {
|
||||
try {
|
||||
loading.value = true
|
||||
await changeProductArchive(id)
|
||||
await Promise.allSettled([reload(), mobileListRef.value?.fetch?.() as Promise<void>])
|
||||
message.success('محصول با موفقیت از بایگانی خارج شد.')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
message.error('خروج از بایگانی انجام نشد.')
|
||||
}
|
||||
}
|
||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||
|
||||
const fetch = async () => {
|
||||
await Promise.allSettled([reload(), mobileListRef.value?.fetch?.() as Promise<void>])
|
||||
}
|
||||
// const spinTable = (val: boolean) => {
|
||||
// // console.log('sss')
|
||||
// table?.value?.loading.value = val
|
||||
// }
|
||||
defineExpose({
|
||||
fetch
|
||||
// spinTable
|
||||
})
|
||||
|
||||
defineExpose({ fetch })
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@prefix: ~'@{prefixCls}-pophoverstatus';
|
||||
|
||||
.@{prefix} {
|
||||
.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;*!
|
||||
}*/
|
||||
.ant-popover,
|
||||
.ant-tooltip {
|
||||
z-index: 1001 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,163 +1,127 @@
|
||||
import type { BaseTableColumn } from '@core/type/baseTable'
|
||||
import { toJalaliNoTime, toTime } from '@core/utils/useDataTime'
|
||||
import CustomTag from '@/components/CustomTag.vue'
|
||||
/*import BudgetSelect from '@/components/BudgetSelect.vue'
|
||||
import { useStaticApiListsStore } from '@/store/staticApisStore'
|
||||
import type { FilterSelect } from '@/model/filterSelect'
|
||||
import type { DocItem } from '@/model/docType'*/
|
||||
import { isStoreApp } from '@/utils/config'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
|
||||
//define store
|
||||
//const store = useStaticApiListsStore()
|
||||
//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
|
||||
}
|
||||
})*/
|
||||
function formatMoney(value: number | null | undefined) {
|
||||
return usePrice(value ?? 0, true, '').trim()
|
||||
}
|
||||
|
||||
export const cols: BaseTableColumn[] = [
|
||||
{
|
||||
title: 'ردیف',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
customRender: (record: any) => {
|
||||
return record.index + 1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'کد کاربری',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
type: 'Number'
|
||||
// className: 'column-money'
|
||||
},
|
||||
{
|
||||
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',
|
||||
function boolTag(value: boolean | number | undefined, onLabel: string, offLabel: string) {
|
||||
const on = Boolean(value)
|
||||
return (
|
||||
<CustomTag
|
||||
class="px-3 font-bold py-1"
|
||||
textColor={on ? '#65a30d' : '#dc2626'}
|
||||
bgColor={on ? '#ecfccb' : '#fee2e2'}
|
||||
>
|
||||
{on ? onLabel : offLabel}
|
||||
</CustomTag>
|
||||
)
|
||||
}
|
||||
|
||||
export function getProductArchiveColumns(): BaseTableColumn[] {
|
||||
const columns: BaseTableColumn[] = [
|
||||
{
|
||||
title: 'ردیف',
|
||||
dataIndex: 'id',
|
||||
key: 'row_index',
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
filterComponentProps: {
|
||||
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>
|
||||
}
|
||||
customRender: (record: { index: number }) => record.index + 1,
|
||||
},
|
||||
{
|
||||
title: 'حوزه',
|
||||
dataIndex: 'section',
|
||||
key: 'section'
|
||||
},
|
||||
{
|
||||
title: 'شماره موبایل',
|
||||
dataIndex: 'mobile',
|
||||
title: 'کد محصول',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
type: 'Number',
|
||||
key: 'mobile'
|
||||
},
|
||||
{
|
||||
title: 'تاریخ ثبت',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
type: 'DateTime',
|
||||
customRender: (record: any) => {
|
||||
return toJalaliNoTime(record.record.created_at)
|
||||
}
|
||||
},*/
|
||||
{
|
||||
title: 'نام',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
type: 'Text',
|
||||
},
|
||||
{
|
||||
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: 'عملیات',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
slot: 'action',
|
||||
fixed: 'right',
|
||||
filterable: false
|
||||
}
|
||||
]
|
||||
filterable: false,
|
||||
})
|
||||
|
||||
return columns
|
||||
}
|
||||
|
||||
/** @deprecated از getProductArchiveColumns استفاده کنید */
|
||||
export const cols = getProductArchiveColumns()
|
||||
|
||||
@@ -1,308 +1,23 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- <BasicMainCard>-->
|
||||
<div class="flex w-full h-full">
|
||||
<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 class="h-full min-h-0 min-w-0 overflow-hidden">
|
||||
<tbl ref="table" class="h-full w-full" @info="openInfo" />
|
||||
<ProductArchiveInfoSheet v-model:visible="infoVisible" :product="selectedProduct" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 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 type { Item } from '@/model/Item'
|
||||
import type { Customer } from '@/model/customer'
|
||||
import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer'
|
||||
import type { Charge } from '@/model/wallet'
|
||||
import customers from '@/router/routes/customers'
|
||||
import { chargeWallet } from '@/api/wallet'
|
||||
import { metalLabel } from '@/utils/adminMetalLabels'
|
||||
import type { Product } from '@/model/product'
|
||||
import tbl from './components/table.vue'
|
||||
import ProductArchiveInfoSheet from './components/ProductArchiveInfoSheet.vue'
|
||||
|
||||
//loading
|
||||
const loading = ref<boolean>(false)
|
||||
const table = ref<null | { fetch: () => void }>(null)
|
||||
const infoVisible = ref(false)
|
||||
const selectedProduct = ref<Product | null>(null)
|
||||
|
||||
//define store
|
||||
const store = useStaticApiListsStore()
|
||||
|
||||
//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
|
||||
}
|
||||
})
|
||||
function openInfo(product: Product) {
|
||||
selectedProduct.value = cloneDeep(product)
|
||||
infoVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
@@ -1,112 +1,39 @@
|
||||
<template>
|
||||
<Collapse class="grid grid-cols-1 gap-2" :activeKey="activeKey" @update:activeKey="$emit('update:activeKey', $event)">
|
||||
<BaseCollapse title="اطلاعات محصول" active-key="0" :actives="activeKey">
|
||||
<template #content>
|
||||
<a-form
|
||||
:model="form"
|
||||
ref="innerFormRef"
|
||||
autocomplete="off"
|
||||
scroll-to-first-error
|
||||
class="product-editor-form"
|
||||
>
|
||||
<div
|
||||
v-if="isMeltedCategory"
|
||||
class="md:flex flex-col gap-3 border-1 border-solid rounded-lg border-gray-200 p-3"
|
||||
>
|
||||
<div class="items-center">
|
||||
<span class="ml-4">مرجع قیمت :</span>
|
||||
<a-radio-group :value="meltedPriceOption" name="meltedPriceGroup" @update:value="$emit('update:meltedPriceOption', $event)">
|
||||
<a-radio value="app">قیمت اپ (صفحه اصلی)</a-radio>
|
||||
<a-radio value="fixed">ثابت</a-radio>
|
||||
<a-radio value="reference">مرجع متفاوت</a-radio>
|
||||
</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"
|
||||
/>
|
||||
<a-form
|
||||
:model="form"
|
||||
ref="innerFormRef"
|
||||
autocomplete="off"
|
||||
scroll-to-first-error
|
||||
class="product-editor-form"
|
||||
>
|
||||
<Collapse class="grid grid-cols-1 gap-2" :activeKey="activeKey" @update:activeKey="$emit('update:activeKey', $event)">
|
||||
<BaseCollapse title="قیمتگذاری" active-key="0" :actives="activeKey">
|
||||
<template #content>
|
||||
<div
|
||||
v-if="isMeltedCategory"
|
||||
class="product-editor-panel flex flex-col gap-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<DashboardLabelWithHelp label="مرجع قیمت" :help="meltedPriceSourceHelp" size="sm" />
|
||||
<a-radio-group :value="meltedPriceOption" name="meltedPriceGroup" @update:value="$emit('update:meltedPriceOption', $event)">
|
||||
<a-radio value="app">قیمت اپ</a-radio>
|
||||
<a-radio value="fixed">ثابت</a-radio>
|
||||
<a-radio value="reference">مرجع متفاوت</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<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 v-if="meltedPriceOption === 'fixed'" class="grid gap-4 md:grid-cols-3">
|
||||
<a-form-item name="price" class="!mb-0">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="قیمت ثابت" :help="fixedPriceHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput addon-before="ریال" type="price" v-model:value="form.price" />
|
||||
</a-form-item>
|
||||
</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 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>
|
||||
<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">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="مرجع قیمت" :help="priceReferenceHelp" size="sm" />
|
||||
</template>
|
||||
<BasicSelect
|
||||
class="price-reference-select"
|
||||
allowClear
|
||||
@@ -119,192 +46,313 @@
|
||||
keyName="id"
|
||||
v-model:value="form.reading_method"
|
||||
/>
|
||||
</div>
|
||||
<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-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 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" />
|
||||
<span class="mr-2 text-xs text-gray-500">
|
||||
اگر مرجع قیمت نداد (صفر)، با آخرین قیمت ذخیرهشده معامله شود
|
||||
</span>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 rounded-lg border border-solid border-blue-200 bg-blue-50 px-3 py-2">
|
||||
<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-x-3 gap-y-1 md:min-w-0 md:flex-1">
|
||||
<span class="shrink-0 text-sm font-600 text-gray-700">قیمت لحظهای:</span>
|
||||
<span v-if="livePricePreview.status === 'ready'" class="text-base font-700 text-blue-700">
|
||||
{{ usePrice(livePricePreview.amount) }}
|
||||
</span>
|
||||
<span v-else-if="livePricePreview.message" class="text-sm text-gray-500">
|
||||
{{ livePricePreview.message }}
|
||||
</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 v-if="!isMeltedCategory" class="product-editor-panel flex flex-col gap-4">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<DashboardLabelWithHelp label="دریافت قیمت از" :help="priceSourceHelp" size="sm" />
|
||||
<a-radio-group :value="methodOption" name="radioGroup" @update:value="$emit('update:methodOption', $event)">
|
||||
<a-radio value="1">دستی</a-radio>
|
||||
<a-radio value="2">اتوماتیک</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div v-if="methodOption === '1'" class="grid gap-4 md:grid-cols-3">
|
||||
<a-form-item name="price" class="!mb-0 md:col-span-2">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="قیمت وسط" :help="manualPriceHelp" size="sm" />
|
||||
</template>
|
||||
<div class="flex items-center gap-2">
|
||||
<BasicInput class="flex-1" addon-before="ریال" type="price" v-model:value="form.price" />
|
||||
<AButton class="border-green text-green shrink-0 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 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>
|
||||
<AButton size="small" class="shrink-0 self-end md:self-auto" @click="$emit('refreshPrices')">
|
||||
بروزرسانی
|
||||
</AButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isMeltedCategory"
|
||||
class="md:flex justify-between p-2 border-1 border-solid rounded-lg border-gray-200 mt-2"
|
||||
>
|
||||
<div>
|
||||
<span class="ml-4">وزن :</span>
|
||||
<a-radio-group :value="weightOption" name="radioGroup" @update:value="$emit('update:weightOption', $event)">
|
||||
<a-radio value="1">دارد</a-radio>
|
||||
<a-radio value="2">ندارد</a-radio>
|
||||
</a-radio-group>
|
||||
<div class="rounded-lg border border-solid border-blue-200 bg-blue-50 px-3 py-2">
|
||||
<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-x-3 gap-y-1 md:min-w-0 md:flex-1">
|
||||
<DashboardLabelWithHelp label="قیمت لحظهای" :help="livePriceHelp" size="sm" />
|
||||
<span v-if="livePricePreview.status === 'ready'" class="text-base font-700 text-blue-700">
|
||||
{{ usePrice(livePricePreview.amount) }}
|
||||
</span>
|
||||
<span v-else-if="livePricePreview.message" class="text-sm text-gray-500">
|
||||
{{ livePricePreview.message }}
|
||||
</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 v-if="weightOption === '1'" class="flex items-center md:col-span-2">
|
||||
<a-form-item
|
||||
class="!mb-0 w-full"
|
||||
:label="weightFieldLabel"
|
||||
name="amount"
|
||||
:rules="inputRulesNumber('وزن')"
|
||||
>
|
||||
<BasicInput v-model:value="form.amount" type="number" />
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
|
||||
<BaseCollapse title="مشخصات محصول" active-key="1" :actives="activeKey">
|
||||
<template #content>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<a-form-item name="title" :rules="inputRules()">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="عنوان" :help="titleHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.title" />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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()">
|
||||
<a-form-item name="group_type" :rules="inputRules()">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="دستهبندی" :help="categoryHelp" size="sm" />
|
||||
</template>
|
||||
<BasicSelect v-model:value="form.group_type" :items="categoryItems" valueName="value" keyName="key" />
|
||||
</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" />
|
||||
</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" />
|
||||
</a-form-item>
|
||||
|
||||
<template v-if="!isMeltedCategory">
|
||||
<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
|
||||
v-if="isMeltedCategory || weightOption === '1'"
|
||||
label="واحد"
|
||||
name="unit"
|
||||
:rules="showUnitField ? inputRules() : []"
|
||||
>
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="واحد" :help="unitHelp" size="sm" />
|
||||
</template>
|
||||
<BasicSelect v-model:value="form.unit" :items="unitItems" valueName="value" keyName="value" />
|
||||
</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
|
||||
v-if="isStore && !isMeltedCategory"
|
||||
label="تعداد موجودی"
|
||||
name="inventory"
|
||||
:rules="inputRules()"
|
||||
>
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="تعداد موجودی" :help="inventoryHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.inventory" type="number" />
|
||||
</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" />
|
||||
</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" />
|
||||
</a-form-item>
|
||||
<a-form-item label="ساعت کاری اختصاصی این محصول" name="override_trading_hours">
|
||||
<a-switch v-model:checked="form.override_trading_hours" />
|
||||
<div class="text-xs text-gray-500 mt-1 leading-6">
|
||||
روشن: این محصول ساعت شروع و پایان مخصوص خودش را دارد و ساعت کاری سراسری فروشگاه را نادیده میگیرد (override).
|
||||
خاموش: همان ساعت کاری سراسری فروشگاه برای این محصول اعمال میشود و زمانهای زیر نادیده گرفته میشوند.
|
||||
</div>
|
||||
<a-form-item name="show_in_assets">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="نمایش در داراییها" :help="showInAssetsHelp" size="sm" />
|
||||
</template>
|
||||
<a-switch v-model:checked="form.show_in_assets" />
|
||||
</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
|
||||
v-model:value="form.auto_open_at"
|
||||
:locale="customLocale"
|
||||
placeholder="انتخاب زمان"
|
||||
:disabled="!form.override_trading_hours"
|
||||
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
|
||||
class="w-full"
|
||||
/>
|
||||
</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
|
||||
v-model:value="form.auto_close_at"
|
||||
:locale="customLocale"
|
||||
placeholder="انتخاب زمان"
|
||||
:disabled="!form.override_trading_hours"
|
||||
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
|
||||
class="w-full"
|
||||
/>
|
||||
</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-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
|
||||
v-if="!isMeltedCategory && !editId"
|
||||
label="معامله مقداری"
|
||||
name="trade_by_quantity"
|
||||
>
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="معامله مقداری" :help="tradeByQuantityHelp" size="sm" />
|
||||
</template>
|
||||
<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
|
||||
v-else-if="!isMeltedCategory && editId"
|
||||
label="نوع معامله"
|
||||
>
|
||||
<div class="text-sm leading-6">
|
||||
{{ form.trade_by_quantity ? 'معامله مقداری (ارزش کل + مقدار)' : 'معامله عادی' }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1 leading-6">
|
||||
نوع معامله پس از ساخت محصول قابل تغییر نیست.
|
||||
<a-form-item v-else-if="!isMeltedCategory && editId">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="نوع معامله" :help="tradeByQuantityHelp" size="sm" />
|
||||
</template>
|
||||
<div class="text-sm leading-6 pt-1">
|
||||
{{ form.trade_by_quantity ? 'معامله مقداری' : 'معامله عادی' }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<template v-if="form.order_limit_enabled">
|
||||
<div class="col-span-2 text-xs text-gray-500 -mt-2 mb-1">
|
||||
حداقل یکی از «حداقل» یا «حداکثر» الزامی است؛ میتوانید فقط یکی را پر کنید.
|
||||
</div>
|
||||
<a-form-item label="حداقل مبلغ سفارش (ریال)" name="min_order_cash">
|
||||
<a-form-item name="min_order_cash">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="حداقل مبلغ سفارش" :help="minOrderHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.min_order_cash" addon-before="ریال" type="price" />
|
||||
</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" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
</div>
|
||||
</a-form>
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
|
||||
<BaseCollapse title="تصاویر محصول" active-key="1" :actives="activeKey">
|
||||
<template #content>
|
||||
<IndexImageUploader
|
||||
:key="editId ?? 'new-product'"
|
||||
:file-list="productIndexImage"
|
||||
@update:file-list="$emit('update:productIndexImage', $event)"
|
||||
@ok="(id, res) => $emit('uploadedIndexFile', id, res)"
|
||||
@delete="$emit('deleteIndexFile')"
|
||||
/>
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
</Collapse>
|
||||
<BaseCollapse title="تصاویر محصول" active-key="3" :actives="activeKey">
|
||||
<template #content>
|
||||
<IndexImageUploader
|
||||
:key="editId ?? 'new-product'"
|
||||
:file-list="productIndexImage"
|
||||
@update:file-list="$emit('update:productIndexImage', $event)"
|
||||
@ok="(id, res) => $emit('uploadedIndexFile', id, res)"
|
||||
@delete="$emit('deleteIndexFile')"
|
||||
/>
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
</Collapse>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -322,9 +370,124 @@ import { usePrice } from '@core/utils/usePrice'
|
||||
import { usesMeltedPriceEditor } from '@/utils/productCategories'
|
||||
import { computeProductEditorPricePreview } from '@/utils/productEditorPrice'
|
||||
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 { 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 props = defineProps<{
|
||||
@@ -351,6 +514,8 @@ const isStore = ref(Boolean(props.isStore))
|
||||
const tradeWeightUnit = ref('گرم')
|
||||
const isMeltedCategory = computed(() => usesMeltedPriceEditor(props.form))
|
||||
|
||||
const showDirectPriceField = computed(() => !isMeltedCategory.value && props.methodOption === '1')
|
||||
|
||||
const showUnitField = computed(() => isMeltedCategory.value || props.weightOption === '1')
|
||||
|
||||
const livePricePreview = computed(() =>
|
||||
@@ -441,6 +606,18 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
width: 100%;
|
||||
min-width: 22rem;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
@toggleSell="$emit('toggleSell', $event)"
|
||||
@toggleActive="$emit('toggleActive', $event)"
|
||||
@edit="$emit('edit', $event)"
|
||||
@archive="$emit('archive', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -42,6 +43,7 @@ defineEmits<{
|
||||
(e: 'toggleBuy', item: Product): void
|
||||
(e: 'toggleSell', item: Product): void
|
||||
(e: 'toggleActive', item: Product): void
|
||||
(e: 'archive', item: Product): void
|
||||
}>()
|
||||
|
||||
const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<Product>()
|
||||
|
||||
@@ -66,6 +66,16 @@
|
||||
<MobileDetailRow label="وزن" :value="row.amount ?? '-'" />
|
||||
<MobileDetailRow label="معامله مقداری" :value="row.trade_by_quantity ? 'بله' : 'خیر'" />
|
||||
</MobileInfoGrid>
|
||||
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="middle"
|
||||
type="default"
|
||||
icon="vuesax-linear:archive-tick"
|
||||
@click="$emit('archive', row)"
|
||||
>
|
||||
بایگانی
|
||||
</BaseButton>
|
||||
</template>
|
||||
</MobileDetailSheet>
|
||||
</template>
|
||||
@@ -93,6 +103,7 @@ const emit = defineEmits<{
|
||||
(e: 'toggleSell', item: Product): void
|
||||
(e: 'toggleActive', item: Product): void
|
||||
(e: 'edit', item: Product): void
|
||||
(e: 'archive', item: Product): void
|
||||
}>()
|
||||
|
||||
const visibleProxy = computed({
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 min-w-0 max-w-full flex-col overflow-hidden">
|
||||
<div class="shrink-0">
|
||||
<Collapse
|
||||
class="product-activation-collapse"
|
||||
:activeKey="activationActiveKey"
|
||||
@update:activeKey="activationActiveKey = $event as (string | number)[]"
|
||||
>
|
||||
<BaseCollapse title="فعال سازی محصولات" active-key="activation" :actives="activationActiveKey">
|
||||
<template #content>
|
||||
<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>
|
||||
<TimePicker
|
||||
v-model:value="startTime"
|
||||
:locale="customLocale"
|
||||
placeholder="انتخاب زمان"
|
||||
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
|
||||
/>
|
||||
</div>
|
||||
<div class="content-center md:mr-4">زمان پایان</div>
|
||||
<div>
|
||||
<TimePicker
|
||||
v-model:value="endTime"
|
||||
:locale="customLocale"
|
||||
placeholder="انتخاب زمان"
|
||||
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
|
||||
/>
|
||||
</div>
|
||||
<BaseButton class="md:mr-2" @click="setTimeActive(startTime, endTime)">ثبت</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
</Collapse>
|
||||
<div class="shrink-0 py-2 pr-3">
|
||||
<DashboardLabelWithHelp
|
||||
label="فعالسازی محصولات"
|
||||
:help="productActivationHelp"
|
||||
size="sm"
|
||||
text-class="font-bold text-[var(--admin-text)]"
|
||||
root-class="mb-3"
|
||||
/>
|
||||
<div class="flex flex-col gap-2 md:flex-row md:flex-wrap md:items-center">
|
||||
<div class="flex items-center gap-1.5 md:mr-4">
|
||||
<DashboardLabelWithHelp label="زمان شروع" :help="activationStartHelp" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<TimePicker
|
||||
v-model:value="startTime"
|
||||
:locale="customLocale"
|
||||
placeholder="انتخاب زمان"
|
||||
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 md:mr-4">
|
||||
<DashboardLabelWithHelp label="زمان پایان" :help="activationEndHelp" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<TimePicker
|
||||
v-model:value="endTime"
|
||||
:locale="customLocale"
|
||||
placeholder="انتخاب زمان"
|
||||
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
|
||||
/>
|
||||
</div>
|
||||
<BaseButton class="md:mr-2" @click="setTimeActive(startTime, endTime)">ثبت</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BasicShowContext title="لیست محصولات" class="min-h-0 flex-1">
|
||||
@@ -93,6 +94,18 @@
|
||||
<template #title><span>ویرایش</span></template>
|
||||
<BaseIcon icon="vuesax-linear:edit" color="#4572E9" />
|
||||
</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>
|
||||
</template>
|
||||
</BaseTable>
|
||||
@@ -105,6 +118,7 @@
|
||||
@toggleBuy="activeBuy"
|
||||
@toggleSell="activeSell"
|
||||
@toggleActive="activeProduct"
|
||||
@archive="handleArchive"
|
||||
/>
|
||||
</div>
|
||||
</BasicShowContext>
|
||||
@@ -112,13 +126,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Collapse, message, Modal, TimePicker } from 'ant-design-vue'
|
||||
import BaseCollapse from '@/components/BaseCollapse.vue'
|
||||
import { message, Modal, TimePicker } from 'ant-design-vue'
|
||||
import locale from 'ant-design-vue/es/date-picker/locale/fa_IR'
|
||||
import dayjs from 'dayjs'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
archiveProduct,
|
||||
changeActiveProduct,
|
||||
changeBuyActiveProduct,
|
||||
changeSellActiveProduct,
|
||||
@@ -127,6 +141,8 @@ import {
|
||||
} from '@/api/product'
|
||||
import { getProductColumns } from '@/pages/menuPages/products/base/hooks/productColumns'
|
||||
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 type { TableInput } from '@core/type/baseTable'
|
||||
import type { Product } from '@/model/product'
|
||||
@@ -134,11 +150,36 @@ import { isMobile } from '@core/utils/is'
|
||||
|
||||
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 emits = defineEmits(['edit', 'add'])
|
||||
|
||||
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 endTime = ref(dayjs('00:00:00', 'HH:mm:ss'))
|
||||
|
||||
@@ -195,6 +236,26 @@ const activeProduct = (product: Product) =>
|
||||
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) {
|
||||
try {
|
||||
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 {
|
||||
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>
|
||||
|
||||
@@ -409,8 +409,10 @@ async function submitProduct() {
|
||||
}
|
||||
|
||||
async function saveProduct() {
|
||||
if (!activeKey_.value.includes('0')) {
|
||||
activeKey_.value = ['0', ...activeKey_.value]
|
||||
const formSectionKeys = ['0', '1', '2']
|
||||
const nextKeys = [...new Set([...formSectionKeys, ...activeKey_.value])]
|
||||
if (nextKeys.length !== activeKey_.value.length) {
|
||||
activeKey_.value = nextKeys
|
||||
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>-->
|
||||
<!-- <!– <BasicTags-->
|
||||
<!-- :items="items"-->
|
||||
<!-- color="green"-->
|
||||
<!-- @add="add"-->
|
||||
<!-- @edit="edit"-->
|
||||
<!-- :key="refresh"-->
|
||||
<!-- :loading="loading"-->
|
||||
<!-- @delete="deleteTag"-->
|
||||
<!-- />–>-->
|
||||
<!-- </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 }">
|
||||
<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">
|
||||
<MobileStatCard bordered variant="success" label="موجودی ریالی" :value="usePrice(row.cash ?? 0, true, '')">
|
||||
<template #prefix>
|
||||
@@ -10,11 +20,34 @@
|
||||
</template>
|
||||
</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
|
||||
v-for="entry in metalEntries(row)"
|
||||
:key="entry.metal_id"
|
||||
bordered
|
||||
variant="warning"
|
||||
variant="neutral"
|
||||
:label="walletMetalBalanceTitle(entry.metal_id, entry.label)"
|
||||
:value="entry.display"
|
||||
>
|
||||
@@ -26,8 +59,26 @@
|
||||
|
||||
<MobileInfoGrid>
|
||||
<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?.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>
|
||||
</template>
|
||||
</MobileDetailSheet>
|
||||
@@ -44,6 +95,7 @@ import { isMobile } from '@core/utils/is'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
import { walletMetalBalanceTitle } from '@/utils/adminMetalLabels'
|
||||
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
|
||||
import { walletKindTagStyle, walletSheetTitle } from '@/utils/walletDisplay'
|
||||
import type { WalletMetalColumn } from '../hooks/walletColumns'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -61,7 +113,8 @@ const visibleProxy = computed({
|
||||
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) {
|
||||
const metals = props.metals?.length
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
<template>
|
||||
<MobileCard clickable @click="$emit('open', item)">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="min-w-0 truncate text-sm font-700 text-gray-900">{{ customerName }}</span>
|
||||
<span class="shrink-0 text-xs text-gray-400">#{{ item.id ?? '-' }}</span>
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<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 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 class="text-gray-500">ریال:</span>
|
||||
<span class="font-600 text-gray-900">{{ cashLabel }}</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
|
||||
v-if="metalEntries.length > 1"
|
||||
v-if="metalEntries.length"
|
||||
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">
|
||||
@@ -35,6 +43,7 @@ import type { Wallet } from '@/model/wallet'
|
||||
import MobileCard from '@/components/mobile/MobileCard.vue'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
|
||||
import { walletKindTagStyle } from '@/utils/walletDisplay'
|
||||
import type { WalletMetalColumn } from '../hooks/walletColumns'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -47,6 +56,9 @@ defineEmits<{
|
||||
}>()
|
||||
|
||||
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, ''))
|
||||
|
||||
function metalValue(metalId: number): number {
|
||||
|
||||
@@ -45,20 +45,14 @@
|
||||
</div>
|
||||
|
||||
<BaseTable v-if="!isMobile" class="min-h-0 min-w-0 flex-1" v-bind="tblProps" @register="submit" ref="tbl">
|
||||
<template #action="{ column, record }">
|
||||
<div class="gap-2 flex justify-center">
|
||||
<template #action="{ record }">
|
||||
<div class="flex justify-center gap-2">
|
||||
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)">
|
||||
<template #title>
|
||||
<span>مشخصات</span>
|
||||
<span>مشخصات کیف پول</span>
|
||||
</template>
|
||||
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
||||
</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>
|
||||
</template>
|
||||
</BaseTable>
|
||||
@@ -78,10 +72,8 @@
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
|
||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import { changeCustomerActivation } from '@/api/customer'
|
||||
import { buildWalletColumns } from '../hooks/walletColumns'
|
||||
import { usePrefix } from '@core/composable/usePrefix'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { getWalletTable } from '@/api/wallet'
|
||||
import { usePrice } from '../../../../core/utils/usePrice'
|
||||
import WalletMobileCardList from './WalletMobileCardList.vue'
|
||||
@@ -92,7 +84,7 @@ import { formatMetalDisplayValue, metalDisplayUnit } from '@/utils/metalWeight'
|
||||
|
||||
const [submit, reload] = useBaseTable()
|
||||
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')
|
||||
|
||||
@@ -133,7 +125,6 @@ const tblProps = ref<any>({
|
||||
return data
|
||||
}
|
||||
})
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
function walletSumTone(value?: number) {
|
||||
if (value == null || value >= 0) {
|
||||
@@ -147,19 +138,6 @@ function onMobileSums(payload: unknown) {
|
||||
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() {
|
||||
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 { metalLabel, walletMetalBalanceTitle } from '@/utils/adminMetalLabels'
|
||||
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
|
||||
import { walletKindTagStyle } from '@/utils/walletDisplay'
|
||||
|
||||
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[] {
|
||||
const baseColumns: BaseTableColumn[] = [
|
||||
{
|
||||
title: 'ردیف',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
key: 'row_index',
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
customRender: (record: any) => {
|
||||
return record.index + 1
|
||||
}
|
||||
customRender: (record: { index: number }) => record.index + 1
|
||||
},
|
||||
{
|
||||
title: 'کد کیف پول',
|
||||
@@ -23,6 +35,58 @@ export function buildWalletColumns(metals: WalletMetalColumn[] = []): BaseTableC
|
||||
key: 'id',
|
||||
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: 'نام مشتری',
|
||||
dataIndex: 'customer.name',
|
||||
@@ -30,7 +94,16 @@ export function buildWalletColumns(metals: WalletMetalColumn[] = []): BaseTableC
|
||||
type: 'Text'
|
||||
},
|
||||
{
|
||||
title: 'موجودی کیف پول (ریال)',
|
||||
title: 'موبایل',
|
||||
dataIndex: 'customer.mobile',
|
||||
key: 'customer.mobile',
|
||||
type: 'Text',
|
||||
customRender(data) {
|
||||
return data.record.customer?.mobile || '—'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'موجودی ریالی',
|
||||
dataIndex: 'cash',
|
||||
key: 'cash',
|
||||
type: 'Text',
|
||||
@@ -40,34 +113,53 @@ export function buildWalletColumns(metals: WalletMetalColumn[] = []): BaseTableC
|
||||
}
|
||||
]
|
||||
|
||||
if (metals.length === 0) {
|
||||
return [
|
||||
...baseColumns,
|
||||
{
|
||||
title: metalLabel.goldWalletBalanceGram(),
|
||||
dataIndex: 'gold',
|
||||
key: 'gold',
|
||||
type: 'Text',
|
||||
customRender(data) {
|
||||
const value = Number(data.record?.gold ?? 0)
|
||||
return formatMetalDisplayValue(value, 1)
|
||||
}
|
||||
const metalColumns: BaseTableColumn[] =
|
||||
metals.length === 0
|
||||
? [
|
||||
{
|
||||
title: metalLabel.goldWalletBalanceGram(),
|
||||
dataIndex: 'gold',
|
||||
key: 'gold',
|
||||
type: 'Text',
|
||||
customRender(data) {
|
||||
const value = Number(data.record?.gold ?? 0)
|
||||
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[] {
|
||||
|
||||
@@ -2,167 +2,60 @@
|
||||
<div class="h-full min-h-0">
|
||||
<BasicMainCard>
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<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="gap-5 text-xs">
|
||||
<div>مجموع موجودی کیف پول:</div>
|
||||
<div class="font-bold">{{ usePrice(sums?.cash ?? 0) }}</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>
|
||||
<div class="shrink-0 md:hidden">
|
||||
<div class="grid grid-cols-3 gap-1 text-xs">
|
||||
<div class="rounded bg-blue-600 p-1 text-white">مجموع</div>
|
||||
<div class="rounded bg-blue-600 p-1 text-white">ریال</div>
|
||||
<div class="rounded bg-blue-600 p-1 text-white">وزن</div>
|
||||
|
||||
<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="text-white rounded p-1"
|
||||
:class="sumToneClass(metal.goldToCash)"
|
||||
>
|
||||
{{ usePrice(metal.goldToCash ?? 0, true, '') }}
|
||||
</div>
|
||||
<div
|
||||
class="text-white rounded p-1"
|
||||
:class="sumToneClass(metal.gold)"
|
||||
>
|
||||
{{ formatMetalDisplayValue(metal.gold ?? 0, metal.metal_id) }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-for="metal in mobileMetalRows" :key="`metal-${metal.metal_id}`">
|
||||
<div class="rounded bg-blue-600 p-1 text-white">{{ metal.label }}</div>
|
||||
<div class="rounded p-1 text-white" :class="sumToneClass(metal.goldToCash)">
|
||||
{{ usePrice(metal.goldToCash ?? 0, true, '') }}
|
||||
</div>
|
||||
<div class="rounded p-1 text-white" :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="text-white rounded p-1"
|
||||
:class="sumToneClass(sums?.cash)"
|
||||
>
|
||||
{{ usePrice(sums?.cash ?? 0, true, '') }}
|
||||
</div>
|
||||
<div
|
||||
class="text-white rounded p-1"
|
||||
:class="sumToneClass(sums?.cashToGold)"
|
||||
>
|
||||
{{ usePrice(sums?.cashToGold ?? 0, true, '') }}
|
||||
<div class="rounded bg-blue-600 p-1 text-white">کیف پول</div>
|
||||
<div class="rounded p-1 text-white" :class="sumToneClass(sums?.cash)">
|
||||
{{ usePrice(sums?.cash ?? 0, true, '') }}
|
||||
</div>
|
||||
<div class="rounded p-1 text-white" :class="sumToneClass(sums?.cashToGold)">
|
||||
{{ usePrice(sums?.cashToGold ?? 0, true, '') }}
|
||||
</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" />
|
||||
<component :is="selectedComponent" class="mr-5" />-->
|
||||
<!-- <BasicShowContext title="پروژه ها"> <Search /> </BasicShowContext>-->
|
||||
</div>
|
||||
<div class="flex min-h-0 flex-1 flex-col">
|
||||
<tbl ref="table" class="h-full min-h-0 w-full" @info="openWalletInfo" @sums="setSums" />
|
||||
</div>
|
||||
</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>
|
||||
<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"
|
||||
|
||||
<WalletInfoSheet
|
||||
v-model:visible="infoVisible"
|
||||
:wallet="selectedWallet"
|
||||
:metals="catalogMetals"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 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, sum } from 'lodash'
|
||||
import type { Item } from '@/model/Item'
|
||||
import type { Customer } from '@/model/customer'
|
||||
import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer'
|
||||
import WalletInfoSheet from './components/WalletInfoSheet.vue'
|
||||
import type { Wallet, WalletTableSums } from '@/model/wallet'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
import type { WalletTableSums } from '@/model/wallet'
|
||||
import { formatMetalDisplayValue } from '@/utils/metalWeight'
|
||||
import type { WalletMetalColumn } from './hooks/walletColumns'
|
||||
|
||||
//loading
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
//define store
|
||||
const store = useStaticApiListsStore()
|
||||
|
||||
const table = ref<null | { fetch: () => Promise<void> }>(null)
|
||||
const sums = ref<WalletTableSums>()
|
||||
const infoVisible = ref(false)
|
||||
const selectedWallet = ref<Wallet | null>(null)
|
||||
const catalogMetals = ref<WalletMetalColumn[]>([])
|
||||
|
||||
const mobileMetalRows = computed(() => {
|
||||
if (sums.value?.metalBalances?.length) {
|
||||
@@ -195,158 +88,18 @@ function sumToneClass(value?: number) {
|
||||
return 'bg-red-600'
|
||||
}
|
||||
|
||||
//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
|
||||
}
|
||||
]
|
||||
|
||||
//form
|
||||
const form = ref<Customer>({})
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
//info
|
||||
let infoParams = reactive<Info>({})
|
||||
|
||||
function setSums(val) {
|
||||
function setSums(val: WalletTableSums) {
|
||||
sums.value = val
|
||||
catalogMetals.value =
|
||||
val?.metalBalances?.map((metal) => ({
|
||||
metal_id: metal.metal_id,
|
||||
label: metal.label
|
||||
})) ?? []
|
||||
}
|
||||
|
||||
function info(val: Customer) {
|
||||
infoParams.name = val.name
|
||||
infoParams.type = val.mobile
|
||||
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
|
||||
}
|
||||
})
|
||||
function openWalletInfo(wallet: Wallet) {
|
||||
selectedWallet.value = wallet
|
||||
infoVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<span class="min-w-0 truncate">{{ sheetTitle }}</span>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<MobileIconButton
|
||||
v-if="!row.tracked_number"
|
||||
v-if="needsTrackingConfirmation(row)"
|
||||
plain
|
||||
icon="vuesax-linear:tick-circle"
|
||||
color="#16A34A"
|
||||
@@ -120,6 +120,7 @@ import { status } from '@/pages/menuPages/withdraw/information/hooks/status'
|
||||
import {
|
||||
formatWithdrawAmountDisplay,
|
||||
isMoneyWithdraw,
|
||||
needsTrackingConfirmation,
|
||||
withdrawAmountValue,
|
||||
withdrawTypeLabel
|
||||
} from '@/pages/menuPages/withdraw/utils/withdrawDisplay'
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
</template>
|
||||
<template #tracked="{ column, record }">
|
||||
<div v-if="record.tracked_number">{{ record.tracked_number }}</div>
|
||||
<div v-else>
|
||||
<BaseButton @click="emits('withdraw', record)">{{ confirmButtonLabel }}</BaseButton>
|
||||
<div v-else-if="needsTrackingConfirmation(record)">
|
||||
<BaseButton @click="emits('withdraw', record)">
|
||||
{{ confirmLabelForRecord(record, props.flowType) }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div v-else>-</div>
|
||||
</template>
|
||||
<template #action="{ column, record }">
|
||||
<div class="gap-2 flex justify-center">
|
||||
@@ -114,6 +117,10 @@ import {getWithdrawTable, updateSts} from '@/api/wallet'
|
||||
import WithdrawMobileCardList from './WithdrawMobileCardList.vue'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import type { WithdrawFlowType } from '../types'
|
||||
import {
|
||||
confirmLabelForRecord,
|
||||
needsTrackingConfirmation
|
||||
} from '@/pages/menuPages/withdraw/utils/withdrawDisplay'
|
||||
import TahesabRetryModal from '@/components/TahesabRetryModal.vue'
|
||||
import { initTahesabSettingFromApi, tahesabEnabledSetting } from '@/utils/config'
|
||||
|
||||
@@ -149,10 +156,6 @@ function checkPermission(temp: string) {
|
||||
const table = ref(null)
|
||||
const lastTableParams = ref<TableInput & { flow_type?: string }>({})
|
||||
|
||||
const confirmButtonLabel = computed(() =>
|
||||
props.flowType === 'deposit' ? 'تایید واریز' : 'تایید برداشت'
|
||||
)
|
||||
|
||||
const tblProps = computed(() => ({
|
||||
columns: getWithdrawColumns(),
|
||||
api: async (params: TableInput) => {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<BasicModal
|
||||
v-model:visible="confirmWithdrawVisible"
|
||||
width="15rem"
|
||||
title="تایید برداشت"
|
||||
:title="confirmModalTitle"
|
||||
wrap-class-name="withdraw-stack-modal"
|
||||
@ok="okWithdraw"
|
||||
:loading="loading"
|
||||
@@ -117,6 +117,7 @@ import type { Wallet, Withdraw } from '@/model/wallet'
|
||||
import {completeWithdraw, exportWithdrawReport, getWithdrawById, sendWeights} from '@/api/wallet'
|
||||
import Button from "@core/components/BaseButton";
|
||||
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 DatePicker from '@core/components/_sample/components/datePicker.vue'
|
||||
|
||||
@@ -283,6 +284,11 @@ const confirmWithdrawVisible = ref<boolean>(false)
|
||||
const confirmWeightVisible = ref<boolean>(false)
|
||||
const confirmWithdraw = ref<Withdraw | null>(null)
|
||||
|
||||
const confirmModalTitle = computed(() => {
|
||||
if (!confirmWithdraw.value) return 'تایید تراکنش'
|
||||
return confirmLabelForRecord(confirmWithdraw.value, flowType.value)
|
||||
})
|
||||
|
||||
const weightTransactions = computed(() => {
|
||||
const tx = confirmWithdraw.value?.transaction
|
||||
return Array.isArray(tx) ? tx : []
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Withdraw } from '@/model/wallet'
|
||||
import type { WithdrawFlowType } from '@/pages/menuPages/withdraw/types'
|
||||
import { metalLabel } from '@/utils/adminMetalLabels'
|
||||
import { getMainMetal } from '@/utils/metalLabels'
|
||||
import {
|
||||
@@ -61,3 +62,50 @@ export function withdrawAmountColorClass(record: Pick<Withdraw, 'cash' | 'gold'>
|
||||
if (!value) return ''
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,35 +1,13 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const Orders: RouteRecordRaw = {
|
||||
/** ریدایرکت آدرس قدیمی — منو زیر پیکربندی است */
|
||||
const Pricing: RouteRecordRaw = {
|
||||
name: 'pricing',
|
||||
path: '/pricing',
|
||||
component: () => import('@core/layouts/BasicLayout.vue'),
|
||||
redirect: { name: 'pricingMain' },
|
||||
meta: {
|
||||
icon: 'vuesax-linear:dollar-circle',
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
hideMenu: true
|
||||
}
|
||||
}
|
||||
|
||||
export default Orders
|
||||
export default Pricing
|
||||
|
||||
@@ -42,6 +42,17 @@ const Setting: RouteRecordRaw = {
|
||||
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',
|
||||
path: 'advanced-configuration',
|
||||
@@ -51,7 +62,7 @@ const Setting: RouteRecordRaw = {
|
||||
deployAdminOnly: true,
|
||||
icon: 'vuesax-linear:code-circle',
|
||||
title: 'تنظیمات پیشرفته',
|
||||
order: 2
|
||||
order: 3
|
||||
},
|
||||
beforeEnter() {
|
||||
const userStore = useUserStore()
|
||||
@@ -67,7 +78,7 @@ const Setting: RouteRecordRaw = {
|
||||
meta: {
|
||||
icon: 'vuesax-linear:image',
|
||||
title: 'بنرها',
|
||||
order: 3
|
||||
order: 4
|
||||
},
|
||||
beforeEnter() {
|
||||
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',
|
||||
path: 'priceTriggers',
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
Reference in New Issue
Block a user