feat(tahesab): import and refresh customers at chosen hours

Each connection can set how many times a day and at which Tehran hours the server pulls Tahesab customers, including name updates for people already linked.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-05 12:31:27 +03:30
co-authored by Cursor
parent c976c0bcd6
commit 94f0383c79
9 changed files with 443 additions and 10 deletions
+131 -1
View File
@@ -10,7 +10,7 @@ import type {
UpdateTahesabConnectionRequest,
} from '@mento/shared';
import { brand } from '@mento/shared';
import { toFaDigits } from '@/lib/jalali';
import { formatJalaliDateTime, toFaDigits } from '@/lib/jalali';
import {
UiBadge,
UiBusyOverlay,
@@ -74,6 +74,77 @@ const syncTablePageSize = ref(50);
const syncTablePage = ref(1);
const syncOnlyNew = ref(false);
const autoCount = ref('0');
const autoHours = ref<string[]>([]);
const autoSyncSaving = ref(false);
const AUTO_HOUR_PRESETS: Record<number, number[]> = {
1: [8],
2: [8, 14],
3: [8, 12, 16],
4: [8, 12, 16, 20],
5: [8, 11, 14, 17, 20],
6: [8, 10, 12, 14, 16, 20],
};
const selectedConnection = computed(() =>
items.value.find((row) => row.id === sync.connectionId),
);
function hourLabel(hour: number) {
return `${toFaDigits(String(hour).padStart(2, '0'))}:۰۰`;
}
function fillAutoSync() {
const hours = [...(selectedConnection.value?.customerSyncHours ?? [])]
.sort((a, b) => a - b)
.slice(0, 6);
autoCount.value = String(hours.length);
autoHours.value = hours.map(String);
}
function onAutoCount(value: string) {
const count = Math.min(6, Math.max(0, Number(value) || 0));
const preset = AUTO_HOUR_PRESETS[count] ?? [];
const next = autoHours.value.slice(0, count);
while (next.length < count) {
next.push(String(preset[next.length] ?? 8));
}
autoCount.value = String(count);
autoHours.value = next;
}
function setAutoHour(index: number, value: string) {
autoHours.value[index] = value;
}
async function saveAutoSync() {
if (!sync.connectionId) return;
const hours = autoHours.value.map((hour) => Number(hour));
if (hours.some((hour) => !Number.isInteger(hour) || hour < 0 || hour > 23)) {
error.value = 'ساعت باید بین ۰ و ۲۳ باشد.';
return;
}
if (new Set(hours).size !== hours.length) {
error.value = 'ساعت‌ها نباید تکراری باشند.';
return;
}
autoSyncSaving.value = true;
error.value = '';
ok.value = '';
try {
await api.patch(`/integrations/tahesab/connections/${sync.connectionId}`, {
customerSyncHours: hours,
});
ok.value = hours.length
? 'زمان‌بندی بروزرسانی ذخیره شد.'
: 'بروزرسانی خودکار خاموش شد.';
await load();
} catch (e) {
error.value = apiErr(e, 'ذخیره زمان‌بندی ناموفق بود');
} finally {
autoSyncSaving.value = false;
}
}
const syncVisibleResults = computed(() =>
syncOnlyNew.value
? syncResults.value.filter((r) => !r.alreadyLinked)
@@ -86,6 +157,7 @@ const syncPagedResults = computed(() => {
const start = (syncTablePage.value - 1) * syncTablePageSize.value;
return syncVisibleResults.value.slice(start, start + syncTablePageSize.value);
});
watch(() => sync.connectionId, () => fillAutoSync());
watch([syncOnlyNew, syncTablePageSize], () => {
syncTablePage.value = 1;
});
@@ -178,6 +250,7 @@ async function load() {
if (!editingId.value) form.isDefault = data.length === 0;
if (!sync.connectionId && data[0]) sync.connectionId = data[0].id;
if (!productSync.connectionId && data[0]) productSync.connectionId = data[0].id;
fillAutoSync();
} catch (e) {
error.value = axios.isAxiosError(e)
? String(
@@ -875,6 +948,63 @@ onMounted(load);
</UiSelect>
</UiField>
<div class="sync-sub-section">
<h4 class="sync-subhead">بروزرسانی خودکار</h4>
<p class="sync-sub-desc">
تعداد دفعات در روز و ساعت هر بار را برای همین اتصال مشخص کنید. سرور در آن ساعت‌ها، به وقت تهران،
مشتریان تازه را وارد می‌کند و نام مشتری‌های قبلی را هم با ته‌حساب تازه می‌کند. اگر شماره یکی باشد
همان مشتری می‌ماند، نام قبلی حفظ می‌شود و شماره به شکل ۰۹۱۲ ذخیره می‌شود. خاموش یعنی فقط ورود دستی.
</p>
<UiField label="تعداد در روز">
<UiSelect
:model-value="autoCount"
:disabled="!sync.connectionId || autoSyncSaving"
@update:model-value="onAutoCount"
>
<option value="0">خاموش</option>
<option v-for="n in 6" :key="n" :value="String(n)">
{{ toFaDigits(n) }} بار
</option>
</UiSelect>
</UiField>
<div v-if="Number(autoCount) > 0" class="crm-form-grid-3">
<UiField
v-for="(_, index) in autoHours"
:key="index"
:label="`ساعت ${toFaDigits(index + 1)}`"
>
<UiSelect
:model-value="autoHours[index]"
:disabled="autoSyncSaving"
@update:model-value="setAutoHour(index, $event)"
>
<option v-for="hour in 24" :key="hour - 1" :value="String(hour - 1)">
{{ hourLabel(hour - 1) }}
</option>
</UiSelect>
</UiField>
</div>
<div class="sync-buttons">
<UiButton
:loading="autoSyncSaving"
:disabled="!sync.connectionId"
@click="saveAutoSync"
>
ذخیره زمان‌بندی
</UiButton>
</div>
<p v-if="selectedConnection?.customerSyncLastRunAt" class="sync-sub-desc">
آخرین اجرا:
{{ formatJalaliDateTime(new Date(selectedConnection.customerSyncLastRunAt)) }}
<template v-if="selectedConnection.customerSyncLastSummary">
— {{ toFaDigits(selectedConnection.customerSyncLastSummary) }}
</template>
</p>
<p v-if="selectedConnection?.customerSyncLastError" class="sync-sub-desc">
آخرین خطا: {{ selectedConnection.customerSyncLastError }}
</p>
</div>
<div class="sync-sub-section">
<h4 class="sync-subhead">۱) وارد کردن دسته‌ای با بازه کد مشتری</h4>
<p class="sync-sub-desc">