1. Overview
The Non-Operational Days System is a comprehensive solution designed to manage and track off days for both sites (physical locations) and users (employees) within an organization. The system consists of two primary microservices: api-sites for managing site-level off days and api-users for managing individual user off days. These services work in tandem to provide a complete off-day management solution with real-time synchronization capabilities.
Key Features
- Dual Management: Separate handling of site-wide closures and individual user time-off
- Real-time Synchronization: PubSub integration for immediate updates across systems
- Flexible Querying: Advanced filtering and reporting capabilities
- Scalable Architecture: Microservices design with MongoDB persistence
- Security: JWT-based authentication and role-based [access control](../Settings/Access control/AccessControlOverview.md)
2. API Endpoints Reference
2.1. Site Off Days API (api-sites)
| Method | Endpoint | Description | Authentication | Validation |
|---|---|---|---|---|
| GET | /siteOffdays/:siteID | Retrieves off days for a specific site | JWT Required | None |
| PUT | /siteOffdays/:siteID | Creates or updates off days for a site | JWT Required | Body validation |
2.2. User Off Days API (api-users)
| Method | Endpoint | Description | Authentication | Query Parameters |
|---|---|---|---|---|
| GET | /userOffDay/ | Retrieves all user off days for the organization | JWT Required | None |
| GET | /userOffDay/count | Gets statistical counts of user off days | JWT Required | None |
| GET | /userOffDay/upcoming | Retrieves upcoming scheduled off days | JWT Required | None |
| GET | /userOffDay/userOffDaysData | Advanced query for user off days with filtering | JWT Required | Multiple filters |
| GET | /userOffDay/userOffDaysPerMonth | Gets user off days for a specific month | JWT Required | yearMonth |
| POST | /userOffDay/ | Creates or updates user off days | JWT Required | Body validation |
3. Libraries and Dependencies
Core Framework Libraries
- Express.js: Web application framework for Node.js
- TypeScript: Type-safe JavaScript superset for enhanced development
- Mongoose: MongoDB object modeling for Node.js
Authentication & Security
- jsonwebtoken: JWT token generation and verification
- @nimbly-technologies/nimbly-backend-utils: Custom authentication middleware
- cors: Cross-Origin Resource Sharing middleware
Validation & Data Processing
- Joi: Schema validation library
- Validator: Custom validation wrapper from nimbly-backend-utils
Database & Caching
- MongoDB: Primary data persistence layer
- Redis: Caching layer for performance optimization
- @nimbly-technologies/entity-node: Database repository implementations
Communication & Integration
- Axios: HTTP client for external API calls
- PubSub: Event-driven messaging for system integration
Logging & Monitoring
- Winston: Comprehensive logging solution
- @nimbly-technologies/nimbly-common: Shared logging utilities
Development & Testing
- Nodemon: Development server with auto-reload
- Babel: JavaScript transpilation
- Jest: Testing framework
4. Data Models and Schemas
4.1. Site Off Day Model
interface SiteOffdayMongo {
siteID: string; // Unique identifier for the site
dates: string[]; // Array of date strings (YYYY-MM-DD format)
disabled: boolean; // Flag to disable off days
organizationID: string; // Organization identifier
updatedAt: number; // Unix timestamp of last update
updatedBy: string; // User ID who made the last update
}4.2. User Off Day Model
interface UserOffdayMongo {
userID: string; // Unique identifier for the user
dates: string[]; // Array of date strings (YYYY-MM-DD format)
disabled: boolean; // Flag to disable off days
organizationID: string; // Organization identifier
updatedAt: number; // Unix timestamp of last update
updatedBy: string; // User ID who made the last update
}4.3. Database Indexing Strategy
Both collections utilize compound indexes for optimal query performance:
Site Off Days Indexes
{organizationID: 1, siteID: 1}- Primary lookup index{organizationID: 1, siteID: 1, disabled: 1}- Status filtering{organizationID: 1, siteID: 1, dates: 1}- Date-based queries
User Off Days Indexes
{organizationID: 1, userID: 1}- Primary lookup index{organizationID: 1, userID: 1, disabled: 1}- Status filtering{organizationID: 1, userID: 1, dates: 1}- Date-based queries
5. Implementation Details
5.1. Request Processing Flow
sequenceDiagram participant Client participant Router participant Middleware participant Controller participant UseCase participant Repository participant MongoDB participant PubSub Client->>Router: HTTP Request Router->>Middleware: JWT Validation Middleware->>Middleware: Extract User Context Middleware->>Controller: Validated Request Controller->>UseCase: Business Logic UseCase->>Repository: Data Operation Repository->>MongoDB: Query/Update MongoDB-->>Repository: Result Repository-->>UseCase: Data UseCase->>PubSub: Sync Event PubSub-->>UseCase: Confirmation UseCase-->>Controller: Response Data Controller-->>Client: HTTP Response
5.2. Site Off Days Update Flow
The site off days update process follows a sophisticated pattern to ensure data consistency and proper synchronization:
-
Validation Phase
- Request validation using Joi schema
- JWT token verification
- User permission checks
-
Existence Check
- Query MongoDB for existing site off days
- Determine create vs update operation
-
Data Transformation
- Convert date object format to array format
- Calculate added and removed dates
- Prepare synchronization payload
-
Persistence Phase
- Create new document or update existing
- Set audit fields (updatedAt, updatedBy)
-
Synchronization Phase
- Publish changes to PubSub topic
- Include detailed change information
5.3. User Off Days Batch Update Flow
The user off days system supports batch updates with complex processing:
flowchart TD A[Start Batch Update] --> B[Parse User IDs Array] B --> C{For Each User} C --> D[Check Existing Record] D --> E{Record Exists?} E -->|No| F[Create New Record] E -->|Yes| G[Calculate Differences] F --> H[Add to Create Queue] G --> I[Add to Update Queue] H --> J{More Users?} I --> J J -->|Yes| C J -->|No| K[Execute All Operations] K --> L[Publish Sync Events] L --> M[Return Success]
5.4. Complex Query Implementation
The userOffDaysData endpoint implements a sophisticated aggregation pipeline:
-
Base Filtering
- Organization ID restriction
- Date range filtering if provided
-
Type-Based Processing
- Date Type: Groups users by date
- User Type: Groups dates by user
-
Department/Site Filtering
- Join with users collection
- Filter by department assignments
- Filter by site assignments
-
Search Implementation
- Text search on user display names
- Case-insensitive matching
-
Pagination & Sorting
- Dynamic sort field selection
- Configurable page size
- Total count calculation
6. Business Logic Flows
6.1. Site Off Days Management Flow
The site off days management system handles the complete lifecycle of site closures:
stateDiagram-v2 [*] --> RequestReceived RequestReceived --> ValidateRequest ValidateRequest --> CheckExistence CheckExistence --> CreateNew: Record Not Found CheckExistence --> UpdateExisting: Record Found CreateNew --> PrepareData UpdateExisting --> CalculateChanges PrepareData --> SaveToDatabase CalculateChanges --> SaveToDatabase SaveToDatabase --> PublishEvent PublishEvent --> [*] note right of CalculateChanges Identifies: - Added dates - Removed dates - Status changes end note note right of PublishEvent Sync with: - Statistics service - Scheduling service - Notification service end note
6.2. Detailed Function Flows
Site Off Days Functions
1. GET /siteOffdays/:siteID - Find Site Off Days
flowchart TD A[GET Request to /siteOffdays/:siteID] --> B[Router: siteOffdays.router.ts] B --> C[AuthMiddleware.expressHandler] C --> D{Valid JWT?} D -->|No| E[Return 401 Unauthorized] D -->|Yes| F[Extract User Context] F --> G[SiteOffdaysController.findOneBySiteID] G --> H[SiteOffdaysUsecase.findOneBySiteID] H --> I[Build Query Object] I --> J[SiteOffdayMongoRepository.findOneByQuery] J --> K[MongoDB Query Execution] K --> L{Record Found?} L -->|No| M[Return null] L -->|Yes| N[Return SiteOffdayMongo Object] N --> O[Controller Response Handler] O --> P[Return HTTP 200 with Data]
Function Implementation Details:
// Controller: siteOffdaysController.ts
public async findOneBySiteID({ context, payload }: FindOneBySiteIDParams) {
try {
// Extract siteID from route params
const siteID = payload.params.siteID;
// Call use case with context and siteID
const siteOffdays = await this.siteOffdaysUsecase.findOneBySiteID(
context,
siteID
);
// Return successful response
return response(siteOffdays, null);
} catch (error) {
log.error('findOneBySiteID', error);
return response(null, error.code || errors.INTERNAL);
}
}
// Use Case: siteOffdays.usecase.ts
public async findOneBySiteID(ctx: Context<UserAuth>, siteID: string): Promise<SiteOffdayMongo | null> {
// Build query with organization isolation
const defaultQuery = {
organizationID: ctx.user.organizationID,
siteID: siteID,
};
// Execute repository query
let siteOffDay = await this.siteOffdaysRepo.findOneByQuery(defaultQuery);
if (!siteOffDay) {
return null;
}
return siteOffDay;
}2. PUT /siteOffdays/:siteID - Upsert Site Off Days
flowchart TD A[PUT Request to /siteOffdays/:siteID] --> B[Router: siteOffdays.router.ts] B --> C[AuthMiddleware.expressHandler] C --> D[ValidatorHandler - Joi Validation] D --> E{Valid Request Body?} E -->|No| F[Return 400 Bad Request] E -->|Yes| G[SiteOffdaysController.upsert] G --> H[SiteOffdaysUsecase.upsert] H --> I[Check Existing Record] I --> J{Record Exists?} J -->|No| K[Create New Flow] J -->|Yes| L[Update Existing Flow] K --> M[Prepare New Document] M --> N[Calculate Added Dates] N --> O[Create Sync Request] O --> P[SiteOffdayMongoRepository.create] L --> Q[Calculate Date Differences] Q --> R[Identify Added/Removed Dates] R --> S[Create Sync Request] S --> T[SiteOffdayMongoRepository.update] P --> U[Publish to PubSub] T --> U U --> V[Return Success Response]
Detailed Upsert Function Implementation:
// Use Case: siteOffdays.usecase.ts
public async upsert(ctx: Context<UserAuth>, siteID: string, data: Partial<SiteOffdayMongo>): Promise<string> {
const { organizationID, userID } = ctx.user;
// Check if site off days already exist
const checkSiteOffdays = await this.siteOffdaysRepo.findOneByQuery({
organizationID,
siteID,
});
// Initialize sync request for PubSub
const scheduleStatisticPubsubRequest: SyncOffDaysRequest = {
organizations: {
addedDates: [],
removedDates: [],
},
sites: [],
};
// CREATE FLOW
if (!checkSiteOffdays) {
// Prepare new document
const newSiteOffdays = new SiteOffdayMongo({
...data,
organizationID,
siteID,
updatedAt: new Date().getTime(),
updatedBy: userID,
});
// Prepare sync request
let siteRequest: SiteOffDaysDetail = {
siteID,
removedDates: [],
addedDates: [],
};
if (!newSiteOffdays.disabled) {
siteRequest.addedDates = newSiteOffdays.dates;
siteRequest.isDisabled = false;
} else {
siteRequest.isDisabled = true;
}
scheduleStatisticPubsubRequest.sites.push(siteRequest);
// Publish event before saving
await this.pubsubRepo.publish(ctx, 'scheduleStatistic:syncOffDays', scheduleStatisticPubsubRequest);
// Save to database
await this.siteOffdaysRepo.create(newSiteOffdays);
} else {
// UPDATE FLOW
const updatedSiteOffdays = {
...data,
updatedAt: new Date().getTime(),
updatedBy: userID,
};
const siteRequest: SiteOffDaysDetail = {
siteID,
addedDates: [],
removedDates: [],
};
// Check if disabled status changed
if (checkSiteOffdays.disabled !== updatedSiteOffdays.disabled) {
siteRequest.isDisabled = updatedSiteOffdays.disabled;
}
// Calculate date differences
if (updatedSiteOffdays.dates && checkSiteOffdays.dates) {
const addedDates = (updatedSiteOffdays.dates || []).filter(
(date) => !checkSiteOffdays.dates.includes(date)
);
const removedDates = checkSiteOffdays.dates.filter(
(date) => !(updatedSiteOffdays.dates || []).includes(date)
);
siteRequest.addedDates = addedDates;
siteRequest.removedDates = removedDates;
}
scheduleStatisticPubsubRequest.sites.push(siteRequest);
// Update database
await this.siteOffdaysRepo.update(
{ organizationID, siteID },
{ $set: updatedSiteOffdays }
);
// Publish sync event
await this.pubsubRepo.publish(ctx, 'scheduleStatistic:syncOffDays', scheduleStatisticPubsubRequest);
}
return siteID;
}User Off Days Functions
1. GET /userOffDay/count - Get Statistical Counts
flowchart TD A[GET Request to /userOffDay/count] --> B[UserOffDayController.getUserOffDayCount] B --> C[UserOffDayUsecase.getUserOffDayCount] C --> D[Calculate Today's Date] D --> E[Query Today Count] E --> F[Calculate Week Range] F --> G[Query Week Count] G --> H[Calculate Month Range] H --> I[Query Month Count] I --> J[Calculate Year Range] J --> K[Query Year Count] K --> L[Aggregate Results] L --> M[Return Statistics Object]
Statistical Count Implementation:
// Use Case: userOffDay.usecase.ts
public async getUserOffDayCount(ctx: Context<UserAuth>): Promise<any> {
// Today's Date
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth() + 1;
const day = today.getDate();
const formattedDate = `${year}-${month.toString().padStart(2, "0")}-${day
.toString()
.padStart(2, "0")}`;
// Count users off today
const todayCount = await this.modelUserOffDays.countDocuments({
dates: formattedDate,
organizationID: ctx.user.organizationID,
});
// Calculate current week range (Monday to Sunday)
const { monday, sunday } = getMondayAndNextSunday();
const weekCount = await this.modelUserOffDays
.find({
dates: {
$elemMatch: {
$gte: monday.toISOString().split("T")[0],
$lte: sunday.toISOString().split("T")[0],
},
},
organizationID: ctx.user.organizationID,
})
.count();
// Calculate current month range
const { startOfMonth, endOfMonth } = getCurrentMonthStartAndEnd();
const monthCount = await this.modelUserOffDays
.find({
dates: {
$elemMatch: {
$gte: startOfMonth.toISOString().split("T")[0],
$lte: endOfMonth.toISOString().split("T")[0],
},
},
organizationID: ctx.user.organizationID,
})
.count();
// Calculate current year range
const { start, end } = getCurrentYearStartAndEnd();
const yearCount = await this.modelUserOffDays
.find({
dates: {
$elemMatch: {
$gte: start.toISOString().split("T")[0],
$lte: end.toISOString().split("T")[0],
},
},
organizationID: ctx.user.organizationID,
})
.count();
return {
today: todayCount,
week: weekCount,
month: monthCount,
year: yearCount,
};
}2. POST /userOffDay - Batch Upsert User Off Days
flowchart TD A[POST Request to /userOffDay] --> B[UserOffDayController.upsertUserOffDay] B --> C[UserOffDayUsecase.upsertUserOffDay] C --> D[Parse userIDs Array] D --> E[Load Existing User Off Days] E --> F{For Each User} F --> G[Extract User ID and Dates] G --> H{User Exists?} H -->|No| I[Create New User Off Day] H -->|Yes| J[Update Existing Off Day] I --> K[Build Create Operation] J --> L[Calculate Date Differences] L --> M[Build Update Operation] K --> N[Add to Batch Operations] M --> N N --> O{More Users?} O -->|Yes| F O -->|No| P[Execute All Operations] P --> Q[Publish Sync Events] Q --> R[Return Success]
Batch Upsert Implementation:
// Use Case: userOffDay.usecase.ts
public async upsertUserOffDay(
ctx: Context<UserAuth>,
data: { userIDs: [{ string: string[] }] }
): Promise<string> {
const { organizationID } = ctx.user;
const newUserOffDays = data.userIDs || [];
// Load all existing user off days for organization
const userOffDay: UserOffdayMongo[] =
await this.userOffDayRepo.findByOrganizationID(organizationID, {});
const scheduleStatisticPubsubRequest: SyncUserOffDaysRequest = {
users: [],
};
const userOffDaysPromise: Promise<any>[] = [];
// Process each user
for (const checkUserOffDay of newUserOffDays) {
const keys = Object.keys(checkUserOffDay);
if (!keys) continue;
const userID = keys[0];
const newDates = checkUserOffDay[keys[0]];
// CREATE FLOW
if (!userOffDay.some((obj) => obj.userID === userID)) {
const newUserOffDay: UserOffdayMongo = {
disabled: false,
organizationID: ctx.user.organizationID,
updatedAt: new Date().getTime(),
updatedBy: ctx.user.userID,
userID: userID,
dates: newDates,
};
let userRequest: UserOffDaysDetail = {
userID: userID,
removedDates: [],
addedDates: [],
};
if (!newUserOffDay.disabled) {
userRequest.addedDates = newUserOffDay.dates;
userRequest.isDisabled = false;
} else {
userRequest.isDisabled = true;
}
scheduleStatisticPubsubRequest.users.push(userRequest);
userOffDaysPromise.push(this.userOffDayRepo.create(newUserOffDay));
} else {
// UPDATE FLOW
const updatedUserOffdays = {
disabled: false,
updatedAt: new Date().getTime(),
organizationID: ctx.user.organizationID,
userID: userID,
updatedBy: ctx.user.userID,
dates: newDates,
};
const selectedUserOffDay = userOffDay.find(
(obj) => obj.userID === userID
);
const userRequest: UserOffDaysDetail = {
userID: userID,
addedDates: [],
removedDates: [],
};
const oldDates = selectedUserOffDay.dates;
// Calculate date differences
if (newDates && oldDates) {
const addedDates = (newDates || []).filter(
(date) => !oldDates.includes(date)
);
const removedDates = oldDates.filter(
(date) => !(newDates || []).includes(date)
);
userRequest.addedDates = addedDates;
userRequest.removedDates = removedDates;
}
scheduleStatisticPubsubRequest.users.push(userRequest);
userOffDaysPromise.push(
this.userOffDayRepo.update(
{ organizationID, userID: userID },
{ $set: updatedUserOffdays }
)
);
}
}
// Execute all database operations
if (userOffDaysPromise.length) {
try {
await Promise.all(userOffDaysPromise);
} catch (error) {
log.error(
`[DEBUG] Error adding to userOffDays, errorMsg:${error}`
);
}
}
// Publish sync events
if (scheduleStatisticPubsubRequest.users.length) {
try {
await this.pubsubRepo.publish(
ctx,
"scheduleStatistic:syncUserOffDays",
scheduleStatisticPubsubRequest
);
} catch (error) {
log.error(
`[DEBUG] Error publishing to scheduleStatistic:syncUserOffDays`
);
}
}
return "SUCCESS";
}3. GET /userOffDay/upcoming - Get Upcoming Schedules
flowchart TD A[GET Request to /userOffDay/upcoming] --> B[UserOffDayController.upcomingSchedules] B --> C[UserOffDayUsecase.upcomingSchedules] C --> D[Calculate Today's Date] D --> E[Query Today's Off Users] E --> F[Get User Display Names] F --> G[Calculate This Week Range] G --> H[Query This Week's Off Users] H --> I[Get User Display Names] I --> J[Calculate Next Week Range] J --> K[Query Next Week's Off Users] K --> L[Get User Display Names] L --> M[Aggregate Results by Period] M --> N[Return Upcoming Schedules]
Upcoming Schedules Implementation:
// Use Case: userOffDay.usecase.ts
public async upcomingSchedules(ctx: Context<UserAuth>): Promise<any> {
const todayDate = new Date();
const year = todayDate.getFullYear();
const month = todayDate.getMonth() + 1;
const day = todayDate.getDate();
const formattedDateFilter = `${year}-${month
.toString()
.padStart(2, "0")}-${day.toString().padStart(2, "0")}`;
// TODAY'S OFF USERS
const userIDs: string[] = [];
const users: string[] = [];
// Find users off today
const upcomingOffUserID = this.modelUserOffDays.find(
{
organizationID: ctx.user.organizationID,
dates: formattedDateFilter
},
{ projection: { userID: 1, _id: 0 } }
);
await upcomingOffUserID.forEach((user: any) => {
userIDs.push(user.userID);
});
// Get user display names
const upcomingOffUser = this.modelUsers.find(
{ userID: { $in: userIDs } },
{ projection: { displayName: 1, _id: 0 } }
);
await upcomingOffUser.forEach((user: any) => {
users.push(user.displayName);
});
// THIS WEEK'S OFF USERS
const weekUserIDs: string[] = [];
const weekUsers: string[] = [];
const { sunday } = getMondayAndNextSunday();
const upcomingWeekOffUserID = this.modelUserOffDays.find(
{
organizationID: ctx.user.organizationID,
dates: {
$elemMatch: {
$gte: formattedDateFilter,
$lte: sunday.toISOString().split("T")[0],
},
},
},
{ projection: { userID: 1, _id: 0 } }
);
await upcomingWeekOffUserID.forEach((user: any) => {
weekUserIDs.push(user.userID);
});
const upcomingWeekOffUser = this.modelUsers.find(
{ userID: { $in: weekUserIDs } },
{ projection: { displayName: 1, _id: 0 } }
);
await upcomingWeekOffUser.forEach((user: any) => {
weekUsers.push(user.displayName);
});
// NEXT WEEK'S OFF USERS
const nextWeekUserIDs: string[] = [];
const nextWeekUsers: string[] = [];
const { nextWeekStart, nextWeekEnd } = getNextWeekStartAndEnd();
const upcomingNextWeekOffUserID = this.modelUserOffDays.find(
{
organizationID: ctx.user.organizationID,
dates: {
$elemMatch: {
$gte: nextWeekStart.toISOString().split("T")[0],
$lte: nextWeekEnd.toISOString().split("T")[0],
},
},
},
{ projection: { userID: 1, _id: 0 } }
);
await upcomingNextWeekOffUserID.forEach((user: any) => {
nextWeekUserIDs.push(user.userID);
});
const upcomingNextWeekOffUser = this.modelUsers.find(
{ userID: { $in: nextWeekUserIDs } },
{ projection: { displayName: 1, _id: 0 } }
);
await upcomingNextWeekOffUser.forEach((user: any) => {
nextWeekUsers.push(user.displayName);
});
return {
today: users,
thisWeek: weekUsers,
nextWeek: nextWeekUsers
};
}6.3. User Off Days Statistical Analysis
The system provides comprehensive statistical analysis across multiple time periods:
-
Daily Statistics
- Count of users off today
- Real-time updates
-
Weekly Statistics
- Current week analysis (Monday-Sunday)
- Trending patterns
-
Monthly Statistics
- Full month coverage
- Department-wise breakdown
-
Annual Statistics
- Year-to-date analysis
- Historical comparisons
4. GET /userOffDay/userOffDaysData - Advanced Query with Filters
flowchart TD A[GET Request with Query Params] --> B[UserOffDayController.userOffDaysData] B --> C[Extract Query Parameters] C --> D[UserOffDayUsecase.userOffDaysData] D --> E{Filter Type?} E -->|Date| F[Date-Based Aggregation] E -->|User| G[User-Based Aggregation] F --> H[Group Users by Date] G --> I[Group Dates by User] H --> J[Apply Department Filter] I --> J J --> K[Apply Site Filter] K --> L[Apply Date Range Filter] L --> M[Apply Search Filter] M --> N[Apply Pagination] N --> O[Sort Results] O --> P[Return Filtered Data]
Advanced Query Implementation:
// Use Case: userOffDay.usecase.ts (partial)
public async userOffDaysData(
ctx: Context<UserAuth>,
options: UserOffDayOptions
): Promise<any> {
let pipeline = [];
const site = options.site;
const department = options.department;
// Filter By Date - Groups users by each date
if (options.type === "date") {
pipeline = [
{
$match: {
organizationID: ctx.user.organizationID,
},
},
{
$unwind: "$dates",
},
{
$group: {
_id: { date: "$dates" },
userID: { $addToSet: "$userID" },
},
},
];
// Date range filter
if (options.startDate && options.endDate) {
pipeline.splice(2, 0, {
$match: {
dates: {
$gte: options.startDate,
$lte: options.endDate,
},
},
});
}
// Specific dates filter
if (options.dates && options.dates.length > 0) {
pipeline.splice(2, 0, {
$match: {
dates: { $in: options.dates },
},
});
}
// Enrich with user details
pipeline.push({
$lookup: {
from: "users",
localField: "userID",
foreignField: "userID",
as: "userDetails",
},
});
// Apply department/site filters
if (department && department.length > 0) {
pipeline.push({
$addFields: {
userDetails: {
$filter: {
input: "$userDetails",
as: "user",
cond: {
$in: ["$$user.department", department],
},
},
},
},
});
}
if (site && site.length > 0) {
pipeline.push({
$addFields: {
userDetails: {
$filter: {
input: "$userDetails",
as: "user",
cond: {
$in: ["$$user.site", site],
},
},
},
},
});
}
// Apply search filter
if (options.search) {
pipeline.push({
$addFields: {
userDetails: {
$filter: {
input: "$userDetails",
as: "user",
cond: {
$regexMatch: {
input: "$$user.displayName",
regex: options.search,
options: "i",
},
},
},
},
},
});
}
// Final projection
pipeline.push({
$project: {
date: "$_id.date",
users: {
$map: {
input: "$userDetails",
as: "user",
in: {
userID: "$$user.userID",
displayName: "$$user.displayName",
},
},
},
count: { $size: "$userDetails" },
},
});
// Sort
if (options.sortBy && options.order) {
const sortOrder = options.order === "asc" ? 1 : -1;
pipeline.push({
$sort: { [options.sortBy]: sortOrder },
});
}
}
// Execute aggregation
const results = await this.modelUserOffDays.aggregate(pipeline);
// Apply pagination
const page = options.page || 1;
const limit = options.limit || 10;
const startIndex = (page - 1) * limit;
const endIndex = page * limit;
const paginatedResults = results.slice(startIndex, endIndex);
return {
data: paginatedResults,
total: results.length,
page: page,
limit: limit,
totalPages: Math.ceil(results.length / limit),
};
}5. GET /userOffDay/userOffDaysPerMonth - Monthly User Off Days
flowchart TD A[GET Request with yearMonth] --> B[UserOffDayController.userOffDaysPerMonth] B --> C[Extract yearMonth Parameter] C --> D[UserOffDayUsecase.userOffDaysPerMonth] D --> E[Parse Year and Month] E --> F[Calculate Month Date Range] F --> G[Query Users with Off Days in Month] G --> H[Group by User] H --> I[Enrich with User Details] I --> J[Calculate Days Count per User] J --> K[Sort by Count] K --> L[Return Monthly Summary]
Monthly Analysis Implementation:
// Use Case: userOffDay.usecase.ts
public async userOffDaysPerMonth(
ctx: Context<UserAuth>,
yearMonth: string
): Promise<any> {
// Parse year and month
const [year, month] = yearMonth.split("-").map(Number);
// Calculate month boundaries
const startDate = new Date(year, month - 1, 1);
const endDate = new Date(year, month, 0);
const startDateStr = startDate.toISOString().split("T")[0];
const endDateStr = endDate.toISOString().split("T")[0];
// Aggregation pipeline
const pipeline = [
{
$match: {
organizationID: ctx.user.organizationID,
dates: {
$exists: true,
$ne: [],
},
},
},
{
$unwind: "$dates",
},
{
$match: {
dates: {
$gte: startDateStr,
$lte: endDateStr,
},
},
},
{
$group: {
_id: "$userID",
dates: { $push: "$dates" },
count: { $sum: 1 },
},
},
{
$lookup: {
from: "users",
localField: "_id",
foreignField: "userID",
as: "userInfo",
},
},
{
$unwind: {
path: "$userInfo",
preserveNullAndEmptyArrays: true,
},
},
{
$project: {
userID: "$_id",
displayName: "$userInfo.displayName",
department: "$userInfo.department",
site: "$userInfo.site",
dates: 1,
count: 1,
},
},
{
$sort: { count: -1 },
},
];
const results = await this.modelUserOffDays.aggregate(pipeline);
// Calculate summary statistics
const totalDaysInMonth = endDate.getDate();
const workingDays = calculateWorkingDays(startDate, endDate);
return {
yearMonth: yearMonth,
totalDaysInMonth: totalDaysInMonth,
workingDays: workingDays,
users: results,
totalUsers: results.length,
averageOffDays: results.length > 0
? results.reduce((sum, user) => sum + user.count, 0) / results.length
: 0,
};
}6. GET /userOffDay - Get All User Off Days
flowchart TD A[GET Request to /userOffDay] --> B[UserOffDayController.getUserOffDay] B --> C[UserOffDayUsecase.getUserOffDay] C --> D[Query All Organization Off Days] D --> E[Transform to User-Dates Map] E --> F[Format Response Object] F --> G[Return Formatted Data]
Get All User Off Days Implementation:
// Use Case: userOffDay.usecase.ts
public async getUserOffDay(ctx: Context<UserAuth>): Promise<any> {
// Fetch all user off days for the organization
const userOffDay: UserOffdayMongo[] =
await this.userOffDayRepo.findByOrganizationID(
ctx.user.organizationID,
{}
);
// Transform to required format
let result = {
userIDs: [],
};
if (userOffDay && userOffDay.length > 0) {
for (const offDay of userOffDay) {
const uID = offDay.userID;
const dates = offDay.dates;
// Create object with userID as key and dates as value
result.userIDs.push({ [uID]: dates });
}
}
return result;
}6.4. Upcoming Schedules Algorithm
The upcoming schedules feature implements a multi-tier analysis:
// Conceptual implementation
async function analyzeUpcomingSchedules() {
const timeFrames = {
today: getCurrentDate(),
thisWeek: getCurrentWeekRange(),
nextWeek: getNextWeekRange()
};
for (const [period, range] of Object.entries(timeFrames)) {
const offDayUsers = await findUsersWithOffDays(range);
const userDetails = await enrichUserInformation(offDayUsers);
results[period] = userDetails;
}
return results;
}6.5. Helper Functions for Date Calculations
The system uses several utility functions for date calculations:
// utils.ts
export function getMondayAndNextSunday() {
const today = new Date();
const day = today.getDay();
const diff = today.getDate() - day + (day === 0 ? -6 : 1);
const monday = new Date(today.setDate(diff));
monday.setHours(0, 0, 0, 0);
const sunday = new Date(monday);
sunday.setDate(monday.getDate() + 6);
sunday.setHours(23, 59, 59, 999);
return { monday, sunday };
}
export function getCurrentMonthStartAndEnd() {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth();
const startOfMonth = new Date(year, month, 1);
const endOfMonth = new Date(year, month + 1, 0);
return { startOfMonth, endOfMonth };
}
export function getCurrentYearStartAndEnd() {
const today = new Date();
const year = today.getFullYear();
const start = new Date(year, 0, 1);
const end = new Date(year, 11, 31);
return { start, end };
}
export function getNextWeekStartAndEnd() {
const { sunday } = getMondayAndNextSunday();
const nextWeekStart = new Date(sunday);
nextWeekStart.setDate(sunday.getDate() + 1);
nextWeekStart.setHours(0, 0, 0, 0);
const nextWeekEnd = new Date(nextWeekStart);
nextWeekEnd.setDate(nextWeekStart.getDate() + 6);
nextWeekEnd.setHours(23, 59, 59, 999);
return { nextWeekStart, nextWeekEnd };
}7. Error Handling Strategies
7.1. Error Classification
The system implements a comprehensive error handling hierarchy:
-
Validation Errors (400 Bad Request)
- Invalid input data
- Missing required fields
- Format violations
-
Authentication Errors (401 Unauthorized)
- Invalid JWT token
- Expired token
- Missing authentication
-
Authorization Errors (403 Forbidden)
- Insufficient permissions
- Cross-organization access attempts
-
Not Found Errors (404 Not Found)
- Non-existent resources
- Invalid IDs
-
Internal Errors (500 Internal Server Error)
- Database connection failures
- Unexpected exceptions
7.2. Error Response Format
Standardized error responses ensure consistency:
{
error: {
code: "ERROR_CODE",
message: "Human-readable error message",
details: {
// Additional context
}
},
data: null
}7.3. Error Recovery Mechanisms
-
Automatic Retries
- Database connection retries
- PubSub publishing retries
- External API call retries
-
Circuit Breaker Pattern
- Prevents cascade failures
- Automatic service recovery
- Fallback mechanisms
-
Graceful Degradation
- Cache fallback on database failure
- Partial response on service unavailability
8. Conclusion
The Non-Operational Days System represents a robust, scalable solution for managing organizational off days. Through its microservices architecture, comprehensive API design, and integration capabilities, it provides a flexible foundation for workforce management. The system’s emphasis on real-time synchronization, performance optimization, and security ensures it can meet the demands of modern enterprise environments while maintaining data integrity and system reliability.
This documentation serves as a comprehensive guide for developers, architects, and operations teams working with the Non-Operational Days System, providing the necessary technical details for effective system understanding, maintenance, and enhancement.