@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
|
@@ -16,7 +16,7 @@ const LEAVE_REQUEST_SELECT = `
|
|
|
16
16
|
leave_type:leave_types!leave_type_id (
|
|
17
17
|
mms_id,
|
|
18
18
|
name,
|
|
19
|
-
|
|
19
|
+
document_after_days
|
|
20
20
|
),
|
|
21
21
|
start_date,
|
|
22
22
|
end_date,
|
|
@@ -30,6 +30,19 @@ const LEAVE_REQUEST_SELECT = `
|
|
|
30
30
|
cancelled_by:user_profiles!cancelled_by (*),
|
|
31
31
|
cancelled_at
|
|
32
32
|
`;
|
|
33
|
+
const LEAVE_CALENDAR_SELECT = `
|
|
34
|
+
mms_id,
|
|
35
|
+
user:user_profiles!user_id (*),
|
|
36
|
+
created_by:user_profiles!created_by (*),
|
|
37
|
+
leave_type:leave_types!leave_type_id (
|
|
38
|
+
mms_id,
|
|
39
|
+
name
|
|
40
|
+
),
|
|
41
|
+
start_date,
|
|
42
|
+
end_date,
|
|
43
|
+
is_half_day,
|
|
44
|
+
status
|
|
45
|
+
`;
|
|
33
46
|
/** The statuses that hold days: a pending request reserves them, an approved one uses them. */
|
|
34
47
|
const LIVE_STATUSES = [enum_2.LeaveRequestStatus.PENDING, enum_2.LeaveRequestStatus.APPROVED];
|
|
35
48
|
class LeaveRequestsResource {
|
|
@@ -110,28 +123,27 @@ class LeaveRequestsResource {
|
|
|
110
123
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
111
124
|
return (count ?? 0) > 0;
|
|
112
125
|
}
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return (data ?? []).reduce((total, row) => total + Number(row.days), 0);
|
|
126
|
+
/** Live requests touching `date_from`..`date_to`, for everyone in the organization - the leave calendar. */
|
|
127
|
+
async calendar(query) {
|
|
128
|
+
return (0, transforms_1.allRows)((from, to) => {
|
|
129
|
+
let calendarQuery = this.supabase
|
|
130
|
+
.from('leave_requests')
|
|
131
|
+
.select(LEAVE_CALENDAR_SELECT)
|
|
132
|
+
.eq('organization_id', this.auth.organization_id)
|
|
133
|
+
.in('status', LIVE_STATUSES)
|
|
134
|
+
.lte('start_date', query.date_to)
|
|
135
|
+
.gte('end_date', query.date_from)
|
|
136
|
+
.order('start_date')
|
|
137
|
+
.order('mms_id')
|
|
138
|
+
.range(from, to);
|
|
139
|
+
if (query.user_id) {
|
|
140
|
+
calendarQuery = calendarQuery.eq('user_id', query.user_id);
|
|
141
|
+
}
|
|
142
|
+
if (query.leave_type_id) {
|
|
143
|
+
calendarQuery = calendarQuery.eq('leave_type_id', query.leave_type_id);
|
|
144
|
+
}
|
|
145
|
+
return calendarQuery;
|
|
146
|
+
});
|
|
135
147
|
}
|
|
136
148
|
async create(input) {
|
|
137
149
|
const row = await this.table.insert({
|
|
@@ -169,10 +181,13 @@ class LeaveRequestsResource {
|
|
|
169
181
|
});
|
|
170
182
|
return row ? this.getById(leaveRequestId) : null;
|
|
171
183
|
}
|
|
172
|
-
/**
|
|
173
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Cancels a request that is still in `expected` - the status the api read and decided on.
|
|
186
|
+
* Null when it has moved on since (reviewed, or cancelled by someone else).
|
|
187
|
+
*/
|
|
188
|
+
async cancel(leaveRequestId, cancelledBy, expected) {
|
|
174
189
|
const now = new Date().toISOString();
|
|
175
|
-
const row = await this.table.updateWhere(leaveRequestId, (query) => query.eq('status',
|
|
190
|
+
const row = await this.table.updateWhere(leaveRequestId, (query) => query.eq('status', expected), {
|
|
176
191
|
status: enum_2.LeaveRequestStatus.CANCELLED,
|
|
177
192
|
cancelled_by: cancelledBy,
|
|
178
193
|
cancelled_at: now,
|
|
@@ -18,9 +18,26 @@ const LEAVE_TYPE_SELECT = `
|
|
|
18
18
|
default_days,
|
|
19
19
|
is_paid,
|
|
20
20
|
requires_approval,
|
|
21
|
-
|
|
21
|
+
document_after_days,
|
|
22
|
+
cycle_years,
|
|
23
|
+
grant_method,
|
|
24
|
+
max_carry_over_days,
|
|
25
|
+
carry_over_expiry_months,
|
|
26
|
+
min_service_months,
|
|
27
|
+
min_days_per_week,
|
|
28
|
+
initial_months,
|
|
29
|
+
initial_days_per_month,
|
|
30
|
+
paid_on_exit,
|
|
22
31
|
active
|
|
23
32
|
`;
|
|
33
|
+
/** Postgres hands `numeric` back as a string; the entity promises numbers. */
|
|
34
|
+
const toLeaveType = (row) => ({
|
|
35
|
+
...row,
|
|
36
|
+
default_days: Number(row.default_days),
|
|
37
|
+
max_carry_over_days: Number(row.max_carry_over_days),
|
|
38
|
+
document_after_days: row.document_after_days === null ? null : Number(row.document_after_days),
|
|
39
|
+
initial_days_per_month: row.initial_days_per_month === null ? null : Number(row.initial_days_per_month),
|
|
40
|
+
});
|
|
24
41
|
class LeaveTypesResource {
|
|
25
42
|
supabase;
|
|
26
43
|
auth;
|
|
@@ -46,7 +63,7 @@ class LeaveTypesResource {
|
|
|
46
63
|
const { data, error } = await typesQuery;
|
|
47
64
|
if (error)
|
|
48
65
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
49
|
-
return data;
|
|
66
|
+
return (data ?? []).map(toLeaveType);
|
|
50
67
|
}
|
|
51
68
|
async getById(leaveTypeId) {
|
|
52
69
|
const { data, error } = await this.supabase
|
|
@@ -57,7 +74,7 @@ class LeaveTypesResource {
|
|
|
57
74
|
.single();
|
|
58
75
|
if (error)
|
|
59
76
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
60
|
-
return data;
|
|
77
|
+
return toLeaveType(data);
|
|
61
78
|
}
|
|
62
79
|
async create(input) {
|
|
63
80
|
const row = await this.table.insert({
|
|
@@ -66,7 +83,16 @@ class LeaveTypesResource {
|
|
|
66
83
|
default_days: input.default_days,
|
|
67
84
|
is_paid: input.is_paid,
|
|
68
85
|
requires_approval: input.requires_approval,
|
|
69
|
-
|
|
86
|
+
document_after_days: input.document_after_days,
|
|
87
|
+
cycle_years: input.cycle_years,
|
|
88
|
+
grant_method: input.grant_method,
|
|
89
|
+
max_carry_over_days: input.max_carry_over_days,
|
|
90
|
+
carry_over_expiry_months: input.carry_over_expiry_months,
|
|
91
|
+
min_service_months: input.min_service_months,
|
|
92
|
+
min_days_per_week: input.min_days_per_week,
|
|
93
|
+
initial_months: input.initial_months,
|
|
94
|
+
initial_days_per_month: input.initial_days_per_month,
|
|
95
|
+
paid_on_exit: input.paid_on_exit,
|
|
70
96
|
organization_id: this.auth.organization_id,
|
|
71
97
|
created_by: this.auth.mms_id,
|
|
72
98
|
created_at: new Date().toISOString(),
|
|
@@ -80,7 +106,16 @@ class LeaveTypesResource {
|
|
|
80
106
|
default_days: input.default_days,
|
|
81
107
|
is_paid: input.is_paid,
|
|
82
108
|
requires_approval: input.requires_approval,
|
|
83
|
-
|
|
109
|
+
document_after_days: input.document_after_days,
|
|
110
|
+
cycle_years: input.cycle_years,
|
|
111
|
+
grant_method: input.grant_method,
|
|
112
|
+
max_carry_over_days: input.max_carry_over_days,
|
|
113
|
+
carry_over_expiry_months: input.carry_over_expiry_months,
|
|
114
|
+
min_service_months: input.min_service_months,
|
|
115
|
+
min_days_per_week: input.min_days_per_week,
|
|
116
|
+
initial_months: input.initial_months,
|
|
117
|
+
initial_days_per_month: input.initial_days_per_month,
|
|
118
|
+
paid_on_exit: input.paid_on_exit,
|
|
84
119
|
active: input.active,
|
|
85
120
|
last_updated_by: this.auth.mms_id,
|
|
86
121
|
updated_at: new Date().toISOString(),
|
|
@@ -4,6 +4,8 @@ import type { LeavePeriod } from '@managemint-solutions/entities/leave';
|
|
|
4
4
|
* customer is South African; this is the one place to change when a column arrives.
|
|
5
5
|
*/
|
|
6
6
|
export declare const LEAVE_TIME_ZONE = "Africa/Johannesburg";
|
|
7
|
+
/** `users.working_days`' default: ISO weekdays, Monday (1) to Friday (5). */
|
|
8
|
+
export declare const MONDAY_TO_FRIDAY: readonly number[];
|
|
7
9
|
/** A `Date` at UTC midnight of a `YYYY-MM-DD` string (a longer ISO string is cut to its date). */
|
|
8
10
|
export declare const parseIsoDate: (value: string) => Date;
|
|
9
11
|
/** The `YYYY-MM-DD` of a `Date`'s UTC calendar day. */
|
|
@@ -12,22 +14,24 @@ export declare const toIsoDate: (date: Date) => string;
|
|
|
12
14
|
export declare const todayInJohannesburg: (now?: Date) => string;
|
|
13
15
|
/** Every calendar date from `start` to `end` inclusive; empty when `end` is before `start`. */
|
|
14
16
|
export declare const eachDate: (start: string, end: string) => string[];
|
|
15
|
-
/**
|
|
16
|
-
export declare const isWorkingDay: (isoDate: string, holidays: ReadonlySet<string
|
|
17
|
+
/** One of the person's `workingDays` (ISO weekdays) and not a public holiday. */
|
|
18
|
+
export declare const isWorkingDay: (isoDate: string, holidays: ReadonlySet<string>, workingDays?: readonly number[]) => boolean;
|
|
17
19
|
/**
|
|
18
20
|
* The days a request is worth: the working days from `start` to `end` inclusive, halved for a
|
|
19
21
|
* half day. A half day is only meaningful on a single working day, so it yields 0.5 there and 0
|
|
20
|
-
* on a
|
|
22
|
+
* on a day off or holiday - the caller rejects a zero either way.
|
|
21
23
|
*/
|
|
22
|
-
export declare const countWorkingDays: (start: string, end: string, holidays: ReadonlySet<string>, isHalfDay?: boolean) => number;
|
|
24
|
+
export declare const countWorkingDays: (start: string, end: string, holidays: ReadonlySet<string>, isHalfDay?: boolean, workingDays?: readonly number[]) => number;
|
|
23
25
|
/** `isoDate` moved by `days` calendar days, either way; `shiftDays('2026-03-01', -1)` is Feb 28. */
|
|
24
26
|
export declare const shiftDays: (isoDate: string, days: number) => string;
|
|
25
|
-
/** The day before the same date next year - the end of a period that starts on `isoDate`. */
|
|
26
|
-
export declare const addOneYearMinusDay: (isoDate: string) => string;
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
* `
|
|
30
|
-
* 2026-02-10 in the year that began 2025-03-01.
|
|
28
|
+
* `isoDate` moved by `months` calendar months, the day clamped to the target month's length:
|
|
29
|
+
* `addMonths('2026-01-31', 1)` is Feb 28.
|
|
31
30
|
*/
|
|
32
|
-
export declare const
|
|
31
|
+
export declare const addMonths: (isoDate: string, months: number) => string;
|
|
32
|
+
/**
|
|
33
|
+
* Whole months from `start` completed by the end of `asOf`: 0 until the day before the
|
|
34
|
+
* one-month mark, 1 from then. A month counts once its last day has been lived.
|
|
35
|
+
*/
|
|
36
|
+
export declare const completedMonths: (start: string, asOf: string) => number;
|
|
33
37
|
export declare const isWithinPeriod: (date: string, period: LeavePeriod) => boolean;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isWithinPeriod = exports.
|
|
4
|
-
// The calendar arithmetic behind leave: which days of a request count, and
|
|
5
|
-
//
|
|
3
|
+
exports.isWithinPeriod = exports.completedMonths = exports.addMonths = exports.shiftDays = exports.countWorkingDays = exports.isWorkingDay = exports.eachDate = exports.todayInJohannesburg = exports.toIsoDate = exports.parseIsoDate = exports.MONDAY_TO_FRIDAY = exports.LEAVE_TIME_ZONE = void 0;
|
|
4
|
+
// The calendar arithmetic behind leave: which days of a request count, and how dates move by
|
|
5
|
+
// days and months. Pure functions over `YYYY-MM-DD` strings - the columns are `date`, so a
|
|
6
6
|
// request never carries a time and nothing here depends on the caller's clock zone.
|
|
7
7
|
/**
|
|
8
8
|
* Where "today" is read for the cancel rule. Organizations carry no time zone yet, and every
|
|
9
9
|
* customer is South African; this is the one place to change when a column arrives.
|
|
10
10
|
*/
|
|
11
11
|
exports.LEAVE_TIME_ZONE = 'Africa/Johannesburg';
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/** `users.working_days`' default: ISO weekdays, Monday (1) to Friday (5). */
|
|
13
|
+
exports.MONDAY_TO_FRIDAY = [1, 2, 3, 4, 5];
|
|
14
14
|
const pad = (value) => String(value).padStart(2, '0');
|
|
15
15
|
/** A `Date` at UTC midnight of a `YYYY-MM-DD` string (a longer ISO string is cut to its date). */
|
|
16
16
|
const parseIsoDate = (value) => {
|
|
@@ -41,16 +41,16 @@ const eachDate = (start, end) => {
|
|
|
41
41
|
return dates;
|
|
42
42
|
};
|
|
43
43
|
exports.eachDate = eachDate;
|
|
44
|
-
/**
|
|
45
|
-
const isWorkingDay = (isoDate, holidays) =>
|
|
44
|
+
/** One of the person's `workingDays` (ISO weekdays) and not a public holiday. */
|
|
45
|
+
const isWorkingDay = (isoDate, holidays, workingDays = exports.MONDAY_TO_FRIDAY) => workingDays.includes((0, exports.parseIsoDate)(isoDate).getUTCDay() || 7) && !holidays.has(isoDate.slice(0, 10));
|
|
46
46
|
exports.isWorkingDay = isWorkingDay;
|
|
47
47
|
/**
|
|
48
48
|
* The days a request is worth: the working days from `start` to `end` inclusive, halved for a
|
|
49
49
|
* half day. A half day is only meaningful on a single working day, so it yields 0.5 there and 0
|
|
50
|
-
* on a
|
|
50
|
+
* on a day off or holiday - the caller rejects a zero either way.
|
|
51
51
|
*/
|
|
52
|
-
const countWorkingDays = (start, end, holidays, isHalfDay = false) => {
|
|
53
|
-
const working = (0, exports.eachDate)(start, end).filter((date) => (0, exports.isWorkingDay)(date, holidays)).length;
|
|
52
|
+
const countWorkingDays = (start, end, holidays, isHalfDay = false, workingDays = exports.MONDAY_TO_FRIDAY) => {
|
|
53
|
+
const working = (0, exports.eachDate)(start, end).filter((date) => (0, exports.isWorkingDay)(date, holidays, workingDays)).length;
|
|
54
54
|
return isHalfDay ? working * 0.5 : working;
|
|
55
55
|
};
|
|
56
56
|
exports.countWorkingDays = countWorkingDays;
|
|
@@ -61,26 +61,33 @@ const shiftDays = (isoDate, days) => {
|
|
|
61
61
|
return (0, exports.toIsoDate)(date);
|
|
62
62
|
};
|
|
63
63
|
exports.shiftDays = shiftDays;
|
|
64
|
-
/**
|
|
65
|
-
|
|
64
|
+
/**
|
|
65
|
+
* `isoDate` moved by `months` calendar months, the day clamped to the target month's length:
|
|
66
|
+
* `addMonths('2026-01-31', 1)` is Feb 28.
|
|
67
|
+
*/
|
|
68
|
+
const addMonths = (isoDate, months) => {
|
|
66
69
|
const date = (0, exports.parseIsoDate)(isoDate);
|
|
67
|
-
date.
|
|
68
|
-
date.setUTCDate(
|
|
70
|
+
const day = date.getUTCDate();
|
|
71
|
+
date.setUTCDate(1);
|
|
72
|
+
date.setUTCMonth(date.getUTCMonth() + months);
|
|
73
|
+
const lastDay = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0)).getUTCDate();
|
|
74
|
+
date.setUTCDate(Math.min(day, lastDay));
|
|
69
75
|
return (0, exports.toIsoDate)(date);
|
|
70
76
|
};
|
|
71
|
-
exports.
|
|
77
|
+
exports.addMonths = addMonths;
|
|
72
78
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* 2026-02-10 in the year that began 2025-03-01.
|
|
79
|
+
* Whole months from `start` completed by the end of `asOf`: 0 until the day before the
|
|
80
|
+
* one-month mark, 1 from then. A month counts once its last day has been lived.
|
|
76
81
|
*/
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
const completedMonths = (start, asOf) => {
|
|
83
|
+
const dayAfter = (0, exports.shiftDays)(asOf, 1);
|
|
84
|
+
let months = 0;
|
|
85
|
+
while ((0, exports.addMonths)(start, months + 1) <= dayAfter) {
|
|
86
|
+
months += 1;
|
|
87
|
+
}
|
|
88
|
+
return months;
|
|
82
89
|
};
|
|
83
|
-
exports.
|
|
90
|
+
exports.completedMonths = completedMonths;
|
|
84
91
|
const isWithinPeriod = (date, period) => {
|
|
85
92
|
const day = date.slice(0, 10);
|
|
86
93
|
return day >= period.period_start && day <= period.period_end;
|
|
@@ -225,6 +225,7 @@ class UpdateOrganizationDto {
|
|
|
225
225
|
industry;
|
|
226
226
|
marketing_consent;
|
|
227
227
|
currency;
|
|
228
|
+
leave_calendar_start_month;
|
|
228
229
|
}
|
|
229
230
|
exports.UpdateOrganizationDto = UpdateOrganizationDto;
|
|
230
231
|
__decorate([
|
|
@@ -442,3 +443,10 @@ __decorate([
|
|
|
442
443
|
(0, class_validator_1.MaxLength)(255, { message: 'Organization currency must be less than 255 characters' }),
|
|
443
444
|
__metadata("design:type", String)
|
|
444
445
|
], UpdateOrganizationDto.prototype, "currency", void 0);
|
|
446
|
+
__decorate([
|
|
447
|
+
(0, class_validator_1.IsOptional)(),
|
|
448
|
+
(0, class_validator_1.IsInt)({ message: 'Leave year start month must be a whole number' }),
|
|
449
|
+
(0, class_validator_1.Min)(1, { message: 'Leave year start month must be between 1 and 12' }),
|
|
450
|
+
(0, class_validator_1.Max)(12, { message: 'Leave year start month must be between 1 and 12' }),
|
|
451
|
+
__metadata("design:type", Number)
|
|
452
|
+
], UpdateOrganizationDto.prototype, "leave_calendar_start_month", void 0);
|
|
@@ -46,6 +46,11 @@ export declare class SupabaseOrganizationScope {
|
|
|
46
46
|
* subscriptions and both webhook handler services.
|
|
47
47
|
*/
|
|
48
48
|
table(options: AuditedTableOptions): AuditedTable;
|
|
49
|
+
/**
|
|
50
|
+
* Gives `userId` a new leave calendar feed token (stored as its SHA-256), which also retires
|
|
51
|
+
* any address they subscribed with before. Service role only: the token is a credential.
|
|
52
|
+
*/
|
|
53
|
+
replaceLeaveCalendarFeed(userId: string, tokenHash: string): Promise<void>;
|
|
49
54
|
/**
|
|
50
55
|
* Clients written service-role on a user's behalf: converting a lead needs only
|
|
51
56
|
* `update_leads`, so the client it becomes is created here rather than through the caller's
|
|
@@ -97,14 +102,23 @@ export declare class SupabaseServiceClient {
|
|
|
97
102
|
*/
|
|
98
103
|
organizationsWithPaystackCustomer(): Promise<string[]>;
|
|
99
104
|
/**
|
|
100
|
-
* Every organization
|
|
101
|
-
* nightly rollover visits
|
|
102
|
-
* from here.
|
|
105
|
+
* Every organization subscribed to `moduleKey`, with its leave-year start month - the tenants
|
|
106
|
+
* the nightly leave rollover visits. Unscoped like the reconcile: the cron picks its tenants
|
|
107
|
+
* from here.
|
|
103
108
|
*/
|
|
104
|
-
|
|
109
|
+
organizationsWithModule(moduleKey: string): Promise<{
|
|
105
110
|
mms_id: string;
|
|
106
|
-
|
|
111
|
+
leave_calendar_start_month: number;
|
|
107
112
|
}[]>;
|
|
113
|
+
/**
|
|
114
|
+
* Whose leave calendar feed a token opens, by the token's SHA-256. Unscoped by necessity: the
|
|
115
|
+
* feed is fetched by a calendar app with no session, and the row is what says which tenant
|
|
116
|
+
* and person it belongs to. `leave_calendar_feeds` has no `authenticated` grant at all.
|
|
117
|
+
*/
|
|
118
|
+
leaveCalendarFeedFor(tokenHash: string): Promise<{
|
|
119
|
+
user_id: string;
|
|
120
|
+
organization_id: string;
|
|
121
|
+
} | null>;
|
|
108
122
|
/**
|
|
109
123
|
* A ledger row by id alone. Also unscoped by necessity: a `charge.success` carries the
|
|
110
124
|
* transaction id in its metadata, and the row is what says which tenant it belongs to.
|
|
@@ -61,6 +61,20 @@ class SupabaseOrganizationScope {
|
|
|
61
61
|
table(options) {
|
|
62
62
|
return (0, audit_1.auditedTable)(this.supabase, this.actor, this.audit, options);
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Gives `userId` a new leave calendar feed token (stored as its SHA-256), which also retires
|
|
66
|
+
* any address they subscribed with before. Service role only: the token is a credential.
|
|
67
|
+
*/
|
|
68
|
+
async replaceLeaveCalendarFeed(userId, tokenHash) {
|
|
69
|
+
const { error } = await this.supabase.from('leave_calendar_feeds').upsert({
|
|
70
|
+
user_id: userId,
|
|
71
|
+
organization_id: this.actor.organization_id,
|
|
72
|
+
token_hash: tokenHash,
|
|
73
|
+
created_at: new Date().toISOString(),
|
|
74
|
+
}, { onConflict: 'user_id' });
|
|
75
|
+
if (error)
|
|
76
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
77
|
+
}
|
|
64
78
|
/**
|
|
65
79
|
* Clients written service-role on a user's behalf: converting a lead needs only
|
|
66
80
|
* `update_leads`, so the client it becomes is created here rather than through the caller's
|
|
@@ -167,19 +181,34 @@ class SupabaseServiceClient {
|
|
|
167
181
|
return (data ?? []).map((row) => row.organization_id);
|
|
168
182
|
}
|
|
169
183
|
/**
|
|
170
|
-
* Every organization
|
|
171
|
-
* nightly rollover visits
|
|
172
|
-
* from here.
|
|
184
|
+
* Every organization subscribed to `moduleKey`, with its leave-year start month - the tenants
|
|
185
|
+
* the nightly leave rollover visits. Unscoped like the reconcile: the cron picks its tenants
|
|
186
|
+
* from here.
|
|
173
187
|
*/
|
|
174
|
-
async
|
|
188
|
+
async organizationsWithModule(moduleKey) {
|
|
175
189
|
const { data, error } = await this.supabase
|
|
176
190
|
.from('organizations')
|
|
177
|
-
.select('mms_id,
|
|
178
|
-
.
|
|
191
|
+
.select('mms_id, leave_calendar_start_month')
|
|
192
|
+
.contains('modules', [moduleKey]);
|
|
179
193
|
if (error)
|
|
180
194
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
181
195
|
return (data ?? []);
|
|
182
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Whose leave calendar feed a token opens, by the token's SHA-256. Unscoped by necessity: the
|
|
199
|
+
* feed is fetched by a calendar app with no session, and the row is what says which tenant
|
|
200
|
+
* and person it belongs to. `leave_calendar_feeds` has no `authenticated` grant at all.
|
|
201
|
+
*/
|
|
202
|
+
async leaveCalendarFeedFor(tokenHash) {
|
|
203
|
+
const { data, error } = await this.supabase
|
|
204
|
+
.from('leave_calendar_feeds')
|
|
205
|
+
.select('user_id, organization_id')
|
|
206
|
+
.eq('token_hash', tokenHash)
|
|
207
|
+
.maybeSingle();
|
|
208
|
+
if (error)
|
|
209
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
210
|
+
return (data ?? null);
|
|
211
|
+
}
|
|
183
212
|
/**
|
|
184
213
|
* A ledger row by id alone. Also unscoped by necessity: a `charge.success` carries the
|
|
185
214
|
* transaction id in its metadata, and the row is what says which tenant it belongs to.
|
package/dist/statuses/dto.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AddStatusDto as AddStatusDtoType, GetStatusesDto as GetStatusesDtoType, StatusIdDto as StatusIdDtoType, UpdateStatusDto as UpdateStatusDtoType } from '@managemint-solutions/entities/status/dto';
|
|
1
|
+
import type { AddStatusDto as AddStatusDtoType, GetStatusesDto as GetStatusesDtoType, ReorderStatusesDto as ReorderStatusesDtoType, StatusIdDto as StatusIdDtoType, UpdateStatusDto as UpdateStatusDtoType } from '@managemint-solutions/entities/status/dto';
|
|
2
2
|
import { StatusEntityTypes } from '@managemint-solutions/entities/status/enum';
|
|
3
3
|
export declare class StatusIdDto implements StatusIdDtoType {
|
|
4
4
|
readonly statusId: string;
|
|
@@ -12,6 +12,9 @@ export declare class AddStatusDto implements AddStatusDtoType {
|
|
|
12
12
|
readonly entity: StatusEntityTypes;
|
|
13
13
|
readonly is_complete?: boolean;
|
|
14
14
|
}
|
|
15
|
+
export declare class ReorderStatusesDto implements ReorderStatusesDtoType {
|
|
16
|
+
readonly status_ids: string[];
|
|
17
|
+
}
|
|
15
18
|
export declare class UpdateStatusDto implements UpdateStatusDtoType {
|
|
16
19
|
readonly label: string;
|
|
17
20
|
readonly colour: string;
|
package/dist/statuses/dto.js
CHANGED
|
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.UpdateStatusDto = exports.AddStatusDto = exports.GetStatusesDto = exports.StatusIdDto = void 0;
|
|
12
|
+
exports.UpdateStatusDto = exports.ReorderStatusesDto = exports.AddStatusDto = exports.GetStatusesDto = exports.StatusIdDto = void 0;
|
|
13
13
|
const class_validator_1 = require("class-validator");
|
|
14
14
|
const enum_1 = require("@managemint-solutions/entities/status/enum");
|
|
15
15
|
// class-validator versions of the shared DTO types from @managemint-solutions/entities.
|
|
@@ -62,6 +62,18 @@ __decorate([
|
|
|
62
62
|
(0, class_validator_1.IsBoolean)({ message: 'is_complete must be true or false' }),
|
|
63
63
|
__metadata("design:type", Boolean)
|
|
64
64
|
], AddStatusDto.prototype, "is_complete", void 0);
|
|
65
|
+
class ReorderStatusesDto {
|
|
66
|
+
// The whole catalogue of the entity type in its new order; the resource checks it is complete
|
|
67
|
+
// and has no repeats, which the decorators cannot (they do not know the organization).
|
|
68
|
+
status_ids;
|
|
69
|
+
}
|
|
70
|
+
exports.ReorderStatusesDto = ReorderStatusesDto;
|
|
71
|
+
__decorate([
|
|
72
|
+
(0, class_validator_1.IsArray)({ message: 'status_ids must be an array' }),
|
|
73
|
+
(0, class_validator_1.ArrayMinSize)(1, { message: 'status_ids cannot be empty' }),
|
|
74
|
+
(0, class_validator_1.IsUUID)('7', { each: true, message: 'Invalid status ID format' }),
|
|
75
|
+
__metadata("design:type", Array)
|
|
76
|
+
], ReorderStatusesDto.prototype, "status_ids", void 0);
|
|
65
77
|
class UpdateStatusDto {
|
|
66
78
|
label;
|
|
67
79
|
// Colour must be a valid hex code (e.g., #FFFFFF)
|
package/dist/statuses/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StatusEntity } from '@managemint-solutions/entities/status';
|
|
2
|
-
import type { AddStatusDto, UpdateStatusDto } from '@managemint-solutions/entities/status/dto';
|
|
2
|
+
import type { AddStatusDto, ReorderStatusesDto, UpdateStatusDto } from '@managemint-solutions/entities/status/dto';
|
|
3
3
|
import type { StatusEntityTypes } from '@managemint-solutions/entities/status/enum';
|
|
4
4
|
import type { AuthContext } from '../auth';
|
|
5
5
|
import { type AuditResource } from '../audit';
|
|
@@ -10,8 +10,17 @@ export declare class StatusesResource {
|
|
|
10
10
|
private readonly auth;
|
|
11
11
|
private readonly table;
|
|
12
12
|
constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
|
|
13
|
+
/** The catalogue in its order: `position` first, then insertion order for the seeded ties. */
|
|
13
14
|
list(entity: StatusEntityTypes): Promise<StatusEntity[]>;
|
|
15
|
+
/** A new status goes last: one past the highest position its entity type has. */
|
|
14
16
|
create(input: AddStatusDto): Promise<StatusEntity>;
|
|
17
|
+
/**
|
|
18
|
+
* Rewrites the whole catalogue's positions from the order given. The ids must be exactly the
|
|
19
|
+
* entity type's statuses, each once - a partial list would leave positions undefined, so it is
|
|
20
|
+
* refused with a 400 before anything is written. Only the rows that move are written (and
|
|
21
|
+
* audited); the answer is the catalogue in its new order.
|
|
22
|
+
*/
|
|
23
|
+
reorder(entity: StatusEntityTypes, input: ReorderStatusesDto): Promise<StatusEntity[]>;
|
|
15
24
|
update(statusId: string, input: UpdateStatusDto): Promise<StatusEntity>;
|
|
16
25
|
/** The Won status cannot be deleted. Every other status can. */
|
|
17
26
|
remove(statusId: string): Promise<void>;
|
package/dist/statuses/index.js
CHANGED
|
@@ -23,7 +23,7 @@ const refuseWon = (message) => new errors_1.SupabaseClientError({ status: 400, e
|
|
|
23
23
|
// The public contract is the shared StatusEntity from @managemint-solutions/entities; the
|
|
24
24
|
// `as StatusEntity` casts declare the shape the select string below produces (the columns are
|
|
25
25
|
// defined in the api's supabase/migrations).
|
|
26
|
-
const STATUS_SELECT = 'mms_id, label, colour, is_complete, is_fail, is_triage, is_won';
|
|
26
|
+
const STATUS_SELECT = 'mms_id, label, colour, is_complete, is_fail, is_triage, is_won, position';
|
|
27
27
|
// The audited write returns the whole row; the wire shape is the STATUS_SELECT columns.
|
|
28
28
|
const toStatus = (row) => ({
|
|
29
29
|
mms_id: row.mms_id,
|
|
@@ -33,6 +33,7 @@ const toStatus = (row) => ({
|
|
|
33
33
|
is_fail: row.is_fail,
|
|
34
34
|
is_triage: row.is_triage,
|
|
35
35
|
is_won: row.is_won,
|
|
36
|
+
position: row.position,
|
|
36
37
|
});
|
|
37
38
|
class StatusesResource {
|
|
38
39
|
supabase;
|
|
@@ -46,27 +47,75 @@ class StatusesResource {
|
|
|
46
47
|
entityType: enum_1.AuditTrailEntityType.STATUS,
|
|
47
48
|
});
|
|
48
49
|
}
|
|
50
|
+
/** The catalogue in its order: `position` first, then insertion order for the seeded ties. */
|
|
49
51
|
async list(entity) {
|
|
50
52
|
const { data, error } = await this.supabase
|
|
51
53
|
.from('statuses')
|
|
52
54
|
.select(STATUS_SELECT)
|
|
53
55
|
.eq('organization_id', this.auth.organization_id)
|
|
54
|
-
.eq('entity_type', entity)
|
|
56
|
+
.eq('entity_type', entity)
|
|
57
|
+
.order('position')
|
|
58
|
+
.order('created_at')
|
|
59
|
+
.order('mms_id');
|
|
55
60
|
if (error)
|
|
56
61
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
57
62
|
return data;
|
|
58
63
|
}
|
|
64
|
+
/** A new status goes last: one past the highest position its entity type has. */
|
|
59
65
|
async create(input) {
|
|
66
|
+
const { data, error } = await this.supabase
|
|
67
|
+
.from('statuses')
|
|
68
|
+
.select('position')
|
|
69
|
+
.eq('organization_id', this.auth.organization_id)
|
|
70
|
+
.eq('entity_type', input.entity)
|
|
71
|
+
.order('position', { ascending: false })
|
|
72
|
+
.limit(1)
|
|
73
|
+
.maybeSingle();
|
|
74
|
+
if (error)
|
|
75
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
76
|
+
const last = data?.position;
|
|
60
77
|
const row = await this.table.insert({
|
|
61
78
|
label: input.label,
|
|
62
79
|
colour: input.colour,
|
|
63
80
|
entity_type: input.entity,
|
|
64
81
|
is_complete: input.is_complete ?? false,
|
|
82
|
+
position: last === undefined ? 0 : last + 1,
|
|
65
83
|
organization_id: this.auth.organization_id,
|
|
66
84
|
created_by: this.auth.mms_id,
|
|
67
85
|
});
|
|
68
86
|
return toStatus(row);
|
|
69
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Rewrites the whole catalogue's positions from the order given. The ids must be exactly the
|
|
90
|
+
* entity type's statuses, each once - a partial list would leave positions undefined, so it is
|
|
91
|
+
* refused with a 400 before anything is written. Only the rows that move are written (and
|
|
92
|
+
* audited); the answer is the catalogue in its new order.
|
|
93
|
+
*/
|
|
94
|
+
async reorder(entity, input) {
|
|
95
|
+
const current = await this.list(entity);
|
|
96
|
+
const currentIds = new Set(current.map((status) => status.mms_id));
|
|
97
|
+
const given = new Set(input.status_ids);
|
|
98
|
+
if (given.size !== input.status_ids.length ||
|
|
99
|
+
given.size !== currentIds.size ||
|
|
100
|
+
input.status_ids.some((id) => !currentIds.has(id))) {
|
|
101
|
+
throw new errors_1.SupabaseClientError({
|
|
102
|
+
status: 400,
|
|
103
|
+
error: 'Database Error',
|
|
104
|
+
message: `status_ids must list every ${entity} status exactly once`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const positionOf = new Map(current.map((status) => [status.mms_id, status.position]));
|
|
108
|
+
for (const [position, id] of input.status_ids.entries()) {
|
|
109
|
+
if (positionOf.get(id) === position)
|
|
110
|
+
continue;
|
|
111
|
+
await this.table.update(id, {
|
|
112
|
+
position,
|
|
113
|
+
updated_at: new Date().toISOString(),
|
|
114
|
+
last_updated_by: this.auth.mms_id,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return this.list(entity);
|
|
118
|
+
}
|
|
70
119
|
async update(statusId, input) {
|
|
71
120
|
if (await this.isWon(statusId)) {
|
|
72
121
|
throw refuseWon('The Won status is fixed and cannot be changed');
|
package/dist/users/dto.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare class GetUsersQueryDto implements Omit<GetUsersDtoType, 'active_i
|
|
|
26
26
|
readonly page: number;
|
|
27
27
|
readonly page_size: number;
|
|
28
28
|
readonly active_in?: boolean[] | null;
|
|
29
|
+
readonly manager_id?: string | null;
|
|
29
30
|
}
|
|
30
31
|
export declare class UpdateUserDto implements UpdateUserDtoType {
|
|
31
32
|
readonly name: string;
|
|
@@ -39,6 +40,7 @@ export declare class UpdateUserDto implements UpdateUserDtoType {
|
|
|
39
40
|
readonly job_title?: string | null;
|
|
40
41
|
readonly manager_id?: string | null;
|
|
41
42
|
readonly secondary_manager_id?: string | null;
|
|
43
|
+
readonly working_days?: number[];
|
|
42
44
|
readonly social_facebook?: string | null;
|
|
43
45
|
readonly social_linkedin?: string | null;
|
|
44
46
|
readonly social_twitter?: string | null;
|