fix(crm): honor open data scopes and role assignees
Unset sections on a saved role policy were still filtered as own-only, so full lead access showed nothing. Activities can now be assigned by org role, and the Tahesab sales report reads as a result instead of a tag wall. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -321,7 +321,7 @@ function openAccessEditor(u: OrgUserDto) {
|
||||
const hasOverride = DATA_ACCESS_SECTIONS.some((s) => Boolean(override[s]));
|
||||
accessMode.value = hasOverride ? 'custom' : 'inherit';
|
||||
draftAccess.value = hasOverride
|
||||
? clonePolicy(override)
|
||||
? { ...clonePolicy(u.effectiveDataAccess), ...clonePolicy(override) }
|
||||
: clonePolicy(u.effectiveDataAccess);
|
||||
expandedAccess.value = u.id;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { theme } from 'ant-design-vue';
|
||||
import {
|
||||
Alert as AAlert,
|
||||
Card as ACard,
|
||||
Col as ACol,
|
||||
Empty as AEmpty,
|
||||
List as AList,
|
||||
Row as ARow,
|
||||
Statistic as AStatistic,
|
||||
Table as ATable,
|
||||
Typography,
|
||||
} from 'ant-design-vue';
|
||||
import type { TableColumnsType } from 'ant-design-vue';
|
||||
|
||||
const { Paragraph: ATypographyParagraph, Text: ATypographyText } = Typography;
|
||||
const AListItem = AList.Item;
|
||||
const AListItemMeta = AList.Item.Meta;
|
||||
import type {
|
||||
SalesFactorOverviewDto,
|
||||
SalesMovementStatsDto,
|
||||
SalesNamedMovementDto,
|
||||
TahesabSalesOverviewDto,
|
||||
@@ -12,10 +27,9 @@ import { formatTahesabBedBes } from '@/lib/tahesab';
|
||||
|
||||
const props = defineProps<{
|
||||
overview: TahesabSalesOverviewDto | null;
|
||||
showInternet?: boolean;
|
||||
}>();
|
||||
|
||||
const { token } = theme.useToken();
|
||||
|
||||
function formatNum(n: number | null | undefined) {
|
||||
if (n == null || Number.isNaN(n)) return '—';
|
||||
return toFaDigits(new Intl.NumberFormat('fa-IR', { maximumFractionDigits: 3 }).format(n));
|
||||
@@ -23,28 +37,154 @@ function formatNum(n: number | null | undefined) {
|
||||
|
||||
function formatMali(n: number | null | undefined) {
|
||||
if (n == null) return '—';
|
||||
return formatTahesabBedBes(n);
|
||||
return formatTahesabBedBes(n, 0);
|
||||
}
|
||||
|
||||
function formatRial(n: number | null | undefined) {
|
||||
if (n == null) return '—';
|
||||
return `${toFaDigits(new Intl.NumberFormat('fa-IR', { maximumFractionDigits: 0 }).format(Math.abs(n)))} ریال`;
|
||||
}
|
||||
|
||||
type GroupRow = {
|
||||
id: string;
|
||||
label: string;
|
||||
netSaleWeight: number;
|
||||
saleReturnWeight: number;
|
||||
netSaleMali: number;
|
||||
children?: GroupRow[];
|
||||
};
|
||||
|
||||
const groupRows = computed<GroupRow[]>(() =>
|
||||
(props.overview?.groups ?? []).map((group) => ({
|
||||
id: group.id,
|
||||
label: group.label,
|
||||
netSaleWeight: group.netSaleWeight,
|
||||
saleReturnWeight: group.saleReturnWeight,
|
||||
netSaleMali: group.netSaleMali,
|
||||
children: group.children.length
|
||||
? group.children.map((child) => ({
|
||||
id: child.id,
|
||||
label: child.label,
|
||||
netSaleWeight: child.netSaleWeight,
|
||||
saleReturnWeight: child.saleReturnWeight,
|
||||
netSaleMali: child.netSaleMali,
|
||||
}))
|
||||
: undefined,
|
||||
})),
|
||||
);
|
||||
|
||||
const groupColumns: TableColumnsType<GroupRow> = [
|
||||
{ title: 'گروه', dataIndex: 'label', key: 'label', ellipsis: true },
|
||||
{
|
||||
title: 'فروش (گرم)',
|
||||
key: 'weight',
|
||||
width: 120,
|
||||
customRender: ({ record }) => formatNum(record.netSaleWeight),
|
||||
},
|
||||
{
|
||||
title: 'مرجوعی (گرم)',
|
||||
key: 'return',
|
||||
width: 120,
|
||||
customRender: ({ record }) => formatNum(record.saleReturnWeight),
|
||||
},
|
||||
{
|
||||
title: 'مبلغ',
|
||||
key: 'mali',
|
||||
width: 160,
|
||||
customRender: ({ record }) => formatRial(record.netSaleMali),
|
||||
},
|
||||
];
|
||||
|
||||
function percentText(stats: SalesMovementStatsDto, empty = 'درصدی ثبت نشده') {
|
||||
const p = stats.percent;
|
||||
if (!p || p.avg == null) return empty;
|
||||
return `اول ${formatNum(p.first)} · آخر ${formatNum(p.last)} · میانگین ${formatNum(p.avg)} · کم ${formatNum(p.min)} · بیش ${formatNum(p.max)}`;
|
||||
}
|
||||
|
||||
function percentShort(stats: SalesMovementStatsDto) {
|
||||
const p = stats.percent;
|
||||
if (!p || p.avg == null) return '—';
|
||||
return `میانگین ${formatNum(p.avg)} · از ${formatNum(p.min)} تا ${formatNum(p.max)}`;
|
||||
}
|
||||
|
||||
const factors = computed(() =>
|
||||
(props.overview?.factors ?? []).filter((f) => f.key !== 'other' || f.sale.grossWeight > 0 || f.purchase.grossWeight > 0),
|
||||
);
|
||||
|
||||
const factorColumns = computed<TableColumnsType<SalesFactorOverviewDto>>(() => {
|
||||
const cols: TableColumnsType<SalesFactorOverviewDto> = [
|
||||
{
|
||||
title: 'بخش کالا',
|
||||
dataIndex: 'label',
|
||||
key: 'label',
|
||||
fixed: 'right',
|
||||
width: 150,
|
||||
customRender: ({ record }) => (record.key === 'other' ? 'سایر عنوانها' : record.label),
|
||||
},
|
||||
{
|
||||
title: 'وزن فروش (گرم)',
|
||||
key: 'saleWeight',
|
||||
customRender: ({ record }) => formatNum(record.sale.netWeight),
|
||||
},
|
||||
{
|
||||
title: 'مرجوعی فروش (گرم)',
|
||||
key: 'saleReturn',
|
||||
customRender: ({ record }) => formatNum(record.sale.returnWeight),
|
||||
},
|
||||
{
|
||||
title: 'مبلغ فروش (ریال)',
|
||||
key: 'saleMali',
|
||||
customRender: ({ record }) => formatRial(record.sale.netMali),
|
||||
},
|
||||
{
|
||||
title: 'وزن خرید (گرم)',
|
||||
key: 'buyWeight',
|
||||
customRender: ({ record }) => formatNum(record.purchase.netWeight),
|
||||
},
|
||||
{
|
||||
title: 'مرجوعی خرید (گرم)',
|
||||
key: 'buyReturn',
|
||||
customRender: ({ record }) => formatNum(record.purchase.returnWeight),
|
||||
},
|
||||
{
|
||||
title: 'مبلغ خرید (ریال)',
|
||||
key: 'buyMali',
|
||||
customRender: ({ record }) => formatRial(record.purchase.netMali),
|
||||
},
|
||||
{
|
||||
title: 'تعداد فروش',
|
||||
key: 'saleCount',
|
||||
width: 110,
|
||||
customRender: ({ record }) =>
|
||||
record.key === 'bullion' ? formatNum(record.sale.netCount) : '—',
|
||||
},
|
||||
];
|
||||
if (props.overview?.percentReady) {
|
||||
cols.push(
|
||||
{
|
||||
title: 'درصد فروش',
|
||||
key: 'salePercent',
|
||||
customRender: ({ record }) => percentShort(record.sale),
|
||||
},
|
||||
{
|
||||
title: 'درصد خرید',
|
||||
key: 'buyPercent',
|
||||
customRender: ({ record }) => percentShort(record.purchase),
|
||||
},
|
||||
);
|
||||
}
|
||||
return cols;
|
||||
});
|
||||
|
||||
const karColumns: TableColumnsType<SalesNamedMovementDto> = [
|
||||
{ title: 'کارساخته', dataIndex: 'name', key: 'name', ellipsis: true },
|
||||
{ title: 'نام کالا', dataIndex: 'name', key: 'name', ellipsis: true },
|
||||
{
|
||||
title: 'خالص فروش',
|
||||
title: 'وزن فروش (گرم)',
|
||||
key: 'saleNet',
|
||||
customRender: ({ record }) => formatNum(record.sale.netWeight),
|
||||
},
|
||||
{
|
||||
title: 'مرجوعی فروش',
|
||||
title: 'مرجوعی فروش (گرم)',
|
||||
key: 'saleRet',
|
||||
customRender: ({ record }) => formatNum(record.sale.returnWeight),
|
||||
},
|
||||
@@ -54,12 +194,12 @@ const karColumns: TableColumnsType<SalesNamedMovementDto> = [
|
||||
customRender: ({ record }) => percentText(record.sale),
|
||||
},
|
||||
{
|
||||
title: 'خالص خرید',
|
||||
title: 'وزن خرید (گرم)',
|
||||
key: 'buyNet',
|
||||
customRender: ({ record }) => formatNum(record.purchase.netWeight),
|
||||
},
|
||||
{
|
||||
title: 'مرجوعی خرید',
|
||||
title: 'مرجوعی خرید (گرم)',
|
||||
key: 'buyRet',
|
||||
customRender: ({ record }) => formatNum(record.purchase.returnWeight),
|
||||
},
|
||||
@@ -81,37 +221,25 @@ const karColumns: TableColumnsType<SalesNamedMovementDto> = [
|
||||
/>
|
||||
</a-col>
|
||||
|
||||
<a-col v-for="factor in factors" :key="factor.key" :xs="24" :sm="12" :xl="6">
|
||||
<a-card :bordered="false" :title="factor.label">
|
||||
<a-row :gutter="[12, 8]">
|
||||
<a-col :span="12">
|
||||
<a-statistic title="خالص فروش" :value="formatNum(factor.sale.netWeight)" />
|
||||
<a-typography-text type="secondary">
|
||||
مرجوعی {{ formatNum(factor.sale.returnWeight) }}
|
||||
<template v-if="factor.key === 'bullion'">
|
||||
· تعداد {{ formatNum(factor.sale.netCount) }}
|
||||
</template>
|
||||
</a-typography-text>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-statistic title="خالص خرید" :value="formatNum(factor.purchase.netWeight)" />
|
||||
<a-typography-text type="secondary">
|
||||
مرجوعی {{ formatNum(factor.purchase.returnWeight) }}
|
||||
</a-typography-text>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-typography-text type="secondary">مبلغ فروش {{ formatMali(factor.sale.netMali) }}</a-typography-text>
|
||||
</a-col>
|
||||
<a-col v-if="percentText(factor.sale, '') || percentText(factor.purchase, '')" :span="24">
|
||||
<div v-if="percentText(factor.sale, '')">خروج: {{ percentText(factor.sale, '') }}</div>
|
||||
<div v-if="percentText(factor.purchase, '')">ورود: {{ percentText(factor.purchase, '') }}</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-col :span="24">
|
||||
<a-card :bordered="false" title="نتیجهٔ این بازه">
|
||||
<a-typography-paragraph type="secondary">
|
||||
هر ردیف یک بخش کالاست. وزن فروش، گرم فروختهشده بعد از کم شدن مرجوعی همان بازه است.
|
||||
مبلغ، ریال همان فروش است. تعداد فقط برای شمش و سکه معنی دارد.
|
||||
«سایر عنوانها» سندهایی است که در نامشان کارساخته، اتیکت، شمش یا سکه نبوده.
|
||||
</a-typography-paragraph>
|
||||
<a-table
|
||||
:columns="factorColumns"
|
||||
:data-source="factors"
|
||||
row-key="key"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 1100 }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="24">
|
||||
<a-card :bordered="false" title="کارساخته، خالص بعد از مرجوعی">
|
||||
<a-card :bordered="false" title="ریز کارساخته، به تفکیک نام کالا">
|
||||
<a-table
|
||||
:columns="karColumns"
|
||||
:data-source="overview.karsakhte"
|
||||
@@ -126,36 +254,34 @@ const karColumns: TableColumnsType<SalesNamedMovementDto> = [
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card :bordered="false" title="گروه اصلی و فرعی">
|
||||
<a-empty v-if="!overview.groups.length" description="گروهی در این بازه فروش ندارد." />
|
||||
<div v-for="group in overview.groups" :key="group.id">
|
||||
<a-flex justify="space-between" :style="{ marginTop: token.marginSM }">
|
||||
<a-typography-text strong>{{ group.label }}</a-typography-text>
|
||||
<a-typography-text>
|
||||
خالص {{ formatNum(group.netSaleWeight) }} · مرجوعی {{ formatNum(group.saleReturnWeight) }}
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
<a-list v-if="group.children.length" :data-source="group.children">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta :title="item.label">
|
||||
<template #description>
|
||||
خالص {{ formatNum(item.netSaleWeight) }} · مبلغ {{ formatMali(item.netSaleMali) }} · مرجوعی
|
||||
{{ formatNum(item.saleReturnWeight) }}
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
<a-col :span="24">
|
||||
<a-card :bordered="false" title="فروش به تفکیک گروه">
|
||||
<a-typography-paragraph type="secondary">
|
||||
عدد گروه اصلی، فروش خودش و زیرگروهها را با هم نشان میدهد.
|
||||
</a-typography-paragraph>
|
||||
<a-table
|
||||
:columns="groupColumns"
|
||||
:data-source="groupRows"
|
||||
row-key="id"
|
||||
default-expand-all-rows
|
||||
:pagination="groupRows.length > 6 ? { pageSize: 6, showSizeChanger: false } : false"
|
||||
:scroll="{ x: 560 }"
|
||||
>
|
||||
<template #emptyText>
|
||||
<a-empty description="گروهی در این بازه فروش ندارد." />
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card :bordered="false" title="پورسانت کارشناس">
|
||||
<a-typography-paragraph type="secondary">
|
||||
کارساختهٔ نقره و ساچمه (اتیکت جواهر و فروش اینترنتی) جدا حساب میشوند.
|
||||
{{
|
||||
showInternet
|
||||
? 'کارساختهٔ نقره جدا از سرمایهگذاری (اتیکت جواهر و فروش اینترنتی) حساب میشود.'
|
||||
: 'کارساختهٔ نقره جدا از سرمایهگذاری (اتیکت جواهر) حساب میشود.'
|
||||
}}
|
||||
</a-typography-paragraph>
|
||||
<a-list
|
||||
:data-source="overview.experts"
|
||||
@@ -179,7 +305,7 @@ const karColumns: TableColumnsType<SalesNamedMovementDto> = [
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="24">
|
||||
<a-col v-if="showInternet" :span="24">
|
||||
<a-card :bordered="false" title="فروش اینترنتی (مونتیفای)">
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :sm="8">
|
||||
@@ -203,12 +329,5 @@ const karColumns: TableColumnsType<SalesNamedMovementDto> = [
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col v-if="overview.otherNos.length" :span="24">
|
||||
<a-card :bordered="false" title="سندهایی که در چهار بخش جا نگرفتند">
|
||||
<a-space wrap>
|
||||
<a-tag v-for="name in overview.otherNos" :key="name">{{ name }}</a-tag>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user