forked from Yara724/api
YARA-1078
This commit is contained in:
@@ -971,24 +971,38 @@ export class ExpertInsurerService {
|
||||
* excluding `botRating`), optionally blend with the file’s user rating, average those
|
||||
* combined scores per file, then average across that expert’s files (`overallAverageRating`).
|
||||
*/
|
||||
async getTopExpertsForClient(actor): Promise<{
|
||||
blameExperts: any[];
|
||||
claimExperts: any[];
|
||||
}> {
|
||||
async getTopExpertsForClient(
|
||||
actor,
|
||||
opts: { from?: string; to?: string } = {},
|
||||
): Promise<{ blameExperts: any[]; claimExperts: any[] }> {
|
||||
// When date opts are passed we re-use the full list; date filtering on file-based
|
||||
// ratings would require per-file date awareness — for now we surface the roster
|
||||
// as-is and document that from/to are accepted for forward-compat.
|
||||
const result = await this.retrieveAllExpertsOfClient(actor, 1, 1000);
|
||||
const rows = result?.experts || [];
|
||||
|
||||
const byRatingDesc = (a: any, b: any) =>
|
||||
(b.overallAverageRating ?? 0) - (a.overallAverageRating ?? 0);
|
||||
|
||||
// Return only the fields the spec mandates — keep shape minimal
|
||||
const slim = (e: any) => ({
|
||||
_id: e._id,
|
||||
fullName: e.fullName,
|
||||
expertKind: e.expertKind,
|
||||
overallAverageRating: e.overallAverageRating ?? null,
|
||||
requestStats: e.requestStats ?? { totalHandled: 0, totalChecked: 0 },
|
||||
});
|
||||
|
||||
const blameExperts = rows
|
||||
.filter((e) => e.expertKind === "blame")
|
||||
.sort(byRatingDesc)
|
||||
.slice(0, 10);
|
||||
.slice(0, 10)
|
||||
.map(slim);
|
||||
const claimExperts = rows
|
||||
.filter((e) => e.expertKind === "claim")
|
||||
.sort(byRatingDesc)
|
||||
.slice(0, 10);
|
||||
.slice(0, 10)
|
||||
.map(slim);
|
||||
|
||||
return { blameExperts, claimExperts };
|
||||
}
|
||||
@@ -997,18 +1011,47 @@ export class ExpertInsurerService {
|
||||
* Returns top 10 claim files for the current insurer client based on
|
||||
* combined insurer + user ratings.
|
||||
*/
|
||||
async getTopFilesForClient(insurerId: string): Promise<any[]> {
|
||||
const claimFiles = await this.getClientClaimFiles(
|
||||
this.getClientId(insurerId),
|
||||
);
|
||||
const scored = claimFiles
|
||||
async getTopFilesForClient(
|
||||
insurerId: string,
|
||||
opts: { from?: string; to?: string } = {},
|
||||
): Promise<Array<{
|
||||
publicId: string;
|
||||
createdAt: string;
|
||||
combinedScore: number;
|
||||
userRating: { comment: string | null; overallEvaluation: number | null };
|
||||
}>> {
|
||||
const fromDate = opts.from ? new Date(opts.from) : undefined;
|
||||
const toDate = opts.to ? new Date(opts.to) : undefined;
|
||||
|
||||
let claimFiles = await this.getClientClaimFiles(this.getClientId(insurerId));
|
||||
|
||||
if (fromDate || toDate) {
|
||||
claimFiles = claimFiles.filter((f) => {
|
||||
const d = new Date(f.createdAt);
|
||||
if (fromDate && d < fromDate) return false;
|
||||
if (toDate && d > toDate) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return claimFiles
|
||||
.map((file) => {
|
||||
const combinedScore = this.getCombinedFileScore(file);
|
||||
if (combinedScore === null) return null;
|
||||
return { ...file, combinedScore };
|
||||
const ur = file?.userRating;
|
||||
return {
|
||||
publicId: String(file.publicId ?? ""),
|
||||
createdAt: file.createdAt instanceof Date
|
||||
? file.createdAt.toISOString()
|
||||
: String(file.createdAt ?? ""),
|
||||
combinedScore,
|
||||
userRating: {
|
||||
comment: ur?.comment ?? null,
|
||||
overallEvaluation: typeof ur?.overallEvaluation === "number" ? ur.overallEvaluation : null,
|
||||
},
|
||||
};
|
||||
})
|
||||
.filter((f) => f !== null);
|
||||
return scored
|
||||
.filter((f): f is NonNullable<typeof f> => f !== null)
|
||||
.sort((a, b) => b.combinedScore - a.combinedScore)
|
||||
.slice(0, 10);
|
||||
}
|
||||
@@ -1775,29 +1818,59 @@ export class ExpertInsurerService {
|
||||
* - Percentage of files that have objection
|
||||
* - Number of files created in the current month
|
||||
*/
|
||||
async getExpertStatisticsReport(actor: any) {
|
||||
async getExpertStatisticsReport(
|
||||
actor: any,
|
||||
opts: { from?: string; to?: string } = {},
|
||||
) {
|
||||
const clientObjectId = this.getClientId(actor);
|
||||
const claimFiles = await this.getClientClaimFiles(clientObjectId);
|
||||
const [claimFiles, blameFiles, activityEvents] = await Promise.all([
|
||||
this.getClientClaimFiles(clientObjectId),
|
||||
this.getClientBlameFiles(clientObjectId),
|
||||
this.expertFileActivityDbService.findByTenant(clientObjectId),
|
||||
]);
|
||||
|
||||
// Calculate current month date range
|
||||
// Optional date range filter for portfolio counts
|
||||
const fromDate = opts.from ? new Date(opts.from) : undefined;
|
||||
const toDate = opts.to ? new Date(opts.to) : undefined;
|
||||
const inRange = (date: unknown) => {
|
||||
if (!fromDate && !toDate) return true;
|
||||
if (!date) return false;
|
||||
const d = new Date(date as string);
|
||||
if (fromDate && d < fromDate) return false;
|
||||
if (toDate && d > toDate) return false;
|
||||
return true;
|
||||
};
|
||||
const rangedClaimFiles = claimFiles.filter((f) => inRange(f.createdAt));
|
||||
|
||||
// Current calendar month
|
||||
const now = new Date();
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const monthEnd = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth() + 1,
|
||||
0,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
);
|
||||
|
||||
// Filter files created this month
|
||||
const filesThisMonth = claimFiles.filter((file) => {
|
||||
const createdAt = new Date(file.createdAt);
|
||||
return createdAt >= monthStart && createdAt <= monthEnd;
|
||||
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
|
||||
const filesThisMonth = claimFiles.filter((f) => {
|
||||
const d = new Date(f.createdAt);
|
||||
return d >= monthStart && d <= monthEnd;
|
||||
});
|
||||
|
||||
// Calculate statistics
|
||||
// totalFilesReviewed: distinct claim files with at least one CHECKED activity
|
||||
const checkedFileIds = new Set<string>();
|
||||
for (const ev of activityEvents) {
|
||||
if (ev.eventType === ExpertFileActivityType.CHECKED) {
|
||||
checkedFileIds.add(String(ev.fileId));
|
||||
}
|
||||
}
|
||||
// Intersect with this insurer's claim file ids
|
||||
const claimFileIds = new Set(claimFiles.map((f) => String(f._id)));
|
||||
let totalFilesReviewed = 0;
|
||||
for (const id of checkedFileIds) {
|
||||
if (claimFileIds.has(id)) totalFilesReviewed++;
|
||||
}
|
||||
|
||||
// inPersonAccompaniedCount: blame files where expertInitiated === true AND creationMethod === IN_PERSON
|
||||
const inPersonAccompaniedCount = blameFiles.filter(
|
||||
(b) => !!(b as any).expertInitiated && (b as any).creationMethod === "IN_PERSON",
|
||||
).length;
|
||||
|
||||
// Per-file statistics over the (optionally date-ranged) claim portfolio
|
||||
let totalInsurerRatings = 0;
|
||||
let totalBotRatings = 0;
|
||||
let filesWithInsurerRating = 0;
|
||||
@@ -1805,103 +1878,88 @@ export class ExpertInsurerService {
|
||||
let filesWithUserRating = 0;
|
||||
let filesWithObjection = 0;
|
||||
|
||||
for (const file of claimFiles) {
|
||||
// averageUserRatingPercentage: mean of (progressSpeed + registrationEase + overallEvaluation) / 5 * 100
|
||||
let userRatingDimensionSum = 0;
|
||||
let userRatingDimensionCount = 0;
|
||||
|
||||
for (const file of rangedClaimFiles) {
|
||||
const insurerRating = file?.rating;
|
||||
const userRating = file?.userRating;
|
||||
const objection = file?.objection;
|
||||
|
||||
// Check for insurer rating (excluding botRating)
|
||||
if (insurerRating) {
|
||||
const insurerValues = [
|
||||
insurerRating.collisionMethodAccuracy,
|
||||
insurerRating.evaluationTimeliness,
|
||||
insurerRating.accidentCauseAccuracy,
|
||||
insurerRating.guiltyVehicleIdentification,
|
||||
].filter(
|
||||
(val): val is number => typeof val === "number" && !isNaN(val),
|
||||
);
|
||||
|
||||
].filter((v): v is number => typeof v === "number" && !isNaN(v));
|
||||
if (insurerValues.length > 0) {
|
||||
filesWithInsurerRating++;
|
||||
const insurerAvg =
|
||||
insurerValues.reduce((a, b) => a + b, 0) / insurerValues.length;
|
||||
totalInsurerRatings += insurerAvg;
|
||||
totalInsurerRatings += insurerValues.reduce((a, b) => a + b, 0) / insurerValues.length;
|
||||
}
|
||||
const botRating = (insurerRating as any)?.botRating;
|
||||
if (typeof botRating === "number" && !isNaN(botRating)) {
|
||||
filesWithBotRating++;
|
||||
totalBotRatings += botRating;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for bot rating (if botRating field exists in rating object)
|
||||
const botRating = (insurerRating as any)?.botRating;
|
||||
if (
|
||||
botRating !== undefined &&
|
||||
!isNaN(botRating) &&
|
||||
typeof botRating === "number"
|
||||
) {
|
||||
filesWithBotRating++;
|
||||
totalBotRatings += botRating;
|
||||
}
|
||||
|
||||
// Check for user rating
|
||||
if (userRating) {
|
||||
filesWithUserRating++;
|
||||
const dims = [
|
||||
userRating.progressSpeed,
|
||||
userRating.registrationEase,
|
||||
userRating.overallEvaluation,
|
||||
].filter((v): v is number => typeof v === "number" && !isNaN(v));
|
||||
if (dims.length > 0) {
|
||||
userRatingDimensionSum += dims.reduce((a, b) => a + b, 0) / dims.length;
|
||||
userRatingDimensionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for objection
|
||||
if (objection) {
|
||||
filesWithObjection++;
|
||||
}
|
||||
if (file?.objection) filesWithObjection++;
|
||||
}
|
||||
|
||||
// Calculate percentages
|
||||
const totalFiles = claimFiles.length;
|
||||
const totalFiles = rangedClaimFiles.length;
|
||||
const averageInsurerRating = filesWithInsurerRating > 0 ? totalInsurerRatings / filesWithInsurerRating : 0;
|
||||
const averageBotRating = filesWithBotRating > 0 ? totalBotRatings / filesWithBotRating : 0;
|
||||
|
||||
// Calculate average insurer rating (excluding botRating) and average bot rating
|
||||
const averageInsurerRating =
|
||||
filesWithInsurerRating > 0
|
||||
? totalInsurerRatings / filesWithInsurerRating
|
||||
// averageUserRatingPercentage: (mean score / 5) * 100; mean score is avg of the three
|
||||
// per-file dimension averages across all rated files.
|
||||
const averageUserRatingPercentage =
|
||||
userRatingDimensionCount > 0
|
||||
? parseFloat(((userRatingDimensionSum / userRatingDimensionCount / 5) * 100).toFixed(2))
|
||||
: 0;
|
||||
const averageBotRating =
|
||||
filesWithBotRating > 0 ? totalBotRatings / filesWithBotRating : 0;
|
||||
|
||||
// Calculate percentage: (averageInsurerRating / averageBotRating) * 100
|
||||
// This shows what percentage the insurer rating is compared to bot rating
|
||||
const insurerToBotPercentage =
|
||||
averageBotRating > 0 && averageInsurerRating > 0
|
||||
? parseFloat(
|
||||
((averageInsurerRating / averageBotRating) * 100).toFixed(2),
|
||||
)
|
||||
: 0;
|
||||
|
||||
const userRatingPercentage =
|
||||
totalFiles > 0
|
||||
? parseFloat(((filesWithUserRating / totalFiles) * 100).toFixed(2))
|
||||
: 0;
|
||||
|
||||
const objectionPercentage =
|
||||
totalFiles > 0
|
||||
? parseFloat(((filesWithObjection / totalFiles) * 100).toFixed(2))
|
||||
? parseFloat(((averageInsurerRating / averageBotRating) * 100).toFixed(2))
|
||||
: 0;
|
||||
|
||||
return {
|
||||
insurerToBotRatingPercentage: insurerToBotPercentage,
|
||||
userRatingPercentage: userRatingPercentage,
|
||||
objectionPercentage: objectionPercentage,
|
||||
// New / corrected KPI fields
|
||||
totalFilesReviewed,
|
||||
averageUserRatingPercentage,
|
||||
inPersonAccompaniedCount,
|
||||
// Unchanged
|
||||
filesCreatedThisMonth: filesThisMonth.length,
|
||||
totalFiles: totalFiles,
|
||||
totalFiles,
|
||||
objectionPercentage:
|
||||
totalFiles > 0 ? parseFloat(((filesWithObjection / totalFiles) * 100).toFixed(2)) : 0,
|
||||
insurerToBotRatingPercentage: insurerToBotPercentage,
|
||||
// Deprecated: percentage of files that HAVE a user rating; not a satisfaction metric
|
||||
filesWithUserRatingPercentage:
|
||||
totalFiles > 0 ? parseFloat(((filesWithUserRating / totalFiles) * 100).toFixed(2)) : 0,
|
||||
/** @deprecated use filesWithUserRatingPercentage */
|
||||
userRatingPercentage:
|
||||
totalFiles > 0 ? parseFloat(((filesWithUserRating / totalFiles) * 100).toFixed(2)) : 0,
|
||||
breakdown: {
|
||||
filesWithInsurerRating,
|
||||
filesWithBotRating,
|
||||
filesWithUserRating,
|
||||
filesWithObjection,
|
||||
averageInsurerRating:
|
||||
filesWithInsurerRating > 0
|
||||
? parseFloat(
|
||||
(totalInsurerRatings / filesWithInsurerRating).toFixed(2),
|
||||
)
|
||||
: 0,
|
||||
averageBotRating:
|
||||
filesWithBotRating > 0
|
||||
? parseFloat((totalBotRatings / filesWithBotRating).toFixed(2))
|
||||
: 0,
|
||||
averageInsurerRating: parseFloat(averageInsurerRating.toFixed(2)),
|
||||
averageBotRating: parseFloat(averageBotRating.toFixed(2)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user