fix some bug

This commit is contained in:
2026-07-01 22:28:09 +03:30
parent d2c5c20729
commit 3bf249377f
38 changed files with 1872 additions and 1610 deletions
+2
View File
@@ -8,6 +8,8 @@ export const createProduct = async (obj: Customer) =>
await useAxios.post<AddId>(`/admin/product`, obj)
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)
+2 -2
View File
@@ -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
View File
@@ -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();
})();
+3
View File
@@ -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
+2
View File
@@ -37,6 +37,8 @@ export interface DashboardFinance {
fullPriceSood?: number
fullPriceKarmozd?: number
fullPriceKaermozd?: number
/** سود اسپرد + کارمزد */
totalTradingProfit?: number
orderCount?: number
Gifts?: string
GiftsPrise?: number
+34
View File
@@ -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: [
{
+10
View File
@@ -0,0 +1,10 @@
/** مبلغ بدون واحد — برای نمایش جدا از «ریال» در کارت‌های داشبورد */
export function formatDashboardAmount(value: number | string | null | undefined): string {
const n = Number(value ?? 0)
if (Number.isNaN(n)) return '0'
return Math.round(n)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
export const DASHBOARD_RIAL_UNIT = 'ریال'
+22 -9
View File
@@ -27,13 +27,17 @@ export const dashboardGateway = help(
)
export const dashboardKpi = {
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(
'مقدار طلای خرید و فروش مشتری در هر روز (فقط سفارش موفق).',
+77 -46
View File
@@ -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: 'حذف مشتری از گزارش تراز',
+103 -22
View File
@@ -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()
+12 -297
View File
@@ -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>
+4 -2
View File
@@ -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>-->
<!-- &lt;!&ndash; <BasicTags-->
<!-- :items="items"-->
<!-- color="green"-->
<!-- @add="add"-->
<!-- @edit="edit"-->
<!-- :key="refresh"-->
<!-- :loading="loading"-->
<!-- @delete="deleteTag"-->
<!-- />&ndash;&gt;-->
<!-- </BasicShowContent>-->
<!-- <a-modal
v-model:visible="addNatureVisible"
title="Vertically centered modal dialog"
centered
@ok="addNatureVisible = false"
>
<BaseInput title="عنوان" size="large" />
</a-modal>-->
<!-- <BasicModal
v-model:visible="addNatureVisible"
:title="editId ? `ویرایش` : 'افزودن'"
@close="addNatureVisible = false"
@ok="ok"
:loading="loading"
>
<a-form ref="formRef" :model="form" autocomplete="off">
<a-form-item name="value" label="حروف اختصاری" :rules="valueValidation">
<BasicInput v-model:value="form.value" placeHolder="حروف اختصاری" />
</a-form-item>
<a-form-item name="title_fa" label="عنوان">
<BasicInput v-model:value="form.title_fa" placeHolder="نام" />
</a-form-item>
</a-form>
</BasicModal>-->
</div>
</template>
<script setup lang="ts">
import BasicShowContent from '@/components/BasicShowContext.vue'
import { inputRules, valueValidation } from '@/utils/inputRules'
import type { Const } from '../../../../model/const'
import type { TagItem } from '@/components/BasicTags/model'
import { message } from 'ant-design-vue'
import type { FormInstance } from 'ant-design-vue'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import { createAddConst, deleteConst, updateEditConst } from '@/api/consts'
import bg1 from '@/pages/login/assets/img/loginBack.svg'
import { usePrice } from '../../../../core/utils/usePrice'
//data Arrays
const items = ref<Array<TagItem>>()
const baseData = ref<Array<Const>>()
//limitedOrder modal
const addNatureVisible = ref<boolean>()
const loading = ref<boolean>()
//refresh
let refresh = 0
const form = ref<Const>({
title_fa: '',
value: ''
})
const formRef = ref<FormInstance>()
//store
const store = useStaticApiListsStore()
//get limitedOrder
/*onMounted(() => {
setTypes()
})
const getTypes = async () =>
(baseData.value = store.natureListData ?? (await store.getNatureConst()))*/
/*async function setTypes() {
try {
loading.value = true
await getTypes()
// console.log('set', baseData.value)
items.value = baseData.value?.map((x: Const) => {
return {
id: x.id,
name: `${x.title_fa ?? ''}(${x.value})`
}
})
refresh++
} catch (e) {
console.error(e)
// message.error('خطا در دریافت اطلاعات')
} finally {
loading.value = false
}
// console.log(items.value)
}*/
//delete limitedOrder item
/*
async function deleteTag(id: number) {
// console.log(id)
try {
await deleteConst(id)
} catch (e) {
console.error(e)
// message.error('خطا در حذف مورد')
await resetData()
}
}
*/
//edit limitedOrder
// let editId: number | undefined = undefined
// function edit(id: number) {
// const obj: Const = baseData.value?.find((x: Const) => x.id === id) ?? ({} as Const)
// addNatureVisible.value = true
// editId = id
// form.value = obj
// }
//resetData
// async function resetData() {
// await store.getNatureConst()
// formRef.value?.resetFields()
// form.value = {
// title_fa: '',
// value: ''
// }
// await setTypes()
// }
//add
// function add() {
// editId = undefined
// addNatureVisible.value = true
// resetData()
// }
//submit
async function ok() {
let obj: Const = {
key: 'limited_price',
// title_fa: form.value.title_fa,
value: form.value.value
// id: editId
}
formRef.value?.validateFields().then(async () => {
try {
loading.value = true
await updateEditConst(obj)
message.success('عملیات با موفقیت انجام شد')
} catch (e) {
console.error(e)
// message.error('خطا در ثبت اطلاعات')
} finally {
loading.value = false
// addNatureVisible.value = false
}
})
}
</script>
<style></style>
@@ -0,0 +1,132 @@
<template>
<BasicActionSheet
v-model:visible="visibleProxy"
title="مشخصات کیف پول"
modal-width="40rem"
:show-button="false"
@close="visibleProxy = false"
>
<template v-if="wallet">
<div v-if="isMobile" class="mb-3 text-base font-700">مشخصات کیف پول</div>
<div class="mb-4 flex flex-col gap-2 rounded-lg bg-gray-100 p-3 md:flex-row md:items-center md:justify-between">
<div class="min-w-0">
<div class="text-base font-bold">{{ wallet.wallet_subject || '—' }}</div>
<div class="mt-1 text-sm text-gray-600">کد کیف پول: {{ wallet.id ?? '—' }}</div>
</div>
<CustomTag
v-bind="kindStyle"
class="self-start px-4 font-bold md:self-auto"
>
{{ wallet.wallet_kind_label || '—' }}
</CustomTag>
</div>
<div class="mb-4 grid gap-3 md:grid-cols-2">
<BasicInfoItem title="کد مشتری" :value="wallet.customer?.id ?? '—'" />
<BasicInfoItem title="نام مشتری" :value="wallet.customer?.name || '—'" />
<BasicInfoItem title="موبایل" :value="wallet.customer?.mobile || '—'" />
<BasicInfoItem title="فلز مرتبط" :value="wallet.wallet_metal_label || '—'" />
</div>
<div v-if="wallet.wallet_kind === 'product'" class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3">
<div class="mb-2 text-sm font-700 text-green-900">اطلاعات محصول</div>
<div class="grid gap-3 md:grid-cols-2">
<BasicInfoItem title="کد محصول" :value="wallet.product_id ?? '—'" />
<BasicInfoItem title="نام محصول" :value="wallet.product_title || wallet.product?.title || '—'" />
<BasicInfoItem title="دسته" :value="wallet.product_group_type || '—'" />
<BasicInfoItem title="وزن / مقدار" :value="productWeightLabel" />
</div>
</div>
<div v-else-if="wallet.wallet_kind === 'fund'" class="mb-4 rounded-lg border border-purple-200 bg-purple-50 p-3">
<div class="mb-2 text-sm font-700 text-purple-900">اطلاعات قلک</div>
<div class="grid gap-3 md:grid-cols-2">
<BasicInfoItem title="کد قلک" :value="wallet.fund_id ?? '—'" />
<BasicInfoItem title="عنوان قلک" :value="wallet.fund_title || wallet.fund?.title || '—'" />
</div>
</div>
<div v-else class="mb-4 rounded-lg border border-blue-200 bg-blue-50 p-3 text-sm text-blue-900">
کیف اصلی مشتری — موجودی ریالی و طلای آبشده (و محصولات تعریف‌شده در تنظیمات کیف اصلی) در همین کیف نگهداری می‌شود.
</div>
<div class="mb-2 text-sm font-700">موجودی</div>
<div class="mb-4 grid gap-2 md:grid-cols-3">
<div class="rounded-lg border border-gray-200 bg-white p-3 text-center">
<div class="mb-1 text-xs text-gray-500">ریال</div>
<div class="font-bold break-all">{{ usePrice(wallet.cash ?? 0) }}</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-3 text-center">
<div class="mb-1 text-xs text-gray-500">ارزش ریالی فلز</div>
<div class="font-bold break-all">{{ usePrice(wallet.gold_to_cash ?? 0) }}</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-3 text-center">
<div class="mb-1 text-xs text-gray-500">جمع ارزش</div>
<div class="font-bold break-all">{{ usePrice(wallet.total_rial_value ?? 0) }}</div>
</div>
</div>
<div v-if="metalRows.length" class="grid gap-2 md:grid-cols-2">
<div
v-for="row in metalRows"
:key="row.metal_id"
class="rounded-lg border border-amber-200 bg-amber-50 p-3"
>
<div class="text-xs text-amber-800">{{ row.label }}</div>
<div class="mt-1 font-bold text-amber-950">{{ row.display }}</div>
</div>
</div>
</template>
</BasicActionSheet>
</template>
<script setup lang="ts">
import type { Wallet } from '@/model/wallet'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import BasicInfoItem from '@/components/BasicInfoItem.vue'
import CustomTag from '@/components/CustomTag.vue'
import { isMobile } from '@core/utils/is'
import { usePrice } from '@core/utils/usePrice'
import { formatMetalDisplayValue, metalCellValue } from '@/utils/metalWeight'
import { walletKindTagStyle, walletProductWeightLabel } from '@/utils/walletDisplay'
import type { WalletMetalColumn } from '../hooks/walletColumns'
const props = defineProps<{
visible: boolean
wallet: Wallet | null
metals?: WalletMetalColumn[]
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
}>()
const visibleProxy = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value)
})
const kindStyle = computed(() => walletKindTagStyle(props.wallet?.wallet_kind))
const productWeightLabel = computed(() => walletProductWeightLabel(props.wallet))
const metalRows = computed(() => {
const wallet = props.wallet
if (!wallet) return []
const metals = props.metals?.length ? props.metals : [{ metal_id: 1, label: 'طلا' }]
return metals
.map((metal) => {
const value = metalCellValue(wallet as Record<string, unknown>, metal.metal_id)
return {
metal_id: metal.metal_id,
label: metal.label,
display: formatMetalDisplayValue(value, metal.metal_id),
raw: value
}
})
.filter((row) => row.raw !== 0)
})
</script>
@@ -3,6 +3,16 @@
<template #default="{ item: row }">
<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[] {
+45 -292
View File
@@ -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) => {
+7 -1
View File
@@ -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
}
+5 -27
View File
@@ -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
+13 -13
View File
@@ -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',
+25
View File
@@ -0,0 +1,25 @@
import type { Wallet, WalletKind } from '@/model/wallet'
export function walletKindTagStyle(kind?: WalletKind) {
switch (kind) {
case 'main':
return { textColor: '#1d4ed8', bgColor: '#dbeafe' }
case 'product':
return { textColor: '#15803d', bgColor: '#dcfce7' }
case 'fund':
return { textColor: '#7e22ce', bgColor: '#f3e8ff' }
default:
return { textColor: '#374151', bgColor: '#f3f4f6' }
}
}
export function walletSheetTitle(wallet: Wallet | null | undefined): string {
if (!wallet) return 'مشخصات کیف پول'
return wallet.wallet_subject || wallet.customer?.name || `کیف پول #${wallet.id ?? '-'}`
}
export function walletProductWeightLabel(wallet: Wallet | null | undefined): string {
if (!wallet?.product_amount && wallet?.product_amount !== 0) return '—'
const unit = wallet.product_unit ? ` ${wallet.product_unit}` : ''
return `${wallet.product_amount}${unit}`
}