Skip to content
Draft
175 changes: 172 additions & 3 deletions src/modules/Dashboard/components/ActivityGrid/ActivityGrid.utils.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
import { t } from 'i18next';
import {
isWithinInterval,
getDay,
getDate,
getYear,
getMonth,
getHours,
getMinutes,
getSeconds,
format,
isWeekend,
isAfter,
setDate,
setMonth,
setYear,
} from 'date-fns';

import { Svg } from 'shared/components/Svg';
import { MenuItem, MenuItemType } from 'shared/components';
Expand All @@ -10,9 +26,160 @@ import {
getIsWebSupported,
} from 'shared/utils';
import { EditablePerformanceTasks } from 'modules/Builder/features/Activities/Activities.const';
import { PeriodicityType } from 'shared/state/Applet/Applet.schema';

import { ActivityActions, ActivityActionProps } from './ActivityGrid.types';

/**
* this function checks if the current date is within the interval passed as arguments
* @param start Date
* @param end Date
* @returns boolean
*/
function datetimeIsWithinInterval(start: Date, end: Date) {
const today = new Date();
if (isAfter(start, end)) {
console.error('The start date is after the end date, be sure to check the dates');

// using the isWithinInterval function with the start and end dates swapped
// in case the start date is after the end date
return isWithinInterval(today, { start: end, end: start });
}

return isWithinInterval(today, { start, end });
}

/**
* this function returns the values of the date represented as an object
* @param date Date
* @returns object
*/
function getDateValues(date: Date) {
if (!date || !(date instanceof Date)) {
console.error('The date is not valid, be sure to pass a valid date');

return {
day: 0,
month: 0,
year: 0,
hour: 0,
minute: 0,
second: 0,
dayInWeek: 0,
date: new Date(),
};
}

return {
day: getDate(date),
month: getMonth(date) + 1,
year: getYear(date),
hour: getHours(date),
minute: getMinutes(date),
second: getSeconds(date),
dayInWeek: getDay(date),
date,
};
}

const PERIODICITY_VALUES = {
ONCE: 'ONCE',
ALWAYS: 'ALWAYS',
DAILY: 'DAILY',
WEEKLY: 'WEEKLY',
WEEKDAYS: 'WEEKDAYS',
MONTHLY: 'MONTHLY',
};

/**
* this function formats the date as YYYY-MM-DD
* @param date Date
* @returns string
*/
function formatDateAsYYYYMMDD(date: Date) {
if (!date || !(date instanceof Date)) {
console.error('The date is not valid, be sure to pass a valid date');
}

return format(date, 'yyyy-MM-dd');
}

/**
* this function validates if the date is in range based on the PeriodicityTypes object
* the options of validation are:
* - if the periodicity is always, which means that the activity is always available
* - if the periodicity is once, which means that the activity is available only once
* - if the periodicity is daily, which means that the activity is available every day
* - if the periodicity is weekly, which means that the activity is available every week at the same week day
* - if the periodicity is weekdays, which means that the activity is available every weekday and not in weekends
* - if the periodicity is monthly, which means that the activity is available every month at the same day
* - if the periodicity is not set
* @param periodicity PeriodicityType
* @returns boolean
*/
function validateIfDateIsInRange(periodicity: PeriodicityType) {
if (!periodicity) {
console.error('The periodicity is not set, be sure to pass a valid periodicity');

return false;
}

if (periodicity.access_before_schedule || periodicity.type === PERIODICITY_VALUES.ALWAYS) {
return true;
}

const currentDate = new Date();
const currentTimeValues = getDateValues(currentDate);
const startDate = new Date(
`${periodicity.start_date ? periodicity.start_date : formatDateAsYYYYMMDD(currentDate)}T${
periodicity.start_time
}`,
);
const endDate = new Date(
`${periodicity?.end_date ? periodicity.end_date : formatDateAsYYYYMMDD(currentDate)}T${
periodicity.end_time
}`,
);

const startTimeValues = getDateValues(startDate);

if (
periodicity.type === PERIODICITY_VALUES.ONCE ||
periodicity.type === PERIODICITY_VALUES.DAILY
) {
const todayEndDateWithEndTime = setYear(
setMonth(setDate(endDate, currentTimeValues.day), currentTimeValues.month - 1),
currentTimeValues.year,
);

return datetimeIsWithinInterval(startDate, todayEndDateWithEndTime);
} else if (periodicity.type === PERIODICITY_VALUES.WEEKLY) {
if (currentTimeValues.dayInWeek !== startTimeValues.dayInWeek) {
return false;
}

return datetimeIsWithinInterval(startDate, endDate);
} else if (periodicity.type === PERIODICITY_VALUES.WEEKDAYS) {
if (isWeekend(currentDate)) {
return false;
}

const todayEndDateWithEndTime = setYear(
setMonth(setDate(endDate, currentTimeValues.day), currentTimeValues.month - 1),
currentTimeValues.year,
);

return datetimeIsWithinInterval(startDate, todayEndDateWithEndTime);
} else if (
periodicity.type === PERIODICITY_VALUES.MONTHLY &&
currentTimeValues.day === startTimeValues.day
) {
return datetimeIsWithinInterval(startDate, endDate);
}

return false;
}

export const getActivityActions = ({
actions: { editActivity, exportData, assignActivity, takeNow },
appletId,
Expand All @@ -34,7 +201,9 @@ export const getActivityActions = ({
const { id: activityId } = activity;
const isWebUnsupported = !getIsWebSupported(activity.items);

if (!activityId) return [];
if (!activityId || !activity?.periodicity) return [];

const isInRange = validateIfDateIsInRange(activity?.periodicity);

return [
{
Expand Down Expand Up @@ -68,8 +237,8 @@ export const getActivityActions = ({
title: t('takeNow.menuItem'),
context: { appletId, activityId },
isDisplayed: canDoTakeNow,
disabled: isWebUnsupported,
tooltip: isWebUnsupported && t('activityIsMobileOnly'),
disabled: isWebUnsupported || !isInRange,
tooltip: (!isInRange && t('')) || (isWebUnsupported && t('activityIsMobileOnly')),
'data-testid': `${dataTestId}-activity-take-now`,
},
];
Expand Down
82 changes: 56 additions & 26 deletions src/shared/state/Applet/Applet.schema.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { ColorResult } from 'react-color';

import { BaseSchema } from 'shared/state/Base';
import { ElementType, RetentionPeriods } from 'shared/types';
import {
ItemResponseType,
SubscaleTotalScore,
CorrectPress,
DeviceType,
FlankerSamplingMethod,
OrderName,
RoundTypeEnum,
} from 'modules/Builder/types';
import {
CalculationType,
ConditionType,
ScoreConditionType,
ConditionalLogicMatch,
CalculationType,
PerfTaskType,
GyroscopeOrTouch,
ItemResponseType,
PerfTaskType,
ScoreConditionType,
ScoreReportType,
SubscaleTotalScore,
} from 'shared/consts';
import { BaseSchema } from 'shared/state/Base';
import { ElementType, RetentionPeriods } from 'shared/types';
import { Encryption } from 'shared/utils/encryption';
import {
CorrectPress,
RoundTypeEnum,
FlankerSamplingMethod,
DeviceType,
OrderName,
} from 'modules/Builder/types';

type ActivityFlowItem = {
activityId: string;
Expand Down Expand Up @@ -661,26 +661,56 @@ export type ScoresObject = {
[key: string]: number;
};

// types from the BE repo defined in src/apps/schedule/domain/constants.py with the same name as the following:
export type PeriodicityTypeValues = {
ONCE: 'ONCE';
DAILY: 'DAILY';
WEEKLY: 'WEEKLY';
WEEKDAYS: 'WEEKDAYS';
MONTHLY: 'MONTHLY';
ALWAYS: 'ALWAYS';
};

export type TimerType = {
NOT_SET: 'NOT_SET';
TIMER: 'TIMER';
IDLE: 'IDLE';
};

export type PeriodicityType = {
access_before_schedule: boolean;
end_date: string; // Format: "YYYY-MM-DD"
end_time: string; // Format: "HH:MM:SS"
one_time_completion: null | string;
selected_date: null | string;
start_date: string; // Format: "YYYY-MM-DD"
start_time: string; // Format: "HH:MM:SS"
timer: null | number;
timer_type: TimerType[keyof TimerType];
type: PeriodicityTypeValues[keyof PeriodicityTypeValues];
};

export type Activity = {
createdAt?: string;
description: string | Record<string, string>;
id?: string;
image?: string;
isHidden?: boolean;
isPerformanceTask?: boolean;
isReviewable?: boolean;
isSkippable?: boolean;
items: Item[];
key?: string;
name: string;
order?: number;
description: string | Record<string, string>;
splashScreen?: string;
image?: string;
showAllAtOnce?: boolean;
isSkippable?: boolean;
isReviewable?: boolean;
performanceTaskType?: PerfTaskType | null;
periodicity?: PeriodicityType;
reportIncludedItemName?: string;
responseIsEditable?: boolean;
isHidden?: boolean;
items: Item[];
scoresAndReports?: ScoresAndReports;
showAllAtOnce?: boolean;
splashScreen?: string;
subscaleSetting?: SubscaleSetting | null;
isPerformanceTask?: boolean;
performanceTaskType?: PerfTaskType | null;
createdAt?: string;
reportIncludedItemName?: string;
};

type Theme = {
Expand Down