Schedule Lite API Documentation
1. Introduction
The Schedule Lite API provides endpoints for managing and retrieving schedule-related information, focusing on efficiency and ease of use. It allows users to access active [schedules](../../Schedule/Schedule Listing/ScheduleListingOverview.md), check-in status, progress, and questionnaire data.
2. API Endpoints
| Endpoint | Description |
|---|---|
/check-in | Checks in a schedule. |
/check-in-report | Creates a check-in report. |
/indicator | Gets the user’s active schedule indicator. |
/aggregates | Gets the user’s active [schedules](../../Schedule/Schedule Listing/ScheduleListingOverview.md) with aggregation. |
/active-period | Gets the user’s active period [schedules](../../Schedule/Schedule Listing/ScheduleListingOverview.md). |
/v2 | Gets the user’s active [schedules](../../Schedule/Schedule Listing/ScheduleListingOverview.md) V2. |
/progress | Gets the schedule progress. |
/count | Gets the schedule count. |
/questionnaire | Gets the questionnaire by context. |
/check-questionnaire | Checks an updated questionnaire. |
/questionnaire-id | Gets the questionnaire by IDs. |
3. Libraries and Dependencies Used for Single Audit Flow
-
@nimbly-technologies/nimbly-common: A common library for Nimbly Technologies projects.
-
@nimbly-technologies/entity-node: An entity library for Nimbly Technologies projects.
-
dayjs: A JavaScript library for date manipulation and formatting.
-
lodash: A JavaScript library providing utility functions. -
@nimbly-technologies/nimbly-backend-utils: A backend utilities library for Nimbly Technologies projects.
-
moment-timezone: A JavaScript library for timezone-aware date and time manipulation.
-
date-fns: A JavaScript library that provides comprehensive date formatting and manipulation capabilities.
-
express: A fast, unopinionated, minimalist web framework for Node.js.
-
@hapi/joi: An object schema description language and validator for JavaScript.
-
lodash: A JavaScript library providing utility functions.
-
dayjs: A JavaScript library for date manipulation and formatting.
-
mongoose: An Object Data Modeling (ODM) library for MongoDB and Node.js.
-
typescript: A superset of JavaScript that adds static typing.
-
date-fns: A JavaScript library that provides comprehensive date formatting and manipulation capabilities.
-
moment: A JavaScript library for parsing, validating, manipulating, and formatting dates.
-
eslint: A JavaScript linter.
4. Single Audit Flow Trace
This flow describes the process of a user checking in.
sequenceDiagram participant User participant Client participant API Gateway participant Schedule Lite Service participant Database User->>Client: Initiates check-in Client->>API Gateway: POST /check-in API Gateway->>Schedule Lite Service: Routes request Schedule Lite Service->>Database: Retrieves schedule and check-in data Database-->>Schedule Lite Service: Returns data Schedule Lite Service->>Database: Updates check-in status Database-->>Schedule Lite Service: Acknowledges update Schedule Lite Service-->>API Gateway: Returns success/failure API Gateway-->>Client: Returns response to user Client->>User: Notifies check-in status
4.1. /questionnaire-id
-
Purpose: Gets the questionnaire by IDs.
-
Controller Function:
ScheduleLiteController.getQuestionnaireByIDs()(src/controllers/scheduleLite.controller.ts:272) -
Usecase Function:
ScheduleLiteUsecase.downloadQuestionnaireByIDs()(src/domains/scheduleLite/usecase/scheduleLite.usecase.ts) -
Data Flow:
- The controller receives the request with the query parameters.
- The controller calls the
getQuestionnaireByIDsfunction in the use case. - The use case retrieves the questionnaire IDs from the query parameters.
- The use case retrieves the questionnaires from the database.
- The use case returns the questionnaires.
-
Code Snippets:
// ScheduleLiteController.getQuestionnaireByIDs()
public async getQuestionnaireByIDs({ context, payload }: GetQuestionnaireByIDsParams) {
const makeArray = (q: string | string[]) => {
if (!q) {
return [];
}
if (Array.isArray(q)) {
return q;
}
if (typeof q === 'object') {
return Object.values(q) as string[];
}
return [q];
};
try {
const questionnaireIDs = makeArray(payload.query.questionnaireIDs);
const questionnaires = await this.usecase.downloadQuestionnaireByIDs(context, questionnaireIDs);
return this.handleSuccess(questionnaires);
} catch (error) {
return this.handleError(error);
}
}
// ScheduleLiteUsecase.downloadQuestionnaireByIDs()
public async downloadQuestionnaireByIDs(
ctx: Context<UserAuth>,
questionnaireIDs: string[],
): Promise<QuestionnaireCompact[]> {
const questionnaire = await this.questionnaireRepo.find(
{ organizationID: ctx.user.organizationID, questionnaireID: { $in: questionnaireIDs } },
[
'questions',
'title',
'questionnaireIndexID',
'questionnaireID',
'dateCreated',
'dateUpdated',
'deductionToggle',
'maxScore',
],
);
const filteredQuestionnaires = questionnaire.filter((q) => !q.disabled);
return filteredQuestionnaires;
}- Mermaid Diagram:
graph LR A[Controller: getQuestionnaireByIDs] --> B(Usecase: downloadQuestionnaireByIDs); B --> C{Retrieve Questionnaire IDs}; C --> D{Get Questionnaires}; D --> E[Return Questionnaires];
4.2. /check-questionnaire
- Purpose: Checks an updated questionnaire.
- Controller Function:
ScheduleLiteController.checkUpdatedQuestionnaire()(src/controllers/scheduleLite.controller.ts) - Usecase Function:
ScheduleLiteUsecase.checkUpdatedQuestionnaire()(src/domains/scheduleLite/usecase/scheduleLite.usecase.ts) - Data Flow:
- The controller receives the request with the query parameters and the questionnaire map.
- The controller calls the
checkUpdatedQuestionnairefunction in the use case. - The use case retrieves the schedule details.
- The use case retrieves the questionnaire indexes.
- The use case retrieves the questionnaires.
- The use case returns the updated questionnaires and the questionnaire map.
- Code Snippets:
// ScheduleLiteController.checkUpdatedQuestionnaire()
public async checkUpdatedQuestionnaire({ context, payload }: CheckUpdatedQuestionnaireParams) {
try {
const { query, questionnaireMap, shouldReturnIDOnly } = payload.data;
const newQuery = new ScheduleLiteQueryOptions({ ...query, users: context.user.userID });
const updatedQuestionnaires = await this.usecase.checkUpdatedQuestionnaire(
context,
newQuery,
questionnaireMap,
shouldReturnIDOnly,
);
return this.handleSuccess(updatedQuestionnaires);
} catch (error) {
return this.handleError(error);
}
}
// ScheduleLiteUsecase.checkUpdatedQuestionnaire()
public async checkUpdatedQuestionnaire(
ctx: Context<UserAuth>,
query: ScheduleLiteQueryOptions,
questionnaireMap: { [questionnaireIndexID: string]: string },
shouldReturnIDOnly = false,
// eslint-disable-next-line max-params
): Promise<CheckUpdatedQuestionnaireResponse> {
const { questionnaireIndexIDs } = await this.getScheduleWithDetails(ctx, query);
const questionnaireIndexes = await this.questionnaireIndexRepo.findByIDs(questionnaireIndexIDs, [
'questionnaireIndexID',
'title',
'latest',
]);
const filteredQuestionnaireIndexes = questionnaireIndexes.filter((q) => !q.disabled);
const newQuestionnaireOptions: QuestionnaireOptions[] = [];
const newQuestionnaireMap = {};
filteredQuestionnaireIndexes.forEach((qIdx) => {
if (questionnaireMap[qIdx.questionnaireIndexID] !== qIdx.latest) {
newQuestionnaireMap[qIdx.questionnaireIndexID] = qIdx.latest;
// Ensure the item is added at least once
if (!newQuestionnaireOptions.some((option) => option.questionnaireID === qIdx.latest)) {
newQuestionnaireOptions.push({ title: qIdx.title, questionnaireID: qIdx.latest });
}
}
});
if (shouldReturnIDOnly) {
const uniqueQuestionnaireMap = {};
const filteredQuestionnaireOptions = newQuestionnaireOptions.filter((newQuestionnaire) => {
if (!uniqueQuestionnaireMap[newQuestionnaire.questionnaireID]) {
uniqueQuestionnaireMap[newQuestionnaire.questionnaireID] = true;
return true;
}
return false;
});
return {
questionnaireMap: newQuestionnaireMap,
newQuestionnaireOptions: filteredQuestionnaireOptions,
};
}
const newQuestionnaireID = newQuestionnaireOptions.map((q) => q.questionnaireID);
const questionnaire = await this.questionnaireRepo.find(
{ organizationID: ctx.user.organizationID, questionnaireID: { $in: newQuestionnaireID } },
[
'questions',
'title',
'questionnaireIndexID',
'questionnaireID',
'dateCreated',
'dateUpdated',
'deductionToggle',
'maxScore',
],
);
const filteredQuestionnaires = questionnaire.filter((q) => !q.disabled);
return { questionnaireMap: newQuestionnaireMap, newQuestionnaires: filteredQuestionnaires };
}- Mermaid Diagram:
graph LR A[Controller: checkUpdatedQuestionnaire] --> B(Usecase: checkUpdatedQuestionnaire); B --> C{Get Schedule Details}; C --> D{Get Questionnaire Indexes}; D --> E{Get Questionnaires}; E --> F[Return Updated Questionnaires];
4.3. /questionnaire
- Purpose: Gets the questionnaire by context.
- Controller Function:
ScheduleLiteController.getQuestionnaireByContext()(src/controllers/scheduleLite.controller.ts:234) - Usecase Function:
ScheduleLiteUsecase.getQuestionnaireByContext()(src/domains/scheduleLite/usecase/scheduleLite.usecase.ts) - Data Flow:
- The controller receives the request with the query parameters.
- The controller calls the
getQuestionnaireByContextfunction in the use case. - The use case retrieves the organization information.
- The use case retrieves the schedule details.
- The use case retrieves the questionnaire indexes.
- The use case retrieves the questionnaires.
- The use case returns the questionnaires.
- Code Snippets:
// ScheduleLiteController.getQuestionnaireByContext()
public async getQuestionnaireByContext({ context, payload }: GetQuestionnaireByContextParams) {
try {
const query = new ScheduleLiteQueryOptions({ ...payload.query, users: context.user.userID });
const questionnaires = await this.usecase.getQuestionnaireByContext(context, query);
return this.handleSuccess(questionnaires);
} catch (error) {
return this.handleError(error);
}
}
// ScheduleLiteUsecase.getQuestionnaireByContext()
public async getQuestionnaireByContext(
ctx: Context<UserAuth>,
query: ScheduleLiteQueryOptions,
): Promise<QuestionnaireCompact[]> {
const { questionnaireIndexIDs } = await this.getScheduleWithDetails(ctx, query);
const questionnaireIndexes = await this.questionnaireIndexRepo.findByIDs(questionnaireIndexIDs, [
'questionnaireIndexID',
'latest',
]);
// fetch questionnaire index details
const filteredQuestionnaireIndexes = questionnaireIndexes.filter((q) => !q.disabled);
const latestQuestionnaireList = filteredQuestionnaireIndexes.map((q) => q.latest);
// fetch question details from latest questionnaire version
const questionnaire = await this.questionnaireRepo.find(
{ organizationID: ctx.user.organizationID, questionnaireID: { $in: latestQuestionnaireList } },
['questions', 'title', 'questionnaireIndexID', 'questionnaireID', 'dateCreated', 'dateUpdated'],
);
const filteredQuestionnaires = questionnaire.filter((q) => !q.disabled);
return filteredQuestionnaires as unknown as QuestionnaireCompact[];
}- Mermaid Diagram:
graph LR A[Controller: getQuestionnaireByContext] --> B(Usecase: getQuestionnaireByContext); B --> C{Get Schedule Details}; C --> D{Get Questionnaire Indexes}; D --> E{Get Questionnaires}; E --> F[Return Questionnaires];
4.4. /count
- Purpose: Gets the schedule count.
- Controller Function:
ScheduleLiteController.getScheduleCount()(src/controllers/scheduleLite.controller.ts) - Usecase Function:
ScheduleLiteUsecase.getUserActiveSchedulesCount()(src/domains/scheduleLite/usecase/scheduleLite.usecase.ts) - Data Flow:
- The controller receives the request with the query parameters.
- The controller calls the
getScheduleCountfunction in the use case. - The use case retrieves the organization information.
- The use case retrieves the active schedules.
- The use case retrieves the reports.
- The use case injects schedule details by date.
- The use case calculates the schedule count.
- The use case returns the schedule count.
- Code Snippets:
// ScheduleLiteController.getScheduleCount()
public async getScheduleCount({ context, payload }: GetUserActiveSchedulesParams) {
try {
const query = new ScheduleLiteQueryOptions({ ...payload.query, users: context.user.userID });
const schedules = await this.usecase.getUserActiveSchedulesCount(context, query);
return this.handleSuccess(schedules);
} catch (error) {
return this.handleError(error);
}
}
// ScheduleLiteUsecase.getUserActiveSchedulesCount()
public async getUserActiveSchedulesCount(
ctx: Context<UserAuth>,
query: ScheduleLiteQueryOptions,
): Promise<GetScheduleActiveCount> {
const orgTimezone = this.initializeOrganizationTimezone(ctx);
const { scheduleIDs, populatedQuestionnaireSchedules, scheduleDateMap, oneDayBeforeStart } =
await this.getScheduleWithDetails(ctx, query);
if (populatedQuestionnaireSchedules.length === 0) {
return { totalCompleted: 0, totalScheduled: 0 };
}
const userOffDays = await this.userOffDayMongoRepo.findOneByQuery({ userID: ctx.user.userID, disabled: false });
const reports = await this.reportRepo.findReportByQueryWithProjection(
{
scheduleID: { $in: scheduleIDs },
scheduledDate: {
$gte: oneDayBeforeStart,
$lte: query.endDate,
},
isAdhoc: false,
status: 'complete',
},
{
reportID: 1,
scheduleID: 1,
scheduledDate: 1,
siteID: 1,
isAdhoc: 1,
totalAnsweredQuestions: 1,
totalQuestions: 1,
status: 1,
},
);
const siteMap = {};
const questionnaireMap = {};
const reportScheduleMap = arrayToNestedMap(
reports.filter((scheduleReport) => scheduleReport.scheduledDate),
'scheduleID',
'scheduledDate',
);
// Questionnaire Schedule
for (const sch of populatedQuestionnaireSchedules) {
const targetQuestionnaire = questionnaireMap[sch.questionnaireIndexID];
const targetSite = siteMap[sch.siteID];
let timezone = sch.timezone || targetSite.timezone || orgTimezone || DEFAULT_TIMEZONE;
injectScheduleByDate({
schedule: sch,
scheduleDateMap,
query,
timezone,
site: targetSite,
questionnaire: targetQuestionnaire,
reports: reportScheduleMap[sch.scheduleID],
isActivePeriodOnly: false,
userOffDays: userOffDays?.dates || [],
});
}
const totalOverview = {
totalCompleted: 0,
totalScheduled: 0,
};
Object.keys(scheduleDateMap).forEach((date) => {
const schedulesPerDate = scheduleDateMap[date];
const filteredData = sortFilterScheduleDetails(schedulesPerDate, query) as SiteScheduleDetail[];
const overview = calculateProgress(filteredData);
totalOverview.totalCompleted += overview.totalCompleted;
totalOverview.totalScheduled += overview.totalSchedule;
});
return totalOverview;
}- Mermaid Diagram:
graph LR A[Controller: getScheduleCount] --> B(Usecase: getUserActiveSchedulesCount); B --> C{Initialize Timezone}; C --> D{Get Schedule Details}; D --> E{Get Reports}; E --> F{Inject Schedule Details}; F --> G{Calculate Count}; G --> H[Return Schedule Count];
4.5. /progress
- Purpose: Gets the schedule progress.
- Controller Function:
ScheduleLiteController.getScheduleProgress()(src/controllers/scheduleLite.controller.ts) - Usecase Function:
ScheduleLiteUsecase.getUserActiveSchedulesProgress()(src/domains/scheduleLite/usecase/scheduleLite.usecase.ts) - Data Flow:
- The controller receives the request with the query parameters.
- The controller calls the
getScheduleProgressfunction in the use case. - The use case retrieves the organization information.
- The use case retrieves the active schedules.
- The use case retrieves the reports.
- The use case injects schedule details by date.
- The use case calculates the schedule progress.
- The use case returns the schedule progress.
- Code Snippets:
// ScheduleLiteController.getScheduleProgress()
public async getScheduleProgress({ context, payload }: GetUserActiveSchedulesParams) {
try {
const query = new ScheduleLiteQueryOptions({ ...payload.query, users: context.user.userID });
const schedules = await this.usecase.getUserActiveSchedulesProgress(context, query);
return this.handleSuccess(schedules);
} catch (error) {
return this.handleError(error);
}
}
// ScheduleLiteUsecase.getUserActiveSchedulesProgress()
public async getUserActiveSchedulesProgress(
ctx: Context<UserAuth>,
query: ScheduleLiteQueryOptions,
): Promise<number> {
const orgTimezone = this.initializeOrganizationTimezone(ctx);
const { scheduleIDs, populatedQuestionnaireSchedules, scheduleDateMap, oneDayBeforeStart } =
await this.getScheduleWithDetails(ctx, query);
if (populatedQuestionnaireSchedules.length === 0) {
return 0;
}
const userOffDays = await this.userOffDayMongoRepo.findOneByQuery({ userID: ctx.user.userID, disabled: false });
const reports = await this.reportRepo.findReportByQueryWithProjection(
{
scheduleID: { $in: scheduleIDs },
scheduledDate: {
$gte: oneDayBeforeStart,
$lte: query.endDate,
},
isAdhoc: false,
status: 'complete',
},
{
reportID: 1,
scheduleID: 1,
scheduledDate: 1,
siteID: 1,
isAdhoc: 1,
totalAnsweredQuestions: 1,
totalQuestions: 1,
status: 1,
},
);
const siteMap = {};
const questionnaireMap = {};
const reportScheduleMap = arrayToNestedMap(
reports.filter((scheduleReport) => scheduleReport.scheduledDate),
'scheduleID',
'scheduledDate',
);
// Questionnaire Schedule
for (const sch of populatedQuestionnaireSchedules) {
const targetQuestionnaire = questionnaireMap[sch.questionnaireIndexID];
const targetSite = siteMap[sch.siteID];
let timezone = sch.timezone || targetSite.timezone || orgTimezone || DEFAULT_TIMEZONE;
injectScheduleByDate({
schedule: sch,
scheduleDateMap,
query,
timezone,
site: targetSite,
questionnaire: targetQuestionnaire,
reports: reportScheduleMap[sch.scheduleID],
isActivePeriodOnly: false,
userOffDays: userOffDays?.dates || [],
});
}
const totalOverview = {
totalCompleted: 0,
totalSchedule: 0,
};
Object.keys(scheduleDateMap).forEach((date) => {
const schedulesPerDate = scheduleDateMap[date].filter((schedule) => schedule.questionnaireID !== '');
const filteredData = sortFilterScheduleDetails(schedulesPerDate, query) as SiteScheduleDetail[];
const overview = calculateProgress(filteredData);
totalOverview.totalCompleted += overview.totalCompleted;
totalOverview.totalSchedule += overview.totalSchedule;
});
const result: number = totalOverview.totalSchedule
? Math.round((totalOverview.totalCompleted / totalOverview.totalSchedule) * 100)
: 0;
return result;
}- Mermaid Diagram:
graph LR A[Controller: getScheduleProgress] --> B(Usecase: getUserActiveSchedulesProgress); B --> C{Initialize Timezone}; C --> D{Get Schedule Details}; D --> E{Get Reports}; E --> F{Inject Schedule Details}; F --> G{Calculate Progress}; G --> H[Return Schedule Progress];
4.6. /v2
- Purpose: Gets the user’s active schedules V2.
- Controller Function:
ScheduleLiteController.getUserActiveSchedulesV2()(src/controllers/scheduleLite.controller.ts) - Usecase Function:
ScheduleLiteUsecase.getUserActiveSchedules()(src/domains/scheduleLite/usecase/scheduleLite.usecase.ts) - Data Flow:
- The controller receives the request with the query parameters.
- The controller calls the
getUserActiveSchedulesV2function in the use case. - The use case retrieves the organization information.
- The use case retrieves the active schedules.
- The use case retrieves the site and questionnaire information.
- The use case injects schedule details by date.
- The use case returns the active schedule details.
- Code Snippets:
// ScheduleLiteController.getUserActiveSchedulesV2()
public async getUserActiveSchedulesV2({ context, payload }: GetUserActiveSchedulesParams) {
const query = new ScheduleLiteQueryOptions({ ...payload.query, users: context.user.userID });
try {
const schedules = await this.usecase.getUserActiveSchedules(context, query, true);
return this.handleSuccess(schedules);
} catch (error) {
return this.handleError(error);
}
}
// ScheduleLiteUsecase.getUserActiveSchedules()
public async getUserActiveSchedules(
ctx: Context<UserAuth>,
query: ScheduleLiteQueryOptions,
allowTeamAudit?: boolean,
): Promise<ActiveScheduleDetails> {
const organization = await this.organizationRepo.findByID(ctx.user.organizationID);
if (!organization) {
throw new ErrorCode('invalid', 'missing-organization');
}
const orgTimezone = organization.schedule?.timezone;
// set default timezone
if (orgTimezone) {
dayjs.tz?.setDefault(organization.schedule.timezone);
} else {
dayjs.tz?.setDefault('Asia/Jakarta');
}
const userOffDays = await this.userOffDayMongoRepo.findOneByQuery({ userID: ctx.user.userID, disabled: false });
const scheduleDateMap: { [date: string]: SiteScheduleDetail[] } = {};
for (let d = dayjs(query.startDate); d.isSameOrBefore(query.endDate, 'day'); d = d.add(1, 'day')) {
const date = d.format('YYYY-MM-DD');
scheduleDateMap[date] = [];
}
// #region get schedule data
let populatedQuestionnaireSchedules
let populatedQuestionnaireSchedulesTeam
let populatedQuestionnaireSchedulesSingle
let oneDayBeforeStart = dayjs(query.startDate).subtract(1, 'day').format('YYYY-MM-DD');
const scheduleIDs: string[] = [];
const teamAuditScheduleIDs: string[] = [];
const siteIDs: string[] = [];
let questionnaireIndexIDs: string[] = [];
if(allowTeamAudit){
populatedQuestionnaireSchedulesSingle = await this.scheduleRepo.findActiveAtDates({
startDate: oneDayBeforeStart,
endDate: query.endDate,
sites: query.sites,
auditors: query.users,
});
populatedQuestionnaireSchedulesTeam = await this.scheduleRepo.findActiveAtDates({
startDate: dayjs(query.startDate).subtract(1, 'years').startOf('years').format('YYYY-MM-DD'),
endDate: query.endDate,
sites: query.sites,
auditors: query.users,
},
true);
populatedQuestionnaireSchedules = [
...(populatedQuestionnaireSchedulesSingle ?? []),
...(populatedQuestionnaireSchedulesTeam ?? [])
];
}else{
populatedQuestionnaireSchedules = await this.scheduleRepo.findActiveAtDates({
startDate: oneDayBeforeStart,
endDate: query.endDate,
sites: query.sites,
auditors: query.users,
});
}- Mermaid Diagram:
graph LR A[Controller: getUserActiveSchedulesV2] --> B(Usecase: getUserActiveSchedules); B --> C{Get Organization Info}; C --> D{Get Active Schedules}; D --> E{Get Site and Questionnaire Info}; E --> F{Inject Schedule Details}; F --> G[Return Active Schedule Details];
4.7. /active-period
- Purpose: Gets the user’s active period schedules.
- Controller Function:
ScheduleLiteController.getUserActivePeriodSchedules()(src/controllers/scheduleLite.controller.ts) - Usecase Function:
ScheduleLiteUsecase.getUserActivePeriodSchedules()(src/domains/scheduleLite/usecase/scheduleLite.usecase.ts) - Data Flow:
- The controller receives the request with the query parameters.
- The controller calls the
getUserActivePeriodSchedulesfunction in the use case. - The use case retrieves the organization information.
- The use case retrieves the active schedules.
- The use case retrieves the site and questionnaire information.
- The use case injects schedule details by date.
- The use case returns the active schedule details.
- Code Snippets:
// ScheduleLiteController.getUserActivePeriodSchedules()
public async getUserActivePeriodSchedules({ context, payload }: GetUserActiveSchedulesParams) {
const query = new ScheduleLiteQueryOptions({ ...payload.query, users: context.user.userID });
try {
const schedules = await this.usecase.getUserActivePeriodSchedules(context, query);
return this.handleSuccess(schedules);
} catch (error) {
return this.handleError(error);
}
}
// ScheduleLiteUsecase.getUserActivePeriodSchedules()
public async getUserActivePeriodSchedules(
ctx: Context<UserAuth>,
query: ScheduleLiteQueryOptions,
): Promise<ActiveScheduleDetails> {
const organization = await this.organizationRepo.findByID(ctx.user.organizationID);
if (!organization) {
throw new ErrorCode('invalid', 'missing-organization');
}
const orgTimezone = organization.schedule?.timezone;
// set default timezone
if (orgTimezone) {
dayjs.tz.setDefault(organization.schedule.timezone);
} else {
dayjs.tz.setDefault('Asia/Jakarta');
}
const scheduleDateMap: { [date: string]: SiteScheduleDetail[] } = {};
for (let d = dayjs(query.startDate); d.isSameOrBefore(query.endDate, 'day'); d = d.add(1, 'day')) {
const date = d.format('YYYY-MM-DD');
scheduleDateMap[date] = [];
}
// #region get schedule data
let oneDayBeforeStart = dayjs(query.startDate).subtract(1, 'day').format('YYYY-MM-DD');
let populatedQuestionnaireSchedules = await this.scheduleRepo.findActiveAtDates({
startDate: oneDayBeforeStart,
endDate: query.endDate,
sites: query.sites,
auditors: query.users,
withDisabled: false,
withScheduleActivePeriod: true,
});
if (populatedQuestionnaireSchedules.length === 0) {
return {
totalPercentage: 0,
details: Object.keys(scheduleDateMap).map((date) => {
return {
date,
schedules: { allSchedules: [] },
};
}),
};
}
// #endregion
const scheduleIDs: string[] = [];
const siteIDs: string[] = [];
let questionnaireIndexIDs: string[] = [];
let activePeriodSchedules: SiteScheduleIndex[] = [];
let activePeriodScheduleIDs: string[] = [];
const reportQuery: MongoQuery<Report> = {
organizationID: ctx.user.organizationID,
$or: [],
};
for (const sch of populatedQuestionnaireSchedules) {
if (sch.scheduleActivePeriod) {
scheduleIDs.push(sch.scheduleID);
siteIDs.push(sch.siteID);
questionnaireIndexIDs = questionnaireIndexIDs.concat(sch.questionnaires);
activePeriodScheduleIDs.push(sch.scheduleID);
activePeriodSchedules.push(sch);
const periodStartDateQuery = dayjs()
.subtract(sch.scheduleActivePeriod.periodLength, sch.scheduleActivePeriod.periodUnit)
.format('YYYY-MM-DD');
const periodEndDateQuery = dayjs()
.add(sch.scheduleActivePeriod.periodLength, sch.scheduleActivePeriod.periodUnit)
.format('YYYY-MM-DD');
reportQuery.$or?.push({
scheduleID: sch.scheduleID,
scheduledDate: {
$gte: periodStartDateQuery,
$lte: periodEndDateQuery,
},
});
}
}- Mermaid Diagram:
graph LR A[Controller: getUserActivePeriodSchedules] --> B(Usecase: getUserActivePeriodSchedules); B --> C{Get Organization Info}; C --> D{Get Active Schedules}; D --> E{Get Site and Questionnaire Info}; E --> F{Inject Schedule Details}; F --> G[Return Active Schedule Details];
4.8. /aggregates
- Purpose: Gets the user’s active schedules with aggregation.
- Controller Function:
ScheduleLiteController.getUserActiveSchedulesWithAggregation()(src/controllers/scheduleLite.controller.ts) - Usecase Function:
ScheduleLiteUsecase.getUserActiveSchedulesWithAggregation()(src/domains/scheduleLite/usecase/scheduleLite.usecase.ts) - Data Flow:
- The controller receives the request with the query parameters.
- The controller calls the
getUserActiveSchedulesWithAggregationfunction in the use case. - The use case retrieves the organization information.
- The use case retrieves the active schedules with populated data.
- The use case retrieves the reports.
- The use case injects schedule details by date.
- The use case returns the active schedule details.
- Code Snippets:
// ScheduleLiteController.getUserActiveSchedulesWithAggregation()
public async getUserActiveSchedulesWithAggregation({ context, payload }: GetUserActiveSchedulesParams) {
const query = new ScheduleLiteQueryOptions({ ...payload.query, users: context.user.userID });
try {
const schedules = await this.usecase.getUserActiveSchedulesWithAggregation(context, query);
return this.handleSuccess(schedules);
} catch (error) {
return this.handleError(error);
}
}
// ScheduleLiteUsecase.getUserActiveSchedulesWithAggregation()
public async getUserActiveSchedulesWithAggregation(
ctx: Context<UserAuth>,
query: ScheduleLiteQueryOptions,
): Promise<ActiveScheduleDetails> {
const organization = await this.organizationRepo.findByID(ctx.user.organizationID);
if (!organization) {
throw new ErrorCode('invalid', 'missing-organization');
}
const orgTimezone = organization.schedule?.timezone;
// set default timezone
if (orgTimezone) {
dayjs.tz.setDefault(organization.schedule.timezone);
} else {
dayjs.tz.setDefault('Asia/Jakarta');
}
const scheduleDateMap: { [date: string]: SiteScheduleDetail[] } = {};
// #region get schedule data
let querySearch = {};
if (query.search) {
const regExpSearch = new RegExp(escapeRegExp(query.search), 'gi');
querySearch = {
$or: [{ 'questionnaire.title': query.search }, { 'questionnaire.title': regExpSearch }],
};
}
let populatedQuestionnaireSchedules = await this.scheduleRepo.findPopulatedActiveSchedules(
{
startDate: query.startDate,
endDate: query.endDate,
sites: query.sites,
auditors: query.users,
},
parseSort(query, 'siteName'),
querySearch,
);
if (populatedQuestionnaireSchedules.length === 0) {
return {
totalPercentage: 0,
details: Object.keys(scheduleDateMap).map((date) => {
return {
date,
schedules: {},
};
}),
};
}
// #endregion
const scheduleIDs: string[] = [];
const siteIDs: string[] = [];
let questionnaireIndexIDs: string[] = [];
for (const sch of populatedQuestionnaireSchedules) {
scheduleIDs.push(sch.scheduleID);
siteIDs.push(sch.siteID);
questionnaireIndexIDs = questionnaireIndexIDs.concat(sch.questionnaires);
}- Mermaid Diagram:
graph LR A[Controller: getUserActiveSchedulesWithAggregation] --> B(Usecase: getUserActiveSchedulesWithAggregation); B --> C{Get Organization Info}; C --> D{Get Active Schedules with Data}; D --> E{Get Reports}; E --> F{Inject Schedule Details}; F --> G[Return Active Schedule Details];
4.9. /indicator
- Purpose: Gets the user’s active schedule indicator.
- Controller Function:
ScheduleLiteController.getUserActiveSchedulesIndicator()(src/controllers/scheduleLite.controller.ts) - Usecase Function:
ScheduleLiteUsecase.getUserActiveSchedulesIndicator()(src/domains/scheduleLite/usecase/scheduleLite.usecase.ts) - Data Flow:
- The controller receives the request with the query parameters.
- The controller calls the
getUserActiveSchedulesIndicatorfunction in the use case. - The use case retrieves the organization information.
- The use case retrieves the active schedules.
- The use case retrieves the site information.
- The use case injects schedule indicator by date.
- The use case returns the schedule date map.
- Code Snippets:
// ScheduleLiteController.getUserActiveSchedulesIndicator()
public async getUserActiveSchedulesIndicator({ context, payload }: GetUserActiveSchedulesParams) {
const query = new ScheduleLiteIndicatorQueryOptions({ ...payload.query, users: context.user.userID });
try {
const schedules = await this.usecase.getUserActiveSchedulesIndicator(context, query);
return this.handleSuccess(schedules);
} catch (error) {
return this.handleError(error);
}
}
// ScheduleLiteUsecase.getUserActiveSchedulesIndicator()
public async getUserActiveSchedulesIndicator(
ctx: Context<UserAuth>,
query: ScheduleLiteIndicatorQueryOptions,
): Promise<SiteScheduleDetailIndicatorsByDate> {
const organization = await this.organizationRepo.findByID(ctx.user.organizationID);
if (!organization) {
throw new ErrorCode('invalid', 'missing-organization');
}
const orgTimezone = organization.schedule?.timezone;
const scheduleDateMap: { [date: string]: boolean } = {};
for (let d = dayjs(query.startDate); d.isSameOrBefore(query.endDate, 'day'); d = d.add(1, 'day')) {
const date = d.format('YYYY-MM-DD');
scheduleDateMap[date] = false;
}
// #region get schedule data
let oneDayBeforeStart = dayjs(query.startDate).subtract(1, 'day').format('YYYY-MM-DD');
let populatedQuestionnaireSchedules = await this.scheduleRepo.findActiveAtDates({
startDate: oneDayBeforeStart,
endDate: query.endDate,
sites: query.sites,
auditors: query.users,
});
if (populatedQuestionnaireSchedules.length === 0) {
return scheduleDateMap;
}
// #endregion
const scheduleIDs: string[] = [];
const siteIDs: string[] = [];
for (const sch of populatedQuestionnaireSchedules) {
scheduleIDs.push(sch.scheduleID);
siteIDs.push(sch.siteID);
}
const sites = await this.siteRepo.findManyByIDs(siteIDs, ['siteID', 'name', 'isMultiSite', 'timezone']);
const siteMap = arrayToMap(sites, 'siteID');
// Questionnaire Schedule
for (const sch of populatedQuestionnaireSchedules) {
const targetSite = siteMap[sch.siteID];
if (!targetSite) continue;
let timezone = sch.timezone || targetSite.timezone || orgTimezone || DEFAULT_TIMEZONE;
injectScheduleIndicatorByDate({
schedule: sch,
scheduleDateMap,
query,
isTeamAudit: targetSite.isMultiSite,
timezone,
});
}
return scheduleDateMap;
}- Mermaid Diagram:
graph LR A[Controller: getUserActiveSchedulesIndicator] --> B(Usecase: getUserActiveSchedulesIndicator); B --> C{Get Organization Info}; C --> D{Get Active Schedules}; D --> E{Get Site Info}; E --> F{Inject Schedule Indicator}; F --> G[Return Schedule Date Map];
4.10. /check-in-report
- Purpose: Creates a check-in report.
- Controller Function:
ScheduleLiteController.checkInCreateReport()(src/controllers/scheduleLite.controller.ts)` - Usecase Function:
ScheduleLiteUsecase.checkInCreateReport() (src/domains/scheduleLite/usecase/scheduleLite.usecase.ts:946) - Data Flow:
- The controller receives the request with the schedule ID, date, type, coordinates, and other details.
- The controller calls the
checkInCreateReportfunction in the use case. - The use case retrieves the schedule and site information.
- The use case validates if the user is within the site radius.
- The use case injects schedule details by date.
- The use case creates a new report and summary.
- The use case returns the report ID, firebase report ID, report details, and summary.
- Code Snippets:
// ScheduleLiteController.checkInCreateReport()
public async checkInCreateReport({ context, payload }: CheckInCreateReportParams) {
try {
const schedules = await this.usecase.checkInCreateReport(
context,
payload.data,
payload.query.onlyCreateReport === 'true',
);
return this.handleSuccess(schedules);
} catch (error) {
return this.handleError(error);
}
}
// ScheduleLiteUsecase.checkInCreateReport()
public async checkInCreateReport(
ctx: Context<UserAuth>,
payload: CheckInCreateReportRequest,
onlyCreateReport = false,
): Promise<CheckInCreateReportDetail> {
const {
date,
type,
coordinates,
makeUpReason = '',
isMakeUp = false,
scheduleID,
metadata,
deviceTimeIn = '',
} = payload;
const { organizationID, userID } = ctx.user;
const organization = await this.organizationRepo.findByID(ctx.user.organizationID);
if (!organization) {
throw new ErrorCode(errors.NOT_FOUND, 'Organization Not Found');
}
const schedule = await this.scheduleRepo.findByID({ ...ctx, timeout: 180000 }, scheduleID);
if (!schedule) throw new ErrorCode(errors.NOT_FOUND, 'Schedule Not Found');
const query = { startDate: date, endDate: date };
if (!onlyCreateReport) {
const today = moment().tz(schedule.timezone).format('YYYY-MM-DD');
if (!schedule.scheduleActivePeriod && date > today)
throw new ErrorCode(errors.INVALID, `Can't check in to the future date schedule`);
}
const [site, reports] = await Promise.all([
this.siteRepo.findByID(schedule.siteID, organizationID),
this.reportRepo.findReportByQueryWithProjection(
{
scheduleID: { $in: [scheduleID] },
scheduledDate: {
$gte: query.startDate,
$lte: query.endDate,
},
},
{
reportID: 1,
scheduleID: 1,
scheduledDate: 1,
siteID: 1,
isAdhoc: 1,
totalAnsweredQuestions: 1,
totalQuestions: 1,
status: 1,
},
),
]);
if (!site || site?.disabled) {
throw new ErrorCode(errors.NOT_FOUND, 'Site Not Found or Disabled');
}
if (!onlyCreateReport) {
// check the user within the site radius, if check-in radius defined
const userWithinTheRadius = isUserWithinTheSiteRadius(coordinates, organization, site);
if (!userWithinTheRadius) {
throw new ErrorCode(errors.INVALID, 'User Outside the Check-in Radius');
}
}
const orgTimezone = organization.schedule?.timezone;
const timezone = schedule.timezone || site.timezone || orgTimezone || DEFAULT_TIMEZONE;
const reportScheduleMap = arrayToMapArray(
reports.filter((scheduleReport) => scheduleReport.scheduledDate),
'scheduledDate',
);
const scheduleDateMap: { [date: string]: SiteScheduleDetail[] } = {
[date]: [],
};
injectScheduleByDate({
schedule,
scheduleDateMap,
query,
timezone,
site,
questionnaire: { questionnaireIndexID: schedule.questionnaireIndexID, title: '', totalQuestions: 0 },
reports: reportScheduleMap,
isActivePeriodOnly: !!schedule.scheduleActivePeriod,
});
if (!scheduleDateMap[date]?.length) throw new ErrorCode(errors.INVALID, 'Invalid schedule date');
const targetSchedule = scheduleDateMap[date].find(
(schedule) => schedule.scheduledDate === date && schedule.scheduleType === type,
);
if (!targetSchedule) throw new ErrorCode(errors.INVALID, 'Invalid schedule date');
if (!onlyCreateReport) {
const isQueryDateYesterday = isYesterday(parseISO(date));
const { isAllowedCheckIn, isAllowedCheckOut, hasDraft } = targetSchedule;
const now = moment().tz(schedule.timezone);
let minuteFromStartOfTheDay = now.hours() * 60 + now.minutes();
if (isQueryDateYesterday) {
minuteFromStartOfTheDay += 1440;
}
const startTime = schedule.startTime || 0;
const endTime = schedule.endTime || 0;
const timeDifferencesStart = Math.abs(minuteFromStartOfTheDay - startTime);
const timeDifferencesEnd = Math.abs(minuteFromStartOfTheDay - endTime);
if (!hasDraft && !isAllowedCheckIn) {
if (minuteFromStartOfTheDay < startTime) {
throw new ErrorCode(
errors.INVALID,
`You are ${convertMinutesToTimeString(timeDifferencesStart)} early for check-in the schedule`,
);
} else if (minuteFromStartOfTheDay > startTime) {
throw new ErrorCode(
errors.INVALID,
`You are ${convertMinutesToTimeString(timeDifferencesStart)} late for check-in the schedule`,
);
}
}
if (!isAllowedCheckOut) {
if (hasDraft) {
throw new ErrorCode(errors.INVALID, 'Continue the draft is not allowed as it already passed check-out time');
} else if (minuteFromStartOfTheDay > endTime) {
throw new ErrorCode(
errors.INVALID,
`You are ${convertMinutesToTimeString(timeDifferencesEnd)} late for schedule check-out time`,
);
}
}
}
const result: CheckInCreateReportDetail = {
scheduleID,
reportID: '',
siteID: schedule.siteID,
firebaseReportID: '',
};
// continue report
if (targetSchedule.hasDraft) {
let reportDraft;
if (type === ScheduleDetailTypes.ADHOC) {
const sortedReport = (reportScheduleMap[date] || [])
.filter((report) => report.isAdhoc)
.sort((reportA, reportB) => (reportA.reportID > reportB.reportID ? 1 : -1));
reportDraft = sortedReport[sortedReport.length - 1];
} else {
reportDraft = (reportScheduleMap[date] || []).filter((report) => !report.isAdhoc).find((report) => report);
}
result.reportID = reportDraft?.reportID || '';
result.firebaseReportID = reportDraft?.reportID.slice(-24) || '';
const [targetReport, targetReportSummary] = await Promise.all([
this.reportRepo.findByID(result.reportID),
this.reportSummaryRepo.findByID(result.reportID),
]);
result.report = reportFirebaseMap(targetReport);
result.report.totalAnsweredQuestions = getTotalQuestionsAnswered(targetReport?.questions || []);
result.report.questionnaireProgress = getQuestionnaireProgress(targetReport?.questions || []);
result.report.siteName = targetSchedule.siteName;
result.summary = reportSummaryFirebaseMap(targetReportSummary);
const score = countReportScore(targetReport || ({} as Report));
result.summary.scoreTotal = score.score.total;
result.summary.scoreRaw = score.score.raw;
result.summary.scoreFlag = score.scoreFlag;
result.summary.flagCount = score.flag;
const { questionnaireIndexID } = schedule;
// fetch site details
const questionnaireIdx = await this.questionnaireIndexRepo.findByID(questionnaireIndexID, organizationID);
if (questionnaireIdx) {
const { latest } = questionnaireIdx;
result.report.questionnaireName = questionnaireIdx.title;
// fetch question details from latest questionnaire version
const questionnaire = await this.questionnaireRepo.findByID(latest, organizationID);
const scoreV1 = await newCountScore(targetReport, questionnaire);
result.additionalQuestionnaireDetails = {
deductionToggle: questionnaire?.deductionToggle || false,
maxScore: questionnaire?.maxScore || 100,
categoryAttributes: questionnaire?.categoryAttributes || [],
};
result.report = { ...result.report, ...result.additionalQuestionnaireDetails };
result.summary.scoreWeighted = scoreV1;
}
return result;
}
// start report
const {
siteID,
hasDeadline,
signatures,
selfieSignatures,
startTime,
endTime,
questionnaireIndexID,
hasStrictTime,
departmentID,
} = schedule;
// fetch site details
const questionnaireIdx = await this.questionnaireIndexRepo.findByID(questionnaireIndexID, organizationID);
// fetch questionnaire index details
if (!questionnaireIdx || questionnaireIdx.disabled) {
throw new ErrorCode(
errors.NOT_FOUND,
`Questionnaire Index not found or disabled, siteID: ${siteID}, scheduleID: ${scheduleID}, scheduleDate: ${date}`,
);
}
const { latest } = questionnaireIdx;
// fetch question details from latest questionnaire version
const questionnaire = await this.questionnaireRepo.findByID(latest, organizationID);
if (!questionnaire || questionnaire.disabled) {
throw new ErrorCode(
errors.NOT_FOUND,
`Questionnaire not found or disabled, siteID: ${siteID}, scheduleID: ${scheduleID}, scheduleDate: ${date}`,
);
}
const { questions } = questionnaire;
// get the timezone info ( schedule leve | site | default)
const { supervisors: siteSupervisors } = site;
const serverTime = new Date();
const now = moment.tz(serverTime, timezone || DEFAULT_TIMEZONE).toISOString(true);
// get department supervisors
const deptSupervisors: string[] =
siteSupervisors
?.filter((supervisor) => supervisor.departmentID === departmentID)
.map((supervisor) => supervisor.userID) ?? [];
const supervisorsData = deptSupervisors.length
? await this.userRepo.find({
userID: {
$in: deptSupervisors,
},
})
: [];
// generate emailTargets
const emailList = Array.from(new Set([...(schedule?.emailTargets || [])]));
const emailTargetPayload: ReportFirebase['emailTargets'] = emailList
.filter((email) => email)
.map((email) => {
return {
email,
enabled: true,
setByAdmin: true,
status: 'unsent',
};
});
// generate signature
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const signaturePayload: Report_Firebase['signatures'] = new Array(signatures || 0).fill('').map((_) => ({
name: '',
position: '',
path: '',
isSigned: false,
}));
// calculate Due date
const periodEndDate = currentPeriodEndDate(organization.schedule);
const scheduledAt =
hasDeadline && startTime
? moment.tz(date, timezone).startOf('day').add(startTime, 'minutes')
: moment.tz(date, timezone).startOf('day');
// generate initial firebase report
const firebaseReportPayload: Partial<Report_Firebase> = {
questions,
questionnaire: latest,
status: 'draft',
site: siteID,
auditor: userID,
emailTargets: emailTargetPayload,
dueDate: periodEndDate ? periodEndDate.toISOString(true) : '',
datetimeIn: deviceTimeIn || now,
datetimeScheduled: scheduledAt.toISOString(true),
isMakeUp,
makeUpReason,
signatures: signaturePayload,
selfieSignatures,
isAdhoc: type === ScheduleDetailTypes.ADHOC,
scheduleKey: scheduleID,
hasDeadline: !!hasStrictTime,
startTime,
endTime,
totalQuestions: questions.length,
totalAnsweredQuestions: 0,
questionnaireProgress: {},
datetimeUpdated: new Date().toISOString(),
metadata,
siteName: site.name,
questionnaireName: questionnaire.title,
isOfflineMode: onlyCreateReport || false,
coordinates: coordinates || {},
};
const report = new ReportFirebase(firebaseReportPayload);
// report firebaseID
const firebaseReportID = `${date}_${moment().valueOf()}`;
// generate mongo report
const mongoReportID = `${siteID}_${firebaseReportID}`;
const mongoReport: Report = {
// team audit not supported for lite
sections: null,
signatures: report.signatures!,
selfieSignatures: report.selfieSignatures!,
emailTargets: report.emailTargets,
hasDeadline: report.hasDeadline,
startTime: report.startTime,
endTime: report.endTime,
questions: report.questions,
status: report.status,
isMakeUp: report.isMakeUp,
makeUpReason: report.makeUpReason,
isAdhoc: report.isAdhoc,
isGenerated: report.isGenerated,
isIssueCreated: report.isIssueCreated,
isInventoryAdjusted: report.isInventoryAdjusted,
department: schedule.departmentID,
reportID: mongoReportID,
scheduleID: report.scheduleKey,
siteID: report.site,
auditorID: report.auditor,
updatedAt: new Date(),
checkInAt: this.dateOrNull(report.datetimeIn) as unknown as Date,
checkOutAt: this.dateOrNull(report.datetimeOut),
deviceCheckInAt: deviceTimeIn || this.getDeviceTimestampString(report.datetimeIn) || '',
deviceCheckOutAt: this.getDeviceTimestampString(report.datetimeOut) || '',
submittedAt: this.dateOrNull(report.datetimeSubmitted),
scheduledAt: this.dateOrNull(report.datetimeScheduled) as unknown as Date,
dueAt: this.dateOrNull(report.dueDate) as unknown as Date,
siteOwnerID: site.owner,
organizationID,
questionnaireID: report.questionnaire!,
downloadURL: report.downloadURL || '',
team: {
[userID]: 'leader',
},
scheduledDate: date,
createdAt: new Date(),
// not supported for lite
isJourney: false,
journeyKey: report.journeyKey || '',
journeyReportCode: report.journeyReportCode || '',
users: [{ uid: userID, role: 'leader' }],
metadata: report.metadata,
grade: report.grade,
totalQuestions: report.questions.length,
totalAnsweredQuestions: 0,
questionnaireProgress: report.questionnaireProgress,
coordinates: coordinates,
};
const summary: ReportSummary_Firebase = {
auditor: userID,
datetimeIn: now,
datetimeOut: '',
datetimeUpdated: now,
status: 'draft',
datetimeScheduled: '',
isAdhoc: type === ScheduleDetailTypes.ADHOC,
scheduleKey: scheduleID,
};
const reportSummary: ReportSummary & { organizationID: string; createdAt: Date } = {
reportID: mongoReport.reportID,
auditorID: mongoReport.auditorID,
siteID: mongoReport.siteID,
scheduleID: mongoReport.scheduleID,
questionnaireID: mongoReport.questionnaireID,
organizationID,
department: mongoReport.department,
team: Object.keys(mongoReport.team),
users: mongoReport.users,
sections: mongoReport.sections,
status: mongoReport.status,
downloadURL: mongoReport.downloadURL || '',
scoreWeighted: -1,
scoreTotal: 0,
scoreRaw: 0,
scoreFlag: {
red: 0,
green: 0,
yellow: 0,
},
isMakeUp: mongoReport.isMakeUp,
isAdhoc: mongoReport.isAdhoc,
flagCount: {
red: 0,
green: 0,
yellow: 0,
},
scheduledAt: mongoReport.scheduledAt,
journeyKey: mongoReport.journeyKey || '',
journeyReportCode: mongoReport.journeyReportCode || '',
isJourney: !!mongoReport.isJourney,
checkOutAt: (mongoReport.checkOutAt ? new Date(mongoReport.checkOutAt) : null) as unknown as Date,
checkInAt: mongoReport.checkInAt,
createdAt: new Date(),
updatedAt: mongoReport.updatedAt,
};
### /check-in
* **Purpose:** Checks in a schedule.
* **Controller Function:** [`ScheduleLiteController.checkInSchedule()`](src/controllers/scheduleLite.controller.ts:113)
* **Usecase Function:** [`ScheduleLiteUsecase.checkInSchedule()`](src/domains/scheduleLite/usecase/scheduleLite.usecase.ts:829)
* **Data Flow:**
1. The controller receives the request with the schedule ID, date, type, and coordinates.
2. The controller calls the `checkInSchedule` function in the use case.
3. The use case retrieves the schedule and site information.
4. The use case validates if the user is within the site radius.
5. The use case injects schedule details by date.
6. The use case checks if the schedule is allowed to check in.
7. The use case returns the report ID and firebase report ID.
* **Code Snippets:**
```typescript
// ScheduleLiteController.checkInSchedule()
public async checkInSchedule({ context, payload }: CheckInScheduleParams) {
const query: CheckInSchedulePayload = {
scheduleID: payload.data.scheduleID,
date: payload.data.date,
type: payload.data.type,
coordinates: payload.data.coordinates,
};
try {
const schedules = await this.usecase.checkInSchedule(context, query);
return this.handleSuccess(schedules);
} catch (error) {
return this.handleError(error);
}
}
// ScheduleLiteUsecase.checkInSchedule()
public async checkInSchedule(ctx: Context<UserAuth>, payload: CheckInSchedulePayload): Promise<CheckInDetail> {
const { scheduleID, date, type, coordinates } = payload;
const { organizationID } = ctx.user;
const schedule = await this.scheduleRepo.findByID({ ...ctx, timeout: 180000 }, scheduleID);
if (!schedule) throw new ErrorCode(errors.NOT_FOUND, 'Schedule Not Found');
const today = moment().tz(schedule.timezone).format('YYYY-MM-DD');
if (!schedule.scheduleActivePeriod && date > today)
throw new ErrorCode(errors.INVALID, `Can't check in to the future date schedule`);
const query = { startDate: date, endDate: date };
const [site, reports] = await Promise.all([
this.siteRepo.findByID(schedule.siteID, organizationID),
this.reportRepo.findReportByQueryWithProjection(
{
scheduleID: { $in: [scheduleID] },
scheduledDate: {
$gte: query.startDate,
$lte: query.endDate,
},
},
{
reportID: 1,
scheduleID: 1,
scheduledDate: 1,
siteID: 1,
isAdhoc: 1,
totalAnsweredQuestions: 1,
totalQuestions: 1,
status: 1,
},
),
]);
if (!site) {
throw new ErrorCode(errors.NOT_FOUND, 'Site Not Found');
}
// check the user within the site radius, if check-in radius defined
const userWithinTheRadius = isUserWithinTheSiteRadius(coordinates, organization, site);
if (!userWithinTheRadius) {
throw new ErrorCode(errors.INVALID, 'User Outside the Check-in Radius');
}
const orgTimezone = organization.schedule?.timezone;
const timezone = schedule.timezone || site.timezone || orgTimezone || DEFAULT_TIMEZONE;
const reportScheduleMap = arrayToMapArray(
reports.filter((scheduleReport) => scheduleReport.scheduledDate),
'scheduledDate',
);
const scheduleDateMap: { [date: string]: SiteScheduleDetail[] } = {
[date]: [],
};
injectScheduleByDate({
schedule,
scheduleDateMap,
query,
timezone,
site,
questionnaire: { questionnaireIndexID: schedule.questionnaireIndexID, title: '', totalQuestions: 0 },
reports: reportScheduleMap,
isActivePeriodOnly: !!schedule.scheduleActivePeriod,
});
if (!scheduleDateMap[date]?.length) throw new ErrorCode(errors.INVALID, 'Invalid schedule date');
const targetSchedule = scheduleDateMap[date].find((schedule) => schedule.scheduleType === type);
if (!targetSchedule) throw new ErrorCode(errors.INVALID, 'Invalid schedule date');
const isAllowedToCheckIn = targetSchedule.isAllowedCheckIn;
const isAllowedToCheckOut = targetSchedule.isAllowedCheckOut;
if (!targetSchedule.hasDraft && (!isAllowedToCheckIn || !isAllowedToCheckOut)) {
const now = moment().tz(schedule.timezone);
const minuteFromStartOfTheDay = now.hours() * 60 + now.minutes();
const timeDifferences = Math.abs(minuteFromStartOfTheDay - (schedule.startTime || 0));
if (minuteFromStartOfTheDay < (schedule.startTime || 0)) {
throw new ErrorCode(
errors.INVALID,
`You are ${convertMinutesToTimeString(timeDifferences)} early for checkin the schedule`,
);
} else if (minuteFromStartOfTheDay > (schedule.startTime || 0)) {
throw new ErrorCode(
errors.INVALID,
`You are ${convertMinutesToTimeString(timeDifferences)} late for checkin the schedule`,
);
}
throw new ErrorCode(errors.INVALID, 'Check-in is not allowed');
}
if (targetSchedule.hasDraft && !isAllowedToCheckOut)
throw new ErrorCode(errors.INVALID, 'Continue the draft is not allowed as it already passed check-out time');
const result: CheckInDetail = {
scheduleID,
reportID: '',
siteID: schedule.siteID,
firebaseReportID: '',
};
if (targetSchedule.hasDraft) {
const reportDraft = (reportScheduleMap[date] || []).find((report) => report);
result.reportID = reportDraft?.reportID || '';
result.firebaseReportID = reportDraft?.reportID.slice(-24) || '';
}
return result;
}- Mermaid Diagram:
graph LR A[Controller: checkInSchedule] --> B(Usecase: checkInSchedule); B --> C{Get Schedule and Site Info}; C --> D{Validate User Radius}; D --> E{Inject Schedule Details}; E --> F{Check Check-in Allowed}; F --> G[Return Report IDs];
5. Implementation Details
5.1. Versioning
The Schedule Lite API uses semantic versioning to manage changes to the API.
5.2. Semantic Versioning
Semantic versioning is a versioning scheme that uses three numbers to represent the version of the API:
MAJOR: The major version number. This number is incremented when there are incompatible API changes.MINOR: The minor version number. This number is incremented when there are new features added to the API.PATCH: The patch version number. This number is incremented when there are bug fixes or other minor changes made to the API.
5.3. API Versioning Strategy
The API uses the following versioning strategy:
- The API’s version number is included in the API’s URL. For example, the URL for version 1 of the API is
/v1/schedules. - The API’s version number is also included in the API’s response headers.
This allows clients to easily determine the version of the API that they are using and to ensure that they are using a compatible version.
6. Logging
The Schedule Lite API uses a logging library to record information about the API’s operation. This information can be used to debug problems, monitor performance, and track usage.
6.1. Logging Levels
The API uses the following logging levels:
DEBUG: This level is used to record detailed information about the API’s operation.INFO: This level is used to record general information about the API’s operation.WARN: This level is used to record warnings about potential problems.ERROR: This level is used to record errors that have occurred.FATAL: This level is used to record fatal errors that have caused the API to crash.
6.2. Logging Format
The API uses a structured logging format. Each log message includes a timestamp, a log level, a message, and any other relevant information.
6.3. Logging Configuration
The API’s logging configuration is stored in a configuration file. This file specifies the logging level, the logging format, and the location of the log files.
Here’s an example of how logging is used in the checkInSchedule method:
// src/controllers/scheduleLite.controller.ts
public checkInSchedule = async (req: Request, res: Response, next: NextFunction) => {
try {
// Log the request
console.log('Received check-in request');
// Implementation details here
} catch (error) {
// Log the error
console.error('Error processing check-in request:', error);
next(error);
}
};7. Request and Response Formats
The Schedule Lite API uses JSON for both request and response bodies.
7.1. Request Format
All requests to the API must include a Content-Type header set to application/json. The request body should be a valid JSON object.
7.2. Response Format
All responses from the API will be in JSON format. The response body will typically include a code field indicating the status of the request, a message field providing a human-readable description of the status, and a data field containing the requested data.
Here’s an example of a successful response:
{
"code": "SUCCESS",
"message": "Request processed successfully",
"data": {
// The requested data here
}
}Here’s an example of an error response:
{
"code": "INTERNAL_SERVER_ERROR",
"message": "An unexpected error occurred",
"data": null
}8. Middleware
The Schedule Lite API uses several middleware functions to handle common tasks such as authentication, validation, and error handling.
8.1. Authentication Middleware
The authentication middleware is used to authenticate requests. It checks for the presence of a valid JWT in the Authorization header and authorizes access to resources based on the user’s roles and permissions.
8.2. Validation Middleware
The validation middleware is used to validate the request body and query parameters. It uses the @nimbly-technologies/nimbly-backend-utils library to validate the request data and returns an error response if the request is invalid.
8.3. Error Handling Middleware
The error handling middleware is used to handle errors that occur during the processing of a request. It logs the error and returns an appropriate error response to the client.
Here’s an example of how the middleware is used in the checkInSchedule method:
// src/routes/scheduleLite.routes.ts
.post(
'/check-in',
middlewares.validatorHandler(validator, 'getCheckInStatus'),
middlewares.expressMiddlewareHandler(this.controller.checkInSchedule),
)In this example, the validatorHandler middleware is used to validate the request body, and the expressMiddlewareHandler middleware is used to handle any errors that occur during the processing of the request.
6. Related Documentation
- [[Schedule|[Schedules](../../Schedule/Schedule Listing/ScheduleListingOverview.md)]]
- [[Features/Notifications/Notifications-Overview|Notifications]]
- [Issue Tracker](../../Issue Tracker/IssueTrackerOverview.md)
- Gallery
- Single Audit
- Mobile App Development Guide
- API Documentation
- Authentication|Authentication]]