@escapenavigator/utils 2.1.30 → 2.2.1

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.
@@ -0,0 +1,23 @@
1
+ import { EmployeeAccess, EmployeeAccessScope } from '@escapenavigator/types/dist/role/employee-access';
2
+ import { RoleRO } from '@escapenavigator/types/dist/role/role.ro';
3
+ export type EmployeeAccessSection = keyof EmployeeAccess;
4
+ export type EmployeeAccessAction = 'view' | 'create' | 'edit' | 'delete';
5
+ type LegacyEmployeeAccessRole = Pick<RoleRO, 'totalAccess' | 'canEditUsersAccessAndTime' | 'canEditUsersAccess' | 'canEditModerators' | 'canEditWorkedHours' | 'canEditOwnWorkedHours' | 'canEditFines' | 'canSeeEmployeeReport' | 'accessToFinanceSection' | 'financePaymentsOnly'> & Partial<Pick<RoleRO, 'employeeAccess'>>;
6
+ export declare const createFullEmployeeAccess: () => EmployeeAccess;
7
+ /**
8
+ * Exact compatibility mapping for roles that have not been saved using the
9
+ * granular editor yet. No existing role is migrated or silently restricted.
10
+ */
11
+ export declare const deriveLegacyEmployeeAccess: (role?: LegacyEmployeeAccessRole | null) => EmployeeAccess;
12
+ /**
13
+ * Form values must always have every section. A stored object with no sections
14
+ * (legacy null, or a nested DTO stripped by serialization) keeps the fallback.
15
+ * A partial object keeps what was saved and denies only the missing sections.
16
+ */
17
+ export declare const completeEmployeeAccess: (access: Partial<EmployeeAccess> | null | undefined, fallback: EmployeeAccess) => EmployeeAccess;
18
+ export declare const resolveEmployeeAccess: (role?: LegacyEmployeeAccessRole | null) => EmployeeAccess;
19
+ export declare const hasEmployeeAccess: (role: LegacyEmployeeAccessRole | null | undefined, section: EmployeeAccessSection, action: EmployeeAccessAction) => boolean;
20
+ export declare const getEmployeeAccessScope: (role: LegacyEmployeeAccessRole | null | undefined, section: "shifts" | "bonuses" | "fines" | "payroll") => EmployeeAccessScope;
21
+ export declare const canManageEmployeeAccess: (role: LegacyEmployeeAccessRole | null | undefined) => boolean;
22
+ export declare const canAssignEmployeeRoles: (role: LegacyEmployeeAccessRole | null | undefined) => boolean;
23
+ export {};
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.canAssignEmployeeRoles = exports.canManageEmployeeAccess = exports.getEmployeeAccessScope = exports.hasEmployeeAccess = exports.resolveEmployeeAccess = exports.completeEmployeeAccess = exports.deriveLegacyEmployeeAccess = exports.createFullEmployeeAccess = void 0;
4
+ const employee_access_1 = require("@escapenavigator/types/dist/role/employee-access");
5
+ const crud = (view, create = view, edit = create, remove = edit) => ({
6
+ view,
7
+ create,
8
+ edit,
9
+ delete: remove,
10
+ });
11
+ const scoped = (permissions, scope) => ({
12
+ ...permissions,
13
+ scope,
14
+ });
15
+ const createFullEmployeeAccess = () => ({
16
+ employees: { ...crud(true), manageAccess: true },
17
+ shifts: scoped(crud(true), employee_access_1.EmployeeAccessScope.ALL),
18
+ bonuses: scoped(crud(true), employee_access_1.EmployeeAccessScope.ALL),
19
+ fines: scoped(crud(true), employee_access_1.EmployeeAccessScope.ALL),
20
+ roles: crud(true),
21
+ positions: crud(true),
22
+ reports: crud(true, false, false, false),
23
+ payroll: scoped(crud(true), employee_access_1.EmployeeAccessScope.ALL),
24
+ });
25
+ exports.createFullEmployeeAccess = createFullEmployeeAccess;
26
+ /**
27
+ * Exact compatibility mapping for roles that have not been saved using the
28
+ * granular editor yet. No existing role is migrated or silently restricted.
29
+ */
30
+ const deriveLegacyEmployeeAccess = (role) => {
31
+ if (role?.totalAccess)
32
+ return (0, exports.createFullEmployeeAccess)();
33
+ const usersBase = !!role?.canEditUsersAccessAndTime;
34
+ const manageAccess = usersBase && !!role?.canEditUsersAccess;
35
+ const editOthers = usersBase && !!role?.canEditModerators;
36
+ const manageShifts = !!role?.canEditWorkedHours;
37
+ const ownShifts = !!role?.canEditOwnWorkedHours;
38
+ const manageAdjustments = usersBase && !!role?.canEditFines;
39
+ const managePayroll = !!role?.accessToFinanceSection && !role?.financePaymentsOnly;
40
+ return {
41
+ employees: {
42
+ ...crud(usersBase, manageAccess, editOthers, manageAccess && editOthers),
43
+ manageAccess,
44
+ },
45
+ shifts: scoped(crud(usersBase || ownShifts, manageShifts || ownShifts, manageShifts || ownShifts), usersBase || manageShifts ? employee_access_1.EmployeeAccessScope.ALL : employee_access_1.EmployeeAccessScope.SELF),
46
+ bonuses: scoped(crud(manageAdjustments), employee_access_1.EmployeeAccessScope.ALL),
47
+ fines: scoped(crud(manageAdjustments), employee_access_1.EmployeeAccessScope.ALL),
48
+ roles: crud(false),
49
+ positions: crud(usersBase),
50
+ reports: crud(!!role?.canSeeEmployeeReport, false, false, false),
51
+ payroll: scoped(crud(managePayroll), employee_access_1.EmployeeAccessScope.ALL),
52
+ };
53
+ };
54
+ exports.deriveLegacyEmployeeAccess = deriveLegacyEmployeeAccess;
55
+ const deniedCrud = () => crud(false);
56
+ const deniedScoped = () => scoped(deniedCrud(), employee_access_1.EmployeeAccessScope.SELF);
57
+ const EMPLOYEE_ACCESS_SECTIONS = [
58
+ 'employees',
59
+ 'shifts',
60
+ 'bonuses',
61
+ 'fines',
62
+ 'roles',
63
+ 'positions',
64
+ 'reports',
65
+ 'payroll',
66
+ ];
67
+ /**
68
+ * Form values must always have every section. A stored object with no sections
69
+ * (legacy null, or a nested DTO stripped by serialization) keeps the fallback.
70
+ * A partial object keeps what was saved and denies only the missing sections.
71
+ */
72
+ const completeEmployeeAccess = (access, fallback) => {
73
+ const hasSection = !!access && EMPLOYEE_ACCESS_SECTIONS.some((section) => access[section] != null);
74
+ if (!hasSection)
75
+ return fallback;
76
+ return {
77
+ employees: access.employees ?? { ...deniedCrud(), manageAccess: false },
78
+ shifts: access.shifts ?? deniedScoped(),
79
+ bonuses: access.bonuses ?? deniedScoped(),
80
+ fines: access.fines ?? deniedScoped(),
81
+ roles: access.roles ?? deniedCrud(),
82
+ positions: access.positions ?? deniedCrud(),
83
+ reports: access.reports ?? deniedCrud(),
84
+ payroll: access.payroll ?? deniedScoped(),
85
+ };
86
+ };
87
+ exports.completeEmployeeAccess = completeEmployeeAccess;
88
+ const resolveEmployeeAccess = (role) => role?.totalAccess
89
+ ? (0, exports.createFullEmployeeAccess)()
90
+ : role?.employeeAccess || (0, exports.deriveLegacyEmployeeAccess)(role);
91
+ exports.resolveEmployeeAccess = resolveEmployeeAccess;
92
+ const hasEmployeeAccess = (role, section, action) => !!(0, exports.resolveEmployeeAccess)(role)[section][action];
93
+ exports.hasEmployeeAccess = hasEmployeeAccess;
94
+ const getEmployeeAccessScope = (role, section) => (0, exports.resolveEmployeeAccess)(role)[section].scope;
95
+ exports.getEmployeeAccessScope = getEmployeeAccessScope;
96
+ const canManageEmployeeAccess = (role) => !!(0, exports.resolveEmployeeAccess)(role).employees.manageAccess;
97
+ exports.canManageEmployeeAccess = canManageEmployeeAccess;
98
+ const canAssignEmployeeRoles = (role) => !!role?.totalAccess ||
99
+ (role?.employeeAccess
100
+ ? !!role.employeeAccess.roles.edit
101
+ : !!role?.canEditUsersAccessAndTime && !!role?.canEditUsersAccess);
102
+ exports.canAssignEmployeeRoles = canAssignEmployeeRoles;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const employee_access_1 = require("@escapenavigator/types/dist/role/employee-access");
4
+ const employee_access_permissions_1 = require("./employee-access-permissions");
5
+ const role = (overrides = {}) => ({
6
+ totalAccess: false,
7
+ canEditUsersAccessAndTime: false,
8
+ canEditUsersAccess: false,
9
+ canEditModerators: false,
10
+ canEditWorkedHours: false,
11
+ canEditOwnWorkedHours: false,
12
+ canEditFines: false,
13
+ canSeeEmployeeReport: false,
14
+ accessToFinanceSection: false,
15
+ financePaymentsOnly: false,
16
+ ...overrides,
17
+ });
18
+ describe('employee access permissions', () => {
19
+ it('keeps a legacy role on the old coarse permissions', () => {
20
+ const permissions = (0, employee_access_permissions_1.deriveLegacyEmployeeAccess)(role({
21
+ canEditUsersAccessAndTime: true,
22
+ canEditWorkedHours: true,
23
+ canEditFines: true,
24
+ }));
25
+ expect(permissions.employees.view).toBe(true);
26
+ expect(permissions.shifts.edit).toBe(true);
27
+ expect(permissions.shifts.scope).toBe(employee_access_1.EmployeeAccessScope.ALL);
28
+ expect(permissions.fines.create).toBe(true);
29
+ expect(permissions.roles.view).toBe(false);
30
+ });
31
+ it('keeps own-shift access scoped to self', () => {
32
+ const permissions = (0, employee_access_permissions_1.deriveLegacyEmployeeAccess)(role({ canEditOwnWorkedHours: true }));
33
+ expect(permissions.shifts.create).toBe(true);
34
+ expect(permissions.shifts.scope).toBe(employee_access_1.EmployeeAccessScope.SELF);
35
+ });
36
+ it('uses explicit granular permissions without merging legacy flags', () => {
37
+ const explicit = (0, employee_access_permissions_1.deriveLegacyEmployeeAccess)(role({ totalAccess: true }));
38
+ explicit.employees.view = false;
39
+ explicit.shifts.view = false;
40
+ const current = role({
41
+ canEditUsersAccessAndTime: true,
42
+ employeeAccess: explicit,
43
+ });
44
+ expect((0, employee_access_permissions_1.resolveEmployeeAccess)(current)).toBe(explicit);
45
+ expect((0, employee_access_permissions_1.hasEmployeeAccess)(current, 'employees', 'view')).toBe(false);
46
+ expect((0, employee_access_permissions_1.hasEmployeeAccess)(current, 'positions', 'view')).toBe(true);
47
+ });
48
+ it('gives total-access roles every supported permission', () => {
49
+ const explicit = (0, employee_access_permissions_1.deriveLegacyEmployeeAccess)(role());
50
+ const permissions = (0, employee_access_permissions_1.resolveEmployeeAccess)(role({ totalAccess: true, employeeAccess: explicit }));
51
+ expect(permissions.employees.manageAccess).toBe(true);
52
+ expect(permissions.roles.delete).toBe(true);
53
+ expect(permissions.payroll.scope).toBe(employee_access_1.EmployeeAccessScope.ALL);
54
+ expect(permissions.reports.create).toBe(false);
55
+ });
56
+ it('keeps a legacy fallback when stored access has no sections', () => {
57
+ const fallback = (0, employee_access_permissions_1.deriveLegacyEmployeeAccess)(role({ canEditUsersAccessAndTime: true }));
58
+ expect((0, employee_access_permissions_1.completeEmployeeAccess)(null, fallback)).toBe(fallback);
59
+ expect((0, employee_access_permissions_1.completeEmployeeAccess)({}, fallback).employees.view).toBe(true);
60
+ });
61
+ it('fills only the sections missing from a partial access object', () => {
62
+ const fallback = (0, employee_access_permissions_1.deriveLegacyEmployeeAccess)(role({ canEditUsersAccessAndTime: true }));
63
+ const access = (0, employee_access_permissions_1.completeEmployeeAccess)({
64
+ employees: {
65
+ view: true,
66
+ create: false,
67
+ edit: false,
68
+ delete: false,
69
+ manageAccess: false,
70
+ },
71
+ }, fallback);
72
+ expect(access.employees.create).toBe(false);
73
+ expect(access.payroll.view).toBe(false);
74
+ expect(access.payroll.scope).toBe(employee_access_1.EmployeeAccessScope.SELF);
75
+ });
76
+ });
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from './convert-hhmm-to-minutes';
7
7
  export * from './convert-minutes-to-hhmm';
8
8
  export * from './convert-to-options';
9
9
  export * from './date';
10
+ export * from './employee-access-permissions';
10
11
  export * from './enum-to-options';
11
12
  export * from './format-flex-duration-label';
12
13
  export * from './format-slot-discount';
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ __exportStar(require("./convert-hhmm-to-minutes"), exports);
23
23
  __exportStar(require("./convert-minutes-to-hhmm"), exports);
24
24
  __exportStar(require("./convert-to-options"), exports);
25
25
  __exportStar(require("./date"), exports);
26
+ __exportStar(require("./employee-access-permissions"), exports);
26
27
  __exportStar(require("./enum-to-options"), exports);
27
28
  __exportStar(require("./format-flex-duration-label"), exports);
28
29
  __exportStar(require("./format-slot-discount"), exports);
@@ -1,5 +1,5 @@
1
1
  import { RoleRO } from '@escapenavigator/types/dist/role/role.ro';
2
- export type RoleEmployeeReportPermissions = Pick<RoleRO, 'totalAccess' | 'canSeeEmployeeReport'>;
2
+ export type RoleEmployeeReportPermissions = Pick<RoleRO, 'totalAccess' | 'canSeeEmployeeReport'> & Partial<Pick<RoleRO, 'employeeAccess'>>;
3
3
  /**
4
4
  * Право видеть отчёт по работникам.
5
5
  * Владелец (`totalAccess`) — как будто флаг включён.
@@ -1,9 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.canSeeEmployeeReport = void 0;
4
+ const employee_access_permissions_1 = require("./employee-access-permissions");
4
5
  /**
5
6
  * Право видеть отчёт по работникам.
6
7
  * Владелец (`totalAccess`) — как будто флаг включён.
7
8
  */
8
- const canSeeEmployeeReport = (role) => !!role?.totalAccess || !!role?.canSeeEmployeeReport;
9
+ const canSeeEmployeeReport = (role) => role?.employeeAccess
10
+ ? (0, employee_access_permissions_1.hasEmployeeAccess)(role, 'reports', 'view')
11
+ : !!role?.totalAccess || !!role?.canSeeEmployeeReport;
9
12
  exports.canSeeEmployeeReport = canSeeEmployeeReport;
@@ -0,0 +1 @@
1
+ import 'reflect-metadata';
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ require("reflect-metadata");
4
+ const create_role_dto_1 = require("@escapenavigator/types/dist/role/create-role.dto");
5
+ const employee_access_1 = require("@escapenavigator/types/dist/role/employee-access");
6
+ const serialize_record_1 = require("./serialize-record");
7
+ describe('CreateRoleDto employeeAccess', () => {
8
+ it('сохраняет вложенные права при сборке формы роли', () => {
9
+ const dto = (0, serialize_record_1.serializeRecord)(create_role_dto_1.CreateRoleDto, {
10
+ title: 'Администратор',
11
+ totalAccess: false,
12
+ employeeAccess: {
13
+ employees: {
14
+ view: true,
15
+ create: false,
16
+ edit: true,
17
+ delete: false,
18
+ manageAccess: true,
19
+ },
20
+ shifts: {
21
+ view: true,
22
+ create: false,
23
+ edit: false,
24
+ delete: false,
25
+ scope: employee_access_1.EmployeeAccessScope.ALL,
26
+ },
27
+ bonuses: {
28
+ view: false,
29
+ create: false,
30
+ edit: false,
31
+ delete: false,
32
+ scope: employee_access_1.EmployeeAccessScope.SELF,
33
+ },
34
+ fines: {
35
+ view: false,
36
+ create: false,
37
+ edit: false,
38
+ delete: false,
39
+ scope: employee_access_1.EmployeeAccessScope.SELF,
40
+ },
41
+ roles: { view: true, create: false, edit: false, delete: false },
42
+ positions: { view: true, create: true, edit: false, delete: false },
43
+ reports: { view: true, create: false, edit: false, delete: false },
44
+ payroll: {
45
+ view: true,
46
+ create: false,
47
+ edit: false,
48
+ delete: false,
49
+ scope: employee_access_1.EmployeeAccessScope.ASSIGNED_LOCATIONS,
50
+ },
51
+ },
52
+ });
53
+ expect(dto.employeeAccess?.employees.view).toBe(true);
54
+ expect(dto.employeeAccess?.employees.manageAccess).toBe(true);
55
+ expect(dto.employeeAccess?.shifts.scope).toBe(employee_access_1.EmployeeAccessScope.ALL);
56
+ expect(dto.employeeAccess?.payroll.view).toBe(true);
57
+ expect(dto.employeeAccess?.roles.view).toBe(true);
58
+ });
59
+ });
@@ -10,10 +10,12 @@ export declare const USER_SESSION_ERROR_KEYS: {
10
10
  };
11
11
  export type UserSessionErrorKey = keyof typeof USER_SESSION_ERROR_KEYS;
12
12
  export declare const USER_SESSION_ERRORS_EN: Record<UserSessionErrorKey, string>;
13
- export type UserSessionRolePermissions = Pick<RoleRO, 'totalAccess' | 'canEditWorkedHours' | 'canEditOwnWorkedHours'>;
13
+ export type UserSessionRolePermissions = Pick<RoleRO, 'totalAccess' | 'canEditWorkedHours' | 'canEditOwnWorkedHours'> & Partial<Pick<RoleRO, 'employeeAccess'>>;
14
14
  export type UserSessionPermissionTarget = {
15
15
  hold?: boolean;
16
16
  authorId?: number | null;
17
+ /** Employee the shift is assigned to. Null when the shift is still open. */
18
+ userId?: number | null;
17
19
  date: string;
18
20
  };
19
21
  export type UserSessionClaimTarget = {
@@ -35,6 +37,7 @@ export type UserSessionClaimDto = {
35
37
  hold?: boolean;
36
38
  };
37
39
  export declare const canEditOtherUserSessions: (role: UserSessionRolePermissions) => boolean;
40
+ export declare const canCreateOtherUserSessions: (role: UserSessionRolePermissions) => boolean;
38
41
  export declare const isSessionDateTodayOrFuture: (date: string, today?: string) => boolean;
39
42
  /** Unassigned shift or a shift with an open replace request — any employee may claim it. */
40
43
  export declare const isOpenUserSession: (session: Pick<UserSessionClaimTarget, "userId" | "needReplace">) => boolean;
@@ -52,6 +55,7 @@ export declare const getUserSessionEditDenial: (params: {
52
55
  session: UserSessionPermissionTarget;
53
56
  role: UserSessionRolePermissions;
54
57
  userId: number;
58
+ action?: "edit" | "delete";
55
59
  }) => string | null;
56
60
  /** Returns an English denial message, or null when creating a shift is allowed. */
57
61
  export declare const getUserSessionCreateDenial: (params: {
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.canEditUserSession = exports.getUserSessionCreateDenial = exports.getUserSessionEditDenial = exports.isTakingOpenUserSession = exports.isOpenUserSession = exports.isSessionDateTodayOrFuture = exports.canEditOtherUserSessions = exports.USER_SESSION_ERRORS_EN = exports.USER_SESSION_ERROR_KEYS = void 0;
3
+ exports.canEditUserSession = exports.getUserSessionCreateDenial = exports.getUserSessionEditDenial = exports.isTakingOpenUserSession = exports.isOpenUserSession = exports.isSessionDateTodayOrFuture = exports.canCreateOtherUserSessions = exports.canEditOtherUserSessions = exports.USER_SESSION_ERRORS_EN = exports.USER_SESSION_ERROR_KEYS = void 0;
4
+ const employee_access_1 = require("@escapenavigator/types/dist/role/employee-access");
5
+ const employee_access_permissions_1 = require("./employee-access-permissions");
4
6
  exports.USER_SESSION_ERROR_KEYS = {
5
7
  shiftLocked: 'shiftLocked',
6
8
  noPermission: 'noPermission',
@@ -13,14 +15,23 @@ exports.USER_SESSION_ERROR_KEYS = {
13
15
  exports.USER_SESSION_ERRORS_EN = {
14
16
  shiftLocked: 'This shift is locked. Unlock it before editing or deleting.',
15
17
  noPermission: 'You do not have permission to manage work shifts.',
16
- notShiftAuthor: 'You can only edit shifts that you created. Ask a manager with worked-hours access to change this shift.',
18
+ notShiftAuthor: 'You can only edit shifts assigned to you, or shifts that you created. Ask a manager with worked-hours access to change this shift.',
17
19
  ownShiftPastDate: 'You can only edit or delete your own shifts on today or future dates.',
18
20
  cannotAssignOthers: 'You cannot create or assign shifts for other employees. Enable "Can edit logged time" on your role.',
19
21
  cannotCreateUnassigned: 'You cannot create unassigned shifts. Enable "Can edit logged time" on your role.',
20
22
  sessionNotFound: 'Work shift not found.',
21
23
  };
22
- const canEditOtherUserSessions = (role) => !!role.totalAccess || !!role.canEditWorkedHours;
24
+ const canMutateOtherUserSessions = (role, action) => role.employeeAccess
25
+ ? (0, employee_access_permissions_1.hasEmployeeAccess)(role, 'shifts', action) &&
26
+ (0, employee_access_permissions_1.getEmployeeAccessScope)(role, 'shifts') !== employee_access_1.EmployeeAccessScope.SELF
27
+ : !!role.totalAccess || !!role.canEditWorkedHours;
28
+ const canMutateOwnUserSessions = (role, action) => role.employeeAccess
29
+ ? (0, employee_access_permissions_1.hasEmployeeAccess)(role, 'shifts', action)
30
+ : !!role.totalAccess || !!role.canEditWorkedHours || !!role.canEditOwnWorkedHours;
31
+ const canEditOtherUserSessions = (role) => canMutateOtherUserSessions(role, 'edit');
23
32
  exports.canEditOtherUserSessions = canEditOtherUserSessions;
33
+ const canCreateOtherUserSessions = (role) => canMutateOtherUserSessions(role, 'create');
34
+ exports.canCreateOtherUserSessions = canCreateOtherUserSessions;
24
35
  const isSessionDateTodayOrFuture = (date, today = formatTodayYmd()) => today <= date;
25
36
  exports.isSessionDateTodayOrFuture = isSessionDateTodayOrFuture;
26
37
  /** Unassigned shift or a shift with an open replace request — any employee may claim it. */
@@ -65,17 +76,22 @@ const formatTodayYmd = () => {
65
76
  };
66
77
  /** Returns an English denial message, or null when edit/delete is allowed. */
67
78
  const getUserSessionEditDenial = (params) => {
68
- const { session, role, userId } = params;
79
+ const { session, role, userId, action = 'edit' } = params;
69
80
  if (session.hold) {
70
81
  return exports.USER_SESSION_ERRORS_EN.shiftLocked;
71
82
  }
72
- if ((0, exports.canEditOtherUserSessions)(role)) {
83
+ if (canMutateOtherUserSessions(role, action)) {
73
84
  return null;
74
85
  }
75
- if (!role.canEditOwnWorkedHours) {
86
+ if (!canMutateOwnUserSessions(role, action)) {
76
87
  return exports.USER_SESSION_ERRORS_EN.noPermission;
77
88
  }
78
- if (session.authorId !== userId) {
89
+ const assignedToActor = session.userId != null && session.userId === userId;
90
+ const authoredByActor = session.authorId === userId;
91
+ // Own-hours access covers the worker's assigned shift even when a location
92
+ // admin created it. Unassigned shifts and other people's shifts stay limited
93
+ // to the author.
94
+ if (!assignedToActor && !authoredByActor) {
79
95
  return exports.USER_SESSION_ERRORS_EN.notShiftAuthor;
80
96
  }
81
97
  if (!(0, exports.isSessionDateTodayOrFuture)(session.date)) {
@@ -87,10 +103,10 @@ exports.getUserSessionEditDenial = getUserSessionEditDenial;
87
103
  /** Returns an English denial message, or null when creating a shift is allowed. */
88
104
  const getUserSessionCreateDenial = (params) => {
89
105
  const { assigneeUserId, role, actorId } = params;
90
- if ((0, exports.canEditOtherUserSessions)(role)) {
106
+ if ((0, exports.canCreateOtherUserSessions)(role)) {
91
107
  return null;
92
108
  }
93
- if (!role.canEditOwnWorkedHours) {
109
+ if (!canMutateOwnUserSessions(role, 'create')) {
94
110
  return exports.USER_SESSION_ERRORS_EN.noPermission;
95
111
  }
96
112
  if (assigneeUserId != null && assigneeUserId !== actorId) {
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const employee_access_permissions_1 = require("./employee-access-permissions");
3
4
  const user_session_permissions_1 = require("./user-session-permissions");
4
5
  const openSession = {
5
6
  hold: false,
@@ -79,16 +80,80 @@ describe('isTakingOpenUserSession', () => {
79
80
  })).toBe(false);
80
81
  });
81
82
  });
83
+ const ownHoursRole = {
84
+ totalAccess: false,
85
+ canEditWorkedHours: false,
86
+ canEditOwnWorkedHours: true,
87
+ };
82
88
  describe('getUserSessionEditDenial', () => {
83
89
  it("still requires author for non-manager edit of someone else's unassigned shift", () => {
84
90
  expect((0, user_session_permissions_1.getUserSessionEditDenial)({
85
91
  session: { hold: false, authorId: 1, date: '2099-01-01' },
86
- role: {
87
- totalAccess: false,
88
- canEditWorkedHours: false,
89
- canEditOwnWorkedHours: true,
90
- },
92
+ role: ownHoursRole,
91
93
  userId: 42,
92
94
  })).toBe(user_session_permissions_1.USER_SESSION_ERRORS_EN.notShiftAuthor);
93
95
  });
96
+ it('allows a worker to edit an unlocked shift assigned to them when an admin created it', () => {
97
+ expect((0, user_session_permissions_1.getUserSessionEditDenial)({
98
+ session: {
99
+ hold: false,
100
+ authorId: 1,
101
+ userId: 42,
102
+ date: '2099-01-01',
103
+ },
104
+ role: ownHoursRole,
105
+ userId: 42,
106
+ })).toBeNull();
107
+ });
108
+ it('still blocks a locked shift assigned to the worker', () => {
109
+ expect((0, user_session_permissions_1.getUserSessionEditDenial)({
110
+ session: {
111
+ hold: true,
112
+ authorId: 1,
113
+ userId: 42,
114
+ date: '2099-01-01',
115
+ },
116
+ role: ownHoursRole,
117
+ userId: 42,
118
+ })).toBe(user_session_permissions_1.USER_SESSION_ERRORS_EN.shiftLocked);
119
+ });
120
+ it('still blocks a past shift assigned to the worker', () => {
121
+ expect((0, user_session_permissions_1.getUserSessionEditDenial)({
122
+ session: {
123
+ hold: false,
124
+ authorId: 1,
125
+ userId: 42,
126
+ date: '2000-01-01',
127
+ },
128
+ role: ownHoursRole,
129
+ userId: 42,
130
+ })).toBe(user_session_permissions_1.USER_SESSION_ERRORS_EN.ownShiftPastDate);
131
+ });
132
+ });
133
+ describe('granular shift actions', () => {
134
+ it('does not let edit permission imply create or delete', () => {
135
+ const employeeAccess = (0, employee_access_permissions_1.createFullEmployeeAccess)();
136
+ employeeAccess.shifts.create = false;
137
+ employeeAccess.shifts.delete = false;
138
+ const role = {
139
+ ...ownHoursRole,
140
+ employeeAccess,
141
+ };
142
+ expect((0, user_session_permissions_1.getUserSessionCreateDenial)({
143
+ assigneeUserId: 42,
144
+ role,
145
+ actorId: 42,
146
+ })).toBe(user_session_permissions_1.USER_SESSION_ERRORS_EN.noPermission);
147
+ expect((0, user_session_permissions_1.getUserSessionEditDenial)({
148
+ session: {
149
+ hold: false,
150
+ authorId: 42,
151
+ userId: 42,
152
+ date: '2099-01-01',
153
+ },
154
+ role,
155
+ userId: 42,
156
+ action: 'delete',
157
+ })).toBe(user_session_permissions_1.USER_SESSION_ERRORS_EN.noPermission);
158
+ });
94
159
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escapenavigator/utils",
3
- "version": "2.1.30",
3
+ "version": "2.2.1",
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.30",
17
+ "@escapenavigator/types": "^2.2.1",
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": "089c7026734588a852850ab98ef6984711a5f343"
32
+ "gitHead": "ea7b4af0c3cf4882d2d5359bf6adeee25dde246e"
33
33
  }