feat(leads): keep gender on leads and form submissions

A lead can carry the same honorific as a person, and a form that asks gender writes it onto the lead.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-31 12:49:42 +03:30
co-authored by Cursor
parent cbc2e4ee8a
commit d7b5085096
12 changed files with 334 additions and 19 deletions
+5
View File
@@ -8,6 +8,7 @@ import {
MaxLength,
MinLength,
} from 'class-validator';
import { PERSON_GENDERS } from '@mento/shared';
export class LeadSmsDto {
@IsString()
@@ -26,6 +27,8 @@ export class CreateLeadDto {
mobile!: string;
@IsOptional() @IsString() contactName?: string;
@IsOptional() @IsIn(PERSON_GENDERS) gender?: (typeof PERSON_GENDERS)[number];
@IsOptional() @IsBoolean() showGenderInName?: boolean;
@IsOptional() @IsString() phone?: string;
@IsOptional() @IsString() city?: string;
@IsOptional() @IsString() region?: string;
@@ -47,6 +50,8 @@ export class UpdateLeadDto {
@IsOptional() @IsString() @MinLength(1) title?: string;
@IsOptional() @IsString() @MinLength(5) mobile?: string;
@IsOptional() @IsString() contactName?: string | null;
@IsOptional() @IsIn(PERSON_GENDERS) gender?: (typeof PERSON_GENDERS)[number];
@IsOptional() @IsBoolean() showGenderInName?: boolean;
@IsOptional() @IsString() phone?: string | null;
@IsOptional() @IsString() city?: string | null;
@IsOptional() @IsString() region?: string | null;
+66 -10
View File
@@ -20,6 +20,7 @@ import type {
PaginatedResponse,
} from '@mento/shared';
import { FILTERED_IDS_MAX } from '@mento/shared';
import { applyLeadGenderName, applyPersonGenderTitle } from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import { SmsGatewayService } from '../sms-billing/sms-gateway.service';
import { renderSmsText } from '../common/sms-text';
@@ -341,6 +342,8 @@ export class LeadsService {
title: string;
mobile: string;
contactName?: string | null;
gender?: string | null;
showGenderInName?: boolean | null;
phone?: string | null;
city?: string | null;
region?: string | null;
@@ -382,11 +385,22 @@ export class LeadsService {
}
const registeredAt = dto.registeredAt ? new Date(dto.registeredAt) : null;
const named = applyLeadGenderName({
title: dto.title,
contactName: dto.contactName,
gender:
dto.gender === 'male' || dto.gender === 'female' || dto.gender === 'none'
? dto.gender
: undefined,
showGenderInName: dto.showGenderInName,
});
const row = await this.prisma.lead.create({
data: {
organizationId: actor.organizationId,
title: dto.title.trim(),
contactName: emptyToNull(dto.contactName),
title: named.title,
contactName: named.contactName,
gender: named.gender,
showGenderInName: named.showGenderInName,
mobile,
phone: emptyToNull(dto.phone),
city: emptyToNull(dto.city),
@@ -461,12 +475,41 @@ export class LeadsService {
}
}
const touchName =
typeof dto.title === 'string' ||
dto.contactName !== undefined ||
dto.gender !== undefined ||
dto.showGenderInName !== undefined;
const named = touchName
? applyLeadGenderName({
title: typeof dto.title === 'string' ? dto.title : existing.title,
contactName:
dto.contactName !== undefined
? (dto.contactName as string | null)
: existing.contactName,
gender:
dto.gender === 'male' || dto.gender === 'female' || dto.gender === 'none'
? dto.gender
: existing.gender === 'male' || existing.gender === 'female'
? existing.gender
: 'none',
showGenderInName:
typeof dto.showGenderInName === 'boolean'
? dto.showGenderInName
: existing.showGenderInName,
})
: null;
const data: Prisma.LeadUpdateInput = {
...(typeof dto.title === 'string' ? { title: dto.title.trim() } : {}),
mobile,
...(dto.contactName !== undefined
? { contactName: emptyToNull(dto.contactName as string) }
...(named
? {
title: named.title,
contactName: named.contactName,
gender: named.gender,
showGenderInName: named.showGenderInName,
}
: {}),
mobile,
...(dto.phone !== undefined
? { phone: emptyToNull(dto.phone as string) }
: {}),
@@ -877,13 +920,24 @@ export class LeadsService {
? `${dto.firstName ?? ''} ${dto.lastName ?? ''}`.trim()
: (lead.contactName || lead.title).trim();
const nameParts = rawName.split(/\s+/).filter(Boolean);
const firstName = nameParts[0] || lead.title;
const lastName = nameParts.slice(1).join(' ') || '—';
const bareFirst = nameParts[0] || lead.title;
const bareLast = nameParts.slice(1).join(' ');
const named = applyPersonGenderTitle({
firstName: bareFirst,
lastName: bareLast,
gender:
lead.gender === 'male' || lead.gender === 'female' || lead.gender === 'none'
? lead.gender
: 'none',
showGenderInName: lead.showGenderInName,
});
person = await this.prisma.person.create({
data: {
organizationId: actor.organizationId,
firstName,
lastName,
firstName: named.firstName,
lastName: named.lastName,
gender: named.gender,
showGenderInName: named.showGenderInName,
mobile: lead.mobile,
phone: lead.phone,
instagram: lead.instagram,
@@ -1202,6 +1256,8 @@ export class LeadsService {
id: row.id,
title: row.title,
contactName: row.contactName,
gender: row.gender === 'male' || row.gender === 'female' ? row.gender : 'none',
showGenderInName: row.showGenderInName,
mobile: row.mobile,
phone: row.phone,
city: row.city,