@managemint-solutions/sdk 0.45.0 → 0.50.0
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/README.md +9 -8
- package/dist/client.d.ts +2 -1
- package/dist/client.js +2 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +14 -7
- package/dist/leave/dto.d.ts +44 -16
- package/dist/leave/dto.js +183 -57
- package/dist/leave/index.d.ts +3 -1
- package/dist/leave/index.js +7 -3
- package/dist/leave/leave-adjustments.d.ts +18 -0
- package/dist/leave/leave-adjustments.js +59 -0
- package/dist/leave/leave-entitlements.d.ts +24 -18
- package/dist/leave/leave-entitlements.js +76 -44
- package/dist/leave/leave-policy.d.ts +50 -0
- package/dist/leave/leave-policy.js +98 -0
- package/dist/leave/leave-requests.d.ts +9 -10
- package/dist/leave/leave-requests.js +41 -26
- package/dist/leave/leave-types.js +40 -5
- package/dist/leave/working-days.d.ts +14 -10
- package/dist/leave/working-days.js +31 -24
- package/dist/organizations/dto.d.ts +1 -0
- package/dist/organizations/dto.js +8 -0
- package/dist/service-client/index.d.ts +19 -5
- package/dist/service-client/index.js +35 -6
- package/dist/statuses/dto.d.ts +4 -1
- package/dist/statuses/dto.js +13 -1
- package/dist/statuses/index.d.ts +10 -1
- package/dist/statuses/index.js +51 -2
- package/dist/users/dto.d.ts +2 -0
- package/dist/users/dto.js +20 -0
- package/dist/users/index.d.ts +12 -2
- package/dist/users/index.js +22 -7
- package/package.json +2 -2
package/dist/leave/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Leave, the first slice of the HR module: one resource per table, the DTOs the api validates
|
|
3
|
-
// with, and the calendar arithmetic the api counts days with. The
|
|
4
|
-
// `SupabaseClient` as `leaveTypes`, `leaveEntitlements
|
|
3
|
+
// with, and the calendar and policy arithmetic the api counts days and balances with. The
|
|
4
|
+
// resources hang off `SupabaseClient` as `leaveTypes`, `leaveEntitlements`, `leaveAdjustments`
|
|
5
|
+
// and `leaveRequests`; the holiday
|
|
5
6
|
// calendar the day count reads is its own resource, `publicHolidays` (src/public-holidays).
|
|
6
7
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
8
|
if (k2 === undefined) k2 = k;
|
|
@@ -18,9 +19,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
18
19
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
19
20
|
};
|
|
20
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.LeaveRequestsResource = exports.LeaveEntitlementsResource = exports.LeaveTypesResource = void 0;
|
|
22
|
+
exports.LeaveRequestsResource = exports.LeaveEntitlementsResource = exports.LeaveTypesResource = exports.LeaveAdjustmentsResource = void 0;
|
|
22
23
|
__exportStar(require("./dto"), exports);
|
|
23
24
|
__exportStar(require("./working-days"), exports);
|
|
25
|
+
__exportStar(require("./leave-policy"), exports);
|
|
26
|
+
var leave_adjustments_1 = require("./leave-adjustments");
|
|
27
|
+
Object.defineProperty(exports, "LeaveAdjustmentsResource", { enumerable: true, get: function () { return leave_adjustments_1.LeaveAdjustmentsResource; } });
|
|
24
28
|
var leave_types_1 = require("./leave-types");
|
|
25
29
|
Object.defineProperty(exports, "LeaveTypesResource", { enumerable: true, get: function () { return leave_types_1.LeaveTypesResource; } });
|
|
26
30
|
var leave_entitlements_1 = require("./leave-entitlements");
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { LeaveAdjustmentEntity } from '@managemint-solutions/entities/leave';
|
|
2
|
+
import { type AuditActor, type AuditResource } from '../audit';
|
|
3
|
+
import type { TypedSupabaseClient } from '../client';
|
|
4
|
+
import type { CreateLeaveAdjustmentDto } from './dto';
|
|
5
|
+
/**
|
|
6
|
+
* Manual corrections to an entitlement's balance. Insert and read only: the table has no
|
|
7
|
+
* update or delete grant, so a mistake is corrected by a second adjustment and both stay on
|
|
8
|
+
* record. The balance itself sums them in `LeaveEntitlementsResource`.
|
|
9
|
+
*/
|
|
10
|
+
export declare class LeaveAdjustmentsResource {
|
|
11
|
+
private readonly supabase;
|
|
12
|
+
private readonly auth;
|
|
13
|
+
private readonly table;
|
|
14
|
+
constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
|
|
15
|
+
/** The adjustments of the given entitlements, oldest first. */
|
|
16
|
+
list(leaveEntitlementIds: string[]): Promise<LeaveAdjustmentEntity[]>;
|
|
17
|
+
create(leaveEntitlementId: string, input: CreateLeaveAdjustmentDto): Promise<LeaveAdjustmentEntity>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LeaveAdjustmentsResource = void 0;
|
|
4
|
+
const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
5
|
+
const audit_1 = require("../audit");
|
|
6
|
+
const errors_1 = require("../errors");
|
|
7
|
+
const LEAVE_ADJUSTMENT_SELECT = `
|
|
8
|
+
mms_id,
|
|
9
|
+
created_at,
|
|
10
|
+
created_by:user_profiles!created_by (*),
|
|
11
|
+
leave_entitlement_id,
|
|
12
|
+
days,
|
|
13
|
+
reason
|
|
14
|
+
`;
|
|
15
|
+
/**
|
|
16
|
+
* Manual corrections to an entitlement's balance. Insert and read only: the table has no
|
|
17
|
+
* update or delete grant, so a mistake is corrected by a second adjustment and both stay on
|
|
18
|
+
* record. The balance itself sums them in `LeaveEntitlementsResource`.
|
|
19
|
+
*/
|
|
20
|
+
class LeaveAdjustmentsResource {
|
|
21
|
+
supabase;
|
|
22
|
+
auth;
|
|
23
|
+
table;
|
|
24
|
+
constructor(supabase, auth, audit) {
|
|
25
|
+
this.supabase = supabase;
|
|
26
|
+
this.auth = auth;
|
|
27
|
+
this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
|
|
28
|
+
table: 'leave_adjustments',
|
|
29
|
+
entityType: enum_1.AuditTrailEntityType.LEAVE_ADJUSTMENTS,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** The adjustments of the given entitlements, oldest first. */
|
|
33
|
+
async list(leaveEntitlementIds) {
|
|
34
|
+
if (leaveEntitlementIds.length === 0)
|
|
35
|
+
return [];
|
|
36
|
+
const { data, error } = await this.supabase
|
|
37
|
+
.from('leave_adjustments')
|
|
38
|
+
.select(LEAVE_ADJUSTMENT_SELECT)
|
|
39
|
+
.eq('organization_id', this.auth.organization_id)
|
|
40
|
+
.in('leave_entitlement_id', leaveEntitlementIds)
|
|
41
|
+
.order('created_at');
|
|
42
|
+
if (error)
|
|
43
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
44
|
+
return (data ?? []).map((row) => ({ ...row, days: Number(row.days) }));
|
|
45
|
+
}
|
|
46
|
+
async create(leaveEntitlementId, input) {
|
|
47
|
+
const row = await this.table.insert({
|
|
48
|
+
leave_entitlement_id: leaveEntitlementId,
|
|
49
|
+
days: input.days,
|
|
50
|
+
reason: input.reason,
|
|
51
|
+
organization_id: this.auth.organization_id,
|
|
52
|
+
created_by: this.auth.mms_id,
|
|
53
|
+
created_at: new Date().toISOString(),
|
|
54
|
+
});
|
|
55
|
+
const [created] = (await this.list([leaveEntitlementId])).filter((adjustment) => adjustment.mms_id === row.mms_id);
|
|
56
|
+
return created;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.LeaveAdjustmentsResource = LeaveAdjustmentsResource;
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import type { MmsList } from '@managemint-solutions/entities/common';
|
|
2
2
|
import type { LeaveEntitlementEntity, LeavePeriod } from '@managemint-solutions/entities/leave';
|
|
3
|
+
import { LeaveGrantMethod } from '@managemint-solutions/entities/leave/enum';
|
|
3
4
|
import { type AuditActor, type AuditResource } from '../audit';
|
|
4
5
|
import type { TypedSupabaseClient } from '../client';
|
|
5
6
|
import type { GetLeaveEntitlementsDto, UpdateLeaveEntitlementDto } from './dto';
|
|
6
|
-
/**
|
|
7
|
-
export type LeaveEntitlementCover = LeavePeriod & {
|
|
8
|
-
mms_id: string;
|
|
9
|
-
days_entitled: number;
|
|
10
|
-
};
|
|
11
|
-
/** The create input after the api has resolved the period defaults. */
|
|
7
|
+
/** The create input after the api has resolved the defaults and copied the type's policy on. */
|
|
12
8
|
export type CreateLeaveEntitlementInput = LeavePeriod & {
|
|
13
9
|
user_id: string;
|
|
14
10
|
leave_type_id: string;
|
|
15
11
|
days_entitled: number;
|
|
12
|
+
days_carried: number;
|
|
13
|
+
carried_expires_on: string | null;
|
|
14
|
+
grant_method: LeaveGrantMethod;
|
|
15
|
+
initial_until: string | null;
|
|
16
|
+
initial_days_per_month: number | null;
|
|
16
17
|
max_carry_over_days: number;
|
|
17
18
|
notes?: string | null;
|
|
18
19
|
};
|
|
@@ -24,32 +25,37 @@ export declare class LeaveEntitlementsResource {
|
|
|
24
25
|
list(query: GetLeaveEntitlementsDto): Promise<MmsList<LeaveEntitlementEntity>>;
|
|
25
26
|
getById(leaveEntitlementId: string): Promise<LeaveEntitlementEntity>;
|
|
26
27
|
/**
|
|
27
|
-
* The entitlement of `userId` for `leaveTypeId` whose period contains `date`,
|
|
28
|
+
* The entitlement of `userId` for `leaveTypeId` whose period contains `date`, with its balance
|
|
29
|
+
* as it stands on that day - what a request starting then is checked against. Null when the
|
|
28
30
|
* type is not tracked for them then. The periods of one user and type cannot overlap (the
|
|
29
31
|
* table's exclusion constraint), so there is at most one.
|
|
30
32
|
*/
|
|
31
|
-
|
|
33
|
+
balanceOn(userId: string, leaveTypeId: string, date: string, excludeRequestId?: string): Promise<LeaveEntitlementEntity | null>;
|
|
32
34
|
/**
|
|
33
35
|
* Whether `userId` already has an entitlement of `leaveTypeId` touching any day of
|
|
34
36
|
* `start`..`end`. The readable half of the rule; the table's exclusion constraint is the half
|
|
35
|
-
* that survives a race.
|
|
37
|
+
* that survives a race.
|
|
36
38
|
*/
|
|
37
|
-
overlaps(userId: string, leaveTypeId: string, start: string, end: string
|
|
39
|
+
overlaps(userId: string, leaveTypeId: string, start: string, end: string): Promise<boolean>;
|
|
38
40
|
/**
|
|
39
|
-
* Every entitlement of the organization
|
|
40
|
-
*
|
|
41
|
-
*
|
|
41
|
+
* Every entitlement of the organization that ended before `today` and has no successor - no
|
|
42
|
+
* later period of the same user and type - balances included: what the rollover carries
|
|
43
|
+
* forward. An ended period keeps qualifying until its next one exists, so a missed run is
|
|
44
|
+
* caught up by the next. PostgREST cannot say "no later row exists", so this reads the ended
|
|
45
|
+
* rows and every row's start, and filters in code. Both reads walk the table in pages so a
|
|
46
|
+
* large tenant is not cut short by the row cap.
|
|
42
47
|
*/
|
|
43
|
-
|
|
48
|
+
endedWithoutSuccessor(today: string): Promise<LeaveEntitlementEntity[]>;
|
|
44
49
|
create(input: CreateLeaveEntitlementInput): Promise<LeaveEntitlementEntity>;
|
|
45
50
|
update(leaveEntitlementId: string, input: UpdateLeaveEntitlementDto): Promise<LeaveEntitlementEntity>;
|
|
46
51
|
/** Hard delete: the type simply becomes untracked for that user and period. */
|
|
47
52
|
remove(leaveEntitlementId: string): Promise<void>;
|
|
48
53
|
/**
|
|
49
|
-
* Adds the
|
|
50
|
-
* against any of them - the approved and pending ones of these users and
|
|
51
|
-
* the span the periods cover
|
|
52
|
-
*
|
|
54
|
+
* Adds the balance to each row (on `asOf`, today by default) from one query over the requests
|
|
55
|
+
* that could count against any of them - the approved and pending ones of these users and
|
|
56
|
+
* types, starting in the span the periods cover - and the adjustments embedded in the rows.
|
|
57
|
+
* Nothing is stored: a balance is always what the requests, the policy and the adjustments
|
|
58
|
+
* say. Paged, since `endedWithoutSuccessor` hands it a whole tenant at once.
|
|
53
59
|
*/
|
|
54
60
|
private withBalances;
|
|
55
61
|
}
|
|
@@ -6,6 +6,9 @@ const enum_2 = require("@managemint-solutions/entities/leave/enum");
|
|
|
6
6
|
const audit_1 = require("../audit");
|
|
7
7
|
const errors_1 = require("../errors");
|
|
8
8
|
const transforms_1 = require("../transforms");
|
|
9
|
+
const leave_policy_1 = require("./leave-policy");
|
|
10
|
+
const working_days_1 = require("./working-days");
|
|
11
|
+
// `adjustments` is embedded only to be summed into the balance; it never leaves the resource.
|
|
9
12
|
const LEAVE_ENTITLEMENT_SELECT = `
|
|
10
13
|
mms_id,
|
|
11
14
|
created_at,
|
|
@@ -16,14 +19,21 @@ const LEAVE_ENTITLEMENT_SELECT = `
|
|
|
16
19
|
leave_type:leave_types!leave_type_id (
|
|
17
20
|
mms_id,
|
|
18
21
|
name,
|
|
19
|
-
|
|
22
|
+
document_after_days
|
|
20
23
|
),
|
|
21
24
|
period_start,
|
|
22
25
|
period_end,
|
|
23
26
|
days_entitled,
|
|
27
|
+
days_carried,
|
|
28
|
+
carried_expires_on,
|
|
29
|
+
grant_method,
|
|
30
|
+
initial_until,
|
|
31
|
+
initial_days_per_month,
|
|
24
32
|
max_carry_over_days,
|
|
25
|
-
notes
|
|
33
|
+
notes,
|
|
34
|
+
adjustments:leave_adjustments (days)
|
|
26
35
|
`;
|
|
36
|
+
const numberOrNull = (value) => (value === null || value === undefined ? null : Number(value));
|
|
27
37
|
class LeaveEntitlementsResource {
|
|
28
38
|
supabase;
|
|
29
39
|
auth;
|
|
@@ -75,14 +85,15 @@ class LeaveEntitlementsResource {
|
|
|
75
85
|
return row;
|
|
76
86
|
}
|
|
77
87
|
/**
|
|
78
|
-
* The entitlement of `userId` for `leaveTypeId` whose period contains `date`,
|
|
88
|
+
* The entitlement of `userId` for `leaveTypeId` whose period contains `date`, with its balance
|
|
89
|
+
* as it stands on that day - what a request starting then is checked against. Null when the
|
|
79
90
|
* type is not tracked for them then. The periods of one user and type cannot overlap (the
|
|
80
91
|
* table's exclusion constraint), so there is at most one.
|
|
81
92
|
*/
|
|
82
|
-
async
|
|
93
|
+
async balanceOn(userId, leaveTypeId, date, excludeRequestId) {
|
|
83
94
|
const { data, error } = await this.supabase
|
|
84
95
|
.from('leave_entitlements')
|
|
85
|
-
.select(
|
|
96
|
+
.select(LEAVE_ENTITLEMENT_SELECT)
|
|
86
97
|
.eq('organization_id', this.auth.organization_id)
|
|
87
98
|
.eq('user_id', userId)
|
|
88
99
|
.eq('leave_type_id', leaveTypeId)
|
|
@@ -93,16 +104,16 @@ class LeaveEntitlementsResource {
|
|
|
93
104
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
94
105
|
if (!data)
|
|
95
106
|
return null;
|
|
96
|
-
const
|
|
97
|
-
return
|
|
107
|
+
const [row] = await this.withBalances([data], { asOf: date, excludeRequestId });
|
|
108
|
+
return row;
|
|
98
109
|
}
|
|
99
110
|
/**
|
|
100
111
|
* Whether `userId` already has an entitlement of `leaveTypeId` touching any day of
|
|
101
112
|
* `start`..`end`. The readable half of the rule; the table's exclusion constraint is the half
|
|
102
|
-
* that survives a race.
|
|
113
|
+
* that survives a race.
|
|
103
114
|
*/
|
|
104
|
-
async overlaps(userId, leaveTypeId, start, end
|
|
105
|
-
|
|
115
|
+
async overlaps(userId, leaveTypeId, start, end) {
|
|
116
|
+
const { count, error } = await this.supabase
|
|
106
117
|
.from('leave_entitlements')
|
|
107
118
|
.select('mms_id', { count: 'exact', head: true })
|
|
108
119
|
.eq('organization_id', this.auth.organization_id)
|
|
@@ -110,28 +121,44 @@ class LeaveEntitlementsResource {
|
|
|
110
121
|
.eq('leave_type_id', leaveTypeId)
|
|
111
122
|
.lte('period_start', end)
|
|
112
123
|
.gte('period_end', start);
|
|
113
|
-
if (excludeId) {
|
|
114
|
-
overlapQuery = overlapQuery.neq('mms_id', excludeId);
|
|
115
|
-
}
|
|
116
|
-
const { count, error } = await overlapQuery;
|
|
117
124
|
if (error)
|
|
118
125
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
119
126
|
return (count ?? 0) > 0;
|
|
120
127
|
}
|
|
121
128
|
/**
|
|
122
|
-
* Every entitlement of the organization
|
|
123
|
-
*
|
|
124
|
-
*
|
|
129
|
+
* Every entitlement of the organization that ended before `today` and has no successor - no
|
|
130
|
+
* later period of the same user and type - balances included: what the rollover carries
|
|
131
|
+
* forward. An ended period keeps qualifying until its next one exists, so a missed run is
|
|
132
|
+
* caught up by the next. PostgREST cannot say "no later row exists", so this reads the ended
|
|
133
|
+
* rows and every row's start, and filters in code. Both reads walk the table in pages so a
|
|
134
|
+
* large tenant is not cut short by the row cap.
|
|
125
135
|
*/
|
|
126
|
-
async
|
|
127
|
-
const
|
|
136
|
+
async endedWithoutSuccessor(today) {
|
|
137
|
+
const ended = await (0, transforms_1.allRows)((from, to) => this.supabase
|
|
128
138
|
.from('leave_entitlements')
|
|
129
139
|
.select(LEAVE_ENTITLEMENT_SELECT)
|
|
130
140
|
.eq('organization_id', this.auth.organization_id)
|
|
131
|
-
.
|
|
141
|
+
.lt('period_end', today)
|
|
142
|
+
.order('mms_id')
|
|
143
|
+
.range(from, to));
|
|
144
|
+
if (ended.length === 0)
|
|
145
|
+
return [];
|
|
146
|
+
const starts = await (0, transforms_1.allRows)((from, to) => this.supabase
|
|
147
|
+
.from('leave_entitlements')
|
|
148
|
+
.select('user_id, leave_type_id, period_start')
|
|
149
|
+
.eq('organization_id', this.auth.organization_id)
|
|
132
150
|
.order('mms_id')
|
|
133
151
|
.range(from, to));
|
|
134
|
-
|
|
152
|
+
// The periods of one user and type never overlap, so a row has a successor exactly when the
|
|
153
|
+
// latest start of its user and type falls after its end.
|
|
154
|
+
const latestStart = new Map();
|
|
155
|
+
for (const row of starts) {
|
|
156
|
+
const key = `${row.user_id}:${row.leave_type_id}`;
|
|
157
|
+
const latest = latestStart.get(key);
|
|
158
|
+
if (latest === undefined || row.period_start > latest)
|
|
159
|
+
latestStart.set(key, row.period_start);
|
|
160
|
+
}
|
|
161
|
+
return this.withBalances(ended.filter((row) => (latestStart.get(`${row.user.mms_id}:${row.leave_type.mms_id}`) ?? '') <= row.period_end));
|
|
135
162
|
}
|
|
136
163
|
async create(input) {
|
|
137
164
|
const row = await this.table.insert({
|
|
@@ -140,6 +167,11 @@ class LeaveEntitlementsResource {
|
|
|
140
167
|
period_start: input.period_start,
|
|
141
168
|
period_end: input.period_end,
|
|
142
169
|
days_entitled: input.days_entitled,
|
|
170
|
+
days_carried: input.days_carried,
|
|
171
|
+
carried_expires_on: input.carried_expires_on,
|
|
172
|
+
grant_method: input.grant_method,
|
|
173
|
+
initial_until: input.initial_until,
|
|
174
|
+
initial_days_per_month: input.initial_days_per_month,
|
|
143
175
|
max_carry_over_days: input.max_carry_over_days,
|
|
144
176
|
notes: input.notes ?? null,
|
|
145
177
|
organization_id: this.auth.organization_id,
|
|
@@ -151,8 +183,9 @@ class LeaveEntitlementsResource {
|
|
|
151
183
|
async update(leaveEntitlementId, input) {
|
|
152
184
|
await this.table.update(leaveEntitlementId, {
|
|
153
185
|
days_entitled: input.days_entitled,
|
|
186
|
+
days_carried: input.days_carried,
|
|
187
|
+
carried_expires_on: input.carried_expires_on,
|
|
154
188
|
max_carry_over_days: input.max_carry_over_days,
|
|
155
|
-
period_end: input.period_end,
|
|
156
189
|
notes: input.notes,
|
|
157
190
|
last_updated_by: this.auth.mms_id,
|
|
158
191
|
updated_at: new Date().toISOString(),
|
|
@@ -164,21 +197,23 @@ class LeaveEntitlementsResource {
|
|
|
164
197
|
await this.table.remove(leaveEntitlementId);
|
|
165
198
|
}
|
|
166
199
|
/**
|
|
167
|
-
* Adds the
|
|
168
|
-
* against any of them - the approved and pending ones of these users and
|
|
169
|
-
* the span the periods cover
|
|
170
|
-
*
|
|
200
|
+
* Adds the balance to each row (on `asOf`, today by default) from one query over the requests
|
|
201
|
+
* that could count against any of them - the approved and pending ones of these users and
|
|
202
|
+
* types, starting in the span the periods cover - and the adjustments embedded in the rows.
|
|
203
|
+
* Nothing is stored: a balance is always what the requests, the policy and the adjustments
|
|
204
|
+
* say. Paged, since `endedWithoutSuccessor` hands it a whole tenant at once.
|
|
171
205
|
*/
|
|
172
|
-
async withBalances(rows) {
|
|
206
|
+
async withBalances(rows, options = {}) {
|
|
173
207
|
if (rows.length === 0)
|
|
174
208
|
return [];
|
|
209
|
+
const asOf = options.asOf ?? (0, working_days_1.todayInJohannesburg)();
|
|
175
210
|
const userIds = [...new Set(rows.map((row) => row.user.mms_id))];
|
|
176
211
|
const typeIds = [...new Set(rows.map((row) => row.leave_type.mms_id))];
|
|
177
212
|
const earliest = rows.map((row) => row.period_start).sort()[0];
|
|
178
213
|
const latest = rows.map((row) => row.period_end).sort().reverse()[0];
|
|
179
214
|
const requests = await (0, transforms_1.allRows)((from, to) => this.supabase
|
|
180
215
|
.from('leave_requests')
|
|
181
|
-
.select('user_id, leave_type_id, start_date, days, status')
|
|
216
|
+
.select('mms_id, user_id, leave_type_id, start_date, days, status')
|
|
182
217
|
.eq('organization_id', this.auth.organization_id)
|
|
183
218
|
.in('user_id', userIds)
|
|
184
219
|
.in('leave_type_id', typeIds)
|
|
@@ -187,25 +222,22 @@ class LeaveEntitlementsResource {
|
|
|
187
222
|
.lte('start_date', latest)
|
|
188
223
|
.order('mms_id')
|
|
189
224
|
.range(from, to));
|
|
190
|
-
return rows.map((row) => {
|
|
191
|
-
const
|
|
192
|
-
request.leave_type_id === row.leave_type.mms_id &&
|
|
193
|
-
request.start_date >= row.period_start &&
|
|
194
|
-
request.start_date <= row.period_end);
|
|
195
|
-
const sum = (status) => mine
|
|
196
|
-
.filter((request) => request.status === status)
|
|
197
|
-
.reduce((total, request) => total + Number(request.days), 0);
|
|
198
|
-
const daysEntitled = Number(row.days_entitled);
|
|
199
|
-
const daysUsed = sum(enum_2.LeaveRequestStatus.APPROVED);
|
|
200
|
-
const daysPending = sum(enum_2.LeaveRequestStatus.PENDING);
|
|
201
|
-
return {
|
|
225
|
+
return rows.map(({ adjustments, ...row }) => {
|
|
226
|
+
const entitlement = {
|
|
202
227
|
...row,
|
|
203
|
-
days_entitled:
|
|
228
|
+
days_entitled: Number(row.days_entitled),
|
|
229
|
+
days_carried: Number(row.days_carried),
|
|
230
|
+
initial_days_per_month: numberOrNull(row.initial_days_per_month),
|
|
204
231
|
max_carry_over_days: Number(row.max_carry_over_days),
|
|
205
|
-
|
|
206
|
-
days_pending: daysPending,
|
|
207
|
-
days_remaining: daysEntitled - daysUsed - daysPending,
|
|
232
|
+
leave_type: { ...row.leave_type, document_after_days: numberOrNull(row.leave_type.document_after_days) },
|
|
208
233
|
};
|
|
234
|
+
const mine = requests.filter((request) => (!options.excludeRequestId || request.mms_id !== options.excludeRequestId) &&
|
|
235
|
+
request.user_id === row.user.mms_id &&
|
|
236
|
+
request.leave_type_id === row.leave_type.mms_id &&
|
|
237
|
+
request.start_date >= row.period_start &&
|
|
238
|
+
request.start_date <= row.period_end);
|
|
239
|
+
const daysAdjusted = (adjustments ?? []).reduce((total, adjustment) => total + Number(adjustment.days), 0);
|
|
240
|
+
return { ...entitlement, ...(0, leave_policy_1.balanceOf)(entitlement, mine, daysAdjusted, asOf) };
|
|
209
241
|
});
|
|
210
242
|
}
|
|
211
243
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { LeaveBalance, LeavePeriod, LeaveTypeEntity } from '@managemint-solutions/entities/leave';
|
|
2
|
+
import { LeaveGrantMethod, LeaveRequestStatus } from '@managemint-solutions/entities/leave/enum';
|
|
3
|
+
/** The entitlement columns a balance is worked out from - the type's policy as it was copied on. */
|
|
4
|
+
export type BalanceEntitlement = LeavePeriod & {
|
|
5
|
+
days_entitled: number;
|
|
6
|
+
days_carried: number;
|
|
7
|
+
carried_expires_on: string | null;
|
|
8
|
+
grant_method: LeaveGrantMethod;
|
|
9
|
+
initial_until: string | null;
|
|
10
|
+
initial_days_per_month: number | null;
|
|
11
|
+
};
|
|
12
|
+
/** The request columns that count against a balance. */
|
|
13
|
+
export type BalanceRequest = {
|
|
14
|
+
start_date: string;
|
|
15
|
+
days: number;
|
|
16
|
+
status: LeaveRequestStatus | string;
|
|
17
|
+
};
|
|
18
|
+
/** A cycle of a type, and the part of it a person's entitlement covers (from their start date, if later). */
|
|
19
|
+
export type LeaveCycle = {
|
|
20
|
+
cycle: LeavePeriod;
|
|
21
|
+
period: LeavePeriod;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The cycle of `type` containing `date` for someone who started on `employmentStart`. Every
|
|
25
|
+
* cycle is `cycle_years` leave years starting on the first of the organization's leave-year
|
|
26
|
+
* month (`startMonth`, 1-12); the first one starts with the leave year the start date falls
|
|
27
|
+
* in. `period` starts no earlier than the start date, so a joiner's first period is the rest
|
|
28
|
+
* of the cycle. A March start puts 2026-02-10 in the leave year that began 2025-03-01.
|
|
29
|
+
*/
|
|
30
|
+
export declare const cyclePeriodFor: (type: Pick<LeaveTypeEntity, "cycle_years">, employmentStart: string, date: string, startMonth: number) => LeaveCycle;
|
|
31
|
+
/** A full cycle's days scaled to the part of it `period` covers, to the half-day precision of the column. */
|
|
32
|
+
export declare const proRataDays: (defaultDays: number, { cycle, period }: LeaveCycle) => number;
|
|
33
|
+
/**
|
|
34
|
+
* The last day of the first-months rate for an entitlement over `period`: `initialMonths`
|
|
35
|
+
* from the start date, or null when that is over before the period begins.
|
|
36
|
+
*/
|
|
37
|
+
export declare const initialUntil: (employmentStart: string, initialMonths: number, period: LeavePeriod) => string | null;
|
|
38
|
+
/**
|
|
39
|
+
* The part of `days_entitled` earned by the end of `asOf`. Upfront: everything from the first
|
|
40
|
+
* day. Monthly: an equal share per completed month (a short last month earns its fraction on
|
|
41
|
+
* the period's final day). Either way, inside the first-months window the total is capped at
|
|
42
|
+
* `initial_days_per_month` per completed month.
|
|
43
|
+
*/
|
|
44
|
+
export declare const accruedDays: (entitlement: BalanceEntitlement, asOf: string) => number;
|
|
45
|
+
/**
|
|
46
|
+
* The balance of an entitlement on `asOf`, from the live requests starting inside its period
|
|
47
|
+
* and the sum of its adjustments. Carried days are spent first: whatever of them the requests
|
|
48
|
+
* starting on or before `carried_expires_on` did not use is forfeited once that day has passed.
|
|
49
|
+
*/
|
|
50
|
+
export declare const balanceOf: (entitlement: BalanceEntitlement, requests: BalanceRequest[], daysAdjusted: number, asOf: string) => LeaveBalance;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.balanceOf = exports.accruedDays = exports.initialUntil = exports.proRataDays = exports.cyclePeriodFor = void 0;
|
|
4
|
+
const enum_1 = require("@managemint-solutions/entities/leave/enum");
|
|
5
|
+
const working_days_1 = require("./working-days");
|
|
6
|
+
const round = (value, places) => Number(value.toFixed(places));
|
|
7
|
+
const daysIn = (period) => (0, working_days_1.eachDate)(period.period_start, period.period_end).length;
|
|
8
|
+
/**
|
|
9
|
+
* The cycle of `type` containing `date` for someone who started on `employmentStart`. Every
|
|
10
|
+
* cycle is `cycle_years` leave years starting on the first of the organization's leave-year
|
|
11
|
+
* month (`startMonth`, 1-12); the first one starts with the leave year the start date falls
|
|
12
|
+
* in. `period` starts no earlier than the start date, so a joiner's first period is the rest
|
|
13
|
+
* of the cycle. A March start puts 2026-02-10 in the leave year that began 2025-03-01.
|
|
14
|
+
*/
|
|
15
|
+
const cyclePeriodFor = (type, employmentStart, date, startMonth) => {
|
|
16
|
+
// The calendar year each leave year starts in: a date before the leave-year month belongs
|
|
17
|
+
// to the leave year that began the calendar year before.
|
|
18
|
+
const firstYear = Number(employmentStart.slice(0, 4)) - (Number(employmentStart.slice(5, 7)) < startMonth ? 1 : 0);
|
|
19
|
+
const dateYear = Number(date.slice(0, 4)) - (Number(date.slice(5, 7)) < startMonth ? 1 : 0);
|
|
20
|
+
const index = Math.floor((dateYear - firstYear) / type.cycle_years);
|
|
21
|
+
const cycleStart = `${firstYear + index * type.cycle_years}-${String(startMonth).padStart(2, '0')}-01`;
|
|
22
|
+
const cycle = {
|
|
23
|
+
period_start: cycleStart,
|
|
24
|
+
period_end: (0, working_days_1.shiftDays)((0, working_days_1.addMonths)(cycleStart, type.cycle_years * 12), -1),
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
cycle,
|
|
28
|
+
period: {
|
|
29
|
+
period_start: cycle.period_start < employmentStart ? employmentStart : cycle.period_start,
|
|
30
|
+
period_end: cycle.period_end,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
exports.cyclePeriodFor = cyclePeriodFor;
|
|
35
|
+
/** A full cycle's days scaled to the part of it `period` covers, to the half-day precision of the column. */
|
|
36
|
+
const proRataDays = (defaultDays, { cycle, period }) => round((defaultDays * daysIn(period)) / daysIn(cycle), 1);
|
|
37
|
+
exports.proRataDays = proRataDays;
|
|
38
|
+
/**
|
|
39
|
+
* The last day of the first-months rate for an entitlement over `period`: `initialMonths`
|
|
40
|
+
* from the start date, or null when that is over before the period begins.
|
|
41
|
+
*/
|
|
42
|
+
const initialUntil = (employmentStart, initialMonths, period) => {
|
|
43
|
+
if (initialMonths <= 0)
|
|
44
|
+
return null;
|
|
45
|
+
const until = (0, working_days_1.shiftDays)((0, working_days_1.addMonths)(employmentStart, initialMonths), -1);
|
|
46
|
+
return until >= period.period_start ? until : null;
|
|
47
|
+
};
|
|
48
|
+
exports.initialUntil = initialUntil;
|
|
49
|
+
/**
|
|
50
|
+
* The part of `days_entitled` earned by the end of `asOf`. Upfront: everything from the first
|
|
51
|
+
* day. Monthly: an equal share per completed month (a short last month earns its fraction on
|
|
52
|
+
* the period's final day). Either way, inside the first-months window the total is capped at
|
|
53
|
+
* `initial_days_per_month` per completed month.
|
|
54
|
+
*/
|
|
55
|
+
const accruedDays = (entitlement, asOf) => {
|
|
56
|
+
const { period_start, period_end, days_entitled } = entitlement;
|
|
57
|
+
if (asOf < period_start)
|
|
58
|
+
return 0;
|
|
59
|
+
let earned = days_entitled;
|
|
60
|
+
if (entitlement.grant_method === enum_1.LeaveGrantMethod.MONTHLY && asOf < period_end) {
|
|
61
|
+
const whole = (0, working_days_1.completedMonths)(period_start, period_end);
|
|
62
|
+
const lastStart = (0, working_days_1.addMonths)(period_start, whole);
|
|
63
|
+
const lastLength = daysIn({ period_start: lastStart, period_end: (0, working_days_1.shiftDays)((0, working_days_1.addMonths)(period_start, whole + 1), -1) });
|
|
64
|
+
const tail = lastStart <= period_end ? daysIn({ period_start: lastStart, period_end }) / lastLength : 0;
|
|
65
|
+
earned = (days_entitled * (0, working_days_1.completedMonths)(period_start, asOf)) / (whole + tail);
|
|
66
|
+
}
|
|
67
|
+
if (entitlement.initial_until && entitlement.initial_days_per_month !== null && asOf <= entitlement.initial_until) {
|
|
68
|
+
earned = Math.min(earned, entitlement.initial_days_per_month * (0, working_days_1.completedMonths)(period_start, asOf));
|
|
69
|
+
}
|
|
70
|
+
return round(Math.min(earned, days_entitled), 2);
|
|
71
|
+
};
|
|
72
|
+
exports.accruedDays = accruedDays;
|
|
73
|
+
/**
|
|
74
|
+
* The balance of an entitlement on `asOf`, from the live requests starting inside its period
|
|
75
|
+
* and the sum of its adjustments. Carried days are spent first: whatever of them the requests
|
|
76
|
+
* starting on or before `carried_expires_on` did not use is forfeited once that day has passed.
|
|
77
|
+
*/
|
|
78
|
+
const balanceOf = (entitlement, requests, daysAdjusted, asOf) => {
|
|
79
|
+
const live = requests.filter((request) => request.status === enum_1.LeaveRequestStatus.APPROVED || request.status === enum_1.LeaveRequestStatus.PENDING);
|
|
80
|
+
const sum = (rows) => rows.reduce((total, request) => total + Number(request.days), 0);
|
|
81
|
+
const daysUsed = sum(live.filter((request) => request.status === enum_1.LeaveRequestStatus.APPROVED));
|
|
82
|
+
const daysPending = sum(live.filter((request) => request.status === enum_1.LeaveRequestStatus.PENDING));
|
|
83
|
+
const expiry = entitlement.carried_expires_on;
|
|
84
|
+
const daysForfeited = expiry && asOf > expiry
|
|
85
|
+
? Math.max(entitlement.days_carried - sum(live.filter((request) => request.start_date <= expiry)), 0)
|
|
86
|
+
: 0;
|
|
87
|
+
const daysAccrued = (0, exports.accruedDays)(entitlement, asOf);
|
|
88
|
+
return {
|
|
89
|
+
as_of: asOf,
|
|
90
|
+
days_accrued: daysAccrued,
|
|
91
|
+
days_forfeited: round(daysForfeited, 2),
|
|
92
|
+
days_adjusted: round(daysAdjusted, 2),
|
|
93
|
+
days_used: round(daysUsed, 2),
|
|
94
|
+
days_pending: round(daysPending, 2),
|
|
95
|
+
days_remaining: round(daysAccrued + entitlement.days_carried - daysForfeited + daysAdjusted - daysUsed - daysPending, 2),
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
exports.balanceOf = balanceOf;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { MmsList } from '@managemint-solutions/entities/common';
|
|
2
|
-
import type {
|
|
2
|
+
import type { LeaveCalendarEntry, LeaveRequestEntity } from '@managemint-solutions/entities/leave';
|
|
3
3
|
import { LeaveRequestStatus, LeaveReviewDecision } from '@managemint-solutions/entities/leave/enum';
|
|
4
4
|
import { type AuditActor, type AuditResource } from '../audit';
|
|
5
5
|
import type { TypedSupabaseClient } from '../client';
|
|
6
|
-
import type { GetLeaveRequestsDto } from './dto';
|
|
6
|
+
import type { GetLeaveCalendarDto, GetLeaveRequestsDto } from './dto';
|
|
7
7
|
/** A request as the api inserts it: the caller's own, with the days already counted. */
|
|
8
8
|
export type CreateLeaveRequestRow = {
|
|
9
9
|
user_id: string;
|
|
@@ -47,12 +47,8 @@ export declare class LeaveRequestsResource {
|
|
|
47
47
|
* half of the rule; the table's exclusion constraint is the half that survives a race.
|
|
48
48
|
*/
|
|
49
49
|
overlaps(userId: string, start: string, end: string, excludeId?: string): Promise<boolean>;
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
* what an entitlement's balance is checked against. `excludeId` leaves out the request being
|
|
53
|
-
* edited or approved so it is not counted against itself.
|
|
54
|
-
*/
|
|
55
|
-
sumDays(userId: string, leaveTypeId: string, period: LeavePeriod, statuses: LeaveRequestStatus[], excludeId?: string): Promise<number>;
|
|
50
|
+
/** Live requests touching `date_from`..`date_to`, for everyone in the organization - the leave calendar. */
|
|
51
|
+
calendar(query: GetLeaveCalendarDto): Promise<LeaveCalendarEntry[]>;
|
|
56
52
|
create(input: CreateLeaveRequestRow): Promise<LeaveRequestEntity>;
|
|
57
53
|
/** Edits a request while it is still pending; null when it no longer is. */
|
|
58
54
|
update(leaveRequestId: string, input: UpdateLeaveRequestRow): Promise<LeaveRequestEntity | null>;
|
|
@@ -62,7 +58,10 @@ export declare class LeaveRequestsResource {
|
|
|
62
58
|
* can say so instead of overwriting their decision.
|
|
63
59
|
*/
|
|
64
60
|
review(leaveRequestId: string, input: ReviewLeaveRequestRow): Promise<LeaveRequestEntity | null>;
|
|
65
|
-
/**
|
|
66
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Cancels a request that is still in `expected` - the status the api read and decided on.
|
|
63
|
+
* Null when it has moved on since (reviewed, or cancelled by someone else).
|
|
64
|
+
*/
|
|
65
|
+
cancel(leaveRequestId: string, cancelledBy: string, expected: LeaveRequestStatus): Promise<LeaveRequestEntity | null>;
|
|
67
66
|
}
|
|
68
67
|
export {};
|