@managemint-solutions/sdk 0.35.0 → 0.37.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 +82 -15
- package/dist/cache/index.d.ts +28 -22
- package/dist/cache/index.js +36 -29
- package/dist/client.d.ts +6 -0
- package/dist/client.js +11 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +25 -1
- package/dist/leave/dto.d.ts +75 -0
- package/dist/leave/dto.js +375 -0
- package/dist/leave/index.d.ts +5 -0
- package/dist/leave/index.js +29 -0
- package/dist/leave/leave-entitlements.d.ts +42 -0
- package/dist/leave/leave-entitlements.js +170 -0
- package/dist/leave/leave-requests.d.ts +69 -0
- package/dist/leave/leave-requests.js +181 -0
- package/dist/leave/leave-types.d.ts +21 -0
- package/dist/leave/leave-types.js +95 -0
- package/dist/leave/working-days.d.ts +29 -0
- package/dist/leave/working-days.js +80 -0
- package/dist/permissions/dto.d.ts +12 -0
- package/dist/permissions/dto.js +60 -24
- package/dist/public-holidays/dto.d.ts +4 -0
- package/dist/public-holidays/dto.js +30 -0
- package/dist/public-holidays/index.d.ts +21 -0
- package/dist/public-holidays/index.js +63 -0
- package/dist/users/index.d.ts +2 -0
- package/dist/users/index.js +11 -0
- package/dist/validators.d.ts +8 -0
- package/dist/validators.js +32 -0
- package/package.json +2 -2
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { MmsList } from '@managemint-solutions/entities/common';
|
|
2
|
+
import type { LeavePeriod, LeaveRequestEntity } from '@managemint-solutions/entities/leave';
|
|
3
|
+
import { LeaveRequestStatus, LeaveReviewDecision } from '@managemint-solutions/entities/leave/enum';
|
|
4
|
+
import type { AuthContext } from '../auth';
|
|
5
|
+
import { type AuditResource } from '../audit';
|
|
6
|
+
import type { TypedSupabaseClient } from '../client';
|
|
7
|
+
import type { GetLeaveRequestsDto } from './dto';
|
|
8
|
+
/** A request as the api inserts it: the caller's own, with the days already counted. */
|
|
9
|
+
export type CreateLeaveRequestRow = {
|
|
10
|
+
user_id: string;
|
|
11
|
+
leave_type_id: string;
|
|
12
|
+
start_date: string;
|
|
13
|
+
end_date: string;
|
|
14
|
+
is_half_day: boolean;
|
|
15
|
+
days: number;
|
|
16
|
+
reason: string | null;
|
|
17
|
+
status: LeaveRequestStatus;
|
|
18
|
+
/** Set on an auto-approved request; `reviewed_by` stays null to say nobody reviewed it. */
|
|
19
|
+
reviewed_at?: string | null;
|
|
20
|
+
};
|
|
21
|
+
/** The recounted fields of a pending request being edited. */
|
|
22
|
+
export type UpdateLeaveRequestRow = {
|
|
23
|
+
leave_type_id: string;
|
|
24
|
+
start_date: string;
|
|
25
|
+
end_date: string;
|
|
26
|
+
is_half_day: boolean;
|
|
27
|
+
days: number;
|
|
28
|
+
reason: string | null;
|
|
29
|
+
};
|
|
30
|
+
export type ReviewLeaveRequestRow = {
|
|
31
|
+
status: LeaveReviewDecision;
|
|
32
|
+
reviewed_by: string;
|
|
33
|
+
review_note: string | null;
|
|
34
|
+
};
|
|
35
|
+
/** The lists share a filter set; `userIds` is the visibility the api resolved for the caller. */
|
|
36
|
+
type ListOptions = {
|
|
37
|
+
userIds?: string[];
|
|
38
|
+
};
|
|
39
|
+
export declare class LeaveRequestsResource {
|
|
40
|
+
private readonly supabase;
|
|
41
|
+
private readonly auth;
|
|
42
|
+
private readonly table;
|
|
43
|
+
constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
|
|
44
|
+
list(query: GetLeaveRequestsDto, options?: ListOptions): Promise<MmsList<LeaveRequestEntity>>;
|
|
45
|
+
getById(leaveRequestId: string): Promise<LeaveRequestEntity>;
|
|
46
|
+
/**
|
|
47
|
+
* Whether `userId` already has a live request touching any day of `start`..`end`. The readable
|
|
48
|
+
* half of the rule; the table's exclusion constraint is the half that survives a race.
|
|
49
|
+
*/
|
|
50
|
+
overlaps(userId: string, start: string, end: string, excludeId?: string): Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* The days of `userId`'s requests of `leaveTypeId` in `statuses` that start inside `period` -
|
|
53
|
+
* what an entitlement's balance is checked against. `excludeId` leaves out the request being
|
|
54
|
+
* edited or approved so it is not counted against itself.
|
|
55
|
+
*/
|
|
56
|
+
sumDays(userId: string, leaveTypeId: string, period: LeavePeriod, statuses: LeaveRequestStatus[], excludeId?: string): Promise<number>;
|
|
57
|
+
create(input: CreateLeaveRequestRow): Promise<LeaveRequestEntity>;
|
|
58
|
+
/** Edits a request while it is still pending; null when it no longer is. */
|
|
59
|
+
update(leaveRequestId: string, input: UpdateLeaveRequestRow): Promise<LeaveRequestEntity | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Approves or rejects a pending request. Resolves to null when the request was no longer
|
|
62
|
+
* pending - another reviewer got there first, or the requester cancelled - so the caller
|
|
63
|
+
* can say so instead of overwriting their decision.
|
|
64
|
+
*/
|
|
65
|
+
review(leaveRequestId: string, input: ReviewLeaveRequestRow): Promise<LeaveRequestEntity | null>;
|
|
66
|
+
/** Cancels a pending or approved request; null when it was already closed. */
|
|
67
|
+
cancel(leaveRequestId: string, cancelledBy: string): Promise<LeaveRequestEntity | null>;
|
|
68
|
+
}
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LeaveRequestsResource = void 0;
|
|
4
|
+
const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
5
|
+
const enum_2 = require("@managemint-solutions/entities/leave/enum");
|
|
6
|
+
const audit_1 = require("../audit");
|
|
7
|
+
const errors_1 = require("../errors");
|
|
8
|
+
const transforms_1 = require("../transforms");
|
|
9
|
+
const LEAVE_REQUEST_SELECT = `
|
|
10
|
+
mms_id,
|
|
11
|
+
created_at,
|
|
12
|
+
created_by:user_profiles!created_by (*),
|
|
13
|
+
updated_at,
|
|
14
|
+
last_updated_by:user_profiles!last_updated_by (*),
|
|
15
|
+
user:user_profiles!user_id (*),
|
|
16
|
+
leave_type:leave_types!leave_type_id (
|
|
17
|
+
mms_id,
|
|
18
|
+
name
|
|
19
|
+
),
|
|
20
|
+
start_date,
|
|
21
|
+
end_date,
|
|
22
|
+
is_half_day,
|
|
23
|
+
days,
|
|
24
|
+
reason,
|
|
25
|
+
status,
|
|
26
|
+
reviewed_by:user_profiles!reviewed_by (*),
|
|
27
|
+
reviewed_at,
|
|
28
|
+
review_note,
|
|
29
|
+
cancelled_by:user_profiles!cancelled_by (*),
|
|
30
|
+
cancelled_at
|
|
31
|
+
`;
|
|
32
|
+
/** The statuses that hold days: a pending request reserves them, an approved one uses them. */
|
|
33
|
+
const LIVE_STATUSES = [enum_2.LeaveRequestStatus.PENDING, enum_2.LeaveRequestStatus.APPROVED];
|
|
34
|
+
class LeaveRequestsResource {
|
|
35
|
+
supabase;
|
|
36
|
+
auth;
|
|
37
|
+
table;
|
|
38
|
+
constructor(supabase, auth, audit) {
|
|
39
|
+
this.supabase = supabase;
|
|
40
|
+
this.auth = auth;
|
|
41
|
+
this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
|
|
42
|
+
table: 'leave_requests',
|
|
43
|
+
entityType: enum_1.AuditTrailEntityType.LEAVE_REQUESTS,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async list(query, options = {}) {
|
|
47
|
+
const page = query.page;
|
|
48
|
+
const pageSize = query.page_size;
|
|
49
|
+
const offset = (page - 1) * pageSize;
|
|
50
|
+
let requestsQuery = this.supabase
|
|
51
|
+
.from('leave_requests')
|
|
52
|
+
.select(LEAVE_REQUEST_SELECT, { count: 'estimated' })
|
|
53
|
+
.eq('organization_id', this.auth.organization_id)
|
|
54
|
+
.order('start_date', { ascending: false })
|
|
55
|
+
.order('created_at', { ascending: false })
|
|
56
|
+
.range(offset, offset + pageSize - 1);
|
|
57
|
+
if (options.userIds) {
|
|
58
|
+
requestsQuery = requestsQuery.in('user_id', options.userIds);
|
|
59
|
+
}
|
|
60
|
+
if (query.user_id) {
|
|
61
|
+
requestsQuery = requestsQuery.eq('user_id', query.user_id);
|
|
62
|
+
}
|
|
63
|
+
if (query.status) {
|
|
64
|
+
requestsQuery = requestsQuery.eq('status', query.status);
|
|
65
|
+
}
|
|
66
|
+
if (query.start_date_after) {
|
|
67
|
+
requestsQuery = requestsQuery.gte('start_date', query.start_date_after);
|
|
68
|
+
}
|
|
69
|
+
if (query.start_date_before) {
|
|
70
|
+
requestsQuery = requestsQuery.lte('start_date', query.start_date_before);
|
|
71
|
+
}
|
|
72
|
+
const { data, error, count } = await requestsQuery;
|
|
73
|
+
if (error)
|
|
74
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
75
|
+
return (0, transforms_1.toMmsList)(data, page, pageSize, count);
|
|
76
|
+
}
|
|
77
|
+
async getById(leaveRequestId) {
|
|
78
|
+
const { data, error } = await this.supabase
|
|
79
|
+
.from('leave_requests')
|
|
80
|
+
.select(LEAVE_REQUEST_SELECT)
|
|
81
|
+
.eq('mms_id', leaveRequestId)
|
|
82
|
+
.eq('organization_id', this.auth.organization_id)
|
|
83
|
+
.single();
|
|
84
|
+
if (error)
|
|
85
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
86
|
+
return data;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether `userId` already has a live request touching any day of `start`..`end`. The readable
|
|
90
|
+
* half of the rule; the table's exclusion constraint is the half that survives a race.
|
|
91
|
+
*/
|
|
92
|
+
async overlaps(userId, start, end, excludeId) {
|
|
93
|
+
let overlapQuery = this.supabase
|
|
94
|
+
.from('leave_requests')
|
|
95
|
+
.select('mms_id', { count: 'exact', head: true })
|
|
96
|
+
.eq('organization_id', this.auth.organization_id)
|
|
97
|
+
.eq('user_id', userId)
|
|
98
|
+
.in('status', LIVE_STATUSES)
|
|
99
|
+
.lte('start_date', end)
|
|
100
|
+
.gte('end_date', start);
|
|
101
|
+
if (excludeId) {
|
|
102
|
+
overlapQuery = overlapQuery.neq('mms_id', excludeId);
|
|
103
|
+
}
|
|
104
|
+
const { count, error } = await overlapQuery;
|
|
105
|
+
if (error)
|
|
106
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
107
|
+
return (count ?? 0) > 0;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The days of `userId`'s requests of `leaveTypeId` in `statuses` that start inside `period` -
|
|
111
|
+
* what an entitlement's balance is checked against. `excludeId` leaves out the request being
|
|
112
|
+
* edited or approved so it is not counted against itself.
|
|
113
|
+
*/
|
|
114
|
+
async sumDays(userId, leaveTypeId, period, statuses, excludeId) {
|
|
115
|
+
let daysQuery = this.supabase
|
|
116
|
+
.from('leave_requests')
|
|
117
|
+
.select('days')
|
|
118
|
+
.eq('organization_id', this.auth.organization_id)
|
|
119
|
+
.eq('user_id', userId)
|
|
120
|
+
.eq('leave_type_id', leaveTypeId)
|
|
121
|
+
.in('status', statuses)
|
|
122
|
+
.gte('start_date', period.period_start)
|
|
123
|
+
.lte('start_date', period.period_end);
|
|
124
|
+
if (excludeId) {
|
|
125
|
+
daysQuery = daysQuery.neq('mms_id', excludeId);
|
|
126
|
+
}
|
|
127
|
+
const { data, error } = await daysQuery;
|
|
128
|
+
if (error)
|
|
129
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
130
|
+
return (data ?? []).reduce((total, row) => total + Number(row.days), 0);
|
|
131
|
+
}
|
|
132
|
+
async create(input) {
|
|
133
|
+
const row = await this.table.insert({
|
|
134
|
+
...input,
|
|
135
|
+
reviewed_at: input.reviewed_at ?? null,
|
|
136
|
+
organization_id: this.auth.organization_id,
|
|
137
|
+
created_by: this.auth.mms_id,
|
|
138
|
+
created_at: new Date().toISOString(),
|
|
139
|
+
});
|
|
140
|
+
return this.getById(row.mms_id);
|
|
141
|
+
}
|
|
142
|
+
/** Edits a request while it is still pending; null when it no longer is. */
|
|
143
|
+
async update(leaveRequestId, input) {
|
|
144
|
+
const row = await this.table.updateWhere(leaveRequestId, (query) => query.eq('status', enum_2.LeaveRequestStatus.PENDING), {
|
|
145
|
+
...input,
|
|
146
|
+
last_updated_by: this.auth.mms_id,
|
|
147
|
+
updated_at: new Date().toISOString(),
|
|
148
|
+
});
|
|
149
|
+
return row ? this.getById(leaveRequestId) : null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Approves or rejects a pending request. Resolves to null when the request was no longer
|
|
153
|
+
* pending - another reviewer got there first, or the requester cancelled - so the caller
|
|
154
|
+
* can say so instead of overwriting their decision.
|
|
155
|
+
*/
|
|
156
|
+
async review(leaveRequestId, input) {
|
|
157
|
+
const now = new Date().toISOString();
|
|
158
|
+
const row = await this.table.updateWhere(leaveRequestId, (query) => query.eq('status', enum_2.LeaveRequestStatus.PENDING), {
|
|
159
|
+
status: input.status,
|
|
160
|
+
reviewed_by: input.reviewed_by,
|
|
161
|
+
reviewed_at: now,
|
|
162
|
+
review_note: input.review_note,
|
|
163
|
+
last_updated_by: this.auth.mms_id,
|
|
164
|
+
updated_at: now,
|
|
165
|
+
});
|
|
166
|
+
return row ? this.getById(leaveRequestId) : null;
|
|
167
|
+
}
|
|
168
|
+
/** Cancels a pending or approved request; null when it was already closed. */
|
|
169
|
+
async cancel(leaveRequestId, cancelledBy) {
|
|
170
|
+
const now = new Date().toISOString();
|
|
171
|
+
const row = await this.table.updateWhere(leaveRequestId, (query) => query.in('status', LIVE_STATUSES), {
|
|
172
|
+
status: enum_2.LeaveRequestStatus.CANCELLED,
|
|
173
|
+
cancelled_by: cancelledBy,
|
|
174
|
+
cancelled_at: now,
|
|
175
|
+
last_updated_by: this.auth.mms_id,
|
|
176
|
+
updated_at: now,
|
|
177
|
+
});
|
|
178
|
+
return row ? this.getById(leaveRequestId) : null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
exports.LeaveRequestsResource = LeaveRequestsResource;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LeaveTypeEntity } from '@managemint-solutions/entities/leave';
|
|
2
|
+
import type { AuthContext } from '../auth';
|
|
3
|
+
import { type AuditResource } from '../audit';
|
|
4
|
+
import type { TypedSupabaseClient } from '../client';
|
|
5
|
+
import type { CreateLeaveTypeDto, GetLeaveTypesDto, UpdateLeaveTypeDto } from './dto';
|
|
6
|
+
export declare class LeaveTypesResource {
|
|
7
|
+
private readonly supabase;
|
|
8
|
+
private readonly auth;
|
|
9
|
+
private readonly table;
|
|
10
|
+
constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
|
|
11
|
+
/** Every type by name; `active` narrows to one side of the flag. Small enough not to page. */
|
|
12
|
+
list(query: GetLeaveTypesDto): Promise<LeaveTypeEntity[]>;
|
|
13
|
+
getById(leaveTypeId: string): Promise<LeaveTypeEntity>;
|
|
14
|
+
create(input: CreateLeaveTypeDto): Promise<LeaveTypeEntity>;
|
|
15
|
+
update(leaveTypeId: string, input: UpdateLeaveTypeDto): Promise<LeaveTypeEntity>;
|
|
16
|
+
/**
|
|
17
|
+
* Hard delete. The entitlement and request foreign keys are `on delete restrict`, so a type
|
|
18
|
+
* that has ever been used fails here with the SDK's 409 - deactivate it instead.
|
|
19
|
+
*/
|
|
20
|
+
remove(leaveTypeId: string): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LeaveTypesResource = 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
|
+
// The public contract is the shared LeaveTypeEntity from @managemint-solutions/entities; the
|
|
8
|
+
// casts at the returns declare the shape the select string produces (the columns are defined in
|
|
9
|
+
// the api's supabase/migrations).
|
|
10
|
+
const LEAVE_TYPE_SELECT = `
|
|
11
|
+
mms_id,
|
|
12
|
+
created_at,
|
|
13
|
+
created_by:user_profiles!created_by (*),
|
|
14
|
+
updated_at,
|
|
15
|
+
last_updated_by:user_profiles!last_updated_by (*),
|
|
16
|
+
name,
|
|
17
|
+
description,
|
|
18
|
+
default_days,
|
|
19
|
+
is_paid,
|
|
20
|
+
requires_approval,
|
|
21
|
+
active
|
|
22
|
+
`;
|
|
23
|
+
class LeaveTypesResource {
|
|
24
|
+
supabase;
|
|
25
|
+
auth;
|
|
26
|
+
table;
|
|
27
|
+
constructor(supabase, auth, audit) {
|
|
28
|
+
this.supabase = supabase;
|
|
29
|
+
this.auth = auth;
|
|
30
|
+
this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
|
|
31
|
+
table: 'leave_types',
|
|
32
|
+
entityType: enum_1.AuditTrailEntityType.LEAVE_TYPES,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** Every type by name; `active` narrows to one side of the flag. Small enough not to page. */
|
|
36
|
+
async list(query) {
|
|
37
|
+
let typesQuery = this.supabase
|
|
38
|
+
.from('leave_types')
|
|
39
|
+
.select(LEAVE_TYPE_SELECT)
|
|
40
|
+
.eq('organization_id', this.auth.organization_id)
|
|
41
|
+
.order('name');
|
|
42
|
+
if (query.active !== undefined && query.active !== null) {
|
|
43
|
+
typesQuery = typesQuery.eq('active', query.active);
|
|
44
|
+
}
|
|
45
|
+
const { data, error } = await typesQuery;
|
|
46
|
+
if (error)
|
|
47
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
async getById(leaveTypeId) {
|
|
51
|
+
const { data, error } = await this.supabase
|
|
52
|
+
.from('leave_types')
|
|
53
|
+
.select(LEAVE_TYPE_SELECT)
|
|
54
|
+
.eq('mms_id', leaveTypeId)
|
|
55
|
+
.eq('organization_id', this.auth.organization_id)
|
|
56
|
+
.single();
|
|
57
|
+
if (error)
|
|
58
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
59
|
+
return data;
|
|
60
|
+
}
|
|
61
|
+
async create(input) {
|
|
62
|
+
const row = await this.table.insert({
|
|
63
|
+
name: input.name,
|
|
64
|
+
description: input.description,
|
|
65
|
+
default_days: input.default_days,
|
|
66
|
+
is_paid: input.is_paid,
|
|
67
|
+
requires_approval: input.requires_approval,
|
|
68
|
+
organization_id: this.auth.organization_id,
|
|
69
|
+
created_by: this.auth.mms_id,
|
|
70
|
+
created_at: new Date().toISOString(),
|
|
71
|
+
});
|
|
72
|
+
return this.getById(row.mms_id);
|
|
73
|
+
}
|
|
74
|
+
async update(leaveTypeId, input) {
|
|
75
|
+
await this.table.update(leaveTypeId, {
|
|
76
|
+
name: input.name,
|
|
77
|
+
description: input.description,
|
|
78
|
+
default_days: input.default_days,
|
|
79
|
+
is_paid: input.is_paid,
|
|
80
|
+
requires_approval: input.requires_approval,
|
|
81
|
+
active: input.active,
|
|
82
|
+
last_updated_by: this.auth.mms_id,
|
|
83
|
+
updated_at: new Date().toISOString(),
|
|
84
|
+
});
|
|
85
|
+
return this.getById(leaveTypeId);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Hard delete. The entitlement and request foreign keys are `on delete restrict`, so a type
|
|
89
|
+
* that has ever been used fails here with the SDK's 409 - deactivate it instead.
|
|
90
|
+
*/
|
|
91
|
+
async remove(leaveTypeId) {
|
|
92
|
+
await this.table.remove(leaveTypeId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.LeaveTypesResource = LeaveTypesResource;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { LeavePeriod } from '@managemint-solutions/entities/leave';
|
|
2
|
+
/**
|
|
3
|
+
* Where "today" is read for the cancel rule. Organizations carry no time zone yet, and every
|
|
4
|
+
* customer is South African; this is the one place to change when a column arrives.
|
|
5
|
+
*/
|
|
6
|
+
export declare const LEAVE_TIME_ZONE = "Africa/Johannesburg";
|
|
7
|
+
/** The `YYYY-MM-DD` of a `Date`'s UTC calendar day. */
|
|
8
|
+
export declare const toIsoDate: (date: Date) => string;
|
|
9
|
+
/** Today's calendar date in `LEAVE_TIME_ZONE`. */
|
|
10
|
+
export declare const todayInJohannesburg: (now?: Date) => string;
|
|
11
|
+
/** Every calendar date from `start` to `end` inclusive; empty when `end` is before `start`. */
|
|
12
|
+
export declare const eachDate: (start: string, end: string) => string[];
|
|
13
|
+
/** Monday to Friday and not one of the organization's holidays. */
|
|
14
|
+
export declare const isWorkingDay: (isoDate: string, holidays: ReadonlySet<string>) => boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The days a request is worth: the working days from `start` to `end` inclusive, halved for a
|
|
17
|
+
* half day. A half day is only meaningful on a single working day, so it yields 0.5 there and 0
|
|
18
|
+
* on a weekend or holiday - the caller rejects a zero either way.
|
|
19
|
+
*/
|
|
20
|
+
export declare const countWorkingDays: (start: string, end: string, holidays: ReadonlySet<string>, isHalfDay?: boolean) => number;
|
|
21
|
+
/** The day before the same date next year - the end of a period that starts on `isoDate`. */
|
|
22
|
+
export declare const addOneYearMinusDay: (isoDate: string) => string;
|
|
23
|
+
/**
|
|
24
|
+
* The leave year containing `date` for an organization whose year starts on the first of
|
|
25
|
+
* `startMonth` (1-12, `organizations.leave_calendar_start_month`). A March start puts
|
|
26
|
+
* 2026-02-10 in the year that began 2025-03-01.
|
|
27
|
+
*/
|
|
28
|
+
export declare const leavePeriodFor: (date: string, startMonth: number) => LeavePeriod;
|
|
29
|
+
export declare const isWithinPeriod: (date: string, period: LeavePeriod) => boolean;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isWithinPeriod = exports.leavePeriodFor = exports.addOneYearMinusDay = exports.countWorkingDays = exports.isWorkingDay = exports.eachDate = exports.todayInJohannesburg = exports.toIsoDate = exports.LEAVE_TIME_ZONE = void 0;
|
|
4
|
+
// The calendar arithmetic behind leave: which days of a request count, and which leave year a
|
|
5
|
+
// date belongs to. Pure functions over `YYYY-MM-DD` strings - the columns are `date`, so a
|
|
6
|
+
// request never carries a time and nothing here depends on the caller's clock zone.
|
|
7
|
+
/**
|
|
8
|
+
* Where "today" is read for the cancel rule. Organizations carry no time zone yet, and every
|
|
9
|
+
* customer is South African; this is the one place to change when a column arrives.
|
|
10
|
+
*/
|
|
11
|
+
exports.LEAVE_TIME_ZONE = 'Africa/Johannesburg';
|
|
12
|
+
/** Saturday and Sunday. Not configurable yet - a working-week setting is a later feature. */
|
|
13
|
+
const WEEKEND_DAYS = new Set([0, 6]);
|
|
14
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
15
|
+
/** A `Date` at UTC midnight of a `YYYY-MM-DD` string (a longer ISO string is cut to its date). */
|
|
16
|
+
const parseIsoDate = (value) => {
|
|
17
|
+
const [year, month, day] = value.slice(0, 10).split('-').map(Number);
|
|
18
|
+
return new Date(Date.UTC(year, month - 1, day));
|
|
19
|
+
};
|
|
20
|
+
/** The `YYYY-MM-DD` of a `Date`'s UTC calendar day. */
|
|
21
|
+
const toIsoDate = (date) => `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
|
|
22
|
+
exports.toIsoDate = toIsoDate;
|
|
23
|
+
/** Today's calendar date in `LEAVE_TIME_ZONE`. */
|
|
24
|
+
const todayInJohannesburg = (now = new Date()) =>
|
|
25
|
+
// en-CA is the locale whose short date is already YYYY-MM-DD.
|
|
26
|
+
new Intl.DateTimeFormat('en-CA', {
|
|
27
|
+
timeZone: exports.LEAVE_TIME_ZONE,
|
|
28
|
+
year: 'numeric',
|
|
29
|
+
month: '2-digit',
|
|
30
|
+
day: '2-digit',
|
|
31
|
+
}).format(now);
|
|
32
|
+
exports.todayInJohannesburg = todayInJohannesburg;
|
|
33
|
+
/** Every calendar date from `start` to `end` inclusive; empty when `end` is before `start`. */
|
|
34
|
+
const eachDate = (start, end) => {
|
|
35
|
+
const dates = [];
|
|
36
|
+
const last = end.slice(0, 10);
|
|
37
|
+
for (const cursor = parseIsoDate(start); (0, exports.toIsoDate)(cursor) <= last; cursor.setUTCDate(cursor.getUTCDate() + 1)) {
|
|
38
|
+
dates.push((0, exports.toIsoDate)(cursor));
|
|
39
|
+
}
|
|
40
|
+
return dates;
|
|
41
|
+
};
|
|
42
|
+
exports.eachDate = eachDate;
|
|
43
|
+
/** Monday to Friday and not one of the organization's holidays. */
|
|
44
|
+
const isWorkingDay = (isoDate, holidays) => !WEEKEND_DAYS.has(parseIsoDate(isoDate).getUTCDay()) && !holidays.has(isoDate.slice(0, 10));
|
|
45
|
+
exports.isWorkingDay = isWorkingDay;
|
|
46
|
+
/**
|
|
47
|
+
* The days a request is worth: the working days from `start` to `end` inclusive, halved for a
|
|
48
|
+
* half day. A half day is only meaningful on a single working day, so it yields 0.5 there and 0
|
|
49
|
+
* on a weekend or holiday - the caller rejects a zero either way.
|
|
50
|
+
*/
|
|
51
|
+
const countWorkingDays = (start, end, holidays, isHalfDay = false) => {
|
|
52
|
+
const working = (0, exports.eachDate)(start, end).filter((date) => (0, exports.isWorkingDay)(date, holidays)).length;
|
|
53
|
+
return isHalfDay ? working * 0.5 : working;
|
|
54
|
+
};
|
|
55
|
+
exports.countWorkingDays = countWorkingDays;
|
|
56
|
+
/** The day before the same date next year - the end of a period that starts on `isoDate`. */
|
|
57
|
+
const addOneYearMinusDay = (isoDate) => {
|
|
58
|
+
const date = parseIsoDate(isoDate);
|
|
59
|
+
date.setUTCFullYear(date.getUTCFullYear() + 1);
|
|
60
|
+
date.setUTCDate(date.getUTCDate() - 1);
|
|
61
|
+
return (0, exports.toIsoDate)(date);
|
|
62
|
+
};
|
|
63
|
+
exports.addOneYearMinusDay = addOneYearMinusDay;
|
|
64
|
+
/**
|
|
65
|
+
* The leave year containing `date` for an organization whose year starts on the first of
|
|
66
|
+
* `startMonth` (1-12, `organizations.leave_calendar_start_month`). A March start puts
|
|
67
|
+
* 2026-02-10 in the year that began 2025-03-01.
|
|
68
|
+
*/
|
|
69
|
+
const leavePeriodFor = (date, startMonth) => {
|
|
70
|
+
const parsed = parseIsoDate(date);
|
|
71
|
+
const year = parsed.getUTCMonth() + 1 >= startMonth ? parsed.getUTCFullYear() : parsed.getUTCFullYear() - 1;
|
|
72
|
+
const period_start = (0, exports.toIsoDate)(new Date(Date.UTC(year, startMonth - 1, 1)));
|
|
73
|
+
return { period_start, period_end: (0, exports.addOneYearMinusDay)(period_start) };
|
|
74
|
+
};
|
|
75
|
+
exports.leavePeriodFor = leavePeriodFor;
|
|
76
|
+
const isWithinPeriod = (date, period) => {
|
|
77
|
+
const day = date.slice(0, 10);
|
|
78
|
+
return day >= period.period_start && day <= period.period_end;
|
|
79
|
+
};
|
|
80
|
+
exports.isWithinPeriod = isWithinPeriod;
|
|
@@ -12,6 +12,18 @@ export declare class UpdatePermissionsDto implements UpdatePermissionsDtoType {
|
|
|
12
12
|
readonly read_tasks: boolean;
|
|
13
13
|
readonly update_tasks: boolean;
|
|
14
14
|
readonly delete_tasks: boolean;
|
|
15
|
+
readonly create_leave: boolean;
|
|
16
|
+
readonly read_leave: boolean;
|
|
17
|
+
readonly update_leave: boolean;
|
|
18
|
+
readonly delete_leave: boolean;
|
|
19
|
+
readonly create_leave_types: boolean;
|
|
20
|
+
readonly read_leave_types: boolean;
|
|
21
|
+
readonly update_leave_types: boolean;
|
|
22
|
+
readonly delete_leave_types: boolean;
|
|
23
|
+
readonly create_leave_requests: boolean;
|
|
24
|
+
readonly read_leave_requests: boolean;
|
|
25
|
+
readonly update_leave_requests: boolean;
|
|
26
|
+
readonly delete_leave_requests: boolean;
|
|
15
27
|
readonly read_invoices: boolean;
|
|
16
28
|
readonly create_users: boolean;
|
|
17
29
|
readonly read_users: boolean;
|
package/dist/permissions/dto.js
CHANGED
|
@@ -42,30 +42,18 @@ class UpdatePermissionsDto {
|
|
|
42
42
|
// readonly update_documents!: boolean;
|
|
43
43
|
// @IsBoolean({ message: 'Delete Documents must be a boolean' })
|
|
44
44
|
// readonly delete_documents!: boolean;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// @IsBoolean({ message: 'Update Leave Types must be a boolean' })
|
|
58
|
-
// readonly update_leave_types!: boolean;
|
|
59
|
-
// @IsBoolean({ message: 'Delete Leave Types must be a boolean' })
|
|
60
|
-
// readonly delete_leave_types!: boolean;
|
|
61
|
-
// @IsBoolean({ message: 'Create Leave Requests must be a boolean' })
|
|
62
|
-
// readonly create_leave_requests!: boolean;
|
|
63
|
-
// @IsBoolean({ message: 'Read Leave Requests must be a boolean' })
|
|
64
|
-
// readonly read_leave_requests!: boolean;
|
|
65
|
-
// @IsBoolean({ message: 'Update Leave Requests must be a boolean' })
|
|
66
|
-
// readonly update_leave_requests!: boolean;
|
|
67
|
-
// @IsBoolean({ message: 'Delete Leave Requests must be a boolean' })
|
|
68
|
-
// readonly delete_leave_requests!: boolean;
|
|
45
|
+
create_leave;
|
|
46
|
+
read_leave;
|
|
47
|
+
update_leave;
|
|
48
|
+
delete_leave;
|
|
49
|
+
create_leave_types;
|
|
50
|
+
read_leave_types;
|
|
51
|
+
update_leave_types;
|
|
52
|
+
delete_leave_types;
|
|
53
|
+
create_leave_requests;
|
|
54
|
+
read_leave_requests;
|
|
55
|
+
update_leave_requests;
|
|
56
|
+
delete_leave_requests;
|
|
69
57
|
// @IsBoolean({ message: 'Create Invoices must be a boolean' })
|
|
70
58
|
// readonly create_invoices!: boolean;
|
|
71
59
|
read_invoices;
|
|
@@ -164,6 +152,54 @@ __decorate([
|
|
|
164
152
|
(0, class_validator_1.IsBoolean)({ message: 'Delete Tasks must be a boolean' }),
|
|
165
153
|
__metadata("design:type", Boolean)
|
|
166
154
|
], UpdatePermissionsDto.prototype, "delete_tasks", void 0);
|
|
155
|
+
__decorate([
|
|
156
|
+
(0, class_validator_1.IsBoolean)({ message: 'Create Leave must be a boolean' }),
|
|
157
|
+
__metadata("design:type", Boolean)
|
|
158
|
+
], UpdatePermissionsDto.prototype, "create_leave", void 0);
|
|
159
|
+
__decorate([
|
|
160
|
+
(0, class_validator_1.IsBoolean)({ message: 'Read Leave must be a boolean' }),
|
|
161
|
+
__metadata("design:type", Boolean)
|
|
162
|
+
], UpdatePermissionsDto.prototype, "read_leave", void 0);
|
|
163
|
+
__decorate([
|
|
164
|
+
(0, class_validator_1.IsBoolean)({ message: 'Update Leave must be a boolean' }),
|
|
165
|
+
__metadata("design:type", Boolean)
|
|
166
|
+
], UpdatePermissionsDto.prototype, "update_leave", void 0);
|
|
167
|
+
__decorate([
|
|
168
|
+
(0, class_validator_1.IsBoolean)({ message: 'Delete Leave must be a boolean' }),
|
|
169
|
+
__metadata("design:type", Boolean)
|
|
170
|
+
], UpdatePermissionsDto.prototype, "delete_leave", void 0);
|
|
171
|
+
__decorate([
|
|
172
|
+
(0, class_validator_1.IsBoolean)({ message: 'Create Leave Types must be a boolean' }),
|
|
173
|
+
__metadata("design:type", Boolean)
|
|
174
|
+
], UpdatePermissionsDto.prototype, "create_leave_types", void 0);
|
|
175
|
+
__decorate([
|
|
176
|
+
(0, class_validator_1.IsBoolean)({ message: 'Read Leave Types must be a boolean' }),
|
|
177
|
+
__metadata("design:type", Boolean)
|
|
178
|
+
], UpdatePermissionsDto.prototype, "read_leave_types", void 0);
|
|
179
|
+
__decorate([
|
|
180
|
+
(0, class_validator_1.IsBoolean)({ message: 'Update Leave Types must be a boolean' }),
|
|
181
|
+
__metadata("design:type", Boolean)
|
|
182
|
+
], UpdatePermissionsDto.prototype, "update_leave_types", void 0);
|
|
183
|
+
__decorate([
|
|
184
|
+
(0, class_validator_1.IsBoolean)({ message: 'Delete Leave Types must be a boolean' }),
|
|
185
|
+
__metadata("design:type", Boolean)
|
|
186
|
+
], UpdatePermissionsDto.prototype, "delete_leave_types", void 0);
|
|
187
|
+
__decorate([
|
|
188
|
+
(0, class_validator_1.IsBoolean)({ message: 'Create Leave Requests must be a boolean' }),
|
|
189
|
+
__metadata("design:type", Boolean)
|
|
190
|
+
], UpdatePermissionsDto.prototype, "create_leave_requests", void 0);
|
|
191
|
+
__decorate([
|
|
192
|
+
(0, class_validator_1.IsBoolean)({ message: 'Read Leave Requests must be a boolean' }),
|
|
193
|
+
__metadata("design:type", Boolean)
|
|
194
|
+
], UpdatePermissionsDto.prototype, "read_leave_requests", void 0);
|
|
195
|
+
__decorate([
|
|
196
|
+
(0, class_validator_1.IsBoolean)({ message: 'Update Leave Requests must be a boolean' }),
|
|
197
|
+
__metadata("design:type", Boolean)
|
|
198
|
+
], UpdatePermissionsDto.prototype, "update_leave_requests", void 0);
|
|
199
|
+
__decorate([
|
|
200
|
+
(0, class_validator_1.IsBoolean)({ message: 'Delete Leave Requests must be a boolean' }),
|
|
201
|
+
__metadata("design:type", Boolean)
|
|
202
|
+
], UpdatePermissionsDto.prototype, "delete_leave_requests", void 0);
|
|
167
203
|
__decorate([
|
|
168
204
|
(0, class_validator_1.IsBoolean)({ message: 'Read Invoices must be a boolean' }),
|
|
169
205
|
__metadata("design:type", Boolean)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.GetPublicHolidaysDto = void 0;
|
|
13
|
+
const class_validator_1 = require("class-validator");
|
|
14
|
+
const class_transformer_1 = require("class-transformer");
|
|
15
|
+
const validators_1 = require("../validators");
|
|
16
|
+
// class-validator version of the shared DTO type from @managemint-solutions/entities.
|
|
17
|
+
// The class `implements` its entities type so the wire contract cannot drift.
|
|
18
|
+
class GetPublicHolidaysDto {
|
|
19
|
+
year;
|
|
20
|
+
}
|
|
21
|
+
exports.GetPublicHolidaysDto = GetPublicHolidaysDto;
|
|
22
|
+
__decorate([
|
|
23
|
+
(0, class_validator_1.IsOptional)(),
|
|
24
|
+
(0, validators_1.IsNullable)(),
|
|
25
|
+
(0, class_transformer_1.Transform)(({ value }) => (value ? Number(value) : value), { toClassOnly: true }),
|
|
26
|
+
(0, class_validator_1.IsInt)({ message: 'Year must be a whole number' }),
|
|
27
|
+
(0, class_validator_1.Min)(2000, { message: 'Year must be 2000 or later' }),
|
|
28
|
+
(0, class_validator_1.Max)(2100, { message: 'Year must be 2100 or earlier' }),
|
|
29
|
+
__metadata("design:type", Object)
|
|
30
|
+
], GetPublicHolidaysDto.prototype, "year", void 0);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PublicHolidayEntity } from '@managemint-solutions/entities/public-holidays';
|
|
2
|
+
import type { TypedSupabaseClient } from '../client';
|
|
3
|
+
import type { GetPublicHolidaysDto } from './dto';
|
|
4
|
+
export * from './dto';
|
|
5
|
+
/**
|
|
6
|
+
* The public-holiday calendar. Global, like configs: every signed-in caller reads the same
|
|
7
|
+
* rows (the READ policy is `true` - there is no tenant on them), so this hangs off the user
|
|
8
|
+
* client with no auth context, and nothing writes them but Scott in Supabase Studio.
|
|
9
|
+
*/
|
|
10
|
+
export declare class PublicHolidaysResource {
|
|
11
|
+
private readonly supabase;
|
|
12
|
+
constructor(supabase: TypedSupabaseClient);
|
|
13
|
+
/** The calendar in date order, or one calendar year of it. */
|
|
14
|
+
list(query: GetPublicHolidaysDto): Promise<PublicHolidayEntity[]>;
|
|
15
|
+
/**
|
|
16
|
+
* The dates between `start` and `end` (inclusive) that are a day off for everyone - the input
|
|
17
|
+
* to leave's working-day count. A faith-specific holiday (`religion` set) is on the calendar
|
|
18
|
+
* but does not shorten a request, since nothing yet says which employees observe it.
|
|
19
|
+
*/
|
|
20
|
+
datesBetween(start: string, end: string): Promise<Set<string>>;
|
|
21
|
+
}
|