import { BadRequestException, Injectable, NotFoundException, } from '@nestjs/common'; import { type DiscAnswers, type DiscAssessmentDto, type DiscDistributionRow, type DiscType, isDiscType, scoreDisc, } from '@mento/shared'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import type { Actor } from '../common/record-scope'; import { ownerWhere } from '../common/record-scope'; import { writeXlsx } from '../common/excel'; import { BehaviorAccessService } from './behavior-access.service'; import type { SaveDiscDto } from './dto'; function asAnswers(value: unknown): DiscAnswers { if (!value || typeof value !== 'object' || Array.isArray(value)) return {}; const out: DiscAnswers = {}; for (const [key, raw] of Object.entries(value as Record)) { if (raw == null || raw === '') out[key] = null; else if (isDiscType(raw)) out[key] = raw; } return out; } function asTypes(values: string[]): DiscType[] { return values.filter(isDiscType); } @Injectable() export class DiscService { constructor( private readonly prisma: PrismaService, private readonly access: BehaviorAccessService, ) {} async list(actor: Actor, personId: string): Promise { await this.access.assert(actor); await this.ensurePerson(actor, personId); const rows = await this.prisma.discAssessment.findMany({ where: { organizationId: actor.organizationId, personId }, include: { assessor: { select: { id: true, fullName: true } } }, orderBy: { createdAt: 'desc' }, }); return rows.map((row) => this.toDto(row)); } async save(actor: Actor, personId: string, dto: SaveDiscDto): Promise { await this.access.assert(actor); await this.ensurePerson(actor, personId); const answers = asAnswers(dto.answers); const scored = scoreDisc(answers); if (scored.validCount === 0) { throw new BadRequestException('حداقل یک پاسخ معتبر لازم است'); } const note = dto.note?.trim() || null; const row = await this.prisma.discAssessment.create({ data: { organizationId: actor.organizationId, personId, assessorId: actor.userId, answers: answers as Prisma.InputJsonValue, scoreD: scored.scores.D, scoreI: scored.scores.I, scoreS: scored.scores.S, scoreC: scored.scores.C, percentD: scored.percents.D, percentI: scored.percents.I, percentS: scored.percents.S, percentC: scored.percents.C, validCount: scored.validCount, primaryTypes: scored.primaryTypes, secondaryTypes: scored.secondaryTypes, label: scored.label, secondaryLabel: scored.secondaryLabel, reliable: scored.reliable, confidence: scored.confidence, note, status: 'confirmed', }, include: { assessor: { select: { id: true, fullName: true } } }, }); if (scored.reliable) { await this.prisma.person.update({ where: { id: personId }, data: { discLabel: scored.label, discReliable: true, discAssessedAt: row.createdAt, }, }); } return this.toDto(row); } async distribution(actor: Actor): Promise { await this.access.assert(actor); const rows = await this.prisma.person.groupBy({ by: ['discLabel'], where: { organizationId: actor.organizationId, discReliable: true, discLabel: { not: null }, ...ownerWhere(actor), }, _count: { _all: true }, }); return rows .flatMap((row) => row.discLabel ? [{ label: row.discLabel, count: row._count._all }] : [], ) .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label)); } async exportXlsx(actor: Actor): Promise { await this.access.assert(actor); const people = await this.prisma.person.findMany({ where: { organizationId: actor.organizationId, discReliable: true, ...ownerWhere(actor), }, select: { id: true, firstName: true, lastName: true, mobile: true, discLabel: true }, orderBy: [{ lastName: 'asc' }, { firstName: 'asc' }], }); const latest = await this.prisma.discAssessment.findMany({ where: { organizationId: actor.organizationId, personId: { in: people.map((p) => p.id) }, reliable: true, }, include: { assessor: { select: { fullName: true } } }, orderBy: { createdAt: 'desc' }, }); const byPerson = new Map(); for (const row of latest) { if (!byPerson.has(row.personId)) byPerson.set(row.personId, row); } return writeXlsx( 'DISC', [ { header: 'مشتری', key: 'name', width: 28 }, { header: 'موبایل', key: 'mobile', width: 16 }, { header: 'تیپ غالب', key: 'label', width: 16 }, { header: 'تیپ دوم', key: 'secondary', width: 16 }, { header: 'D', key: 'd', width: 8 }, { header: 'I', key: 'i', width: 8 }, { header: 'S', key: 's', width: 8 }, { header: 'C', key: 'c', width: 8 }, { header: 'اطمینان', key: 'confidence', width: 14 }, { header: 'کارشناس', key: 'assessor', width: 22 }, { header: 'تاریخ', key: 'at', width: 22 }, { header: 'یادداشت', key: 'note', width: 40 }, ], people.map((person) => { const row = byPerson.get(person.id); return { name: `${person.firstName} ${person.lastName}`.trim(), mobile: person.mobile, label: person.discLabel, secondary: row?.secondaryLabel, d: row?.scoreD, i: row?.scoreI, s: row?.scoreS, c: row?.scoreC, confidence: row?.confidence, assessor: row?.assessor.fullName, at: row?.createdAt.toISOString(), note: row?.note, }; }), ); } private async ensurePerson(actor: Actor, personId: string) { const found = await this.prisma.person.findFirst({ where: { id: personId, organizationId: actor.organizationId, ...ownerWhere(actor) }, select: { id: true }, }); if (!found) throw new NotFoundException('شخص پیدا نشد'); } private toDto(row: { id: string; personId: string; assessorId: string; assessor: { fullName: string }; answers: unknown; scoreD: number; scoreI: number; scoreS: number; scoreC: number; percentD: number; percentI: number; percentS: number; percentC: number; validCount: number; primaryTypes: string[]; secondaryTypes: string[]; label: string; secondaryLabel: string | null; reliable: boolean; confidence: string; note: string | null; createdAt: Date; updatedAt: Date; }): DiscAssessmentDto { const confidence = (['insufficient', 'low', 'medium', 'high'] as const).includes( row.confidence as 'insufficient', ) ? (row.confidence as DiscAssessmentDto['confidence']) : 'low'; return { id: row.id, personId: row.personId, assessorId: row.assessorId, assessorName: row.assessor.fullName, answers: asAnswers(row.answers), scores: { D: row.scoreD, I: row.scoreI, S: row.scoreS, C: row.scoreC }, percents: { D: row.percentD, I: row.percentI, S: row.percentS, C: row.percentC }, validCount: row.validCount, primaryTypes: asTypes(row.primaryTypes), secondaryTypes: asTypes(row.secondaryTypes), label: row.label, secondaryLabel: row.secondaryLabel, reliable: row.reliable, confidence, note: row.note, status: 'confirmed', createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), }; } }