add analytics

This commit is contained in:
2025-06-27 14:39:12 +05:30
parent 38e6521930
commit a8136b8175
9 changed files with 336 additions and 1 deletions

282
cron/analytics.js Normal file
View File

@@ -0,0 +1,282 @@
import mongoose from "mongoose";
const permitSchema = new mongoose.Schema({
tenantId: {
type: String,
required: true,
},
pid: {
type: String,
unique: true,
},
permitNumber: String,
county: Object,
client: {
type: mongoose.Types.ObjectId,
ref: "organization",
},
clientData: Object,
permitDate: Date,
stage: new mongoose.Schema(
{
pipeline: Array,
currentStage: Number,
},
{ _id: false }
),
status: String,
manualStatus: String,
cleanStatus: String,
permitType: String,
utility: String,
assignedTo: {
type: mongoose.Types.ObjectId,
ref: "user",
},
link: String,
address: Object,
recordType: String,
description: String,
applicationDetails: Object,
applicationInfo: Object,
applicationInfoTable: Object,
conditions: Array,
ownerDetails: String,
parcelInfo: Object,
paymentData: Object,
professionalsList: Array,
inspections: Object,
createdAt: Date,
createdBy: {
type: mongoose.Types.ObjectId,
ref: "user",
},
newProcessingStatus: Array,
newPayment: Array,
newConditions: Array,
professionals: Object,
recordId: String,
relatedRecords: Object,
accelaStatus: String,
openDate: Date,
lastUpdateDate: Date,
statusUpdated: Date,
issuedDate: Date,
communityName: String,
lot: String,
block: String,
jobNumber: String,
startDate: Date,
history: Array,
}).index({ tenantId: 1, permitNumber: 1 }, { unique: true });
const permitModel = mongoose.model("permit", permitSchema);
const processedModel = mongoose.model("processed", permitSchema, "processed");
const analyticsModel = mongoose.model(
"analytics",
new mongoose.Schema(
{
tenantId: String,
},
{ strict: false }
),
"analytics"
);
let combinedPermits = [];
function dateDiff(date1, date2, unit = "days") {
const d1 = new Date(date1);
const d2 = new Date(date2);
const diffMs = Math.abs(d2 - d1);
const conversions = {
milliseconds: diffMs,
seconds: diffMs / 1000,
minutes: diffMs / (1000 * 60),
hours: diffMs / (1000 * 60 * 60),
days: diffMs / (1000 * 60 * 60 * 24),
weeks: diffMs / (1000 * 60 * 60 * 24 * 7),
months: diffMs / (1000 * 60 * 60 * 24 * 30.44),
years: diffMs / (1000 * 60 * 60 * 24 * 365.25),
};
if (conversions.hasOwnProperty(unit)) {
return conversions[unit];
} else {
throw new Error(
`Invalid unit: ${unit}. Use one of: ${Object.keys(conversions).join(
", "
)}`
);
}
}
async function getSubmissionMetrics() {
const groupByCountySubmissions = {};
const groupByClientSubmissions = {};
for (const permit of combinedPermits) {
try {
const dateStr = permit.openDate.toISOString().split("T")[0];
if (!groupByClientSubmissions[dateStr])
groupByClientSubmissions[dateStr] = {};
if (!groupByCountySubmissions[dateStr])
groupByCountySubmissions[dateStr] = {};
if (!groupByClientSubmissions[dateStr][permit.clientData?.name])
groupByClientSubmissions[dateStr][permit.clientData?.name] = 0;
groupByClientSubmissions[dateStr][permit.clientData?.name]++;
if (!groupByCountySubmissions[dateStr][permit.county?.name])
groupByCountySubmissions[dateStr][permit.county?.name] = 0;
groupByCountySubmissions[dateStr][permit.county?.name]++;
} catch (err) {
console.log(err);
}
}
return {
groupByClientSubmissions,
groupByCountySubmissions,
};
}
function calculateAverages(arr) {
if (!Array.isArray(arr)) {
throw new TypeError("Input must be an array");
}
if (arr.length === 0) {
return {
min: null,
max: null,
median: null,
average: null,
sum: null,
count: 0,
};
}
// Filter out non-numeric values and convert valid numbers
const cleanArr = arr.map(Number).filter((n) => !isNaN(n));
if (cleanArr.length === 0) {
return {
min: null,
max: null,
median: null,
average: null,
sum: null,
count: 0,
};
}
// Sort array for median calculation (slice to avoid mutating original)
const sorted = [...cleanArr].sort((a, b) => a - b);
const average = sum / count;
// Calculate median
const middle = Math.floor(count / 2);
let median;
if (count % 2 === 0) {
median = (sorted[middle - 1] + sorted[middle]) / 2;
} else {
median = sorted[middle];
}
return {
min: sorted[0],
max: sorted[count - 1],
median,
average,
};
}
async function getApprovalMetrics() {
const approvedPermits = combinedPermits.filter(
(item) => item.accelaStatus === "APPROVED"
);
const approvalDurationClient = {};
const approvalDurationCounty = {};
for (const permit of approvedPermits) {
const diff = dateDiff(permit.issuedDate, permit.openDate);
if (!approvalDurationClient[permit.clientData?.name])
approvalDurationClient[permit.clientData?.name] = [];
approvalDurationClient[permit.clientData?.name]?.push(diff);
if (!approvalDurationCounty[permit.county?.name])
approvalDurationClient[permit.county?.name] = [];
approvalDurationCounty[permit.county?.name]?.push(diff);
}
for (const client in approvalDurationClient) {
approvalDurationClient[client] = calculateAverages(
approvalDurationClient[client]
);
}
for (const county in approvalDurationCounty) {
approvalDurationCounty[county] = calculateAverages(
approvalDurationCounty[county]
);
}
return {
totalApproved: approvedPermits.length,
approvalDurationClient,
approvalDurationCounty,
};
}
(async () => {
await mongoose.connect(process.env.DB_URI);
const startDate = new Date(Date.now() - 3600 * 24 * 30 * 1000);
const endDate = new Date();
const recentPermits = await permitModel
.find({
openDate: { $gte: startDate, $lte: endDate },
})
.select(
"clientData county openDate issuedDate accelaStatus status manualStatus cleanStatus"
);
const recentProcessed = await processedModel
.find({
openDate: { $gte: startDate, $lte: endDate },
})
.select(
"clientData county openDate issuedDate accelaStatus status manualStatus cleanStatus"
);
combinedPermits = [...recentPermits, ...recentProcessed];
const submissionsByOrg = await getSubmissionMetrics();
const approvedCount = await getApprovalMetrics();
const analytics = {
...submissionsByOrg,
...approvedCount,
};
await analyticsModel.findOneAndUpdate(
{ tenantId: "arf4w59nzduytv7" },
analytics,
{
upsert: true,
}
);
await mongoose.connection.close();
})().catch((err) => console.log(err));

View File

@@ -16,3 +16,4 @@ cd "$WORKDIR" || { echo "Failed to change directory to $WORKDIR"; exit 1; }
node --env-file=.env "$CRON_DIR/archive.js"
node --env-file=.env "$CRON_DIR/configUpdate.js"
node --env-file=.env "$CRON_DIR/analytics.js"

View File

@@ -0,0 +1,15 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { getAnalytics } from "./analytics.service";
export async function getAnalyticsHandler(
req: FastifyRequest,
res: FastifyReply
) {
try {
const analytics = await getAnalytics(req.user);
if (!analytics) return res.code(404).send({ error: "resource not found" });
return res.code(200).send(analytics);
} catch (err) {
return err;
}
}

View File

@@ -0,0 +1,13 @@
import { FastifyInstance } from "fastify";
import { getAnalyticsHandler } from "./analytics.controller";
export async function analyticsRoutes(fastify: FastifyInstance) {
fastify.get(
"",
{
config: { requiredClaims: ["analytics:read"] },
preHandler: [fastify.authorize],
},
getAnalyticsHandler
);
}

View File

@@ -0,0 +1,12 @@
import mongoose from "mongoose";
export const analyticsModel = mongoose.model(
"analytics",
new mongoose.Schema(
{
tenantId: String,
},
{ strict: false }
),
"analytics"
);

View File

@@ -0,0 +1,6 @@
import { AuthenticatedUser } from "../auth";
import { analyticsModel } from "./analytics.schema";
export async function getAnalytics(user: AuthenticatedUser) {
return await analyticsModel.findOne({ tenantId: user.tenantId });
}

View File

@@ -16,6 +16,7 @@ import { processedRoutes } from "./processed/processed.route";
import { ctaskRoutes } from "./ctask/ctask.route";
import { paymentRoutes } from "./payments/payment.route";
import { alertRoutes } from "./alert/alert.route";
import { analyticsRoutes } from "./analytics/analytics.routes";
export default async function routes(fastify: FastifyInstance) {
fastify.addHook("preHandler", authHandler);
@@ -34,5 +35,6 @@ export default async function routes(fastify: FastifyInstance) {
fastify.register(processedRoutes, { prefix: "/processed" });
fastify.register(paymentRoutes, { prefix: "/payments" });
fastify.register(alertRoutes, { prefix: "/alerts" });
fastify.register(analyticsRoutes, { prefix: "/analytics" });
fastify.register(realTimeRoutes);
}

View File

@@ -37,4 +37,5 @@ export type Claim =
| "note:delete"
| "payment:read"
| "alert:read"
| "alert:write";
| "alert:write"
| "analytics:read";

View File

@@ -39,6 +39,7 @@ export const rules: Record<
"payment:read",
"alert:read",
"alert:write",
"analytics:read",
],
hiddenFields: {
orgs: ["__v"],
@@ -79,6 +80,7 @@ export const rules: Record<
"payment:read",
"alert:read",
"alert:write",
"analytics:read",
],
hiddenFields: {
orgs: ["__v"],
@@ -111,6 +113,7 @@ export const rules: Record<
"note:delete",
"alert:read",
"alert:write",
"analytics:read",
],
hiddenFields: {
orgs: ["__v"],