Unified search algorithm across all lists; per-section record visibility (contacts, deals, activities, leads, finance, tickets, notes, projects)

This commit is contained in:
2026-10-06 15:04:46 +03:30
parent 43717392b2
commit e08f1452ef
38 changed files with 699 additions and 420 deletions
+16 -13
View File
@@ -19,6 +19,7 @@ import { PrismaService } from '../prisma/prisma.service';
import {
type Actor,
elevated,
forScope,
} from '../common/record-scope';
import {
asAddresses,
@@ -28,6 +29,7 @@ import {
import { emptyToNull, normalizePhone } from '../contacts/normalize';
import { LocalFileStorage } from '../storage/local-file.storage';
import { LeadStagesService } from './lead-stages.service';
import { buildSearchWhere, ci } from '../common/search';
const LOGO_MAX_BYTES = 2 * 1024 * 1024;
const LOGO_EXT: Record<string, string> = {
@@ -48,7 +50,8 @@ function mobileKey(raw: string | null | undefined): string | null {
return d.startsWith('0') ? d : d;
}
function leadAccessWhere(actor: Actor): Prisma.LeadWhereInput {
function leadAccessWhere(baseActor: Actor): Prisma.LeadWhereInput {
const actor = forScope(baseActor, 'leads');
if (elevated(actor.role)) return {};
if (actor.dataVisibility === 'org') return {};
if (
@@ -95,7 +98,17 @@ export class LeadsService {
await this.stages.ensureDefaults(actor.organizationId);
const page = Math.max(1, opts.page ?? 1);
const limit = Math.min(100, Math.max(1, opts.limit ?? 20));
const q = opts.q?.trim();
const search = buildSearchWhere<Prisma.LeadWhereInput>(opts.q, (t) => [
{ title: ci(t) },
{ contactName: ci(t) },
{ mobile: { contains: t } },
{ phone: { contains: t } },
{ city: ci(t) },
{ region: ci(t) },
{ trade: ci(t) },
{ instagram: ci(t) },
{ source: ci(t) },
]);
const where: Prisma.LeadWhereInput = {
organizationId: actor.organizationId,
@@ -109,17 +122,7 @@ export class LeadsService {
...(opts.source
? { source: { contains: opts.source, mode: 'insensitive' } }
: {}),
...(q
? {
OR: [
{ title: { contains: q, mode: 'insensitive' } },
{ contactName: { contains: q, mode: 'insensitive' } },
{ mobile: { contains: q } },
{ city: { contains: q, mode: 'insensitive' } },
{ trade: { contains: q, mode: 'insensitive' } },
],
}
: {}),
...(search ? { AND: [search] } : {}),
};
const orderBy: Prisma.LeadOrderByWithRelationInput =