diff --git a/apps/api/prisma/migrations/20261112120000_customer_group_assignees/migration.sql b/apps/api/prisma/migrations/20261112120000_customer_group_assignees/migration.sql new file mode 100644 index 0000000..e6d04a0 --- /dev/null +++ b/apps/api/prisma/migrations/20261112120000_customer_group_assignees/migration.sql @@ -0,0 +1,28 @@ +-- مسئولان اختیاری روی گروه‌های مشتری +CREATE TABLE IF NOT EXISTS "_CustomerGroupAssignees" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS "_CustomerGroupAssignees_AB_unique" ON "_CustomerGroupAssignees"("A", "B"); +CREATE INDEX IF NOT EXISTS "_CustomerGroupAssignees_B_index" ON "_CustomerGroupAssignees"("B"); + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = '_CustomerGroupAssignees_A_fkey' + ) THEN + ALTER TABLE "_CustomerGroupAssignees" + ADD CONSTRAINT "_CustomerGroupAssignees_A_fkey" + FOREIGN KEY ("A") REFERENCES "CustomerGroup"("id") + ON DELETE CASCADE ON UPDATE CASCADE; + END IF; + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = '_CustomerGroupAssignees_B_fkey' + ) THEN + ALTER TABLE "_CustomerGroupAssignees" + ADD CONSTRAINT "_CustomerGroupAssignees_B_fkey" + FOREIGN KEY ("B") REFERENCES "User"("id") + ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 420f54e..49712c6 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -306,6 +306,7 @@ model User { ownedInvoices Invoice[] @relation("InvoiceOwner") ownedPeople Person[] @relation("PersonOwner") ownedCompanies Company[] @relation("CompanyOwner") + assignedCustomerGroups CustomerGroup[] @relation("CustomerGroupAssignees") financialPartyDocuments FinancialDocument[] @relation("FinancialDocumentParty") ownedProjectCards ProjectCard[] @relation("ProjectCardOwner") activities Activity[] @relation("ActivityAssignee") @@ -576,6 +577,8 @@ model CustomerGroup { children CustomerGroup[] @relation("CustomerGroupTree") people PersonGroupLink[] companies CompanyGroupLink[] + /// مسئولان گروه — اختیاری؛ اعضای گروه را مثل مشتریان خود می‌بینند + assignees User[] @relation("CustomerGroupAssignees") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/apps/api/src/common/data-access.ts b/apps/api/src/common/data-access.ts index d65b590..6c10b18 100644 --- a/apps/api/src/common/data-access.ts +++ b/apps/api/src/common/data-access.ts @@ -135,6 +135,23 @@ function orParts(parts: T[]): T | { OR: T[] } | { id: string } { return parts.length === 1 ? parts[0]! : { OR: parts }; } +/** اعضای گروه‌هایی که کاربر مسئولشان است */ +function personViaGroupAssignee(userId: string): Prisma.PersonWhereInput { + return { + groupLinks: { + some: { group: { assignees: { some: { id: userId } } } }, + }, + }; +} + +function companyViaGroupAssignee(userId: string): Prisma.CompanyWhereInput { + return { + groupLinks: { + some: { group: { assignees: { some: { id: userId } } } }, + }, + }; +} + export function customersPersonWhere( actor: Pick, ): Prisma.PersonWhereInput { @@ -149,6 +166,8 @@ export function customersPersonWhere( }); if (w) parts.push(w); } + // مسئول گروه: اتحاد با محدودیت نقش (مالک خودم یا عضو گروه تحت مسئولیت) + parts.push(personViaGroupAssignee(actor.userId)); return orParts(parts); } @@ -188,6 +207,7 @@ export function customersCompanyWhere( if (!and.length) continue; parts.push(and.length === 1 ? and[0]! : { AND: and }); } + parts.push(companyViaGroupAssignee(actor.userId)); return orParts(parts); } diff --git a/apps/api/src/contacts/birthday-month.ts b/apps/api/src/contacts/birthday-month.ts index d243e0e..525f1b3 100644 --- a/apps/api/src/contacts/birthday-month.ts +++ b/apps/api/src/contacts/birthday-month.ts @@ -60,6 +60,7 @@ export function selectMonthBirthdays(people: BirthdayRow[], now = new Date(), pr const upcoming = matched.filter((row) => row.day >= today.jd).sort(byDay); const passed = matched.filter((row) => row.day < today.jd).sort(byDay); return { + year: today.jy, month: today.jm, monthName: jalaliMonthName(today.jm), today: today.jd, diff --git a/apps/api/src/contacts/companies.service.ts b/apps/api/src/contacts/companies.service.ts index d7143ed..88db4f4 100644 --- a/apps/api/src/contacts/companies.service.ts +++ b/apps/api/src/contacts/companies.service.ts @@ -169,6 +169,7 @@ export class CompaniesService { fromTahesab: Boolean(g.tahesabGid), personCount: 0, companyCount: countMap.get(g.id) ?? 0, + assignees: [], })); } diff --git a/apps/api/src/contacts/contacts.module.ts b/apps/api/src/contacts/contacts.module.ts index 5fa3765..9ee223b 100644 --- a/apps/api/src/contacts/contacts.module.ts +++ b/apps/api/src/contacts/contacts.module.ts @@ -3,6 +3,7 @@ import { LoyaltyModule } from '../loyalty/loyalty.module'; import { AutomationsModule } from '../automations/automations.module'; import { MessageAutomationsModule } from '../message-automations/message-automations.module'; import { PublicApiModule } from '../public-api/public-api.module'; +import { NotificationsModule } from '../notifications/notifications.module'; import { CompaniesController } from './companies.controller'; import { CompaniesService } from './companies.service'; import { PeopleController } from './people.controller'; @@ -20,6 +21,7 @@ import { CustomerGroupsModule } from '../customer-groups/customer-groups.module' PublicApiModule, BehaviorModule, CustomerGroupsModule, + NotificationsModule, ], controllers: [PeopleController, CompaniesController], providers: [PeopleService, CompaniesService, ContactsExcelService, SameMobileMergeService], diff --git a/apps/api/src/contacts/dto.ts b/apps/api/src/contacts/dto.ts index bda6b74..221d9d1 100644 --- a/apps/api/src/contacts/dto.ts +++ b/apps/api/src/contacts/dto.ts @@ -23,6 +23,11 @@ export class BirthdaySmsDto { @MinLength(1) @MaxLength(500) text!: string; + + /** تأیید ارسال مجدد وقتی قبلاً مسئول دیگری تبریک فرستاده */ + @IsOptional() + @IsBoolean() + force?: boolean; } export class BulkDeletePeopleDto { diff --git a/apps/api/src/contacts/people.controller.ts b/apps/api/src/contacts/people.controller.ts index 87447b4..b015349 100644 --- a/apps/api/src/contacts/people.controller.ts +++ b/apps/api/src/contacts/people.controller.ts @@ -139,6 +139,7 @@ export class PeopleController { } @Get('birthdays') + @RequirePermission('customers', { reads: true }) birthdays(@CurrentUser() user: Actor) { return this.peopleService.birthdaysThisMonth(user); } @@ -171,7 +172,7 @@ export class PeopleController { @Param('id') id: string, @Body() dto: BirthdaySmsDto, ) { - return this.peopleService.sendBirthdaySms(user, id, dto.text); + return this.peopleService.sendBirthdaySms(user, id, dto.text, Boolean(dto.force)); } @Get(':id') diff --git a/apps/api/src/contacts/people.service.ts b/apps/api/src/contacts/people.service.ts index 4e469c1..0849b02 100644 --- a/apps/api/src/contacts/people.service.ts +++ b/apps/api/src/contacts/people.service.ts @@ -22,6 +22,7 @@ import { FILTERED_IDS_MAX, PERSON_MOODS, applyPersonGenderTitle, + canViewAllRecords, splitPersonHonorific, } from '@mento/shared'; import { Prisma } from '@prisma/client'; @@ -48,6 +49,7 @@ import { import { actorCanAssignToOthers, assertAssigneeSet } from '../common/assign-access'; import { LoyaltyService } from '../loyalty/loyalty.service'; import { AutomationsService } from '../automations/automations.service'; +import { NotificationsService } from '../notifications/notifications.service'; import { MessageAutomationsService } from '../message-automations/message-automations.service'; import { WebhooksService } from '../public-api/webhooks.service'; import { GamificationService } from '../gamification/gamification.service'; @@ -157,6 +159,7 @@ export class PeopleService implements OnModuleInit { private readonly outboundSms: OutboundSmsLogService, private readonly behaviorAccess: BehaviorAccessService, private readonly groups: CustomerGroupsService, + private readonly notifications: NotificationsService, ) {} private personListWhere( @@ -349,6 +352,7 @@ export class PeopleService implements OnModuleInit { fromTahesab: Boolean(g.tahesabGid), personCount: countMap.get(g.id) ?? 0, companyCount: 0, + assignees: [], })); } @@ -684,8 +688,8 @@ export class PeopleService implements OnModuleInit { const rows = await this.prisma.person.findMany({ where: { organizationId: actor.organizationId, - ownerId: actor.userId, birthDate: { not: null }, + ...customersPersonWhere(actor), }, select: { id: true, @@ -732,13 +736,17 @@ export class PeopleService implements OnModuleInit { }; } - async sendBirthdaySms(actor: Actor, personId: string, text: string) { + async sendBirthdaySms(actor: Actor, personId: string, text: string, force = false) { const body = text.trim(); if (!body || body.length > 500) { throw new BadRequestException('متن پیامک را تا ۵۰۰ نویسه بنویسید.'); } const person = await this.prisma.person.findFirst({ - where: { id: personId, organizationId: actor.organizationId }, + where: { + id: personId, + organizationId: actor.organizationId, + ...customersPersonWhere(actor), + }, select: { id: true, firstName: true, @@ -748,11 +756,29 @@ export class PeopleService implements OnModuleInit { ownerId: true, birthDate: true, company: { select: { name: true } }, + groupLinks: { + select: { + group: { + select: { + assignees: { select: { id: true, fullName: true } }, + }, + }, + }, + }, }, }); if (!person) throw new NotFoundException('مشتری پیدا نشد'); - if (person.ownerId !== actor.userId) { - throw new ForbiddenException('فقط مسئول این مشتری می‌تواند پیامک تبریک بفرستد.'); + const assigneeIds = new Set( + person.groupLinks.flatMap((link) => link.group.assignees.map((a) => a.id)), + ); + const canSend = + canViewAllRecords(actor.role) || + person.ownerId === actor.userId || + assigneeIds.has(actor.userId); + if (!canSend) { + throw new ForbiddenException( + 'فقط مالک مشتری یا مسئول گروه او می‌تواند پیامک تبریک بفرستد.', + ); } if (!person.birthDate) { throw new BadRequestException('برای این مشتری تاریخ تولد ثبت نشده است.'); @@ -774,6 +800,32 @@ export class PeopleService implements OnModuleInit { const mobile = person.mobile || person.phone; if (!mobile) throw new BadRequestException('برای این مشتری شماره موبایل ثبت نشده است.'); const fullName = `${person.firstName} ${person.lastName}`.trim(); + const birthdayKey = `birthday:${inMonth.year}-${inMonth.month}`; + if (!force) { + const prior = await this.prisma.outboundSmsLog.findFirst({ + where: { + organizationId: actor.organizationId, + personId: person.id, + status: 'sent', + sourceId: birthdayKey, + NOT: { actorUserId: actor.userId }, + }, + orderBy: { createdAt: 'desc' }, + select: { actorUserId: true }, + }); + if (prior) { + const priorUser = prior.actorUserId + ? await this.prisma.user.findFirst({ + where: { id: prior.actorUserId }, + select: { fullName: true }, + }) + : null; + const who = priorUser?.fullName?.trim() || 'مسئول دیگری'; + throw new ConflictException( + `${who} قبلاً پیامک تبریک این مشتری را فرستاده است. آیا می‌خواهید دوباره بفرستید؟`, + ); + } + } const message = renderSmsText(body, { name: fullName, phone: mobile, @@ -793,6 +845,7 @@ export class PeopleService implements OnModuleInit { mobile, body: message, source: 'manual', + sourceId: birthdayKey, status: result.ok ? 'sent' : 'failed', error: result.ok ? null : result.reason, actorUserId: actor.userId, @@ -806,6 +859,28 @@ export class PeopleService implements OnModuleInit { : 'پیامک ارسال نشد. کمی بعد دوباره تلاش کنید.', ); } + const senderName = ( + await this.prisma.user.findFirst({ + where: { id: actor.userId }, + select: { fullName: true }, + }) + )?.fullName?.trim() || 'یکی از مسئولان'; + const peers = [...assigneeIds].filter((id) => id !== actor.userId); + await Promise.all( + peers.map((userId) => + this.notifications.create({ + organizationId: actor.organizationId, + userId, + type: 'birthday_sms_sent', + title: 'پیامک تبریک تولد ارسال شد', + body: `${senderName} برای «${fullName}» پیامک تبریک فرستاد. اگر لازم است می‌توانید دوباره بفرستید.`, + link: `/contacts/people/${person.id}`, + entityType: 'person', + entityId: person.id, + dedupe: false, + }), + ), + ); return { ok: true }; } diff --git a/apps/api/src/customer-groups/customer-groups.controller.ts b/apps/api/src/customer-groups/customer-groups.controller.ts index ec7e416..7b01ac2 100644 --- a/apps/api/src/customer-groups/customer-groups.controller.ts +++ b/apps/api/src/customer-groups/customer-groups.controller.ts @@ -8,7 +8,14 @@ import { Post, UseGuards, } from '@nestjs/common'; -import { IsIn, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; +import { + IsArray, + IsIn, + IsOptional, + IsString, + MaxLength, + MinLength, +} from 'class-validator'; import { CUSTOMER_GROUP_COLORS, type CustomerGroupColor } from '@mento/shared'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { RolesGuard } from '../auth/roles.guard'; @@ -26,6 +33,11 @@ class CreateCustomerGroupDto { @IsOptional() @IsIn([...CUSTOMER_GROUP_COLORS]) color?: CustomerGroupColor; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + assigneeIds?: string[]; } class UpdateCustomerGroupDto { @@ -43,6 +55,11 @@ class UpdateCustomerGroupDto { @IsOptional() @IsString() parentId?: string | null; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + assigneeIds?: string[]; } @Controller('customer-groups') diff --git a/apps/api/src/customer-groups/customer-groups.service.ts b/apps/api/src/customer-groups/customer-groups.service.ts index 859396d..a77e4ea 100644 --- a/apps/api/src/customer-groups/customer-groups.service.ts +++ b/apps/api/src/customer-groups/customer-groups.service.ts @@ -51,26 +51,21 @@ export class CustomerGroupsService { include: { _count: { select: { people: true, companies: true } }, parent: { select: { name: true } }, + assignees: { select: { id: true, fullName: true }, orderBy: { fullName: 'asc' } }, }, }); - return rows.map((r) => ({ - id: r.id, - name: r.name, - color: r.color, - position: r.position, - fromTahesab: Boolean(r.tahesabGid), - personCount: r._count.people, - companyCount: r._count.companies, - parentId: r.parentId, - parentName: r.parent?.name ?? null, - })); + return rows.map((r) => this.toDto(r)); } async create( organizationId: string, - dto: { name: string; color?: CustomerGroupColor }, + dto: { name: string; color?: CustomerGroupColor; assigneeIds?: string[] }, ): Promise { const name = this.cleanName(dto.name); + const assigneeIds = + dto.assigneeIds !== undefined + ? await this.validAssigneeIds(organizationId, dto.assigneeIds) + : []; const max = await this.prisma.customerGroup.aggregate({ where: { organizationId }, _max: { position: true }, @@ -82,6 +77,9 @@ export class CustomerGroupsService { name, color: this.cleanColor(dto.color), position: (max._max.position ?? -1) + 1, + ...(assigneeIds.length + ? { assignees: { connect: assigneeIds.map((id) => ({ id })) } } + : {}), }, }); } catch (e) { @@ -93,7 +91,12 @@ export class CustomerGroupsService { async update( organizationId: string, id: string, - dto: { name?: string; color?: CustomerGroupColor; parentId?: string | null }, + dto: { + name?: string; + color?: CustomerGroupColor; + parentId?: string | null; + assigneeIds?: string[]; + }, ): Promise { await this.require(organizationId, id); const parentId = @@ -101,6 +104,10 @@ export class CustomerGroupsService { if (parentId !== undefined) { await this.assertParent(organizationId, id, parentId); } + const assigneeIds = + dto.assigneeIds !== undefined + ? await this.validAssigneeIds(organizationId, dto.assigneeIds) + : undefined; try { await this.prisma.customerGroup.update({ where: { id }, @@ -108,6 +115,9 @@ export class CustomerGroupsService { ...(dto.name != null ? { name: this.cleanName(dto.name) } : {}), ...(dto.color != null ? { color: this.cleanColor(dto.color) } : {}), ...(parentId !== undefined ? { parentId } : {}), + ...(assigneeIds !== undefined + ? { assignees: { set: assigneeIds.map((uid) => ({ id: uid })) } } + : {}), }, }); } catch (e) { @@ -317,6 +327,48 @@ export class CustomerGroupsService { return link?.group.name ?? null; } + private toDto(r: { + id: string; + name: string; + color: string; + position: number; + tahesabGid: string | null; + parentId: string | null; + parent?: { name: string } | null; + _count: { people: number; companies: number }; + assignees: Array<{ id: string; fullName: string }>; + }): CustomerGroupDto { + return { + id: r.id, + name: r.name, + color: r.color, + position: r.position, + fromTahesab: Boolean(r.tahesabGid), + personCount: r._count.people, + companyCount: r._count.companies, + parentId: r.parentId, + parentName: r.parent?.name ?? null, + assignees: r.assignees.map((a) => ({ id: a.id, fullName: a.fullName })), + }; + } + + private async validAssigneeIds(organizationId: string, ids: string[]): Promise { + const unique = [...new Set((ids ?? []).map((id) => id?.trim()).filter(Boolean))]; + if (!unique.length) return []; + const rows = await this.prisma.user.findMany({ + where: { + id: { in: unique }, + isActive: true, + memberships: { some: { organizationId, isActive: true } }, + }, + select: { id: true }, + }); + if (rows.length !== unique.length) { + throw new BadRequestException('یکی از مسئولان انتخاب‌شده معتبر نیست'); + } + return rows.map((r) => r.id); + } + private async require(organizationId: string, id: string) { const row = await this.prisma.customerGroup.findFirst({ where: { id, organizationId }, diff --git a/apps/api/test/record-scope.test.ts b/apps/api/test/record-scope.test.ts index 822105c..55bff67 100644 --- a/apps/api/test/record-scope.test.ts +++ b/apps/api/test/record-scope.test.ts @@ -1,6 +1,11 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { leadsDeepWhere, resolveSectionAccess } from '../src/common/data-access'; +import { + customersCompanyWhere, + customersPersonWhere, + leadsDeepWhere, + resolveSectionAccess, +} from '../src/common/data-access'; import { noteAccessWhere, normalizeDataVisibility, @@ -122,3 +127,49 @@ describe('عمق داده وقتی نقش فقط بعضی بخش‌ها را ذ assert.deepEqual(leads.section, { mode: 'rules', rules: [{ owners: ['self'] }] }); }); }); + +describe('مسئول گروه مشتری روی عمق داده', () => { + const selfAccess = { mode: 'rules' as const, rules: [{ owners: ['self' as const] }] }; + + it('محدودیت self با OR مسئول گروه برای شخص', () => { + const where = customersPersonWhere({ + userId: 'u1', + role: 'sales', + customersAccess: selfAccess, + teamPeerIds: null, + }); + assert.deepEqual(where, { + OR: [ + { ownerId: 'u1' }, + { groupLinks: { some: { group: { assignees: { some: { id: 'u1' } } } } } }, + ], + }); + }); + + it('محدودیت self با OR مسئول گروه برای شرکت', () => { + const where = customersCompanyWhere({ + userId: 'u1', + role: 'sales', + customersAccess: selfAccess, + teamPeerIds: null, + }); + assert.deepEqual(where, { + OR: [ + { ownerId: 'u1' }, + { groupLinks: { some: { group: { assignees: { some: { id: 'u1' } } } } } }, + ], + }); + }); + + it('بدون محدودیت عمقی، فیلتر اضافه نمی‌شود', () => { + assert.deepEqual( + customersPersonWhere({ + userId: 'u1', + role: 'sales', + customersAccess: { mode: 'all', rules: [] }, + teamPeerIds: null, + }), + {}, + ); + }); +}); diff --git a/apps/web/src/components/CustomerGroupsManager.vue b/apps/web/src/components/CustomerGroupsManager.vue index bdeb5d5..04bfb2e 100644 --- a/apps/web/src/components/CustomerGroupsManager.vue +++ b/apps/web/src/components/CustomerGroupsManager.vue @@ -1,29 +1,54 @@ + + + + diff --git a/apps/web/src/components/SmsComposeModal.vue b/apps/web/src/components/SmsComposeModal.vue index e3c2433..6bee146 100644 --- a/apps/web/src/components/SmsComposeModal.vue +++ b/apps/web/src/components/SmsComposeModal.vue @@ -122,17 +122,27 @@ function apiMessage(e: unknown, fallback: string) { return fallback; } -async function send() { +async function send(force = false) { if (!props.endpoint || busy.value || done.value) return; busy.value = true; error.value = ''; try { - await api.post(props.endpoint, { text: text.value }); + await api.post(props.endpoint, { text: text.value, ...(force ? { force: true } : {}) }); done.value = 'پیامک ارسال شد.'; emit('sent'); void loadHistory(); } catch (e) { - error.value = apiMessage(e, 'ارسال پیامک ناموفق بود.'); + if (axios.isAxiosError(e) && e.response?.status === 409) { + const msg = apiMessage(e, 'قبلاً پیامک تبریک فرستاده شده است. دوباره بفرستیم؟'); + if (confirm(msg)) { + busy.value = false; + await send(true); + return; + } + error.value = msg; + } else { + error.value = apiMessage(e, 'ارسال پیامک ناموفق بود.'); + } } finally { busy.value = false; } diff --git a/apps/web/src/components/dashboard/HomeBirthdayStrip.vue b/apps/web/src/components/dashboard/HomeBirthdayStrip.vue index 0bb80fe..edc0bb2 100644 --- a/apps/web/src/components/dashboard/HomeBirthdayStrip.vue +++ b/apps/web/src/components/dashboard/HomeBirthdayStrip.vue @@ -3,6 +3,8 @@ import { computed, onMounted, ref } from 'vue'; import { RouterLink } from 'vue-router'; import { api } from '@/lib/api'; import { toFaDigits } from '@/lib/jalali'; +import { hasSectionPermission } from '@/lib/section-permissions'; +import { useAuthStore } from '@/stores/auth'; import UiIcon from '@/components/UiIcon.vue'; import { UiModal } from '@/components/ui'; import SmsComposeModal from '@/components/SmsComposeModal.vue'; @@ -25,9 +27,13 @@ type BirthdayMonth = { items: BirthdayItem[]; }; +const auth = useAuthStore(); const data = ref(null); const listOpen = ref(false); const smsTarget = ref(null); +const canSeeCustomers = computed(() => + hasSectionPermission(auth.user?.role, auth.user?.permissions, 'customers'), +); const groups = computed(() => { const map = new Map(); @@ -44,6 +50,10 @@ function openSms(person: BirthdayItem) { } onMounted(async () => { + if (!canSeeCustomers.value) { + data.value = null; + return; + } try { const { data: body } = await api.get('/people/birthdays'); data.value = body.total > 0 ? body : null; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 4f503b8..935c7eb 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1324,6 +1324,11 @@ export interface CustomerGroupSummaryDto { source?: "manual" | "tahesab"; } +export interface CustomerGroupAssigneeDto { + id: string; + fullName: string; +} + export interface CustomerGroupDto { id: string; name: string; @@ -1336,11 +1341,14 @@ export interface CustomerGroupDto { /** گروه اصلی؛ خالی یعنی خودش اصلی است */ parentId?: string | null; parentName?: string | null; + /** مسئولان گروه (اختیاری) */ + assignees: CustomerGroupAssigneeDto[]; } export interface CreateCustomerGroupRequest { name: string; color?: CustomerGroupColor; + assigneeIds?: string[]; } export interface UpdateCustomerGroupRequest { @@ -1348,6 +1356,8 @@ export interface UpdateCustomerGroupRequest { color?: CustomerGroupColor; /** null = برداشتن گروه اصلی */ parentId?: string | null; + /** جایگزینی کامل فهرست مسئولان */ + assigneeIds?: string[]; } export interface CompanySummaryDto {