fix(tahesab): stop treating shared phone numbers as new customers
A second Tahesab code with the same phone stays linked to the existing person, so the import list no longer counts it as unimported and sync no longer overwrites that person's name. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -64,6 +64,9 @@ type SyncResult = {
|
||||
alreadyLinked?: boolean;
|
||||
personId?: string | null;
|
||||
personName?: string | null;
|
||||
duplicateOfCode?: number | null;
|
||||
duplicatePersonId?: string | null;
|
||||
duplicatePersonName?: string | null;
|
||||
};
|
||||
const syncResults = ref<SyncResult[]>([]);
|
||||
/** هر درخواست listAll یک بازهٔ ۲۰۰۰ کدی است؛ بعد از دو بازهٔ خالی پشتسرهم، خواندن تمام میشود */
|
||||
@@ -145,9 +148,61 @@ async function saveAutoSync() {
|
||||
autoSyncSaving.value = false;
|
||||
}
|
||||
}
|
||||
/** همان قاعدهٔ سرور: موبایل ایران 09… است، بقیهٔ شمارهها فقط رقماند. */
|
||||
function customerPhoneKey(value?: string | null): string {
|
||||
const fa = '۰۱۲۳۴۵۶۷۸۹';
|
||||
const ar = '٠١٢٣٤٥٦٧٨٩';
|
||||
let digits = String(value ?? '')
|
||||
.replace(/[۰-۹٠-٩]/g, (ch) => {
|
||||
const faIndex = fa.indexOf(ch);
|
||||
return String(faIndex >= 0 ? faIndex : ar.indexOf(ch));
|
||||
})
|
||||
.replace(/\D/g, '');
|
||||
if (!digits) return '';
|
||||
if (digits.startsWith('0098') && digits.length >= 14) digits = digits.slice(4);
|
||||
else if (digits.startsWith('98') && digits.length >= 12) digits = digits.slice(2);
|
||||
if (digits.length === 10 && digits.startsWith('9')) digits = `0${digits}`;
|
||||
return digits;
|
||||
}
|
||||
|
||||
const linkedPhoneOwners = computed(() => {
|
||||
const owners = new Map<string, { code: number; personId: string | null; personName: string | null }>();
|
||||
for (const row of syncResults.value) {
|
||||
if (!row.alreadyLinked || row.moshtariCode == null) continue;
|
||||
const key = customerPhoneKey(row.tel);
|
||||
if (key && !owners.has(key)) {
|
||||
owners.set(key, {
|
||||
code: row.moshtariCode,
|
||||
personId: row.personId ?? null,
|
||||
personName: row.personName ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
return owners;
|
||||
});
|
||||
|
||||
function duplicateOwner(row: SyncResult) {
|
||||
if (row.alreadyLinked || row.moshtariCode == null) return null;
|
||||
if (row.duplicateOfCode != null) {
|
||||
return {
|
||||
code: row.duplicateOfCode,
|
||||
personId: row.duplicatePersonId ?? null,
|
||||
personName: row.duplicatePersonName ?? null,
|
||||
};
|
||||
}
|
||||
const key = customerPhoneKey(row.tel);
|
||||
if (!key) return null;
|
||||
const owner = linkedPhoneOwners.value.get(key);
|
||||
if (!owner || owner.code === row.moshtariCode) return null;
|
||||
return owner;
|
||||
}
|
||||
|
||||
const duplicateCustomerCount = computed(
|
||||
() => syncResults.value.filter((row) => duplicateOwner(row)).length,
|
||||
);
|
||||
const syncVisibleResults = computed(() =>
|
||||
syncOnlyNew.value
|
||||
? syncResults.value.filter((r) => !r.alreadyLinked)
|
||||
? syncResults.value.filter((row) => !row.alreadyLinked && !duplicateOwner(row))
|
||||
: syncResults.value,
|
||||
);
|
||||
const syncTableTotalPages = computed(() =>
|
||||
@@ -184,7 +239,7 @@ const productCategoryLabel = ref('کارساخته');
|
||||
|
||||
function pendingImportCodes() {
|
||||
return syncResults.value
|
||||
.filter((r) => r.moshtariCode != null && !r.alreadyLinked)
|
||||
.filter((r) => r.moshtariCode != null && !r.alreadyLinked && !duplicateOwner(r))
|
||||
.map((r) => r.moshtariCode as number);
|
||||
}
|
||||
|
||||
@@ -458,10 +513,12 @@ async function searchTahesab(mode: 'range' | 'filter') {
|
||||
ok.value = 'نتیجهای یافت نشد.';
|
||||
} else {
|
||||
const linked = syncResults.value.filter((r) => r.alreadyLinked).length;
|
||||
const pending = syncResults.value.length - linked;
|
||||
const duplicate = syncResults.value.filter((r) => duplicateOwner(r)).length;
|
||||
const pending = syncResults.value.length - linked - duplicate;
|
||||
ok.value =
|
||||
`${toFaDigits(syncResults.value.length)} مشتری پیدا شد` +
|
||||
` · ${toFaDigits(linked)} قبلاً وارد شده · ${toFaDigits(pending)} واردنشده`;
|
||||
` · ${toFaDigits(linked)} قبلاً وارد شده · ${toFaDigits(pending)} واردنشده` +
|
||||
(duplicate ? ` · ${toFaDigits(duplicate)} شماره تکراری` : '');
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = apiErr(e, 'جستجو ناموفق');
|
||||
@@ -953,7 +1010,8 @@ onMounted(load);
|
||||
<p class="sync-sub-desc">
|
||||
تعداد دفعات در روز و ساعت هر بار را برای همین اتصال مشخص کنید. سرور در آن ساعتها، به وقت تهران،
|
||||
مشتریان تازه را وارد میکند و نام مشتریهای قبلی را هم با تهحساب تازه میکند. اگر شماره یکی باشد
|
||||
همان مشتری میماند، نام قبلی حفظ میشود و شماره به شکل ۰۹۱۲ ذخیره میشود. خاموش یعنی فقط ورود دستی.
|
||||
همان مشتری میماند و کد دیگر بهعنوان مشتری تازه وارد نمیشود. شماره به شکل ۰۹۱۲ ذخیره میشود.
|
||||
خاموش یعنی فقط ورود دستی.
|
||||
</p>
|
||||
<UiField label="تعداد در روز">
|
||||
<UiSelect
|
||||
@@ -1100,6 +1158,9 @@ onMounted(load);
|
||||
<h4 class="sync-results-title">
|
||||
نتایج جستجو ({{ syncVisibleResults.length }}<template v-if="syncOnlyNew"> از {{ syncResults.length }}</template>)
|
||||
</h4>
|
||||
<p v-if="duplicateCustomerCount" class="sync-sub-desc">
|
||||
{{ toFaDigits(duplicateCustomerCount) }} مشتری شمارهٔ تکراری دارند و جداگانه وارد نمیشوند؛ همان شخص با کد دیگری از قبل لینک است.
|
||||
</p>
|
||||
<div class="tc-row">
|
||||
<TableColumnsMenu table-key="tahesab.customers" align="end" />
|
||||
</div>
|
||||
@@ -1120,7 +1181,9 @@ onMounted(load);
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="!syncPagedResults.length">
|
||||
<td colspan="5" class="sync-empty-row">همهٔ مشتریان این نتایج قبلاً وارد شدهاند.</td>
|
||||
<td colspan="5" class="sync-empty-row">
|
||||
مشتری تازهای برای ورود نیست. شمارههای تکراری از قبل با کد دیگری لینک شدهاند.
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="r in syncPagedResults" :key="String(r.moshtariCode)">
|
||||
<td class="font-mono">{{ r.moshtariCode }}</td>
|
||||
@@ -1130,11 +1193,14 @@ onMounted(load);
|
||||
<UiBadge v-if="r.alreadyLinked" variant="success">
|
||||
لینک شده{{ r.personName ? ` (${r.personName})` : '' }}
|
||||
</UiBadge>
|
||||
<UiBadge v-else-if="duplicateOwner(r)" variant="warning">
|
||||
شماره تکراری، کد {{ toFaDigits(duplicateOwner(r)!.code) }}
|
||||
</UiBadge>
|
||||
<UiBadge v-else variant="neutral">آماده ورود</UiBadge>
|
||||
</td>
|
||||
<td>
|
||||
<UiButton
|
||||
v-if="!r.alreadyLinked"
|
||||
v-if="!r.alreadyLinked && !duplicateOwner(r)"
|
||||
size="sm"
|
||||
:disabled="!r.moshtariCode || syncBusy"
|
||||
@click="importOne(r.moshtariCode!)"
|
||||
@@ -1142,8 +1208,8 @@ onMounted(load);
|
||||
وارد کردن
|
||||
</UiButton>
|
||||
<RouterLink
|
||||
v-else-if="r.personId"
|
||||
:to="{ name: 'person-detail', params: { id: r.personId } }"
|
||||
v-else-if="r.personId || duplicateOwner(r)?.personId"
|
||||
:to="{ name: 'person-detail', params: { id: (r.personId || duplicateOwner(r)?.personId)! } }"
|
||||
>
|
||||
<UiButton variant="ghost" size="sm">
|
||||
مشاهده شخص
|
||||
|
||||
Reference in New Issue
Block a user