Unified search algorithm across all lists; per-section record visibility (contacts, deals, activities, leads, finance, tickets, notes, projects)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { matchesSearch } from '@/lib/search';
|
||||
|
||||
export type PipelineAccent = 'green' | 'orange' | 'purple';
|
||||
|
||||
export type PipelineDeal = {
|
||||
@@ -14,6 +16,8 @@ export type PipelineDeal = {
|
||||
assigneeNames?: string[];
|
||||
ownerId?: string | null;
|
||||
assigneeIds?: string[];
|
||||
/** متنهای قابل جستجو (مشتری، شرکت، یادداشت، مسئولان …) */
|
||||
searchFields?: Array<string | null | undefined>;
|
||||
};
|
||||
|
||||
export type PipelineUiCopy = {
|
||||
@@ -190,6 +194,12 @@ export function mapBoardToStages(
|
||||
assigneeNames,
|
||||
ownerId: deal.owner?.id ?? null,
|
||||
assigneeIds: (deal.assignees ?? []).map((a) => a.id),
|
||||
searchFields: [
|
||||
deal.notes,
|
||||
deal.company?.name,
|
||||
deal.person?.fullName,
|
||||
...assigneeNames,
|
||||
],
|
||||
};
|
||||
}),
|
||||
};
|
||||
@@ -246,6 +256,13 @@ export function mapProjectBoardToStages(
|
||||
assigneeNames,
|
||||
ownerId: card.owner?.id ?? null,
|
||||
assigneeIds: (card.assignees ?? []).map((a) => a.id),
|
||||
searchFields: [
|
||||
card.notes,
|
||||
card.company?.name,
|
||||
card.person?.fullName,
|
||||
card.deal?.title,
|
||||
...assigneeNames,
|
||||
],
|
||||
};
|
||||
}),
|
||||
}));
|
||||
@@ -343,11 +360,7 @@ export function filterStages(
|
||||
(d.assigneeIds ?? []).includes(mineUserId);
|
||||
if (!isMine) return false;
|
||||
}
|
||||
if (
|
||||
q &&
|
||||
!d.title.toLowerCase().includes(q) &&
|
||||
!d.description.toLowerCase().includes(q)
|
||||
) {
|
||||
if (q && !matchesSearch(q, [d.title, d.description, ...(d.searchFields ?? [])])) {
|
||||
return false;
|
||||
}
|
||||
if (min != null && !Number.isNaN(min) && (d.amount == null || d.amount < min)) {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* همان الگوریتم جستجوی سرور (apps/api/src/common/search.ts) برای فیلترهای سمت کاربر:
|
||||
* کلمهبهکلمه، بخشی از کلمه کافی است، حروف عربی/فارسی و ارقام فارسی/لاتین یکساناند.
|
||||
*/
|
||||
|
||||
function toAsciiDigits(s: string): string {
|
||||
return s.replace(/[۰-۹]/g, (d) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d))).replace(
|
||||
/[٠-٩]/g,
|
||||
(d) => String('٠١٢٣٤٥٦٧٨٩'.indexOf(d)),
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeSearchText(raw: unknown): string {
|
||||
return toAsciiDigits(String(raw ?? ''))
|
||||
.toLowerCase()
|
||||
.replace(/[يىئ]/g, 'ی')
|
||||
.replace(/ك/g, 'ک')
|
||||
.replace(/ة/g, 'ه')
|
||||
.replace(/[أإٱ]/g, 'ا')
|
||||
.replace(/ؤ/g, 'و')
|
||||
.replace(/[\u064B-\u065F\u0670]/g, '')
|
||||
.replace(/[\u200c\u200f\u200e]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function phoneCore(token: string): string | null {
|
||||
if (!/^\+?[0-9]+$/.test(token)) return null;
|
||||
const core = token.replace(/^\+/, '').replace(/^(0098|98|0)/, '');
|
||||
return core.length >= 3 ? core : null;
|
||||
}
|
||||
|
||||
/** آیا همه کلمات عبارت جستجو در یکی از فیلدها پیدا میشوند؟ عبارت خالی = true */
|
||||
export function matchesSearch(query: string, fields: unknown[]): boolean {
|
||||
const q = normalizeSearchText(query);
|
||||
if (!q) return true;
|
||||
const hay = normalizeSearchText(fields.filter((f) => f != null && f !== '').join(' \u0001 '));
|
||||
return q.split(' ').every((token) => {
|
||||
if (hay.includes(token)) return true;
|
||||
const core = phoneCore(token);
|
||||
return core ? hay.includes(core) : false;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user