added charge for all products 2

This commit is contained in:
2026-07-11 22:31:45 +03:30
parent 654002f1d6
commit 6107a2cbf3
13 changed files with 1010 additions and 213 deletions
+3
View File
@@ -10,6 +10,7 @@ export {}
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
export interface GlobalComponents { export interface GlobalComponents {
_index: typeof import('./src/core/components/_sample/_index.vue')['default'] _index: typeof import('./src/core/components/_sample/_index.vue')['default']
AAlert: typeof import('ant-design-vue/es')['Alert']
AAvatar: typeof import('ant-design-vue/es')['Avatar'] AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge'] ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button'] AButton: typeof import('ant-design-vue/es')['Button']
@@ -42,6 +43,7 @@ declare module '@vue/runtime-core' {
APagination: typeof import('ant-design-vue/es')['Pagination'] APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
APopover: typeof import('ant-design-vue/es')['Popover'] APopover: typeof import('ant-design-vue/es')['Popover']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton'] ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARow: typeof import('ant-design-vue/es')['Row'] ARow: typeof import('ant-design-vue/es')['Row']
@@ -56,6 +58,7 @@ declare module '@vue/runtime-core' {
ATextarea: typeof import('ant-design-vue/es')['Textarea'] ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker'] ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip'] ATooltip: typeof import('ant-design-vue/es')['Tooltip']
AUpload: typeof import('ant-design-vue/es')['Upload']
BaseButton: typeof import('./src/core/components/BaseButton/index.ts')['default'] BaseButton: typeof import('./src/core/components/BaseButton/index.ts')['default']
BaseCard: typeof import('./src/core/components/BaseCard/index.ts')['default'] BaseCard: typeof import('./src/core/components/BaseCard/index.ts')['default']
BaseCollapse: typeof import('./src/components/BaseCollapse.vue')['default'] BaseCollapse: typeof import('./src/components/BaseCollapse.vue')['default']
+23 -2
View File
@@ -60,6 +60,9 @@
<div class="flex cursor-pointer justify-center pt-2" @click="visibleProxy = false"> <div class="flex cursor-pointer justify-center pt-2" @click="visibleProxy = false">
<span class="w-12 rounded-sm border-t-2 border-t-solid border-t-gray-500"></span> <span class="w-12 rounded-sm border-t-2 border-t-solid border-t-gray-500"></span>
</div> </div>
<div v-if="title" class="basic-action-sheet__header">
<h3 class="basic-action-sheet__title">{{ title }}</h3>
</div>
<div class="basic-action-sheet__content"> <div class="basic-action-sheet__content">
<slot /> <slot />
</div> </div>
@@ -167,7 +170,8 @@ function handleOk() {
.basic-action-sheet { .basic-action-sheet {
.ant-drawer-content { .ant-drawer-content {
border-radius: 16px 16px 0 0; border-radius: 16px 16px 0 0;
overflow: hidden; overflow-x: visible;
overflow-y: hidden;
} }
.ant-drawer-body { .ant-drawer-body {
@@ -185,10 +189,27 @@ function handleOk() {
.basic-action-sheet__content { .basic-action-sheet__content {
flex: 1; flex: 1;
min-width: 0;
overflow-y: auto; overflow-y: auto;
padding: 0 16px 12px; padding: 0 16px 12px;
} }
.basic-action-sheet__header {
flex: 0 0 auto;
padding: 8px 16px 12px;
border-bottom: 1px solid var(--admin-border);
background: var(--admin-bg-elevated);
}
.basic-action-sheet__title {
margin: 0;
color: var(--admin-text);
font-size: 1rem;
font-weight: 700;
line-height: 1.6;
text-align: right;
}
.basic-action-sheet--compact { .basic-action-sheet--compact {
.ant-drawer-content-wrapper { .ant-drawer-content-wrapper {
height: auto !important; height: auto !important;
@@ -209,7 +230,7 @@ function handleOk() {
.basic-action-sheet__container--compact .basic-action-sheet__content { .basic-action-sheet__container--compact .basic-action-sheet__content {
flex: 0 1 auto; flex: 0 1 auto;
max-height: calc(88vh - 40px); max-height: calc(88vh - 92px);
} }
.basic-action-sheet__footer { .basic-action-sheet__footer {
@@ -1,8 +1,14 @@
<template> <template>
<div class="customer-wallet-balance" :class="compact ? 'space-y-1.5' : 'space-y-3'"> <div
class="customer-wallet-balance"
:class="[
compact ? 'customer-wallet-balance--compact space-y-1.5' : 'space-y-3',
layout === 'sidebar' ? 'customer-wallet-balance--sidebar' : '',
]"
>
<div <div
v-if="loading" v-if="loading"
class="rounded-xl border border-gray-200 bg-gray-50 text-center text-sm text-gray-500" class="customer-wallet-balance__placeholder rounded-xl text-center text-sm"
:class="compact ? 'px-3 py-3' : 'px-4 py-6'" :class="compact ? 'px-3 py-3' : 'px-4 py-6'"
> >
در حال بارگذاری موجودی… در حال بارگذاری موجودی…
@@ -16,24 +22,30 @@
{{ error }} {{ error }}
</div> </div>
<div v-else-if="summary && compact" class="customer-wallet-balance__minimal"> <div
v-else-if="summary && compact"
ref="minimalTrackRef"
class="customer-wallet-balance__minimal-track"
>
<div class="customer-wallet-balance__minimal">
<template v-if="chargeTargets.length"> <template v-if="chargeTargets.length">
<button <component
:is="interactive ? 'button' : 'div'"
v-for="target in chargeTargets" v-for="target in chargeTargets"
:key="target.key" :key="target.key"
type="button" :type="interactive ? 'button' : undefined"
class="customer-wallet-balance__minimal-item" class="customer-wallet-balance__minimal-item"
:class="[ :class="[
targetToneClass(target), targetToneClass(target),
{ 'customer-wallet-balance__minimal-item--inactive': target.active === false }, { 'customer-wallet-balance__minimal-item--inactive': target.active === false },
{ 'customer-wallet-balance__minimal-item--selected': target.key === selectedChargeKey }, { 'customer-wallet-balance__minimal-item--selected': interactive && target.key === selectedChargeKey },
]" ]"
:title="target.label" :title="target.label"
@click="emit('selectChargeTarget', target.key)" @click="interactive ? emit('selectChargeTarget', target.key) : undefined"
> >
<span class="customer-wallet-balance__minimal-label">{{ minimalTargetLabel(target) }}</span> <span class="customer-wallet-balance__minimal-label">{{ minimalTargetLabel(target) }}</span>
<span class="customer-wallet-balance__minimal-value">{{ balanceForTarget(target) }}</span> <span class="customer-wallet-balance__minimal-value">{{ balanceForTarget(target) }}</span>
</button> </component>
</template> </template>
<template v-else> <template v-else>
<div <div
@@ -50,68 +62,83 @@
<span class="customer-wallet-balance__minimal-value">{{ usePrice(summary.cash_wallet ?? 0, true, '') }}</span> <span class="customer-wallet-balance__minimal-value">{{ usePrice(summary.cash_wallet ?? 0, true, '') }}</span>
</div> </div>
</template> </template>
</div>
</div> </div>
<template v-else-if="summary"> <div v-else-if="summary" class="customer-wallet-balance__full">
<div <p v-if="layout === 'sidebar'" class="customer-wallet-balance__sidebar-title">موجودی دارایی‌ها</p>
v-for="metal in summary.metal_totals" <div class="customer-wallet-balance__overview">
:key="metal.metal_id" <article
class="overflow-hidden rounded-xl border border-gray-200" v-for="metal in summary.metal_totals"
> :key="metal.metal_id"
<div class="flex items-center justify-between gap-3 bg-emerald-50 px-4 py-3"> class="customer-wallet-balance__summary-card"
<div class="flex min-w-0 items-center gap-2"> :class="groupToneClass(metal.metal_id)"
<BaseIcon icon="vuesax-linear:coin" size="18" color="#15803d" /> >
<span class="truncate font-700 text-emerald-900">{{ metal.label }}</span> <span class="customer-wallet-balance__summary-label">{{ minimalMetalLabel(metal) }}</span>
</div> <span class="customer-wallet-balance__summary-value">{{ formatMetalTotal(metal) }}</span>
<span class="shrink-0 font-800 text-emerald-800">{{ formatMetalTotal(metal) }}</span> </article>
</div>
<div v-if="linesForMetal(metal.metal_id).length" class="divide-y divide-gray-100 bg-white"> <article class="customer-wallet-balance__summary-card customer-wallet-balance__group--cash">
<div <span class="customer-wallet-balance__summary-label">ریالی</span>
v-for="line in linesForMetal(metal.metal_id)" <span class="customer-wallet-balance__summary-value">{{ usePrice(summary.total_rial_value ?? 0, true, '') }}</span>
:key="line.key" </article>
class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm"
>
<span class="min-w-0 truncate text-gray-600">{{ line.label }}</span>
<span class="shrink-0 font-600 text-gray-900">{{ line.value }}</span>
</div>
</div>
</div> </div>
<div class="overflow-hidden rounded-xl border border-gray-200"> <div class="customer-wallet-balance__details">
<div class="flex items-center justify-between gap-3 bg-sky-50 px-4 py-3"> <div class="customer-wallet-balance__details-title">جزئیات موجودی</div>
<div class="flex items-center gap-2">
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#0369a1" />
<span class="font-700 text-sky-900">کل دارایی ریالی</span>
</div>
<span class="font-800 text-sky-800">{{ usePrice(summary.total_rial_value ?? 0, true, '') }}</span>
</div>
<div class="divide-y divide-gray-100 bg-white"> <template v-for="metal in summary.metal_totals" :key="`detail-${metal.metal_id}`">
<div class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm"> <section
<span class="text-gray-600">کیف پول ریالی</span> v-if="linesForMetal(metal.metal_id).length"
<span class="font-600">{{ usePrice(summary.cash_wallet ?? 0, true, '') }}</span> class="customer-wallet-balance__detail-group"
:class="groupToneClass(metal.metal_id)"
>
<div class="customer-wallet-balance__detail-group-title">
<span class="customer-wallet-balance__group-dot" aria-hidden="true" />
<span>{{ metal.label }}</span>
</div>
<div
v-for="line in linesForMetal(metal.metal_id)"
:key="line.key"
class="customer-wallet-balance__detail-row"
:class="{ 'customer-wallet-balance__detail-row--strong': line.key === 'total' }"
>
<span class="customer-wallet-balance__detail-label">{{ line.label }}</span>
<span class="customer-wallet-balance__detail-value">{{ line.value }}</span>
</div>
</section>
</template>
<section class="customer-wallet-balance__detail-group customer-wallet-balance__group--cash">
<div class="customer-wallet-balance__detail-group-title">
<span class="customer-wallet-balance__group-dot" aria-hidden="true" />
<span>دارایی ریالی</span>
</div>
<div class="customer-wallet-balance__detail-row">
<span class="customer-wallet-balance__detail-label">کیف پول ریالی</span>
<span class="customer-wallet-balance__detail-value">{{ usePrice(summary.cash_wallet ?? 0, true, '') }}</span>
</div> </div>
<div <div
v-if="Number(summary.assets_rial_value ?? 0) > 0" v-if="Number(summary.assets_rial_value ?? 0) > 0"
class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm" class="customer-wallet-balance__detail-row"
> >
<span class="text-gray-600">ارزش ریالی فلزات</span> <span class="customer-wallet-balance__detail-label">ارزش ریالی فلزات</span>
<span class="font-600">{{ usePrice(summary.assets_rial_value ?? 0, true, '') }}</span> <span class="customer-wallet-balance__detail-value">{{ usePrice(summary.assets_rial_value ?? 0, true, '') }}</span>
</div> </div>
<div class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm font-700"> <div class="customer-wallet-balance__detail-row customer-wallet-balance__detail-row--strong">
<span>مجموع کل</span> <span class="customer-wallet-balance__detail-label">مجموع کل</span>
<span>{{ usePrice(summary.total_rial_value ?? 0, true, '') }}</span> <span class="customer-wallet-balance__detail-value">{{ usePrice(summary.total_rial_value ?? 0, true, '') }}</span>
</div> </div>
</div> </section>
</div> </div>
</template> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { getCustomerAssetsSummary } from '@/api/customer' import { getCustomerAssetsSummary } from '@/api/customer'
import { useHorizontalTouchScroll } from '@/components/mobile/composables/useHorizontalTouchScroll'
import type { ChargeTarget } from '@/model/wallet' import type { ChargeTarget } from '@/model/wallet'
import type { WalletAssetsSummary, WalletInventoryCard, WalletMetalTotal } from '@/model/walletAssets' import type { WalletAssetsSummary, WalletInventoryCard, WalletMetalTotal } from '@/model/walletAssets'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
@@ -122,14 +149,20 @@ const props = withDefaults(
customerId?: number | null customerId?: number | null
/** نمایش مینیمال برای مودال‌ها */ /** نمایش مینیمال برای مودال‌ها */
compact?: boolean compact?: boolean
/** چیدمان ستون کناری در اطلاعات کاربر */
layout?: 'default' | 'sidebar'
/** گزینه‌های شارژ — برای نمایش موجودی هر محصول */ /** گزینه‌های شارژ — برای نمایش موجودی هر محصول */
chargeTargets?: ChargeTarget[] chargeTargets?: ChargeTarget[]
selectedChargeKey?: string selectedChargeKey?: string
/** false = فقط نمایش، بدون کلیک */
interactive?: boolean
}>(), }>(),
{ {
compact: false, compact: false,
layout: 'default',
chargeTargets: () => [], chargeTargets: () => [],
selectedChargeKey: '', selectedChargeKey: '',
interactive: true,
}, },
) )
@@ -167,9 +200,23 @@ function minimalMetalToneClass(metal: WalletMetalTotal) {
function minimalTargetLabel(target: ChargeTarget) { function minimalTargetLabel(target: ChargeTarget) {
if (target.kind === 'money') return 'نقدی' if (target.kind === 'money') return 'نقدی'
const label = target.label.replace(/\s*\(کیف اصلی\)\s*$/u, '').trim() return target.label
if (label.length <= 14) return label .replace(/\s*\(کیف اصلی\)\s*$/u, '')
return `${label.slice(0, 13)}…` .replace(/\s*\(غیرفعال\)\s*$/u, '')
.trim()
}
function groupToneClass(metalId: number | string | undefined) {
switch (Number(metalId)) {
case 1:
return 'customer-wallet-balance__group--gold'
case 2:
return 'customer-wallet-balance__group--silver'
case 3:
return 'customer-wallet-balance__group--copper'
default:
return 'customer-wallet-balance__group--product'
}
} }
function targetToneClass(target: ChargeTarget) { function targetToneClass(target: ChargeTarget) {
@@ -210,6 +257,9 @@ function balanceForTarget(target: ChargeTarget) {
const loading = ref(false) const loading = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const summary = ref<WalletAssetsSummary | null>(null) const summary = ref<WalletAssetsSummary | null>(null)
const minimalTrackRef = ref<HTMLElement | null>(null)
useHorizontalTouchScroll(minimalTrackRef)
watch( watch(
() => props.customerId, () => props.customerId,
@@ -287,36 +337,262 @@ defineExpose({ reload: () => props.customerId && load(props.customerId) })
</script> </script>
<style scoped> <style scoped>
.customer-wallet-balance__minimal { .customer-wallet-balance--compact {
display: grid; width: 100%;
grid-template-columns: repeat(auto-fill, minmax(4.75rem, 1fr)); min-width: 0;
gap: 0.35rem; max-width: 100%;
padding: 0.45rem; }
border-radius: 0.6rem;
border: 1px solid #e2e8f0; .customer-wallet-balance__placeholder {
background: #f8fafc; border: 1px solid var(--admin-border, #e2e8f0);
max-height: 14rem; background: var(--admin-bg-muted, #f8fafc);
color: var(--admin-text-secondary, #64748b);
}
/* ---------- نمای کامل (خلاصه + جزئیات) ---------- */
.customer-wallet-balance__full {
width: 100%;
min-width: 0;
}
.customer-wallet-balance--sidebar .customer-wallet-balance__full {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.customer-wallet-balance__sidebar-title {
margin: 0;
font-size: 0.8125rem;
font-weight: 700;
color: var(--admin-text-secondary, #64748b);
}
.customer-wallet-balance--sidebar .customer-wallet-balance__overview {
grid-template-columns: 1fr;
}
.customer-wallet-balance--sidebar .customer-wallet-balance__details {
max-height: min(52vh, 26rem);
overflow-y: auto; overflow-y: auto;
} }
.customer-wallet-balance__minimal-item { .customer-wallet-balance__overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
gap: 0.5rem;
}
.customer-wallet-balance__summary-card {
display: flex; display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.1rem;
min-width: 0; min-width: 0;
padding: 0.35rem 0.25rem; align-items: center;
justify-content: space-between;
gap: 0.6rem;
border-radius: 0.65rem;
border: 1px solid color-mix(in srgb, var(--wallet-accent, #94a3b8) 28%, transparent);
background: color-mix(in srgb, var(--wallet-accent, #94a3b8) 10%, var(--admin-bg-elevated, #fff));
padding: 0.55rem 0.7rem;
}
.customer-wallet-balance__summary-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.75rem;
font-weight: 700;
color: var(--admin-text-secondary, #64748b);
}
.customer-wallet-balance__summary-value {
flex: 0 0 auto;
font-size: 0.8125rem;
font-weight: 800;
color: var(--wallet-accent-strong, var(--admin-text, #0f172a));
white-space: nowrap;
}
.customer-wallet-balance__details {
margin-top: 0.6rem;
overflow: hidden;
border-radius: 0.75rem;
border: 1px solid var(--admin-border, #e2e8f0);
background: var(--admin-bg-elevated, #fff);
}
.customer-wallet-balance__details-title {
padding: 0.55rem 0.8rem;
border-bottom: 1px solid var(--admin-border, #e2e8f0);
color: var(--admin-text-secondary, #64748b);
font-size: 0.75rem;
font-weight: 700;
}
.customer-wallet-balance__detail-group + .customer-wallet-balance__detail-group {
border-top: 1px solid var(--admin-border, #e2e8f0);
}
.customer-wallet-balance__detail-group-title {
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.55rem 0.8rem 0.25rem;
color: var(--admin-text, #0f172a);
font-size: 0.75rem;
font-weight: 800;
}
.customer-wallet-balance__group-dot {
width: 0.45rem;
height: 0.45rem;
flex: 0 0 auto;
border-radius: 9999px;
background: var(--wallet-accent, var(--admin-text-secondary, #94a3b8));
}
.customer-wallet-balance__detail-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.75rem;
padding: 0.45rem 0.8rem;
font-size: 0.8125rem;
}
.customer-wallet-balance__detail-row + .customer-wallet-balance__detail-row {
border-top: 1px solid color-mix(in srgb, var(--admin-border, #e2e8f0) 70%, transparent);
}
.customer-wallet-balance__detail-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--admin-text-secondary, #64748b);
}
.customer-wallet-balance__detail-value {
font-weight: 600;
color: var(--admin-text, #0f172a);
white-space: nowrap;
}
.customer-wallet-balance__detail-row--strong {
background: color-mix(in srgb, var(--wallet-accent, #94a3b8) 8%, transparent);
}
.customer-wallet-balance__detail-row--strong .customer-wallet-balance__detail-label,
.customer-wallet-balance__detail-row--strong .customer-wallet-balance__detail-value {
font-weight: 800;
color: var(--admin-text, #0f172a);
}
.customer-wallet-balance__group--gold {
--wallet-accent: #d97706;
--wallet-accent-strong: #b45309;
}
.customer-wallet-balance__group--silver {
--wallet-accent: #64748b;
--wallet-accent-strong: #475569;
}
.customer-wallet-balance__group--copper {
--wallet-accent: #ea580c;
--wallet-accent-strong: #c2410c;
}
.customer-wallet-balance__group--cash {
--wallet-accent: #0284c7;
--wallet-accent-strong: #0369a1;
}
.customer-wallet-balance__group--product {
--wallet-accent: var(--admin-text-secondary, #64748b);
--wallet-accent-strong: var(--admin-text, #0f172a);
}
:global(html.is-dark) .customer-wallet-balance__group--gold {
--wallet-accent: #fbbf24;
--wallet-accent-strong: #fcd34d;
}
:global(html.is-dark) .customer-wallet-balance__group--silver {
--wallet-accent: #94a3b8;
--wallet-accent-strong: #cbd5e1;
}
:global(html.is-dark) .customer-wallet-balance__group--copper {
--wallet-accent: #fb923c;
--wallet-accent-strong: #fdba74;
}
:global(html.is-dark) .customer-wallet-balance__group--cash {
--wallet-accent: #60a5fa;
--wallet-accent-strong: #93c5fd;
}
.customer-wallet-balance__minimal-track {
display: block;
width: 100%;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
scrollbar-width: thin;
scrollbar-color: var(--admin-border-strong, #cbd5e1) transparent;
padding: 0.45rem 0.35rem;
border-radius: 0.6rem;
border: 1px solid var(--admin-border, #e2e8f0);
background: var(--admin-bg-muted, #f8fafc);
direction: ltr;
}
.customer-wallet-balance__minimal-track::-webkit-scrollbar {
height: 0.35rem;
}
.customer-wallet-balance__minimal-track::-webkit-scrollbar-thumb {
border-radius: 9999px;
background: var(--admin-border-strong, #cbd5e1);
}
.customer-wallet-balance__minimal {
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: stretch;
gap: 0.35rem;
width: max-content;
min-width: 100%;
direction: rtl;
}
.customer-wallet-balance__minimal-item {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
flex: 0 0 6.25rem;
width: 6.25rem;
min-width: 6.25rem;
min-height: 4rem;
gap: 0.25rem;
padding: 0.45rem 0.35rem;
border-radius: 0.45rem; border-radius: 0.45rem;
background: #fff; background: var(--wallet-tile-bg, var(--admin-bg-elevated, #fff));
text-align: center; text-align: center;
border: 1px solid transparent; border: 1px solid var(--wallet-tile-border, var(--admin-border, #e2e8f0));
cursor: default; cursor: default;
box-sizing: border-box;
} }
button.customer-wallet-balance__minimal-item { button.customer-wallet-balance__minimal-item {
cursor: pointer; cursor: pointer;
transition: box-shadow 0.15s ease, transform 0.15s ease; transition: box-shadow 0.15s ease, transform 0.15s ease, border-color 0.15s ease;
} }
button.customer-wallet-balance__minimal-item:hover { button.customer-wallet-balance__minimal-item:hover {
@@ -324,7 +600,9 @@ button.customer-wallet-balance__minimal-item:hover {
} }
.customer-wallet-balance__minimal-item--selected { .customer-wallet-balance__minimal-item--selected {
box-shadow: 0 0 0 2px #2563eb; border-width: 2px;
padding: calc(0.45rem - 1px) calc(0.35rem - 1px);
box-shadow: none;
} }
.customer-wallet-balance__minimal-item--inactive { .customer-wallet-balance__minimal-item--inactive {
@@ -332,84 +610,98 @@ button.customer-wallet-balance__minimal-item:hover {
} }
.customer-wallet-balance__minimal-item--gold { .customer-wallet-balance__minimal-item--gold {
background: #fffbeb; --wallet-tile-bg: #fffbeb;
border-color: #fde68a; --wallet-tile-border: #fde68a;
--wallet-tile-label: #92400e;
--wallet-tile-value: #b45309;
} }
.customer-wallet-balance__minimal-item--silver { .customer-wallet-balance__minimal-item--silver {
background: #f8fafc; --wallet-tile-bg: #f8fafc;
border-color: #cbd5e1; --wallet-tile-border: #cbd5e1;
--wallet-tile-label: #64748b;
--wallet-tile-value: #334155;
} }
.customer-wallet-balance__minimal-item--copper { .customer-wallet-balance__minimal-item--copper {
background: #fff7ed; --wallet-tile-bg: #fff7ed;
border-color: #fdba74; --wallet-tile-border: #fdba74;
--wallet-tile-label: #c2410c;
--wallet-tile-value: #9a3412;
} }
.customer-wallet-balance__minimal-item--cash { .customer-wallet-balance__minimal-item--cash {
background: #eff6ff; --wallet-tile-bg: #eff6ff;
border-color: #93c5fd; --wallet-tile-border: #93c5fd;
--wallet-tile-label: #0369a1;
--wallet-tile-value: #0284c7;
} }
.customer-wallet-balance__minimal-item--product { .customer-wallet-balance__minimal-item--product {
background: #f8fafc; --wallet-tile-bg: var(--admin-bg-elevated, #fff);
border-color: #e2e8f0; --wallet-tile-border: var(--admin-border, #e2e8f0);
--wallet-tile-label: #475569;
--wallet-tile-value: #1e293b;
} }
.customer-wallet-balance__minimal-label { .customer-wallet-balance__minimal-label {
font-size: 0.6875rem; display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
align-self: start;
min-height: 0;
max-height: 2.45em;
font-size: 0.625rem;
font-weight: 600; font-weight: 600;
color: #64748b; line-height: 1.25;
max-width: 100%; color: var(--wallet-tile-label, var(--admin-text-secondary, #64748b));
width: 100%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; white-space: normal;
white-space: nowrap; word-break: break-word;
} }
.customer-wallet-balance__minimal-value { .customer-wallet-balance__minimal-value {
font-size: 0.75rem; align-self: end;
font-size: 0.6875rem;
font-weight: 700; font-weight: 700;
color: #0f172a; line-height: 1.25;
color: var(--wallet-tile-value, var(--admin-text, #0f172a));
white-space: nowrap; white-space: nowrap;
} }
.customer-wallet-balance__minimal-item--gold .customer-wallet-balance__minimal-label { :global(html.is-dark) .customer-wallet-balance__minimal-item--gold {
color: #92400e; --wallet-tile-bg: rgba(245, 158, 11, 0.14);
--wallet-tile-border: rgba(251, 191, 36, 0.28);
--wallet-tile-label: #fbbf24;
--wallet-tile-value: #fcd34d;
} }
.customer-wallet-balance__minimal-item--gold .customer-wallet-balance__minimal-value { :global(html.is-dark) .customer-wallet-balance__minimal-item--silver {
color: #b45309; --wallet-tile-bg: rgba(148, 163, 184, 0.12);
--wallet-tile-border: rgba(148, 163, 184, 0.28);
--wallet-tile-label: #94a3b8;
--wallet-tile-value: #cbd5e1;
} }
.customer-wallet-balance__minimal-item--silver .customer-wallet-balance__minimal-label { :global(html.is-dark) .customer-wallet-balance__minimal-item--copper {
color: #64748b; --wallet-tile-bg: rgba(249, 115, 22, 0.14);
--wallet-tile-border: rgba(251, 146, 60, 0.28);
--wallet-tile-label: #fb923c;
--wallet-tile-value: #fdba74;
} }
.customer-wallet-balance__minimal-item--silver .customer-wallet-balance__minimal-value { :global(html.is-dark) .customer-wallet-balance__minimal-item--cash {
color: #334155; --wallet-tile-bg: rgba(59, 130, 246, 0.14);
--wallet-tile-border: rgba(96, 165, 250, 0.32);
--wallet-tile-label: #60a5fa;
--wallet-tile-value: #93c5fd;
} }
.customer-wallet-balance__minimal-item--copper .customer-wallet-balance__minimal-label { :global(html.is-dark) .customer-wallet-balance__minimal-item--product {
color: #c2410c; --wallet-tile-bg: rgba(255, 255, 255, 0.04);
} --wallet-tile-border: var(--admin-border);
--wallet-tile-label: var(--admin-text-secondary);
.customer-wallet-balance__minimal-item--copper .customer-wallet-balance__minimal-value { --wallet-tile-value: var(--admin-text);
color: #9a3412;
}
.customer-wallet-balance__minimal-item--cash .customer-wallet-balance__minimal-label {
color: #0369a1;
}
.customer-wallet-balance__minimal-item--cash .customer-wallet-balance__minimal-value {
color: #0284c7;
}
.customer-wallet-balance__minimal-item--product .customer-wallet-balance__minimal-label {
color: #475569;
}
.customer-wallet-balance__minimal-item--product .customer-wallet-balance__minimal-value {
color: #1e293b;
} }
</style> </style>
+10 -2
View File
@@ -1,12 +1,12 @@
<template> <template>
<BasicActionSheet <BasicActionSheet
v-model:visible="visibleModel" v-model:visible="visibleModel"
:title="''" :title="title"
:showButton="false" :showButton="false"
:compact="true" :compact="true"
:drawerHeight="drawerHeight" :drawerHeight="drawerHeight"
> >
<div v-if="item" class="space-y-3 pb-2"> <div v-if="item" class="mobile-detail-sheet__body space-y-3 pb-2">
<slot :item="item" /> <slot :item="item" />
</div> </div>
</BasicActionSheet> </BasicActionSheet>
@@ -36,3 +36,11 @@ const visibleModel = computed({
set: (value: boolean) => emit('update:visible', value) set: (value: boolean) => emit('update:visible', value)
}) })
</script> </script>
<style scoped>
.mobile-detail-sheet__body {
width: 100%;
min-width: 0;
max-width: 100%;
}
</style>
@@ -0,0 +1,69 @@
import { onBeforeUnmount, watch, type Ref } from 'vue'
function bindHorizontalTouchScroll(el: HTMLElement) {
let startX = 0
let startY = 0
let scrollStart = 0
let axis: 'x' | 'y' | null = null
const onTouchStart = (event: TouchEvent) => {
if (event.touches.length !== 1) return
startX = event.touches[0].clientX
startY = event.touches[0].clientY
scrollStart = el.scrollLeft
axis = null
}
const onTouchMove = (event: TouchEvent) => {
if (event.touches.length !== 1) return
const dx = event.touches[0].clientX - startX
const dy = event.touches[0].clientY - startY
if (!axis) {
if (Math.abs(dx) < 4 && Math.abs(dy) < 4) return
axis = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y'
}
if (axis !== 'x') return
event.preventDefault()
event.stopPropagation()
el.scrollLeft = scrollStart - dx
}
el.addEventListener('touchstart', onTouchStart, { passive: true })
el.addEventListener('touchmove', onTouchMove, { passive: false })
requestAnimationFrame(() => {
const max = el.scrollWidth - el.clientWidth
if (max > 0) el.scrollLeft = max
})
return () => {
el.removeEventListener('touchstart', onTouchStart)
el.removeEventListener('touchmove', onTouchMove)
}
}
/**
* Reliable horizontal swipe inside RTL pages and nested vertical scroll areas.
*/
export function useHorizontalTouchScroll(trackRef: Ref<HTMLElement | null>) {
let cleanup: (() => void) | undefined
watch(
trackRef,
(el) => {
cleanup?.()
cleanup = undefined
if (!el) return
cleanup = bindHorizontalTouchScroll(el)
},
{ flush: 'post' },
)
onBeforeUnmount(() => {
cleanup?.()
})
}
+2 -1
View File
@@ -49,7 +49,8 @@
.ant-drawer-bottom .ant-drawer-content { .ant-drawer-bottom .ant-drawer-content {
border-radius: 16px 16px 0 0; border-radius: 16px 16px 0 0;
overflow: hidden; overflow-x: visible;
overflow-y: hidden;
} }
.ant-card-body{ .ant-card-body{
+160
View File
@@ -428,4 +428,164 @@ html.is-dark {
border-color: var(--admin-gold-light) !important; border-color: var(--admin-gold-light) !important;
box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.18) !important; box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.18) !important;
} }
.wallet-charge-form__direction.ant-radio-group-solid {
.ant-radio-button-wrapper {
background: var(--admin-bg-muted) !important;
border-color: var(--admin-border-strong) !important;
color: var(--admin-text) !important;
}
.ant-radio-button-wrapper:hover {
color: var(--admin-text) !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:hover,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:active {
background: #16a34a !important;
border-color: #16a34a !important;
color: #fff !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:hover::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:active::before {
background-color: #16a34a !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:hover,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:active {
background: #dc2626 !important;
border-color: #dc2626 !important;
color: #fff !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:hover::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:active::before {
background-color: #dc2626 !important;
}
}
}
.wallet-charge-form__direction.ant-radio-group-solid {
.ant-radio-button-wrapper {
background: var(--admin-bg-muted) !important;
border-color: var(--admin-border-strong) !important;
color: var(--admin-text) !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:hover,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:active {
background: #15803d !important;
border-color: #15803d !important;
color: #fff !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:hover::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:active::before {
background-color: #15803d !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:hover,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:active {
background: #b91c1c !important;
border-color: #b91c1c !important;
color: #fff !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:hover::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:active::before {
background-color: #b91c1c !important;
}
}
html.is-dark {
.customer-wallet-balance__minimal {
background: var(--admin-bg-muted);
border-color: var(--admin-border);
}
.customer-wallet-balance__minimal-item--gold {
background: rgba(245, 158, 11, 0.14);
border-color: rgba(251, 191, 36, 0.28);
}
.customer-wallet-balance__minimal-item--gold .customer-wallet-balance__minimal-label {
color: #fbbf24;
}
.customer-wallet-balance__minimal-item--gold .customer-wallet-balance__minimal-value {
color: #fcd34d;
}
.customer-wallet-balance__minimal-item--silver {
background: rgba(148, 163, 184, 0.12);
border-color: rgba(148, 163, 184, 0.28);
}
.customer-wallet-balance__minimal-item--silver .customer-wallet-balance__minimal-label {
color: #94a3b8;
}
.customer-wallet-balance__minimal-item--silver .customer-wallet-balance__minimal-value {
color: #cbd5e1;
}
.customer-wallet-balance__minimal-item--copper {
background: rgba(249, 115, 22, 0.14);
border-color: rgba(251, 146, 60, 0.28);
}
.customer-wallet-balance__minimal-item--copper .customer-wallet-balance__minimal-label {
color: #fb923c;
}
.customer-wallet-balance__minimal-item--copper .customer-wallet-balance__minimal-value {
color: #fdba74;
}
.customer-wallet-balance__minimal-item--cash {
background: rgba(59, 130, 246, 0.14);
border-color: rgba(96, 165, 250, 0.32);
}
.customer-wallet-balance__minimal-item--cash .customer-wallet-balance__minimal-label {
color: #60a5fa;
}
.customer-wallet-balance__minimal-item--cash .customer-wallet-balance__minimal-value {
color: #93c5fd;
}
.customer-wallet-balance__minimal-item--product {
background: rgba(255, 255, 255, 0.04);
border-color: var(--admin-border);
}
.customer-wallet-balance__minimal-item--product .customer-wallet-balance__minimal-label {
color: var(--admin-text-secondary);
}
.customer-wallet-balance__minimal-item--product .customer-wallet-balance__minimal-value {
color: var(--admin-text);
}
.wallet-charge-form__preview--charge {
background: rgba(34, 197, 94, 0.14);
border-color: rgba(34, 197, 94, 0.32);
color: #86efac;
}
.wallet-charge-form__preview--discharge {
background: rgba(239, 68, 68, 0.14);
border-color: rgba(239, 68, 68, 0.32);
color: #fca5a5;
}
} }
+8
View File
@@ -58,6 +58,14 @@ export interface DashboardFinance {
profitGoldGrams?: number profitGoldGrams?: number
/** سود ریالی: مجموع کارمزدهای کسرشده از ریال */ /** سود ریالی: مجموع کارمزدهای کسرشده از ریال */
profitRial?: number profitRial?: number
/** سود ریالی قبل از کسر سندهای تبدیل */
profitRialGross?: number
/** سود طلایی قبل از افزودن سندهای تبدیل */
profitGoldGramsGross?: number
/** جمع ریالی تبدیل‌شده به طلا در بازه */
profitRialConverted?: number
/** جمع گرم تبدیل‌شده از سود ریالی در بازه */
profitGoldConvertedGram?: number
/** ارزش ریالی سود طلایی با قیمت روز */ /** ارزش ریالی سود طلایی با قیمت روز */
profitGoldValueRial?: number profitGoldValueRial?: number
/** تفکیک روزانهٔ سود طلایی/ریالی */ /** تفکیک روزانهٔ سود طلایی/ریالی */
@@ -55,6 +55,9 @@
<span class="dashboard-profit-split__unit">گرم</span> <span class="dashboard-profit-split__unit">گرم</span>
</p> </p>
<p class="dashboard-profit-split__sub">≈ {{ formatDashboardAmount(profitGoldValueRial) }} ریال</p> <p class="dashboard-profit-split__sub">≈ {{ formatDashboardAmount(profitGoldValueRial) }} ریال</p>
<p v-if="profitGoldConvertedGram > 0" class="dashboard-profit-split__note">
شامل {{ formatGram(profitGoldConvertedGram) }} گرم از تبدیل سود ریالی
</p>
</article> </article>
<article class="dashboard-profit-split__card dashboard-profit-split__card--rial"> <article class="dashboard-profit-split__card dashboard-profit-split__card--rial">
@@ -69,7 +72,14 @@
<span class="dashboard-profit-split__amount">{{ formatDashboardAmount(profitRial) }}</span> <span class="dashboard-profit-split__amount">{{ formatDashboardAmount(profitRial) }}</span>
<span class="dashboard-profit-split__unit">ریال</span> <span class="dashboard-profit-split__unit">ریال</span>
</p> </p>
<p class="dashboard-profit-split__sub">{{ dailyRows.length }} روز دارای معامله</p> <p class="dashboard-profit-split__sub">
<template v-if="profitRialConverted > 0">
{{ formatGram(profitGoldConvertedGram) }} گرم تبدیل‌شده به طلا
</template>
<template v-else>
{{ dailyRows.length }} روز دارای معامله
</template>
</p>
</article> </article>
</div> </div>
@@ -291,15 +301,15 @@ const activeEnd = ref(rangeEnd.value)
const sectionHelp: DashboardHelp = { const sectionHelp: DashboardHelp = {
description: description:
'کارمزد معاملات بر اساس فیلدی که مشتری وارد می‌کند تفکیک می‌شود: وقتی مشتری «مبلغ ریالی» ثابت می‌زند کارمزد از طلا کسر می‌شود (سود طلایی)، و وقتی «وزن» ثابت می‌زند کارمزد از ریال کسر می‌شود (سود ریالی). اعداد بر اساس بازهٔ انتخابی همین بخش محاسبه می‌شوند.', 'کارمزد معاملات بر اساس فیلدی که مشتری وارد می‌کند تفکیک می‌شود: وقتی مشتری «مبلغ ریالی» ثابت می‌زند کارمزد از طلا کسر می‌شود (سود طلایی)، و وقتی «وزن» ثابت می‌زند کارمزد از ریال کسر می‌شود (سود ریالی). اعداد بر اساس بازهٔ انتخابی همین بخش محاسبه می‌شوند.',
formula: 'سود طلایی = جمع کارمزدِ کسرشده از طلا (گرم) | سود ریالی = جمع کارمزدِ کسرشده از ریال (ریال)', formula: 'سود طلایی = جمع کارمزدِ کسرشده از طلا (گرم) + گرم سندهای تبدیل سود ریالی | سود ریالی = جمع کارمزدِ کسرشده از ریال − ریال تبدیل‌شده به طلا',
} }
const goldHelp: DashboardHelp = { const goldHelp: DashboardHelp = {
description: 'مجموع کارمزدهایی که در معامله‌های «مبلغ‌محور» (مشتری مبلغ ریالی ثابت زده) از طلا کسر شده است.', description: 'مجموع کارمزدهای کسرشده از طلا به‌علاوهٔ گرم سندهای «تبدیل سود ریالی به طلا» در همین بازه.',
formula: 'مجموع کارمزد طلا (گرم) × قیمت روز = ارزش ریالی تقریبی', formula: 'کارمزد طلا (گرم) + گرم تبدیل‌شده از سود ریالی',
} }
const rialHelp: DashboardHelp = { const rialHelp: DashboardHelp = {
description: 'مجموع کارمزدهایی که در معامله‌های «وزن‌محور» (مشتری وزن ثابت زده) از ریال کسر شده است.', description: 'سود ریالی باقی‌مانده پس از کسر سندهای تبدیل به طلا در همین بازه.',
formula: 'مجموع کارمزد ریالی معامله‌های وزن‌محور', formula: 'کارمزد ریالی − ریال تبدیل‌شده به طلا',
} }
const loading = ref(false) const loading = ref(false)
@@ -308,6 +318,8 @@ const finance = ref<DashboardFinance | null>(null)
const profitGoldGrams = computed(() => Number(finance.value?.profitGoldGrams ?? 0)) const profitGoldGrams = computed(() => Number(finance.value?.profitGoldGrams ?? 0))
const profitRial = computed(() => Number(finance.value?.profitRial ?? 0)) const profitRial = computed(() => Number(finance.value?.profitRial ?? 0))
const profitGoldValueRial = computed(() => Number(finance.value?.profitGoldValueRial ?? 0)) const profitGoldValueRial = computed(() => Number(finance.value?.profitGoldValueRial ?? 0))
const profitRialConverted = computed(() => Number(finance.value?.profitRialConverted ?? 0))
const profitGoldConvertedGram = computed(() => Number(finance.value?.profitGoldConvertedGram ?? 0))
const dailyRows = computed(() => finance.value?.daily ?? []) const dailyRows = computed(() => finance.value?.daily ?? [])
// صفحه‌بندی جدول روزانه // صفحه‌بندی جدول روزانه
@@ -467,7 +479,7 @@ async function submitSanad() {
}) })
message.success('سند تبدیل سود ثبت شد') message.success('سند تبدیل سود ثبت شد')
closeSanad() closeSanad()
await loadConversions() reloadAll()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
message.error('خطا در ثبت سند') message.error('خطا در ثبت سند')
@@ -480,7 +492,7 @@ async function removeConversion(id: number) {
try { try {
await deleteProfitConversion(id) await deleteProfitConversion(id)
message.success('سند حذف شد') message.success('سند حذف شد')
await loadConversions() reloadAll()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
message.error('خطا در حذف سند') message.error('خطا در حذف سند')
@@ -595,6 +607,12 @@ reloadAll()
color: var(--admin-text-secondary); color: var(--admin-text-secondary);
} }
.dashboard-profit-split__note {
margin: 0.2rem 0 0;
font-size: 0.6875rem;
color: var(--admin-text-secondary);
}
.dashboard-profit-split__sanad { .dashboard-profit-split__sanad {
border-top: 1px solid var(--admin-border); border-top: 1px solid var(--admin-border);
padding-top: 0.75rem; padding-top: 0.75rem;
@@ -1,78 +1,96 @@
<template> <template>
<MobileDetailSheet v-model:visible="visibleProxy" :item="item" title=""> <MobileDetailSheet v-model:visible="visibleProxy" :item="item" :title="sheetTitle">
<template #default="{ item: row }"> <template #default="{ item: row }">
<h2 class="text-base font-700 leading-relaxed text-gray-900">{{ row.name || '-' }}</h2> <div class="customer-mobile-sheet">
<p v-if="row.mobile" class="text-sm text-gray-500" dir="ltr">{{ row.mobile }}</p> <p v-if="row.mobile" class="customer-mobile-sheet__mobile" dir="ltr">{{ row.mobile }}</p>
<div class="customer-mobile-sheet__chips">
<span class="customer-mobile-sheet__chip" :class="row.active ? 'is-success' : 'is-danger'">
{{ row.active ? 'فعال' : 'غیرفعال' }}
</span>
<span
class="customer-mobile-sheet__chip"
:class="row.trade_active === 1 ? 'is-success' : 'is-danger'"
>
{{ row.trade_active === 1 ? 'سفارش مجاز' : 'سفارش ممنوع' }}
</span>
</div>
<section class="customer-mobile-sheet__section">
<h4 class="customer-mobile-sheet__section-title">موجودی</h4>
<CustomerWalletBalancePanel
:customer-id="row.id"
:charge-targets="chargeTargets"
:interactive="false"
compact
/>
</section>
<div class="grid grid-cols-2 gap-2">
<BaseButton class="w-full col-span-2" size="middle" type="primary" icon="vuesax-linear:wallet-add" @click="closeAndEmit('chargeWallet', row)">
شارژ کیف پول
</BaseButton>
<BaseButton <BaseButton
v-if="!row.is_legal" class="customer-mobile-sheet__charge-btn"
class="w-full"
size="middle" size="middle"
type="default" type="primary"
icon="vuesax-linear:user-search" icon="vuesax-linear:wallet-add"
@click="closeAndEmit('confirmUser', row)" @click="closeAndEmit('chargeWallet', row)"
> >
تایید کاربر شارژ / دشارژ کیف پول
</BaseButton>
<BaseButton class="w-full" size="middle" type="default" icon="vuesax-linear:stickynote" @click="closeAndEmit('info', row)">
مشخصات کامل
</BaseButton>
<BaseButton class="w-full" size="middle" type="default" icon="vuesax-linear:edit" @click="closeAndEmit('edit', row)">
ویرایش
</BaseButton>
<BaseButton class="w-full" size="middle" type="default" icon="vuesax-linear:archive-tick" @click="closeAndEmit('archive', row)">
بایگانی
</BaseButton>
<BaseButton
class="w-full"
size="middle"
:type="row.active ? 'default' : 'primary'"
:icon="row.active ? 'vuesax-linear:user-remove' : 'vuesax-linear:user-tick'"
@click="confirmToggleActive(row)"
>
{{ row.active ? 'غیرفعال کردن' : 'فعال کردن' }}
</BaseButton>
<BaseButton
class="w-full"
size="middle"
:type="row.trade_active === 1 ? 'default' : 'primary'"
:icon="row.trade_active === 1 ? 'vuesax-linear:box-remove' : 'vuesax-linear:box-tick'"
@click="confirmToggleTrade(row)"
>
{{ row.trade_active === 1 ? 'ممنوعیت سفارش' : 'مجاز کردن سفارش' }}
</BaseButton> </BaseButton>
<div class="customer-mobile-sheet__actions">
<BaseButton size="small" type="default" icon="vuesax-linear:stickynote" @click="closeAndEmit('info', row)">
مشخصات
</BaseButton>
<BaseButton size="small" type="default" icon="vuesax-linear:edit" @click="closeAndEmit('edit', row)">
ویرایش
</BaseButton>
<BaseButton size="small" type="default" icon="vuesax-linear:archive-tick" @click="closeAndEmit('archive', row)">
بایگانی
</BaseButton>
<BaseButton
v-if="!row.is_legal"
size="small"
type="default"
icon="vuesax-linear:user-search"
@click="closeAndEmit('confirmUser', row)"
>
تایید
</BaseButton>
<BaseButton
size="small"
:type="row.active ? 'default' : 'primary'"
:icon="row.active ? 'vuesax-linear:user-remove' : 'vuesax-linear:user-tick'"
@click="confirmToggleActive(row)"
>
{{ row.active ? 'غیرفعال' : 'فعال' }}
</BaseButton>
<BaseButton
size="small"
:type="row.trade_active === 1 ? 'default' : 'primary'"
:icon="row.trade_active === 1 ? 'vuesax-linear:box-remove' : 'vuesax-linear:box-tick'"
@click="confirmToggleTrade(row)"
>
{{ row.trade_active === 1 ? 'ممنوعیت' : 'مجاز سفارش' }}
</BaseButton>
</div>
<MobileInfoGrid>
<MobileDetailRow label="کد کاربری" :value="row.id ?? '-'" />
<MobileDetailRow label="نوع کاربر" :value="row.type_str || '-'" />
<MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" />
<MobileDetailRow label="نام معرف" :value="row.referral?.name ?? '-'" />
<MobileDetailRow label="آخرین بازدید" :value="lastSeen(row)" value-class="text-xs" />
</MobileInfoGrid>
</div> </div>
<CustomerWalletBalancePanel :customer-id="row?.id" />
<MobileInfoGrid>
<MobileDetailRow label="وضعیت کاربر" full-width :value-class="row.active ? 'text-green-700' : 'text-red-600'">
{{ row.active ? 'فعال' : 'غیرفعال' }}
</MobileDetailRow>
<MobileDetailRow
label="وضعیت سفارش دهی"
full-width
:value-class="row.trade_active === 1 ? 'text-green-700' : 'text-red-600'"
>
{{ row.trade_active === 1 ? 'مجاز' : 'غیر مجاز' }}
</MobileDetailRow>
<MobileDetailRow label="کد کاربری" :value="row.id ?? '-'" />
<MobileDetailRow label="نوع کاربر" :value="row.type_str || '-'" />
<MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" />
<MobileDetailRow label="نام معرف کاربر" :value="row.referral?.name ?? '-'" />
<MobileDetailRow label="آخرین بازدید" :value="lastSeen(row)" value-class="text-xs" />
</MobileInfoGrid>
</template> </template>
</MobileDetailSheet> </MobileDetailSheet>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { Modal } from 'ant-design-vue' import { Modal } from 'ant-design-vue'
import type { Customer } from '@/model/customer' import type { Customer } from '@/model/customer'
import type { ChargeTarget } from '@/model/wallet'
import { getChargeTargets } from '@/api/wallet'
import { MODAL_ABOVE_ACTION_SHEET_Z_INDEX } from '@/constants/overlayZIndex' import { MODAL_ABOVE_ACTION_SHEET_Z_INDEX } from '@/constants/overlayZIndex'
import MobileDetailRow from '@/components/mobile/MobileDetailRow.vue' import MobileDetailRow from '@/components/mobile/MobileDetailRow.vue'
import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue' import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue'
@@ -96,11 +114,15 @@ const emit = defineEmits<{
(e: 'confirmUser', item: Customer): void (e: 'confirmUser', item: Customer): void
}>() }>()
const chargeTargets = ref<ChargeTarget[]>([])
const visibleProxy = computed({ const visibleProxy = computed({
get: () => props.visible, get: () => props.visible,
set: (value: boolean) => emit('update:visible', value) set: (value: boolean) => emit('update:visible', value),
}) })
const sheetTitle = computed(() => props.item?.name?.trim() || 'مشتری')
type SheetEmit = 'archive' | 'chargeWallet' | 'info' | 'confirmUser' | 'edit' type SheetEmit = 'archive' | 'chargeWallet' | 'info' | 'confirmUser' | 'edit'
function closeAndEmit(event: SheetEmit, row: Customer) { function closeAndEmit(event: SheetEmit, row: Customer) {
@@ -115,7 +137,7 @@ function confirmToggleActive(row: Customer) {
cancelText: 'رد', cancelText: 'رد',
centered: true, centered: true,
zIndex: MODAL_ABOVE_ACTION_SHEET_Z_INDEX, zIndex: MODAL_ABOVE_ACTION_SHEET_Z_INDEX,
onOk: () => emit('toggleActive', row) onOk: () => emit('toggleActive', row),
}) })
} }
@@ -126,11 +148,98 @@ function confirmToggleTrade(row: Customer) {
cancelText: 'رد', cancelText: 'رد',
centered: true, centered: true,
zIndex: MODAL_ABOVE_ACTION_SHEET_Z_INDEX, zIndex: MODAL_ABOVE_ACTION_SHEET_Z_INDEX,
onOk: () => emit('toggleTrade', row) onOk: () => emit('toggleTrade', row),
}) })
} }
function lastSeen(row: Customer) { function lastSeen(row: Customer) {
return row.last_seen_at ? toJalali(row.last_seen_at) : '-' return row.last_seen_at ? toJalali(row.last_seen_at) : '-'
} }
async function loadChargeTargets() {
try {
const res = await getChargeTargets()
chargeTargets.value = res?.targets ?? []
} catch {
chargeTargets.value = []
}
}
watch(
() => [props.visible, props.item?.id] as const,
([open]) => {
if (open) void loadChargeTargets()
},
)
</script> </script>
<style scoped>
.customer-mobile-sheet {
display: flex;
flex-direction: column;
gap: 0.75rem;
width: 100%;
min-width: 0;
max-width: 100%;
}
.customer-mobile-sheet__mobile {
margin: 0;
font-size: 0.8125rem;
color: var(--admin-text-secondary);
}
.customer-mobile-sheet__chips {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.customer-mobile-sheet__chip {
padding: 0.2rem 0.55rem;
border-radius: 9999px;
font-size: 0.6875rem;
font-weight: 700;
}
.customer-mobile-sheet__chip.is-success {
background: rgba(34, 197, 94, 0.14);
color: #15803d;
}
.customer-mobile-sheet__chip.is-danger {
background: rgba(239, 68, 68, 0.14);
color: #b91c1c;
}
.customer-mobile-sheet__section {
width: 100%;
min-width: 0;
max-width: 100%;
}
.customer-mobile-sheet__section-title {
margin: 0 0 0.35rem;
font-size: 0.75rem;
font-weight: 700;
color: var(--admin-text-secondary);
}
.customer-mobile-sheet__charge-btn {
width: 100%;
}
.customer-mobile-sheet__actions {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.35rem;
}
:global(html.is-dark) .customer-mobile-sheet__chip.is-success {
color: #4ade80;
}
:global(html.is-dark) .customer-mobile-sheet__chip.is-danger {
color: #f87171;
}
</style>
@@ -138,12 +138,27 @@ defineExpose({
margin-bottom: 0.65rem; margin-bottom: 0.65rem;
} }
.wallet-charge-form :deep(.ant-form-item-label > label) {
color: var(--admin-text, rgba(0, 0, 0, 0.88));
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper) { .wallet-charge-form__direction :deep(.ant-radio-button-wrapper) {
width: 50%; width: 50%;
text-align: center; text-align: center;
height: 2.35rem; height: 2.35rem;
line-height: 2.25rem; line-height: 2.25rem;
font-weight: 700; font-weight: 700;
background: var(--admin-bg-muted, #fafafa) !important;
border-color: var(--admin-border-strong, #d9d9d9) !important;
color: var(--admin-text, rgba(0, 0, 0, 0.88)) !important;
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper:not(:first-child)::before) {
background-color: var(--admin-border-strong, #d9d9d9) !important;
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper:hover) {
color: var(--admin-text, rgba(0, 0, 0, 0.88)) !important;
} }
.wallet-charge-form__radio-icon { .wallet-charge-form__radio-icon {
@@ -180,29 +195,29 @@ defineExpose({
.wallet-charge-form__preview--charge { .wallet-charge-form__preview--charge {
background: rgba(34, 197, 94, 0.12); background: rgba(34, 197, 94, 0.12);
border-color: rgba(34, 197, 94, 0.35); border-color: rgba(34, 197, 94, 0.35);
color: #86efac; color: #15803d;
} }
.wallet-charge-form__preview--charge .wallet-charge-form__preview-sign { .wallet-charge-form__preview--charge .wallet-charge-form__preview-sign {
background: rgba(34, 197, 94, 0.2); background: rgba(34, 197, 94, 0.2);
color: #4ade80; color: #16a34a;
} }
.wallet-charge-form__preview--discharge { .wallet-charge-form__preview--discharge {
background: rgba(239, 68, 68, 0.12); background: rgba(239, 68, 68, 0.12);
border-color: rgba(239, 68, 68, 0.35); border-color: rgba(239, 68, 68, 0.35);
color: #fca5a5; color: #b91c1c;
} }
.wallet-charge-form__preview--discharge .wallet-charge-form__preview-sign { .wallet-charge-form__preview--discharge .wallet-charge-form__preview-sign {
background: rgba(239, 68, 68, 0.2); background: rgba(239, 68, 68, 0.2);
color: #f87171; color: #dc2626;
} }
.wallet-charge-form__hint { .wallet-charge-form__hint {
margin-top: 0.35rem; margin-top: 0.35rem;
font-size: 0.75rem; font-size: 0.75rem;
opacity: 0.75; color: var(--admin-text-secondary, rgba(0, 0, 0, 0.45));
} }
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge) { .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge) {
@@ -211,9 +226,59 @@ defineExpose({
color: #fff !important; color: #fff !important;
} }
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge::before) {
background-color: #15803d !important;
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge) { .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge) {
background: #b91c1c !important; background: #b91c1c !important;
border-color: #b91c1c !important; border-color: #b91c1c !important;
color: #fff !important; color: #fff !important;
} }
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge::before) {
background-color: #b91c1c !important;
}
:global(html.is-dark) .wallet-charge-form__preview--charge {
background: rgba(34, 197, 94, 0.14);
border-color: rgba(34, 197, 94, 0.32);
color: #86efac;
}
:global(html.is-dark) .wallet-charge-form__preview--charge .wallet-charge-form__preview-sign {
background: rgba(34, 197, 94, 0.22);
color: #4ade80;
}
:global(html.is-dark) .wallet-charge-form__preview--discharge {
background: rgba(239, 68, 68, 0.14);
border-color: rgba(239, 68, 68, 0.32);
color: #fca5a5;
}
:global(html.is-dark) .wallet-charge-form__preview--discharge .wallet-charge-form__preview-sign {
background: rgba(239, 68, 68, 0.22);
color: #f87171;
}
:global(html.is-dark) .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge) {
background: #16a34a !important;
border-color: #16a34a !important;
color: #fff !important;
}
:global(html.is-dark) .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge::before) {
background-color: #16a34a !important;
}
:global(html.is-dark) .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge) {
background: #dc2626 !important;
border-color: #dc2626 !important;
color: #fff !important;
}
:global(html.is-dark) .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge::before) {
background-color: #dc2626 !important;
}
</style> </style>
@@ -15,14 +15,15 @@
/> />
</div> </div>
<CustomerWalletBalancePanel <div v-if="resolvedCustomerId && !targetsLoading" class="wallet-charge-modal-body__assets">
v-if="resolvedCustomerId && !targetsLoading" <CustomerWalletBalancePanel
:customer-id="resolvedCustomerId" :customer-id="resolvedCustomerId"
:charge-targets="targets" :charge-targets="targets"
:selected-charge-key="selectedTargetKey" :selected-charge-key="selectedTargetKey"
compact compact
@select-charge-target="selectedTargetKeyProxy = $event" @select-charge-target="selectedTargetKeyProxy = $event"
/> />
</div>
<div class="wallet-charge-modal-body__section"> <div class="wallet-charge-modal-body__section">
<label class="wallet-charge-modal-body__label">محصول / کیف پول</label> <label class="wallet-charge-modal-body__label">محصول / کیف پول</label>
@@ -151,6 +152,13 @@ defineExpose({
padding: 0 0.75rem 0.25rem; padding: 0 0.75rem 0.25rem;
} }
.wallet-charge-modal-body__assets {
margin-bottom: 0.75rem;
width: 100%;
min-width: 0;
max-width: 100%;
}
.wallet-charge-modal-body__section { .wallet-charge-modal-body__section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+39 -4
View File
@@ -132,10 +132,12 @@
:showButton="false" :showButton="false"
@close="userInfoVisible = false" @close="userInfoVisible = false"
> >
<div v-if="isMobile" class="mb-3 text-base font-700">اطلاعات کاربر</div>
<a-tabs v-if="userInfo" v-model:activeKey="activeKey"> <a-tabs v-if="userInfo" v-model:activeKey="activeKey">
<a-tab-pane key="اطلاعات جامع" tab="اطلاعات جامع"> <a-tab-pane key="اطلاعات جامع" tab="اطلاعات جامع">
<div class="space-y-2"> <div
class="grid min-w-0 grid-cols-1 items-start gap-3 lg:grid-cols-[minmax(17rem,21rem)_minmax(0,1fr)] xl:grid-cols-[minmax(18rem,23rem)_minmax(0,1fr)]"
>
<div class="min-w-0 space-y-2 lg:order-2">
<div class="admin-stat-muted rounded-lg w-full flex flex-col gap-3 p-3 md:flex-row md:justify-between"> <div class="admin-stat-muted rounded-lg w-full flex flex-col gap-3 p-3 md:flex-row md:justify-between">
<div> <div>
<div class="font-bold">{{ userInfo.name }}</div> <div class="font-bold">{{ userInfo.name }}</div>
@@ -172,7 +174,7 @@
</div> </div>
</div> </div>
<div class="grid grid-cols-2 gap-2 md:grid-cols-4"> <div class="grid min-w-0 grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
<BasicInfoItem title="کد کاربری" :value="userInfo?.id" /> <BasicInfoItem title="کد کاربری" :value="userInfo?.id" />
<BasicInfoItem title="نوع کاربر" :value="userInfo?.type_str" /> <BasicInfoItem title="نوع کاربر" :value="userInfo?.type_str" />
<BasicInfoItem <BasicInfoItem
@@ -215,7 +217,26 @@
</BasicInfoItem> </BasicInfoItem>
</div> </div>
<CustomerWalletBalancePanel v-if="userInfo?.id" :customer-id="userInfo.id" class="mt-2" /> <CustomerWalletBalancePanel
v-if="userInfo?.id && isMobile"
:customer-id="userInfo.id"
compact
:charge-targets="infoChargeTargets"
:interactive="false"
class="mt-2"
/>
</div>
<aside
v-if="userInfo?.id && !isMobile"
class="surface-muted min-w-0 rounded-xl border border-base p-2.5 lg:order-1 lg:sticky lg:top-0"
>
<CustomerWalletBalancePanel
:customer-id="userInfo.id"
layout="sidebar"
:interactive="false"
/>
</aside>
</div> </div>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="لیست سفارشات" tab="لیست سفارشات"> <a-tab-pane key="لیست سفارشات" tab="لیست سفارشات">
@@ -283,6 +304,8 @@ import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { isMobile } from '@core/utils/is' import { isMobile } from '@core/utils/is'
import WalletChargeModal from './components/WalletChargeModal.vue' import WalletChargeModal from './components/WalletChargeModal.vue'
import CustomerWalletBalancePanel from '@/components/customer/CustomerWalletBalancePanel.vue' import CustomerWalletBalancePanel from '@/components/customer/CustomerWalletBalancePanel.vue'
import { getChargeTargets } from '@/api/wallet'
import type { ChargeTarget } from '@/model/wallet'
import { metalLabel } from '@/utils/adminMetalLabels' import { metalLabel } from '@/utils/adminMetalLabels'
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy' import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue' import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
@@ -482,6 +505,7 @@ async function info(val: Customer) {
activeKey.value = 'اطلاعات جامع' activeKey.value = 'اطلاعات جامع'
userInfo.value = cloneDeep(val) userInfo.value = cloneDeep(val)
userInfoVisible.value = true userInfoVisible.value = true
void loadInfoChargeTargets()
await nextTick(() => { await nextTick(() => {
loading.value = true loading.value = true
refreshFundTable() refreshFundTable()
@@ -551,6 +575,17 @@ onMounted(async () => {
//add project modal //add project modal
const addUserVisible = ref<boolean>(false) const addUserVisible = ref<boolean>(false)
const userInfoVisible = ref<boolean>(false) const userInfoVisible = ref<boolean>(false)
const infoChargeTargets = ref<ChargeTarget[]>([])
async function loadInfoChargeTargets() {
if (!isMobile.value) return
try {
const res = await getChargeTargets()
infoChargeTargets.value = res?.targets ?? []
} catch {
infoChargeTargets.value = []
}
}
const userInfo = ref<Customer>() const userInfo = ref<Customer>()
//submit user //submit user