2026-10-22 18:20:46 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
|
|
|
|
import { useRouter } from 'vue-router';
|
2026-10-22 20:35:21 +03:30
|
|
|
|
import {
|
|
|
|
|
|
Alert as AAlert,
|
|
|
|
|
|
Button as AButton,
|
|
|
|
|
|
Card as ACard,
|
|
|
|
|
|
Col as ACol,
|
|
|
|
|
|
Empty as AEmpty,
|
|
|
|
|
|
Flex as AFlex,
|
|
|
|
|
|
Form as AForm,
|
|
|
|
|
|
FormItem as AFormItem,
|
|
|
|
|
|
List as AList,
|
|
|
|
|
|
Row as ARow,
|
|
|
|
|
|
Select as ASelect,
|
|
|
|
|
|
Space as ASpace,
|
|
|
|
|
|
Statistic as AStatistic,
|
|
|
|
|
|
Table as ATable,
|
|
|
|
|
|
Tag as ATag,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
message,
|
|
|
|
|
|
theme,
|
|
|
|
|
|
} from 'ant-design-vue';
|
2026-10-22 18:20:46 +03:30
|
|
|
|
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue';
|
2026-10-22 19:47:58 +03:30
|
|
|
|
import {
|
|
|
|
|
|
ArrowRightOutlined,
|
|
|
|
|
|
FilterOutlined,
|
|
|
|
|
|
SearchOutlined,
|
|
|
|
|
|
SyncOutlined,
|
|
|
|
|
|
} from '@ant-design/icons-vue';
|
2026-10-22 20:04:59 +03:30
|
|
|
|
import axios from 'axios';
|
2026-10-22 20:35:21 +03:30
|
|
|
|
|
|
|
|
|
|
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
|
|
|
|
|
const AListItem = AList.Item;
|
|
|
|
|
|
const AListItemMeta = AList.Item.Meta;
|
2026-10-22 18:20:46 +03:30
|
|
|
|
import {
|
|
|
|
|
|
TAHESAB_ASNAD_KIND_LABELS,
|
|
|
|
|
|
TAHESAB_ASNAD_KINDS,
|
|
|
|
|
|
TAHESAB_JENS_FELEZ_LABELS,
|
|
|
|
|
|
type CustomerGroupDto,
|
|
|
|
|
|
type PersonDto,
|
|
|
|
|
|
type TahesabAsnadKind,
|
|
|
|
|
|
type TahesabAsnadLineDto,
|
|
|
|
|
|
type TahesabAsnadReportPageDto,
|
|
|
|
|
|
type TahesabAsnadSummaryDto,
|
|
|
|
|
|
type TahesabAsnadSyncJobDto,
|
|
|
|
|
|
type TahesabConnectionDto,
|
|
|
|
|
|
} from '@mento/shared';
|
|
|
|
|
|
import { api } from '@/lib/api';
|
|
|
|
|
|
import {
|
|
|
|
|
|
dateToJalali,
|
|
|
|
|
|
formatLocalDateInput,
|
|
|
|
|
|
parseLocalInput,
|
|
|
|
|
|
toFaDigits,
|
|
|
|
|
|
} from '@/lib/jalali';
|
2026-10-24 21:47:46 +03:30
|
|
|
|
import { formatTahesabBedBes } from '@/lib/tahesab';
|
2026-10-22 18:20:46 +03:30
|
|
|
|
import { useCustomerGroups } from '@/lib/customer-groups';
|
|
|
|
|
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
|
|
|
|
|
|
|
|
|
|
|
const router = useRouter();
|
2026-10-22 19:47:58 +03:30
|
|
|
|
const { token } = theme.useToken();
|
2026-10-22 18:20:46 +03:30
|
|
|
|
const { groups: customerGroups, load: loadCustomerGroups } = useCustomerGroups();
|
|
|
|
|
|
|
|
|
|
|
|
const connections = ref<TahesabConnectionDto[]>([]);
|
|
|
|
|
|
const connectionId = ref<string | undefined>();
|
|
|
|
|
|
const fromDate = ref('');
|
|
|
|
|
|
const toDate = ref('');
|
|
|
|
|
|
const groupIds = ref<string[]>([]);
|
|
|
|
|
|
const personId = ref<string | undefined>();
|
|
|
|
|
|
const personOptions = ref<Array<{ value: string; label: string }>>([]);
|
|
|
|
|
|
const kinds = ref<TahesabAsnadKind[]>(['sale', 'return']);
|
|
|
|
|
|
const jensFelez = ref<number | undefined>();
|
|
|
|
|
|
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
|
const syncing = ref(false);
|
|
|
|
|
|
const error = ref('');
|
|
|
|
|
|
const syncHint = ref('');
|
|
|
|
|
|
const page = ref(1);
|
|
|
|
|
|
const pageSize = ref(50);
|
|
|
|
|
|
const report = ref<TahesabAsnadReportPageDto | null>(null);
|
|
|
|
|
|
const summary = ref<TahesabAsnadSummaryDto | null>(null);
|
|
|
|
|
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
const kindOptions = TAHESAB_ASNAD_KINDS.map((k) => ({
|
|
|
|
|
|
value: k,
|
|
|
|
|
|
label: TAHESAB_ASNAD_KIND_LABELS[k],
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const metalOptions = [
|
|
|
|
|
|
{ value: 0, label: TAHESAB_JENS_FELEZ_LABELS[0] },
|
|
|
|
|
|
{ value: 1, label: TAHESAB_JENS_FELEZ_LABELS[1] },
|
|
|
|
|
|
{ value: 2, label: TAHESAB_JENS_FELEZ_LABELS[2] },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
function toShamsiYmd(localIso: string): string | null {
|
|
|
|
|
|
const d = parseLocalInput(localIso);
|
|
|
|
|
|
if (!d) return null;
|
|
|
|
|
|
const { jy, jm, jd } = dateToJalali(d);
|
|
|
|
|
|
return `${jy}-${String(jm).padStart(2, '0')}-${String(jd).padStart(2, '0')}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function defaultRange() {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
const from = new Date(now);
|
|
|
|
|
|
from.setDate(from.getDate() - 30);
|
|
|
|
|
|
fromDate.value = formatLocalDateInput(from);
|
|
|
|
|
|
toDate.value = formatLocalDateInput(now);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function kindLabel(k: string) {
|
|
|
|
|
|
return TAHESAB_ASNAD_KIND_LABELS[k as TahesabAsnadKind] ?? k;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function metalLabel(v: number | null) {
|
|
|
|
|
|
if (v == null) return '—';
|
|
|
|
|
|
return TAHESAB_JENS_FELEZ_LABELS[v] ?? String(v);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatNum(n: number | null | undefined) {
|
|
|
|
|
|
if (n == null) return '—';
|
2026-10-22 19:47:58 +03:30
|
|
|
|
return toFaDigits(
|
|
|
|
|
|
new Intl.NumberFormat('fa-IR', { maximumFractionDigits: 3 }).format(n),
|
|
|
|
|
|
);
|
2026-10-22 18:20:46 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatMali(n: number | null | undefined) {
|
2026-10-24 21:47:46 +03:30
|
|
|
|
return formatTahesabBedBes(n);
|
2026-10-22 18:20:46 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const columns: TableColumnsType<TahesabAsnadLineDto> = [
|
2026-10-22 19:47:58 +03:30
|
|
|
|
{
|
|
|
|
|
|
title: 'تاریخ',
|
|
|
|
|
|
dataIndex: 'tarikh',
|
|
|
|
|
|
key: 'tarikh',
|
|
|
|
|
|
width: 110,
|
|
|
|
|
|
customRender: ({ text }) => toFaDigits(String(text ?? '')),
|
|
|
|
|
|
},
|
2026-10-22 18:20:46 +03:30
|
|
|
|
{
|
|
|
|
|
|
title: 'شخص',
|
|
|
|
|
|
key: 'person',
|
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
|
customRender: ({ record }) =>
|
|
|
|
|
|
record.personName
|
|
|
|
|
|
? `${record.personName} (${toFaDigits(String(record.moshtariCode))})`
|
|
|
|
|
|
: toFaDigits(String(record.moshtariCode)),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'گروه',
|
|
|
|
|
|
key: 'groups',
|
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
|
customRender: ({ record }) =>
|
|
|
|
|
|
record.groups?.map((g) => g.name).join('، ') || '—',
|
|
|
|
|
|
},
|
|
|
|
|
|
{ title: 'نوع سند (NO)', dataIndex: 'noRaw', key: 'noRaw', ellipsis: true },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'طبقهبندی',
|
|
|
|
|
|
key: 'kind',
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
customRender: ({ record }) => kindLabel(record.kind),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'فلز',
|
|
|
|
|
|
key: 'jensFelez',
|
|
|
|
|
|
width: 90,
|
|
|
|
|
|
customRender: ({ record }) => metalLabel(record.jensFelez),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'وزن',
|
|
|
|
|
|
key: 'vazn',
|
|
|
|
|
|
width: 90,
|
|
|
|
|
|
customRender: ({ record }) => formatNum(record.vazn),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'مبلغ',
|
|
|
|
|
|
key: 'mali',
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
customRender: ({ record }) => formatMali(record.mali),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'ش. فاکتور',
|
|
|
|
|
|
key: 'shFactor',
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
customRender: ({ record }) =>
|
|
|
|
|
|
record.shFactor != null ? toFaDigits(String(record.shFactor)) : '—',
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const filterParams = computed(() => {
|
|
|
|
|
|
const from = toShamsiYmd(fromDate.value);
|
|
|
|
|
|
const to = toShamsiYmd(toDate.value);
|
|
|
|
|
|
if (!from || !to) return null;
|
|
|
|
|
|
return {
|
|
|
|
|
|
connectionId: connectionId.value,
|
|
|
|
|
|
from,
|
|
|
|
|
|
to,
|
|
|
|
|
|
groupIds: groupIds.value.length ? groupIds.value.join(',') : undefined,
|
|
|
|
|
|
personId: personId.value,
|
|
|
|
|
|
kinds: kinds.value.length ? kinds.value.join(',') : undefined,
|
|
|
|
|
|
jensFelez: jensFelez.value,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-22 19:47:58 +03:30
|
|
|
|
const lastSyncedLabel = computed(() => {
|
|
|
|
|
|
const raw = summary.value?.lastSyncedAt;
|
|
|
|
|
|
if (!raw) return null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
return toFaDigits(new Date(raw).toLocaleString('fa-IR'));
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-22 18:20:46 +03:30
|
|
|
|
async function loadConnections() {
|
|
|
|
|
|
const { data } = await api.get<TahesabConnectionDto[]>(
|
|
|
|
|
|
'/integrations/tahesab/connections/public',
|
|
|
|
|
|
);
|
|
|
|
|
|
connections.value = data.filter((c) => c.enabled);
|
|
|
|
|
|
const def =
|
|
|
|
|
|
connections.value.find((c) => c.isDefault) ?? connections.value[0];
|
|
|
|
|
|
if (!connectionId.value && def) connectionId.value = def.id;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function searchPeople(q: string) {
|
|
|
|
|
|
if (!q || q.trim().length < 2) {
|
|
|
|
|
|
personOptions.value = [];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.get<{ items: PersonDto[] }>('/people', {
|
|
|
|
|
|
params: { q: q.trim(), limit: 20 },
|
|
|
|
|
|
});
|
|
|
|
|
|
const items = Array.isArray(data?.items) ? data.items : [];
|
|
|
|
|
|
personOptions.value = items.map((p) => ({
|
|
|
|
|
|
value: p.id,
|
|
|
|
|
|
label: [p.firstName, p.lastName].filter(Boolean).join(' ') || p.id,
|
|
|
|
|
|
}));
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
personOptions.value = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 20:04:59 +03:30
|
|
|
|
function apiErrMessage(e: unknown, fallback: string): string {
|
|
|
|
|
|
if (!axios.isAxiosError(e)) return fallback;
|
|
|
|
|
|
const raw = e.response?.data as { message?: string | string[] } | undefined;
|
|
|
|
|
|
const msg = raw?.message;
|
|
|
|
|
|
if (Array.isArray(msg)) return msg.filter(Boolean).join(' · ') || fallback;
|
|
|
|
|
|
if (typeof msg === 'string' && msg.trim()) return msg;
|
|
|
|
|
|
return fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadReport(opts?: { notifyEmpty?: boolean }) {
|
2026-10-22 18:20:46 +03:30
|
|
|
|
const params = filterParams.value;
|
|
|
|
|
|
if (!params) {
|
|
|
|
|
|
error.value = 'تاریخ از و تا را انتخاب کنید.';
|
2026-10-22 20:04:59 +03:30
|
|
|
|
message.warning(error.value);
|
2026-10-22 18:20:46 +03:30
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
error.value = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [pageRes, sumRes] = await Promise.all([
|
|
|
|
|
|
api.get<TahesabAsnadReportPageDto>('/integrations/tahesab/reports/asnad', {
|
|
|
|
|
|
params: { ...params, page: page.value, pageSize: pageSize.value },
|
|
|
|
|
|
}),
|
|
|
|
|
|
api.get<TahesabAsnadSummaryDto>(
|
|
|
|
|
|
'/integrations/tahesab/reports/asnad/summary',
|
|
|
|
|
|
{ params },
|
|
|
|
|
|
),
|
|
|
|
|
|
]);
|
|
|
|
|
|
report.value = pageRes.data;
|
|
|
|
|
|
summary.value = sumRes.data;
|
2026-10-22 20:04:59 +03:30
|
|
|
|
if (opts?.notifyEmpty && (pageRes.data.total ?? 0) === 0) {
|
|
|
|
|
|
message.info(
|
|
|
|
|
|
'ردیفی نیست. اول «بهروزرسانی از تهحساب» را برای همین بازه بزنید.',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-10-22 18:20:46 +03:30
|
|
|
|
} catch (e: unknown) {
|
2026-10-22 20:04:59 +03:30
|
|
|
|
error.value = apiErrMessage(e, 'بارگذاری گزارش ناموفق بود.');
|
|
|
|
|
|
message.error(error.value);
|
2026-10-22 18:20:46 +03:30
|
|
|
|
report.value = null;
|
|
|
|
|
|
summary.value = null;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopPoll() {
|
|
|
|
|
|
if (pollTimer) {
|
|
|
|
|
|
clearInterval(pollTimer);
|
|
|
|
|
|
pollTimer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function pollJob(jobId: string) {
|
|
|
|
|
|
stopPoll();
|
|
|
|
|
|
pollTimer = setInterval(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.get<TahesabAsnadSyncJobDto>(
|
|
|
|
|
|
`/integrations/tahesab/reports/asnad/sync/${jobId}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
syncHint.value = `همگامسازی: ${toFaDigits(String(data.processed))} از ${toFaDigits(String(data.total))} · ${toFaDigits(String(data.linesUpserted))} ردیف`;
|
|
|
|
|
|
if (data.status === 'done' || data.status === 'failed') {
|
|
|
|
|
|
stopPoll();
|
|
|
|
|
|
syncing.value = false;
|
|
|
|
|
|
if (data.status === 'failed') {
|
|
|
|
|
|
error.value = data.errorMessage || 'همگامسازی ناموفق بود.';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (data.truncatedPersonIds?.length) {
|
|
|
|
|
|
syncHint.value += ` · هشدار: برای ${toFaDigits(String(data.truncatedPersonIds.length))} مشتری ممکن است اسناد ناقص باشد (سقف ۲۰۰).`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
syncHint.value = `همگامسازی تمام شد · ${toFaDigits(String(data.linesUpserted))} ردیف`;
|
|
|
|
|
|
}
|
|
|
|
|
|
await loadReport();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
stopPoll();
|
|
|
|
|
|
syncing.value = false;
|
|
|
|
|
|
error.value = 'وضعیت همگامسازی دریافت نشد.';
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1500);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function startSync() {
|
|
|
|
|
|
const params = filterParams.value;
|
|
|
|
|
|
if (!params) {
|
2026-10-22 19:47:58 +03:30
|
|
|
|
error.value = 'ابتدا بازه تاریخ را مشخص کنید، بعد بهروزرسانی را بزنید.';
|
2026-10-22 20:04:59 +03:30
|
|
|
|
message.warning(error.value);
|
2026-10-22 18:20:46 +03:30
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
syncing.value = true;
|
|
|
|
|
|
error.value = '';
|
|
|
|
|
|
syncHint.value = 'شروع همگامسازی…';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.post<TahesabAsnadSyncJobDto>(
|
|
|
|
|
|
'/integrations/tahesab/reports/asnad/sync',
|
|
|
|
|
|
{
|
|
|
|
|
|
connectionId: params.connectionId,
|
|
|
|
|
|
from: params.from,
|
|
|
|
|
|
to: params.to,
|
|
|
|
|
|
},
|
|
|
|
|
|
{ timeout: 60_000 },
|
|
|
|
|
|
);
|
2026-10-22 20:04:59 +03:30
|
|
|
|
message.success('همگامسازی شروع شد.');
|
2026-10-22 18:20:46 +03:30
|
|
|
|
await pollJob(data.id);
|
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
|
syncing.value = false;
|
2026-10-22 20:04:59 +03:30
|
|
|
|
syncHint.value = '';
|
|
|
|
|
|
error.value = apiErrMessage(e, 'شروع همگامسازی ناموفق بود.');
|
|
|
|
|
|
message.error(error.value);
|
2026-10-22 18:20:46 +03:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 19:47:58 +03:30
|
|
|
|
function applyFilters() {
|
|
|
|
|
|
page.value = 1;
|
2026-10-22 20:04:59 +03:30
|
|
|
|
void loadReport({ notifyEmpty: true });
|
2026-10-22 19:47:58 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 20:04:59 +03:30
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
defaultRange();
|
|
|
|
|
|
try {
|
|
|
|
|
|
await Promise.all([loadConnections(), loadCustomerGroups()]);
|
|
|
|
|
|
await loadReport();
|
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
|
error.value = apiErrMessage(e, 'بارگذاری اولیه ناموفق بود.');
|
|
|
|
|
|
message.error(error.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-22 18:20:46 +03:30
|
|
|
|
function onTableChange(pag: TablePaginationConfig) {
|
|
|
|
|
|
page.value = pag.current ?? 1;
|
|
|
|
|
|
pageSize.value = pag.pageSize ?? 50;
|
|
|
|
|
|
void loadReport();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch([connectionId, groupIds, kinds, jensFelez, personId], () => {
|
|
|
|
|
|
page.value = 1;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => stopPoll());
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-10-22 19:47:58 +03:30
|
|
|
|
<a-row :gutter="[24, 24]">
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-flex
|
|
|
|
|
|
justify="space-between"
|
|
|
|
|
|
align="center"
|
|
|
|
|
|
wrap="wrap"
|
|
|
|
|
|
:gap="token.marginSM"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<a-typography-title :level="3" :style="{ margin: 0 }">
|
|
|
|
|
|
گزارش فروش تهحساب
|
|
|
|
|
|
</a-typography-title>
|
|
|
|
|
|
<a-typography-text type="secondary">
|
|
|
|
|
|
فروش و مرجوعی مشتریان لینکشده بر اساس بازه تاریخ
|
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-space :size="8" wrap>
|
|
|
|
|
|
<a-button @click="router.push({ name: 'reports' })">
|
|
|
|
|
|
<template #icon><ArrowRightOutlined /></template>
|
|
|
|
|
|
بازگشت
|
2026-10-22 18:20:46 +03:30
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button type="primary" :loading="syncing" @click="startSync">
|
|
|
|
|
|
<template #icon><SyncOutlined /></template>
|
|
|
|
|
|
بهروزرسانی از تهحساب
|
|
|
|
|
|
</a-button>
|
2026-10-22 19:47:58 +03:30
|
|
|
|
</a-space>
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
</a-col>
|
2026-10-22 18:20:46 +03:30
|
|
|
|
|
2026-10-22 19:47:58 +03:30
|
|
|
|
<a-col v-if="error" :span="24">
|
|
|
|
|
|
<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>
|
2026-10-22 18:20:46 +03:30
|
|
|
|
|
2026-10-22 19:47:58 +03:30
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-card :bordered="false">
|
|
|
|
|
|
<template #title>
|
|
|
|
|
|
<a-space :size="8">
|
|
|
|
|
|
<FilterOutlined />
|
|
|
|
|
|
<span>فیلترها</span>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</template>
|
2026-10-22 20:04:59 +03:30
|
|
|
|
<a-form layout="vertical" @submit.prevent>
|
2026-10-22 19:47:58 +03:30
|
|
|
|
<a-row :gutter="[16, 8]">
|
|
|
|
|
|
<a-col :xs="24" :sm="12" :md="8" :lg="6">
|
|
|
|
|
|
<a-form-item label="اتصال">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="connectionId"
|
|
|
|
|
|
:options="
|
|
|
|
|
|
connections.map((c) => ({ value: c.id, label: c.label }))
|
|
|
|
|
|
"
|
|
|
|
|
|
placeholder="اتصال تهحساب"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :sm="12" :md="8" :lg="6">
|
2026-10-22 20:04:59 +03:30
|
|
|
|
<a-form-item label="از تاریخ">
|
2026-10-22 19:47:58 +03:30
|
|
|
|
<JalaliDateInput v-model="fromDate" placeholder="از تاریخ" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :sm="12" :md="8" :lg="6">
|
2026-10-22 20:04:59 +03:30
|
|
|
|
<a-form-item label="تا تاریخ">
|
2026-10-22 19:47:58 +03:30
|
|
|
|
<JalaliDateInput v-model="toDate" placeholder="تا تاریخ" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :sm="12" :md="8" :lg="6">
|
|
|
|
|
|
<a-form-item label="گروه مشتری">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="groupIds"
|
|
|
|
|
|
mode="multiple"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
placeholder="همه گروهها"
|
|
|
|
|
|
:options="
|
|
|
|
|
|
(customerGroups as CustomerGroupDto[]).map((g) => ({
|
|
|
|
|
|
value: g.id,
|
|
|
|
|
|
label: g.name,
|
|
|
|
|
|
}))
|
|
|
|
|
|
"
|
|
|
|
|
|
:max-tag-count="2"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :sm="12" :md="8" :lg="6">
|
|
|
|
|
|
<a-form-item label="نوع سند">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="kinds"
|
|
|
|
|
|
mode="multiple"
|
|
|
|
|
|
:options="kindOptions"
|
|
|
|
|
|
placeholder="فروش / مرجوعی / …"
|
|
|
|
|
|
:max-tag-count="2"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :sm="12" :md="8" :lg="6">
|
|
|
|
|
|
<a-form-item label="فلز">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="jensFelez"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
placeholder="همه"
|
|
|
|
|
|
:options="metalOptions"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :sm="12" :md="8" :lg="6">
|
|
|
|
|
|
<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>
|
2026-10-22 20:04:59 +03:30
|
|
|
|
<a-button type="primary" :loading="loading" @click="applyFilters">
|
2026-10-22 19:47:58 +03:30
|
|
|
|
اعمال فیلتر
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</a-col>
|
2026-10-22 18:20:46 +03:30
|
|
|
|
|
2026-10-22 19:47:58 +03:30
|
|
|
|
<a-col v-if="summary" :xs="24" :sm="8">
|
|
|
|
|
|
<a-card :bordered="false">
|
|
|
|
|
|
<a-statistic
|
|
|
|
|
|
title="تعداد ردیف"
|
2026-10-22 19:52:11 +03:30
|
|
|
|
:value="toFaDigits(String(summary.totalLines))"
|
2026-10-22 19:47:58 +03:30
|
|
|
|
/>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col v-if="summary" :xs="24" :sm="8">
|
|
|
|
|
|
<a-card :bordered="false">
|
|
|
|
|
|
<a-statistic title="جمع مبلغ" :value="formatMali(summary.maliSum)" />
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col v-if="summary" :xs="24" :sm="8">
|
|
|
|
|
|
<a-card :bordered="false">
|
|
|
|
|
|
<a-statistic title="جمع وزن" :value="formatNum(summary.vaznSum)" />
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</a-col>
|
2026-10-22 18:20:46 +03:30
|
|
|
|
|
2026-10-22 19:47:58 +03:30
|
|
|
|
<a-col v-if="summary?.byKind?.length" :xs="24" :lg="12">
|
|
|
|
|
|
<a-card :bordered="false" title="به تفکیک نوع">
|
|
|
|
|
|
<a-list :data-source="summary.byKind" item-layout="horizontal">
|
|
|
|
|
|
<template #renderItem="{ item }">
|
|
|
|
|
|
<a-list-item>
|
|
|
|
|
|
<a-list-item-meta :title="item.label">
|
|
|
|
|
|
<template #description>
|
|
|
|
|
|
{{ toFaDigits(String(item.lineCount)) }} ردیف ·
|
|
|
|
|
|
{{ formatMali(item.maliSum) }} · وزن {{ formatNum(item.vaznSum) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list-item-meta>
|
|
|
|
|
|
</a-list-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col v-if="summary?.byGroup?.length" :xs="24" :lg="12">
|
|
|
|
|
|
<a-card :bordered="false" title="به تفکیک گروه">
|
|
|
|
|
|
<a-list
|
|
|
|
|
|
:data-source="summary.byGroup.slice(0, 8)"
|
|
|
|
|
|
item-layout="horizontal"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #renderItem="{ item }">
|
|
|
|
|
|
<a-list-item>
|
|
|
|
|
|
<a-list-item-meta :title="item.label">
|
|
|
|
|
|
<template #description>
|
|
|
|
|
|
{{ toFaDigits(String(item.lineCount)) }} ردیف ·
|
|
|
|
|
|
{{ formatMali(item.maliSum) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list-item-meta>
|
|
|
|
|
|
</a-list-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</a-col>
|
2026-10-22 18:20:46 +03:30
|
|
|
|
|
2026-10-22 19:47:58 +03:30
|
|
|
|
<a-col v-if="summary?.distinctNos?.length" :span="24">
|
|
|
|
|
|
<a-card :bordered="false" title="انواع سند خام (NO)">
|
|
|
|
|
|
<a-space wrap>
|
|
|
|
|
|
<a-tag v-for="no in summary.distinctNos" :key="no">{{ no }}</a-tag>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</a-col>
|
2026-10-22 18:20:46 +03:30
|
|
|
|
|
2026-10-22 19:47:58 +03:30
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-card :bordered="false" title="ردیفهای گزارش">
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
:columns="columns"
|
|
|
|
|
|
:data-source="report?.items ?? []"
|
|
|
|
|
|
:loading="loading"
|
|
|
|
|
|
:pagination="{
|
|
|
|
|
|
current: page,
|
|
|
|
|
|
pageSize,
|
|
|
|
|
|
total: report?.total ?? 0,
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
showTotal: (total) => `${toFaDigits(String(total))} ردیف`,
|
|
|
|
|
|
pageSizeOptions: ['20', '50', '100'],
|
|
|
|
|
|
}"
|
|
|
|
|
|
row-key="id"
|
|
|
|
|
|
:scroll="{ x: 1100 }"
|
|
|
|
|
|
@change="onTableChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #emptyText>
|
|
|
|
|
|
<a-empty description="برای این بازه ردیفی نیست. بازه را اعمال کنید یا از تهحساب بهروز کنید." />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
2026-10-22 18:20:46 +03:30
|
|
|
|
</template>
|