@escapenavigator/utils 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/get-slot-cancelation-date/index.js +21 -1
- package/dist/get-slot-cancelation-date/index.test.js +20 -0
- package/dist/part-of-day/index.d.ts +7 -0
- package/dist/part-of-day/index.js +40 -0
- package/dist/role-lead-permissions.d.ts +7 -0
- package/dist/role-lead-permissions.js +14 -1
- package/dist/role-lead-permissions.spec.d.ts +1 -0
- package/dist/role-lead-permissions.spec.js +29 -0
- package/package.json +3 -3
|
@@ -6,10 +6,30 @@ const date_1 = require("../date");
|
|
|
6
6
|
function getSlotCancelationDate({ date, time, minHoursForFreeCanceling, cancelationRule, timeZone, now = new Date(), }) {
|
|
7
7
|
const minHours = cancelationRule === questroom_cancelation_type_enum_1.QuestroomCancelationTypeEnum.NO ? 0 : minHoursForFreeCanceling;
|
|
8
8
|
const timeHm = /^(\d{2}:\d{2})/.exec(time.trim())?.[1] ?? '00:00';
|
|
9
|
-
const
|
|
9
|
+
const fallback = {
|
|
10
|
+
canCancel: false,
|
|
11
|
+
slotCancelationDate: now,
|
|
12
|
+
slotCancelationHumenDate: '',
|
|
13
|
+
};
|
|
14
|
+
// Invalid Date: date-fns-tz возвращает NaN, наш zonedTimeToUtc кидает
|
|
15
|
+
// RangeError в formatToParts. Оба пути — ESCAPE-NAVIGATOR-WIDGET-7Q
|
|
16
|
+
// (`deadlineUtc.toISOString()`).
|
|
17
|
+
let slotUtc;
|
|
18
|
+
try {
|
|
19
|
+
slotUtc = (0, date_1.zonedTimeToUtc)(`${date} ${timeHm}`, timeZone);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
if (Number.isNaN(slotUtc.getTime())) {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
10
27
|
/** Виджет передаёт отрицательное значение часов до слота; в тестах — положительное. */
|
|
11
28
|
const deadlineHours = minHours <= 0 ? minHours : -minHours;
|
|
12
29
|
const deadlineUtc = (0, date_1.addHours)(slotUtc, deadlineHours);
|
|
30
|
+
if (Number.isNaN(deadlineUtc.getTime())) {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
13
33
|
const d = (0, date_1.utcToZonedTime)(deadlineUtc.toISOString(), timeZone);
|
|
14
34
|
return {
|
|
15
35
|
canCancel: (0, date_1.isBefore)(now, deadlineUtc),
|
|
@@ -77,4 +77,24 @@ describe('getSlotCancelationDate function', () => {
|
|
|
77
77
|
});
|
|
78
78
|
expect(result.slotCancelationHumenDate).toBe('01.06.2023, 09:00');
|
|
79
79
|
});
|
|
80
|
+
test('does not throw on unparseable date', () => {
|
|
81
|
+
const result = (0, _1.getSlotCancelationDate)({
|
|
82
|
+
cancelationRule: questroom_cancelation_type_enum_1.QuestroomCancelationTypeEnum.FULL_PRICE,
|
|
83
|
+
date: 'not-a-date',
|
|
84
|
+
time: '',
|
|
85
|
+
minHoursForFreeCanceling: 48,
|
|
86
|
+
timeZone: 'Europe/Vienna',
|
|
87
|
+
});
|
|
88
|
+
expect(result.canCancel).toBe(false);
|
|
89
|
+
expect(result.slotCancelationHumenDate).toBe('');
|
|
90
|
+
});
|
|
91
|
+
test('does not throw when widget passes utcDate ISO as date and empty time', () => {
|
|
92
|
+
expect(() => (0, _1.getSlotCancelationDate)({
|
|
93
|
+
cancelationRule: questroom_cancelation_type_enum_1.QuestroomCancelationTypeEnum.FULL_PRICE,
|
|
94
|
+
date: '2023-01-03T08:00:00.000Z',
|
|
95
|
+
time: '',
|
|
96
|
+
minHoursForFreeCanceling: -48,
|
|
97
|
+
timeZone: 'Europe/Vienna',
|
|
98
|
+
})).not.toThrow();
|
|
99
|
+
});
|
|
80
100
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PartOfDayEnum } from '@escapenavigator/types/dist/packages/part-of-day.enum';
|
|
2
|
+
/** Порядок сегментов в UI — от раннего к позднему. */
|
|
3
|
+
export declare const PARTS_OF_DAY: PartOfDayEnum[];
|
|
4
|
+
/** `HH:mm` → минуты от полуночи. Невалидная строка даёт 0. */
|
|
5
|
+
export declare const timeToMinutes: (time: string) => number;
|
|
6
|
+
export declare const getPartOfDay: (start: string) => PartOfDayEnum;
|
|
7
|
+
export declare const isInPartOfDay: (start: string, part?: PartOfDayEnum | null) => boolean;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isInPartOfDay = exports.getPartOfDay = exports.timeToMinutes = exports.PARTS_OF_DAY = void 0;
|
|
4
|
+
const part_of_day_enum_1 = require("@escapenavigator/types/dist/packages/part-of-day.enum");
|
|
5
|
+
/**
|
|
6
|
+
* Классификация времени старта по частям дня. Живёт в utils, потому что
|
|
7
|
+
* границы нужны и бэкенду (он режет и считает варианты), и виджету (он
|
|
8
|
+
* подписывает сегменты) — разъезд границ дал бы «Вечер · 5», в котором
|
|
9
|
+
* оказалось бы 4 варианта.
|
|
10
|
+
*/
|
|
11
|
+
/** Начало части дня в минутах от полуночи. Верхняя граница — начало следующей. */
|
|
12
|
+
const PART_OF_DAY_START_MINUTES = {
|
|
13
|
+
[part_of_day_enum_1.PartOfDayEnum.MORNING]: 0,
|
|
14
|
+
[part_of_day_enum_1.PartOfDayEnum.DAY]: 12 * 60,
|
|
15
|
+
[part_of_day_enum_1.PartOfDayEnum.EVENING]: 17 * 60,
|
|
16
|
+
};
|
|
17
|
+
/** Порядок сегментов в UI — от раннего к позднему. */
|
|
18
|
+
exports.PARTS_OF_DAY = [
|
|
19
|
+
part_of_day_enum_1.PartOfDayEnum.MORNING,
|
|
20
|
+
part_of_day_enum_1.PartOfDayEnum.DAY,
|
|
21
|
+
part_of_day_enum_1.PartOfDayEnum.EVENING,
|
|
22
|
+
];
|
|
23
|
+
/** `HH:mm` → минуты от полуночи. Невалидная строка даёт 0. */
|
|
24
|
+
const timeToMinutes = (time) => {
|
|
25
|
+
const [hours = '0', minutes = '0'] = (time ?? '').split(':');
|
|
26
|
+
const parsed = Number(hours) * 60 + Number(minutes);
|
|
27
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
28
|
+
};
|
|
29
|
+
exports.timeToMinutes = timeToMinutes;
|
|
30
|
+
const getPartOfDay = (start) => {
|
|
31
|
+
const minutes = (0, exports.timeToMinutes)(start);
|
|
32
|
+
if (minutes >= PART_OF_DAY_START_MINUTES[part_of_day_enum_1.PartOfDayEnum.EVENING])
|
|
33
|
+
return part_of_day_enum_1.PartOfDayEnum.EVENING;
|
|
34
|
+
if (minutes >= PART_OF_DAY_START_MINUTES[part_of_day_enum_1.PartOfDayEnum.DAY])
|
|
35
|
+
return part_of_day_enum_1.PartOfDayEnum.DAY;
|
|
36
|
+
return part_of_day_enum_1.PartOfDayEnum.MORNING;
|
|
37
|
+
};
|
|
38
|
+
exports.getPartOfDay = getPartOfDay;
|
|
39
|
+
const isInPartOfDay = (start, part) => !part || (0, exports.getPartOfDay)(start) === part;
|
|
40
|
+
exports.isInPartOfDay = isInPartOfDay;
|
|
@@ -5,3 +5,10 @@ export type RoleLeadPermissions = Pick<RoleRO, 'totalAccess' | 'accessToLeadsSec
|
|
|
5
5
|
* включён. Раздел новый, поэтому у старых ролей флага нет и доступа тоже.
|
|
6
6
|
*/
|
|
7
7
|
export declare const canAccessLeads: (role?: RoleLeadPermissions | null) => boolean;
|
|
8
|
+
export type RoleIncomingLeadCardsPermissions = Partial<Pick<RoleRO, 'canSeeIncomingLeadCards'>>;
|
|
9
|
+
/**
|
|
10
|
+
* Всплывающие карточки входящих звонков и заявок.
|
|
11
|
+
* Намеренно без `totalAccess`: владелец настраивает это отдельно,
|
|
12
|
+
* иначе собственник получал бы каждую карточку вместе с полным доступом.
|
|
13
|
+
*/
|
|
14
|
+
export declare const canSeeIncomingLeadCards: (role?: RoleIncomingLeadCardsPermissions | null) => boolean;
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.canAccessLeads = void 0;
|
|
3
|
+
exports.canSeeIncomingLeadCards = exports.canAccessLeads = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Доступ к разделу «Заявки». Владелец (`totalAccess`) — как будто флаг
|
|
6
6
|
* включён. Раздел новый, поэтому у старых ролей флага нет и доступа тоже.
|
|
7
7
|
*/
|
|
8
8
|
const canAccessLeads = (role) => !!role?.totalAccess || !!role?.accessToLeadsSection;
|
|
9
9
|
exports.canAccessLeads = canAccessLeads;
|
|
10
|
+
/**
|
|
11
|
+
* Всплывающие карточки входящих звонков и заявок.
|
|
12
|
+
* Намеренно без `totalAccess`: владелец настраивает это отдельно,
|
|
13
|
+
* иначе собственник получал бы каждую карточку вместе с полным доступом.
|
|
14
|
+
*/
|
|
15
|
+
const canSeeIncomingLeadCards = (role) => {
|
|
16
|
+
if (!role)
|
|
17
|
+
return false;
|
|
18
|
+
// Нет поля (старый API / роль до колонки) — как сейчас: карточки видны.
|
|
19
|
+
// Явный `false` — единственный способ скрыть, в том числе у owner.
|
|
20
|
+
return role.canSeeIncomingLeadCards !== false;
|
|
21
|
+
};
|
|
22
|
+
exports.canSeeIncomingLeadCards = canSeeIncomingLeadCards;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const role_lead_permissions_1 = require("./role-lead-permissions");
|
|
4
|
+
describe('canAccessLeads', () => {
|
|
5
|
+
it('is false without role or flag', () => {
|
|
6
|
+
expect((0, role_lead_permissions_1.canAccessLeads)()).toBe(false);
|
|
7
|
+
expect((0, role_lead_permissions_1.canAccessLeads)(null)).toBe(false);
|
|
8
|
+
expect((0, role_lead_permissions_1.canAccessLeads)({ totalAccess: false, accessToLeadsSection: false })).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
it('is true when flag is on', () => {
|
|
11
|
+
expect((0, role_lead_permissions_1.canAccessLeads)({ totalAccess: false, accessToLeadsSection: true })).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it('is true for totalAccess even when flag is off', () => {
|
|
14
|
+
expect((0, role_lead_permissions_1.canAccessLeads)({ totalAccess: true, accessToLeadsSection: false })).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('canSeeIncomingLeadCards', () => {
|
|
18
|
+
it('is false without a role', () => {
|
|
19
|
+
expect((0, role_lead_permissions_1.canSeeIncomingLeadCards)()).toBe(false);
|
|
20
|
+
expect((0, role_lead_permissions_1.canSeeIncomingLeadCards)(null)).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
it('is true when the flag is on or missing', () => {
|
|
23
|
+
expect((0, role_lead_permissions_1.canSeeIncomingLeadCards)({ canSeeIncomingLeadCards: true })).toBe(true);
|
|
24
|
+
expect((0, role_lead_permissions_1.canSeeIncomingLeadCards)({})).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
it('is false only when the flag is explicitly off', () => {
|
|
27
|
+
expect((0, role_lead_permissions_1.canSeeIncomingLeadCards)({ canSeeIncomingLeadCards: false })).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escapenavigator/utils",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"test": "jest"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@escapenavigator/types": "^2.1.
|
|
17
|
+
"@escapenavigator/types": "^2.1.2",
|
|
18
18
|
"axios": "^0.21.4",
|
|
19
19
|
"class-transformer": "^0.5.1",
|
|
20
20
|
"class-validator": "^0.13.2",
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
"ts-jest": "^29.1.1",
|
|
30
30
|
"typescript": "^5.6"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "dd6e8ce621abae35e9e462a7a1e1a3cf54cdbf6f"
|
|
33
33
|
}
|