add analytics
This commit is contained in:
282
cron/analytics.js
Normal file
282
cron/analytics.js
Normal 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));
|
||||
@@ -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"
|
||||
15
src/analytics/analytics.controller.ts
Normal file
15
src/analytics/analytics.controller.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
13
src/analytics/analytics.routes.ts
Normal file
13
src/analytics/analytics.routes.ts
Normal 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
|
||||
);
|
||||
}
|
||||
12
src/analytics/analytics.schema.ts
Normal file
12
src/analytics/analytics.schema.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
export const analyticsModel = mongoose.model(
|
||||
"analytics",
|
||||
new mongoose.Schema(
|
||||
{
|
||||
tenantId: String,
|
||||
},
|
||||
{ strict: false }
|
||||
),
|
||||
"analytics"
|
||||
);
|
||||
6
src/analytics/analytics.service.ts
Normal file
6
src/analytics/analytics.service.ts
Normal 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 });
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -37,4 +37,5 @@ export type Claim =
|
||||
| "note:delete"
|
||||
| "payment:read"
|
||||
| "alert:read"
|
||||
| "alert:write";
|
||||
| "alert:write"
|
||||
| "analytics:read";
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user