Service Library - aiWorkforceAnalytics
This document provides a complete reference of the custom code library for the aiWorkforceAnalytics service. It includes all library functions, edge functions with their REST endpoints, templates, and assets.
Library Functions
Library functions are reusable modules available to all business APIs and other custom code within the service via require("lib/<moduleName>").
checkCompanyAIAccess.js
module.exports = function checkCompanyAIAccess(companySubscription) {
if (!companySubscription) return false;
if (companySubscription.status !== 'active') return false;
if (companySubscription.paymentStatus && companySubscription.paymentStatus !== 'paid') return false;
if (!companySubscription.subscribedFeatures || !Array.isArray(companySubscription.subscribedFeatures)) return false;
if (companySubscription.expiryDate && new Date(companySubscription.expiryDate) < new Date()) return false;
return companySubscription.subscribedFeatures.includes('aiInsights') || companySubscription.subscribedFeatures.includes('aiWorkforceAnalytics');
};
generateInsightStream.js
const common = require("common");
const serviceCommon = require("serviceCommon");
module.exports = async (context) => {
const { goal, period, inputData, insightType, applicablePeriod, details, session } = context;
// Validate required fields
const requiredFields = ['insightType', 'applicablePeriod', 'details'];
const missingFields = requiredFields.filter(field => {
const value = context[field];
return value === undefined || value === null || value === '';
});
if (missingFields.length > 0) {
const error = new Error(`Missing required fields: ${missingFields.join(', ')}`);
error.statusCode = 400;
error.code = 'VALIDATION_ERROR';
throw error;
}
// Validate applicablePeriod has startDate and endDate
if (!applicablePeriod || !applicablePeriod.startDate || !applicablePeriod.endDate) {
const error = new Error('applicablePeriod must contain startDate and endDate');
error.statusCode = 400;
error.code = 'VALIDATION_ERROR';
throw error;
}
// Build the prompt for AI agent
const prompt = JSON.stringify({
goal: goal || 'Generate workforce insight',
period: period || '',
inputData: inputData || {},
insightType: insightType,
applicablePeriod: applicablePeriod
});
// Call AI Agent via AgentHub
try {
const agentHub = serviceCommon.getAgentHubClient();
const agentResult = await agentHub.callAgent('insightGenerator', {
message: prompt,
session: session
});
// Return the AI insight result for SSE streaming
return {
success: true,
insightType: insightType,
applicablePeriod: applicablePeriod,
details: agentResult.response || details,
aiStatus: 'delivered',
generatedContent: agentResult.response || ''
};
} catch (agentError) {
console.error('AI Agent call failed:', agentError);
throw new Error(`AI insight generation failed: ${agentError.message}`);
}
};
fetchCompanyWorkforceData.js
const axios = require("axios");
const common = require("common");
module.exports = async (context) => {
const { companyId, period } = context;
if (!companyId) throw new Error("companyId is required");
const authUrl = process.env.AUTH_SERVICE_URL || "http://auth-api:3011";
const employeeUrl = process.env.EMPLOYEEPROFILE_API_URL || "http://employeeprofile-api:3011";
const scheduleUrl = process.env.SCHEDULEMANAGEMENT_API_URL || "http://schedulemanagement-api:3011";
const attendanceUrl = process.env.ATTENDANCEMANAGEMENT_API_URL || "http://attendancemanagement-api:3011";
const taskUrl = process.env.TASKMANAGEMENT_API_URL || "http://taskmanagement-api:3011";
const leaveUrl = process.env.LEAVEMANAGEMENT_API_URL || "http://leavemanagement-api:3011";
const jwtToken = await common.crypto.createInternalServiceJWT();
const headers = { Authorization: `Bearer ${jwtToken}` };
try {
// Build query params with companyId filter
const companyQuery = `companyId=${companyId}&pageRowCount=0`;
const [
departmentsRes,
usersRes,
employeeProfilesRes,
shiftsRes,
attendanceRes,
tasksRes,
leaveRes,
] = await Promise.allSettled([
axios.get(`${authUrl}/v1/usergroups?${companyQuery}`, { headers })
.catch(() => ({ data: { userGroups: [] } })),
axios.get(`${authUrl}/v1/users?${companyQuery}`, { headers })
.catch(() => ({ data: { items: [] } })),
axios.get(`${employeeUrl}/v1/employeeprofiles?${companyQuery}`, { headers })
.catch(() => ({ data: { employeeProfiles: [] } })),
axios.get(`${scheduleUrl}/v1/shifts?${companyQuery}`, { headers })
.catch(() => ({ data: { shifts: [] } })),
axios.get(`${attendanceUrl}/v1/attendancerecords?${companyQuery}`, { headers })
.catch(() => ({ data: { attendanceRecords: [] } })),
axios.get(`${taskUrl}/v1/taskassignments?${companyQuery}`, { headers })
.catch(() => ({ data: { taskAssignments: [] } })),
axios.get(`${leaveUrl}/v1/leaverequests?${companyQuery}`, { headers })
.catch(() => ({ data: { leaveRequests: [] } })),
]);
const departments = departmentsRes.status === "fulfilled"
? departmentsRes.value.data?.userGroups || []
: [];
const users = usersRes.status === "fulfilled"
? usersRes.value.data?.items || []
: [];
const employeeProfiles = employeeProfilesRes.status === "fulfilled"
? employeeProfilesRes.value.data?.employeeProfiles || employeeProfilesRes.value.data?.items || []
: [];
const shifts = shiftsRes.status === "fulfilled"
? shiftsRes.value.data?.shifts || []
: [];
const attendance = attendanceRes.status === "fulfilled"
? attendanceRes.value.data?.attendanceRecords || attendanceRes.value.data?.items || []
: [];
const tasks = tasksRes.status === "fulfilled"
? tasksRes.value.data?.taskAssignments || tasksRes.value.data?.items || []
: [];
const leaveRequests = leaveRes.status === "fulfilled"
? leaveRes.value.data?.leaveRequests || []
: [];
const stats = {
totalEmployees: employeeProfiles.length || users.length,
totalDepartments: departments.length,
totalShifts: shifts.length,
totalAttendanceRecords: attendance.length,
totalTasks: tasks.length,
pendingLeaveRequests: leaveRequests.filter((l) => l.status === "pending").length,
approvedLeaveRequests: leaveRequests.filter((l) => l.status === "approved").length,
rejectedLeaveRequests: leaveRequests.filter((l) => l.status === "rejected").length,
lateCheckIns: attendance.filter((a) => a.status === "late").length,
earlyDepartures: attendance.filter((a) => a.status === "earlyLeave").length,
absences: attendance.filter((a) => a.status === "absent").length,
completedTasks: tasks.filter((t) => t.status === "completed").length,
pendingTasks: tasks.filter((t) => t.status === "pending" || t.status === "active").length,
};
return {
stats,
departments: departments.map((d) => ({ id: d.id, groupName: d.groupName })),
employees: employeeProfiles.map((p) => {
const user = users.find((u) => u.id === p.userId);
const dept = departments.find((d) => d.id === p.departmentId);
return {
id: p.id,
userId: p.userId,
fullname: user?.fullname || "Unknown",
email: user?.email || "N/A",
department: dept?.groupName || "Unassigned",
position: p.position || "N/A",
contractType: p.contractType || "N/A",
};
}),
attendanceSummary: attendance.slice(0, 50),
recentTasks: tasks.slice(0, 20),
recentLeaveRequests: leaveRequests.slice(0, 20),
};
} catch (error) {
console.error("fetchCompanyWorkforceData error:", error.message);
return { error: error.message, stats: {}, departments: [], employees: [] };
}
};
fetchCompanyData.js
const axios = require('axios');
module.exports = async (context) => {
const { token, baseUrl, userId, companyCodename, companyId } = context;
if (!token || !baseUrl) {
throw new Error('Token and baseUrl are required');
}
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
if (companyCodename) {
headers['mbx-company-codename'] = companyCodename;
}
try {
// Get current user info
const userResponse = await axios.get(`${baseUrl}/auth-api/currentuser`, { headers });
const currentUser = userResponse.data;
const userCompanyId = companyId || currentUser?.company?.companyId;
// Get employee profiles for the company (use companyId filter if API supports it)
let employeeCount = 0;
try {
const employeeResponse = await axios.get(`${baseUrl}/employeeprofile-api/v1/employeeprofiles?pageNumber=0`, { headers });
// Filter by company if the API returns all - items should already be filtered by session
const items = employeeResponse.data?.data || employeeResponse.data?.items || [];
employeeCount = employeeResponse.data.rowCount || items.length;
} catch (e) {
console.log('Employee API error:', e.message);
}
// Get departments (userGroups)
let departmentCount = 0;
try {
const deptResponse = await axios.get(`${baseUrl}/auth-api/v1/usergroups?pageNumber=0`, { headers });
const items = deptResponse.data?.data || deptResponse.data?.userGroups || [];
departmentCount = deptResponse.data.rowCount || items.length;
} catch (e) {
console.log('Department API error:', e.message);
}
// Get today's shifts
let todayShifts = 0;
try {
const today = new Date().toISOString().split('T')[0];
const shiftsResponse = await axios.get(`${baseUrl}/schedulemanagement-api/v1/shifts?shiftDate=${today}&pageNumber=0`, { headers });
const items = shiftsResponse.data?.data || shiftsResponse.data?.shifts || [];
todayShifts = shiftsResponse.data.rowCount || items.length;
} catch (e) {
console.log('Today shifts API error:', e.message);
}
// Get all shifts
let totalShifts = 0;
try {
const allShiftsResponse = await axios.get(`${baseUrl}/schedulemanagement-api/v1/shifts?pageNumber=0`, { headers });
const items = allShiftsResponse.data?.data || allShiftsResponse.data?.shifts || [];
totalShifts = allShiftsResponse.data.rowCount || items.length;
} catch (e) {
console.log('All shifts API error:', e.message);
}
// Get task assignments - THIS WAS THE BUG - using wrong endpoint
let taskCount = 0;
try {
const tasksResponse = await axios.get(`${baseUrl}/taskmanagement-api/v1/taskassignments?pageNumber=0`, { headers });
const items = tasksResponse.data?.data || tasksResponse.data?.items || [];
taskCount = tasksResponse.data.rowCount || items.length;
} catch (e) {
console.log('Tasks API error:', e.message);
}
// Get individual tasks for this user
let myTaskCount = 0;
try {
const myTasksResponse = await axios.get(`${baseUrl}/taskmanagement-api/v1/myindividualtasks?pageNumber=0`, { headers });
const items = myTasksResponse.data?.data || myTasksResponse.data?.items || [];
myTaskCount = myTasksResponse.data.rowCount || items.length;
} catch (e) {
console.log('My tasks API error:', e.message);
}
// Get attendance records
let attendanceCount = 0;
try {
const attendanceResponse = await axios.get(`${baseUrl}/attendancemanagement-api/v1/attendancerecords?pageNumber=0`, { headers });
const items = attendanceResponse.data?.data || attendanceResponse.data?.items || [];
attendanceCount = attendanceResponse.data.rowCount || items.length;
} catch (e) {
console.log('Attendance API error:', e.message);
}
// Get leave requests
let leaveCount = 0;
try {
const leaveResponse = await axios.get(`${baseUrl}/leavemanagement-api/v1/leaverequests?pageNumber=0`, { headers });
const items = leaveResponse.data?.data || leaveResponse.data?.leaveRequests || [];
leaveCount = leaveResponse.data.rowCount || items.length;
} catch (e) {
console.log('Leave API error:', e.message);
}
return {
user: currentUser,
employeeCount,
departmentCount,
todayShifts,
totalShifts,
taskCount,
myTaskCount,
attendanceCount,
leaveCount,
today: new Date().toISOString().split('T')[0],
companyCodename: companyCodename || currentUser?.company?.codename || 'unknown',
userCompanyId: userCompanyId
};
} catch (error) {
console.error('Error fetching company data:', error.message);
if (error.response) {
console.error('Response status:', error.response.status);
console.error('Response data:', error.response.data);
}
throw new Error(`Failed to fetch company data: ${error.message}`);
}
};
Edge Functions
Edge functions are custom HTTP endpoint handlers that run outside the standard Business API pipeline. Each edge function is paired with an Edge Controller that defines its REST endpoint.
generateAiInsightStreamHandler.js
Edge Controller:
- Path:
/ai-insight/stream - Method:
GET - Login Required: No
const axios = require("axios");
module.exports = async (req) => {
const baseUrl = "https://workforceos.prw.mindbricks.com";
const authHeader = req.headers?.authorization;
const token = authHeader?.substring(7);
const companyCodename = req.headers["mbx-company-codename"];
if (!token)
return {
status: 401,
headers: { "Content-Type": "text/event-stream" },
content: `event: error\ndata: {"error":"Authentication required"}\n\n`,
};
const goal = req.body?.goal || req.body?.query;
if (!goal)
return {
status: 400,
headers: { "Content-Type": "text/event-stream" },
content: `event: error\ndata: {"error":"Goal or query is required"}\n\n`,
};
const headers = {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
};
if (companyCodename) headers["mbx-company-codename"] = companyCodename;
try {
// Call the workforceAssistant AI agent in agentHub
const agentUrl = `${baseUrl}/agenthub-api/agents/workforceAssistant`;
const agentRes = await axios.post(
agentUrl,
{ message: goal },
{ headers }
);
const aiResponse = agentRes.data;
// Stream the response
const sse =
`event: start\ndata: {}\n\n` +
`event: chunk\ndata: {"chunk": "Workforce AI Analysis", "type": "title"}\n\n` +
`event: chunk\ndata: {"chunk": ${JSON.stringify(aiResponse)}, "type": "summary"}\n\n` +
`event: complete\ndata: {"status": "completed", "insightType": "companySpecific"}\n\n`;
return {
status: 200,
headers: { "Content-Type": "text/event-stream" },
content: sse,
};
} catch (agentError) {
console.error("AI Agent call failed:", agentError.message);
// Fallback response if AI agent fails
const sse =
`event: start\ndata: {}\n\n` +
`event: chunk\ndata: {"chunk": "Workforce AI", "type": "title"}\n\n` +
`event: chunk\ndata: {"chunk": "I'm having trouble accessing the AI service right now. Please try again in a moment.", "type": "summary"}\n\n` +
`event: complete\ndata: {"status": "completed", "insightType": "general"}\n\n`;
return {
status: 200,
headers: { "Content-Type": "text/event-stream" },
content: sse,
};
}
};
streamAiInsight.js
// Wrapper for generateAiInsightStreamHandler with login enforcement
const handler = require('./generateAiInsightStreamHandler');
module.exports = async (request) => {
return handler(request);
};
testFunction.js
module.exports = async (req) => { return { status: 200, content: 'test' }; };
generateInsightHandler.js
Edge Controller:
- Path:
generate-insight - Method:
GET - Login Required: Yes
const axios = require('axios');
module.exports = async (req, res) => {
try {
// Get token from request
const authHeader = req.headers['authorization'] || req.headers['Authorization'];
const token = authHeader ? authHeader.replace('Bearer ', '') : null;
if (!token) {
return res.status(401).json({
status: 'ERR',
message: 'Authorization token required'
});
}
// Get company codename from header
const companyCodename = req.headers['mbx-company-codename'] || req.headers['mbx-Company-Codename'];
// Build base URL from request
const protocol = req.headers['x-forwarded-proto'] || 'https';
const host = req.headers['host'];
const baseUrl = `${protocol}://${host}`;
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
if (companyCodename) {
headers['mbx-company-codename'] = companyCodename;
}
// Get current user info
const userResponse = await axios.get(`${baseUrl}/auth-api/currentuser`, { headers });
const currentUser = userResponse.data;
if (!currentUser || !currentUser.userId) {
return res.status(401).json({
status: 'ERR',
message: 'Invalid token or user not found'
});
}
// Fetch all company data using the APIs
const today = new Date().toISOString().split('T')[0];
// Get employee profiles for the company
let employeeCount = 0;
try {
const employeeResponse = await axios.get(`${baseUrl}/employeeprofile-api/v1/employeeprofiles?pageNumber=0`, { headers });
employeeCount = employeeResponse.data.rowCount || 0;
} catch (e) {
console.log('Employee API error:', e.message);
}
// Get departments (userGroups)
let departmentCount = 0;
try {
const deptResponse = await axios.get(`${baseUrl}/auth-api/v1/usergroups?pageNumber=0`, { headers });
departmentCount = deptResponse.data.rowCount || 0;
} catch (e) {
console.log('Department API error:', e.message);
}
// Get today's shifts
let todayShifts = 0;
try {
const shiftsResponse = await axios.get(`${baseUrl}/schedulemanagement-api/v1/shifts?shiftDate=${today}&pageNumber=0`, { headers });
todayShifts = shiftsResponse.data.rowCount || 0;
} catch (e) {
console.log('Today shifts API error:', e.message);
}
// Get all shifts
let totalShifts = 0;
try {
const allShiftsResponse = await axios.get(`${baseUrl}/schedulemanagement-api/v1/shifts?pageNumber=0`, { headers });
totalShifts = allShiftsResponse.data.rowCount || 0;
} catch (e) {
console.log('All shifts API error:', e.message);
}
// Get task assignments
let taskCount = 0;
try {
const tasksResponse = await axios.get(`${baseUrl}/taskmanagement-api/v1/taskassignments?pageNumber=0`, { headers });
taskCount = tasksResponse.data.rowCount || 0;
} catch (e) {
console.log('Tasks API error:', e.message);
}
// Get individual tasks for this user
let myTaskCount = 0;
try {
const myTasksResponse = await axios.get(`${baseUrl}/taskmanagement-api/v1/myindividualtasks?pageNumber=0`, { headers });
myTaskCount = myTasksResponse.data.rowCount || 0;
} catch (e) {
console.log('My tasks API error:', e.message);
}
// Get attendance records
let attendanceCount = 0;
try {
const attendanceResponse = await axios.get(`${baseUrl}/attendancemanagement-api/v1/attendancerecords?pageNumber=0`, { headers });
attendanceCount = attendanceResponse.data.rowCount || 0;
} catch (e) {
console.log('Attendance API error:', e.message);
}
// Get leave requests
let leaveCount = 0;
try {
const leaveResponse = await axios.get(`${baseUrl}/leavemanagement-api/v1/leaverequests?pageNumber=0`, { headers });
leaveCount = leaveResponse.data.rowCount || 0;
} catch (e) {
console.log('Leave API error:', e.message);
}
// Build company data summary
const companyData = {
employeeCount,
departmentCount,
todayShifts,
totalShifts,
taskCount,
myTaskCount,
attendanceCount,
leaveCount,
today,
companyCodename: companyCodename || currentUser?.company?.codename || 'unknown',
userCompanyId: currentUser?.company?.companyId || currentUser?.companyId
};
// Get user prompt from request
const userPrompt = req.body?.prompt || 'Provide a workforce overview';
// Build enriched prompt with company data
const enrichedPrompt = `ACTUAL COMPANY DATA (from database):
- Employee Count: ${companyData.employeeCount}
- Department Count: ${companyData.departmentCount}
- Shifts Today (${today}): ${companyData.todayShifts}
- Total Shifts: ${companyData.totalShifts}
- Task Assignments: ${companyData.taskCount}
- My Tasks: ${companyData.myTaskCount}
- Attendance Records: ${companyData.attendanceCount}
- Leave Requests: ${companyData.leaveCount}
- Company Codename: ${companyData.companyCodename}
USER QUESTION: ${userPrompt}
IMPORTANT: Use ONLY the numbers above. Do not make up or hallucinate any data. If a count is 0, report it as 0.
Provide your response as a JSON object with this exact structure:
{
"title": "Brief insight title",
"insightType": "companySpecific",
"summary": "2-3 sentence answer using the actual numbers above",
"data": {
"keyMetrics": [{"metric": "Name", "value": number}],
"findings": ["Finding 1"],
"recommendations": []
},
"audienceType": "company",
"suggestion": "Optional suggestion"
}`;
// Call the AI agent
const aiResponse = await axios.post(
`${baseUrl}/aiworkforceanalytics-api/v1/aiinsight`,
{
prompt: enrichedPrompt
},
{ headers }
);
// Return the insight
res.json({
status: 'OK',
insight: aiResponse.data.insight
});
} catch (error) {
console.error('Edge controller error:', error.message);
if (error.response) {
console.error('Response status:', error.response.status);
console.error('Response data:', error.response.data);
}
if (!res.headersSent) {
res.status(500).json({
status: 'ERR',
message: error.message || 'Internal server error',
error: error.response?.data || error.message
});
}
}
};
Edge Controllers Summary
| Function Name | Method | Path | Login Required |
|---|---|---|---|
generateInsightHandler |
GET |
generate-insight |
Yes |
generateAiInsightStreamHandler |
GET |
/ai-insight/stream |
No |
This document was generated from the service library configuration and should be kept in sync with design changes.