feat(tahesab): sync filtered customers and pick Jalali periods
Document refresh can follow the selected group or person instead of every linked customer, and the sales report picks a named month or season. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -38,6 +38,12 @@ import {
|
||||
|
||||
const ASNAD_COUNT_LAST = 5000;
|
||||
const SYNC_CONCURRENCY = 3;
|
||||
|
||||
function latinDigits(value: string): string {
|
||||
return value
|
||||
.replace(/[۰-۹]/g, (ch) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(ch)))
|
||||
.replace(/[٠-٩]/g, (ch) => String('٠١٢٣٤٥٦٧٨٩'.indexOf(ch)));
|
||||
}
|
||||
const ASNAD_KINDS = new Set<string>(['sale', 'return', 'purchase', 'other']);
|
||||
|
||||
function normalizeShamsiYmd(input: string): string {
|
||||
@@ -184,6 +190,7 @@ export class TahesabAsnadReportService {
|
||||
connectionId: string | undefined,
|
||||
from: string,
|
||||
to: string,
|
||||
scope?: { personId?: string; groupIds?: string[] },
|
||||
): Promise<TahesabAsnadSyncJobDto> {
|
||||
const fromNorm = normalizeShamsiYmd(from);
|
||||
const toNorm = normalizeShamsiYmd(to);
|
||||
@@ -194,6 +201,22 @@ export class TahesabAsnadReportService {
|
||||
organizationId,
|
||||
connectionId,
|
||||
);
|
||||
const scopedPersonIds = await this.resolveSyncPersonIds(organizationId, scope);
|
||||
if (scopedPersonIds && scopedPersonIds.length === 0) {
|
||||
throw new BadRequestException(
|
||||
'برای گروه یا شخص انتخابشده مشتریای پیدا نشد.',
|
||||
);
|
||||
}
|
||||
if (scopedPersonIds) {
|
||||
const linked = await this.prisma.personAccountingLink.count({
|
||||
where: { connectionId: resolvedId, personId: { in: scopedPersonIds } },
|
||||
});
|
||||
if (linked === 0) {
|
||||
throw new BadRequestException(
|
||||
'مشتریان این فیلتر به تهحساب وصل نیستند. بهروزرسانی همه را بزنید یا اول اتصال مشتری را انجام دهید.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const job = await this.prisma.accountingSyncJob.create({
|
||||
data: {
|
||||
@@ -201,7 +224,13 @@ export class TahesabAsnadReportService {
|
||||
connectionId: resolvedId,
|
||||
kind: 'asnad-sync',
|
||||
status: 'pending',
|
||||
request: { from: fromNorm, to: toNorm, connectionId: resolvedId },
|
||||
request: {
|
||||
from: fromNorm,
|
||||
to: toNorm,
|
||||
connectionId: resolvedId,
|
||||
personId: scope?.personId ?? null,
|
||||
groupIds: scope?.groupIds ?? [],
|
||||
},
|
||||
result: {
|
||||
connectionId: resolvedId,
|
||||
from: fromNorm,
|
||||
@@ -214,9 +243,14 @@ export class TahesabAsnadReportService {
|
||||
},
|
||||
});
|
||||
|
||||
void this.runSync(job.id, organizationId, resolvedId, fromNorm, toNorm).catch(
|
||||
() => undefined,
|
||||
);
|
||||
void this.runSync(
|
||||
job.id,
|
||||
organizationId,
|
||||
resolvedId,
|
||||
fromNorm,
|
||||
toNorm,
|
||||
scopedPersonIds,
|
||||
).catch(() => undefined);
|
||||
|
||||
return this.mapJob(job);
|
||||
}
|
||||
@@ -238,6 +272,7 @@ export class TahesabAsnadReportService {
|
||||
connectionId: string,
|
||||
from: string,
|
||||
to: string,
|
||||
personIds: string[] | null,
|
||||
) {
|
||||
const progress: SyncProgress = {
|
||||
connectionId,
|
||||
@@ -257,7 +292,10 @@ export class TahesabAsnadReportService {
|
||||
|
||||
const { api } = await this.connections.getApi(organizationId, connectionId);
|
||||
const links = await this.prisma.personAccountingLink.findMany({
|
||||
where: { connectionId },
|
||||
where: {
|
||||
connectionId,
|
||||
...(personIds ? { personId: { in: personIds } } : {}),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
personId: true,
|
||||
@@ -276,6 +314,7 @@ export class TahesabAsnadReportService {
|
||||
organizationId,
|
||||
connectionId,
|
||||
tarikh: { gte: from, lte: to },
|
||||
...(personIds ? { personId: { in: personIds } } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -638,6 +677,82 @@ export class TahesabAsnadReportService {
|
||||
};
|
||||
}
|
||||
|
||||
/** null یعنی همهٔ مشتریان متصل. آرایه یعنی فقط همین اشخاص. */
|
||||
private async resolveSyncPersonIds(
|
||||
organizationId: string,
|
||||
scope?: { personId?: string; groupIds?: string[] },
|
||||
): Promise<string[] | null> {
|
||||
const groupIds = (scope?.groupIds ?? []).map((id) => id.trim()).filter(Boolean);
|
||||
const personId = scope?.personId?.trim() || undefined;
|
||||
if (!personId && groupIds.length === 0) return null;
|
||||
let ids: string[] | null = null;
|
||||
if (groupIds.length) {
|
||||
const expanded = await this.expandGroupIds(organizationId, groupIds);
|
||||
const rows = await this.prisma.personGroupLink.findMany({
|
||||
where: {
|
||||
groupId: { in: expanded },
|
||||
person: { organizationId },
|
||||
},
|
||||
select: { personId: true },
|
||||
distinct: ['personId'],
|
||||
});
|
||||
ids = rows.map((row) => row.personId);
|
||||
}
|
||||
if (personId) ids = ids ? ids.filter((id) => id === personId) : [personId];
|
||||
return ids ?? [];
|
||||
}
|
||||
|
||||
async searchCustomers(
|
||||
organizationId: string,
|
||||
connectionId: string | undefined,
|
||||
q: string,
|
||||
) {
|
||||
const resolvedId = await this.connections.resolveConnectionId(
|
||||
organizationId,
|
||||
connectionId,
|
||||
);
|
||||
const query = q.trim();
|
||||
const tokens = query.split(/\s+/).filter(Boolean);
|
||||
const code = Number(latinDigits(query));
|
||||
const or: Prisma.PersonAccountingLinkWhereInput[] = [];
|
||||
if (tokens.length) {
|
||||
or.push({
|
||||
person: {
|
||||
organizationId,
|
||||
AND: tokens.map((token) => ({
|
||||
OR: [
|
||||
{ firstName: { contains: token } },
|
||||
{ lastName: { contains: token } },
|
||||
],
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
if (Number.isInteger(code) && code > 0 && latinDigits(query) === String(code)) {
|
||||
or.push({ moshtariCode: code, person: { organizationId } });
|
||||
}
|
||||
const rows = await this.prisma.personAccountingLink.findMany({
|
||||
where: {
|
||||
connectionId: resolvedId,
|
||||
...(or.length ? { OR: or } : { person: { organizationId } }),
|
||||
},
|
||||
take: 30,
|
||||
orderBy: [{ person: { lastName: 'asc' } }, { person: { firstName: 'asc' } }],
|
||||
select: {
|
||||
personId: true,
|
||||
moshtariCode: true,
|
||||
person: { select: { firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
return {
|
||||
items: rows.map((row) => ({
|
||||
personId: row.personId,
|
||||
moshtariCode: row.moshtariCode,
|
||||
name: `${row.person.firstName} ${row.person.lastName}`.trim(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/** گروه اصلی، زیرگروهها را هم داخل فیلتر میآورد. */
|
||||
private async expandGroupIds(organizationId: string, ids: string[]): Promise<string[]> {
|
||||
const rows = await this.prisma.customerGroup.findMany({
|
||||
|
||||
@@ -447,6 +447,18 @@ export class SyncAsnadReportDto {
|
||||
@MinLength(8)
|
||||
@MaxLength(12)
|
||||
to!: string;
|
||||
|
||||
/** اگر باشد، فقط سندهای همین شخص گرفته میشود */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
personId?: string;
|
||||
|
||||
/** اگر باشد، فقط مشتریان این گروهها (و زیرگروهها) گرفته میشوند */
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ArrayMaxSize(200)
|
||||
groupIds?: string[];
|
||||
}
|
||||
|
||||
export class UpdateTahesabInvoiceSettingsDto {
|
||||
|
||||
@@ -309,6 +309,20 @@ export class TahesabController {
|
||||
dto.connectionId,
|
||||
dto.from,
|
||||
dto.to,
|
||||
{ personId: dto.personId, groupIds: dto.groupIds },
|
||||
);
|
||||
}
|
||||
|
||||
@Get('reports/asnad/customers')
|
||||
searchAsnadCustomers(
|
||||
@CurrentUser() user: { organizationId: string },
|
||||
@Query('connectionId') connectionId?: string,
|
||||
@Query('q') q?: string,
|
||||
) {
|
||||
return this.asnadReport.searchCustomers(
|
||||
user.organizationId,
|
||||
connectionId,
|
||||
q ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Form as AForm,
|
||||
FormItem as AFormItem,
|
||||
List as AList,
|
||||
Modal as AModal,
|
||||
Row as ARow,
|
||||
Segmented as ASegmented,
|
||||
Select as ASelect,
|
||||
@@ -41,7 +42,6 @@ import {
|
||||
TAHESAB_ASNAD_KINDS,
|
||||
TAHESAB_JENS_FELEZ_LABELS,
|
||||
type CustomerGroupDto,
|
||||
type PersonDto,
|
||||
type TahesabAsnadKind,
|
||||
type TahesabAsnadLineDto,
|
||||
type TahesabAsnadReportPageDto,
|
||||
@@ -54,6 +54,8 @@ import { api } from '@/lib/api';
|
||||
import {
|
||||
dateToJalali,
|
||||
formatLocalDateInput,
|
||||
jalaliMonthLength,
|
||||
jalaliMonthName,
|
||||
jalaliToDate,
|
||||
parseLocalInput,
|
||||
toFaDigits,
|
||||
@@ -78,6 +80,20 @@ const personId = ref<string | undefined>();
|
||||
const personOptions = ref<Array<{ value: string; label: string }>>([]);
|
||||
const kinds = ref<TahesabAsnadKind[]>([]);
|
||||
const period = ref<'custom' | 'week' | 'month' | 'season'>('month');
|
||||
const periodModalOpen = ref(false);
|
||||
const periodModalKind = ref<'month' | 'season'>('month');
|
||||
const pickerYear = ref(dateToJalali(new Date()).jy);
|
||||
const syncModalOpen = ref(false);
|
||||
const SEASONS = [
|
||||
{ key: 'spring', label: 'بهار', start: 1, end: 3 },
|
||||
{ key: 'summer', label: 'تابستان', start: 4, end: 6 },
|
||||
{ key: 'autumn', label: 'پاییز', start: 7, end: 9 },
|
||||
{ key: 'winter', label: 'زمستان', start: 10, end: 12 },
|
||||
] as const;
|
||||
const pickerYears = computed(() => {
|
||||
const current = dateToJalali(new Date()).jy;
|
||||
return Array.from({ length: 8 }, (_, index) => current - index);
|
||||
});
|
||||
const overview = ref<TahesabSalesOverviewDto | null>(null);
|
||||
const auth = useAuthStore();
|
||||
const canGroups = computed(() => canManageSettings(auth.user?.role));
|
||||
@@ -203,6 +219,66 @@ function markCustomPeriod() {
|
||||
period.value = 'custom';
|
||||
}
|
||||
|
||||
function openPeriodModal(kind: 'month' | 'season') {
|
||||
const parsed = parseLocalInput(fromDate.value);
|
||||
pickerYear.value = parsed ? dateToJalali(parsed).jy : dateToJalali(new Date()).jy;
|
||||
periodModalKind.value = kind;
|
||||
periodModalOpen.value = true;
|
||||
}
|
||||
|
||||
function onPeriodPick(value: string | number) {
|
||||
const key = String(value) as 'week' | 'month' | 'season' | 'custom';
|
||||
if (key === 'month' || key === 'season') {
|
||||
openPeriodModal(key);
|
||||
return;
|
||||
}
|
||||
applyNamedPeriod(key, true);
|
||||
}
|
||||
|
||||
function onPeriodReclick(event: MouseEvent) {
|
||||
const item = (event.target as HTMLElement | null)?.closest('.ant-segmented-item');
|
||||
const text = item?.querySelector('.ant-segmented-item-label')?.textContent?.replace(/\s/g, '') ?? '';
|
||||
if (text === 'ماه') openPeriodModal('month');
|
||||
if (text === 'فصل') openPeriodModal('season');
|
||||
}
|
||||
|
||||
function applyJalaliSpan(
|
||||
year: number,
|
||||
startMonth: number,
|
||||
endMonth: number,
|
||||
key: 'month' | 'season',
|
||||
) {
|
||||
const now = new Date();
|
||||
const today = dateToJalali(now);
|
||||
const start = jalaliToDate(year, startMonth, 1);
|
||||
let end = jalaliToDate(year, endMonth, jalaliMonthLength(year, endMonth));
|
||||
if (
|
||||
today.jy === year &&
|
||||
today.jm >= startMonth &&
|
||||
today.jm <= endMonth &&
|
||||
end.getTime() > now.getTime()
|
||||
) {
|
||||
end = now;
|
||||
}
|
||||
applyingPeriod = true;
|
||||
period.value = key;
|
||||
fromDate.value = formatLocalDateInput(start);
|
||||
toDate.value = formatLocalDateInput(end);
|
||||
periodModalOpen.value = false;
|
||||
void nextTick(() => {
|
||||
applyingPeriod = false;
|
||||
});
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function pickMonth(month: number) {
|
||||
applyJalaliSpan(pickerYear.value, month, month, 'month');
|
||||
}
|
||||
|
||||
function pickSeason(start: number, end: number) {
|
||||
applyJalaliSpan(pickerYear.value, start, end, 'season');
|
||||
}
|
||||
|
||||
function groupOptionLabel(g: CustomerGroupDto) {
|
||||
return g.parentName ? `${g.parentName} / ${g.name}` : g.name;
|
||||
}
|
||||
@@ -375,25 +451,51 @@ async function loadConnections() {
|
||||
if (!connectionId.value && def) connectionId.value = def.id;
|
||||
}
|
||||
|
||||
async function searchPeople(q: string) {
|
||||
if (!q || q.trim().length < 2) {
|
||||
let personSearchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let customersEndpointMissing = false;
|
||||
|
||||
async function loadTahesabCustomers(q: string) {
|
||||
if (!connectionId.value) {
|
||||
personOptions.value = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { data } = await api.get<{ items: PersonDto[] }>('/people', {
|
||||
params: { q: q.trim(), limit: 20 },
|
||||
const { data } = await api.get<{
|
||||
items: Array<{ personId: string; name: string; moshtariCode: number }>;
|
||||
}>('/integrations/tahesab/reports/asnad/customers', {
|
||||
params: { connectionId: connectionId.value, q: q.trim() },
|
||||
});
|
||||
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,
|
||||
const next = items.map((item) => ({
|
||||
value: item.personId,
|
||||
label: `${item.name || 'بدون نام'} (${toFaDigits(String(item.moshtariCode))})`,
|
||||
}));
|
||||
} catch {
|
||||
personOptions.value = [];
|
||||
const selected = personOptions.value.find((item) => item.value === personId.value);
|
||||
if (selected && !next.some((item) => item.value === selected.value)) next.unshift(selected);
|
||||
personOptions.value = next;
|
||||
} catch (e: unknown) {
|
||||
const status = axios.isAxiosError(e) ? e.response?.status : undefined;
|
||||
if (status === 404 && !customersEndpointMissing) {
|
||||
customersEndpointMissing = true;
|
||||
message.warning('جستجوی مشتری تهحساب بعد از بهروزرسانی سرور فعال میشود.');
|
||||
}
|
||||
personOptions.value = personId.value
|
||||
? personOptions.value.filter((item) => item.value === personId.value)
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
function searchTahesabCustomers(q: string) {
|
||||
if (personSearchTimer) clearTimeout(personSearchTimer);
|
||||
personSearchTimer = setTimeout(() => {
|
||||
void loadTahesabCustomers(q);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function onPersonDropdown(open: boolean) {
|
||||
if (open) void loadTahesabCustomers('');
|
||||
}
|
||||
|
||||
function apiErrMessage(e: unknown, fallback: string): string {
|
||||
if (!axios.isAxiosError(e)) return fallback;
|
||||
const raw = e.response?.data as { message?: string | string[] } | undefined;
|
||||
@@ -493,7 +595,20 @@ async function pollJob(jobId: string) {
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
async function startSync() {
|
||||
const hasSyncScope = computed(
|
||||
() => Boolean(personId.value) || groupIds.value.length > 0,
|
||||
);
|
||||
|
||||
function openSyncChoice() {
|
||||
if (!filterParams.value) {
|
||||
error.value = 'ابتدا بازه تاریخ را مشخص کنید، بعد بهروزرسانی را بزنید.';
|
||||
message.warning(error.value);
|
||||
return;
|
||||
}
|
||||
syncModalOpen.value = true;
|
||||
}
|
||||
|
||||
async function startSync(scoped: boolean) {
|
||||
const params = filterParams.value;
|
||||
if (!params) {
|
||||
error.value = 'ابتدا بازه تاریخ را مشخص کنید، بعد بهروزرسانی را بزنید.';
|
||||
@@ -502,7 +617,10 @@ async function startSync() {
|
||||
}
|
||||
syncing.value = true;
|
||||
error.value = '';
|
||||
syncHint.value = 'شروع همگامسازی…';
|
||||
syncHint.value = scoped
|
||||
? 'شروع بهروزرسانی مشتریان همین فیلتر…'
|
||||
: 'شروع بهروزرسانی همه مشتریان متصل…';
|
||||
syncModalOpen.value = false;
|
||||
try {
|
||||
const { data } = await api.post<TahesabAsnadSyncJobDto>(
|
||||
'/integrations/tahesab/reports/asnad/sync',
|
||||
@@ -510,6 +628,8 @@ async function startSync() {
|
||||
connectionId: params.connectionId,
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
...(scoped && personId.value ? { personId: personId.value } : {}),
|
||||
...(scoped && groupIds.value.length ? { groupIds: [...groupIds.value] } : {}),
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
@@ -549,7 +669,10 @@ watch([connectionId, groupIds, kinds, jensFelez, personId], () => {
|
||||
page.value = 1;
|
||||
});
|
||||
|
||||
onUnmounted(() => stopPoll());
|
||||
onUnmounted(() => {
|
||||
stopPoll();
|
||||
if (personSearchTimer) clearTimeout(personSearchTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -574,7 +697,7 @@ onUnmounted(() => stopPoll());
|
||||
<template #icon><ArrowRightOutlined /></template>
|
||||
بازگشت
|
||||
</a-button>
|
||||
<a-button type="primary" :loading="syncing" @click="startSync">
|
||||
<a-button type="primary" :loading="syncing" @click="openSyncChoice">
|
||||
<template #icon><SyncOutlined /></template>
|
||||
بهروزرسانی از تهحساب
|
||||
</a-button>
|
||||
@@ -613,17 +736,19 @@ onUnmounted(() => stopPoll());
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-form-item label="بازه">
|
||||
<a-segmented
|
||||
v-model:value="period"
|
||||
block
|
||||
:options="[
|
||||
{ label: 'هفته', value: 'week' },
|
||||
{ label: 'ماه', value: 'month' },
|
||||
{ label: 'فصل', value: 'season' },
|
||||
{ label: 'تاریخ دلخواه', value: 'custom' },
|
||||
]"
|
||||
@change="(value: string | number) => applyNamedPeriod(value as 'week' | 'month' | 'season' | 'custom', true)"
|
||||
/>
|
||||
<div @click="onPeriodReclick">
|
||||
<a-segmented
|
||||
:value="period"
|
||||
block
|
||||
:options="[
|
||||
{ label: 'هفته', value: 'week' },
|
||||
{ label: 'ماه', value: 'month' },
|
||||
{ label: 'فصل', value: 'season' },
|
||||
{ label: 'تاریخ دلخواه', value: 'custom' },
|
||||
]"
|
||||
@change="onPeriodPick"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="8" :lg="6">
|
||||
@@ -642,6 +767,8 @@ onUnmounted(() => stopPoll());
|
||||
v-model:value="groupIds"
|
||||
mode="multiple"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="همه گروهها"
|
||||
:options="
|
||||
(customerGroups as CustomerGroupDto[]).map((g) => ({
|
||||
@@ -658,6 +785,8 @@ onUnmounted(() => stopPoll());
|
||||
<a-select
|
||||
v-model:value="kinds"
|
||||
mode="multiple"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:options="kindOptions"
|
||||
placeholder="همه انواع"
|
||||
:max-tag-count="2"
|
||||
@@ -669,6 +798,8 @@ onUnmounted(() => stopPoll());
|
||||
<a-select
|
||||
v-model:value="jensFelez"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="همه"
|
||||
:options="metalOptions"
|
||||
/>
|
||||
@@ -680,10 +811,11 @@ onUnmounted(() => stopPoll());
|
||||
v-model:value="personId"
|
||||
show-search
|
||||
allow-clear
|
||||
placeholder="جستجوی نام…"
|
||||
placeholder="جستجوی مشتری تهحساب"
|
||||
:filter-option="false"
|
||||
:options="personOptions"
|
||||
@search="searchPeople"
|
||||
@search="searchTahesabCustomers"
|
||||
@dropdown-visible-change="onPersonDropdown"
|
||||
>
|
||||
<template #suffixIcon><SearchOutlined /></template>
|
||||
</a-select>
|
||||
@@ -883,4 +1015,61 @@ onUnmounted(() => stopPoll());
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-modal
|
||||
v-model:open="periodModalOpen"
|
||||
:title="periodModalKind === 'month' ? 'انتخاب ماه' : 'انتخاب فصل'"
|
||||
:footer="null"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="سال">
|
||||
<a-select
|
||||
v-model:value="pickerYear"
|
||||
:options="pickerYears.map((year) => ({ value: year, label: toFaDigits(year) }))"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-row v-if="periodModalKind === 'month'" :gutter="[8, 8]">
|
||||
<a-col v-for="month in 12" :key="month" :span="8">
|
||||
<a-button block @click="pickMonth(month)">{{ jalaliMonthName(month) }}</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-else :gutter="[8, 8]">
|
||||
<a-col v-for="season in SEASONS" :key="season.key" :span="12">
|
||||
<a-button block @click="pickSeason(season.start, season.end)">{{ season.label }}</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
v-model:open="syncModalOpen"
|
||||
title="بهروزرسانی از تهحساب"
|
||||
:footer="null"
|
||||
>
|
||||
<a-typography-paragraph>
|
||||
تهحساب سند را مشتریبهمشتری میدهد. اگر گروه یا شخص را انتخاب کنید، فقط همانها گرفته میشود و سریعتر تمام میشود. نوع سند و فلز بعد از دریافت، روی همین دادهها فیلتر میشوند.
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph v-if="!hasSyncScope" type="secondary">
|
||||
الان گروه یا شخصی انتخاب نشده، پس بهروزرسانی همهٔ مشتریان متصل این بازه را از تهحساب میگیرد.
|
||||
</a-typography-paragraph>
|
||||
<a-flex vertical :gap="8">
|
||||
<a-button
|
||||
v-if="hasSyncScope"
|
||||
type="primary"
|
||||
block
|
||||
:loading="syncing"
|
||||
@click="startSync(true)"
|
||||
>
|
||||
فقط مشتریان همین فیلتر
|
||||
</a-button>
|
||||
<a-button
|
||||
block
|
||||
:type="hasSyncScope ? 'default' : 'primary'"
|
||||
:loading="syncing"
|
||||
@click="startSync(false)"
|
||||
>
|
||||
همه مشتریان این اتصال
|
||||
</a-button>
|
||||
</a-flex>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
@@ -4199,6 +4199,10 @@ export interface TahesabAsnadSyncRequest {
|
||||
from: string;
|
||||
/** شمسی YYYY-MM-DD */
|
||||
to: string;
|
||||
/** محدود کردن همگامسازی به یک شخص */
|
||||
personId?: string;
|
||||
/** محدود کردن همگامسازی به گروهها و زیرگروهها */
|
||||
groupIds?: string[];
|
||||
}
|
||||
|
||||
export interface TahesabAsnadSyncJobDto {
|
||||
|
||||
Reference in New Issue
Block a user