fix(tahesab): polish sales report page chrome

Match leads/invoices header layout, keep title clean, and add a clear apply-filters action for the date range.
This commit is contained in:
2026-10-22 19:47:58 +03:30
parent ade43fea0f
commit 6d8ede8e2a
+249 -179
View File
@@ -1,8 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { theme } from 'ant-design-vue';
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue'; import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue';
import { ReloadOutlined, SyncOutlined } from '@ant-design/icons-vue'; import {
ArrowRightOutlined,
FilterOutlined,
SearchOutlined,
SyncOutlined,
} from '@ant-design/icons-vue';
import { import {
TAHESAB_ASNAD_KIND_LABELS, TAHESAB_ASNAD_KIND_LABELS,
TAHESAB_ASNAD_KINDS, TAHESAB_ASNAD_KINDS,
@@ -28,6 +34,7 @@ import { useCustomerGroups } from '@/lib/customer-groups';
import JalaliDateInput from '@/components/JalaliDateInput.vue'; import JalaliDateInput from '@/components/JalaliDateInput.vue';
const router = useRouter(); const router = useRouter();
const { token } = theme.useToken();
const { groups: customerGroups, load: loadCustomerGroups } = useCustomerGroups(); const { groups: customerGroups, load: loadCustomerGroups } = useCustomerGroups();
const connections = ref<TahesabConnectionDto[]>([]); const connections = ref<TahesabConnectionDto[]>([]);
@@ -87,7 +94,9 @@ function metalLabel(v: number | null) {
function formatNum(n: number | null | undefined) { function formatNum(n: number | null | undefined) {
if (n == null) return '—'; if (n == null) return '—';
return toFaDigits(new Intl.NumberFormat('fa-IR', { maximumFractionDigits: 3 }).format(n)); return toFaDigits(
new Intl.NumberFormat('fa-IR', { maximumFractionDigits: 3 }).format(n),
);
} }
function formatMali(n: number | null | undefined) { function formatMali(n: number | null | undefined) {
@@ -96,7 +105,13 @@ function formatMali(n: number | null | undefined) {
} }
const columns: TableColumnsType<TahesabAsnadLineDto> = [ const columns: TableColumnsType<TahesabAsnadLineDto> = [
{ title: 'تاریخ', dataIndex: 'tarikh', key: 'tarikh', width: 110 }, {
title: 'تاریخ',
dataIndex: 'tarikh',
key: 'tarikh',
width: 110,
customRender: ({ text }) => toFaDigits(String(text ?? '')),
},
{ {
title: 'شخص', title: 'شخص',
key: 'person', key: 'person',
@@ -162,6 +177,16 @@ const filterParams = computed(() => {
}; };
}); });
const lastSyncedLabel = computed(() => {
const raw = summary.value?.lastSyncedAt;
if (!raw) return null;
try {
return toFaDigits(new Date(raw).toLocaleString('fa-IR'));
} catch {
return null;
}
});
async function loadConnections() { async function loadConnections() {
const { data } = await api.get<TahesabConnectionDto[]>( const { data } = await api.get<TahesabConnectionDto[]>(
'/integrations/tahesab/connections/public', '/integrations/tahesab/connections/public',
@@ -263,7 +288,7 @@ async function pollJob(jobId: string) {
async function startSync() { async function startSync() {
const params = filterParams.value; const params = filterParams.value;
if (!params) { if (!params) {
error.value = 'تاریخ از و تا را انتخاب کنید.'; error.value = 'ابتدا بازه تاریخ را مشخص کنید، بعد به‌روزرسانی را بزنید.';
return; return;
} }
syncing.value = true; syncing.value = true;
@@ -289,6 +314,11 @@ async function startSync() {
} }
} }
function applyFilters() {
page.value = 1;
void loadReport();
}
function onTableChange(pag: TablePaginationConfig) { function onTableChange(pag: TablePaginationConfig) {
page.value = pag.current ?? 1; page.value = pag.current ?? 1;
pageSize.value = pag.pageSize ?? 50; pageSize.value = pag.pageSize ?? 50;
@@ -313,190 +343,230 @@ onUnmounted(() => stopPoll());
</script> </script>
<template> <template>
<a-flex vertical :gap="16"> <a-row :gutter="[24, 24]">
<a-row :gutter="[16, 16]" align="middle" justify="space-between"> <a-col :span="24">
<a-col :xs="24" :md="14"> <a-flex
<a-typography-title :level="3" style="margin: 0"> justify="space-between"
گزارش فروش ته‌حساب align="center"
</a-typography-title> wrap="wrap"
<a-typography-text type="secondary"> :gap="token.marginSM"
اسنپ‌شات اسناد مشتریان لینک‌شده — طبقه‌بندی فروش/مرجوعی تقریبی است و بعد از بازخورد کارفرما اصلاح می‌شود. >
</a-typography-text> <div>
</a-col> <a-typography-title :level="3" :style="{ margin: 0 }">
<a-col :xs="24" :md="10"> گزارش فروش ته‌حساب
<a-flex justify="flex-end" wrap="wrap" :gap="8"> </a-typography-title>
<a-button @click="router.push({ name: 'reports' })">بازگشت به گزارش‌ها</a-button> <a-typography-text type="secondary">
<a-button :loading="loading" @click="loadReport"> فروش و مرجوعی مشتریان لینک‌شده بر اساس بازه تاریخ
<template #icon><ReloadOutlined /></template> </a-typography-text>
نمایش </div>
<a-space :size="8" wrap>
<a-button @click="router.push({ name: 'reports' })">
<template #icon><ArrowRightOutlined /></template>
بازگشت
</a-button> </a-button>
<a-button type="primary" :loading="syncing" @click="startSync"> <a-button type="primary" :loading="syncing" @click="startSync">
<template #icon><SyncOutlined /></template> <template #icon><SyncOutlined /></template>
به‌روزرسانی از ته‌حساب به‌روزرسانی از ته‌حساب
</a-button> </a-button>
</a-flex> </a-space>
</a-col> </a-flex>
</a-row> </a-col>
<a-alert v-if="error" type="error" show-icon :message="error" closable @close="error = ''" /> <a-col v-if="error" :span="24">
<a-alert v-if="syncHint" type="info" show-icon :message="syncHint" /> <a-alert type="error" show-icon :message="error" closable @close="error = ''" />
</a-col>
<a-col v-if="syncHint" :span="24">
<a-alert type="info" show-icon :message="syncHint" />
</a-col>
<a-card :bordered="false" title="فیلترها"> <a-col :span="24">
<a-form layout="vertical"> <a-card :bordered="false">
<a-row :gutter="[16, 8]"> <template #title>
<a-col :xs="24" :sm="12" :md="8" :lg="6"> <a-space :size="8">
<a-form-item label="اتصال ته‌حساب"> <FilterOutlined />
<a-select <span>فیلترها</span>
v-model:value="connectionId" </a-space>
:options="connections.map((c) => ({ value: c.id, label: c.label }))" </template>
placeholder="اتصال" <a-form layout="vertical" @finish="applyFilters">
allow-clear <a-row :gutter="[16, 8]">
/> <a-col :xs="24" :sm="12" :md="8" :lg="6">
</a-form-item> <a-form-item label="اتصال">
</a-col> <a-select
<a-col :xs="24" :sm="12" :md="8" :lg="6"> v-model:value="connectionId"
<a-form-item label="از تاریخ"> :options="
<JalaliDateInput v-model="fromDate" placeholder="از" /> connections.map((c) => ({ value: c.id, label: c.label }))
</a-form-item> "
</a-col> placeholder="اتصال ته‌حساب"
<a-col :xs="24" :sm="12" :md="8" :lg="6"> allow-clear
<a-form-item label="تا تاریخ"> />
<JalaliDateInput v-model="toDate" placeholder="تا" /> </a-form-item>
</a-form-item> </a-col>
</a-col> <a-col :xs="24" :sm="12" :md="8" :lg="6">
<a-col :xs="24" :sm="12" :md="8" :lg="6"> <a-form-item label="از تاریخ" required>
<a-form-item label="گروه مشتری"> <JalaliDateInput v-model="fromDate" placeholder="از تاریخ" />
<a-select </a-form-item>
v-model:value="groupIds" </a-col>
mode="multiple" <a-col :xs="24" :sm="12" :md="8" :lg="6">
allow-clear <a-form-item label="تا تاریخ" required>
placeholder="همه گروه‌ها" <JalaliDateInput v-model="toDate" placeholder="تا تاریخ" />
:options="(customerGroups as CustomerGroupDto[]).map((g) => ({ value: g.id, label: g.name }))" </a-form-item>
:max-tag-count="2" </a-col>
/> <a-col :xs="24" :sm="12" :md="8" :lg="6">
</a-form-item> <a-form-item label="گروه مشتری">
</a-col> <a-select
<a-col :xs="24" :sm="12" :md="8" :lg="6"> v-model:value="groupIds"
<a-form-item label="نوع سند"> mode="multiple"
<a-select allow-clear
v-model:value="kinds" placeholder="همه گروه‌ها"
mode="multiple" :options="
:options="kindOptions" (customerGroups as CustomerGroupDto[]).map((g) => ({
placeholder="نوع" value: g.id,
:max-tag-count="2" label: g.name,
/> }))
</a-form-item> "
</a-col> :max-tag-count="2"
<a-col :xs="24" :sm="12" :md="8" :lg="6"> />
<a-form-item label="فلز"> </a-form-item>
<a-select </a-col>
v-model:value="jensFelez" <a-col :xs="24" :sm="12" :md="8" :lg="6">
allow-clear <a-form-item label="نوع سند">
placeholder="همه" <a-select
:options="metalOptions" v-model:value="kinds"
/> mode="multiple"
</a-form-item> :options="kindOptions"
</a-col> placeholder="فروش / مرجوعی / …"
<a-col :xs="24" :sm="12" :md="8" :lg="6"> :max-tag-count="2"
<a-form-item label="شخص"> />
<a-select </a-form-item>
v-model:value="personId" </a-col>
show-search <a-col :xs="24" :sm="12" :md="8" :lg="6">
allow-clear <a-form-item label="فلز">
placeholder="جستجوی نام…" <a-select
:filter-option="false" v-model:value="jensFelez"
:options="personOptions" allow-clear
@search="searchPeople" placeholder="همه"
/> :options="metalOptions"
</a-form-item> />
</a-col> </a-form-item>
</a-row> </a-col>
</a-form> <a-col :xs="24" :sm="12" :md="8" :lg="6">
</a-card> <a-form-item label="شخص">
<a-select
v-model:value="personId"
show-search
allow-clear
placeholder="جستجوی نام…"
:filter-option="false"
:options="personOptions"
@search="searchPeople"
>
<template #suffixIcon><SearchOutlined /></template>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-flex justify="space-between" align="center" wrap="wrap" :gap="8">
<a-typography-text v-if="lastSyncedLabel" type="secondary">
آخرین همگام‌سازی: {{ lastSyncedLabel }}
</a-typography-text>
<a-typography-text v-else type="secondary">
بعد از انتخاب بازه، فیلتر را اعمال کنید یا از ته‌حساب به‌روز کنید.
</a-typography-text>
<a-button type="primary" html-type="submit" :loading="loading">
اعمال فیلتر
</a-button>
</a-flex>
</a-form>
</a-card>
</a-col>
<a-row v-if="summary" :gutter="[16, 16]"> <a-col v-if="summary" :xs="24" :sm="8">
<a-col :xs="24" :sm="8"> <a-card :bordered="false">
<a-card :bordered="false"> <a-statistic
<a-statistic title="تعداد ردیف" :value="summary.totalLines" /> title="تعداد ردیف"
</a-card> :value="summary.totalLines"
</a-col> :formatter="(val) => toFaDigits(String(val))"
<a-col :xs="24" :sm="8"> />
<a-card :bordered="false"> </a-card>
<a-statistic title="جمع مبلغ" :value="formatMali(summary.maliSum)" /> </a-col>
</a-card> <a-col v-if="summary" :xs="24" :sm="8">
</a-col> <a-card :bordered="false">
<a-col :xs="24" :sm="8"> <a-statistic title="جمع مبلغ" :value="formatMali(summary.maliSum)" />
<a-card :bordered="false"> </a-card>
<a-statistic title="جمع وزن" :value="formatNum(summary.vaznSum)" /> </a-col>
</a-card> <a-col v-if="summary" :xs="24" :sm="8">
</a-col> <a-card :bordered="false">
</a-row> <a-statistic title="جمع وزن" :value="formatNum(summary.vaznSum)" />
</a-card>
</a-col>
<a-row v-if="summary?.byKind?.length" :gutter="[16, 16]"> <a-col v-if="summary?.byKind?.length" :xs="24" :lg="12">
<a-col :xs="24" :lg="12"> <a-card :bordered="false" title="به تفکیک نوع">
<a-card :bordered="false" title="به تفکیک نوع"> <a-list :data-source="summary.byKind" item-layout="horizontal">
<a-list <template #renderItem="{ item }">
:data-source="summary.byKind" <a-list-item>
item-layout="horizontal" <a-list-item-meta :title="item.label">
size="small" <template #description>
> {{ toFaDigits(String(item.lineCount)) }} ردیف ·
<template #renderItem="{ item }"> {{ formatMali(item.maliSum) }} · وزن {{ formatNum(item.vaznSum) }}
<a-list-item> </template>
<a-list-item-meta :title="item.label"> </a-list-item-meta>
<template #description> </a-list-item>
{{ toFaDigits(String(item.lineCount)) }} ردیف · </template>
{{ formatMali(item.maliSum) }} · وزن {{ formatNum(item.vaznSum) }} </a-list>
</template> </a-card>
</a-list-item-meta> </a-col>
</a-list-item> <a-col v-if="summary?.byGroup?.length" :xs="24" :lg="12">
</template> <a-card :bordered="false" title="به تفکیک گروه">
</a-list> <a-list
</a-card> :data-source="summary.byGroup.slice(0, 8)"
</a-col> item-layout="horizontal"
<a-col :xs="24" :lg="12"> >
<a-card :bordered="false" title="به تفکیک گروه"> <template #renderItem="{ item }">
<a-list <a-list-item>
:data-source="summary.byGroup.slice(0, 8)" <a-list-item-meta :title="item.label">
item-layout="horizontal" <template #description>
size="small" {{ toFaDigits(String(item.lineCount)) }} ردیف ·
> {{ formatMali(item.maliSum) }}
<template #renderItem="{ item }"> </template>
<a-list-item> </a-list-item-meta>
<a-list-item-meta :title="item.label"> </a-list-item>
<template #description> </template>
{{ toFaDigits(String(item.lineCount)) }} ردیف · </a-list>
{{ formatMali(item.maliSum) }} </a-card>
</template> </a-col>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
</a-row>
<a-card v-if="summary?.distinctNos?.length" :bordered="false" title="مقادیر خام NO در این بازه"> <a-col v-if="summary?.distinctNos?.length" :span="24">
<a-space wrap> <a-card :bordered="false" title="انواع سند خام (NO)">
<a-tag v-for="no in summary.distinctNos" :key="no">{{ no }}</a-tag> <a-space wrap>
</a-space> <a-tag v-for="no in summary.distinctNos" :key="no">{{ no }}</a-tag>
</a-card> </a-space>
</a-card>
</a-col>
<a-card :bordered="false" title="ردیف‌ها"> <a-col :span="24">
<a-table <a-card :bordered="false" title="ردیف‌های گزارش">
:columns="columns" <a-table
:data-source="report?.items ?? []" :columns="columns"
:loading="loading" :data-source="report?.items ?? []"
:pagination="{ :loading="loading"
current: page, :pagination="{
pageSize, current: page,
total: report?.total ?? 0, pageSize,
showSizeChanger: true, total: report?.total ?? 0,
pageSizeOptions: ['20', '50', '100'], showSizeChanger: true,
}" showTotal: (total) => `${toFaDigits(String(total))} ردیف`,
row-key="id" pageSizeOptions: ['20', '50', '100'],
:scroll="{ x: 1100 }" }"
@change="onTableChange" row-key="id"
/> :scroll="{ x: 1100 }"
</a-card> @change="onTableChange"
</a-flex> >
<template #emptyText>
<a-empty description="برای این بازه ردیفی نیست. بازه را اعمال کنید یا از ته‌حساب به‌روز کنید." />
</template>
</a-table>
</a-card>
</a-col>
</a-row>
</template> </template>