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:

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:

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.